diff --git a/AGENTS.md b/AGENTS.md index 144d2c60a..065f2541f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -7,6 +7,7 @@ - No fluff or cheerful filler text (e.g., "Thanks @user" not "Thanks so much @user!") - Technical prose only, be direct - When the user asks a question, answer it first before making edits or running implementation commands. +- When responding to user feedback or an analysis, explicitly say whether you agree or disagree before saying what you changed. ## Code Quality diff --git a/packages/ai/CHANGELOG.md b/packages/ai/CHANGELOG.md index 9802972f9..768a0525e 100644 --- a/packages/ai/CHANGELOG.md +++ b/packages/ai/CHANGELOG.md @@ -2,8 +2,13 @@ ## [Unreleased] +### Breaking Changes + +- Changed `OAuthLoginCallbacks` to require `onDeviceCode` and `onSelect`, so OAuth providers can rely on pi supplying device-code and selection UI callbacks. + ### Fixed +- Fixed GitHub Copilot OAuth login to rely on the required device-code callback without a runtime callback availability guard. - Fixed Amazon Bedrock Claude requests to send the model output token cap by default, matching Anthropic requests and avoiding Bedrock's 4096-token default truncation ([#4848](https://github.com/earendil-works/pi/issues/4848)). ## [0.75.4] - 2026-05-20 diff --git a/packages/ai/src/cli.ts b/packages/ai/src/cli.ts index 442ee8ea6..21699dbdb 100644 --- a/packages/ai/src/cli.ts +++ b/packages/ai/src/cli.ts @@ -50,6 +50,15 @@ async function login(providerId: OAuthProviderId): Promise { onPrompt: async (p) => { return await promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`); }, + onSelect: async (p) => { + console.log(`\n${p.message}`); + for (let i = 0; i < p.options.length; i++) { + console.log(` ${i + 1}. ${p.options[i].label}`); + } + const choice = await promptFn(`Enter number (1-${p.options.length}):`); + const index = parseInt(choice, 10) - 1; + return p.options[index]?.id; + }, onProgress: (msg) => console.log(msg), }); diff --git a/packages/ai/src/utils/oauth/github-copilot.ts b/packages/ai/src/utils/oauth/github-copilot.ts index c4ce36358..d5adb58a7 100644 --- a/packages/ai/src/utils/oauth/github-copilot.ts +++ b/packages/ai/src/utils/oauth/github-copilot.ts @@ -319,10 +319,6 @@ export const githubCopilotOAuthProvider: OAuthProviderInterface = { name: "GitHub Copilot", async login(callbacks: OAuthLoginCallbacks): Promise { - if (!callbacks.onDeviceCode) { - throw new Error("GitHub Copilot OAuth requires a device code callback"); - } - return loginGitHubCopilot({ onDeviceCode: callbacks.onDeviceCode, onPrompt: callbacks.onPrompt, diff --git a/packages/ai/src/utils/oauth/types.ts b/packages/ai/src/utils/oauth/types.ts index 3220dcf9d..008be405e 100644 --- a/packages/ai/src/utils/oauth/types.ts +++ b/packages/ai/src/utils/oauth/types.ts @@ -42,12 +42,12 @@ export type OAuthSelectPrompt = { export interface OAuthLoginCallbacks { onAuth: (info: OAuthAuthInfo) => void; - onDeviceCode?: (info: OAuthDeviceCodeInfo) => void; + onDeviceCode: (info: OAuthDeviceCodeInfo) => 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; + onSelect: (prompt: OAuthSelectPrompt) => Promise; signal?: AbortSignal; } diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 304bc4365..33cf2967a 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -13,6 +13,7 @@ ### Fixed - Fixed exported session HTML to escape quote characters in attribute values ([#4832](https://github.com/earendil-works/pi/issues/4832)). +- Fixed GitHub Copilot device-code login to keep opening the verification URL in browser-capable environments while ignoring browser launch failures for headless use. - Fixed git package installs to reconcile existing checkouts to the requested ref and update package settings without losing filters ([#4870](https://github.com/earendil-works/pi/issues/4870)). - Published a 0.74.2 rescue release that tells Node 20 users to upgrade Node before updating to newer Pi versions ([#4876](https://github.com/earendil-works/pi/issues/4876)). - Fixed final bash tool cards to avoid rendering duplicate full-output truncation paths ([#4819](https://github.com/earendil-works/pi/issues/4819)). diff --git a/packages/coding-agent/docs/custom-provider.md b/packages/coding-agent/docs/custom-provider.md index 09eb6e6e3..e7fecc2d2 100644 --- a/packages/coding-agent/docs/custom-provider.md +++ b/packages/coding-agent/docs/custom-provider.md @@ -263,17 +263,28 @@ pi.registerProvider("corporate-ai", { name: "Corporate AI (SSO)", async login(callbacks: OAuthLoginCallbacks): Promise { - // Option 1: Browser-based OAuth - callbacks.onAuth({ url: "https://sso.corp.com/authorize?..." }); - - // Option 2: Device code flow - callbacks.onDeviceCode({ - userCode: "ABCD-1234", - verificationUri: "https://sso.corp.com/device" + const method = await callbacks.onSelect({ + message: "Select login method:", + options: [ + { id: "browser", label: "Browser OAuth" }, + { id: "device", label: "Device code" } + ] }); + if (!method) throw new Error("Login cancelled"); - // Option 3: Prompt for token/code - const code = await callbacks.onPrompt({ message: "Enter SSO code:" }); + let code: string; + if (method === "device") { + callbacks.onDeviceCode({ + userCode: "ABCD-1234", + verificationUri: "https://sso.corp.com/device", + intervalSeconds: 5, + expiresInSeconds: 900 + }); + code = await pollDeviceCodeUntilComplete(); + } else { + callbacks.onAuth({ url: "https://sso.corp.com/authorize?..." }); + code = await callbacks.onPrompt({ message: "Enter SSO code:" }); + } // Exchange for tokens (your implementation) const tokens = await exchangeCodeForTokens(code); @@ -322,10 +333,21 @@ interface OAuthLoginCallbacks { onAuth(params: { url: string }): void; // Show device code (for device authorization flow) - onDeviceCode(params: { userCode: string; verificationUri: string }): void; + onDeviceCode(params: { + userCode: string; + verificationUri: string; + intervalSeconds?: number; + expiresInSeconds?: number; + }): void; // Prompt user for input (for manual token entry) onPrompt(params: { message: string }): Promise; + + // Show an interactive selector, e.g. to choose browser OAuth vs device code + onSelect(params: { + message: string; + options: { id: string; label: string }[]; + }): Promise; } ``` 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 5a2c365af..ee3f45cbf 100644 --- a/packages/coding-agent/src/modes/interactive/components/login-dialog.ts +++ b/packages/coding-agent/src/modes/interactive/components/login-dialog.ts @@ -122,13 +122,17 @@ export class LoginDialogComponent extends Container implements Focusable { 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.openUrl(info.verificationUri); this.tui.requestRender(); } private openUrl(url: string): void { const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"; - exec(`${openCmd} "${url}"`); + try { + exec(`${openCmd} "${url}"`, () => {}); + } catch { + // Ignore browser launch failures. The URL remains visible for manual opening/copying. + } } /**