feat(ai): add device code login callback and use for copilot

This commit is contained in:
Vegard Stikbakke
2026-05-20 09:30:58 +02:00
Unverified
parent 04e93af5c4
commit bf5ac0011e
11 changed files with 316 additions and 129 deletions
@@ -1,4 +1,4 @@
import { getOAuthProviders } from "@earendil-works/pi-ai/oauth";
import { getOAuthProviders, type OAuthDeviceCodeInfo } from "@earendil-works/pi-ai/oauth";
import { Container, type Focusable, getKeybindings, Input, Spacer, Text, type TUI } from "@earendil-works/pi-tui";
import { exec } from "child_process";
import { theme } from "../theme/theme.ts";
@@ -86,7 +86,7 @@ export class LoginDialogComponent extends Container implements Focusable {
/**
* Called by onAuth callback - show URL and optional instructions
*/
showAuth(url: string, instructions?: string): void {
showAuth(url: string, instructions?: string, options: { autoOpenBrowser?: boolean } = {}): void {
this.contentContainer.clear();
this.contentContainer.addChild(new Spacer(1));
const linkedUrl = `\x1b]8;;${url}\x07${url}\x1b]8;;\x07`;
@@ -101,11 +101,34 @@ export class LoginDialogComponent extends Container implements Focusable {
this.contentContainer.addChild(new Text(theme.fg("warning", instructions), 1, 0));
}
// Try to open browser
if (options.autoOpenBrowser ?? true) {
this.openUrl(url);
}
this.tui.requestRender();
}
/**
* Called by onDeviceCode callback - show URL and user code.
*/
showDeviceCode(info: OAuthDeviceCodeInfo): void {
this.contentContainer.clear();
this.contentContainer.addChild(new Spacer(1));
const linkedUrl = `\x1b]8;;${info.verificationUri}\x07${info.verificationUri}\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;;${info.verificationUri}\x07${clickHint}\x1b]8;;\x07`;
this.contentContainer.addChild(new Text(theme.fg("dim", hyperlink), 1, 0));
this.contentContainer.addChild(new Spacer(1));
this.contentContainer.addChild(new Text(theme.fg("warning", `Enter code: ${info.userCode}`), 1, 0));
// Do not open device-code URLs automatically. These flows need to work in headless environments.
this.tui.requestRender();
}
private openUrl(url: string): void {
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
exec(`${openCmd} "${url}"`);
this.tui.requestRender();
}
/**
@@ -4815,13 +4815,15 @@ export class InteractiveMode {
manualCodeReject = undefined;
}
});
} else if (providerId === "github-copilot") {
// GitHub Copilot polls after onAuth
dialog.showWaiting("Waiting for browser authentication...");
}
// For Anthropic: onPrompt is called immediately after
},
onDeviceCode: (info) => {
dialog.showDeviceCode(info);
dialog.showWaiting("Waiting for authentication...");
},
onPrompt: async (prompt: { message: string; placeholder?: string }) => {
return dialog.showPrompt(prompt.message, prompt.placeholder);
},