Skip to content

Commit e7fd64d

Browse files
committed
Pre-release 0.38.129
1 parent d1f7de3 commit e7fd64d

28 files changed

+820
-334
lines changed

Core/Sources/ChatService/ChatService.swift

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,6 @@ public final class ChatService: ChatServiceType, ObservableObject {
9595

9696
subscribeToNotifications()
9797
subscribeToConversationContextRequest()
98-
subscribeToWatchedFilesHandler()
9998
subscribeToClientToolInvokeEvent()
10099
subscribeToClientToolConfirmationEvent()
101100
}
@@ -143,13 +142,6 @@ public final class ChatService: ChatServiceType, ObservableObject {
143142
}
144143
}).store(in: &cancellables)
145144
}
146-
147-
private func subscribeToWatchedFilesHandler() {
148-
self.watchedFilesHandler.onWatchedFiles.sink(receiveValue: { [weak self] (request, completion) in
149-
guard let self, request.params!.workspaceFolder.uri != "/" else { return }
150-
self.startFileChangeWatcher()
151-
}).store(in: &cancellables)
152-
}
153145

154146
private func subscribeToClientToolConfirmationEvent() {
155147
ClientToolHandlerImpl.shared.onClientToolConfirmationEvent.sink(receiveValue: { [weak self] (request, completion) in
@@ -1042,26 +1034,6 @@ extension ChatService {
10421034
func fetchAllChatMessagesFromStorage() -> [ChatMessage] {
10431035
return ChatMessageStore.getAll(by: self.chatTabInfo.id, metadata: .init(workspacePath: self.chatTabInfo.workspacePath, username: self.chatTabInfo.username))
10441036
}
1045-
1046-
/// for file change watcher
1047-
func startFileChangeWatcher() {
1048-
Task { [weak self] in
1049-
guard let self else { return }
1050-
let workspaceURL = URL(fileURLWithPath: self.chatTabInfo.workspacePath)
1051-
let projectURL = WorkspaceXcodeWindowInspector.extractProjectURL(workspaceURL: workspaceURL, documentURL: nil) ?? workspaceURL
1052-
await FileChangeWatcherServicePool.shared.watch(
1053-
for: workspaceURL
1054-
) { fileEvents in
1055-
Task { [weak self] in
1056-
guard let self else { return }
1057-
try? await self.conversationProvider?.notifyDidChangeWatchedFiles(
1058-
.init(workspaceUri: projectURL.path, changes: fileEvents),
1059-
workspace: .init(workspaceURL: workspaceURL, projectURL: projectURL)
1060-
)
1061-
}
1062-
}
1063-
}
1064-
}
10651037
}
10661038

10671039
func replaceFirstWord(in content: String, from oldWord: String, to newWord: String) -> String {

Core/Sources/ConversationTab/ChatPanel.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -499,7 +499,7 @@ struct ChatPanelInputArea: View {
499499
var focusedField: FocusState<Chat.State.Field?>.Binding
500500
@State var cancellable = Set<AnyCancellable>()
501501
@State private var isFilePickerPresented = false
502-
@State private var allFiles: [FileReference] = []
502+
@State private var allFiles: [FileReference]? = nil
503503
@State private var filteredTemplates: [ChatTemplate] = []
504504
@State private var filteredAgent: [ChatAgent] = []
505505
@State private var showingTemplates = false
@@ -528,7 +528,7 @@ struct ChatPanelInputArea: View {
528528
}
529529
)
530530
.onAppear() {
531-
allFiles = ContextUtils.getFilesInActiveWorkspace(workspaceURL: chat.workspaceURL)
531+
allFiles = ContextUtils.getFilesFromWorkspaceIndex(workspaceURL: chat.workspaceURL)
532532
}
533533
}
534534

Core/Sources/ConversationTab/ContextUtils.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import SystemUtils
77

88
public struct ContextUtils {
99

10+
public static func getFilesFromWorkspaceIndex(workspaceURL: URL?) -> [FileReference]? {
11+
guard let workspaceURL = workspaceURL else { return [] }
12+
return WorkspaceFileIndex.shared.getFiles(for: workspaceURL)
13+
}
14+
1015
public static func getFilesInActiveWorkspace(workspaceURL: URL?) -> [FileReference] {
1116
if let workspaceURL = workspaceURL, let info = WorkspaceFile.getWorkspaceInfo(workspaceURL: workspaceURL) {
1217
return WorkspaceFile.getFilesInActiveWorkspace(workspaceURL: info.workspaceURL, workspaceRootURL: info.projectURL)

Core/Sources/ConversationTab/FilePicker.swift

Lines changed: 25 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import SwiftUI
55
import SystemUtils
66

77
public struct FilePicker: View {
8-
@Binding var allFiles: [FileReference]
8+
@Binding var allFiles: [FileReference]?
99
let workspaceURL: URL?
1010
var onSubmit: (_ file: FileReference) -> Void
1111
var onExit: () -> Void
@@ -14,20 +14,21 @@ public struct FilePicker: View {
1414
@State private var selectedId: Int = 0
1515
@State private var localMonitor: Any? = nil
1616

17-
private var filteredFiles: [FileReference] {
17+
private var filteredFiles: [FileReference]? {
1818
if searchText.isEmpty {
1919
return allFiles
2020
}
2121

22-
return allFiles.filter { doc in
22+
return allFiles?.filter { doc in
2323
(doc.fileName ?? doc.url.lastPathComponent) .localizedCaseInsensitiveContains(searchText)
2424
}
2525
}
2626

2727
private static let defaultEmptyStateText = "No results found."
28+
private static let isIndexingStateText = "Indexing files, try later..."
2829

2930
private var emptyStateAttributedString: AttributedString? {
30-
var message = FilePicker.defaultEmptyStateText
31+
var message = allFiles == nil ? FilePicker.isIndexingStateText : FilePicker.defaultEmptyStateText
3132
if let workspaceURL = workspaceURL {
3233
let status = FileUtils.checkFileReadability(at: workspaceURL.path)
3334
if let errorMessage = status.errorMessage(using: ContextUtils.workspaceReadabilityErrorMessageProvider) {
@@ -89,25 +90,25 @@ public struct FilePicker: View {
8990
ScrollViewReader { proxy in
9091
ScrollView {
9192
LazyVStack(alignment: .leading, spacing: 4) {
92-
ForEach(Array(filteredFiles.enumerated()), id: \.element) { index, doc in
93-
FileRowView(doc: doc, id: index, selectedId: $selectedId)
94-
.contentShape(Rectangle())
95-
.onTapGesture {
96-
onSubmit(doc)
97-
selectedId = index
98-
isSearchBarFocused = true
99-
}
100-
.id(index)
101-
}
102-
103-
if filteredFiles.isEmpty {
93+
if allFiles == nil || filteredFiles?.isEmpty == true {
10494
emptyStateView
10595
.foregroundColor(.secondary)
10696
.padding(.leading, 4)
10797
.padding(.vertical, 4)
98+
} else {
99+
ForEach(Array((filteredFiles ?? []).enumerated()), id: \.element) { index, doc in
100+
FileRowView(doc: doc, id: index, selectedId: $selectedId)
101+
.contentShape(Rectangle())
102+
.onTapGesture {
103+
onSubmit(doc)
104+
selectedId = index
105+
isSearchBarFocused = true
106+
}
107+
.id(index)
108+
}
108109
}
109110
}
110-
.id(filteredFiles.hashValue)
111+
.id(filteredFiles?.hashValue)
111112
}
112113
.frame(maxHeight: 200)
113114
.padding(.horizontal, 4)
@@ -158,16 +159,14 @@ public struct FilePicker: View {
158159
}
159160

160161
private func moveSelection(up: Bool, proxy: ScrollViewProxy) {
161-
let files = filteredFiles
162-
guard !files.isEmpty else { return }
162+
guard let files = filteredFiles, !files.isEmpty else { return }
163163
let nextId = selectedId + (up ? -1 : 1)
164164
selectedId = max(0, min(nextId, files.count - 1))
165165
proxy.scrollTo(selectedId, anchor: .bottom)
166166
}
167167

168168
private func handleEnter() {
169-
let files = filteredFiles
170-
guard !files.isEmpty && selectedId < files.count else { return }
169+
guard let files = filteredFiles, !files.isEmpty && selectedId < files.count else { return }
171170
onSubmit(files[selectedId])
172171
}
173172
}
@@ -192,9 +191,13 @@ struct FileRowView: View {
192191
Text(doc.fileName ?? doc.url.lastPathComponent)
193192
.font(.body)
194193
.hoverPrimaryForeground(isHovered: selectedId == id)
194+
.lineLimit(1)
195+
.truncationMode(.middle)
195196
Text(doc.relativePath ?? doc.url.path)
196197
.font(.caption)
197198
.foregroundColor(.secondary)
199+
.lineLimit(1)
200+
.truncationMode(.middle)
198201
}
199202

200203
Spacer()
@@ -206,7 +209,7 @@ struct FileRowView: View {
206209
.onHover(perform: { hovering in
207210
isHovered = hovering
208211
})
209-
.transition(.move(edge: .bottom))
212+
.help(doc.relativePath ?? doc.url.path)
210213
}
211214
}
212215
}

Core/Sources/GitHubCopilotViewModel/GitHubCopilotViewModel.swift

Lines changed: 184 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,18 +163,198 @@ public class GitHubCopilotViewModel: ObservableObject {
163163
CopilotModelManager.updateLLMs(models)
164164
}
165165
} catch let error as GitHubCopilotError {
166-
if case .languageServerError(.timeout) = error {
167-
// TODO figure out how to extend the default timeout on a Chime LSP request
168-
// Until then, reissue request
166+
switch error {
167+
case .languageServerError(.timeout):
169168
waitForSignIn()
170169
return
170+
case .languageServerError(
171+
.serverError(
172+
code: CLSErrorCode.deviceFlowFailed.rawValue,
173+
message: _,
174+
data: _
175+
)
176+
):
177+
await showSignInFailedAlert(error: error)
178+
waitingForSignIn = false
179+
return
180+
default:
181+
throw error
171182
}
172-
throw error
173183
} catch {
174184
toast(error.localizedDescription, .error)
175185
}
176186
}
177187
}
188+
189+
private func extractSigninErrorMessage(error: GitHubCopilotError) -> String {
190+
let errorDescription = error.localizedDescription
191+
192+
// Handle specific EACCES permission denied errors
193+
if errorDescription.contains("EACCES") {
194+
// Look for paths wrapped in single quotes
195+
let pattern = "'([^']+)'"
196+
if let regex = try? NSRegularExpression(pattern: pattern, options: []) {
197+
let range = NSRange(location: 0, length: errorDescription.utf16.count)
198+
if let match = regex.firstMatch(in: errorDescription, options: [], range: range) {
199+
let pathRange = Range(match.range(at: 1), in: errorDescription)!
200+
let path = String(errorDescription[pathRange])
201+
return path
202+
}
203+
}
204+
}
205+
206+
return errorDescription
207+
}
208+
209+
private func getSigninErrorTitle(error: GitHubCopilotError) -> String {
210+
let errorDescription = error.localizedDescription
211+
212+
if errorDescription.contains("EACCES") {
213+
return "Can't sign you in. The app couldn't create or access files in"
214+
}
215+
216+
return "Error details:"
217+
}
218+
219+
private var accessPermissionCommands: String {
220+
"""
221+
sudo mkdir -p ~/.config/github-copilot
222+
sudo chown -R $(whoami):staff ~/.config
223+
chmod -N ~/.config ~/.config/github-copilot
224+
"""
225+
}
226+
227+
private var containerBackgroundColor: CGColor {
228+
let isDarkMode = NSApp.effectiveAppearance.name == .darkAqua
229+
return isDarkMode
230+
? NSColor.black.withAlphaComponent(0.85).cgColor
231+
: NSColor.white.withAlphaComponent(0.85).cgColor
232+
}
233+
234+
// MARK: - Alert Building Functions
235+
236+
private func showSignInFailedAlert(error: GitHubCopilotError) async {
237+
let alert = NSAlert()
238+
alert.messageText = "GitHub Copilot Sign-in Failed"
239+
alert.alertStyle = .critical
240+
241+
let accessoryView = createAlertAccessoryView(error: error)
242+
alert.accessoryView = accessoryView
243+
alert.addButton(withTitle: "Copy Commands")
244+
alert.addButton(withTitle: "Cancel")
245+
246+
let response = await MainActor.run {
247+
alert.runModal()
248+
}
249+
250+
if response == .alertFirstButtonReturn {
251+
copyCommandsToClipboard()
252+
}
253+
}
254+
255+
private func createAlertAccessoryView(error: GitHubCopilotError) -> NSView {
256+
let accessoryView = NSView(frame: NSRect(x: 0, y: 0, width: 400, height: 142))
257+
258+
let detailsHeader = createDetailsHeader(error: error)
259+
accessoryView.addSubview(detailsHeader)
260+
261+
let errorContainer = createErrorContainer(error: error)
262+
accessoryView.addSubview(errorContainer)
263+
264+
let terminalHeader = createTerminalHeader()
265+
accessoryView.addSubview(terminalHeader)
266+
267+
let commandsContainer = createCommandsContainer()
268+
accessoryView.addSubview(commandsContainer)
269+
270+
return accessoryView
271+
}
272+
273+
private func createDetailsHeader(error: GitHubCopilotError) -> NSView {
274+
let detailsHeader = NSView(frame: NSRect(x: 16, y: 122, width: 368, height: 20))
275+
276+
let warningIcon = NSImageView(frame: NSRect(x: 0, y: 4, width: 16, height: 16))
277+
warningIcon.image = NSImage(systemSymbolName: "exclamationmark.triangle.fill", accessibilityDescription: "Warning")
278+
warningIcon.contentTintColor = NSColor.systemOrange
279+
detailsHeader.addSubview(warningIcon)
280+
281+
let detailsLabel = NSTextField(wrappingLabelWithString: getSigninErrorTitle(error: error))
282+
detailsLabel.frame = NSRect(x: 20, y: 0, width: 346, height: 20)
283+
detailsLabel.font = NSFont.systemFont(ofSize: 12, weight: .regular)
284+
detailsLabel.textColor = NSColor.labelColor
285+
detailsHeader.addSubview(detailsLabel)
286+
287+
return detailsHeader
288+
}
289+
290+
private func createErrorContainer(error: GitHubCopilotError) -> NSView {
291+
let errorContainer = NSView(frame: NSRect(x: 16, y: 96, width: 368, height: 22))
292+
errorContainer.wantsLayer = true
293+
errorContainer.layer?.backgroundColor = containerBackgroundColor
294+
errorContainer.layer?.borderColor = NSColor.separatorColor.cgColor
295+
errorContainer.layer?.borderWidth = 1
296+
errorContainer.layer?.cornerRadius = 6
297+
298+
let errorMessage = NSTextField(wrappingLabelWithString: extractSigninErrorMessage(error: error))
299+
errorMessage.frame = NSRect(x: 8, y: 4, width: 368, height: 14)
300+
errorMessage.font = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular)
301+
errorMessage.textColor = NSColor.labelColor
302+
errorMessage.backgroundColor = .clear
303+
errorMessage.isBordered = false
304+
errorMessage.isEditable = false
305+
errorMessage.drawsBackground = false
306+
errorMessage.usesSingleLineMode = true
307+
errorContainer.addSubview(errorMessage)
308+
309+
return errorContainer
310+
}
311+
312+
private func createTerminalHeader() -> NSView {
313+
let terminalHeader = NSView(frame: NSRect(x: 16, y: 66, width: 368, height: 20))
314+
315+
let toolIcon = NSImageView(frame: NSRect(x: 0, y: 4, width: 16, height: 16))
316+
toolIcon.image = NSImage(systemSymbolName: "terminal.fill", accessibilityDescription: "Terminal")
317+
toolIcon.contentTintColor = NSColor.secondaryLabelColor
318+
terminalHeader.addSubview(toolIcon)
319+
320+
let terminalLabel = NSTextField(wrappingLabelWithString: "Copy and run the commands below in Terminal, then retry.")
321+
terminalLabel.frame = NSRect(x: 20, y: 0, width: 346, height: 20)
322+
terminalLabel.font = NSFont.systemFont(ofSize: 12, weight: .regular)
323+
terminalLabel.textColor = NSColor.labelColor
324+
terminalHeader.addSubview(terminalLabel)
325+
326+
return terminalHeader
327+
}
328+
329+
private func createCommandsContainer() -> NSView {
330+
let commandsContainer = NSView(frame: NSRect(x: 16, y: 4, width: 368, height: 58))
331+
commandsContainer.wantsLayer = true
332+
commandsContainer.layer?.backgroundColor = containerBackgroundColor
333+
commandsContainer.layer?.borderColor = NSColor.separatorColor.cgColor
334+
commandsContainer.layer?.borderWidth = 1
335+
commandsContainer.layer?.cornerRadius = 6
336+
337+
let commandsText = NSTextField(wrappingLabelWithString: accessPermissionCommands)
338+
commandsText.frame = NSRect(x: 8, y: 8, width: 344, height: 42)
339+
commandsText.font = NSFont.monospacedSystemFont(ofSize: 11, weight: .regular)
340+
commandsText.textColor = NSColor.labelColor
341+
commandsText.backgroundColor = .clear
342+
commandsText.isBordered = false
343+
commandsText.isEditable = false
344+
commandsText.isSelectable = true
345+
commandsText.drawsBackground = false
346+
commandsContainer.addSubview(commandsText)
347+
348+
return commandsContainer
349+
}
350+
351+
private func copyCommandsToClipboard() {
352+
NSPasteboard.general.clearContents()
353+
NSPasteboard.general.setString(
354+
self.accessPermissionCommands.replacingOccurrences(of: "\n", with: " && "),
355+
forType: .string
356+
)
357+
}
178358

179359
public func broadcastStatusChange() {
180360
DistributedNotificationCenter.default().post(

0 commit comments

Comments
 (0)