@@ -163,18 +163,198 @@ public class GitHubCopilotViewModel: ObservableObject {
163163CopilotModelManager . updateLLMs ( models)
164164}
165165} catch let error asGitHubCopilotError {
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) :
169168waitForSignIn ( )
170169return
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 {
174184toast ( 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
179359public func broadcastStatusChange( ) {
180360DistributedNotificationCenter . default ( ) . post (