mirror of
https://github.com/stablyai/orca.git
synced 2026-10-21 00:03:46 +00:00
fix(workspaces): preserve remote task launch options
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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' })
|
||||
})
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -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 }
|
||||
}
|
||||
})
|
||||
)
|
||||
})
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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' })
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user