Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions src/main/automations/headless-automation-agent-launch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ function harness(options: { startupTerminal?: Record<string, unknown> | 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) => [
Expand Down Expand Up @@ -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')
Expand All @@ -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()
Expand Down
1 change: 1 addition & 0 deletions src/main/automations/headless-automation-agent-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ export async function launchHeadlessAutomationAgent(
agent,
prompt: startupPrompt ?? '',
title: run.title,
suppressStartupUpdatePrompt: true,
...(automation.extraAgentArgs ? { extraAgentArgs: automation.extraAgentArgs } : {})
})
terminal = launched
Expand Down
1 change: 1 addition & 0 deletions src/main/automations/headless-workspace-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion src/main/automations/runtime-automation-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -132,6 +133,7 @@ describe('headless automation dispatch with extra agent args', () => {
agent: 'claude',
prompt: 'go',
title: 'Nightly',
suppressStartupUpdatePrompt: true,
extraAgentArgs: '--model opus'
})
})
Expand Down
12 changes: 12 additions & 0 deletions src/main/ipc/pty-agent-pane-launch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => ({
Expand Down
7 changes: 6 additions & 1 deletion src/main/ipc/pty/ipc/spawn-agent-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ export async function planPaneAgentLaunch(ctx: PtyIpcSpawnState): Promise<void>
? request.launch
: undefined
const params = AgentLaunch.safeParse({ ...launch, paneKey })
const suppressStartupUpdatePrompt =
request !== null &&
'suppressStartupUpdatePrompt' in request &&
request.suppressStartupUpdatePrompt === true
if (
!runtime ||
!paneKey ||
Expand Down Expand Up @@ -115,7 +119,8 @@ export async function planPaneAgentLaunch(ctx: PtyIpcSpawnState): Promise<void>
...(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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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' })
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down Expand Up @@ -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
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export async function createWorkerWorktree(args: {
? {
startupAgent: args.agent,
startupLaunchSource: 'orchestration',
startupSuppressUpdatePrompt: true,
...(args.launchPreferences ? { startupLaunchPreferences: args.launchPreferences } : {})
}
: {}),
Expand Down
3 changes: 2 additions & 1 deletion src/main/runtime/runtime-agent-terminal-startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
2 changes: 2 additions & 0 deletions src/main/runtime/runtime-managed-worktree-create-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/main/runtime/runtime-terminal-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/main/runtime/runtime-worktree-agent-startup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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 ?? ''
Expand Down Expand Up @@ -229,6 +232,7 @@ export function resolveWorktreeCreateAgentStartup(
inputs: {
agentArgs?: string | null
extraAgentArgs?: string
suppressStartupUpdatePrompt?: boolean
launchSource?: string
onPromptCarry?: (carried: boolean) => void
}
Expand All @@ -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 } : {})
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<RuntimeTerminalCreate> {
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
Expand All @@ -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) {
Expand Down
2 changes: 2 additions & 0 deletions src/main/runtime/workspaces/workspace-activation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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`. */
Expand All @@ -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(),
Expand Down
6 changes: 5 additions & 1 deletion src/renderer/src/lib/agent-launch-through-host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, SessionOptionValue> | undefined): {
Expand Down Expand Up @@ -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()) {
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/lib/launch-agent-in-new-tab.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'"
})
)
})
Expand Down
5 changes: 4 additions & 1 deletion src/renderer/src/lib/launch-agent-new-tab-host-route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 7 additions & 3 deletions src/renderer/src/lib/launch-agent-startup-prompt-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
})
Expand Down
3 changes: 2 additions & 1 deletion src/renderer/src/lib/launch-work-item-direct-agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/shared/agent-pane-launch-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<z.input<typeof AgentLaunch>, 'paneKey' | 'operationId'>
/** The window submits a prompt into this agent once it starts, so no startup update may take it. */
suppressStartupUpdatePrompt?: boolean
}
Loading
Loading