fix(workspaces): preserve remote task launch options

This commit is contained in:
Jinwoo-H
2026-08-09 22:44:00 -04:00
parent ce8c3267b7
commit acd8425fe1
24 changed files with 693 additions and 58 deletions
+137
View File
@@ -10254,6 +10254,143 @@
],
"demotionRule": "Demote if any exit duration can evade the spawn budget, concurrent children or timers appear, cleanup retains resources, or a transient failure cannot recover within the budget."
},
{
"id": "worktree.remote-task-agent-startup-authority",
"title": "Remote task workspaces start one host-owned agent with truthful launch state",
"maturity": "experimental",
"protection": "partial",
"owner": "worktree-runtime",
"layer": "renderer-runtime-worktree-create-contract",
"surfaces": [
"Tasks issue and pull-request workspace creation",
"paired runtime Git and folder workspaces",
"nested SSH worktrees owned by a paired runtime",
"startup terminal mirroring and wake recovery",
"agent launch session-option state",
"mixed client and host versions"
],
"platforms": ["macos", "linux", "windows"],
"providers": ["local", "ssh", "paired-runtime"],
"coveredPlatforms": ["macos", "windows"],
"coveredProviders": ["local", "ssh", "paired-runtime"],
"coverageNotes": "Deterministic contracts join renderer request construction, RPC validation and forwarding, exact host argv for Git/SSH/folder workspaces, agent-scoped startup acknowledgement, renderer fallback suppression, and truthful session-option seeding. A physical paired Windows v1.4.177 host covers the reported Tasks journey and exact host PTY inventory; applying the new session-option field on a physical candidate host remains a gap.",
"motivatingLinks": ["https://github.com/stablyai/orca/pull/13412"],
"invariant": "A Tasks-created paired workspace launches exactly one agent PTY on the owning host, mirrors that terminal without a renderer wake or fallback shell, preserves every supported client-selected launch option through host-side quoting, and marks only agent-matched host-acknowledged values as applied. Missing optional fields, fallback agents, invalid option kinds, or receipts from older hosts degrade without RPC failure or false applied state.",
"oracle": "Send a linked-task draft and session options through the real worktree.create schema and handler. Require Git, nested SSH, and folder hosts to emit the selected model and effort in the exact startup command, return the actual applied subset with the startup-terminal receipt, and publish no client-authored command. Feed that receipt through Tasks completion and activation; require the host tab to own draft/session state, zero fallback or wake creation, and no applied cache entry when a legacy receipt omits acknowledgement. On a physical paired Windows host, compare authoritative PTY inventory before and after the fix and require Codex plus Setup only, a running Codex process, the unsent issue draft, completed Setup, and no ConPTY DA1 text.",
"commands": [
"pnpm exec vitest run --config config/vitest.config.ts src/shared/tui-agent-startup-session-options.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/schemas.test.ts src/main/runtime/rpc/methods/worktree.test.ts src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts src/renderer/src/store/slices/worktrees.test.ts src/renderer/src/lib/worktree-creation-flow.test.ts src/renderer/src/lib/worktree-creation-flow-startup.test.ts src/renderer/src/lib/worktree-creation-agent-seeds.test.ts src/renderer/src/lib/worktree-activation.test.ts",
"Tasks -> GitHub issue -> Codex -> windows 2 paired host"
],
"testFiles": [
"src/shared/tui-agent-startup-session-options.test.ts",
"src/main/runtime/orca-runtime.test.ts",
"src/main/runtime/rpc/schemas.test.ts",
"src/main/runtime/rpc/methods/worktree.test.ts",
"src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts",
"src/renderer/src/store/slices/worktrees.test.ts",
"src/renderer/src/lib/worktree-creation-flow.test.ts",
"src/renderer/src/lib/worktree-creation-flow-startup.test.ts",
"src/renderer/src/lib/worktree-creation-agent-seeds.test.ts",
"src/renderer/src/lib/worktree-activation.test.ts"
],
"assertionRefs": [
{
"file": "src/main/runtime/orca-runtime.test.ts",
"assertions": [
"Git host startup argv replaces host model and effort defaults with client session options",
"nested SSH startup uses remote-shell quoting and returns applied session options",
"folder workspace startup returns the same authoritative receipt",
"fallback agents neither receive nor acknowledge another agent's options"
]
},
{
"file": "src/shared/tui-agent-startup-session-options.test.ts",
"assertions": [
"option values with the wrong catalog kind are neither encoded nor acknowledged"
]
},
{
"file": "src/main/runtime/rpc/methods/worktree.test.ts",
"assertions": [
"worktree.create forwards the bounded optional startup draft and session options"
]
},
{
"file": "src/renderer/src/lib/worktree-creation-flow-startup.test.ts",
"assertions": [
"host-built draft session options are scoped to the requested agent",
"client-built startup does not also request host-owned draft startup"
]
},
{
"file": "src/renderer/src/lib/worktree-creation-flow.test.ts",
"assertions": [
"the backend startup receipt identifies the agent tab rather than a configured default tab"
]
},
{
"file": "src/renderer/src/lib/worktree-activation.test.ts",
"assertions": [
"explicit backend spawn evidence suppresses fallback creation while the host tab mirrors"
]
},
{
"file": "src/renderer/src/lib/worktree-creation-agent-seeds.test.ts",
"assertions": [
"legacy hosts without acknowledgement cannot stamp client options as applied",
"client-built commands retain their known applied state",
"fallback agents own draft state without inheriting requested-agent options"
]
}
],
"evidenceRuns": [
{
"date": "2026-08-09",
"runner": "local",
"platform": "macos",
"result": "passed",
"command": "pnpm exec vitest run --config config/vitest.config.ts src/shared/tui-agent-startup-session-options.test.ts src/main/runtime/orca-runtime.test.ts src/main/runtime/rpc/schemas.test.ts src/main/runtime/rpc/methods/worktree.test.ts src/renderer/src/hooks/useComposerState-host-context-boundaries.test.ts src/renderer/src/store/slices/worktrees.test.ts src/renderer/src/lib/worktree-creation-flow.test.ts src/renderer/src/lib/worktree-creation-flow-startup.test.ts src/renderer/src/lib/worktree-creation-agent-seeds.test.ts src/renderer/src/lib/worktree-activation.test.ts",
"durationSeconds": 12,
"summary": "1,516 deterministic renderer, RPC, Git, SSH, folder, launch-command, compatibility, and activation tests passed; one unrelated test was skipped."
},
{
"date": "2026-08-09",
"runner": "manual",
"platform": "windows",
"result": "passed",
"command": "Tasks -> GitHub issue -> Codex -> windows 2 paired host",
"durationSeconds": 60,
"summary": "Baseline had two blank shells plus Setup and no Codex. Candidate had Codex plus Setup only, Codex running with the issue URL drafted, Setup complete, and no DA1 text."
}
],
"runtimeBudget": {
"p95Seconds": 30,
"scope": "deterministic renderer, RPC, full runtime-service, and shared launch contracts"
},
"flakeHistory": {
"status": "not-started",
"evidence": "New gate; controlled promises, exact call counts, and host inventories replace timing or eventual-state assertions."
},
"redGreenEvidence": {
"status": "complete",
"evidence": "On baseline 94cc867e2a the physical Windows journey produced three host PTYs, no running Codex, and visible DA1 text; the candidate produced exactly Codex plus Setup. Independent review then proved the first candidate built from host defaults while stamping client options as applied. The exact argv and acknowledgement oracle fails on that candidate and passes after the optional session-options request and host-applied receipt; omitting the receipt reproduces the legacy no-claim result."
},
"performanceBudget": {
"required": false,
"evidence": "Creation adds one bounded optional JSON object and one response property. It adds no polling, retries, scans, subprocesses, timers, listeners, or hot-path fanout."
},
"promotionCriteria": [
"Collect 100 consecutive focused CI passes or 14 days of soak history.",
"Run the candidate host build on physical Windows and headless serve with non-default model and effort selections.",
"Add a versioned worktree.create harness spanning v1.4.19, v1.4.68, v1.4.138, and the current host."
],
"knownGaps": [
"The physical Windows host was v1.4.177, so it proves terminal topology but not application of the new session-options field.",
"Headless serve parity and a second simultaneous viewer have not been exercised for this Tasks creation path.",
"Historical receipt boundaries are reconstructed from release history and response-shape tests rather than four packaged hosts."
],
"demotionRule": "Demote if a task create can produce a second shell, if any client-only value is marked applied without host acknowledgement, if an old peer rejects the optional fields, or if the gate depends on elapsed time instead of authoritative inventory."
},
{
"id": "ssh-managed-hooks.node18-runtime-compatibility",
"title": "SSH managed-hook companions load and install hooks on Node 18",
+49 -6
View File
@@ -4057,6 +4057,14 @@ describe('OrcaRuntimeService', () => {
}
const runtimeStore = {
...store,
getSettings: () => ({
...store.getSettings(),
agentCmdOverrides: {},
agentDefaultArgs: {
codex:
'-m folder-host-model -c model_reasoning_effort=low --dangerously-bypass-approvals-and-sandbox'
}
}),
getRepos: () => [folderRepo],
getRepo: (id: string) => (id === folderRepo.id ? folderRepo : undefined),
getAllWorktreeMeta: () => metaById,
@@ -4097,14 +4105,25 @@ describe('OrcaRuntimeService', () => {
repoSelector: 'id:folder-repo',
name: 'folder-session',
createdWithAgent: 'codex',
startup: { command: 'codex', viewMode: 'chat' }
startupDraft: 'https://github.com/stablyai/orca/issues/12',
startupSessionOptions: {
agent: 'codex',
values: { model: 'gpt-5.6-sol', effort: 'high' }
}
})
expect(addWorktreeMock).not.toHaveBeenCalled()
expect(createTerminal).toHaveBeenCalledWith(
`id:${result.worktree.id}`,
expect.objectContaining({ command: 'codex', viewMode: 'chat' })
expect.objectContaining({
command:
"codex '--dangerously-bypass-approvals-and-sandbox' '-m' 'gpt-5.6-sol' '-c' 'model_reasoning_effort=high'"
})
)
expect(result.startupTerminal?.appliedSessionOptions).toEqual({
agent: 'codex',
values: { model: 'gpt-5.6-sol', effort: 'high' }
})
expect(result.worktree).toEqual(
expect.objectContaining({
id: expect.stringMatching(/^folder-repo::\/workspace\/folder::workspace:[0-9a-f-]{36}$/),
@@ -42539,7 +42558,11 @@ describe('OrcaRuntimeService', () => {
getSettings: () => ({
...store.getSettings(),
defaultTuiAgent: 'codex' as const,
agentCmdOverrides: { codex: 'codex --profile work' }
agentCmdOverrides: { codex: 'codex --profile work' },
agentDefaultArgs: {
codex:
'-m host-model -c model_reasoning_effort=low --dangerously-bypass-approvals-and-sandbox'
}
}),
getAllWorktreeMeta: () => metaById,
getWorktreeMeta: (worktreeId: string) => metaById[worktreeId],
@@ -42590,6 +42613,10 @@ describe('OrcaRuntimeService', () => {
repoSelector: 'id:repo-1',
name: 'runtime-startup-draft',
startupDraft: draftUrl,
startupSessionOptions: {
agent: 'codex',
values: { model: 'gpt-5.6-sol', effort: 'high' }
},
activate: true
})
@@ -42598,10 +42625,15 @@ describe('OrcaRuntimeService', () => {
expect(spawn).toHaveBeenCalledWith(
expect.objectContaining({
cwd: '/tmp/workspaces/runtime-startup-draft',
command: "codex --profile work '--dangerously-bypass-approvals-and-sandbox'",
command:
"codex --profile work '--dangerously-bypass-approvals-and-sandbox' '-m' 'gpt-5.6-sol' '-c' 'model_reasoning_effort=high'",
worktreeId: result.worktree.id
})
)
expect(result.startupTerminal?.appliedSessionOptions).toEqual({
agent: 'codex',
values: { model: 'gpt-5.6-sol', effort: 'high' }
})
expect(metaById[result.worktree.id]).toMatchObject({ createdWithAgent: 'codex' })
runtime.onPtyData('pty-startup-draft', '\x1b[?2004h›', Date.now())
@@ -42906,6 +42938,10 @@ describe('OrcaRuntimeService', () => {
repoSelector: TEST_REPO_ID,
name: 'runtime-fallback-draft',
startupDraft: 'https://github.com/stablyai/orca/issues/456',
startupSessionOptions: {
agent: 'codex',
values: { model: 'gpt-5.6-sol', effort: 'high' }
},
createdWithAgent: 'codex',
activate: true
})
@@ -42919,6 +42955,8 @@ describe('OrcaRuntimeService', () => {
})
)
expect(metaById[result.worktree.id]).toMatchObject({ createdWithAgent: 'claude' })
expect(spawn.mock.calls[0]?.[0].command).not.toContain('gpt-5.6-sol')
expect(result.startupTerminal?.appliedSessionOptions).toBeUndefined()
})
it('honors split setup placement for opted-in local startup-draft worktrees', async () => {
@@ -43375,7 +43413,8 @@ describe('OrcaRuntimeService', () => {
const result = await runtime.createManagedWorktree({
repoSelector: TEST_REPO_ID,
name: 'mobile-startup-draft',
startupDraft: draftUrl
startupDraft: draftUrl,
startupSessionOptions: { agent: 'claude', values: { model: 'opus', effort: 'high' } }
})
expect(detectRemoteAgentsMock).toHaveBeenCalledWith({ connectionId: 'ssh-1' })
@@ -43383,11 +43422,15 @@ describe('OrcaRuntimeService', () => {
expect(spawn).toHaveBeenCalledWith(
expect.objectContaining({
cwd: '/remote/mobile-startup-draft',
command: `claude '--dangerously-skip-permissions' --prefill '${draftUrl}'`,
command: `claude '--dangerously-skip-permissions' '--model' 'opus' '--effort' 'high' --prefill '${draftUrl}'`,
connectionId: 'ssh-1',
worktreeId: result.worktree.id
})
)
expect(result.startupTerminal?.appliedSessionOptions).toEqual({
agent: 'claude',
values: { model: 'opus', effort: 'high' }
})
expect(metaById[result.worktree.id]).toMatchObject({ createdWithAgent: 'claude' })
})
+55 -8
View File
@@ -219,6 +219,7 @@ import type {
TerminalPaneLayoutNode,
TerminalTab,
TuiAgent,
AgentLaunchSessionOptions,
WorkspaceCreateTelemetrySource,
WorkspaceSessionState,
WorkspaceLinkedItem,
@@ -21124,11 +21125,13 @@ export class OrcaRuntimeService {
private async buildStartupForDraft(
repo: Repo,
draft: string,
requestedAgent?: TuiAgent
requestedAgent?: TuiAgent,
requestedSessionOptions?: AgentLaunchSessionOptions
): Promise<{
agent: TuiAgent
startup: WorktreeStartupLaunch
draftPaste?: WorktreeStartupDraftPaste
appliedSessionOptions?: AgentLaunchSessionOptions
} | null> {
if (!this.store) {
return null
@@ -21164,6 +21167,8 @@ export class OrcaRuntimeService {
if (!agent) {
return null
}
const sessionOptions =
requestedSessionOptions?.agent === agent ? requestedSessionOptions.values : undefined
// Why: a mobile client can run on Windows while the workspace shell is
// Linux over SSH. Startup command quoting must target the shell that runs it.
@@ -21180,6 +21185,8 @@ export class OrcaRuntimeService {
cmdOverrides: settings.agentCmdOverrides ?? {},
agentArgs: resolveTuiAgentLaunchArgs(agent, settings.agentDefaultArgs),
agentEnv: resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv),
sessionOptions,
sessionOptionsOverrideAgentArgs: Boolean(sessionOptions),
platform: agentLaunchPlatform,
shell: queuedShell,
isRemote
@@ -21194,7 +21201,10 @@ export class OrcaRuntimeService {
? { startupCommandDelivery: draftLaunchPlan.startupCommandDelivery }
: {}),
...(draftLaunchPlan.env ? { env: draftLaunchPlan.env } : {})
}
},
...(draftLaunchPlan.sessionOptions
? { appliedSessionOptions: { agent, values: draftLaunchPlan.sessionOptions } }
: {})
}
}
@@ -21204,12 +21214,17 @@ export class OrcaRuntimeService {
cmdOverrides: settings.agentCmdOverrides ?? {},
agentArgs: resolveTuiAgentLaunchArgs(agent, settings.agentDefaultArgs),
agentEnv: resolveTuiAgentLaunchEnv(agent, settings.agentDefaultEnv),
sessionOptions,
sessionOptionsOverrideAgentArgs: Boolean(sessionOptions),
platform: agentLaunchPlatform,
shell: queuedShell,
isRemote,
allowEmptyPromptLaunch: true
})
if (!startupPlan) {
if (sessionOptions) {
throw new Error(`Could not apply launch preferences for ${agent}.`)
}
return null
}
return {
@@ -21222,7 +21237,10 @@ export class OrcaRuntimeService {
: {}),
...(startupPlan.env ? { env: startupPlan.env } : {})
},
draftPaste: { agent, content }
draftPaste: { agent, content },
...(startupPlan.sessionOptions
? { appliedSessionOptions: { agent, values: startupPlan.sessionOptions } }
: {})
}
}
@@ -21231,7 +21249,12 @@ export class OrcaRuntimeService {
agent: TuiAgent,
prompt: string | undefined,
launchPreferences?: AgentLaunchPreferences
): { agent: TuiAgent; startup: WorktreeStartupLaunch; followup?: WorktreeStartupFollowup } {
): {
agent: TuiAgent
startup: WorktreeStartupLaunch
followup?: WorktreeStartupFollowup
appliedSessionOptions?: AgentLaunchSessionOptions
} {
if (!this.store) {
throw new Error('runtime_unavailable')
}
@@ -21282,6 +21305,9 @@ export class OrcaRuntimeService {
prompt: startupPlan.followupPrompt
}
}
: {}),
...(startupPlan.sessionOptions
? { appliedSessionOptions: { agent, values: startupPlan.sessionOptions } }
: {})
}
}
@@ -21696,6 +21722,7 @@ export class OrcaRuntimeService {
createdWithAgent?: TuiAgent
startupAgent?: TuiAgent
startupLaunchPreferences?: AgentLaunchPreferences
startupSessionOptions?: AgentLaunchSessionOptions
startupPrompt?: string
pendingFirstAgentMessageRename?: boolean
automationProvenance?: AutomationWorkspaceProvenance
@@ -21737,10 +21764,17 @@ export class OrcaRuntimeService {
: null
const draftStartup =
!args.startup && !agentStartup && args.startupDraft
? await this.buildStartupForDraft(repo, args.startupDraft, requestedAgent)
? await this.buildStartupForDraft(
repo,
args.startupDraft,
requestedAgent,
args.startupSessionOptions
)
: null
const effectiveStartup = args.startup ?? agentStartup?.startup ?? draftStartup?.startup
const effectiveStartupFollowup = agentStartup?.followup
const effectiveAppliedSessionOptions =
agentStartup?.appliedSessionOptions ?? draftStartup?.appliedSessionOptions
const effectiveCreatedWithAgent = args.startup
? args.createdWithAgent
: (agentStartup?.agent ??
@@ -21841,7 +21875,10 @@ export class OrcaRuntimeService {
...(terminal.tabId ? { tabId: terminal.tabId } : {}),
...(terminal.paneKey ? { paneKey: terminal.paneKey } : {}),
...(terminal.ptyId ? { ptyId: terminal.ptyId } : {}),
surface: 'background'
surface: 'background',
...(effectiveAppliedSessionOptions
? { appliedSessionOptions: effectiveAppliedSessionOptions }
: {})
}
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
@@ -21894,6 +21931,9 @@ export class OrcaRuntimeService {
...(effectiveStartup ? { startup: effectiveStartup } : {}),
...(effectiveStartupFollowup ? { startupFollowup: effectiveStartupFollowup } : {}),
...(effectiveCreatedWithAgent ? { createdWithAgent: effectiveCreatedWithAgent } : {}),
...(effectiveAppliedSessionOptions
? { startupAppliedSessionOptions: effectiveAppliedSessionOptions }
: {}),
...(effectiveDraftPaste ? { startupDraftPaste: effectiveDraftPaste } : {})
})
const recordedLineage = this.recordCreatedWorktreeLineage(result.worktree, lineageResolution)
@@ -22762,7 +22802,10 @@ export class OrcaRuntimeService {
...(startupTerminalTabId ? { tabId: startupTerminalTabId } : {}),
...(startupTerminalPaneKey ? { paneKey: startupTerminalPaneKey } : {}),
...(startupTerminalPtyId ? { ptyId: startupTerminalPtyId } : {}),
surface: 'background' as const
surface: 'background' as const,
...(effectiveAppliedSessionOptions
? { appliedSessionOptions: effectiveAppliedSessionOptions }
: {})
}
}
: {})
@@ -22806,6 +22849,7 @@ export class OrcaRuntimeService {
startup?: WorktreeStartupLaunch
startupFollowup?: WorktreeStartupFollowup
startupDraftPaste?: WorktreeStartupDraftPaste
startupAppliedSessionOptions?: AgentLaunchSessionOptions
}
): Promise<CreateWorktreeResult> {
if (!this.store) {
@@ -23073,7 +23117,10 @@ export class OrcaRuntimeService {
...(startupTerminalTabId ? { tabId: startupTerminalTabId } : {}),
...(startupTerminalPaneKey ? { paneKey: startupTerminalPaneKey } : {}),
...(startupTerminalPtyId ? { ptyId: startupTerminalPtyId } : {}),
surface: 'background' as const
surface: 'background' as const,
...(args.startupAppliedSessionOptions
? { appliedSessionOptions: args.startupAppliedSessionOptions }
: {})
}
}
: resultForRenderer
@@ -0,0 +1,46 @@
import { z } from 'zod'
import { isTuiAgent } from '../../../../shared/tui-agent-config'
import type { AgentLaunchSessionOptions, TuiAgent } from '../../../../shared/types'
const MAX_LAUNCH_OPTION_LENGTH = 512
const MAX_SESSION_OPTION_COUNT = 32
const LaunchOption = (label: string) =>
z
.string()
.min(1, `Invalid ${label}`)
.max(MAX_LAUNCH_OPTION_LENGTH, `Invalid ${label}`)
.refine(
(value) => value === value.trim(),
`Invalid ${label}; surrounding whitespace is invalid`
)
const SessionOptionKey = LaunchOption('session option key')
.regex(/^[A-Za-z][A-Za-z0-9_.-]*$/, 'Invalid session option key')
.refine((value) => !['__proto__', 'constructor', 'prototype'].includes(value), {
message: 'Invalid session option key'
})
export const AgentLaunchPreferencesSchema = z
.object({
model: LaunchOption('model preference').optional(),
effort: LaunchOption('effort preference').optional(),
mode: LaunchOption('mode preference').optional()
})
.strict()
export const AgentSessionOptionsSchema = z
.record(SessionOptionKey, z.union([LaunchOption('session option value'), z.boolean()]))
.refine((value) => Object.keys(value).length > 0, {
message: 'Session options must not be empty'
})
.refine((value) => Object.keys(value).length <= MAX_SESSION_OPTION_COUNT, {
message: 'Too many session options'
})
export const AgentLaunchSessionOptionsSchema: z.ZodType<AgentLaunchSessionOptions> = z
.object({
agent: z.custom<TuiAgent>((value) => isTuiAgent(value), 'Unknown TUI agent'),
values: AgentSessionOptionsSchema
})
.strict()
+3 -17
View File
@@ -19,12 +19,12 @@ import { isTuiAgent } from '../../../../shared/tui-agent-config'
import { isValidTerminalTabId } from '../../../../shared/terminal-tab-id'
import type { OrcaRuntimeService } from '../../orca-runtime'
import { defineMethod, type RpcAnyMethod } from '../core'
import { AgentLaunchPreferencesSchema } from './agent-launch-option-schemas'
const MAX_WORKTREE_SELECTOR_LENGTH = 32_768
const MAX_TRANSCRIPT_PATH_BYTES = 16 * 1024
const MAX_PROMPT_BYTES = 256 * 1024
const MAX_AGENT_ARGS_BYTES = 16 * 1024
const MAX_LAUNCH_PREFERENCE_LENGTH = 512
const StrictNonEmptyString = (max: number, message: string) =>
z
@@ -55,20 +55,6 @@ const Placement = z
message: 'Placement must include a tab or leaf ID'
})
const LaunchPreferences = z
.object({
model: StrictNonEmptyString(
MAX_LAUNCH_PREFERENCE_LENGTH,
'Invalid model preference'
).optional(),
effort: StrictNonEmptyString(
MAX_LAUNCH_PREFERENCE_LENGTH,
'Invalid effort preference'
).optional(),
mode: StrictNonEmptyString(MAX_LAUNCH_PREFERENCE_LENGTH, 'Invalid mode preference').optional()
})
.strict()
const PromptDelivery = z.enum(['auto-submit', 'draft'])
const AgentArgs = z
@@ -131,7 +117,7 @@ const ExplicitEnsure = z
providerSession: ProviderSession,
ompResumeFilePath: OmpResumeFilePath.optional(),
agentArgs: AgentArgs.optional(),
launchPreferences: LaunchPreferences.optional(),
launchPreferences: AgentLaunchPreferencesSchema.optional(),
presentation: Presentation.optional(),
placement: Placement.optional()
})
@@ -175,7 +161,7 @@ export const CreateAgentSessionParams: z.ZodType<RuntimeCreateAgentSessionReques
.optional(),
promptDelivery: PromptDelivery.optional(),
agentArgs: AgentArgs.optional(),
launchPreferences: LaunchPreferences.optional(),
launchPreferences: AgentLaunchPreferencesSchema.optional(),
startupCwd: z.string().min(1).max(MAX_WORKTREE_SELECTOR_LENGTH).optional(),
presentation: Presentation.optional(),
placement: Placement.optional(),
@@ -14,6 +14,7 @@ import {
import { TaskSourceContextSchema } from '../../../../shared/task-source-context-schema'
import { WorkspaceLinkedItemSchema } from '../../../../shared/workspace-linked-item-schema'
import { isWorkspaceLinkedItemSourceContextMatch } from '../../../../shared/workspace-linked-item-source-context'
import { AgentLaunchSessionOptionsSchema } from './agent-launch-option-schemas'
const OptionalTuiAgent = z
.unknown()
@@ -180,6 +181,7 @@ export const WorktreeCreate = z
// Why: task-driven mobile creates need desktop parity: the host chooses
// the same default/detected agent and drafts the linked issue/PR URL into it.
startupDraft: OptionalString,
startupSessionOptions: AgentLaunchSessionOptionsSchema.optional(),
createdWithAgent: z
.unknown()
.transform((value) => (isTuiAgent(value) ? value : undefined))
@@ -210,6 +212,13 @@ export const WorktreeCreate = z
message: 'startupPrompt requires startupAgent'
})
}
if (params.startupSessionOptions !== undefined && params.startupDraft === undefined) {
ctx.addIssue({
code: z.ZodIssueCode.custom,
path: ['startupSessionOptions'],
message: 'startupSessionOptions requires startupDraft'
})
}
})
export const WorktreePrefetchCreateBase = z.object({
@@ -588,6 +588,10 @@ describe('worktree RPC methods', () => {
repo: 'repo-1',
name: 'issue-123',
startupDraft: 'https://github.com/stablyai/orca/issues/123',
startupSessionOptions: {
agent: 'codex',
values: { model: 'gpt-5.6-sol', effort: 'high', fastMode: true }
},
createdWithAgent: 'codex',
activate: true
})
@@ -600,7 +604,11 @@ describe('worktree RPC methods', () => {
activate: true,
createdWithAgent: 'codex',
startup: undefined,
startupDraft: 'https://github.com/stablyai/orca/issues/123'
startupDraft: 'https://github.com/stablyai/orca/issues/123',
startupSessionOptions: {
agent: 'codex',
values: { model: 'gpt-5.6-sol', effort: 'high', fastMode: true }
}
})
)
})
+3
View File
@@ -158,6 +158,9 @@ export const WORKTREE_METHODS: RpcMethod[] = [
}
: undefined,
...(params.startupAgent ? { startupAgent: params.startupAgent } : {}),
...(params.startupSessionOptions
? { startupSessionOptions: params.startupSessionOptions }
: {}),
...(params.startupPrompt !== undefined ? { startupPrompt: params.startupPrompt } : {}),
startupDraft: params.startupDraft,
lineage: {
+28
View File
@@ -180,6 +180,10 @@ describe('RPC optional pipe schemas', () => {
setupDecision: 'run',
activate: true,
startupDraft: 'https://github.com/acme/app/pull/123',
startupSessionOptions: {
agent: 'codex',
values: { model: 'gpt-5.6-sol', effort: 'high', fastMode: true }
},
createdWithAgent: 'codex',
linkedPR: 123,
baseBranch: 'origin/main',
@@ -187,6 +191,30 @@ describe('RPC optional pipe schemas', () => {
branchNameOverride: 'feature/mobile-tasks',
pushTarget: { remoteName: 'origin', branchName: 'feature/mobile-tasks' }
})
expectRejects(create, {
repo: 'id:repo-github',
startupDraft: 'https://github.com/acme/app/pull/123',
startupSessionOptions: { agent: 'codex', values: { model: ' gpt-5.6-sol' } }
})
expectRejects(create, {
repo: 'id:repo-github',
startupDraft: 'https://github.com/acme/app/pull/123',
startupSessionOptions: { agent: 'codex', values: { constructor: 'polluted' } }
})
expectRejects(create, {
repo: 'id:repo-github',
startupSessionOptions: { agent: 'codex', values: { model: 'gpt-5.6-sol' } }
})
expectRejects(create, {
repo: 'id:repo-github',
startupDraft: 'https://github.com/acme/app/pull/123',
startupSessionOptions: { values: { model: 'gpt-5.6-sol' } }
})
expectRejects(create, {
repo: 'id:repo-github',
startupDraft: 'https://github.com/acme/app/pull/123',
startupSessionOptions: { agent: 'not-an-agent', values: { model: 'gpt-5.6-sol' } }
})
expectParses(create, {
repo: 'id:repo-gitlab',
name: 'mr-7',
@@ -751,6 +751,10 @@ describe('useComposerState host-context boundaries', () => {
)
expect(fullSubmit).toContain('platform: selectedRepoAgentLaunchPlatform')
expect(fullSubmit).toContain('startupDraft: startupPlan.draftPrompt')
expect(fullSubmit).toContain(
'startupSessionOptions: { agent: tuiAgent, values: startupPlan.sessionOptions }'
)
expect(fullSubmit).toContain('resolveCreatedAgentAppliedSessionOptions')
expect(fullSubmit).not.toContain('platform: CLIENT_PLATFORM')
const quickSubmit = sourceBetween(
+24 -2
View File
@@ -15,6 +15,7 @@ import {
} from '@/lib/github-links'
import { activateAndRevealWorktree, type AgentStartedTelemetry } from '@/lib/worktree-activation'
import { runBackgroundWorktreeCreation } from '@/lib/worktree-creation-flow'
import { resolveCreatedAgentAppliedSessionOptions } from '@/lib/worktree-creation-agent-seeds'
import {
findPendingLinkedWorkItemCreationId,
type WorktreeCreationRequest
@@ -3851,7 +3852,14 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
linkedWorkItem: toFolderWorkspaceLinkedTask(submitLinkedWorkItem),
linkedTaskSourceContext: taskSourceContext,
...(!backendStartup && startupPlan?.draftPrompt
? { startupDraft: startupPlan.draftPrompt }
? {
startupDraft: startupPlan.draftPrompt,
...(startupPlan.sessionOptions
? {
startupSessionOptions: { agent: tuiAgent, values: startupPlan.sessionOptions }
}
: {})
}
: {})
}
)
@@ -3907,10 +3915,24 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
: {})
})
if (startupPlan) {
const createdAgent =
backendSpawnedStartup && !backendStartup
? (worktree.createdWithAgent ?? tuiAgent)
: tuiAgent
const optionScopeKey =
(activation !== false ? activation.primaryTabId : null) ?? result.startupTerminal?.tabId
if (optionScopeKey) {
seedNativeChatAppliedSessionOptions(optionScopeKey, tuiAgent, startupPlan.sessionOptions)
seedNativeChatAppliedSessionOptions(
optionScopeKey,
createdAgent,
resolveCreatedAgentAppliedSessionOptions({
agent: createdAgent,
backendSpawned: backendSpawnedStartup,
clientBuiltStartup: Boolean(backendStartup),
clientSessionOptions: startupPlan.sessionOptions,
hostAppliedSessionOptions: result.startupTerminal?.appliedSessionOptions
})
)
}
}
if (startupPlan && !backendSpawnedStartup) {
@@ -1,7 +1,10 @@
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { useAppStore } from '@/store'
import { resetHookCommandDelayedDeliveryForTests } from './hook-command-delayed-delivery'
import { seedAgentTabStateAfterWorktreeCreate } from './worktree-creation-agent-seeds'
import {
resolveCreatedAgentAppliedSessionOptions,
seedAgentTabStateAfterWorktreeCreate
} from './worktree-creation-agent-seeds'
const mocks = vi.hoisted(() => ({
seedNativeChatAppliedSessionOptions: vi.fn(),
@@ -108,6 +111,46 @@ afterEach(() => {
})
describe('seedAgentTabStateAfterWorktreeCreate', () => {
it('requires acknowledgement only when the host built the launch command', () => {
const clientSessionOptions = { model: 'client-model', effort: 'high' }
const hostAppliedSessionOptions = { model: 'host-confirmed-model' }
expect(
resolveCreatedAgentAppliedSessionOptions({
agent: 'claude',
backendSpawned: true,
clientBuiltStartup: false,
clientSessionOptions,
hostAppliedSessionOptions: { agent: 'claude', values: hostAppliedSessionOptions }
})
).toEqual(hostAppliedSessionOptions)
expect(
resolveCreatedAgentAppliedSessionOptions({
agent: 'claude',
backendSpawned: true,
clientBuiltStartup: false,
clientSessionOptions
})
).toBeUndefined()
expect(
resolveCreatedAgentAppliedSessionOptions({
agent: 'claude',
backendSpawned: true,
clientBuiltStartup: false,
clientSessionOptions,
hostAppliedSessionOptions: { agent: 'codex', values: clientSessionOptions }
})
).toBeUndefined()
expect(
resolveCreatedAgentAppliedSessionOptions({
agent: 'claude',
backendSpawned: true,
clientBuiltStartup: true,
clientSessionOptions
})
).toEqual(clientSessionOptions)
})
it('seeds the backend-spawned startup tab, not the first default terminal tab', () => {
// Repo default tabs (dev server / logs / shell) run no agent; on the
// backend-spawn path none of them carries launchAgent either.
@@ -129,6 +172,93 @@ describe('seedAgentTabStateAfterWorktreeCreate', () => {
)
})
it('seeds only host-confirmed options for a host-built startup', () => {
setTabs([{ id: 'agent-tab' }])
const requestWithOptions = {
...request,
startupPlan: {
agent: 'claude',
launchCommand: 'claude',
sessionOptions: { model: 'client-model', effort: 'high' }
} as never
}
seedAgentTabStateAfterWorktreeCreate({
request: requestWithOptions,
worktreeId: 'wt-1',
primaryTabId: 'agent-tab',
startupTerminalTabId: 'agent-tab',
backendSpawned: true,
hostAppliedSessionOptions: {
agent: 'claude',
values: { model: 'host-confirmed-model' }
}
})
expect(mocks.seedNativeChatAppliedSessionOptions).toHaveBeenCalledWith('agent-tab', 'claude', {
model: 'host-confirmed-model'
})
})
it('does not claim client options when an older host returns no acknowledgement', () => {
setTabs([{ id: 'agent-tab' }])
const requestWithOptions = {
...request,
startupPlan: {
agent: 'claude',
launchCommand: 'claude',
sessionOptions: { model: 'client-model', effort: 'high' }
} as never
}
seedAgentTabStateAfterWorktreeCreate({
request: requestWithOptions,
worktreeId: 'wt-1',
primaryTabId: 'agent-tab',
startupTerminalTabId: 'agent-tab',
backendSpawned: true
})
expect(mocks.seedNativeChatAppliedSessionOptions).toHaveBeenCalledWith(
'agent-tab',
'claude',
undefined
)
})
it('uses the host fallback agent without applying requested-agent options', () => {
setTabs([{ id: 'agent-tab' }])
const codexRequest = {
...request,
agent: 'codex' as const,
startupPlan: {
agent: 'codex',
launchCommand: 'codex',
sessionOptions: { model: 'gpt-5.6-sol', effort: 'high' }
} as never
}
seedAgentTabStateAfterWorktreeCreate({
request: codexRequest,
worktreeId: 'wt-1',
primaryTabId: 'agent-tab',
startupTerminalTabId: 'agent-tab',
backendSpawned: true,
backendAgent: 'claude'
})
expect(mocks.seedNativeChatAppliedSessionOptions).toHaveBeenCalledWith(
'agent-tab',
'claude',
undefined
)
expect(mocks.seedNativeChatLaunchDraftForAgentTab).toHaveBeenCalledWith({
tabId: 'agent-tab',
agent: 'claude',
text: DRAFT
})
})
it('moves a backend-spawned non-mirrorable draft out of an inherited chat view', () => {
setTabs([{ id: 'agent-tab', launchAgent: 'claude', viewMode: 'chat' }])
@@ -9,12 +9,28 @@ import { nativeChatRequiresLocalTranscript } from '@/lib/native-chat-supported-a
import { getRuntimeEnvironmentIdForWorktree } from '@/lib/worktree-runtime-owner'
import { toWebTerminalSurfaceTabId } from '@/runtime/web-terminal-surface-id'
import type { WorktreeCreationRequest } from '@/lib/pending-worktree-creation'
import type { TuiAgent } from '../../../shared/types'
import type { AgentLaunchSessionOptions, TuiAgent } from '../../../shared/types'
import type { SessionOptionValue } from '../../../shared/native-chat-session-options'
type AppStoreSnapshot = ReturnType<typeof useAppStore.getState>
type SeedRequest = Pick<WorktreeCreationRequest, 'agent' | 'startupPlan' | 'launchDraftPrompt'>
export function resolveCreatedAgentAppliedSessionOptions(args: {
agent: TuiAgent
backendSpawned: boolean
clientBuiltStartup: boolean
clientSessionOptions?: Record<string, SessionOptionValue>
hostAppliedSessionOptions?: AgentLaunchSessionOptions
}): Record<string, SessionOptionValue> | undefined {
if (!args.backendSpawned || args.clientBuiltStartup) {
return args.clientSessionOptions
}
return args.hostAppliedSessionOptions?.agent === args.agent
? args.hostAppliedSessionOptions.values
: undefined
}
/**
* Resolve the tab the launch agent actually runs in.
*
@@ -92,10 +108,15 @@ function applyAgentTabSeeds(args: {
tabId: string
worktreeId: string
backendSpawned: boolean
backendAppliedSessionOptions?: Record<string, SessionOptionValue>
}): void {
const { request, agent, tabId } = args
const { request, agent, tabId, backendSpawned, backendAppliedSessionOptions } = args
applyBackendSpawnedDraftViewMode(args)
seedNativeChatAppliedSessionOptions(tabId, agent, request.startupPlan?.sessionOptions)
seedNativeChatAppliedSessionOptions(
tabId,
agent,
backendSpawned ? backendAppliedSessionOptions : request.startupPlan?.sessionOptions
)
// Why: draft launch context reaches only the TUI input; seed the
// chat-composer copy so it isn't invisible in the chat view.
if (request.launchDraftPrompt) {
@@ -113,16 +134,38 @@ export function seedAgentTabStateAfterWorktreeCreate(args: {
primaryTabId: string | null
startupTerminalTabId: string | null | undefined
backendSpawned: boolean
backendAgent?: TuiAgent
clientBuiltStartup?: boolean
hostAppliedSessionOptions?: AgentLaunchSessionOptions
}): void {
const { request, worktreeId, backendSpawned } = args
const agent = request.agent
if (!request.startupPlan || !agent) {
const requestedAgent = request.agent
if (!request.startupPlan || !requestedAgent) {
return
}
const agent =
backendSpawned && !args.clientBuiltStartup
? (args.backendAgent ?? requestedAgent)
: requestedAgent
const backendAppliedSessionOptions = resolveCreatedAgentAppliedSessionOptions({
agent,
backendSpawned,
clientBuiltStartup: Boolean(args.clientBuiltStartup),
clientSessionOptions: request.startupPlan.sessionOptions,
hostAppliedSessionOptions: args.hostAppliedSessionOptions
})
const state = useAppStore.getState()
const tabId = resolveLaunchAgentTabId(state, { ...args, agent })
if (tabId) {
applyAgentTabSeeds({ state, request, agent, tabId, worktreeId, backendSpawned })
applyAgentTabSeeds({
state,
request,
agent,
tabId,
worktreeId,
backendSpawned,
backendAppliedSessionOptions
})
return
}
if ((state.tabsByWorktree[worktreeId] ?? []).length > 0) {
@@ -144,7 +187,8 @@ export function seedAgentTabStateAfterWorktreeCreate(args: {
agent,
tabId: mirroredTabId,
worktreeId,
backendSpawned
backendSpawned,
backendAppliedSessionOptions
})
return
}
@@ -159,7 +203,8 @@ export function seedAgentTabStateAfterWorktreeCreate(args: {
agent,
tabId: firstTerminalTabId,
worktreeId,
backendSpawned
backendSpawned,
backendAppliedSessionOptions
})
}
}
@@ -0,0 +1,27 @@
import { describe, expect, it } from 'vitest'
import type { WorktreeCreationRequest } from './pending-worktree-creation'
import { buildHostDraftStartupOptions } from './worktree-creation-flow-startup'
const request = {
agent: 'codex',
launchDraftPrompt: 'https://github.com/stablyai/orca/issues/12',
startupPlan: {
sessionOptions: { model: 'gpt-5.6-sol', effort: 'high' }
}
} as unknown as WorktreeCreationRequest
describe('buildHostDraftStartupOptions', () => {
it('scopes launch options to the requested agent', () => {
expect(buildHostDraftStartupOptions(request, false)).toEqual({
startupDraft: request.launchDraftPrompt,
startupSessionOptions: {
agent: 'codex',
values: { model: 'gpt-5.6-sol', effort: 'high' }
}
})
})
it('omits host-owned startup when the client already built it', () => {
expect(buildHostDraftStartupOptions(request, true)).toEqual({})
})
})
@@ -5,6 +5,27 @@ import type {
WorktreeCreationPhase,
WorktreeCreationRequest
} from '@/lib/pending-worktree-creation'
import type { AgentLaunchSessionOptions } from '../../../shared/types'
export function buildHostDraftStartupOptions(
request: WorktreeCreationRequest,
clientBuiltStartup: boolean
): { startupDraft?: string; startupSessionOptions?: AgentLaunchSessionOptions } {
if (clientBuiltStartup || !request.agent || !request.launchDraftPrompt) {
return {}
}
return {
startupDraft: request.launchDraftPrompt,
...(request.startupPlan?.sessionOptions
? {
startupSessionOptions: {
agent: request.agent,
values: request.startupPlan.sessionOptions
}
}
: {})
}
}
// Why: mirrors the startup-opt the composer used to build inline. The renderer
// only seeds the first terminal when the backend did not already spawn it.
@@ -739,10 +739,6 @@ describe('staged background worktree creation', () => {
expect(store.seedNativeChatLaunchDraft).toHaveBeenCalledWith(
expect.objectContaining({ tabId: 'agent-tab' })
)
const createCall = store.createWorktree.mock.calls[0] as unknown[] | undefined
expect(createCall?.[25]).toEqual({
startupDraft: 'https://github.com/o/r/issues/12'
})
})
it.each([
@@ -27,6 +27,7 @@ import { createBrowserUuid } from '@/lib/browser-uuid'
import { seedAgentTabStateAfterWorktreeCreate } from '@/lib/worktree-creation-agent-seeds'
import { resolveBackendDraftStartup } from '@/lib/worktree-draft-startup-view-mode'
import {
buildHostDraftStartupOptions,
buildWorktreeCreationStartupOpt,
getInitialWorktreeCreationPhase,
getWorktreeCreationIndeterminate
@@ -103,9 +104,9 @@ async function executeWorktreeCreation(
return
}
const backendStartup = resolveBackendDraftStartup(preparedRequest)
let result: CreateWorktreeResult
try {
const backendStartup = resolveBackendDraftStartup(preparedRequest)
result = await useAppStore
.getState()
.createWorktree(
@@ -141,10 +142,7 @@ async function executeWorktreeCreation(
...(preparedRequest.linkedTaskSourceContext !== undefined
? { linkedTaskSourceContext: preparedRequest.linkedTaskSourceContext }
: {}),
// Why: the remote host must own task-draft startup so its initial terminal is the agent, not an idle fallback shell.
...(!backendStartup && preparedRequest.agent && preparedRequest.launchDraftPrompt
? { startupDraft: preparedRequest.launchDraftPrompt }
: {})
...buildHostDraftStartupOptions(preparedRequest, Boolean(backendStartup))
}
)
} catch (error) {
@@ -243,7 +241,10 @@ async function executeWorktreeCreation(
worktreeId: worktree.id,
primaryTabId,
startupTerminalTabId: result.startupTerminal?.tabId,
backendSpawned
backendSpawned,
backendAgent: result.worktree.createdWithAgent,
clientBuiltStartup: Boolean(backendStartup),
hostAppliedSessionOptions: result.startupTerminal?.appliedSessionOptions
})
if (preparedRequest.startupPlan && !backendSpawned) {
void ensureAgentStartupInTerminal({
@@ -1,6 +1,7 @@
import type {
CreateWorktreeResult,
CreateWorktreeArgs,
AgentLaunchSessionOptions,
CreateSparseCheckoutRequest,
DetectedWorktree,
DetectedWorktreeListResult,
@@ -200,6 +201,8 @@ export type WorktreeSlice = {
linkedTaskSourceContext?: TaskSourceContext | null
/** Lets the owning runtime launch and prefill a task agent without first creating an idle shell. */
startupDraft?: string
/** Picker values the owning runtime should apply while building that launch. */
startupSessionOptions?: AgentLaunchSessionOptions
}
) => Promise<CreateWorktreeResult>
/** Register an in-flight background creation and make it the active surface. */
@@ -5933,7 +5933,13 @@ describe('worktree remote runtime mutations', () => {
const createWorktree = store.getState().createWorktree
const args: Parameters<typeof createWorktree> = ['repo1', 'task-draft', undefined, 'inherit']
args[10] = 'codex'
args[25] = { startupDraft: 'https://github.com/stablyai/orca/issues/12' }
args[25] = {
startupDraft: 'https://github.com/stablyai/orca/issues/12',
startupSessionOptions: {
agent: 'codex',
values: { model: 'gpt-5.6-sol', effort: 'high', fastMode: true }
}
}
await createWorktree(...args)
@@ -5942,7 +5948,11 @@ describe('worktree remote runtime mutations', () => {
method: 'worktree.create',
params: expect.objectContaining({
createdWithAgent: 'codex',
startupDraft: 'https://github.com/stablyai/orca/issues/12'
startupDraft: 'https://github.com/stablyai/orca/issues/12',
startupSessionOptions: {
agent: 'codex',
values: { model: 'gpt-5.6-sol', effort: 'high', fastMode: true }
}
})
})
)
@@ -3990,6 +3990,7 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
const linkedWorkItem = options?.linkedWorkItem
const linkedTaskSourceContext = options?.linkedTaskSourceContext
const startupDraft = options?.startupDraft
const startupSessionOptions = options?.startupSessionOptions
try {
for (let attempt = 0; attempt < CLIENT_WORKTREE_CREATE_MAX_ATTEMPTS; attempt += 1) {
const candidateName = getClientWorktreeCreateCandidate(name, attempt)
@@ -4097,6 +4098,7 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
...(linkedWorkItem !== undefined ? { linkedWorkItem } : {}),
...(linkedTaskSourceContext !== undefined ? { linkedTaskSourceContext } : {}),
...(startupDraft ? { startupDraft } : {}),
...(startupSessionOptions ? { startupSessionOptions } : {}),
...(automationProvenanceRequest ? { automationProvenanceRequest } : {}),
...(startup
? {
@@ -48,6 +48,12 @@ export function resolveAgentSessionOptionLaunch(
modelOptions.flatMap((option) => {
const explicitValue = values[option.id]
if (explicitValue !== undefined) {
if (
(option.kind.type === 'boolean' && typeof explicitValue !== 'boolean') ||
(option.kind.type === 'select' && typeof explicitValue !== 'string')
) {
return []
}
if (
!model &&
option.kind.type === 'select' &&
@@ -54,6 +54,24 @@ describe('tui agent startup session options', () => {
expect(plan?.sessionOptions).toEqual({ model: 'custom-codex-model', effort: 'high' })
})
it('quotes overridden task preferences for a Windows host shell', () => {
const plan = buildAgentStartupPlan({
agent: 'codex',
prompt: '',
cmdOverrides: {},
platform: 'win32',
allowEmptyPromptLaunch: true,
sessionOptions: { model: 'gpt-5.6-sol', effort: 'high' },
sessionOptionsOverrideAgentArgs: true,
agentArgs:
'-m host-model -c model_reasoning_effort=low --dangerously-bypass-approvals-and-sandbox'
})
expect(plan?.launchCommand).toBe(
"codex '--dangerously-bypass-approvals-and-sandbox' '-m' 'gpt-5.6-sol' '-c' 'model_reasoning_effort=high'"
)
expect(plan?.sessionOptions).toEqual({ model: 'gpt-5.6-sol', effort: 'high' })
})
it('inserts worker preferences before an argument terminator', () => {
const plan = buildAgentStartupPlan({
agent: 'codex',
@@ -140,6 +158,21 @@ describe('tui agent startup session options', () => {
expect(plan?.sessionOptions).toEqual({ model: 'opus', effort: 'high' })
})
it('lets host-built drafts override host defaults with client preferences', () => {
const plan = buildAgentDraftLaunchPlan({
agent: 'claude',
draft: 'review this',
cmdOverrides: {},
platform: 'linux',
sessionOptions: { model: 'opus', effort: 'high' },
sessionOptionsOverrideAgentArgs: true,
agentArgs: '--model haiku --effort low'
})
expect(plan?.launchCommand).toContain("claude '--model' 'opus' '--effort' 'high'")
expect(plan?.launchCommand).not.toContain('haiku')
expect(plan?.sessionOptions).toEqual({ model: 'opus', effort: 'high' })
})
it('never injects session options into resume commands', () => {
const plan = buildAgentResumeStartupPlan({
agent: 'codex',
@@ -151,4 +184,21 @@ describe('tui agent startup session options', () => {
expect(plan?.launchCommand).toBe("codex 'resume' 'thread-1'")
expect(plan?.sessionOptions).toBeUndefined()
})
it('never acknowledges an option whose value has the wrong catalog type', () => {
const resolved = resolveAgentLaunchCommand({
agent: 'cursor',
cmdOverrides: {},
platform: 'linux',
shell: 'posix',
sessionOptions: { model: 'gpt-5.3-codex', fastMode: 'false' },
sessionOptionsOverrideAgentArgs: true
})
expect(resolved.ok).toBe(true)
if (resolved.ok) {
expect(resolved.command).not.toContain('-fast')
expect(resolved.appliedSessionOptions).toEqual({ model: 'gpt-5.3-codex' })
}
})
})
+3 -2
View File
@@ -1,4 +1,4 @@
import { isShellProcess } from './agent-detection'
export { isShellProcess } from './agent-detection'
import {
getAgentResumeArgv,
type AgentProviderSessionMetadata,
@@ -256,6 +256,7 @@ export function buildAgentDraftLaunchPlan(args: {
agentArgs?: string | null
agentEnv?: Record<string, string> | null
sessionOptions?: Record<string, SessionOptionValue>
sessionOptionsOverrideAgentArgs?: boolean
/** Why: see buildAgentStartupPlan — remote launches use the plain `orca` shim. */
isRemote?: boolean
}): AgentDraftLaunchPlan | null {
@@ -273,6 +274,7 @@ export function buildAgentDraftLaunchPlan(args: {
shell,
agentArgs: args.agentArgs,
sessionOptions: args.sessionOptions,
sessionOptionsOverrideAgentArgs: args.sessionOptionsOverrideAgentArgs,
isRemote: args.isRemote
})
if (!baseCommand.ok) {
@@ -316,7 +318,6 @@ export function buildAgentDraftLaunchPlan(args: {
return plan
}
export { isShellProcess }
export {
buildShellCommandFromArgv,
planAgentCliArgsSuffix,
+11 -1
View File
@@ -48,7 +48,10 @@ import type {
} from './project-execution-runtime'
import type { UsagePercentageDisplay } from './usage-percentage-display'
import type { StatusBarUsageMode } from './status-bar-usage-mode'
import type { PersistedNativeChatSessionOptions } from './native-chat-session-options'
import type {
PersistedNativeChatSessionOptions,
SessionOptionValue
} from './native-chat-session-options'
import type { CodexResetCreditAttemptLedger } from './codex-reset-credit-attempt-ledger'
import type { TaskSourceContext } from './task-source-context'
import type { SetupRunnerShell } from './setup-runner-command'
@@ -2326,6 +2329,8 @@ export type CreateWorktreeResult = {
paneKey?: string | null
ptyId?: string | null
surface?: 'visible' | 'background'
/** Host-confirmed agent and picker values emitted into the startup command. */
appliedSessionOptions?: AgentLaunchSessionOptions
}
timing?: WorktreeCreateTiming
}
@@ -2627,6 +2632,11 @@ export type TuiAgent =
| 'trae' // Trae CLI
| 'prime-agent' // Prime Agent (Prime Intellect)
export type AgentLaunchSessionOptions = {
agent: TuiAgent
values: Record<string, SessionOptionValue>
}
export type TaskViewPresetId = 'all' | 'issues' | 'review' | 'my-issues' | 'my-prs' | 'prs'
/** Where the repo setup script runs when a worktree is created.