Merge origin/main into brennanb2025/orchestration-structured-delivery

Carries main's #19040 through this branch's two extractions, which would
otherwise have silently reverted it: both conflicts are the same shape,
where main fixed code this branch had already moved elsewhere.

- resolveStructuredNativeChatSupport no longer refuses a launch that has
  saved session options. Main removed that term from resolveAgentLaunchRoute
  directly; this branch had lifted the whole predicate into the shared
  module, so taking either conflict side kept the refusal alive. The blocker
  member, the unused input field, the orchestration blocker-to-reason entry
  and the case pinning it are removed with it. Main's own routing test failed
  on this merge before the port and passes after, which is what proves the
  port is load-bearing rather than cosmetic.

- The structured create fingerprint now goes through attachFingerprintFields.
  Same shape again: main replaced the inline field list in
  structured-agent-session.ts, but this branch had moved that computation
  into structured-agent-session-create.ts. Options stay out of the digest,
  which is main's point: they are the session's initial state, not its
  identity, so a retry after the user reselects a model replays instead of
  conflicting.
This commit is contained in:
Merge Sim
2026-09-06 11:08:58 -07:00
62 changed files with 1876 additions and 493 deletions
@@ -28,7 +28,8 @@ afterEach(async () => {
function attachParams(
operationId: string,
expectedRuntimeFence: number | null
expectedRuntimeFence: number | null,
options?: Readonly<Record<string, string>>
): AgentSessionAttachParams {
const params: AgentSessionAttachParams = {
envelope: {
@@ -47,6 +48,7 @@ function attachParams(
agent: 'codex',
accountHome: { variable: 'CODEX_HOME', path: '/home/dev/.codex' },
runtimeKind: 'native',
...(options ? { options } : {}),
providerHandle: { kind: 'codex', threadId: 'legacy-thread' }
}
return {
@@ -101,6 +103,70 @@ function expectSettledAttachLease(record: AgentSessionRecord | null): void {
}
describe('structured session acquisition options', () => {
it('persists create defaults before the first provider acquisition', async () => {
root = await mkdtemp(join(tmpdir(), 'orca-create-options-'))
const store = await AgentSessionRecordStore.open({
directory: join(root, 'store'),
hostId: 'local'
})
const sessionAdapter = adapter({ origin: 'created' })
const options = { model: 'gpt-5.6-sol', effort: 'medium' }
const created = await performAttach({
store,
adapter: sessionAdapter,
journalRoot: root,
authority: {
spawnToken: 'spawn-a',
claimKeyId: 'key-1',
handoffOperationId: CREATE_OPERATION,
probe: { outcome: 'reservation-unused' }
},
callerKey: 'client-1',
params: attachParams(CREATE_OPERATION, null, options),
now: () => NOW,
onAttached: () => {}
})
expect(created).toMatchObject({ ok: true })
expect(sessionAdapter.acquire).toHaveBeenCalledWith(expect.objectContaining({ options }))
expect(store.getRecord(SESSION)?.options).toEqual(options)
})
it('replays a create retried after the host re-resolved different options', async () => {
root = await mkdtemp(join(tmpdir(), 'orca-create-retry-'))
const store = await AgentSessionRecordStore.open({
directory: join(root, 'store'),
hostId: 'local'
})
const sessionAdapter = adapter({ origin: 'created' })
const attempt = async (options: Readonly<Record<string, string>>, spawnToken: string) =>
performAttach({
store,
adapter: sessionAdapter,
journalRoot: root!,
authority: {
spawnToken,
claimKeyId: 'key-1',
handoffOperationId: CREATE_OPERATION,
probe: { outcome: 'reservation-unused' }
},
callerKey: 'client-1',
params: attachParams(CREATE_OPERATION, null, options),
now: () => NOW,
onAttached: () => {}
})
const created = await attempt({ model: 'gpt-5.6-sol', effort: 'medium' }, 'spawn-a')
// Why: the user may reselect a model between an unknown-outcome create and the
// retry that reuses its operation id; the retry must replay, not conflict.
const retried = await attempt({ model: 'gpt-5.5', effort: 'high' }, 'spawn-b')
expect(created).toMatchObject({ ok: true })
expect(retried).toMatchObject({ ok: true })
expect(store.getRecord(SESSION)?.options).toEqual({ model: 'gpt-5.6-sol', effort: 'medium' })
})
it('persists provider options before proving a resumed legacy record', async () => {
root = await mkdtemp(join(tmpdir(), 'orca-acquisition-options-'))
const storeDir = join(root, 'store')
@@ -54,6 +54,8 @@ export type AgentSessionAttachParams = {
agent: AgentSessionHandleProvider
accountHome: AgentSessionAccountHome
runtimeKind: AgentSessionOwnerRuntimeKind
/** Host-resolved defaults for a create-by-intent; remote attach schemas do not accept them. */
options?: Readonly<Record<string, string>>
/** Omitted only for create-by-intent; the adapter proves the durable handle. */
providerHandle?: Exclude<AgentSessionProviderHandle, { kind: 'opaque' }>
}
@@ -70,7 +72,10 @@ export type AgentSessionAttachAuthority = {
/** The fields that define WHICH session this call would attach to. Deliberately
* excludes the spawn token and the probe: those differ between a first attempt
* and its retry, and a retry must replay rather than conflict. */
* and its retry, and a retry must replay rather than conflict. `options` is
* excluded for the same reason — it is the session's initial state, not its
* identity, and the host re-resolves it from settings the user may have changed
* between an unknown-outcome attempt and its retry. */
export function attachFingerprintFields(params: AgentSessionAttachParams): Record<string, unknown> {
return {
location: params.location,
@@ -191,6 +196,7 @@ export function reserveRequestFor(input: {
location: params.location,
provider: params.provider,
accountHome: params.accountHome,
...(params.options ? { options: params.options } : {}),
...(authority.launchArgs ? { launchArgs: authority.launchArgs } : {}),
...(authority.launchEnv ? { launchEnv: authority.launchEnv } : {}),
runtimeKind: params.runtimeKind,
@@ -21,6 +21,7 @@ import {
agentSessionExecutionLocationsEqual,
isAgentSessionLaunchArgs,
isAgentSessionLaunchEnv,
isAgentSessionOptions,
type AgentSessionAccountHome,
type AgentSessionExecutionLocation,
type AgentSessionLaunchArgs,
@@ -43,6 +44,8 @@ export type AgentSessionReserveRequest = {
launchArgs?: AgentSessionLaunchArgs
/** Current launch input validated here but never written to the durable record. */
launchEnv?: AgentSessionLaunchEnv
/** Initial provider options persisted before the first process is acquired. */
options?: Readonly<Record<string, string>>
runtimeKind: AgentSessionReservation['runtimeKind']
/** Null when the session does not exist yet; otherwise the fence the caller last observed. */
expectedFence: number | null
@@ -131,6 +134,9 @@ export function applyAgentSessionReservation(
if (request.launchArgs && !isAgentSessionLaunchArgs(request.launchArgs)) {
throw new Error('agent_session_launch_args_invalid')
}
if (request.options && !isAgentSessionOptions(request.options)) {
throw new Error('agent_session_options_invalid')
}
const reservation: AgentSessionReservation = {
runtimeKind: request.runtimeKind,
spawnToken:
@@ -186,6 +192,7 @@ function createAgentSessionRecord(
provider: request.provider,
providerHandleChain: [],
accountHome: request.accountHome,
...(request.options ? { options: { ...request.options } } : {}),
...(request.launchArgs ? { launchArgs: [...request.launchArgs] } : {}),
createdAt: request.now,
updatedAt: request.now,
@@ -13,6 +13,7 @@ import { getLocalProjectWorktreeGitOptions } from '../project-runtime-git-option
import type { AgentSessionAttachParams } from '../native-chat/agent-session-wire/structured-agent-session-attach'
import { getSystemCodexHomePath } from '../codex/codex-home-paths'
import { resolveTuiAgentLaunchEnv } from '../../shared/tui-agent-launch-defaults'
import { resolveStructuredLaunchSeedOptions } from '../../shared/native-chat-session-option-defaults'
import { hasPersistedStructuredAgentSessionStore as hasPersistedStructuredAgentSessionStoreOnDisk } from './structured-agent-session-runtime'
import { getProfileUserDataPath } from '../orca-profiles/profile-storage-paths'
import { homedir } from 'node:os'
@@ -161,6 +162,10 @@ export class OrcaRuntimeWithResolveRecoveredStructuredTuiTranscript extends Orca
}
const settings = this.requireStore().getSettings()
const launchEnv = resolveTuiAgentLaunchEnv(input.agent, settings.agentDefaultEnv)
const options = resolveStructuredLaunchSeedOptions(
settings.nativeChatSessionOptions,
input.agent
)
const location = await this.resolveStructuredAgentSessionLocation(input.worktree)
const workspacePath = (await this.resolveRuntimeFileTarget(input.worktree)).worktree.path
return {
@@ -177,6 +182,7 @@ export class OrcaRuntimeWithResolveRecoveredStructuredTuiTranscript extends Orca
variable: input.agent === 'claude' ? 'CLAUDE_CONFIG_DIR' : 'CODEX_HOME',
path: await resolveAccountHomePath({ workspacePath, launchEnv, location })
},
...(options ? { options } : {}),
runtimeKind: 'native'
}
}
@@ -7,7 +7,15 @@ describe('structured agent-session create intent', () => {
const runtime = new OrcaRuntimeService(
{
getSettings: () => ({
agentDefaultEnv: { codex: { CODEX_HOME: '/configured/home' } }
agentDefaultEnv: { codex: { CODEX_HOME: '/configured/home' } },
nativeChatSessionOptions: {
codex: {
model: 'gpt-5.6-sol',
valuesByModel: {
'gpt-5.6-sol': { effort: 'medium', fastMode: true, personality: 'concise' }
}
}
}
})
} as never,
undefined,
@@ -51,6 +59,7 @@ describe('structured agent-session create intent', () => {
variable: 'CODEX_HOME',
path: '/accounts/selected/home'
})
expect(intent.options).toEqual({ model: 'gpt-5.6-sol', effort: 'medium' })
})
it('pins the configured Claude launch home without Codex launch preparation', async () => {
@@ -60,6 +69,12 @@ describe('structured agent-session create intent', () => {
getSettings: () => ({
agentDefaultEnv: {
claude: { CLAUDE_CONFIG_DIR: '/configured/claude-home' }
},
nativeChatSessionOptions: {
claude: {
model: 'opus',
valuesByModel: { opus: { effort: 'high', fastMode: true } }
}
}
})
} as never,
@@ -101,6 +116,7 @@ describe('structured agent-session create intent', () => {
variable: 'CLAUDE_CONFIG_DIR',
path: '/configured/claude-home'
})
expect(intent.options).toEqual({ model: 'opus', effort: 'high' })
})
it('uses the managed Claude launch home before falling back to ~/.claude', async () => {
@@ -86,7 +86,6 @@ const BLOCKER_REASON: Record<
'draft-prompt': 'structured_unsupported_on_host',
'floating-workspace': 'structured_unsupported_on_host',
'tui-launch-customization': 'tui_launch_customization',
'initial-session-options': 'structured_unsupported_on_host',
'remote-execution-host': 'remote_execution_host',
'codex-on-windows': 'codex_on_windows',
'project-runtime': 'wsl_execution_runtime',
@@ -18,7 +18,10 @@ import type {
AgentSessionMutationEnvelope,
AgentSessionMutationResult
} from '../../../../shared/agent-session-wire'
import type { AgentSessionAttachParams } from '../../../native-chat/agent-session-wire/structured-agent-session-attach'
import {
attachFingerprintFields,
type AgentSessionAttachParams
} from '../../../native-chat/agent-session-wire/structured-agent-session-attach'
import type { StructuredAgentSessionHost } from '../../../native-chat/agent-session-wire/structured-agent-session-host'
import type { StructuredAgentSessionCaller } from '../../../native-chat/agent-session-wire/structured-agent-session-host-types'
import type { OrcaRuntimeService } from '../../orca-runtime'
@@ -52,14 +55,7 @@ export async function prepareStructuredAgentSessionCreateForWorktree(args: {
const hostFingerprint = computeAgentSessionPayloadFingerprint({
method: 'agentSession.attach',
sessionId: args.envelope.sessionId,
fields: {
location: resolved.location,
provider: resolved.provider,
agent: resolved.agent,
accountHome: resolved.accountHome,
runtimeKind: resolved.runtimeKind,
expectedRuntimeFence: null
}
fields: attachFingerprintFields({ ...resolved, envelope: args.envelope })
})
const host = await args.ensureHost()
const { agent: _resolvedAgent, provider: _resolvedProvider, ...resolvedAttach } = resolved
@@ -200,6 +200,10 @@ function dispatcher(runtimeOverrides: Record<string, unknown> = {}): RpcDispatch
variable: params.agent === 'claude' ? 'CLAUDE_CONFIG_DIR' : 'CODEX_HOME',
path: params.agent === 'claude' ? '/host/.claude' : '/host/.codex'
},
options:
params.agent === 'claude'
? { model: 'opus', effort: 'high' }
: { model: 'gpt-5.6-sol', effort: 'medium' },
runtimeKind: 'native'
})),
publishStructuredAgentSessionTab: vi.fn()
@@ -482,7 +486,8 @@ describe('method routing', () => {
expect(hostCalls.attach).toHaveBeenCalledWith(
expect.anything(),
expect.objectContaining({
accountHome: { variable: 'CODEX_HOME', path: '/host/.codex' }
accountHome: { variable: 'CODEX_HOME', path: '/host/.codex' },
options: { model: 'gpt-5.6-sol', effort: 'medium' }
})
)
expect(hostCalls.attach.mock.calls[0]?.[1]).not.toHaveProperty('providerHandle')
@@ -113,20 +113,20 @@ export function CommitNotices({
role={createPrIntentNotice.tone === 'destructive' ? 'alert' : 'status'}
aria-live="polite"
className={cn(
'mt-1 flex min-w-0 items-center gap-1.5 text-[11px]',
'mt-1 flex min-w-0 flex-col items-start gap-1 text-[11px]',
createPrIntentNotice.tone === 'destructive'
? 'text-destructive'
: 'text-muted-foreground'
)}
>
{/* Why: Create Review blockers carry recovery steps; truncating hides the action the user needs in a narrow sidebar. */}
<span className="min-w-0 flex-1 break-words leading-4 [overflow-wrap:anywhere]">
<span className="min-w-0 self-stretch break-words leading-4 [overflow-wrap:anywhere]">
{createPrIntentNotice.message}
</span>
{createPrIntentNotice.action === 'settings' && onOpenSourceControlAiSettings ? (
<button
type="button"
className="shrink-0 font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
className="text-left font-medium text-foreground underline decoration-border underline-offset-2 hover:decoration-foreground"
onClick={() => onOpenSourceControlAiSettings()}
>
{translate(
@@ -68,9 +68,12 @@ export function TooManyChangesBanner({
}
return (
<div className="rounded-md border border-amber-500/25 bg-amber-500/5 px-3 py-2">
<div className="flex items-center gap-2">
<AlertTriangle className="size-4 shrink-0 text-amber-600 dark:text-amber-400" />
<div
data-testid="too-many-changes-banner"
className="flex flex-col gap-2 rounded-md border border-amber-500/25 bg-amber-500/5 px-3 py-2"
>
<div className="flex items-start gap-2">
<AlertTriangle className="mt-px size-4 shrink-0 text-amber-600 dark:text-amber-400" />
<span className="min-w-0 flex-1 text-xs text-foreground">
{translate(
'auto.components.right.sidebar.SourceControl.tooManyChanges',
@@ -78,18 +81,18 @@ export function TooManyChangesBanner({
{ value0: limit.toLocaleString() }
)}
</span>
<Button
type="button"
variant="outline"
size="xs"
className="w-24 shrink-0 text-xs"
disabled={isRetrying}
onClick={() => void handleRetry()}
>
{showSpinner ? <Loader2 className="size-3 animate-spin" /> : null}
{translate('auto.components.right.sidebar.SourceControl.286dbda4d6', 'Retry')}
</Button>
</div>
<Button
type="button"
variant="outline"
size="xs"
className="self-end text-xs"
disabled={isRetrying}
onClick={() => void handleRetry()}
>
{showSpinner ? <Loader2 className="size-3 animate-spin" /> : null}
{translate('auto.components.right.sidebar.SourceControl.286dbda4d6', 'Retry')}
</Button>
</div>
)
}
@@ -31,6 +31,9 @@ describe('resolveAgentLaunchRoute', () => {
'routes a supported local %s launch to structured native chat',
(agent) => {
expect(route({ agent })).toBe('structured-native-chat')
expect(route({ agent, initialSessionOptions: { model: 'gpt-5.6-sol' } })).toBe(
'structured-native-chat'
)
expect(
route({ agent, launchText: 'explain this change', promptDelivery: 'auto-submit' })
).toBe('structured-native-chat')
@@ -118,7 +121,6 @@ describe('resolveAgentLaunchRoute', () => {
expect(route({ agent: 'openclaude' })).toBe('legacy-native-chat')
expect(route({ agent: 'grok' })).toBe('legacy-native-chat')
expect(route({ requiresTuiLaunchCustomization: true })).toBe('legacy-native-chat')
expect(route({ initialSessionOptions: { model: 'gpt-5.6-sol' } })).toBe('legacy-native-chat')
})
it.each([
+1 -2
View File
@@ -64,8 +64,7 @@ export function resolveAgentLaunchRoute(input: AgentLaunchRoutingInput): AgentLa
workspaceKind: input.workspaceKind,
projectRuntime: input.projectRuntime,
isDraftPrompt: input.promptDelivery === 'draft',
requiresTuiLaunchCustomization: input.requiresTuiLaunchCustomization,
initialSessionOptions: input.initialSessionOptions
requiresTuiLaunchCustomization: input.requiresTuiLaunchCustomization
}).supported
? 'structured-native-chat'
: 'legacy-native-chat'
@@ -43,7 +43,13 @@ const store = {
activeRuntimeEnvironmentId: null,
experimentalNativeChat: true,
experimentalStructuredNativeChat: true,
openAgentTabsInChatByDefault: true
openAgentTabsInChatByDefault: true,
nativeChatSessionOptions: undefined as
| Record<
string,
{ model?: string; valuesByModel?: Record<string, Record<string, string | boolean>> }
>
| undefined
},
projects: [{ id: 'repo-1', localWindowsRuntimePreference: { kind: 'inherit-global' as const } }],
repos: [{ id: 'repo-1', connectionId: null as string | null, path: '/repo' }],
@@ -153,6 +159,7 @@ describe('structured chat adoption guard on the launch path', () => {
mockToastError.mockReset()
hostCapabilities = STRUCTURED_HOST_CAPABILITIES
store.settings.openAgentTabsInChatByDefault = true
store.settings.nativeChatSessionOptions = undefined
})
it('takes the structured path when the chat-default view is selected', async () => {
@@ -175,6 +182,23 @@ describe('structured chat adoption guard on the launch path', () => {
expect(mockWaitForAgentReady).not.toHaveBeenCalled()
})
// Routing only: the host seeds the saved values, so preservation is pinned there.
it('takes the structured path when a Codex model and effort are already saved', async () => {
store.settings.nativeChatSessionOptions = {
codex: {
model: 'gpt-5.6-sol',
valuesByModel: { 'gpt-5.6-sol': { effort: 'medium' } }
}
}
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
const result = launchAgentInNewTab({ agent: 'codex', worktreeId: 'wt-1' })
expect(result).toMatchObject({ tabId: null, focusAfterMenuClose: 'structured-session' })
expect(mockCreateStructuredCodexSessionLaunchIntent).toHaveBeenCalledWith('wt-1', 'codex')
expect(mockCreateTab).not.toHaveBeenCalled()
})
it('takes the structured path for Claude, naming Claude as the create provider', async () => {
const { launchAgentInNewTab } = await import('./launch-agent-in-new-tab')
+1 -1
View File
@@ -221,7 +221,7 @@ function isAgentSessionAccountHome(value: unknown): value is AgentSessionAccount
)
}
function isAgentSessionOptions(value: unknown): value is Record<string, string> {
export function isAgentSessionOptions(value: unknown): value is Record<string, string> {
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
return false
}
@@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'
import {
clearNativeChatSessionOptionModel,
resolveNativeChatSessionOptionDefaults,
resolveStructuredLaunchSeedOptions,
updateNativeChatSessionOptionDefaults
} from './native-chat-session-option-defaults'
import type { PersistedNativeChatSessionOptions } from './native-chat-session-options'
@@ -74,3 +75,66 @@ describe('resolveNativeChatSessionOptionDefaults', () => {
})
})
})
describe('resolveStructuredLaunchSeedOptions', () => {
const persistedCodex = (
valuesByModel: Record<string, Record<string, string | boolean>>
): PersistedNativeChatSessionOptions =>
({ codex: { model: 'gpt-5.6-sol', valuesByModel } }) as PersistedNativeChatSessionOptions
it('seeds the saved model and effort a structured create must apply', () => {
expect(
resolveStructuredLaunchSeedOptions(
persistedCodex({ 'gpt-5.6-sol': { effort: 'medium' } }),
'codex'
)
).toEqual({ model: 'gpt-5.6-sol', effort: 'medium' })
})
it('drops ids the providers only accept mid-session', () => {
// `fastMode` is a boolean and `personality` is settable only mid-session;
// neither belongs in the reservation's Record<string, string>.
expect(
resolveStructuredLaunchSeedOptions(
persistedCodex({
'gpt-5.6-sol': { effort: 'high', fastMode: true, personality: 'concise' }
}),
'codex'
)
).toEqual({ model: 'gpt-5.6-sol', effort: 'high' })
})
it('drops a seeded id whose persisted value is not a usable string', () => {
// settings.json is user-writable, so a non-string `effort` must not reach a
// record typed Record<string, string> and be emitted as a turn option.
expect(
resolveStructuredLaunchSeedOptions(
persistedCodex({ 'gpt-5.6-sol': { effort: true } }),
'codex'
)
).toEqual({ model: 'gpt-5.6-sol' })
expect(
resolveStructuredLaunchSeedOptions(
persistedCodex({ 'gpt-5.6-sol': { effort: ' ' } }),
'codex'
)
).toEqual({ model: 'gpt-5.6-sol' })
})
it('seeds nothing when the stored values empty the model out', () => {
// `valuesByModel` is merged over the resolved model, so a stored `model` key
// can blank it. Emitting `{ model: '' }` fails the record's bounded-string
// guard, and that throw is not a wire refusal code — it escapes as a raw
// error the client reads as unknown, stranding the launch with no fallback.
expect(
resolveStructuredLaunchSeedOptions(persistedCodex({ 'gpt-5.6-sol': { model: '' } }), 'codex')
).toBeUndefined()
})
it('seeds nothing until a model is picked, so the CLI default survives', () => {
expect(resolveStructuredLaunchSeedOptions(undefined, 'codex')).toBeUndefined()
expect(
resolveStructuredLaunchSeedOptions({ codex: { valuesByModel: {} } }, 'codex')
).toBeUndefined()
})
})
@@ -28,6 +28,33 @@ export function resolveNativeChatSessionOptionDefaults(
return values
}
/** Why only these two: they are the only ids the picker persists into
* `nativeChatSessionOptions` that both structured providers also accept as
* strings. Claude's `fastMode` is a boolean the durable `Record<string, string>`
* record cannot carry, and the providers' remaining keys are settable only
* mid-session, never seeded at launch. */
const STRUCTURED_LAUNCH_SEED_OPTION_IDS = ['model', 'effort'] as const
/** The saved selection a structured create seeds into its reservation, narrowed
* to the wire-safe string subset the durable record and both providers accept. */
export function resolveStructuredLaunchSeedOptions(
persisted: PersistedNativeChatSessionOptions | null | undefined,
agent: AgentType
): Record<string, string> | undefined {
const defaults = resolveNativeChatSessionOptionDefaults(persisted, agent)
if (!defaults) {
return undefined
}
const seeded: Record<string, string> = {}
for (const id of STRUCTURED_LAUNCH_SEED_OPTION_IDS) {
const value = defaults[id]
if (typeof value === 'string' && value.trim()) {
seeded[id] = value
}
}
return Object.keys(seeded).length > 0 ? seeded : undefined
}
/** Why: an authoritative probe proved this id gone, and a stale `model` is emitted
* verbatim as a launch flag — grok exits fatally on an unknown one. Dropping only
* `model` keeps the per-model option values for a later reselect. */
@@ -63,7 +63,6 @@ describe('per-launch structured feasibility', () => {
['a draft prompt', { isDraftPrompt: true }, 'draft-prompt'],
['a floating workspace', { workspaceKind: 'floating' }, 'floating-workspace'],
['a custom TUI launch', { requiresTuiLaunchCustomization: true }, 'tui-launch-customization'],
['session options', { initialSessionOptions: { model: 'x' } }, 'initial-session-options'],
['an SSH host', { executionHostId: 'ssh:host-a' }, 'remote-execution-host'],
['Codex on Windows', { agent: 'codex', platform: 'win32' }, 'codex-on-windows'],
['a missing capability', { hostCapabilities: [] }, 'runtime-capability']
@@ -25,7 +25,6 @@ export type StructuredNativeChatBlocker =
| 'draft-prompt'
| 'floating-workspace'
| 'tui-launch-customization'
| 'initial-session-options'
| 'remote-execution-host'
| 'codex-on-windows'
| 'project-runtime'
@@ -45,7 +44,6 @@ export type StructuredNativeChatSupportInput = {
/** A draft stays terminal-backed: the composer, not a turn, owns unsent text. */
isDraftPrompt?: boolean
requiresTuiLaunchCustomization?: boolean
initialSessionOptions?: Readonly<Record<string, unknown>>
}
/** The user's default for a new agent tab: native chat rather than the raw TUI. */
@@ -81,9 +79,6 @@ export function resolveStructuredNativeChatSupport(
if (input.requiresTuiLaunchCustomization === true) {
return { supported: false, blocker: 'tui-launch-customization' }
}
if (input.initialSessionOptions && Object.keys(input.initialSessionOptions).length > 0) {
return { supported: false, blocker: 'initial-session-options' }
}
if (input.executionHostId !== 'local') {
return { supported: false, blocker: 'remote-execution-host' }
}