fix(auth): mount login dialog input for paste-code fallback URL

Paste-code OAuth providers (Codex, Anthropic, Gemini CLI, GitLab Duo,
Antigravity, Devin) need the user to paste the fallback redirect URL
when the loopback callback cannot complete (headless/remote/Windows).
The login dialog took focus and cleared the editor but only rendered the
auth URL plus a tip pointing at `/login <redirect URL>` — a command only
reachable through the now-hidden, unfocused editor. The dialog never
mounted an Input, so a pasted URL was silently dropped and login stalled.

Route onManualCodeInput through the focused dialog's showManualInput so
the paste lands in a visible, submittable field. Make showManualInput
idempotent so the OAuth callback retry loop reuses the mounted input
instead of stacking duplicate prompts.

Fixes #5339
This commit is contained in:
roboomp
2026-07-14 18:44:01 +00:00
parent edd959a381
commit 4882c9b011
6 changed files with 82 additions and 15 deletions
@@ -82,7 +82,7 @@ import { UserMessageSelectorComponent } from "../components/user-message-selecto
import type { SessionObserverRegistry } from "../session-observer-registry";
import { buildCopyTargets } from "../utils/copy-targets";
const MANUAL_LOGIN_TIP = "Tip: You can complete pairing with /login <redirect URL>.";
const MANUAL_LOGIN_PROMPT = "Paste the authorization code (or full redirect URL), then press Enter:";
export class SelectorController {
constructor(private ctx: InteractiveModeContext) {}
@@ -1265,7 +1265,6 @@ export class SelectorController {
*/
async #handleOAuthLogin(providerId: string): Promise<boolean> {
this.ctx.showStatus(`Logging in to ${providerId}…`);
const manualInput = this.ctx.oauthManualInput;
const useManualInput = PASTE_CODE_LOGIN_PROVIDERS.has(providerId);
let restored = false;
const restoreEditor = () => {
@@ -1293,16 +1292,19 @@ export class SelectorController {
// The dialog renders the full URL (SSH-safe copy target) and
// opens the browser best-effort.
dialog.showAuth(info.url, info.instructions, info.launchUrl);
if (useManualInput) {
dialog.showProgress(MANUAL_LOGIN_TIP);
}
},
onPrompt: (prompt: { message: string; placeholder?: string }) =>
dialog.showPrompt(prompt.message, prompt.placeholder),
onProgress: (message: string) => {
dialog.showProgress(message);
},
onManualCodeInput: useManualInput ? () => manualInput.waitForInput(providerId) : undefined,
// Paste-code providers (e.g. Codex) may need the user to paste the
// fallback redirect URL when the loopback callback can't complete
// (headless/remote/Windows). Mount a focused input in the dialog so
// the paste lands somewhere the OAuth flow consumes — the hidden
// editor's `/login <url>` path is unreachable while the dialog holds
// focus (#5339).
onManualCodeInput: useManualInput ? () => dialog.showManualInput(MANUAL_LOGIN_PROMPT) : undefined,
});
this.ctx.session.modelRegistry.refreshInBackground();
const block = new TranscriptBlock();
@@ -1321,9 +1323,6 @@ export class SelectorController {
this.ctx.showError(`Login failed: ${error instanceof Error ? error.message : String(error)}`);
return false;
} finally {
if (useManualInput) {
manualInput.clear(`Manual OAuth input cleared for ${providerId}`);
}
restoreEditor();
}
}