@@ -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