diff --git a/src/main/automations/headless-automation-agent-launch.test.ts b/src/main/automations/headless-automation-agent-launch.test.ts index f370a398d469..e8cb3c3881df 100644 --- a/src/main/automations/headless-automation-agent-launch.test.ts +++ b/src/main/automations/headless-automation-agent-launch.test.ts @@ -39,7 +39,8 @@ function harness(options: { startupTerminal?: Record | null } = paneKey: 'tab-1:leaf-1', ptyId: 'pty-1', worktreeId: 'wt-1', - title: 'Nightly' + title: 'Nightly', + suppressStartupUpdatePrompt: true })), showManagedWorktree: vi.fn(async () => ({ displayName: 'repo' })), getAgentStatusRowsForPane: vi.fn((_paneKey: string) => [ @@ -124,7 +125,8 @@ describe('headless automation agent launch', () => { expect(runtime.launchAgentTerminal).toHaveBeenCalledWith('id:wt-1', { agent: 'hermes', prompt: 'fix the flaky test', - title: 'Nightly' + title: 'Nightly', + suppressStartupUpdatePrompt: true }) expect(runtime.deliverStartupFollowup).not.toHaveBeenCalled() expect(launched.terminalHandle).toBe('term_existing') @@ -139,6 +141,7 @@ describe('headless automation agent launch', () => { agent: 'claude', prompt: 'fix the flaky test', title: 'Nightly', + suppressStartupUpdatePrompt: true, extraAgentArgs: '--model opus' }) expect(runtime.deliverStartupFollowup).not.toHaveBeenCalled() diff --git a/src/main/automations/headless-automation-agent-launch.ts b/src/main/automations/headless-automation-agent-launch.ts index 4b0dcb8b4f31..cab1941c51d2 100644 --- a/src/main/automations/headless-automation-agent-launch.ts +++ b/src/main/automations/headless-automation-agent-launch.ts @@ -103,6 +103,7 @@ export async function launchHeadlessAutomationAgent( agent, prompt: startupPrompt ?? '', title: run.title, + suppressStartupUpdatePrompt: true, ...(automation.extraAgentArgs ? { extraAgentArgs: automation.extraAgentArgs } : {}) }) terminal = launched diff --git a/src/main/automations/headless-workspace-create.ts b/src/main/automations/headless-workspace-create.ts index a19849458dde..239d67db26fd 100644 --- a/src/main/automations/headless-workspace-create.ts +++ b/src/main/automations/headless-workspace-create.ts @@ -40,6 +40,7 @@ export function buildHeadlessAutomationWorktreeCreateArgs({ createdWithAgent: automation.agentId, startupAgent: automation.agentId, startupPrompt: automation.prompt, + startupSuppressUpdatePrompt: true, ...(automation.extraAgentArgs ? { startupExtraAgentArgs: automation.extraAgentArgs } : {}), telemetrySource: 'unknown', automationProvenance: buildAutomationWorkspaceProvenance(automation, run, repo, createdAt) diff --git a/src/main/automations/runtime-automation-service.test.ts b/src/main/automations/runtime-automation-service.test.ts index 965ae0a61a41..0ce2fb1c7e89 100644 --- a/src/main/automations/runtime-automation-service.test.ts +++ b/src/main/automations/runtime-automation-service.test.ts @@ -75,7 +75,8 @@ describe('headless automation dispatch', () => { expect(runtime.launchAgentTerminal).toHaveBeenCalledWith('id:wt-1', { agent: 'goose', prompt: '', - title: 'Nightly' + title: 'Nightly', + suppressStartupUpdatePrompt: true }) expect(runtime.waitForTerminal).not.toHaveBeenCalled() // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: captured from the mocked constructor above. @@ -132,6 +133,7 @@ describe('headless automation dispatch with extra agent args', () => { agent: 'claude', prompt: 'go', title: 'Nightly', + suppressStartupUpdatePrompt: true, extraAgentArgs: '--model opus' }) }) diff --git a/src/main/ipc/pty-agent-pane-launch.test.ts b/src/main/ipc/pty-agent-pane-launch.test.ts index dd81c9086818..b1c7869658fa 100644 --- a/src/main/ipc/pty-agent-pane-launch.test.ts +++ b/src/main/ipc/pty-agent-pane-launch.test.ts @@ -145,6 +145,18 @@ describe('a pane that starts its own agent launch', () => { }) }) + // Why: the window submits a handoff into this pane, and Enter picks Codex's "Update now" (#18153). + it('plans a pane whose prompt the window submits without the startup update prompt', async () => { + const runtime = register(vi.fn(async () => ({ id: 'pty-agent' }))) + + await spawnPane({ ...AI_BUTTON, suppressStartupUpdatePrompt: true }) + + expect(runtime.planAgentPaneLaunch).toHaveBeenCalledWith( + worktreeId, + expect.objectContaining({ startupAgent: 'claude', suppressStartupUpdatePrompt: true }) + ) + }) + // Planned command quoting requires the planner's runtime shell; an inferred tab shell can reject WSL-share repos. it("runs the plan's runtime shell, not the tab's, as a host launch does", async () => { const providerSpawn = vi.fn(async (_options: { shellOverride?: string }) => ({ diff --git a/src/main/ipc/pty/ipc/spawn-agent-launch.ts b/src/main/ipc/pty/ipc/spawn-agent-launch.ts index b720ecc87462..f12a2195e5a4 100644 --- a/src/main/ipc/pty/ipc/spawn-agent-launch.ts +++ b/src/main/ipc/pty/ipc/spawn-agent-launch.ts @@ -81,6 +81,10 @@ export async function planPaneAgentLaunch(ctx: PtyIpcSpawnState): Promise ? request.launch : undefined const params = AgentLaunch.safeParse({ ...launch, paneKey }) + const suppressStartupUpdatePrompt = + request !== null && + 'suppressStartupUpdatePrompt' in request && + request.suppressStartupUpdatePrompt === true if ( !runtime || !paneKey || @@ -115,7 +119,8 @@ export async function planPaneAgentLaunch(ctx: PtyIpcSpawnState): Promise ...(intent.agentArgs !== undefined ? { agentArgs: intent.agentArgs } : {}), ...(launchPreferences ? { launchPreferences } : {}), ...(intent.launchSource ? { launchSource: intent.launchSource } : {}), - ...(intent.cwd ? { cwd: intent.cwd } : {}) + ...(intent.cwd ? { cwd: intent.cwd } : {}), + ...(suppressStartupUpdatePrompt ? { suppressStartupUpdatePrompt } : {}) }) ctx.paneAgentLaunch.launchRuntime = planned.launchRuntime ctx.args = withPlannedLaunch(args, planned) diff --git a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-02.spec.ts b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-02.spec.ts index c366966a5d7f..dcf061517729 100644 --- a/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-02.spec.ts +++ b/src/main/runtime/orca-runtime-tests/terminal-creation-and-readiness-part-02.spec.ts @@ -550,6 +550,33 @@ describe('OrcaRuntimeService', () => { expect(spawnCall?.env).toMatchObject({ CURSOR_PROFILE: 'captured' }) }) + // Why: workers and host-launched handoffs build their Codex command here (#15814, #18153). + it('skips the startup update prompt on the spawned command only when the launch asks', async () => { + const spawn = vi.fn(async (_options: { command?: string }) => ({ id: 'pty-bg' })) + const runtime = new OrcaRuntimeService({ + ...store, + getSettings: () => ({ ...store.getSettings(), disabledTuiAgents: [], agentCmdOverrides: {} }) + }) + runtime.setPtyController({ + spawn, + write: () => true, + kill: () => true, + getForegroundProcess: async () => null + }) + + await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { + startupAgent: 'codex', + suppressStartupUpdatePrompt: true + }) + await runtime.createTerminal(`path:${TEST_WORKTREE_PATH}`, { startupAgent: 'codex' }) + + const [suppressed, ordinary] = spawn.mock.calls.map(([options]) => options.command) + expect(ordinary).not.toContain('check_for_update_on_startup') + expect(suppressed).toBe( + ordinary?.replace(/^codex/, "codex '-c' 'check_for_update_on_startup=false'") + ) + }) + it('resolves a startupAgent to the CLI binary on Windows, where `cursor` is the IDE', async () => { setPlatform('win32') const spawn = vi.fn().mockResolvedValue({ id: 'pty-bg' }) diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.test.ts b/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.test.ts index 23176159713b..c669c7e9c2e9 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.test.ts @@ -94,6 +94,7 @@ describe('a federated worker through the launch executor', () => { createdWithAgent: 'claude', startupAgent: 'claude', startupLaunchSource: 'orchestration', + startupSuppressUpdatePrompt: true, startupLaunchPreferences: { model: 'opus' }, activate: false, lineage: { noParent: true } @@ -163,6 +164,7 @@ describe('a federated worker through the launch executor', () => { expect(f.runtime.createTerminal).toHaveBeenCalledWith('id:folder:remote', { startupAgent: 'claude', launchSource: 'orchestration', + suppressStartupUpdatePrompt: true, launchPreferences: { model: 'opus' }, title: 'worker-task_remote', presentation: 'background' diff --git a/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.ts b/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.ts index 3a43fa29801c..07d2c77ac7aa 100644 --- a/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.ts +++ b/src/main/runtime/rpc/methods/orchestration/federation/federated-worker-agent-launch.ts @@ -91,6 +91,7 @@ export async function launchFederatedWorkerAgent(args: { createdWithAgent: agent, startupAgent: agent, startupLaunchSource: 'orchestration', + startupSuppressUpdatePrompt: true, ...(args.launchPreferences ? { startupLaunchPreferences: args.launchPreferences } : {}), activate: false, lineage: { noParent: true } @@ -133,6 +134,7 @@ export async function launchFederatedWorkerAgent(args: { // its CLI is `cursor-agent`); resolve through the TUI agent config. startupAgent: agent, launchSource: 'orchestration', + suppressStartupUpdatePrompt: true, ...(args.launchPreferences ? { launchPreferences: args.launchPreferences } : {}), title: `worker-${params.taskId}`, presentation: 'background' diff --git a/src/main/runtime/rpc/methods/orchestration/worker/composed-workers.test.ts b/src/main/runtime/rpc/methods/orchestration/worker/composed-workers.test.ts index 2c95af1aa072..57927ecc25e1 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/composed-workers.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/composed-workers.test.ts @@ -156,6 +156,7 @@ describe('orchestration RPC methods', () => { expect(runtime.createTerminal).toHaveBeenCalledWith('id:repo::worktree', { startupAgent: 'codex', launchSource: 'orchestration', + suppressStartupUpdatePrompt: true, title: `worker-${task.id}`, surfaceOwner: false }) diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-start-launch-executor-parity.test.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-start-launch-executor-parity.test.ts index 2e4b47b08fba..943fc256bd17 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-start-launch-executor-parity.test.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-start-launch-executor-parity.test.ts @@ -196,6 +196,7 @@ describe('a fresh worker through the launch executor', () => { expect(f.runtime.createTerminal).toHaveBeenCalledWith('id:wt_new', { startupAgent: 'claude', launchSource: 'orchestration', + suppressStartupUpdatePrompt: true, title: 'worker-task_1', surfaceOwner: false }) @@ -258,6 +259,7 @@ describe('a fresh worker through the launch executor', () => { expect(f.runtime.createTerminal).toHaveBeenCalledWith('id:wt_existing', { startupAgent: 'claude', launchSource: 'orchestration', + suppressStartupUpdatePrompt: true, title: 'worker-task_1', surfaceOwner: false }) diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-topology.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-topology.ts index 9fe094038f10..100a19d6c634 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-topology.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-topology.ts @@ -70,6 +70,7 @@ export async function createExistingWorktreeWorkerTerminal(args: { // configured launcher instead of executing the raw id. startupAgent: args.agent, launchSource: 'orchestration', + suppressStartupUpdatePrompt: true, ...(args.launchPreferences ? { launchPreferences: args.launchPreferences } : {}), title: `worker-${args.taskId}`, // Why: dispatching a worker is background work; it must not pull the sidebar diff --git a/src/main/runtime/rpc/methods/orchestration/worker/worker-worktree-creation.ts b/src/main/runtime/rpc/methods/orchestration/worker/worker-worktree-creation.ts index 32e1ae2ed4f3..689a9a7ca1e7 100644 --- a/src/main/runtime/rpc/methods/orchestration/worker/worker-worktree-creation.ts +++ b/src/main/runtime/rpc/methods/orchestration/worker/worker-worktree-creation.ts @@ -69,6 +69,7 @@ export async function createWorkerWorktree(args: { ? { startupAgent: args.agent, startupLaunchSource: 'orchestration', + startupSuppressUpdatePrompt: true, ...(args.launchPreferences ? { startupLaunchPreferences: args.launchPreferences } : {}) } : {}), diff --git a/src/main/runtime/runtime-agent-terminal-startup.ts b/src/main/runtime/runtime-agent-terminal-startup.ts index 0a8d64f7c69f..dd873648d42d 100644 --- a/src/main/runtime/runtime-agent-terminal-startup.ts +++ b/src/main/runtime/runtime-agent-terminal-startup.ts @@ -187,7 +187,8 @@ async function buildRuntimeAgentTerminalStartupOptions( isRemote, ...(opts.agentArgs !== undefined ? { agentArgs: opts.agentArgs } : {}), shell: agentLaunchQuotingShell(runtime), - sessionOptions: host.toSessionOptions(opts.launchPreferences) + sessionOptions: host.toSessionOptions(opts.launchPreferences), + ...(opts.suppressStartupUpdatePrompt ? { suppressStartupUpdatePrompt: true } : {}) }), prompt: opts.startupPrompt ?? '', cwd: resolveTerminalStartupCwd(workspace.path, opts.cwd) ?? workspace.path, diff --git a/src/main/runtime/runtime-managed-worktree-create-types.ts b/src/main/runtime/runtime-managed-worktree-create-types.ts index 77eb0e3b6f89..2d37b28be7b7 100644 --- a/src/main/runtime/runtime-managed-worktree-create-types.ts +++ b/src/main/runtime/runtime-managed-worktree-create-types.ts @@ -72,6 +72,8 @@ export type RuntimeManagedWorktreeCreateArgs = { startupAgentArgs?: string | null /** Main-internal: an automation's saved extras, merged over the startup agent's arguments. */ startupExtraAgentArgs?: string + /** Main-internal: Orca, not a user, types into the startup agent once it starts. */ + startupSuppressUpdatePrompt?: boolean startupCwd?: string /** The surface behind a host-built startup agent (`startupAgent` or `startupDraft`). */ startupLaunchSource?: string diff --git a/src/main/runtime/runtime-terminal-contracts.ts b/src/main/runtime/runtime-terminal-contracts.ts index 81d9d85df7df..fa86da3f1bf5 100644 --- a/src/main/runtime/runtime-terminal-contracts.ts +++ b/src/main/runtime/runtime-terminal-contracts.ts @@ -81,6 +81,8 @@ export type TerminalCreateOptions = { * the runtime still builds, so overriding them does not take the launch away from it. */ agentArgs?: string | null + /** Main-internal: Orca, not a user, types into this `startupAgent` once it starts. */ + suppressStartupUpdatePrompt?: boolean /** Main-internal: the runtime fact this agent's command was quoted for; its spawn takes the PTY * shell from it. Set by the host planner or a host caller that quoted with it, never a client. */ launchRuntime?: AgentLaunchRuntime diff --git a/src/main/runtime/runtime-worktree-agent-startup.ts b/src/main/runtime/runtime-worktree-agent-startup.ts index fc5ed7a2ca65..fe761c88984d 100644 --- a/src/main/runtime/runtime-worktree-agent-startup.ts +++ b/src/main/runtime/runtime-worktree-agent-startup.ts @@ -38,6 +38,8 @@ type StartupEnvironment = { agentArgs?: string | null /** An automation's saved extras, merged over the launch's arguments. */ extraAgentArgs?: string + /** Orca, not a user, types into this agent once it starts. */ + suppressStartupUpdatePrompt?: boolean /** Caller-supplied telemetry attribution, validated leniently at the host boundary. */ launchSource?: string } @@ -170,6 +172,7 @@ export function buildWorktreeStartupForAgent( isRemote: repoIsRemote(repo), ...(environment.agentArgs !== undefined ? { agentArgs: environment.agentArgs } : {}), ...(environment.extraAgentArgs ? { extraAgentArgs: environment.extraAgentArgs } : {}), + ...(environment.suppressStartupUpdatePrompt ? { suppressStartupUpdatePrompt: true } : {}), sessionOptions: environment.toSessionOptions(environment.launchPreferences) }) const prompt = environment.prompt ?? '' @@ -229,6 +232,7 @@ export function resolveWorktreeCreateAgentStartup( inputs: { agentArgs?: string | null extraAgentArgs?: string + suppressStartupUpdatePrompt?: boolean launchSource?: string onPromptCarry?: (carried: boolean) => void } @@ -245,6 +249,7 @@ export function resolveWorktreeCreateAgentStartup( const built = build(args.startupAgent, args.startupPrompt, args.startupLaunchPreferences, { ...(args.startupAgentArgs !== undefined ? { agentArgs: args.startupAgentArgs } : {}), ...(args.startupExtraAgentArgs ? { extraAgentArgs: args.startupExtraAgentArgs } : {}), + ...(args.startupSuppressUpdatePrompt ? { suppressStartupUpdatePrompt: true } : {}), ...(args.startupLaunchSource ? { launchSource: args.startupLaunchSource } : {}), ...(args.onStartupPromptCarry ? { onPromptCarry: args.onStartupPromptCarry } : {}) }) diff --git a/src/main/runtime/terminal/launch/terminal-create-deduplication.ts b/src/main/runtime/terminal/launch/terminal-create-deduplication.ts index 1d77b402e00d..9d9789df370d 100644 --- a/src/main/runtime/terminal/launch/terminal-create-deduplication.ts +++ b/src/main/runtime/terminal/launch/terminal-create-deduplication.ts @@ -153,7 +153,13 @@ export abstract class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRun async launchAgentTerminal( worktreeSelector: string, - opts: { agent: TuiAgent; prompt: string; title?: string; extraAgentArgs?: string } + opts: { + agent: TuiAgent + prompt: string + title?: string + extraAgentArgs?: string + suppressStartupUpdatePrompt?: boolean + } ): Promise { const worktree = await this.resolveWorktreeSelector(worktreeSelector) // Why: `getRepo(id)` is host-blind; the same repo id on two hosts must build the launch for the @@ -168,6 +174,7 @@ export abstract class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRun } const startup = this.buildStartupForAgent(repo, opts.agent, opts.prompt, undefined, { ...(opts.extraAgentArgs ? { extraAgentArgs: opts.extraAgentArgs } : {}), + ...(opts.suppressStartupUpdatePrompt ? { suppressStartupUpdatePrompt: true } : {}), launchScope: await this.resolveTerminalWorkspaceLaunchScope(`id:${worktree.id}`) }) if (startup.launchRuntime instanceof Error) { diff --git a/src/main/runtime/workspaces/workspace-activation.ts b/src/main/runtime/workspaces/workspace-activation.ts index 9c7f40ae3d06..c6d7c4e73bf2 100644 --- a/src/main/runtime/workspaces/workspace-activation.ts +++ b/src/main/runtime/workspaces/workspace-activation.ts @@ -173,6 +173,7 @@ export abstract class OrcaRuntimeWithActivateManagedWorktree extends OrcaRuntime launchInputs?: { agentArgs?: string | null extraAgentArgs?: string + suppressStartupUpdatePrompt?: boolean launchSource?: string onPromptCarry?: (carried: boolean) => void /** The existing workspace it starts in; absent is a new workspace of `repo`. */ @@ -189,6 +190,7 @@ export abstract class OrcaRuntimeWithActivateManagedWorktree extends OrcaRuntime ...(launchPreferences ? { launchPreferences } : {}), ...(launchInputs?.agentArgs !== undefined ? { agentArgs: launchInputs.agentArgs } : {}), ...(launchInputs?.extraAgentArgs ? { extraAgentArgs: launchInputs.extraAgentArgs } : {}), + ...(launchInputs?.suppressStartupUpdatePrompt ? { suppressStartupUpdatePrompt: true } : {}), ...(launchInputs?.launchSource ? { launchSource: launchInputs.launchSource } : {}), ...(launchInputs?.onPromptCarry ? { onPromptCarry: launchInputs.onPromptCarry } : {}), settings: this.store.getSettings(), diff --git a/src/renderer/src/lib/agent-launch-through-host.ts b/src/renderer/src/lib/agent-launch-through-host.ts index a61d06959a6b..dd8a1fc99f72 100644 --- a/src/renderer/src/lib/agent-launch-through-host.ts +++ b/src/renderer/src/lib/agent-launch-through-host.ts @@ -38,6 +38,7 @@ export type HostAgentLaunchArgs = { pendingActivationSpawn?: boolean /** The view the tab opens in, decided as for any new agent tab. */ viewMode?: Tab['viewMode'] + suppressStartupUpdatePrompt?: boolean } function stringSessionOptions(options: Record | undefined): { @@ -87,7 +88,10 @@ export function launchAgentThroughHost(args: HostAgentLaunchArgs): { pendingStartup: { command: '', launchAgent: args.agent, - agentLaunch: { launch: launchParams(args) } + agentLaunch: { + launch: launchParams(args), + ...(args.suppressStartupUpdatePrompt ? { suppressStartupUpdatePrompt: true } : {}) + } } }) if (args.cwd?.trim()) { diff --git a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts index 743ac9a847f1..2be10173b1ed 100644 --- a/src/renderer/src/lib/launch-agent-in-new-tab.test.ts +++ b/src/renderer/src/lib/launch-agent-in-new-tab.test.ts @@ -895,7 +895,8 @@ describe('launchAgentInNewTab', () => { expect(queuedStartups()).toContainEqual( expect.objectContaining({ - command: "codex '--model' 'gpt-5.5'" + // Codex's update prompt would take the Enter that submits the pasted prompt (#18153). + command: "codex '-c' 'check_for_update_on_startup=false' '--model' 'gpt-5.5'" }) ) }) diff --git a/src/renderer/src/lib/launch-agent-new-tab-host-route.ts b/src/renderer/src/lib/launch-agent-new-tab-host-route.ts index adb9898b8692..1e92068a7449 100644 --- a/src/renderer/src/lib/launch-agent-new-tab-host-route.ts +++ b/src/renderer/src/lib/launch-agent-new-tab-host-route.ts @@ -44,7 +44,10 @@ export function launchNewTabPromptThroughHost( promptDeliveryResult: Promise<{ delivered: boolean; failureNotified: boolean }> } { const { pasteContent, submit, onPromptDelivered, onPromptDeliveryUnconfirmed, ...launch } = args - const { tabId, started } = launchAgentThroughHost(launch) + const { tabId, started } = launchAgentThroughHost({ + ...launch, + suppressStartupUpdatePrompt: submit + }) const pasted = pasteAgentLaunchPromptOnceReady({ worktreeId: args.worktreeId, tabId, diff --git a/src/renderer/src/lib/launch-agent-startup-prompt-plan.ts b/src/renderer/src/lib/launch-agent-startup-prompt-plan.ts index 0723d3ede9a8..cf80b8442d07 100644 --- a/src/renderer/src/lib/launch-agent-startup-prompt-plan.ts +++ b/src/renderer/src/lib/launch-agent-startup-prompt-plan.ts @@ -30,10 +30,14 @@ export function planLaunchAgentStartupPrompt(args: { }): LaunchAgentStartupPromptPlan { const { base, prompt, promptDelivery, isFollowupPath } = args const hasPrompt = prompt.length > 0 - const launchEmpty = (): AgentStartupPlan | null => - buildAgentStartupPlan({ ...base, prompt: '', allowEmptyPromptLaunch: true }) const pasteAfterReady = (submit: boolean): LaunchAgentStartupPromptPlan => ({ - startupPlan: launchEmpty(), + startupPlan: buildAgentStartupPlan({ + ...base, + prompt: '', + allowEmptyPromptLaunch: true, + // Why: Orca's own Enter must not answer a startup update prompt. + suppressStartupUpdatePrompt: submit + }), pasteDraftAfterLaunch: prompt, submitPastedPrompt: submit }) diff --git a/src/renderer/src/lib/launch-work-item-direct-agent.ts b/src/renderer/src/lib/launch-work-item-direct-agent.ts index ec105dddb16a..ede6ff16f31d 100644 --- a/src/renderer/src/lib/launch-work-item-direct-agent.ts +++ b/src/renderer/src/lib/launch-work-item-direct-agent.ts @@ -88,7 +88,8 @@ export function buildDirectWorkItemAgentStartupPlan(args: { isRemote: args.isRemote, agentArgs: effectiveAgentArgs, agentEnv: effectiveAgentEnv, - allowEmptyPromptLaunch: true + allowEmptyPromptLaunch: true, + ...(args.promptDelivery === 'submit-after-ready' ? { suppressStartupUpdatePrompt: true } : {}) }) if (startupPlan && args.promptDelivery === 'draft') { startupPlan.draftPrompt = args.draftContent diff --git a/src/shared/agent-pane-launch-request.ts b/src/shared/agent-pane-launch-request.ts index 96ed8c5adf94..9b1d159ff42c 100644 --- a/src/shared/agent-pane-launch-request.ts +++ b/src/shared/agent-pane-launch-request.ts @@ -4,4 +4,6 @@ import type { AgentLaunch } from './rpc-contract/agent-launch-params' /** `agent.launch` params a pane hands its own spawn; main names the pane, and nothing is recorded. */ export type AgentPaneLaunchRequest = { launch: Omit, 'paneKey' | 'operationId'> + /** The window submits a prompt into this agent once it starts, so no startup update may take it. */ + suppressStartupUpdatePrompt?: boolean } diff --git a/src/shared/agent-startup-plan-inputs.spec.ts b/src/shared/agent-startup-plan-inputs.spec.ts index bc645aba5ea1..0f814e331f41 100644 --- a/src/shared/agent-startup-plan-inputs.spec.ts +++ b/src/shared/agent-startup-plan-inputs.spec.ts @@ -158,3 +158,41 @@ describe('a picked session option outranks configured launch arguments', () => { ) }) }) + +describe('an Orca-driven launch skips the startup update prompt (#15814, #18153)', () => { + function planFor( + agent: 'codex' | 'claude', + suppressStartupUpdatePrompt: boolean, + sessionOptions?: { model: string } + ) { + return buildAgentStartupPlan({ + ...resolveAgentStartupPlanInputs({ + agent, + settings: { agentDefaultArgs: { codex: '--search' } }, + platform: 'darwin', + isRemote: false, + shell: undefined, + suppressStartupUpdatePrompt, + ...(sessionOptions ? { sessionOptions } : {}) + }), + prompt: '', + allowEmptyPromptLaunch: true + }) + } + + it('adds Codex config only to the typed command, never to the resumable one', () => { + for (const sessionOptions of [undefined, { model: 'gpt-5' }]) { + const ordinary = planFor('codex', false, sessionOptions) + const suppressed = planFor('codex', true, sessionOptions) + expect(ordinary?.launchCommand).not.toContain('check_for_update_on_startup') + expect(suppressed?.launchCommand).toBe( + ordinary?.launchCommand.replace(/^codex/, "codex '-c' 'check_for_update_on_startup=false'") + ) + expect(suppressed?.launchConfig).toEqual(ordinary?.launchConfig) + } + }) + + it('leaves agents without a startup updater unchanged', () => { + expect(planFor('claude', true)?.launchCommand).toBe(planFor('claude', false)?.launchCommand) + }) +}) diff --git a/src/shared/agent-startup-plan-inputs.ts b/src/shared/agent-startup-plan-inputs.ts index 77cda50c95a4..454984519549 100644 --- a/src/shared/agent-startup-plan-inputs.ts +++ b/src/shared/agent-startup-plan-inputs.ts @@ -30,6 +30,7 @@ export type AgentStartupPlanInputs = { isRemote: boolean sessionOptions?: Record sessionOptionsOverrideAgentArgs: boolean + suppressStartupUpdatePrompt?: boolean } /** @@ -55,6 +56,8 @@ export function resolveAgentStartupPlanInputs(args: { * right only where the Settings shell is the one the PTY gets. */ shell: AgentStartupShell | undefined sessionOptions?: Record | undefined + /** Orca types into this launch after start (worker, automation, handoff), not a user. */ + suppressStartupUpdatePrompt?: boolean }): AgentStartupPlanInputs { const { agent, settings, platform, isRemote, sessionOptions } = args // A per-launch override wins over the Settings default; `null` is "no arguments", so this @@ -91,6 +94,7 @@ export function resolveAgentStartupPlanInputs(args: { ...(sessionOptions ? { sessionOptions } : {}), // Why: session options are an explicit per-launch pick, so they outrank configured args — // without this the two spellings of the same flag both reach argv and the last one wins. - sessionOptionsOverrideAgentArgs: Boolean(sessionOptions) + sessionOptionsOverrideAgentArgs: Boolean(sessionOptions), + ...(args.suppressStartupUpdatePrompt ? { suppressStartupUpdatePrompt: true } : {}) } } diff --git a/src/shared/launch-parity-workspace-start-cases.test-fixture.ts b/src/shared/launch-parity-workspace-start-cases.test-fixture.ts index 4fb52e8137ba..a34ad31f6eb4 100644 --- a/src/shared/launch-parity-workspace-start-cases.test-fixture.ts +++ b/src/shared/launch-parity-workspace-start-cases.test-fixture.ts @@ -113,13 +113,19 @@ export const WORKSPACE_START_LAUNCH_CASES: WindowLaunchCase[] = [ { terminalWindowsShell: 'wsl.exe' } ), // Fix checks submit after the agent is ready: the command carries no prompt, the window pastes it, - // and main's prompt-less Codex start is typed without waiting for the shell to be ready. + // and main's prompt-less Codex start is typed without waiting for the shell to be ready. The + // window's Enter must not pick Codex's "Update now", so only the typed command skips that prompt. start( 'work item, submit after ready', 'darwin', repo(POSIX_PATH), { kind: 'work-item', agent: 'codex', delivery: 'submit-after-ready', text: BOB }, - { command: POSIX_CODEX, agentCommand: POSIX_CODEX, shellReady: false }, + { + command: + "codex '-c' 'check_for_update_on_startup=false' '--dangerously-bypass-approvals-and-sandbox'", + agentCommand: POSIX_CODEX, + shellReady: false + }, ZSH ), // The composer's note is an auto-submitted prompt on argv. diff --git a/src/shared/tui-agent-launch-command.ts b/src/shared/tui-agent-launch-command.ts index 6b6fa0681277..5d4ecb8ee80f 100644 --- a/src/shared/tui-agent-launch-command.ts +++ b/src/shared/tui-agent-launch-command.ts @@ -13,6 +13,11 @@ import { } from './tui-agent-startup-shell' import type { TuiAgent } from './tui-agent' +// Why: Enter on Codex's "Update available" screen picks "Update now", which installs and exits. +const STARTUP_UPDATE_PROMPT_SUPPRESSION_ARGS: Partial> = { + codex: ['-c', 'check_for_update_on_startup=false'] +} + export type ResolvedAgentLaunchCommand = | { ok: true @@ -31,6 +36,8 @@ export function resolveAgentLaunchCommand(args: { sessionOptions?: Record sessionOptionsOverrideAgentArgs?: boolean isRemote?: boolean + /** Launch-only, like session options: a resumed session must not inherit it. */ + suppressStartupUpdatePrompt?: boolean }): ResolvedAgentLaunchCommand { const override = args.cmdOverrides[args.agent] const command = @@ -38,6 +45,12 @@ export function resolveAgentLaunchCommand(args: { getTuiAgentLaunchCommand(TUI_AGENT_CONFIG[args.agent], args.platform, { isRemote: args.isRemote }) + const suppressionArgs = args.suppressStartupUpdatePrompt + ? (STARTUP_UPDATE_PROMPT_SUPPRESSION_ARGS[args.agent] ?? []) + : [] + const launchHead = suppressionArgs.length + ? `${command} ${suppressionArgs.map((arg) => quoteStartupArg(arg, args.shell)).join(' ')}` + : command const suffix = planAgentCliArgsSuffix(args.agentArgs, args.shell) if (!suffix.ok) { return suffix @@ -79,7 +92,7 @@ export function resolveAgentLaunchCommand(args: { } const optionSuffix = resolvedOptions.args.map((arg) => quoteStartupArg(arg, args.shell)).join(' ') const commandWithoutSessionOptions = suffix.suffix ? `${command} ${suffix.suffix}` : command - const commandWithOptions = optionSuffix ? `${command} ${optionSuffix}` : command + const commandWithOptions = optionSuffix ? `${launchHead} ${optionSuffix}` : launchHead const overrideTokens = args.sessionOptionsOverrideAgentArgs ? insertBeforeTerminator( args.agent, @@ -88,8 +101,8 @@ export function resolveAgentLaunchCommand(args: { ) : [] const commandWithOverrides = overrideTokens.length - ? `${command} ${overrideTokens.map((token) => quoteStartupArg(token, args.shell)).join(' ')}` - : command + ? `${launchHead} ${overrideTokens.map((token) => quoteStartupArg(token, args.shell)).join(' ')}` + : launchHead return { ok: true, command: args.sessionOptionsOverrideAgentArgs diff --git a/src/shared/tui-agent-startup.ts b/src/shared/tui-agent-startup.ts index ba516c97f157..ccb0a1da4c2b 100644 --- a/src/shared/tui-agent-startup.ts +++ b/src/shared/tui-agent-startup.ts @@ -50,6 +50,8 @@ export function buildAgentStartupPlan(args: { /** Why: SSH remotes deploy the CLI shim as plain `orca`, so the Linux-only * `orca-ide` rename must be skipped for remote launches. */ isRemote?: boolean + /** Orca types into this launch after start, so a startup update prompt would take that input. */ + suppressStartupUpdatePrompt?: boolean }): AgentStartupPlan | null { const { agent, prompt, cmdOverrides, platform, allowEmptyPromptLaunch = false } = args const shell = resolveStartupShell(platform, args.shell) @@ -64,7 +66,8 @@ export function buildAgentStartupPlan(args: { agentArgs: usesQuery ? null : args.agentArgs, sessionOptions: args.sessionOptions, sessionOptionsOverrideAgentArgs: args.sessionOptionsOverrideAgentArgs, - isRemote: args.isRemote + isRemote: args.isRemote, + suppressStartupUpdatePrompt: args.suppressStartupUpdatePrompt }) if (!baseCommand.ok) { return null