@@ -163,18 +163,198 @@ public class GitHubCopilotViewModel: ObservableObject {
163
163
CopilotModelManager . updateLLMs ( models)
164
164
}
165
165
} 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) :
169
168
waitForSignIn ( )
170
169
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
171
182
}
172
- throw error
173
183
} catch {
174
184
toast ( error. localizedDescription, . error)
175
185
}
176
186
}
177
187
}
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
+ }
178
358
179
359
public func broadcastStatusChange( ) {
180
360
DistributedNotificationCenter . default ( ) . post (