mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
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:
@@ -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(() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
+2
@@ -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,
|
||||
|
||||
+7
-1
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user