From b5755fd27d115910249e8e29d48685569bfc8498 Mon Sep 17 00:00:00 2001 From: Armin Ronacher Date: Tue, 5 May 2026 13:16:56 +0200 Subject: [PATCH] feat(oauth): support interactive login selection (#4190) --- packages/ai/src/index.ts | 2 ++ packages/ai/src/utils/oauth/types.ts | 12 +++++++ .../interactive/components/login-dialog.ts | 3 +- .../src/modes/interactive/interactive-mode.ts | 31 +++++++++++++++++++ 4 files changed, 47 insertions(+), 1 deletion(-) diff --git a/packages/ai/src/index.ts b/packages/ai/src/index.ts index 9f996059..2502008a 100644 --- a/packages/ai/src/index.ts +++ b/packages/ai/src/index.ts @@ -34,6 +34,8 @@ export type { OAuthProviderId, OAuthProviderInfo, OAuthProviderInterface, + OAuthSelectOption, + OAuthSelectPrompt, } from "./utils/oauth/types.js"; export * from "./utils/overflow.js"; export * from "./utils/typebox-helpers.js"; diff --git a/packages/ai/src/utils/oauth/types.ts b/packages/ai/src/utils/oauth/types.ts index e3520342..03cbf101 100644 --- a/packages/ai/src/utils/oauth/types.ts +++ b/packages/ai/src/utils/oauth/types.ts @@ -23,11 +23,23 @@ export type OAuthAuthInfo = { instructions?: string; }; +export type OAuthSelectOption = { + id: string; + label: string; +}; + +export type OAuthSelectPrompt = { + message: string; + options: OAuthSelectOption[]; +}; + export interface OAuthLoginCallbacks { onAuth: (info: OAuthAuthInfo) => void; onPrompt: (prompt: OAuthPrompt) => Promise; onProgress?: (message: string) => void; onManualCodeInput?: () => Promise; + /** Show an interactive selector and return the selected option id, or undefined on cancel. */ + onSelect?: (prompt: OAuthSelectPrompt) => Promise; signal?: AbortSignal; } diff --git a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts index 98d2096b..f12bd417 100644 --- a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts +++ b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts @@ -87,7 +87,8 @@ export class LoginDialogComponent extends Container implements Focusable { showAuth(url: string, instructions?: string): void { this.contentContainer.clear(); this.contentContainer.addChild(new Spacer(1)); - this.contentContainer.addChild(new Text(theme.fg("accent", url), 1, 0)); + const linkedUrl = `\x1b]8;;${url}\x07${url}\x1b]8;;\x07`; + this.contentContainer.addChild(new Text(theme.fg("accent", linkedUrl), 1, 0)); const clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open"; const hyperlink = `\x1b]8;;${url}\x07${clickHint}\x1b]8;;\x07`; diff --git a/packages/coding-agent/src/modes/interactive/interactive-mode.ts b/packages/coding-agent/src/modes/interactive/interactive-mode.ts index 2fab6f56..05a01148 100644 --- a/packages/coding-agent/src/modes/interactive/interactive-mode.ts +++ b/packages/coding-agent/src/modes/interactive/interactive-mode.ts @@ -15,6 +15,7 @@ import { type Message, type Model, type OAuthProviderId, + type OAuthSelectPrompt, } from "@mariozechner/pi-ai"; import type { AutocompleteItem, @@ -4645,6 +4646,34 @@ export class InteractiveMode { } } + private showOAuthLoginSelect(dialog: LoginDialogComponent, prompt: OAuthSelectPrompt): Promise { + return new Promise((resolve) => { + const restoreDialog = () => { + this.editorContainer.clear(); + this.editorContainer.addChild(dialog); + this.ui.setFocus(dialog); + this.ui.requestRender(); + }; + const labels = prompt.options.map((option) => option.label); + const selector = new ExtensionSelectorComponent( + prompt.message, + labels, + (optionLabel) => { + restoreDialog(); + resolve(prompt.options.find((option) => option.label === optionLabel)?.id); + }, + () => { + restoreDialog(); + resolve(undefined); + }, + ); + this.editorContainer.clear(); + this.editorContainer.addChild(selector); + this.ui.setFocus(selector); + this.ui.requestRender(); + }); + } + private async showLoginDialog(providerId: string, providerName: string): Promise { const providerInfo = this.session.modelRegistry.authStorage .getOAuthProviders() @@ -4722,6 +4751,8 @@ export class InteractiveMode { dialog.showProgress(message); }, + onSelect: (prompt: OAuthSelectPrompt) => this.showOAuthLoginSelect(dialog, prompt), + onManualCodeInput: () => manualCodePromise, signal: dialog.signal,