diff --git a/webviews/src/agent-session/acpmux/App.tsx b/webviews/src/agent-session/acpmux/App.tsx index 263e99b9e255..013b513e15cd 100644 --- a/webviews/src/agent-session/acpmux/App.tsx +++ b/webviews/src/agent-session/acpmux/App.tsx @@ -541,7 +541,7 @@ import { InboxNoticeCard } from "./conversation/InboxNoticeCard"; import { mergeInboxNotices, type InboxMessage } from "./conversation/inboxNotice"; import { configureNativeSidebarPreviewVariant, configureSidebarPreviewVariant } from "./sidebarPreview"; import { cloudCredentialRecovery } from "./cloudCredentialRecovery"; -import { cloudAgentLogin } from "./cloudAgentLogin"; +import { cloudAgentLogin, localApiKeyLogin } from "./cloudAgentLogin"; import { handoffTurns } from "./cloudHandoffTurn"; import { configureTurnActionsPlacement } from "./turnActionsPlacement"; import { archiveRow, markUnreadRow } from "./header/archiveRow"; @@ -7618,7 +7618,13 @@ function AcpmuxPane() { ), // A Cloud chat's agent signs in on its own machine (Leo 2026-11-01: "yes those are // important"): its own login, never this Mac's. Its page opens as a link click does here. - cloudSignIn: cloudAgentLogin({ + // A chat here whose built-in Claude Code or Codex is not signed in offers Use an API key + // (Leo 2026-10-26: "if user picks a model they don't have api key, we offer them to add + // credits or add their own api key"). + ...((login: Parameters[0]) => ({ + cloudSignIn: cloudAgentLogin(login), + apiKeySignIn: localApiKeyLogin(login), + }))({ connected, machineID: cloudMachineID, sessionId: snapshot.sessionId, diff --git a/webviews/src/agent-session/acpmux/cloudAgentLogin.ts b/webviews/src/agent-session/acpmux/cloudAgentLogin.ts index 1275ae51d8bf..6004c9e2ed1a 100644 --- a/webviews/src/agent-session/acpmux/cloudAgentLogin.ts +++ b/webviews/src/agent-session/acpmux/cloudAgentLogin.ts @@ -144,10 +144,8 @@ export class LoginStepError extends Error { } } -/// The sign-in a Cloud chat's sign-in failure offers, or undefined where there is none: a chat -/// not connected, not on a Cloud machine, not Claude or Codex, or on an acpmux that does not -/// advertise `agent_login`. -export function cloudAgentLogin(options: { +/// What ``cloudAgentLogin`` and ``localApiKeyLogin`` take. +type LoginOptions = { connected: boolean; machineID: string | undefined; sessionId: string | undefined; @@ -157,11 +155,34 @@ export function cloudAgentLogin(options: { openURL: (url: string) => void; isCurrent: (sessionId: string) => boolean; currentClient: () => LoginClient | undefined; -}): CloudSignIn | undefined { - const { connected, machineID, sessionId, harness, supported, gesture, openURL, isCurrent, currentClient } = options; +}; + +/// The sign-in a Cloud chat's sign-in failure offers, or undefined where there is none: a chat +/// not connected, not on a Cloud machine, not Claude or Codex, or on an acpmux that does not +/// advertise `agent_login`. +export function cloudAgentLogin(options: LoginOptions): CloudSignIn | undefined { + return options.machineID ? agentLogin(options) : undefined; +} + +/// Use an API key for a chat on this Mac whose built-in Claude Code or Codex is not signed in (Leo +/// 2026-10-26: "if user picks a model they don't have api key, we offer them to add credits or add +/// their own api key"). acpmux saves the key where the agent's own CLI reads it (Claude's +/// `settings.json` `env.ANTHROPIC_API_KEY`, `codex login --with-api-key`) and restarts the chat on it. +/// Only the built-in `claude` and `codex` harnesses: a route (`claude-cr`, `claude-sr`) sets its own +/// key and endpoint, which a saved key would never reach. Undefined for a Cloud chat (its card is +/// ``cloudAgentLogin``'s). +export function localApiKeyLogin(options: LoginOptions): CloudSignIn | undefined { + const harness = options.harness?.split("/").pop(); + if (options.machineID || (harness !== "claude" && harness !== "codex")) return undefined; + return agentLogin(options); +} + +/// The agent's own login on the chat's machine, or undefined for a chat not connected, not Claude or +/// Codex, or on an acpmux that does not advertise `agent_login`. +function agentLogin(options: LoginOptions): CloudSignIn | undefined { + const { connected, sessionId, harness, supported, gesture, openURL, isCurrent, currentClient } = options; const agent = agentKey(harness?.split("/").pop()); - if (!connected || !machineID || !sessionId || !supported || (agent !== "claude" && agent !== "codex")) - return undefined; + if (!connected || !sessionId || !supported || (agent !== "claude" && agent !== "codex")) return undefined; const client = () => { const current = currentClient(); if (!current || !isCurrent(sessionId)) throw new LoginStepError("failed"); diff --git a/webviews/src/agent-session/acpmux/conversation/TurnRows.tsx b/webviews/src/agent-session/acpmux/conversation/TurnRows.tsx index 7200195a7ad7..2415f3960568 100644 --- a/webviews/src/agent-session/acpmux/conversation/TurnRows.tsx +++ b/webviews/src/agent-session/acpmux/conversation/TurnRows.tsx @@ -275,6 +275,7 @@ export function TurnFooter({ row }: { row: AcpmuxRow }) { reauthenticate, transferCloudCredentials, cloudSignIn, + apiKeySignIn, resume, compact, route, @@ -373,6 +374,7 @@ export function TurnFooter({ row }: { row: AcpmuxRow }) { compact={compact} reauthenticate={reauthenticate} cloudSignIn={cloudSignIn} + apiKeySignIn={apiKeySignIn} retryRowId={row.retryRowId} prompt={prompt} retry={failedRetry} @@ -482,6 +484,7 @@ function TurnFailure({ compact, reauthenticate, cloudSignIn, + apiKeySignIn, retryRowId, prompt, retry, @@ -500,6 +503,9 @@ function TurnFailure({ reauthenticate?: () => void; /// A Cloud chat's own sign-in on its machine (cloudAgentLogin.ts, CloudSignIn.tsx). cloudSignIn?: CloudSignInActions; + /// A chat here whose built-in Claude Code or Codex is not signed in: Use an API key + /// (cloudAgentLogin.ts `localApiKeyLogin`). + apiKeySignIn?: CloudSignInActions; /// The failed prompt's row and text, which the Cloud sign-in sends again once signed in. retryRowId?: string; prompt?: string; @@ -557,6 +563,10 @@ function TurnFailure({ // card keeps only Use an API key of the sign-in, never Sign in to Claude, which would dead-end. const teamMachine = isTeamMachineSignIn(kind, route); const cloudSignInLeads = recovery === "sign-in" && cloudSignIn !== undefined && !signInFirst && !teamMachine; + // A chat here whose built-in Claude Code or Codex is not signed in (Leo 2026-10-26: "if user picks a + // model they don't have api key, we offer them to add credits or add their own api key"): Use an API + // key follows Sign in again, outlined; the key goes where the agent's own CLI reads it. + const localKey = recovery === "sign-in" && cloudSignIn === undefined ? apiKeySignIn : undefined; // Continue or Retry is the way out, or the second one after Switch model for a key or proxy // failure, or after Sign in again; never for trust (errorRecovery.ts). const offersRetry = goOn !== undefined && (recovery !== "sign-in" || signInFirst); @@ -646,6 +656,7 @@ function TurnFailure({ {t("turn.signInAgain")} )} + {localKey && } {offersRetry && (