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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy