refactor(pty): one spawn preflight step for both launch lanes (#29101)

* test(launch): pin where the two spawn preflights differ, on main

Row 3 (one spawn preflight for both launch lanes) first pins main's behaviour:

- Row 8, agents started with their workspace (from the earlier row-8 work): a work item's
  "start agent" and the folder-workspace composer through the real producer and pane and
  pty:spawn: draft and note on argv, a long multi-line note, PowerShell quoting under cmd.exe,
  project WSL, terminal shell WSL, and a prompt-less Codex start with no shell wait.
- A deleted folder: the window starts in the workspace root and says so; the host passes the
  folder on and the provider refuses it.
- A Windows plain terminal in a folder containing a WSL-project repo, and in a \\wsl$ repo with
  no saved runtime: the window uses the runtime the renderer sends; the host reads only the
  repo's project.
- A managed SSH Windows host: only the host lane opens the OpenSSH login shell.
- A pane's own agent launch drops the root fallback (TEMPORARY parity).

* refactor(pty): one spawn preflight step for both launch lanes

The window's pty:spawn and the host's runtime controller each kept their own copy of the
first spawn step: resolve the folder a new terminal starts in and the shell/runtime its PTY
uses. resolvePtySpawnStartupTarget now does both for both lanes. Each lane passes the two
inputs it still differs on, so nothing changes for users:

- missingCwdFallback (TEMPORARY, row 13a): only window starts fall back to the workspace root,
  with a notice, when the requested folder is gone (including the WSL cwd probe).
- projectRuntimeSource (TEMPORARY, row 13b): the window uses the runtime its renderer sent;
  the host reads the repo's project from the store.

The OpenSSH login shell (#9327) now resolves for both lanes; only an SSH-launched orcad turns
it on, and that process has no window, so the window lane never meets it.

Both lanes resolve after fresh-spawn recovery and session-id mint (the WSL distro reads the
session id). Admission, adoption, hidden mark, recovery and mint stay where they were.
This commit is contained in:
Brennan Benson
2026-10-15 16:21:51 -07:00
committed by GitHub
parent 09a1465060
commit 5b7845e36f
11 changed files with 680 additions and 172 deletions
+22 -2
View File
@@ -1,6 +1,7 @@
import { afterEach, describe, expect, it, vi } from 'vitest'
import type { AgentPaneLaunchRequest } from '../../shared/agent-pane-launch-request'
import { setupPtyIpcSuite } from './pty-ipc-test-harness'
import { statSyncMock } from './pty-ipc-mock-registry'
import { registerPtyHandlers } from './pty/register-handlers'
import {
markAgentLaunchesClosedByUser,
@@ -108,7 +109,6 @@ describe('a pane that starts its own agent launch', () => {
): Promise<unknown> {
return Promise.resolve(
handlers.get('pty:spawn')!(null, {
...pane,
cols: 132,
rows: 40,
cwd: '/tmp/pane-launch',
@@ -116,7 +116,8 @@ describe('a pane that starts its own agent launch', () => {
tabId,
leafId,
command: '',
agentLaunch
agentLaunch,
...pane
})
)
}
@@ -181,6 +182,25 @@ describe('a pane that starts its own agent launch', () => {
)
})
// TEMPORARY parity (row 3 pin; spawn-agent-launch.ts drops cwdFallback): a launch into a deleted
// folder fails there, as the host's launch does, instead of starting in the workspace root.
it('starts in the requested folder even when it is gone, without the root fallback', async () => {
const missing = '/tmp/pane-launch/deleted'
statSyncMock.mockImplementation((target: string) => {
if (target === missing) {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
}
return { isDirectory: () => true, mode: 0o755 }
})
const providerSpawn = vi.fn(async (_options: { cwd?: string }) => ({ id: 'pty-agent' }))
register(providerSpawn)
const result = await spawnPane(AI_BUTTON, { cwd: missing, cwdFallback: 'worktree' })
expect(providerSpawn.mock.calls[0]?.[0].cwd).toBe(missing)
expect(result).not.toHaveProperty('startupCwdFallback')
})
it('starts one agent when the pane spawns twice at once', async () => {
const providerSpawn = vi.fn(async () => ({ id: 'pty-agent' }))
register(providerSpawn)
@@ -146,7 +146,11 @@ export function startParityLanes(
): ParityLanes {
const settings = parityMainSettings(args.settings)
const persistPtyBinding = vi.fn(async () => true)
const { repo, folderWorkspace } = launchScope(args.workspace)
const { repo: scopeRepo, folderWorkspace } = launchScope(args.workspace)
// A folder's contained repo is saved too, so the host can borrow its runtime if it would.
const repo =
scopeRepo ??
(args.workspace.repoPath ? launchScope({ kind: 'repo', path: args.workspace.repoPath }).repo : null)
const store = {
getSettings: () => settings,
getProjects: () => [
+119 -4
View File
@@ -1,6 +1,6 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { setupPtyIpcSuite } from './pty-ipc-test-harness'
import { classifyErrorMock, trackMock } from './pty-ipc-mock-registry'
import { classifyErrorMock, statSyncMock, trackMock } from './pty-ipc-mock-registry'
import {
providerFacts,
recordOf,
@@ -11,9 +11,12 @@ import {
} from './pty-launch-parity.test-fixture'
import {
POSIX_PATH,
WINDOW_LAUNCH_CASES
WIN_PATH,
WINDOW_LAUNCH_CASES,
WSL_PATH
} from '../../shared/launch-parity-window-cases.test-fixture'
import { PLUS_LAUNCH_CASES } from '../../shared/launch-parity-plus-cases.test-fixture'
import { WORKSPACE_START_LAUNCH_CASES } from '../../shared/launch-parity-workspace-start-cases.test-fixture'
import {
expectedWindowProvider,
launchWorkspaceId,
@@ -53,6 +56,7 @@ import {
resolveOpenCodeDirectModelExecutable
} from '../opencode/opencode-model-availability'
import { probeOpenCodeLaunchModelContext } from '../opencode/opencode-launch-model-context'
import { readOpenSshDefaultShell } from '../../shared/openssh-default-shell'
vi.mock('electron', () => import('./pty-ipc-mock-registry').then((m) => m.electronModuleMock()))
vi.mock('fs', () => import('./pty-ipc-mock-registry').then((m) => m.fsModuleMock()))
@@ -120,6 +124,8 @@ vi.mock('../antigravity/native-account-launch', () => ({
vi.mock('../codex/codex-state-db-backfill-recovery', () =>
import('./pty-ipc-mock-registry').then((m) => m.codexBackfillRecoveryModuleMock())
)
// Why: the OpenSSH DefaultShell registry read; only a managed SSH host's case answers it.
vi.mock('../../shared/openssh-default-shell', () => ({ readOpenSshDefaultShell: vi.fn(() => '') }))
type WindowLaneCase = {
label: string
@@ -133,7 +139,7 @@ type WindowLaneCase = {
}
const WINDOW_LANE_CASES: WindowLaneCase[] = [
...[...WINDOW_LAUNCH_CASES, ...PLUS_LAUNCH_CASES].map((c) => ({
...[...WINDOW_LAUNCH_CASES, ...PLUS_LAUNCH_CASES, ...WORKSPACE_START_LAUNCH_CASES].map((c) => ({
...c,
label: `row ${c.row}: ${c.name}`,
request: windowSpawnRequest(c),
@@ -147,7 +153,7 @@ const WINDOW_LANE_CASES: WindowLaneCase[] = [
}))
]
// Pins main's current launch behaviour as the convergence parity baseline (rows 1, 2, 5 and 6, main
// Pins main's current launch behaviour as the convergence parity baseline (rows 1, 2, 5, 6 and 8, main
// half): each table request, fed to pty:spawn, reaches the provider with these facts (the pane's
// measured size, never hidden), one agent_started, and a pane binding.
// C2 (row 1), C5a-e and C6 check the merged spawn pipeline against it.
@@ -451,3 +457,112 @@ describe('launch facts outside the tables', () => {
})
})
})
const WSL_UBUNTU = { kind: 'wsl', distro: 'Ubuntu' } as const
// What the renderer sends for a WSL project (it reads a never-saved \\wsl$ path the same way).
const WSL_PROJECT_SENT = {
status: 'resolved',
runtime: {
kind: 'wsl',
hostPlatform: 'wsl',
projectId: 'repo-1',
distro: 'Ubuntu',
reason: 'project-override',
cacheKey: 'repo-1:wsl:Ubuntu'
}
}
const LOGIN_SHELL = String.raw`C:\Program Files\PowerShell\7\pwsh.exe`
// Pins main's current spawn preflight (row 3, C5b): where the two lanes resolve a plain terminal's
// start differently today. Row 13a removes the missing-folder split; row 13b the runtime split.
describe('spawn preflight: where the lanes differ today', () => {
const suite = setupPtyIpcSuite()
afterEach(() => {
restoreMainPlatform()
vi.unstubAllEnvs()
})
async function spawnBoth(
lanes: ParityLanes,
workspace: LaunchWorkspace,
window: Record<string, unknown>,
host: Record<string, unknown> = {}
): Promise<Record<'window' | 'host', Record<string, unknown>>> {
const worktreeId = launchWorkspaceId(workspace)
const result = recordOf(
await lanes.spawnWindow({ cols: 80, rows: 24, cwd: workspace.path, worktreeId, ...window })
)
const windowFacts = { ...(await providerFacts(lanes.provider)), result }
await lanes.runtime.createTerminal(`id:${worktreeId}`, host)
return { window: windowFacts, host: await providerFacts(lanes.provider) }
}
// The host hands the provider the missing folder, and the provider's own check then refuses it
// ("Working directory ... does not exist").
it('a deleted folder: the window starts in the workspace root and says so; the host does not', async () => {
setMainPlatform('darwin')
const missing = `${POSIX_PATH}/deleted`
statSyncMock.mockImplementation((target: string) => {
if (target === missing) {
throw Object.assign(new Error('ENOENT'), { code: 'ENOENT' })
}
return { isDirectory: () => true, mode: 0o755 }
})
const lanes = startParityLanes(suite, { workspace: POSIX_REPO })
const facts = await spawnBoth(
lanes,
POSIX_REPO,
{ cwd: missing, cwdFallback: 'worktree' },
{ cwd: missing }
)
expect(facts.window).toMatchObject({
cwd: POSIX_PATH,
result: { startupCwdFallback: { kind: 'worktree', cwd: POSIX_PATH } }
})
expect(facts.host).toMatchObject({ cwd: missing })
})
it.each([
{
name: 'a folder containing a WSL-project repo',
workspace: {
kind: 'folder',
path: String.raw`C:\Users\alice`,
repoPath: WIN_PATH,
projectRuntime: WSL_UBUNTU
} satisfies LaunchWorkspace,
host: { shellOverride: 'powershell.exe', terminalWindowsWslDistro: null }
},
{
name: 'a \\wsl$ repo with no saved runtime',
workspace: { kind: 'repo', path: WSL_PATH } satisfies LaunchWorkspace,
// The distro comes from the path, but the shell stays the settings' PowerShell.
host: { shellOverride: 'powershell.exe', terminalWindowsWslDistro: 'Ubuntu' }
}
])('Windows plain terminal, $name: the window uses the sent runtime', async (c) => {
setMainPlatform('win32')
const lanes = startParityLanes(suite, { workspace: c.workspace })
const facts = await spawnBoth(lanes, c.workspace, { projectRuntime: WSL_PROJECT_SENT })
expect(facts.window).toMatchObject({
shellOverride: 'wsl.exe',
terminalWindowsWslDistro: 'Ubuntu'
})
expect(facts.host).toMatchObject(c.host)
})
// One resolution reads it for both lanes. Only an SSH-launched orcad has this env, and it has no
// window (no pty:spawn), so the window lane never meets it in practice.
it('Windows managed SSH host: both lanes open the OpenSSH login shell', async () => {
setMainPlatform('win32')
vi.stubEnv('ORCA_ORCAD_MANAGED_ACTIVATION_ROOT', '/activation')
vi.mocked(readOpenSshDefaultShell).mockReturnValue(LOGIN_SHELL)
const workspace = { kind: 'repo', path: WIN_PATH } satisfies LaunchWorkspace
const lanes = startParityLanes(suite, { workspace })
const facts = await spawnBoth(lanes, workspace, {})
expect(facts.window).toMatchObject({ shellOverride: LOGIN_SHELL })
expect(facts.host).toMatchObject({ shellOverride: LOGIN_SHELL })
})
})
-6
View File
@@ -11,7 +11,6 @@ import { ensureCodexStateDbBackfillRecoveryStarted } from '../../../codex/codex-
import { buildPtyHostEnv } from '../host-env/assembly'
import {
getCompatibleSelectedCodexHomePath,
getCodexSelectionTargetForPty,
resolveCodexHomeAfterManagedAuthReadiness,
shouldSkipCodexHomeEnvForWindowsShell,
shouldStripInheritedOrcaCodexHome,
@@ -29,11 +28,6 @@ export async function assemblePtyIpcSpawnCodexEnv(ctx: PtyIpcSpawnState): Promis
cwd: args.cwd,
shellOverride: ctx.effectiveShellOverride
})
ctx.codexSelectionTarget = getCodexSelectionTargetForPty(
ctx.effectiveShellOverride,
ctx.cwd,
ctx.expectedWslDistro
)
const codexResumePreparation = ctx.preAdoptedStablePane
? null
: ctx.deps.prepareCodexResumeHome({
+25 -121
View File
@@ -1,21 +1,13 @@
import { withClaudeProfileTerminalEnv } from '../../../claude-accounts/claude-profile-installed-router'
import {
isWslShellName,
resolveLocalWindowsTerminalRuntimeOptions
} from '../../../../shared/local-windows-terminal-runtime'
import { isWslUncPath, toWindowsWslPath } from '../../../../shared/wsl-paths'
import { mintPtySessionId } from '../../../daemon/pty-session-id'
import { resolveWslSessionContext } from '../../../daemon/wsl-session-context'
import { LocalPtyProvider } from '../../../providers/local-pty-provider'
import { normalizeWindowsTerminalCwd } from '../../../providers/windows-shell-args'
import { wslUncDirectoryExistsAsync } from '../../../wsl'
import { getCodexSelectionTargetForPty } from '../host-env/codex-home'
import {
isClaudeLaunchCommand,
recoverFreshSpawnProviderRouting,
routesFreshSpawnsToLocalProvider
} from '../host-env/fresh-spawn-routing'
import { getProvider } from '../provider/registry'
import { resolvePtySpawnStartupTarget } from '../spawn-startup-target'
import type { PtyIpcSpawnState } from './spawn-state'
import { getConnectionExecutionHostId } from '../../../../shared/execution-host'
@@ -65,89 +57,6 @@ export async function preparePtyIpcSpawnPreflight(ctx: PtyIpcSpawnState): Promis
await admission
}
}
if (!ctx.preAdoptedStablePane) {
// Why: reattach needs exact cwd, and successful stable-pane adoption needs no launch preflight.
const requestedMissingCwdFallback = !args.sessionId && args.cwdFallback === 'worktree'
const isPosixStartupCwd = args.cwd?.startsWith('/') === true
const startupWorkspaceCwd =
requestedMissingCwdFallback && isPosixStartupCwd
? ctx.deps.resolvePtySpawnStartupCwd(args.worktreeId, '.')
: undefined
const initiallyResolvedStartupCwd =
requestedMissingCwdFallback && isPosixStartupCwd ? ctx.cwd : undefined
const startupTerminalRuntimeOptions =
requestedMissingCwdFallback && process.platform === 'win32'
? resolveLocalWindowsTerminalRuntimeOptions({
requestedShellOverride: args.shellOverride,
settings: ctx.deps.getSettings?.(),
projectRuntime: args.projectRuntime,
fallbackHostShell: process.env.COMSPEC || 'powershell.exe'
})
: undefined
const wslRuntimeOwnsStartupCwd =
requestedMissingCwdFallback &&
isPosixStartupCwd &&
(isWslShellName(startupTerminalRuntimeOptions?.shellOverride) ||
isWslUncPath(startupWorkspaceCwd ?? ''))
const startupWslContext = wslRuntimeOwnsStartupCwd
? resolveWslSessionContext({
cwd: startupWorkspaceCwd,
shellOverride: startupTerminalRuntimeOptions?.shellOverride,
terminalWindowsWslDistro: startupTerminalRuntimeOptions?.terminalWindowsWslDistro
})
: undefined
let wslStartupCwdExists: boolean | null = null
let wslWorkspaceCwdExists: boolean | null = null
if (startupWslContext && initiallyResolvedStartupCwd) {
const validationCwd = toWindowsWslPath(initiallyResolvedStartupCwd, startupWslContext.distro)
wslStartupCwdExists = isWslUncPath(validationCwd)
? await wslUncDirectoryExistsAsync(validationCwd)
: ctx.deps.localStartupCwdDirectoryExists(validationCwd)
if (wslStartupCwdExists === true) {
ctx.prevalidatedCwd = validationCwd
}
if (wslStartupCwdExists === false && startupWorkspaceCwd) {
wslWorkspaceCwdExists = isWslUncPath(startupWorkspaceCwd)
? await wslUncDirectoryExistsAsync(startupWorkspaceCwd)
: ctx.deps.localStartupCwdDirectoryExists(startupWorkspaceCwd)
if (wslWorkspaceCwdExists === true) {
ctx.prevalidatedCwd = startupWorkspaceCwd
}
}
}
const allowMissingCwdFallback =
requestedMissingCwdFallback &&
(!wslRuntimeOwnsStartupCwd ||
(wslStartupCwdExists === false && wslWorkspaceCwdExists === true))
let didFallbackToWorkspaceRootCwd = false
ctx.cwd = ctx.deps.resolvePtySpawnStartupCwd(
args.worktreeId,
args.cwd,
allowMissingCwdFallback
? {
directoryExists: (path) =>
startupWslContext &&
wslStartupCwdExists === false &&
path === initiallyResolvedStartupCwd
? false
: startupWslContext && path === startupWorkspaceCwd
? wslWorkspaceCwdExists === true
: ctx.deps.localStartupCwdDirectoryExists(
process.platform === 'win32' ? normalizeWindowsTerminalCwd(path) : path
),
onFallbackToWorkspaceRoot: () => {
didFallbackToWorkspaceRootCwd = true
}
}
: undefined
)
if (didFallbackToWorkspaceRootCwd && wslWorkspaceCwdExists === true && ctx.cwd) {
ctx.prevalidatedCwd = ctx.cwd
}
ctx.startupCwdFallback =
didFallbackToWorkspaceRootCwd && ctx.cwd ? { kind: 'worktree', cwd: ctx.cwd } : undefined
}
ctx.spawnTiming.mark('preflight')
const freshSpawnRecovery = ctx.preAdoptedStablePane
? undefined
: recoverFreshSpawnProviderRouting(ctx.provider, args.sessionId)
@@ -174,40 +83,35 @@ export async function preparePtyIpcSpawnPreflight(ctx: PtyIpcSpawnState): Promis
}
}
ctx.isClaudeLaunch = !ctx.preAdoptedStablePane && isClaudeLaunchCommand(args.command)
ctx.terminalRuntimeOptions =
ctx.paneAgentLaunch?.launchRuntime?.pty ??
(process.platform === 'win32'
? resolveLocalWindowsTerminalRuntimeOptions({
requestedShellOverride: args.shellOverride,
settings: ctx.deps.getSettings?.(),
projectRuntime: args.projectRuntime,
fallbackHostShell: process.env.COMSPEC || 'powershell.exe'
})
: {
shellOverride:
args.shellOverride ?? (ctx.deps.getSettings?.()?.terminalDefaultShell || undefined),
terminalWindowsWslDistro: null
})
const initialShellOverride = ctx.terminalRuntimeOptions.shellOverride
// Why: daemon host-env setup needs a stable id BEFORE provider.spawn so buildPtyHostEnv hooks/Pi cleanup can run; daemon still honors opts.sessionId ?? mint().
// Note: sessionId is STABLE across daemon restarts by design — do NOT simplify to a fresh UUID per spawn; that orphans reconnectable state.
// Why: only clear ids minted in THIS request on failure — a caller-supplied args.sessionId may name an existing PTY we must not clobber.
ctx.expectedWslDistro =
resolveWslSessionContext({
cwd: ctx.cwd,
sessionId: ctx.effectiveSessionId,
shellOverride: ctx.terminalRuntimeOptions.shellOverride,
terminalWindowsWslDistro: ctx.terminalRuntimeOptions.terminalWindowsWslDistro
})?.distro ?? null
const initialSelectionTarget = getCodexSelectionTargetForPty(
initialShellOverride,
ctx.cwd,
ctx.expectedWslDistro
)
args.env = withClaudeProfileTerminalEnv(args.env, initialSelectionTarget)
const target = await resolvePtySpawnStartupTarget({
worktreeId: args.worktreeId,
cwd: args.cwd,
sessionId: ctx.effectiveSessionId,
shellOverride: args.shellOverride,
plannedPty: ctx.paneAgentLaunch?.launchRuntime?.pty,
projectRuntimeSource: { kind: 'sent', projectRuntime: args.projectRuntime },
// Why: reattach needs exact cwd, and successful stable-pane adoption needs no launch preflight.
missingCwdFallback:
!ctx.preAdoptedStablePane && !args.sessionId && args.cwdFallback === 'worktree'
? { directoryExists: ctx.deps.localStartupCwdDirectoryExists }
: undefined,
getSettings: ctx.deps.getSettings,
resolveStartupCwd: ctx.deps.resolvePtySpawnStartupCwd
})
ctx.cwd = target.cwd
ctx.prevalidatedCwd = target.prevalidatedCwd
ctx.startupCwdFallback = target.startupCwdFallback
ctx.terminalRuntimeOptions = target.terminalRuntimeOptions
ctx.expectedWslDistro = target.expectedWslDistro
ctx.codexSelectionTarget = target.codexSelectionTarget
ctx.spawnTiming.mark('preflight')
args.env = withClaudeProfileTerminalEnv(args.env, ctx.codexSelectionTarget)
ctx.claudeAuth =
ctx.isClaudeLaunch && ctx.deps.prepareClaudeAuth
? await ctx.deps.prepareClaudeAuth(initialSelectionTarget)
? await ctx.deps.prepareClaudeAuth(ctx.codexSelectionTarget)
: null
ctx.spawnTiming.mark('auth')
}
+18 -27
View File
@@ -9,7 +9,6 @@ import { getProvider } from '../provider/registry'
import { buildPtyHostEnv } from '../host-env/assembly'
import {
getCompatibleSelectedCodexHomePath,
getCodexSelectionTargetForPty,
resolveCodexHomeAfterManagedAuthReadiness,
shouldSkipCodexHomeEnvForWindowsShell,
shouldStripInheritedOrcaCodexHome,
@@ -27,9 +26,7 @@ import {
mintPtySessionId,
ptySessionIdForAgentCreateOperation
} from '../../../daemon/pty-session-id'
import { resolveWslSessionContext } from '../../../daemon/wsl-session-context'
import { isAgentStatusHooksEnabled } from '../../../agent-hooks/managed-agent-hook-controls'
import { resolvePlainTerminalPtyOptions } from '../../../local-project-runtime-resolution'
import { resolvePathEnvKey } from '../../../pty/windows-environment-path'
import { stampWslOrchestrationCompatibilityHost } from '../../../pty/wsl-orca-env'
import { stampRuntimeSourceEnv } from '../../../../shared/runtime-source-env'
@@ -38,6 +35,7 @@ import { ensureCodexStateDbBackfillRecoveryStarted } from '../../../codex/codex-
import { clearProviderPtyState } from '../provider/state-cleanup'
import { awaitExplicitPiOmpGuestReadiness } from '../../../agent-hooks/wsl-pi-omp-guest-readiness'
import type { RuntimePtySpawnState } from './spawn-state'
import { resolvePtySpawnStartupTarget } from '../spawn-startup-target'
import { getConnectionExecutionHostId } from '../../../../shared/execution-host'
export async function prepareRuntimePtySpawn(
@@ -50,7 +48,6 @@ export async function prepareRuntimePtySpawn(
await admission
}
}
ctx.cwd = ctx.deps.resolvePtySpawnStartupCwd(args.worktreeId, args.cwd)
// Only this machine spawns here: an `ssh:` connection throws, so every line below is local.
ctx.provider = getProvider(getConnectionExecutionHostId(args.connectionId))
const freshSpawnRecovery = ctx.preAdoptedStablePane
@@ -60,17 +57,6 @@ export async function prepareRuntimePtySpawn(
await freshSpawnRecovery
}
ctx.isClaudeLaunch = !ctx.preAdoptedStablePane && isClaudeLaunchCommand(args.command)
// Why: runtime-created terminals carry no renderer projectRuntime. An agent launch brings the PTY
// its command was quoted for; anything else takes the project's runtime and `--shell` pick.
ctx.terminalRuntimeOptions =
args.launchRuntime?.pty ??
resolvePlainTerminalPtyOptions(
ctx.deps.store,
ctx.deps.getSettings?.(),
args.worktreeId,
args.shellOverride
)
ctx.daemonShellOverride = ctx.terminalRuntimeOptions.shellOverride
ctx.isDaemonHostSpawn =
!(ctx.provider instanceof LocalPtyProvider) && !routesFreshSpawnsToLocalProvider(ctx.provider)
ctx.callerRequestedSessionId = args.sessionId?.trim()
@@ -87,18 +73,23 @@ export async function prepareRuntimePtySpawn(
!ctx.preAdoptedStablePane &&
ctx.isDaemonHostSpawn &&
(ctx.callerRequestedSessionId === undefined || args.isNewSession === true)
ctx.expectedWslDistro =
resolveWslSessionContext({
cwd: ctx.cwd,
sessionId: ctx.sessionId,
shellOverride: ctx.terminalRuntimeOptions.shellOverride,
terminalWindowsWslDistro: ctx.terminalRuntimeOptions.terminalWindowsWslDistro
})?.distro ?? null
ctx.codexSelectionTarget = getCodexSelectionTargetForPty(
ctx.daemonShellOverride,
ctx.cwd,
ctx.expectedWslDistro
)
// Why: runtime-created terminals carry no renderer projectRuntime. An agent launch brings the PTY
// its command was quoted for; anything else takes the project's runtime and `--shell` pick.
const target = await resolvePtySpawnStartupTarget({
worktreeId: args.worktreeId,
cwd: args.cwd,
sessionId: ctx.sessionId,
shellOverride: args.shellOverride,
plannedPty: args.launchRuntime?.pty,
projectRuntimeSource: { kind: 'store', store: ctx.deps.store },
getSettings: ctx.deps.getSettings,
resolveStartupCwd: ctx.deps.resolvePtySpawnStartupCwd
})
ctx.cwd = target.cwd
ctx.terminalRuntimeOptions = target.terminalRuntimeOptions
ctx.daemonShellOverride = ctx.terminalRuntimeOptions.shellOverride
ctx.expectedWslDistro = target.expectedWslDistro
ctx.codexSelectionTarget = target.codexSelectionTarget
const codexResumePreparation = ctx.preAdoptedStablePane
? null
: ctx.deps.prepareCodexResumeHome({
+165
View File
@@ -0,0 +1,165 @@
// Where a new PTY starts and in which shell/runtime, for both the window and the host spawn lane.
import {
isWslShellName,
type LocalWindowsTerminalRuntimeOptions
} from '../../../shared/local-windows-terminal-runtime'
import type { ProjectExecutionRuntimeResolution } from '../../../shared/project-execution-runtime'
import type { TerminalStartupCwdMissingDirFallback } from '../../../shared/terminal-startup-cwd'
import { isWslUncPath, toWindowsWslPath } from '../../../shared/wsl-paths'
import type { CodexAccountSelectionTarget } from '../../codex-accounts/runtime-selection'
import { resolveWslSessionContext } from '../../daemon/wsl-session-context'
import {
resolveLocalProjectRuntimeForWorktreeId,
resolveLocalPtyOptions,
type ProjectRuntimeResolutionStore
} from '../../local-project-runtime-resolution'
import { normalizeWindowsTerminalCwd } from '../../providers/windows-shell-args'
import { wslUncDirectoryExistsAsync } from '../../wsl'
import { getCodexSelectionTargetForPty } from './host-env/codex-home'
// TEMPORARY (row 13b removes it): the window uses the renderer's runtime; the host reads the store.
export type PtySpawnProjectRuntimeSource =
| { kind: 'sent'; projectRuntime: ProjectExecutionRuntimeResolution | undefined }
| { kind: 'store'; store: ProjectRuntimeResolutionStore | undefined }
export type PtySpawnStartupTarget = {
cwd: string | undefined
/** Already proven to exist by the WSL probe. */
prevalidatedCwd?: string
startupCwdFallback?: { kind: 'worktree'; cwd: string }
terminalRuntimeOptions: LocalWindowsTerminalRuntimeOptions
expectedWslDistro: string | null
codexSelectionTarget: CodexAccountSelectionTarget
}
type PtySpawnStartupArgs = {
worktreeId: string | undefined
cwd: string | undefined
/** Its worktree can imply the WSL distro. */
sessionId: string | undefined
shellOverride: string | undefined
/** The PTY an agent launch's command was quoted for. */
plannedPty?: LocalWindowsTerminalRuntimeOptions
projectRuntimeSource: PtySpawnProjectRuntimeSource
/** TEMPORARY (row 13a removes it): only window starts fall back to the root of a missing cwd. */
missingCwdFallback?: { directoryExists: (path: string) => boolean }
getSettings: (() => Parameters<typeof resolveLocalPtyOptions>[0]) | undefined
resolveStartupCwd: (
worktreeId: string | undefined,
cwd: string | undefined,
missingDirFallback?: TerminalStartupCwdMissingDirFallback
) => string | undefined
}
export async function resolvePtySpawnStartupTarget(
args: PtySpawnStartupArgs
): Promise<PtySpawnStartupTarget> {
const source = args.projectRuntimeSource
const terminalRuntimeOptions =
args.plannedPty ??
resolveLocalPtyOptions(
args.getSettings?.(),
source.kind === 'sent'
? source.projectRuntime
: resolveLocalProjectRuntimeForWorktreeId(source.store, args.worktreeId),
args.shellOverride,
process.env.COMSPEC || 'powershell.exe'
)
const startup = args.missingCwdFallback
? await resolveStartupCwdWithFallback(args, args.missingCwdFallback, terminalRuntimeOptions)
: { cwd: args.resolveStartupCwd(args.worktreeId, args.cwd) }
const expectedWslDistro =
resolveWslSessionContext({
cwd: startup.cwd,
sessionId: args.sessionId,
shellOverride: terminalRuntimeOptions.shellOverride,
terminalWindowsWslDistro: terminalRuntimeOptions.terminalWindowsWslDistro
})?.distro ?? null
return {
...startup,
terminalRuntimeOptions,
expectedWslDistro,
codexSelectionTarget: getCodexSelectionTargetForPty(
terminalRuntimeOptions.shellOverride,
startup.cwd,
expectedWslDistro
)
}
}
async function resolveStartupCwdWithFallback(
{ worktreeId, resolveStartupCwd, ...args }: PtySpawnStartupArgs,
fallback: { directoryExists: (path: string) => boolean },
runtimeOptions: LocalWindowsTerminalRuntimeOptions
): Promise<Pick<PtySpawnStartupTarget, 'cwd' | 'prevalidatedCwd' | 'startupCwdFallback'>> {
const isPosixStartupCwd = args.cwd?.startsWith('/') === true
const startupWorkspaceCwd = isPosixStartupCwd ? resolveStartupCwd(worktreeId, '.') : undefined
const initiallyResolvedStartupCwd = isPosixStartupCwd
? resolveStartupCwd(worktreeId, args.cwd)
: undefined
// Only a Windows runtime's shell can put a POSIX cwd inside WSL.
const startupRuntime = process.platform === 'win32' ? runtimeOptions : undefined
const wslRuntimeOwnsStartupCwd =
isPosixStartupCwd &&
(isWslShellName(startupRuntime?.shellOverride) || isWslUncPath(startupWorkspaceCwd ?? ''))
const startupWslContext = wslRuntimeOwnsStartupCwd
? resolveWslSessionContext({
cwd: startupWorkspaceCwd,
shellOverride: startupRuntime?.shellOverride,
terminalWindowsWslDistro: startupRuntime?.terminalWindowsWslDistro
})
: undefined
let prevalidatedCwd: string | undefined
let wslStartupCwdExists: boolean | null = null
let wslWorkspaceCwdExists: boolean | null = null
if (startupWslContext && initiallyResolvedStartupCwd) {
const validationCwd = toWindowsWslPath(initiallyResolvedStartupCwd, startupWslContext.distro)
wslStartupCwdExists = isWslUncPath(validationCwd)
? await wslUncDirectoryExistsAsync(validationCwd)
: fallback.directoryExists(validationCwd)
if (wslStartupCwdExists === true) {
prevalidatedCwd = validationCwd
}
if (wslStartupCwdExists === false && startupWorkspaceCwd) {
wslWorkspaceCwdExists = isWslUncPath(startupWorkspaceCwd)
? await wslUncDirectoryExistsAsync(startupWorkspaceCwd)
: fallback.directoryExists(startupWorkspaceCwd)
if (wslWorkspaceCwdExists === true) {
prevalidatedCwd = startupWorkspaceCwd
}
}
}
const allowMissingCwdFallback =
!wslRuntimeOwnsStartupCwd || (wslStartupCwdExists === false && wslWorkspaceCwdExists === true)
let didFallbackToWorkspaceRootCwd = false
const cwd = resolveStartupCwd(
worktreeId,
args.cwd,
allowMissingCwdFallback
? {
directoryExists: (path) =>
startupWslContext &&
wslStartupCwdExists === false &&
path === initiallyResolvedStartupCwd
? false
: startupWslContext && path === startupWorkspaceCwd
? wslWorkspaceCwdExists === true
: fallback.directoryExists(
process.platform === 'win32' ? normalizeWindowsTerminalCwd(path) : path
),
onFallbackToWorkspaceRoot: () => {
didFallbackToWorkspaceRootCwd = true
}
}
: undefined
)
if (didFallbackToWorkspaceRootCwd && wslWorkspaceCwdExists === true && cwd) {
prevalidatedCwd = cwd
}
return {
cwd,
prevalidatedCwd,
startupCwdFallback: didFallbackToWorkspaceRootCwd && cwd ? { kind: 'worktree', cwd } : undefined
}
}
@@ -15,6 +15,11 @@ import {
import { getDefaultSettings } from '../../../../shared/constants'
import { WINDOW_LAUNCH_CASES } from '../../../../shared/launch-parity-window-cases.test-fixture'
import { PLUS_LAUNCH_CASES } from '../../../../shared/launch-parity-plus-cases.test-fixture'
import { WORKSPACE_START_LAUNCH_CASES } from '../../../../shared/launch-parity-workspace-start-cases.test-fixture'
import {
loadWorkspaceStartModules,
startWorkspaceAgent
} from '@/lib/launch-parity-workspace-start.test-fixture'
import {
LAUNCH_TAB_ID,
LAUNCH_TOKEN,
@@ -40,6 +45,12 @@ vi.mock('react', async (importOriginal) => {
const actual = await importOriginal<typeof React>()
return { ...actual, useCallback: <T>(fn: T): T => fn }
})
// Row 8: what precedes the launch (setup policy and hooks trust) is decided, not asked.
vi.mock('@/lib/launch-work-item-direct-preflight', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
resolveDirectSetupDecision: async () => ({ kind: 'decided', decision: 'inherit' })
}))
vi.mock('@/lib/ensure-hooks-confirmed', () => ({ ensureHooksConfirmed: async () => 'run' }))
vi.mock('@/lib/windows-terminal-capabilities', async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
hasCachedWindowsTerminalCapabilities: () => true,
@@ -60,7 +71,8 @@ const load = perClientLoader(async () => ({
pane: await import('./pty-connection'),
paneOptions: await import('./terminal-pane-manager-options'),
defaults: await import('@/lib/pane-manager/pane-terminal-options'),
lifecycle: await import('./terminal-pane-lifecycle-primitives')
lifecycle: await import('./terminal-pane-lifecycle-primitives'),
workspaceStart: await loadWorkspaceStartModules()
}))
/** Runs the case's real producer against a real store; returns the tab it opened. */
@@ -75,7 +87,9 @@ async function openTab(c: WindowLaunchCase) {
)
const worktreeId = launchWorkspaceId(c.workspace)
const producer = c.producer
if (producer.kind === 'quick-command') {
if (producer.kind === 'work-item' || producer.kind === 'folder-composer') {
await startWorkspaceAgent(modules.workspaceStart, store, { ...c, producer })
} else if (producer.kind === 'quick-command') {
modules.quick.runQuickCommandInNewTab({
command: { id: 'qc-1', label: 'Fix', action: 'agent-prompt', ...producer },
worktreeId
@@ -205,7 +219,10 @@ describe('window-tab producers: the pty:spawn request a real pane sends', () =>
vi.unstubAllGlobals()
})
it.each([...WINDOW_LAUNCH_CASES, ...PLUS_LAUNCH_CASES])('row $row: $name', async (c) => {
expect(await paneSpawnRequest(c)).toEqual(windowSpawnRequest(c))
})
it.each([...WINDOW_LAUNCH_CASES, ...PLUS_LAUNCH_CASES, ...WORKSPACE_START_LAUNCH_CASES])(
'row $row: $name',
async (c) => {
expect(await paneSpawnRequest(c)).toEqual(windowSpawnRequest(c))
}
)
})
@@ -0,0 +1,104 @@
// Test-only: runs the row-8 producers (an agent started with a workspace) against the parity
// harness's real store. Only what precedes the launch is stubbed: setup policy, hooks trust, agent
// detection and the create itself.
import type { LaunchTestStore } from './launch-parity-renderer.test-fixture'
import type * as WorkItemLaunch from '@/lib/launch-work-item-direct'
import type * as FolderComposerSubmit from '@/components/sidebar/folder-workspace-composer-submit'
import {
resolveTuiAgentLaunchArgs,
resolveTuiAgentLaunchEnv
} from '../../../shared/tui-agent-launch-defaults'
import {
LAUNCH_FOLDER_ID,
type WindowLaunchCase
} from '../../../shared/launch-parity-window-request.test-fixture'
type WorkspaceStartProducer = Extract<
WindowLaunchCase['producer'],
{ kind: 'work-item' | 'folder-composer' }
>
export type WorkspaceStartModules = {
workItem: typeof WorkItemLaunch
folder: typeof FolderComposerSubmit
}
export const loadWorkspaceStartModules = async (): Promise<WorkspaceStartModules> => ({
workItem: await import('@/lib/launch-work-item-direct'),
folder: await import('@/components/sidebar/folder-workspace-composer-submit')
})
export async function startWorkspaceAgent(
modules: WorkspaceStartModules,
store: LaunchTestStore,
c: WindowLaunchCase & { producer: WorkspaceStartProducer }
): Promise<void> {
const { producer } = c
const state = store.getState()
// Activating a workspace asks for a PR refresh, which this harness has no bridge for; a prompt
// pasted after start waits on timers.
Object.assign(window.api, { gh: {} })
Object.assign(window, { setTimeout, clearTimeout })
store.setState({
ensureDetectedAgents: async () => [producer.agent],
updateWorktreeMeta: async () => ({ ok: true })
})
if (producer.kind === 'work-item') {
// The workspace the create returns is the one the harness seeded.
const worktree = state.worktreesByRepo['repo-1']?.[0]
store.setState({ createWorktree: async () => ({ worktree }) })
const started = await modules.workItem.launchWorkItemDirect({
item: {
title: 'Fix the bug',
url: 'https://github.com/acme/repo/issues/7',
type: 'issue',
number: 7,
pasteContent: producer.text
},
repoId: 'repo-1',
launchSource: 'task_page',
telemetrySource: 'sidebar',
agentOverride: producer.agent,
promptDelivery: producer.delivery,
openModalFallback: () => {
throw new Error(`${c.name}: fell back to the modal`)
}
})
if (!started) {
throw new Error(`${c.name}: the work item started no agent`)
}
return
}
const folderWorkspace = state.folderWorkspaces.find(
(workspace) => `folder:${workspace.id}` === LAUNCH_FOLDER_ID
)
const settings = state.settings
await modules.folder.submitFolderWorkspaceCreate({
projectGroup: {
id: 'pg-1',
name: 'group',
parentPath: c.workspace.path,
parentGroupId: null,
createdFrom: 'manual',
tabOrder: 0,
isCollapsed: false,
color: null,
createdAt: 0,
updatedAt: 0
},
name: 'folder',
lastAutoName: '',
linkedWorkItem: null,
note: producer.note,
quickAgent: producer.agent,
autoRenameBranchFromWork: false,
// As the composer's folder submit passes them.
agentCmdOverrides: settings?.agentCmdOverrides,
agentArgs: resolveTuiAgentLaunchArgs(producer.agent, settings?.agentDefaultArgs),
agentEnv: resolveTuiAgentLaunchEnv(producer.agent, settings?.agentDefaultEnv),
terminalWindowsShell: settings?.terminalWindowsShell,
launchSource: 'new_workspace_composer',
createFolderWorkspace: async () => folderWorkspace ?? null,
onOpenChange: () => {}
})
}
@@ -33,9 +33,24 @@ export type WindowLaunchProducer =
| { kind: 'transcript-continue'; transcript: string; cwd: string | null }
/** A "+" launch with no prompt, as each "+" entry point calls launchAgentInNewTab. */
| { kind: 'plus'; agent: 'claude'; via: PlusEntry }
/** A work item's "start agent" (launch-work-item-direct.ts): the workspace and its agent at once. */
| {
kind: 'work-item'
agent: 'claude' | 'codex'
delivery: 'draft' | 'submit-after-ready'
text: string
}
/** The folder-workspace composer's create with an agent; the note is its prompt. */
| { kind: 'folder-composer'; agent: 'claude' | 'codex'; note: string }
/** "+" entry points. Quick Launch items omit launchSource and get `menu`'s source by default. */
export type PlusEntry = 'menu' | 'shortcut' | 'dashboard'
const PRODUCER_LAUNCH_SOURCE = {
'quick-command': 'quick_command',
continuation: 'terminal_context_menu',
'work-item': 'task_page',
'folder-composer': 'new_workspace_composer'
} as const
const PLUS_LAUNCH_SOURCE: Record<PlusEntry, string> = {
menu: 'tab_bar_quick_launch',
shortcut: 'shortcut',
@@ -57,10 +72,14 @@ export type WindowRequestFacts = {
agentEnv?: Record<string, string>
/** The agent's saved default args; absent = the settings default. */
agentArgs?: string
/** Whether the command waits for the shell to be ready; absent = only Codex does. */
shellReady?: boolean
}
/** What reaches the provider for that request (main half). */
export type WindowProviderFacts = {
/** Default: the request's command. */
command?: string
shellOverride: string | undefined
terminalWindowsWslDistro?: string | null
}
@@ -68,7 +87,7 @@ export type WindowProviderFacts = {
export type WindowLaunchCase = {
name: string
/** The launch-convergence producer row. */
row: 1 | 2 | 6
row: 1 | 2 | 6 | 8
client: LaunchClient
workspace: LaunchWorkspace
settings?: Record<string, unknown>
@@ -138,7 +157,7 @@ export function windowSpawnRequest(c: WindowLaunchCase): Record<string, unknown>
},
launchToken: LAUNCH_TOKEN,
launchAgent: agent,
...(agent === 'codex' ? { startupCommandDelivery: 'shell-ready' } : {}),
...((request.shellReady ?? agent === 'codex') ? { startupCommandDelivery: 'shell-ready' } : {}),
worktreeId,
tabId: LAUNCH_TAB_ID,
leafId: LAUNCH_LEAF_ID,
@@ -153,9 +172,7 @@ export function windowSpawnRequest(c: WindowLaunchCase): Record<string, unknown>
launch_source:
producer.kind === 'plus'
? PLUS_LAUNCH_SOURCE[producer.via]
: producer.kind === 'quick-command'
? 'quick_command'
: 'terminal_context_menu',
: PRODUCER_LAUNCH_SOURCE[producer.kind],
request_kind: 'new'
}
}
@@ -182,7 +199,7 @@ export function expectedWindowProvider(
// main adds the terminal handle to the pane's env.
const env = { ...(typeof request.env === 'object' ? request.env : {}), ORCA_TERMINAL_HANDLE: '' }
return {
command: request.command,
command: provider.command ?? request.command,
// main normalizes the cwd to forward slashes (resolveRuntimePath).
cwd: String(request.cwd).replaceAll('\\', '/'),
// The pane's measured size, and never marked hidden before its pane shows.
@@ -0,0 +1,177 @@
// Test-only (never imported by production code): the window-lane launch parity table for row 8
// (an agent started together with its workspace: a work item's "start agent", the folder-workspace
// composer). Renderer suites assert the real producer + pane send `request`; main suites feed it to
// pty:spawn and assert `provider`. C4 moves these launchers onto the pane trigger: the request then
// carries the launch instead of a command, and every provider fact here must hold unchanged.
import type {
LaunchClient,
LaunchWorkspace,
WindowLaunchCase,
WindowLaunchProducer,
WindowProviderFacts,
WindowRequestFacts
} from './launch-parity-window-request.test-fixture'
import { POSIX_PATH, WIN_PATH } from './launch-parity-window-cases.test-fixture'
const SKIP = '--dangerously-skip-permissions'
const POSIX_CLAUDE = `claude '${SKIP}'`
const CMD_CLAUDE = `claude "${SKIP}"`
const POSIX_CODEX = "codex '--dangerously-bypass-approvals-and-sandbox'"
const BOB = "fix Bob's bug"
// Longer than one typed line can safely hold, and multi-line: main's window still puts it on argv.
const LONG_NOTE = `${'Read the failing test output and fix the cause. '.repeat(12)}\nThen run it again.`
const ZSH = { shellOverride: '/bin/zsh' }
const IN_CMD = {
shellOverride: 'cmd.exe',
projectRuntime: { kind: 'windows-host', reason: 'global-default' }
} as const
const CMD_EXE = { shellOverride: 'cmd.exe', terminalWindowsWslDistro: null }
const IN_WSL = {
shellOverride: 'wsl.exe',
projectRuntime: { kind: 'wsl', reason: 'project-override', distro: 'Ubuntu' }
} as const
const WSL_EXE = { shellOverride: 'wsl.exe', terminalWindowsWslDistro: 'Ubuntu' }
const CMD = { terminalWindowsShell: 'cmd.exe' }
const repo = (path: string, extra: Partial<LaunchWorkspace> = {}): LaunchWorkspace => ({
kind: 'repo',
path,
...extra
})
const folder = (path: string): LaunchWorkspace => ({ kind: 'folder', path })
const draft = { kind: 'work-item', agent: 'claude', delivery: 'draft', text: BOB } as const
const note = (text: string): WindowLaunchProducer => ({
kind: 'folder-composer',
agent: 'claude',
note: text
})
function start(
name: string,
client: LaunchClient,
workspace: LaunchWorkspace,
producer: WindowLaunchProducer,
request: WindowRequestFacts,
provider: WindowProviderFacts,
settings?: Record<string, unknown>
): WindowLaunchCase {
return { name, row: 8, client, workspace, producer, request, provider, settings }
}
export const WORKSPACE_START_LAUNCH_CASES: WindowLaunchCase[] = [
// A work item's context is a draft: Claude takes it on argv.
start(
'work item, macOS repo, draft rides argv',
'darwin',
repo(POSIX_PATH),
draft,
{ command: `${POSIX_CLAUDE} --prefill 'fix Bob'"'"'s bug'`, agentCommand: POSIX_CLAUDE },
ZSH
),
// main today: the work item quotes for PowerShell on Windows even when cmd.exe is the shell
// (C4 keeps it; C4-fix flips this row to cmd.exe quoting).
start(
'work item, Windows C: repo, cmd.exe set',
'win32',
repo(WIN_PATH),
draft,
{
command: `${POSIX_CLAUDE} --prefill 'fix Bob''s bug'`,
agentCommand: POSIX_CLAUDE,
...IN_CMD
},
CMD_EXE,
CMD
),
start(
'work item, Windows C: repo, project WSL',
'win32',
repo(WIN_PATH, { projectRuntime: { kind: 'wsl', distro: 'Ubuntu' } }),
draft,
{
command: `${POSIX_CLAUDE} --prefill 'fix Bob'"'"'s bug'`,
agentCommand: POSIX_CLAUDE,
...IN_WSL
},
WSL_EXE,
CMD
),
// A C: project never set to a runtime, with WSL as the terminal shell: the work item follows the
// Windows runtime default.
start(
'work item, Windows C: repo, terminal shell WSL',
'win32',
repo(WIN_PATH),
draft,
{
command: `${POSIX_CLAUDE} --prefill 'fix Bob''s bug'`,
agentCommand: POSIX_CLAUDE,
shellOverride: 'powershell.exe',
projectRuntime: { kind: 'windows-host', reason: 'global-default' }
},
{ shellOverride: 'powershell.exe', terminalWindowsWslDistro: null },
{ terminalWindowsShell: 'wsl.exe' }
),
// Fix checks submit after the agent is ready: the command carries no prompt, the window pastes it,
// and main's prompt-less Codex start is typed without waiting for the shell to be ready.
start(
'work item, submit after ready',
'darwin',
repo(POSIX_PATH),
{ kind: 'work-item', agent: 'codex', delivery: 'submit-after-ready', text: BOB },
{ command: POSIX_CODEX, agentCommand: POSIX_CODEX, shellReady: false },
ZSH
),
// The composer's note is an auto-submitted prompt on argv.
start(
'folder composer, macOS folder, note rides argv',
'darwin',
folder(POSIX_PATH),
note(BOB),
{ command: `${POSIX_CLAUDE} 'fix Bob'"'"'s bug'`, agentCommand: POSIX_CLAUDE },
ZSH
),
// Unlike the work item, the composer quotes for the Windows shell it runs in.
start(
'folder composer, Windows C: folder, cmd.exe set',
'win32',
folder(WIN_PATH),
note(BOB),
// A folder has no project, so no project runtime is sent.
{
command: `${CMD_CLAUDE} "fix Bob's bug"`,
agentCommand: CMD_CLAUDE,
shellOverride: 'cmd.exe'
},
CMD_EXE,
CMD
),
// A folder keeps WSL when it is the terminal shell (the host's wslShellRule).
start(
'folder composer, Windows C: folder, terminal shell WSL',
'win32',
folder(WIN_PATH),
note(BOB),
{
command: `${POSIX_CLAUDE} 'fix Bob'"'"'s bug'`,
agentCommand: POSIX_CLAUDE,
shellOverride: 'wsl.exe'
},
// main today: the terminal's default distro, none named.
{ shellOverride: 'wsl.exe', terminalWindowsWslDistro: null },
{ terminalWindowsShell: 'wsl.exe' }
),
// main's window carries a prompt on argv at any length, where the host would paste a long one.
start(
'folder composer, long multi-line note rides argv',
'darwin',
folder(POSIX_PATH),
note(LONG_NOTE),
{
command: `${POSIX_CLAUDE} '${LONG_NOTE}'`,
agentCommand: POSIX_CLAUDE
},
ZSH
)
]