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.
This commit is contained in:
Brennan Benson
2026-10-12 03:04:19 -07:00
parent 33c65ab0fe
commit 3e4311db19
11 changed files with 83 additions and 19 deletions
@@ -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'])
})
@@ -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)
@@ -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,
@@ -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,
@@ -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.
@@ -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')
@@ -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<string, string>
@@ -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,
@@ -237,9 +237,11 @@ export class OrcaRuntimeWithResolveWorktreeRemovalTarget extends OrcaRuntimeWith
protected async resolveAgentTerminalCreateOptions(
workspace: TerminalWorkspaceLaunchScope,
opts: TerminalCreateOptions
opts: TerminalCreateOptions,
windowSpawnsPane = false
): Promise<TerminalCreateOptions> {
return resolveAgentTerminalCreateOptions(workspace, opts, {
windowSpawnsPane,
store: this.store,
hostIdentity: this.runtimeId,
toSessionOptions: (preferences) => this.toAgentSessionOptions(preferences)
@@ -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. */
@@ -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')
@@ -40,6 +40,8 @@ type AgentTerminalPlanHost = {
toSessionOptions: (
preferences: AgentLaunchPreferences | undefined
) => Record<string, SessionOptionValue> | 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