Skip to content

Commit 7ae30a5

Browse files
kody-wclaude
andcommitted
fix(macos): auto-trigger Copilot device-code reauth on 401
When the gateway reports a Copilot auth failure (401/403), the chat panel now starts the device-code flow inline and posts the user code into the chat instead of showing a raw error bubble that tells the user to run a CLI command. - ChatViewModel: detect Copilot auth errors, invoke onAuthRequired, latch to avoid retry storms; expose authFlowFinished() to reset. - ChatWindowManager: wire onAuthRequired -> handleReauth() in init. - copilot-token.ts: drop "Run 'openrappter onboard'" tail from the error message; UI handles it now. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1bf9c6e commit 7ae30a5

3 files changed

Lines changed: 48 additions & 3 deletions

File tree

macos/Sources/OpenRappterBar/ViewModels/ChatViewModel.swift

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ public final class ChatViewModel {
1717
private var rpcClient: RpcClient?
1818
private var sessionStore: SessionStore?
1919

20+
/// Called when the gateway reports a GitHub/Copilot auth failure. The host
21+
/// should kick off the device-code flow inline (no manual button click).
22+
public var onAuthRequired: (() -> Void)?
23+
24+
/// Guard so we only auto-trigger reauth once per failing burst, not on every
25+
/// retry until the device-code flow completes.
26+
private var isAutoReauthing: Bool = false
27+
2028
// MARK: - Computed
2129

2230
public var canSend: Bool {
@@ -132,16 +140,45 @@ public final class ChatViewModel {
132140
case .error:
133141
let errorMsg = payload.errorMessage ?? "Unknown error"
134142
chatState = .error(errorMsg)
143+
streamingText = ""
144+
145+
if isCopilotAuthError(errorMsg), let trigger = onAuthRequired {
146+
// Inline auth flow: skip the raw error bubble, show a friendly
147+
// status line, and kick off the device-code flow ourselves.
148+
if !isAutoReauthing {
149+
isAutoReauthing = true
150+
addSystemMessage("🔑 GitHub Copilot needs re-authentication — starting sign-in…")
151+
trigger()
152+
}
153+
return
154+
}
155+
135156
let msg = ChatMessage(
136157
role: .error,
137158
content: "Agent error: \(errorMsg)",
138159
sessionKey: payload.sessionKey
139160
)
140161
messages.append(msg)
141-
streamingText = ""
142162
}
143163
}
144164

165+
/// Reset the auto-reauth latch once the host knows the device-code flow has
166+
/// resolved (success or failure), so a later failure can trigger it again.
167+
public func authFlowFinished() {
168+
isAutoReauthing = false
169+
}
170+
171+
/// True for the gateway's Copilot 401/403 errors. Matches the exact phrase
172+
/// from `resolveCopilotApiToken` plus a generic fallback for related cases.
173+
private func isCopilotAuthError(_ text: String) -> Bool {
174+
let lowered = text.lowercased()
175+
if lowered.contains("copilot api access") { return true }
176+
if lowered.contains("copilot") && (lowered.contains("401") || lowered.contains("403")) {
177+
return true
178+
}
179+
return false
180+
}
181+
145182
// MARK: - Session Switching
146183

147184
public func switchToSession(sessionKey: String) {

macos/Sources/OpenRappterBar/Views/Chat/ChatWindowManager.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,13 @@ public final class ChatWindowManager {
3838
public init(viewModel: AppViewModel, settingsViewModel: SettingsViewModel) {
3939
self.viewModel = viewModel
4040
self.settingsViewModel = settingsViewModel
41+
42+
// Auto-trigger the device-code flow whenever the gateway reports a
43+
// Copilot auth failure — no manual button click required.
44+
viewModel.chatViewModel.onAuthRequired = { [weak self] in
45+
guard let self else { return }
46+
self.handleReauth()
47+
}
4148
}
4249

4350
/// Re-auth handler: starts device code flow, shows code in chat, restarts gateway
@@ -72,6 +79,8 @@ public final class ChatWindowManager {
7279
} else if let error = auth.error {
7380
viewModel.chatViewModel.addSystemMessage("❌ Auth failed: \(error)")
7481
}
82+
83+
viewModel.chatViewModel.authFlowFinished()
7584
}
7685
}
7786

typescript/src/providers/copilot-token.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,7 @@ export async function resolveCopilotApiToken(params: {
170170
if (res.status === 404 || res.status === 401 || res.status === 403) {
171171
throw new Error(
172172
`GitHub token does not have Copilot API access (HTTP ${res.status}). ` +
173-
`The token may be from the gh CLI which uses a different OAuth app. ` +
174-
`Run 'openrappter onboard' to authenticate with the Copilot device code flow.`
173+
`Sign in with a GitHub account that has Copilot enabled.`
175174
);
176175
}
177176
throw new Error(`Copilot token exchange failed: HTTP ${res.status}${body ? ` — ${body}` : ''}`);

0 commit comments

Comments
 (0)