mirror of
https://github.com/getcompanion-ai/co-mono.git
synced 2026-04-18 10:03:27 +00:00
Improve OAuth login UI with consistent dialog component
- Add LoginDialogComponent with proper borders (top/bottom DynamicBorder) - Refactor all OAuth providers to use racing approach (browser callback vs manual paste) - Add onEscape handler to Input component for cancellation - Add abortable sleep for GitHub Copilot polling (instant cancel on Escape) - Show OS-specific click hint (Cmd+click on macOS, Ctrl+click elsewhere) - Clear content between login phases (fixes GitHub Copilot two-phase flow) - Use InteractiveMode's showStatus/showError for result messages - Reorder providers: Anthropic, ChatGPT, GitHub Copilot, Gemini CLI, Antigravity
This commit is contained in:
parent
05b9d55656
commit
9b12719ab1
9 changed files with 550 additions and 200 deletions
|
|
@ -0,0 +1,161 @@
|
|||
import { getOAuthProviders } from "@mariozechner/pi-ai";
|
||||
import { Container, getEditorKeybindings, Input, Spacer, Text, type TUI } from "@mariozechner/pi-tui";
|
||||
import { exec } from "child_process";
|
||||
import { theme } from "../theme/theme.js";
|
||||
import { DynamicBorder } from "./dynamic-border.js";
|
||||
|
||||
/**
|
||||
* Login dialog component - replaces editor during OAuth login flow
|
||||
*/
|
||||
export class LoginDialogComponent extends Container {
|
||||
private contentContainer: Container;
|
||||
private input: Input;
|
||||
private tui: TUI;
|
||||
private abortController = new AbortController();
|
||||
private inputResolver?: (value: string) => void;
|
||||
private 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;
|
||||
}
|
||||
|
||||
private 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));
|
||||
}
|
||||
|
||||
// Try to open browser
|
||||
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
||||
exec(`${openCmd} "${url}"`);
|
||||
|
||||
this.tui.requestRender();
|
||||
}
|
||||
|
||||
/**
|
||||
* Show input for manual code/URL entry (for callback server providers)
|
||||
*/
|
||||
showManualInput(prompt: string): Promise<string> {
|
||||
this.contentContainer.addChild(new Spacer(1));
|
||||
this.contentContainer.addChild(new Text(theme.fg("dim", prompt), 1, 0));
|
||||
this.contentContainer.addChild(this.input);
|
||||
this.contentContainer.addChild(new Text(theme.fg("dim", "(Escape to cancel)"), 1, 0));
|
||||
this.tui.requestRender();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.inputResolver = resolve;
|
||||
this.inputRejecter = reject;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by onPrompt callback - show prompt and wait for input
|
||||
*/
|
||||
showPrompt(message: string, placeholder?: string): Promise<string> {
|
||||
this.contentContainer.clear();
|
||||
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));
|
||||
}
|
||||
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();
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
this.inputResolver = resolve;
|
||||
this.inputRejecter = reject;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = getEditorKeybindings();
|
||||
|
||||
if (kb.matches(data, "selectCancel")) {
|
||||
this.cancel();
|
||||
return;
|
||||
}
|
||||
|
||||
// Pass to input
|
||||
this.input.handleInput(data);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue