fix(wsl): main's background terminal starts never start a stopped WSL distro (#35865)

* fix(wsl): main's background terminal starts never start a stopped WSL distro

Every start main makes through the runtime controller is background work unless its caller names the user's act (CLI and window creates and splits, paired-client creates and tab opens, agent launches and sessions, conversation resumes). #35813's hold now also holds those fresh and restored starts while the distro is stopped or unverifiable.

* fix(wsl): headless Run now starts as the user's act; update create expectations

A headless automation's Run now names the user's act through launchAgentTerminal, so it may start a stopped WSL distro; a scheduled run stays background and is held. The paired-client background create expectation now includes wslUserAct.

* fix(wsl): main's background start checks a folder workspace's path passively

With #35899's folder stat gate, the runtime preflight passes passiveWslRestore = !wslUserAct, so a background start never stats a stopped distro's folder as a user act before the daemon hold.
This commit is contained in:
Neil
2026-10-29 03:54:50 -07:00
committed by GitHub
parent a5bb1ae9ce
commit 211bbbb6d1
34 changed files with 239 additions and 58 deletions
@@ -68,7 +68,12 @@ function automation(overrides: Record<string, unknown>) {
}
}
const RUN = { id: 'run-1', title: 'Nightly', scheduledFor: Date.UTC(2026, 9, 9, 3) }
const RUN = {
id: 'run-1',
title: 'Nightly',
scheduledFor: Date.UTC(2026, 9, 9, 3),
trigger: 'scheduled' as const
}
const TARGET = { ok: true, repo: { id: 'repo-1', path: '/repo', displayName: 'repo' } }
function previousRun(overrides: Partial<AutomationRun> = {}): AutomationRun {
@@ -100,7 +105,8 @@ function previousRun(overrides: Partial<AutomationRun> = {}): AutomationRun {
function launch(
runtime: ReturnType<typeof harness>,
overrides: Record<string, unknown>,
runs: AutomationRun[] = []
runs: AutomationRun[] = [],
run: typeof RUN | (Omit<typeof RUN, 'trigger'> & { trigger: 'manual' }) = RUN
) {
return launchHeadlessAutomationAgent(
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the launch reads only the members faked in harness().
@@ -108,7 +114,7 @@ function launch(
{
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only these automation fields are read.
automation: automation(overrides) as never,
run: RUN,
run,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only the repo is read.
target: TARGET as never,
runs
@@ -117,6 +123,20 @@ function launch(
}
describe('headless automation agent launch', () => {
// E9: the runtime holds a background start on a stopped WSL distro; only Run now names the user.
it("starts Run now as the user's act, and a scheduled run as background work", async () => {
const scheduled = harness()
await launch(scheduled, {})
expect(scheduled.launchAgentTerminal.mock.calls[0]?.[1]).not.toHaveProperty('wslUserAct')
const manual = harness()
await launch(manual, {}, [], { ...RUN, trigger: 'manual' })
expect(manual.launchAgentTerminal).toHaveBeenCalledWith(
'id:wt-1',
expect.objectContaining({ wslUserAct: true })
)
})
it('starts a post-start agent bare and delivers its prompt once, after the terminal is recorded', async () => {
const runtime = harness()
let finishDelivery: (delivered: boolean) => void = () => {}
@@ -30,7 +30,7 @@ export async function launchHeadlessAutomationAgent(
runtime: OrcaRuntimeService,
request: {
automation: Automation
run: Pick<AutomationRun, 'id' | 'title' | 'scheduledFor'>
run: Pick<AutomationRun, 'id' | 'title' | 'scheduledFor' | 'trigger'>
target: Extract<AutomationRunTargetResult, { ok: true }>
runs: readonly AutomationRun[]
}
@@ -103,7 +103,9 @@ export async function launchHeadlessAutomationAgent(
prompt: startupPrompt ?? '',
title: run.title,
suppressStartupUpdatePrompt: true,
...(automation.extraAgentArgs ? { extraAgentArgs: automation.extraAgentArgs } : {})
...(automation.extraAgentArgs ? { extraAgentArgs: automation.extraAgentArgs } : {}),
// Run now is the user's act and may start a stopped WSL distro; a scheduled run never does.
...(run.trigger === 'scheduled' ? {} : { wslUserAct: true as const })
})
terminal = launched
const shown = await runtime.showManagedWorktree(`id:${launched.worktreeId}`)
@@ -70,8 +70,8 @@ describe('headless automation dispatch', () => {
workspaceId: 'wt-1',
agentId: 'goose'
} as never,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only the title is read.
run: { title: 'Nightly' } as never,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: only the title and trigger are read.
run: { title: 'Nightly', trigger: 'scheduled' } as never,
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: unused for an existing workspace.
target: {} as never
})
@@ -104,4 +104,69 @@ describe('a passive restore of a WSL bridge pane (E9)', () => {
expect(spawns).toBe(1)
expect(readState).not.toHaveBeenCalled()
})
describe('a background start from main (orchestration, automations, recovery)', () => {
const fresh = () =>
adapter.spawn({
cols: 80,
rows: 24,
cwd: WSL_CWD,
isNewSession: true,
wslBackgroundStart: true
})
it('holds a new terminal too, starting nothing, while the distro is stopped', async () => {
const error = await fresh().catch((caught: unknown) => caught)
expect(error instanceof Error ? parseWslDistroHold(error.message) : null).toEqual({
distro: 'Ubuntu',
state: 'stopped'
})
expect(spawns).toBe(0)
})
it('holds it as unverifiable when the read cannot tell', async () => {
readState.mockResolvedValue('unverifiable')
const error = await fresh().catch((caught: unknown) => caught)
expect(error instanceof Error ? parseWslDistroHold(error.message) : null).toEqual({
distro: 'Ubuntu',
state: 'unverifiable'
})
expect(spawns).toBe(0)
})
it('holds its restore of a saved pane', async () => {
const held = await adapter
.spawn({
cols: 80,
rows: 24,
cwd: WSL_CWD,
sessionId: SESSION_ID,
wslBackgroundStart: true
})
.catch((caught: unknown) => caught)
expect(held instanceof Error ? parseWslDistroHold(held.message) : null).toEqual({
distro: 'Ubuntu',
state: 'stopped'
})
expect(spawns).toBe(0)
})
it('starts once a settled read proves the distro running', async () => {
readState.mockResolvedValue('running')
await fresh()
expect(spawns).toBe(1)
})
it('leaves a non-WSL start alone', async () => {
await adapter.spawn({
cols: 80,
rows: 24,
cwd: 'C:\\repo',
isNewSession: true,
wslBackgroundStart: true
})
expect(spawns).toBe(1)
expect(readState).not.toHaveBeenCalled()
})
})
})
@@ -3,21 +3,22 @@ import { wslDistroHoldMessage } from '../../shared/wsl-distro-stopped-hold'
import type { PtySpawnOptions } from '../providers/pty-provider-contract'
/**
* Launch restore, a reload or a remount the user did not ask for would start a stopped distro's
* `wsl.exe` (E9). It starts only when a settled read proves the distro running; a stopped or
* unanswered read holds the pane until the user's Start.
* Launch restore, a reload, a remount or main's background start (orchestration, automations,
* recovery) the user did not ask for would start a stopped distro's `wsl.exe` (E9). It starts
* only when a settled read proves the distro running; a stopped or unanswered read holds it
* until the user's Start.
*/
export async function holdPassiveRestoreOfStoppedWslDistro(
opts: Pick<PtySpawnOptions, 'wslPassiveRestore' | 'sessionId' | 'isNewSession' | 'attachOnly'>,
opts: Pick<
PtySpawnOptions,
'wslPassiveRestore' | 'wslBackgroundStart' | 'sessionId' | 'isNewSession' | 'attachOnly'
>,
wslDistro: string | undefined
): Promise<void> {
if (
opts.wslPassiveRestore !== true ||
!wslDistro ||
!opts.sessionId ||
opts.isNewSession === true ||
opts.attachOnly === true
) {
const passiveRestore =
opts.wslPassiveRestore === true && Boolean(opts.sessionId) && opts.isNewSession !== true
// Background work holds fresh starts too: a new session's `wsl.exe` starts the distro as well.
if (!wslDistro || opts.attachOnly === true || !(passiveRestore || opts.wslBackgroundStart)) {
return
}
const state = await readWslDistroRunningState(wslDistro, { retryUnverifiable: true })
@@ -124,6 +124,10 @@ export async function buildRuntimePtySpawnOptions(
if (args.signal) {
ctx.spawnOptions.signal = args.signal
}
// Default-deny (E9): a main-side start is background work unless its caller names the user's act.
if (!ctx.preAdoptedStablePane && args.wslUserAct !== true) {
ctx.spawnOptions.wslBackgroundStart = true
}
if (
args.onPtySpawnCommitted &&
(ctx.provider instanceof LocalPtyProvider || routesFreshSpawnsToLocalProvider(ctx.provider))
@@ -104,6 +104,30 @@ describe('runtime pty spawn options: declared spawn intent', () => {
})
})
describe("runtime pty spawn options: the user's act on a WSL distro (E9)", () => {
it("checks a folder workspace's path passively unless the start is the user's act", async () => {
const admit = vi.fn<PtyRuntimeControllerDeps['assertWorkspaceAcceptsNewPty']>(() => undefined)
for (const args of [{}, { wslUserAct: true as const }]) {
const ctx = createRuntimePtySpawnState(
{ ...makeDeps(), assertWorkspaceAcceptsNewPty: admit },
{ cols: 120, rows: 40, worktreeId: 'wt-1', ...args }
)
await prepareRuntimePtySpawn(ctx)
ctx.finishTerminalInstall()
}
expect(admit.mock.calls.map((call) => call[3])).toEqual([true, false])
})
it('marks every main-side start as background work unless its caller names the user', async () => {
await expect(resolveSpawnOptions({ cols: 120, rows: 40 })).resolves.toMatchObject({
wslBackgroundStart: true
})
await expect(
resolveSpawnOptions({ cols: 120, rows: 40, wslUserAct: true })
).resolves.not.toHaveProperty('wslBackgroundStart')
})
})
/** Runtime --shell must choose the provider shell, not type its name into the host default shell. */
describe('runtime pty spawn preflight: requested shell on a local Windows host', () => {
afterEach(() => {
+3 -1
View File
@@ -33,7 +33,9 @@ export async function prepareRuntimePtySpawn(
const admission = ctx.deps.assertWorkspaceAcceptsNewPty(
args.worktreeId,
args.sessionId,
localPtySpawnCwd(args)
localPtySpawnCwd(args),
// Main's background start must not stat a stopped distro's folder as a user act (E9).
args.wslUserAct !== true
)
if (admission) {
await admission
+2
View File
@@ -129,6 +129,8 @@ export type RuntimePtySpawnArgs = {
resumeProviderSession?: AgentProviderSessionMetadata
/** `saved`: built from a pane's saved facts, which the host refuses for a chat's conversation. */
resumeOrigin?: 'saved'
/** The user's own act (a CLI command, a paired client's create, a launch); without it a stopped WSL distro holds the start. */
wslUserAct?: true
connectionId?: string | null
worktreeId?: string
preAllocatedHandle?: string
@@ -97,6 +97,8 @@ export type PtySpawnOptions = {
spawnIntent?: SpawnIntent
/** A restore no user act asked for: a WSL bridge pane whose distro is stopped is held, not started. */
wslPassiveRestore?: true
/** A main-side start no user act asked for (orchestration, automations, recovery): held like a passive restore. */
wslBackgroundStart?: true
agentSessionEnsure?: {
claim: AgentSessionExecutionClaim
surface: AgentSessionSurfaceBinding
@@ -168,6 +168,8 @@ export abstract class OrcaRuntimeWithCreateAgentSession extends OrcaRuntimeWithG
}
try {
terminal = await this.createTerminal(`id:${workspace.id}`, {
// Only the agent-session RPC reaches here: the caller's own act.
wslUserAct: true,
// No requested surface: the host-planned launch uses unknown.
startupAgent: request.agent,
...(request.prompt ? { startupPrompt: request.prompt } : {}),
@@ -51,14 +51,20 @@ export const spawnShellInputs = ({
...(launchRuntime ? { launchRuntime } : {})
})
/** Abort disconnected callers, refuse sleeping workspaces, and pass fork intent for host validation. */
/** Abort disconnected callers, refuse sleeping workspaces, and pass fork intent and the user's act for host validation. */
export function callerSpawnScope(
opts: Pick<TerminalCreateOptions, 'signal' | 'refuseSleptWorktree' | 'spawnIntent'>
): { signal?: AbortSignal; refuseSleptWorktree?: true; spawnIntent?: SpawnIntent } {
opts: Pick<TerminalCreateOptions, 'signal' | 'refuseSleptWorktree' | 'spawnIntent' | 'wslUserAct'>
): {
signal?: AbortSignal
refuseSleptWorktree?: true
spawnIntent?: SpawnIntent
wslUserAct?: true
} {
return {
...(opts.signal ? { signal: opts.signal } : {}),
...(opts.refuseSleptWorktree ? { refuseSleptWorktree: true } : {}),
...(opts.spawnIntent ? { spawnIntent: opts.spawnIntent } : {})
...(opts.spawnIntent ? { spawnIntent: opts.spawnIntent } : {}),
...(opts.wslUserAct ? { wslUserAct: true } : {})
}
}
@@ -128,6 +128,7 @@ describe('agent.launch with the real floating workspace resolver', () => {
expect(createTerminal).toHaveBeenCalledExactlyOnceWith(
`id:${FLOATING_TERMINAL_WORKTREE_ID}`,
{
wslUserAct: true,
startupAgent: 'codex',
terminalImageViewerSupported: false,
// Derived on the host by the window's rule, from the same chat-view setting.
@@ -140,6 +140,8 @@ export function agentLaunchSurfaceFactory(
terminalSpawn.rethrow(new AgentLaunchTabClosedError())
}
const created = context.runtime.createTerminal(`id:${worktreeId}`, {
// An agent launch the caller asked for (CLI, window or paired client) is the user's act.
wslUserAct: true,
// Let the runtime build the configured launcher: agent ids are not commands (cursor uses cursor-agent).
startupAgent: agent,
terminalImageViewerSupported: terminalImageViewerSupported(context),
@@ -546,6 +546,7 @@ describe('the terminal factory', () => {
const result = await launch(CREATE_LAUNCH, runtime)
expect(runtime.createTerminal).toHaveBeenCalledWith('id:wt-new', {
wslUserAct: true,
startupAgent: 'claude',
terminalImageViewerSupported: false,
viewMode: 'terminal',
@@ -571,6 +572,7 @@ describe('the terminal factory', () => {
expect(runtime.showManagedTerminalWorkspace).not.toHaveBeenCalled()
// Normalize selectors to ids before lower layers add their own id: prefix.
expect(runtime.createTerminal).toHaveBeenCalledWith('id:wt-7', {
wslUserAct: true,
startupAgent: 'gemini',
terminalImageViewerSupported: false,
viewMode: 'terminal',
@@ -231,6 +231,7 @@ describe('agent session RPC methods', () => {
expect.any(Function)
)
expect(createTerminal).toHaveBeenCalledWith('id:worktree-1', {
wslUserAct: true,
command: 'codex resume provider-session-1',
startupCommandDelivery: undefined,
env: undefined,
@@ -86,6 +86,8 @@ export const TERMINAL_LIFECYCLE_METHODS = [
params.reconcileExisting === true,
(canonicalWorktreeSelector, preAllocatedHandle) =>
runtime.createTerminal(canonicalWorktreeSelector, {
// A CLI command (typed or an agent's) or a window's create is the user's act.
wslUserAct: true,
command: params.command,
terminalImageViewerSupported: terminalImageViewerSupported(context),
...(params.shell ? { shellOverride: params.shell } : {}),
@@ -149,7 +151,8 @@ export const TERMINAL_LIFECYCLE_METHODS = [
command: params.command,
env: params.env,
// The window's split shows why its start failed, as its own spawn did.
keepPaneOnStartFailure: true
keepPaneOnStartFailure: true,
wslUserAct: true
})
: // The schema requires a handle when no pane is named.
await runtime.splitTerminal(params.terminal ?? '', {
@@ -157,7 +160,8 @@ export const TERMINAL_LIFECYCLE_METHODS = [
command: params.command,
env: params.env,
telemetrySource: params.telemetrySource,
retrySplit: params.retrySplit
retrySplit: params.retrySplit,
wslUserAct: true
})
})
}),
@@ -88,6 +88,8 @@ export type RuntimePtyController = {
sessionId?: string
/** Windows shell to spawn AS this PTY, instead of the host default. */
shellOverride?: string
/** The user's own act (a CLI command, a paired client's create, a launch); without it a stopped WSL distro holds the start. */
wslUserAct?: true
/** The agent launch's runtime fact; when present the PTY shell comes only from it. */
launchRuntime?: AgentLaunchRuntime
isNewSession?: boolean
@@ -103,6 +103,8 @@ export type TerminalCreateOptions = {
agentSessionCreateOperationId?: string
/** The paired client's declared intent; the host's fork gate validates and may defer it. */
spawnIntent?: SpawnIntent
/** The user's own act (a CLI command, a paired client's create, a launch); without it a stopped WSL distro holds the start. */
wslUserAct?: true
signal?: AbortSignal
/** Refuse (`worktree_terminals_sleeping`) instead of waking a worktree the host has put to sleep. */
refuseSleptWorktree?: boolean
@@ -156,6 +156,8 @@ export abstract class OrcaRuntimeWithGetAgentSessionExecutionNamespace extends O
throw new Error('client_disconnected')
}
const terminal = await this.createTerminal(`id:${workspace.id}`, {
// Only the agent-session RPC reaches here: the caller's own act.
wslUserAct: true,
command: startup.launchCommand,
env: withAgentSessionAccountEnv(startup.env, request.accountEnv),
launchConfig: startup.launchConfig,
@@ -183,7 +183,13 @@ export abstract class OrcaRuntimeWithPerformMobileSessionPtyRecordsRefresh exten
const targetGroupId = snapshot?.tabGroups?.find((group) =>
group.tabOrder.includes(tab.parentTabId)
)?.id
const trigger = { automatic, targetsHost, targetGroupId }
// An explicit tab open is the user's act; an automatic probe never starts a stopped distro.
const trigger = {
automatic,
targetsHost,
targetGroupId,
...(automatic ? {} : { wslUserAct: true as const })
}
let startupCwdFallback: RuntimeMobileSessionTabActivationResult['startupCwdFallback']
try {
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
@@ -199,7 +199,8 @@ export abstract class OrcaRuntimeWithRestoredPaneStart extends OrcaRuntimeWithBu
automatic = false,
targetsHost = false,
targetGroupId,
sessionId
sessionId,
wslUserAct
}: RestoredPaneTrigger & { sessionId?: string }
): Promise<RuntimeMobileSessionCreateTerminalResult> {
// Recover undelivered startup commands (#7587) or recorded agent sessions (#27147, #27777); live-session attach skips startup commands, preventing double launches.
@@ -262,6 +263,7 @@ export abstract class OrcaRuntimeWithRestoredPaneStart extends OrcaRuntimeWithBu
: {}),
launchRuntime: agentStartup.launchRuntime,
targetGroupId,
...(wslUserAct ? { wslUserAct } : {}),
onPtySpawnDispatched: () => {
dispatched = true
},
@@ -170,6 +170,8 @@ export abstract class OrcaRuntimeWithResumeAgentSessionInWorkspace extends OrcaR
false,
undefined,
{
// A resume the user asked for (a vault or external-conversation reopen).
wslUserAct: true,
command: resume.command,
env: resume.env,
startupCommandDelivery: resume.startupCommandDelivery,
@@ -170,7 +170,9 @@ export abstract class OrcaRuntimeWithFailedStartPanes extends OrcaRuntimeWithCre
...opts,
...pane,
presentation: 'background',
keepPaneOnStartFailure: true
keepPaneOnStartFailure: true,
// The window's own new tab: a user act, as New Terminal is for a window spawn.
wslUserAct: true
})
}
@@ -1,8 +1,8 @@
import {
OrcaRuntimeWithCreateMobileSessionTerminal,
type MobileTerminalCreateOptions,
type OwnedMobileTerminalCreateOptions
} from './mobile-terminal-create'
import type { MobileTerminalCreateOptions } from './mobile-terminal-renderer-create'
import type { TerminalWorkspaceLaunchScope } from '../../runtime-legacy-worker-terminal-recovery-types'
import type { RuntimeMobileSessionCreateTerminalResult } from '../../../../shared/runtime-types'
import {
@@ -157,6 +157,8 @@ export abstract class OrcaRuntimeWithMobileChatForkCreate extends OrcaRuntimeWit
opts.activate !== false,
phoneAnchor,
{
// The phone user's own fork.
wslUserAct: true,
...startup,
identity: {
tabId: deterministicAgentSessionUuid(`mobile-chat-fork-tab\0${seed}`),
@@ -1,4 +1,3 @@
import type { MobileTerminalCreateOptions } from './mobile-terminal-renderer-create'
import type { RuntimeMobileSessionTabsResult } from '../../../../shared/runtime-session-contracts'
import { OrcaRuntimeWithFailedStartPanes } from './failed-start-pane'
import type { WorktreeStartupLaunch } from '../../../../shared/worktree/launch-types'
@@ -19,7 +18,33 @@ import { navigationTargetsHost } from '../../../../shared/runtime-navigation'
import { MOBILE_TERMINAL_CREATE_RESULT_TTL_MS } from '../../orca-runtime-core'
import { seedResumeLaunchIdentity } from '../../../ipc/pty/pane/resume-launch-identity'
/** A paired client's terminal create request. */
export type MobileTerminalCreateOptions = {
terminalImageViewerSupported?: boolean
afterTabId?: string
sourceChatFork?: { sessionId: string }
targetGroupId?: string
command?: string
shellOverride?: string
cwd?: string
env?: Record<string, string>
envToDelete?: string[]
startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery']
agent?: TuiAgent
agentPrompt?: string
launchConfig?: SleepingAgentLaunchConfig
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
activate?: boolean
clientNavigationId?: string
clientMutationId?: string
supportsSplitGroupPlacement?: boolean
signal?: AbortSignal
}
export type OwnedMobileTerminalCreateOptions = {
/** A paired client's own create or tab open; without it a stopped WSL distro holds the start. */
wslUserAct?: true
command?: string
shellOverride?: string
cwd?: string
@@ -103,6 +103,7 @@ export abstract class OrcaRuntimeWithCreateRuntimeOwnedMobileSessionTerminal ext
? { resumeProviderSession: opts.resumeProviderSession }
: {}),
...(opts.spawnIntent ? { spawnIntent: opts.spawnIntent } : {}),
...(opts.wslUserAct ? { wslUserAct: true } : {}),
...(opts.resumeOrigin ? { resumeOrigin: opts.resumeOrigin } : {}),
...(opts.launchAgent ? { launchAgent: opts.launchAgent } : {}),
...(opts.launchRuntime ? { launchRuntime: opts.launchRuntime } : {}),
@@ -3,9 +3,6 @@ import type { MobileTerminalCommandOptions, MobileTerminalCommand } from './mobi
import type { RuntimePtyWorktreeRecord } from '../../runtime-terminal-state-records'
import type { TerminalWorkspaceLaunchScope } from '../../runtime-legacy-worker-terminal-recovery-types'
import { OrcaRuntimeWithMobileChatForkCreate } from './mobile-terminal-chat-fork-create'
import type { WorktreeStartupLaunch } from '../../../../shared/worktree/launch-types'
import type { TuiAgent } from '../../../../shared/tui-agent'
import type { SleepingAgentLaunchConfig } from '../../../../shared/agent-session-resume'
import type { RuntimeMobileSessionCreateTerminalResult } from '../../../../shared/runtime-types'
import { randomUUID } from 'node:crypto'
import { getRuntimeDesktopSurface } from '../../runtime-desktop-surface'
@@ -19,29 +16,7 @@ import { rendererPublicationThrottle } from '../../../window/renderer-publicatio
import { deterministicAgentSessionUuid } from '../../runtime-agent-launch-resolution'
import { assertMobileTerminalShellSupported } from './mobile-terminal-shell-refusal'
import { anchorTabOfCreate, mainPlacedAfter } from './mobile-terminal-create-anchor'
export type MobileTerminalCreateOptions = {
terminalImageViewerSupported?: boolean
afterTabId?: string
sourceChatFork?: { sessionId: string }
targetGroupId?: string
command?: string
shellOverride?: string
cwd?: string
env?: Record<string, string>
envToDelete?: string[]
startupCommandDelivery?: WorktreeStartupLaunch['startupCommandDelivery']
agent?: TuiAgent
agentPrompt?: string
launchConfig?: SleepingAgentLaunchConfig
launchAgent?: TuiAgent
viewMode?: 'terminal' | 'chat'
activate?: boolean
clientNavigationId?: string
clientMutationId?: string
supportsSplitGroupPlacement?: boolean
signal?: AbortSignal
}
import type { MobileTerminalCreateOptions } from './mobile-terminal-create'
export abstract class OrcaRuntimeWithRunCreateMobileSessionTerminal extends OrcaRuntimeWithMobileChatForkCreate {
protected abstract resolveMobileSessionTerminalCommand(
@@ -130,6 +105,8 @@ export abstract class OrcaRuntimeWithRunCreateMobileSessionTerminal extends Orca
opts.activate !== false,
afterTabId,
{
// The paired client's own create: its user's act.
wslUserAct: true,
...startupCommand,
cwd,
identity: {
@@ -151,6 +128,7 @@ export abstract class OrcaRuntimeWithRunCreateMobileSessionTerminal extends Orca
opts.activate !== false,
afterTabId,
{
wslUserAct: true,
command: startupCommand.command,
...(opts.shellOverride ? { shellOverride: opts.shellOverride } : {}),
cwd,
@@ -277,6 +255,7 @@ export abstract class OrcaRuntimeWithRunCreateMobileSessionTerminal extends Orca
opts.activate !== false,
afterTabId,
{
wslUserAct: true,
command: startupCommand.command,
...(opts.shellOverride ? { shellOverride: opts.shellOverride } : {}),
cwd,
@@ -156,6 +156,8 @@ export abstract class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRun
title?: string
extraAgentArgs?: string
suppressStartupUpdatePrompt?: boolean
/** Run now is the user's act; a scheduled run is background and a stopped WSL distro holds it. */
wslUserAct?: true
}
): Promise<RuntimeTerminalCreate> {
const worktree = await this.resolveGitWorktreeSelector(worktreeSelector)
@@ -184,7 +186,8 @@ export abstract class OrcaRuntimeWithTerminalCreateDeduplication extends OrcaRun
launchRuntime: startup.launchRuntime,
startupCommandDelivery: startup.startup.startupCommandDelivery,
telemetry: startup.startup.telemetry,
title: opts.title
title: opts.title,
...(opts.wslUserAct ? { wslUserAct: true } : {})
})
// Why: agents that read the prompt after they start get it typed in, not on argv.
if (startup.followup) {
@@ -27,6 +27,8 @@ export type SplitPaneInMainOptions = {
activate?: boolean
/** The window's own split keeps a pane whose start failed, showing why, as its spawn did. */
keepPaneOnStartFailure?: boolean
/** The user's own act (a CLI or window split); without it a stopped WSL distro holds the start. */
wslUserAct?: true
}
export abstract class OrcaRuntimeWithSplitPaneInMain extends OrcaRuntimeWithSplitPtyBackedTerminal {
@@ -109,6 +111,7 @@ export abstract class OrcaRuntimeWithSplitPaneInMain extends OrcaRuntimeWithSpli
...opts.env
}),
envToDelete: opts.envToDelete,
...(opts.wslUserAct ? { wslUserAct: true } : {}),
connectionId: workspace.connectionId,
worktreeId: workspace.id,
preAllocatedHandle,
@@ -22,6 +22,8 @@ export type TerminalSplitOptions = {
// workspace, for splits the user never asked to see.
surfaceOwner?: false
telemetrySource?: TerminalPaneSplitSource
/** The user's own act (a CLI or window split); without it a stopped WSL distro holds the start. */
wslUserAct?: true
}
export abstract class OrcaRuntimeWithSplitPtyBackedTerminal extends OrcaRuntimeWithSplitTerminal {
@@ -78,6 +80,7 @@ export abstract class OrcaRuntimeWithSplitPtyBackedTerminal extends OrcaRuntimeW
envToDelete: opts.envToDelete,
connectionId: workspace.connectionId,
worktreeId: workspace.id,
...(opts.wslUserAct ? { wslUserAct: true } : {}),
preAllocatedHandle,
tabId: parentTabId,
leafId,
@@ -45,6 +45,8 @@ export abstract class OrcaRuntimeWithSplitTerminal extends OrcaRuntimeWithStopEx
telemetrySource?: TerminalPaneSplitSource
/** The leaf id a timed-out split reported: wait for that pane instead of splitting again. */
retrySplit?: string
/** The user's own act (a CLI or window split); without it a stopped WSL distro holds the start. */
wslUserAct?: true
} = {},
// Internal creation evidence; RPC and preload callers never supply it.
createdWorktree?: Worktree
@@ -76,7 +78,8 @@ export abstract class OrcaRuntimeWithSplitTerminal extends OrcaRuntimeWithStopEx
command: opts.command,
env: opts.env,
envToDelete: opts.envToDelete,
activate: opts.activate
activate: opts.activate,
...(opts.wslUserAct ? { wslUserAct: true } : {})
}
)
}
@@ -15,6 +15,8 @@ export type RestoredPaneStartPlan = {
/** Who asked, which the start keeps: an automatic probe is refused for a slept worktree. */
export type RestoredPaneTrigger = {
automatic?: boolean
/** A user's own tab open or Start; without it a stopped WSL distro holds the start. */
wslUserAct?: true
targetsHost?: boolean
targetGroupId?: string
}
@@ -140,6 +140,8 @@ describe('launchAgentBackgroundSession remote runtime and SSH startup delivery',
terminal: { handle: 'prepared-background', worktreeId: 'wt-1', title: null }
})
expect(spawn).toHaveBeenCalledExactlyOnceWith('id:wt-1', {
// A paired client's terminal.create is the user's act (E9).
wslUserAct: true,
command: plan.launchCommand,
env: { ACTION_PROFILE: 'captured' },
envToDelete: undefined,