Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
72 commits
Select commit Hold shift + click to select a range
bc8b39a
Source control actions start chats and terminals on their saved model
brennanb2025 Oct 18, 2026
268f78c
Merge remote-tracking branch 'origin/main' into brennan/sta-11102-cha…
brennanb2025 Oct 18, 2026
d3f1a6e
Name the model in the repository override note
brennanb2025 Oct 18, 2026
b3de904
Type the dialog's model hand-off; keep Agent in English in Japanese copy
brennanb2025 Oct 18, 2026
58a03f2
fix: preserve action chat model choices through launch
brennanb2025 Oct 19, 2026
67e02bc
Preserve action selection ownership across chat launches
brennanb2025 Oct 19, 2026
27d7e93
Keep terminal host contract fixtures aligned with preflight
brennanb2025 Oct 19, 2026
b1ec857
fix: wait for action Settings agent inventory
brennanb2025 Oct 19, 2026
990f0ba
Merge main into action chat launch fixes
brennanb2025 Oct 19, 2026
ef198aa
fix: consolidate direct agent routing imports
brennanb2025 Oct 19, 2026
b2d6aa1
fix: use standard action option labels
brennanb2025 Oct 19, 2026
b9c66df
fix: align action launch contracts with shared types
brennanb2025 Oct 19, 2026
9841b88
fix: sync action option localization metadata
brennanb2025 Oct 19, 2026
c3dc7bf
test: provide action Settings inventory in mounted fixtures
brennanb2025 Oct 19, 2026
4cbe2e6
Keep action options row height stable while loading
brennanb2025 Oct 19, 2026
efe329d
Merge remote-tracking branch 'origin/main' into brennan/sta-11102-cha…
brennanb2025 Oct 19, 2026
6c7a7d9
Let each Source Control action choose its chat permission mode
brennanb2025 Oct 19, 2026
3cadfcf
Merge remote-tracking branch 'origin/main' into brennan/sta-11102-cha…
brennanb2025 Oct 19, 2026
ec6b407
fix(native-chat): leave older hosts their own permission mode and kee…
brennanb2025 Oct 19, 2026
663ce7f
fix(settings): reserve one options row while loading an action with n…
brennanb2025 Oct 19, 2026
96d88d6
fix(native-chat): keep the create options mode a checked literal type
brennanb2025 Oct 19, 2026
0e81f75
fix(settings): share verified action option fields
brennanb2025 Oct 20, 2026
773d306
Merge current main into action options fix
brennanb2025 Oct 20, 2026
f05ba6c
fix(settings): keep action selectors on standard typography
brennanb2025 Oct 20, 2026
ad8ce46
Merge current main into action launch options
brennanb2025 Oct 20, 2026
e93fdfc
Merge remote-tracking branch 'origin/main' into brennan/sta-11102-cha…
brennanb2025 Oct 20, 2026
5a914f5
Keep the launch files under their length limits after the main merge
brennanb2025 Oct 20, 2026
ea5c913
One action settings section for chat and terminal launches
brennanb2025 Oct 21, 2026
eee1ff5
Merge remote-tracking branch 'origin/main' into brennan/sta-11102-cha…
brennanb2025 Oct 21, 2026
9509c76
fix(source-control-ai): one terminal projection for saved action choices
brennanb2025 Oct 21, 2026
3f7de64
fix(source-control-ai): an edited repository action saves complete
brennanb2025 Oct 21, 2026
d91a0eb
fix(settings): style the Extra CLI arguments toggle through Button's …
brennanb2025 Oct 21, 2026
877891f
fix: keep saved action and restart launch recipes consistent
brennanb2025 Oct 21, 2026
0b7df87
fix: describe inherited arguments and chat-only permissions accurately
brennanb2025 Oct 21, 2026
57f0d06
Merge remote-tracking branch 'origin/main' into brennan/sta-11102-cha…
brennanb2025 Oct 21, 2026
505317a
test: check repository action settings fixture type
brennanb2025 Oct 21, 2026
d91139d
test: map saved action options into terminal compiler inputs
brennanb2025 Oct 21, 2026
36572cc
fix: retain saved action controls during agent detection
brennanb2025 Oct 22, 2026
cb256a4
Merge pinned main into action launch preferences
brennanb2025 Oct 22, 2026
5720cdd
fix(i18n): retire obsolete Portuguese action labels
brennanb2025 Oct 22, 2026
5cf3ada
test: align folder launch fallback mock with outcome export
brennanb2025 Oct 22, 2026
3cb6845
Merge main into saved action launch preferences
brennanb2025 Oct 22, 2026
aaeafd2
Merge origin/main into saved action launch preferences
brennanb2025 Oct 22, 2026
fdf410a
Merge origin/main into sta-11102 chat launch model branch
brennanb2025 Oct 23, 2026
2c22172
fix: carry resolved connector launch intent and mode together
brennanb2025 Oct 23, 2026
f44a2e2
test: pin shared action launch branches to recorded agent output
brennanb2025 Oct 23, 2026
97093ab
test: preserve saved source control choices through detection and mig…
brennanb2025 Oct 23, 2026
235430a
Merge main into saved agent launch preferences
brennanb2025 Oct 24, 2026
93fbd53
test: align launch fixtures with integrated result contract
brennanb2025 Oct 24, 2026
3300d51
Merge main to clear final integration conflict
brennanb2025 Oct 24, 2026
978c60a
test: retain recipe and restart contracts after main integration
brennanb2025 Oct 24, 2026
f997db4
Merge main while preserving saved agent launch choices
brennanb2025 Oct 25, 2026
660243e
Merge main and preserve action launch permission precedence
brennanb2025 Oct 25, 2026
abe3fa2
Unify Source Control action permission settings
brennanb2025 Oct 26, 2026
b7fde11
Keep native Ask copy with its owning change
brennanb2025 Oct 26, 2026
556ff0d
Merge main into Source Control launch settings
brennanb2025 Oct 26, 2026
a6896a5
Preserve action drafts and old-host permission intent
brennanb2025 Oct 26, 2026
7805b18
Consume rejected Source Control action saves
brennanb2025 Oct 26, 2026
6a9667d
Merge main to resolve Settings test conflicts
brennanb2025 Oct 26, 2026
e89a051
Follow the moved Settings persistence fixture
brennanb2025 Oct 26, 2026
4b1e5fe
Keep permission integration contracts in the cross-version test project
brennanb2025 Oct 26, 2026
03d298d
Include the action permission inheritance label in English catalogs
brennanb2025 Oct 26, 2026
5f39eae
Align launch contracts with shared permission recipes
brennanb2025 Oct 26, 2026
b3db16d
Merge main and preserve compiled terminal resume inputs
brennanb2025 Oct 26, 2026
1d78cad
Keep Source Control action controls within their Settings columns
brennanb2025 Oct 26, 2026
cfbcc5b
Use primitive variants for compact action settings controls
brennanb2025 Oct 26, 2026
654bfa5
Show action arguments only for terminal defaults and widen recipe rows
brennanb2025 Oct 26, 2026
31f5a68
Merge main and preserve explicit Source Control chat choices
brennanb2025 Oct 26, 2026
00bd1fb
Apply terminal-only action arguments visibility to repository overrides
brennanb2025 Oct 26, 2026
be14df4
Align action permission control with model controls
brennanb2025 Oct 26, 2026
2acf269
Use shared settings label for action permissions
brennanb2025 Oct 26, 2026
ea508af
Hide action model controls until argument migration ships
brennanb2025 Oct 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 6 additions & 2 deletions config/scripts/pr-unit-support-routing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {
collectUnitDependencyGraph,
unitConsumers
} from './ci-unit-dependency-graph.mjs'
import { discoverUnitFiles } from './ci-unit-files.mjs'
import { CROSS_VERSION_WIRE_DIR, discoverUnitFiles } from './ci-unit-files.mjs'
import { planUnitSelection } from './ci-unit-selection.mjs'

const harness = 'src/renderer/src/runtime/web-session-tabs-sync-test-harness.ts'
Expand Down Expand Up @@ -34,7 +34,11 @@ const supportBridges = new Set([

function productConsumers(file, graph, unitFiles) {
return [...unitConsumers([file], graph.reverse)].filter(
(consumer) => consumer !== file && !unitFiles.has(consumer) && !supportBridges.has(consumer)
(consumer) =>
consumer !== file &&
!unitFiles.has(consumer) &&
!(consumer.startsWith(CROSS_VERSION_WIRE_DIR) && consumer.endsWith('.unit.test.ts')) &&
!supportBridges.has(consumer)
)
}

Expand Down
22 changes: 19 additions & 3 deletions mobile/src/session/mobile-existing-agent-launch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,19 +209,35 @@ describe('launchAgentInExistingWorkspace', () => {
})

it('treats a refusal of the first send as an unsupported host', async () => {
const { client } = scriptedClient(refused('agent_launch_replay_unsupported'))
await expect(launch(client)).resolves.toEqual({ kind: 'unsupported' })
const { client, sendRequest } = scriptedClient(refused('agent_launch_replay_unsupported'))
await expect(
launch(client, { chatOptions: { model: 'opus', fastMode: 'false' } })
).resolves.toEqual({ kind: 'unsupported' })
expect(sendRequest.mock.calls[0]?.[1]).not.toHaveProperty('chatOptions')
})

it('never reads a refusal after a replay as unsupported: the first attempt may have run', async () => {
const { client, sendRequest } = scriptedClient(
{ throws: markRpcDeliveryUnknown(new Error('Connection interrupted')) },
refused('agent_launch_replay_unsupported')
)
await expect(launch(client)).resolves.toEqual({
const chatOptions = { model: 'opus', fastMode: 'false' }
const capabilities = [...LAUNCH_CAPABILITIES, 'agent.launch.chat-options.v1']
const pending = launch(client, { hostCapabilities: capabilities, chatOptions })
chatOptions.model = 'changed-after-send'
capabilities.length = 0
await expect(pending).resolves.toEqual({
kind: 'unknown',
message: AGENT_LAUNCH_UNCONFIRMED_MESSAGE
})
expect(sendRequest.mock.calls.map(([, params]) => requestParam(params, 'chatOptions'))).toEqual(
[
{ model: 'opus', fastMode: 'false' },
{ model: 'opus', fastMode: 'false' }
]
)
expect(sendRequest.mock.calls[0]?.[1]).toBe(sendRequest.mock.calls[1]?.[1])
// The replay reuses the same operation id.
expect(sendRequest.mock.calls.map(([, params]) => requestParam(params, 'operationId'))).toEqual(
['op-1', 'op-1']
)
Expand Down
9 changes: 9 additions & 0 deletions mobile/src/session/mobile-existing-agent-launch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// Send launch intent to the host, which selects terminal versus structured execution and prompt delivery.

import { AGENT_LAUNCH_CHAT_OPTIONS_RUNTIME_CAPABILITY } from '../../../src/shared/agent-launch-runtime-capability'
import type {
AgentLaunchPlacement,
AgentLaunchPrompt,
Expand Down Expand Up @@ -76,6 +77,9 @@ export async function launchAgentInExistingWorkspace(args: {
worktreeId: string
agent: TuiAgent
prompt?: AgentLaunchPrompt
/** Model and effort the host applies on whichever route it picks. */
sessionOptions?: Readonly<Record<string, string>>
chatOptions?: Readonly<Record<string, string>>
launchSource?: string
reservation?: MobileAgentLaunchReservation
placement?: AgentLaunchPlacement
Expand All @@ -89,6 +93,11 @@ export async function launchAgentInExistingWorkspace(args: {
worktreeId: args.worktreeId,
operationId: (args.mintOperationId ?? structuredSessionOperationId)(),
...(args.prompt ? { prompt: args.prompt } : {}),
...(args.sessionOptions ? { sessionOptions: args.sessionOptions } : {}),
...(args.chatOptions &&
args.hostCapabilities?.includes(AGENT_LAUNCH_CHAT_OPTIONS_RUNTIME_CAPABILITY)
? { chatOptions: args.chatOptions }
: {}),
Comment thread
brennanb2025 marked this conversation as resolved.
Resolved
...(args.launchSource ? { launchSource: args.launchSource } : {}),
...(args.placement ? { placement: args.placement } : {}),
...(args.reservation
Expand Down
5 changes: 5 additions & 0 deletions mobile/src/session/pr-ai-triage-launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
loadMobileAgentLaunchContext
} from './mobile-new-tab-agent-loader'
import { resolveMobileSourceControlLaunchAgent } from './mobile-source-control-launch-agent'
import { resolveSourceControlActionSessionOptions } from '../../../src/shared/source-control-action-launch-preferences'

// Have the host launch and deliver AI prompts; mobile must never type them into a shell.

Expand Down Expand Up @@ -68,11 +69,15 @@ export async function launchAgentWithPrompt(args: {
"This action's saved prompt is empty. Update Source Control AI settings on your computer."
}
}
const sessionOptions = recipe
? resolveSourceControlActionSessionOptions(recipe, resolved.agent)
: undefined
const launched = await launchAgentInExistingWorkspace({
client: args.client,
hostCapabilities: args.hostCapabilities,
worktreeId: args.worktreeId,
agent: resolved.agent,
...(sessionOptions ? { chatOptions: sessionOptions } : {}),
// Leave agent arguments to host defaults because applicability depends on the host-selected route and shell.
prompt: { text, delivery: 'submit' },
launchSource: args.launchSource
Expand Down
18 changes: 18 additions & 0 deletions mobile/src/tasks/agent-launch-request.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
agentLaunchCreateParams,
agentLaunchExistingParams,
isAgentLaunchUnsupportedRefusal,
readAgentLaunchCreateOutcome
} from './agent-launch-request'
Expand Down Expand Up @@ -36,6 +37,23 @@ describe('agentLaunchCreateParams', () => {
})
})

describe('agentLaunchExistingParams', () => {
const launch = { agent: 'codex', worktreeId: 'wt-1', operationId: 'op-1' } as const

it("sends an action's model and effort as session options", () => {
expect(
agentLaunchExistingParams({
...launch,
sessionOptions: { model: 'gpt-5.6-terra', effort: 'high' }
}).sessionOptions
).toEqual({ model: 'gpt-5.6-terra', effort: 'high' })
})

it('omits session options when the action saved no model', () => {
expect(agentLaunchExistingParams(launch)).not.toHaveProperty('sessionOptions')
})
})

describe('readAgentLaunchCreateOutcome', () => {
it.each([
{
Expand Down
4 changes: 4 additions & 0 deletions mobile/src/tasks/agent-launch-request.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,12 +61,16 @@ export function agentLaunchExistingParams(args: {
paneKey?: string
sessionId?: string
placement?: AgentLaunchPlacement
sessionOptions?: Readonly<Record<string, string>>
chatOptions?: Readonly<Record<string, string>>
}): RpcSendParams<'agent.launchReplay'> {
return {
agent: args.agent,
operationId: args.operationId,
target: { kind: 'existing', worktree: `id:${args.worktreeId}` },
...(args.prompt ? { prompt: args.prompt } : {}),
...(args.sessionOptions ? { sessionOptions: { ...args.sessionOptions } } : {}),
...(args.chatOptions ? { chatOptions: { ...args.chatOptions } } : {}),
...(args.launchSource ? { launchSource: args.launchSource } : {}),
...(args.paneKey ? { paneKey: args.paneKey } : {}),
...(args.sessionId ? { sessionId: args.sessionId } : {}),
Expand Down
20 changes: 18 additions & 2 deletions src/main/agent-launch/agent-launch-executor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,10 +375,17 @@ describe("a caller's chat default in place of the host's", () => {
describe('a launch into a workspace that already exists', () => {
it('opens a session without creating anything', async () => {
const h = harness({})
const result = await h.run({ agent: 'codex', target: { kind: 'existing', worktree: 'wt-7' } })
const result = await h.run({
agent: 'codex',
target: { kind: 'existing', worktree: 'wt-7' },
chatOptions: { model: 'action-model', fastMode: 'false' }
})
expect(h.calls).toEqual(['createSupport', 'createStructuredSession'])
expect(h.createWorktree).not.toHaveBeenCalled()
expect(result.worktreeId).toBe('wt-7')
expect(h.createStructuredSession).toHaveBeenCalledWith(
expect.objectContaining({ chatOptions: { model: 'action-model', fastMode: 'false' } })
)
})

it.each(['claude', 'grok'] as const)(
Expand All @@ -400,7 +407,16 @@ describe('a launch into a workspace that already exists', () => {
describe('an agent with no structured session', () => {
it('stays a terminal without asking the host', async () => {
const h = harness({})
const result = await h.run({ agent: 'gemini', target: { kind: 'existing', worktree: 'wt-7' } })
const result = await h.run({
agent: 'gemini',
target: { kind: 'existing', worktree: 'wt-7' },
chatOptions: { model: 'action-model', fastMode: 'false' }
})
expect(h.createTerminalAgent.mock.calls[0]?.[0]).not.toHaveProperty('chatOptions')
// The saved choice reaches the terminal too; its converter keeps only what has a flag.
expect(h.createTerminalAgent.mock.calls[0]?.[0]).toMatchObject({
options: { model: 'action-model', fastMode: 'false' }
})
expect(h.calls).toEqual(['createTerminalAgent'])
expect(result.receipt).toMatchObject({ reason: 'agent_without_structured_session' })
})
Expand Down
37 changes: 8 additions & 29 deletions src/main/agent-launch/agent-launch-executor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/** Ask after workspace creation: the host needs resolved metadata to decide chat support. */
import { decideAgentLaunchIntentMode } from './agent-launch-intent-mode'
/** Structured launches create the workspace without an agent, then ask its host for chat support.
* The host needs the created workspace to answer; a definitive refusal starts a terminal agent. */

import { assertOpenCodeModelLaunchPreferencesAbsent } from '../opencode/opencode-model-startup-plan'
import { assertAgentPromptRequestActive } from '../runtime/orca-runtime-core'
import { parsePaneKey } from '../../shared/stable-pane-id'
import type { AgentLaunchResult } from '../../shared/agent-launch-intent'
Expand All @@ -18,11 +19,8 @@ import {
settledAtCreation,
settleLaunchPromptDisposal
} from './agent-launch-prompt-delivery'
import { workspaceKindForLaunchTarget } from '../../shared/workspace-launch-kind'
import { isDefinitiveAgentSessionCreateRefusal } from '../../shared/agent-session-definitive-refusal'
import {
decideAgentLaunchMode,
readAgentLaunchModeSettings,
resolveAgentLaunchModeOnHost,
type AgentLaunchModeReceipt,
type AgentLaunchModeSettlement,
Expand All @@ -37,6 +35,7 @@ import {
} from './agent-launch-legacy-host'
import {
AgentLaunchStructuredSessionRefusedError,
assertTerminalLaunchAdmissible,
terminalLaunchInputs,
type AgentLaunchStructuredSurface,
type AgentLaunchWorkspaceFactory
Expand All @@ -53,31 +52,10 @@ export async function executeAgentLaunch(
execution: AgentLaunchExecution
): Promise<AgentLaunchResult> {
const { intent, runtime } = execution
if (intent.reuseTerminal || intent.target.kind === 'create-worktree') {
assertOpenCodeModelLaunchPreferencesAbsent(intent.agent, intent.sessionOptions)
}
const vocabulary = execution.vocabulary ?? DEFAULT_LAUNCH_VOCABULARY
const preflight =
execution.decidedMode ??
decideAgentLaunchMode({
placement: {
agent: intent.agent,
workspaceKind: workspaceKindForLaunchTarget(intent.target),
...(intent.reuseTerminal ? { terminal: intent.reuseTerminal.handle } : {}),
...(intent.cwd ? { cwd: intent.cwd } : {}),
...(intent.target.kind === 'existing' && intent.target.workspacePath
? { workspacePath: intent.target.workspacePath }
: {}),
...(execution.callerRendersStructured === false ? { callerRendersStructured: false } : {})
},
settings: execution.terminalOnly
? undefined
: readAgentLaunchModeSettings(runtime, intent.nativeChatDefault),
...(execution.terminalOnly ? { terminalOnly: true } : {}),
...(intent.surface ? { surface: intent.surface } : {}),
vocabulary
})
// Intent preparation may have outlived the request.
const preflight = execution.decidedMode ?? decideAgentLaunchIntentMode(execution)
assertTerminalLaunchAdmissible(intent, preflight.mode)
// A caller's own preparation (intent resolution) may have outlived its request.
assertAgentPromptRequestActive(execution.signal)
if (execution.promptPolicy === 'legacy-host') {
assertLegacyHostTarget(execution)
Expand Down Expand Up @@ -271,6 +249,7 @@ async function createSurface(
worktreeId: workspace.worktreeId,
agent: intent.agent,
...(intent.sessionOptions ? { options: intent.sessionOptions } : {}),
...(intent.chatOptions ? { chatOptions: intent.chatOptions } : {}),
...(intent.sessionId ? { sessionId: intent.sessionId } : {}),
...(reservedTabId ? { tabId: reservedTabId } : {}),
...launchPromptCreateInputs(intent, execution.signal),
Expand Down
38 changes: 38 additions & 0 deletions src/main/agent-launch/agent-launch-intent-mode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import type { AgentLaunchIntent } from '../../shared/agent-launch-intent'
import { workspaceKindForLaunchTarget } from '../../shared/workspace-launch-kind'
import {
decideAgentLaunchMode,
readAgentLaunchModeSettings,
DEFAULT_LAUNCH_VOCABULARY,
type AgentLaunchModeReceipt,
type AgentLaunchModeVocabulary
} from './agent-launch-mode'

/** The same preflight is used for admission and execution, before workspace creation. */
export function decideAgentLaunchIntentMode(execution: {
intent: AgentLaunchIntent
runtime: Parameters<typeof readAgentLaunchModeSettings>[0]
terminalOnly?: boolean
callerRendersStructured?: boolean
vocabulary?: AgentLaunchModeVocabulary
}): AgentLaunchModeReceipt {
const { intent, runtime } = execution
return decideAgentLaunchMode({
placement: {
agent: intent.agent,
workspaceKind: workspaceKindForLaunchTarget(intent.target),
...(intent.reuseTerminal ? { terminal: intent.reuseTerminal.handle } : {}),
...(intent.cwd ? { cwd: intent.cwd } : {}),
...(intent.target.kind === 'existing' && intent.target.workspacePath
? { workspacePath: intent.target.workspacePath }
: {}),
...(execution.callerRendersStructured === false ? { callerRendersStructured: false } : {})
},
settings: execution.terminalOnly
? undefined
: readAgentLaunchModeSettings(runtime, intent.nativeChatDefault),
...(execution.terminalOnly ? { terminalOnly: true } : {}),
...(intent.surface ? { surface: intent.surface } : {}),
vocabulary: execution.vocabulary ?? DEFAULT_LAUNCH_VOCABULARY
})
}
37 changes: 34 additions & 3 deletions src/main/agent-launch/agent-launch-opencode-model.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import { describe, expect, it, vi } from 'vitest'
import recordings from '../../shared/fixtures/source-control-launch-recordings.json'
import type { AgentLaunchIntent } from '../../shared/agent-launch-intent'
import { executeAgentLaunch } from './agent-launch-executor'
import { toAgentLaunchPreferences } from '../../shared/agent-launch-preferences'
import { assertOpenCodeModelLaunchPreferencesAbsent } from '../opencode/opencode-model-startup-plan'
import { agentPermissionChoices } from '../../shared/tui-agent-permission-choice'

function harness() {
const readSettings = vi.fn(() => {
throw new Error('settings_not_needed_for_refusal')
throw new Error('settings_unavailable')
})
const createSupport = vi.fn(async () => {
throw new Error('support_not_needed_for_refusal')
Expand Down Expand Up @@ -59,12 +63,39 @@ const creating: AgentLaunchIntent = {
}

describe('OpenCode model preferences on unsupported launch placements', () => {
it('keeps own selectable without projecting a meaningless permission into either terminal guard', async () => {
const sessionOptions = { permissionMode: 'own' }
const projected = toAgentLaunchPreferences(sessionOptions, 'opencode')
expect(agentPermissionChoices('opencode')).toContain('own')
expect(projected).toBeUndefined()
// Workspace creation consumes this projection through the same unchanged broad guard.
expect(() => assertOpenCodeModelLaunchPreferencesAbsent('opencode', projected)).not.toThrow()
for (const intent of [reused, creating]) {
const h = harness()
await expect(h.run({ ...intent, sessionOptions })).resolves.toBeDefined()
}
})

it('refuses the real OpenCode model readback on a reused terminal before prompt delivery', async () => {
const model = recordings.opencode.update.message.params.update.configOptions.find(
(option) => option.category === 'model'
)?.currentValue
expect(model).toBe('opencode/big-pickle')
const h = harness()
await expect(h.run({ ...reused, sessionOptions: { model } })).rejects.toMatchObject({
code: 'capability_unsupported'
})
expect(h.createTerminal).not.toHaveBeenCalled()
expect(h.createWorktree).not.toHaveBeenCalled()
expect(h.deliverPrompt).not.toHaveBeenCalled()
})

it.each([reused, creating])(
'refuses before reading settings or creating or delivering',
'preserves definitive refusal without creating or delivering when the route settings read throws',
async (intent) => {
const h = harness()
await expect(h.run(intent)).rejects.toMatchObject({ code: 'capability_unsupported' })
expect(h.readSettings).not.toHaveBeenCalled()
expect(h.readSettings).toHaveBeenCalledOnce()
expect(h.createTerminal).not.toHaveBeenCalled()
expect(h.createWorktree).not.toHaveBeenCalled()
expect(h.deliverPrompt).not.toHaveBeenCalled()
Expand Down
Loading
Loading