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