import { getOAuthProviders } from "@oh-my-pi/pi-ai/utils/oauth"; import { Container, getKeybindings, Input, Spacer, Text, type TUI } from "@oh-my-pi/pi-tui"; import { theme } from "../../modes/theme/theme"; import { openPath } from "../../utils/open"; import { DynamicBorder } from "./dynamic-border"; /** * Login dialog component - replaces editor during OAuth login flow */ export class LoginDialogComponent extends Container { #contentContainer: Container; #input: Input; #tui: TUI; #abortController = new AbortController(); #inputResolver?: (value: string) => void; #inputRejecter?: (error: Error) => void; constructor( tui: TUI, providerId: string, private onComplete: (success: boolean, message?: string) => void, ) { super(); this.#tui = tui; const providerInfo = getOAuthProviders().find(p => p.id === providerId); const providerName = providerInfo?.name || providerId; // Top border this.addChild(new DynamicBorder()); // Title this.addChild(new Text(theme.fg("warning", `Login to ${providerName}`), 1, 0)); // Dynamic content area this.#contentContainer = new Container(); this.addChild(this.#contentContainer); // Input (always present, used when needed) this.#input = new Input(); this.#input.onSubmit = () => { if (this.#inputResolver) { this.#inputResolver(this.#input.getValue()); this.#inputResolver = undefined; this.#inputRejecter = undefined; } }; this.#input.onEscape = () => { this.#cancel(); }; // Bottom border this.addChild(new DynamicBorder()); } get signal(): AbortSignal { return this.#abortController.signal; } #cancel(): void { this.#abortController.abort(); if (this.#inputRejecter) { this.#inputRejecter(new Error("Login cancelled")); this.#inputResolver = undefined; this.#inputRejecter = undefined; } this.onComplete(false, "Login cancelled"); } /** * Called by onAuth callback - show URL and optional instructions */ 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 clickHint = process.platform === "darwin" ? "Cmd+click to open" : "Ctrl+click to open"; const hyperlink = `\x1b]8;;${url}\x07${clickHint}\x1b]8;;\x07`; this.#contentContainer.addChild(new Text(theme.fg("dim", hyperlink), 1, 0)); if (instructions) { this.#contentContainer.addChild(new Spacer(1)); this.#contentContainer.addChild(new Text(theme.fg("warning", instructions), 1, 0)); } // Open browser (best-effort) openPath(url); this.#tui.requestRender(); } /** * Show input for manual code/URL entry (for callback server providers) */ showManualInput(prompt: string): Promise { this.#contentContainer.addChild(new Spacer(1)); this.#contentContainer.addChild(new Text(theme.fg("dim", prompt), 1, 0)); if (!this.#contentContainer.children.includes(this.#input)) { this.#contentContainer.addChild(this.#input); } this.#contentContainer.addChild(new Text(theme.fg("dim", "(Escape to cancel)"), 1, 0)); this.#tui.requestRender(); const { promise, resolve, reject } = Promise.withResolvers(); this.#inputResolver = resolve; this.#inputRejecter = reject; return promise; } /** * Called by onPrompt callback - show prompt and wait for input * Note: Does NOT clear content, appends to existing (preserves URL from showAuth) */ showPrompt(message: string, placeholder?: string): Promise { this.#contentContainer.addChild(new Spacer(1)); this.#contentContainer.addChild(new Text(theme.fg("text", message), 1, 0)); if (placeholder) { this.#contentContainer.addChild(new Text(theme.fg("dim", `e.g., ${placeholder}`), 1, 0)); } if (!this.#contentContainer.children.includes(this.#input)) { this.#contentContainer.addChild(this.#input); } this.#contentContainer.addChild(new Text(theme.fg("dim", "(Escape to cancel, Enter to submit)"), 1, 0)); this.#input.setValue(""); this.#tui.requestRender(); const { promise, resolve, reject } = Promise.withResolvers(); this.#inputResolver = resolve; this.#inputRejecter = reject; return promise; } /** * Show waiting message (for polling flows like GitHub Copilot) */ showWaiting(message: string): void { this.#contentContainer.addChild(new Spacer(1)); this.#contentContainer.addChild(new Text(theme.fg("dim", message), 1, 0)); this.#contentContainer.addChild(new Text(theme.fg("dim", "(Escape to cancel)"), 1, 0)); this.#tui.requestRender(); } /** * Called by onProgress callback */ showProgress(message: string): void { this.#contentContainer.addChild(new Text(theme.fg("dim", message), 1, 0)); this.#tui.requestRender(); } handleInput(data: string): void { const kb = getKeybindings(); if (kb.matches(data, "tui.select.cancel")) { this.#cancel(); return; } // Pass to input this.#input.handleInput(data); } }