From 3e4311db19ffb84fcec13a4fadecd5f0db9ad5f7 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Mon, 12 Oct 2026 03:04:19 -0700 Subject: [PATCH] fix(agent-launch): keep the window's WSL answer for panes the window spawns Live Windows QA (Terminal shell wsl.exe, global default Windows host, repo with no project setting) saw an agent command written with PowerShell quoting typed into a WSL pane, so codex lost the apostrophes in its prompt. The host wrote the command for its own answer (Windows, PowerShell), but on routes where the window spawns the pane (the phone while a window is open, a focused desktop chat create, `orca terminal create --command` in a repo) the window spawned it by the Terminal shell, which is WSL. Quoting and the pane came from two different answers. Those routes ran in WSL with Linux-style quoting on main and worked, so the temporary host-legacy term (STA-10231) now also covers them: when the window spawns the pane, the path and Terminal shell decide, as on main. Background routes keep the approved change (PowerShell instead of cmd.exe with Linux-style quoting, which was broken on main). createTerminal and the pre-launch planning share one `windowSpawnsTerminal` rule so they can't disagree about who spawns the pane. --- .../runtime/agent-launch-scope-runtime.test.ts | 16 ++++++++++++++++ src/main/runtime/agent-launch-scope-runtime.ts | 14 +++++++++----- .../orca-runtime-create-agent-session.ts | 10 +++++++++- ...orca-runtime-create-terminal-dependencies.ts | 16 ++++++++++++++++ .../orca-runtime-create-terminal-desktop.ts | 2 +- .../runtime/orca-runtime-create-terminal.ts | 5 +---- ...e-resolve-mobile-session-terminal-command.ts | 6 ++++-- ...a-runtime-resolve-worktree-removal-target.ts | 4 +++- ...estore-structured-agent-session-tabs-once.ts | 5 +++-- ...untime-run-create-mobile-session-terminal.ts | 7 ++++++- .../runtime/runtime-agent-terminal-startup.ts | 17 +++++++++++++++-- 11 files changed, 83 insertions(+), 19 deletions(-) diff --git a/src/main/runtime/agent-launch-scope-runtime.test.ts b/src/main/runtime/agent-launch-scope-runtime.test.ts index 9c332190f25..1af616d9b1e 100644 --- a/src/main/runtime/agent-launch-scope-runtime.test.ts +++ b/src/main/runtime/agent-launch-scope-runtime.test.ts @@ -127,3 +127,19 @@ it("keeps a folder's WSL Terminal shell where its repo's project is set to Windo 'posix' ]) }) + +// TEMPORARY until STA-10231: a pane the window spawns keeps main's Terminal-shell answer. +it('writes the command for the Terminal shell when the window spawns the pane', () => { + const s = store({ global: { kind: 'windows-host' }, shell: 'wsl.exe' }) + const quoting = (windowSpawnsPane: boolean) => + agentLaunchQuotingShell( + resolveAgentLaunchRuntime( + s, + scope(WIN, { repo: repo(WIN) }), + undefined, + 'codex', + windowSpawnsPane + ) + ) + expect([quoting(true), quoting(false)]).toEqual(['posix', 'powershell']) +}) diff --git a/src/main/runtime/agent-launch-scope-runtime.ts b/src/main/runtime/agent-launch-scope-runtime.ts index 91eef3937dd..ecb3361258b 100644 --- a/src/main/runtime/agent-launch-scope-runtime.ts +++ b/src/main/runtime/agent-launch-scope-runtime.ts @@ -47,7 +47,9 @@ export function agentLaunchPlatformForScope( export function agentLaunchProjectRuntime( store: LaunchScopeRuntimeStore, scope: TerminalWorkspaceLaunchScope, - agent: TuiAgent | undefined + agent: TuiAgent | undefined, + /** The window spawns the pane, by the Terminal shell, so the command must be written for it. */ + windowSpawnsPane = false ): ProjectExecutionRuntimeResolution | undefined { const runtime = resolveLocalLaunchScopeRuntime(store, scope) const onWindowsHost = runtime?.status === 'resolved' && runtime.runtime.kind === 'windows-host' @@ -57,8 +59,9 @@ export function agentLaunchProjectRuntime( // PERMANENT fix, not parity: a `\\wsl$` cwd runs in WSL whatever the setting says. Keep it // when the temporary terms go; the window adopts it in C2. (onWindowsHost && isWslUncPath(scope.path)) || - // TEMPORARY (STA-10231): an explicit Windows host can't be told from the shipped default. - (onWindowsHost && mainHadNone) || + // TEMPORARY (STA-10231): an explicit Windows host can't be told from the shipped default, so + // folders and panes the window spawns keep main's Terminal-shell answer (often WSL). + (onWindowsHost && (mainHadNone || windowSpawnsPane)) || // TEMPORARY (STA-10251): OpenCode models can't yet be verified inside WSL. (agent === 'opencode' && mainHadNone) return pathDecides ? undefined : runtime @@ -70,7 +73,8 @@ export function resolveAgentLaunchRuntime( store: AgentLaunchRuntimeStore, scope: TerminalWorkspaceLaunchScope, requestedShellOverride: string | undefined, - agent: TuiAgent | undefined + agent: TuiAgent | undefined, + windowSpawnsPane = false ): AgentLaunchRuntime { if (scope.connectionId) { return { @@ -78,7 +82,7 @@ export function resolveAgentLaunchRuntime( pty: { shellOverride: undefined, terminalWindowsWslDistro: null } } } - const projectRuntime = agentLaunchProjectRuntime(store, scope, agent) + const projectRuntime = agentLaunchProjectRuntime(store, scope, agent, windowSpawnsPane) return { platform: executionRuntimePlatform(projectRuntime, scope.path, process.platform), pty: resolveLocalPtyOptions(store.getSettings(), projectRuntime, requestedShellOverride) diff --git a/src/main/runtime/orca-runtime-create-agent-session.ts b/src/main/runtime/orca-runtime-create-agent-session.ts index 8d721333abe..c04afba5450 100644 --- a/src/main/runtime/orca-runtime-create-agent-session.ts +++ b/src/main/runtime/orca-runtime-create-agent-session.ts @@ -14,6 +14,7 @@ import { createHash } from 'node:crypto' import { isTuiAgentEnabled } from '../../shared/tui-agent-selection' import { resolveAgentStartupPlanInputs } from '../../shared/agent-startup-plan-inputs' import { agentLaunchQuotingShell } from '../../shared/agent-launch-runtime' +import { windowSpawnsTerminal } from './orca-runtime-create-terminal-dependencies' import { buildExecutionHostAgentStartupPlan } from '../opencode/opencode-model-startup-plan' import type { RuntimeTerminalCreate } from '../../shared/runtime-types' import type { @@ -131,7 +132,14 @@ export class OrcaRuntimeWithCreateAgentSession extends OrcaRuntimeWithGetAgentSe if (!isTuiAgentEnabled(request.agent, settings.disabledTuiAgents)) { throw new Error('Selected agent is disabled. Choose an enabled agent before creating.') } - const launchRuntime = this.resolveAgentLaunchRuntime(workspace, request.agent) + const launchRuntime = this.resolveAgentLaunchRuntime( + workspace, + request.agent, + windowSpawnsTerminal( + { presentation: request.presentation ?? 'background' }, + this.getAvailableAuthoritativeWindow() !== null + ) + ) const startupArgs = resolveAgentStartupPlanInputs({ agent: request.agent, settings, diff --git a/src/main/runtime/orca-runtime-create-terminal-dependencies.ts b/src/main/runtime/orca-runtime-create-terminal-dependencies.ts index 85e4e1e5fdd..a4078247999 100644 --- a/src/main/runtime/orca-runtime-create-terminal-dependencies.ts +++ b/src/main/runtime/orca-runtime-create-terminal-dependencies.ts @@ -42,6 +42,22 @@ export const BACKGROUND_TERMINAL_SPAWN_FLAGS = { persistHostSessionBinding: true } as const +/** Whether the window spawns this create's pane, by its own shell rule, rather than this host. */ +export function windowSpawnsTerminal( + opts: Pick< + TerminalCreateOptions, + 'agentSessionClaim' | 'requireFreshPane' | 'presentation' | 'focus' | 'rendererBacked' + >, + windowAvailable: boolean +): boolean { + return ( + windowAvailable && + !opts.agentSessionClaim && + !opts.requireFreshPane && + (opts.presentation === 'focused' || opts.focus === true || opts.rendererBacked === true) + ) +} + /** The PTY's shell inputs: a requested shell, and an agent launch's runtime fact. */ export const spawnShellInputs = ({ shellOverride, diff --git a/src/main/runtime/orca-runtime-create-terminal-desktop.ts b/src/main/runtime/orca-runtime-create-terminal-desktop.ts index 6a56bff56b0..631f3b597a9 100644 --- a/src/main/runtime/orca-runtime-create-terminal-desktop.ts +++ b/src/main/runtime/orca-runtime-create-terminal-desktop.ts @@ -18,7 +18,7 @@ export async function createDesktopTerminal( ? await runtime.resolveTerminalWorkspaceLaunchScope(worktreeSelector, createdWorktree) : null const launchOpts = workspace - ? await runtime.resolveAgentTerminalCreateOptions(workspace, opts) + ? await runtime.resolveAgentTerminalCreateOptions(workspace, opts, true) : opts // `resolveAgentTerminalCreateOptions` refuses an unapplicable shell, and it only runs with a // workspace; a worktree-less create has no execution host to apply one to either. diff --git a/src/main/runtime/orca-runtime-create-terminal.ts b/src/main/runtime/orca-runtime-create-terminal.ts index a4c7ed44652..dcbc7cdcc00 100644 --- a/src/main/runtime/orca-runtime-create-terminal.ts +++ b/src/main/runtime/orca-runtime-create-terminal.ts @@ -22,14 +22,11 @@ export class OrcaRuntimeWithCreateTerminal extends OrcaRuntimeWithTerminalCreate dependencies.setPairedViewerColors(callerColors) } const presentation = dependencies.resolveTerminalPresentation(opts) - const requiresRendererFocus = opts.presentation === 'focused' || opts.focus === true const availableAuthoritativeWindow = this.getAvailableAuthoritativeWindow() const rendererWindow = opts.rendererBacked === true ? availableAuthoritativeWindow : null const shouldCreateInBackground = worktreeSelector !== undefined && - (Boolean(opts.agentSessionClaim || opts.requireFreshPane) || - (!requiresRendererFocus && opts.rendererBacked !== true) || - availableAuthoritativeWindow === null) + !dependencies.windowSpawnsTerminal(opts, availableAuthoritativeWindow !== null) if (shouldCreateInBackground) { if (!this.ptyController?.spawn) { throw new Error('runtime_unavailable') diff --git a/src/main/runtime/orca-runtime-resolve-mobile-session-terminal-command.ts b/src/main/runtime/orca-runtime-resolve-mobile-session-terminal-command.ts index 41032c139ef..a2ac5df8682 100644 --- a/src/main/runtime/orca-runtime-resolve-mobile-session-terminal-command.ts +++ b/src/main/runtime/orca-runtime-resolve-mobile-session-terminal-command.ts @@ -21,7 +21,9 @@ export class OrcaRuntimeWithResolveMobileSessionTerminalCommand extends OrcaRunt agentPrompt?: string launchConfig?: SleepingAgentLaunchConfig launchAgent?: TuiAgent - } + }, + /** The window spawns the tab by its own shell rule. */ + windowSpawnsPane = false ): Promise<{ command?: string env?: Record @@ -49,7 +51,7 @@ export class OrcaRuntimeWithResolveMobileSessionTerminalCommand extends OrcaRunt if (!isTuiAgentEnabled(opts.agent, settings.disabledTuiAgents)) { throw new Error('Selected agent is disabled. Choose an enabled agent before creating.') } - const launchRuntime = this.resolveAgentLaunchRuntime(workspace, opts.agent) + const launchRuntime = this.resolveAgentLaunchRuntime(workspace, opts.agent, windowSpawnsPane) const startupPlan = buildAgentStartupPlan({ ...resolveAgentStartupPlanInputs({ agent: opts.agent, diff --git a/src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts b/src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts index 79c3726653e..bff92162bd0 100644 --- a/src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts +++ b/src/main/runtime/orca-runtime-resolve-worktree-removal-target.ts @@ -237,9 +237,11 @@ export class OrcaRuntimeWithResolveWorktreeRemovalTarget extends OrcaRuntimeWith protected async resolveAgentTerminalCreateOptions( workspace: TerminalWorkspaceLaunchScope, - opts: TerminalCreateOptions + opts: TerminalCreateOptions, + windowSpawnsPane = false ): Promise { return resolveAgentTerminalCreateOptions(workspace, opts, { + windowSpawnsPane, store: this.store, hostIdentity: this.runtimeId, toSessionOptions: (preferences) => this.toAgentSessionOptions(preferences) diff --git a/src/main/runtime/orca-runtime-restore-structured-agent-session-tabs-once.ts b/src/main/runtime/orca-runtime-restore-structured-agent-session-tabs-once.ts index c848374caa7..f2409f14600 100644 --- a/src/main/runtime/orca-runtime-restore-structured-agent-session-tabs-once.ts +++ b/src/main/runtime/orca-runtime-restore-structured-agent-session-tabs-once.ts @@ -276,9 +276,10 @@ export class OrcaRuntimeWithRestoreStructuredAgentSessionTabsOnce extends OrcaRu protected resolveAgentLaunchRuntime( scope: TerminalWorkspaceLaunchScope, - agent: TuiAgent | undefined + agent: TuiAgent | undefined, + windowSpawnsPane = false ): AgentLaunchRuntime { - return resolveAgentLaunchRuntime(this.requireStore(), scope, undefined, agent) + return resolveAgentLaunchRuntime(this.requireStore(), scope, undefined, agent, windowSpawnsPane) } /** A startup's fact: the workspace it starts in, else the new worktree a create is making. */ diff --git a/src/main/runtime/orca-runtime-run-create-mobile-session-terminal.ts b/src/main/runtime/orca-runtime-run-create-mobile-session-terminal.ts index 0813497d2de..f0cbaef2893 100644 --- a/src/main/runtime/orca-runtime-run-create-mobile-session-terminal.ts +++ b/src/main/runtime/orca-runtime-run-create-mobile-session-terminal.ts @@ -62,7 +62,12 @@ export class OrcaRuntimeWithRunCreateMobileSessionTerminal extends OrcaRuntimeWi afterDesktopTabId = anchor.type === 'terminal' ? anchor.parentTabId : anchor.id } } - const startupCommand = await this.resolveMobileSessionTerminalCommand(workspace, opts) + // With a window, it creates the tab and spawns the pane, so the command is written for that pane. + const startupCommand = await this.resolveMobileSessionTerminalCommand( + workspace, + opts, + this.getAvailableAuthoritativeWindow() !== null + ) this.assertStableReadyGraph(graphEpoch) if (opts.signal?.aborted) { throw new Error('client_disconnected') diff --git a/src/main/runtime/runtime-agent-terminal-startup.ts b/src/main/runtime/runtime-agent-terminal-startup.ts index e641108001e..7ad57724caa 100644 --- a/src/main/runtime/runtime-agent-terminal-startup.ts +++ b/src/main/runtime/runtime-agent-terminal-startup.ts @@ -40,6 +40,8 @@ type AgentTerminalPlanHost = { toSessionOptions: ( preferences: AgentLaunchPreferences | undefined ) => Record | undefined + /** The window spawns this pane by its own shell rule (`windowSpawnsTerminal`). */ + windowSpawnsPane?: boolean } /** @@ -81,7 +83,12 @@ export async function resolveAgentTerminalCreateOptions( projectRuntime: opts.shellOverride && store ? plannedAgent || opts.launchRuntime - ? agentLaunchProjectRuntime(store, workspace, plannedAgent ?? opts.launchAgent) + ? agentLaunchProjectRuntime( + store, + workspace, + plannedAgent ?? opts.launchAgent, + host.windowSpawnsPane + ) : resolveLocalProjectRuntimeForWorktreeId(store, workspace.id) : undefined, cwd: resolveTerminalStartupCwd(workspace.path, opts.cwd) ?? workspace.path, @@ -109,7 +116,13 @@ export async function resolveAgentTerminalCreateOptions( const runtime = opts.launchRuntime ?? (plannedAgent - ? resolveAgentLaunchRuntime(store, workspace, opts.shellOverride, plannedAgent) + ? resolveAgentLaunchRuntime( + store, + workspace, + opts.shellOverride, + plannedAgent, + host.windowSpawnsPane + ) : undefined) const launch = plannedAgent && runtime