Files
orca/src/shared/native-chat-session-options.ts
T
Brennan BensonandMerge Sim 546fd9b21f fix(native-chat): remember structured chat model and effort picks (#19147)
* fix(native-chat): remember structured chat model and effort picks

Structured Claude and Codex sessions already read the saved launch
options at create, but nothing ever wrote them back. The only writer of
`nativeChatSessionOptions` was the PTY picker, and the composer swaps in
the structured surface for structured panes, so a structured pick went
nowhere: it was forgotten when the session ended and every new session
started at the CLI default.

Persist a settled pick from both the desktop and mobile structured
surfaces. Model and effort are stored as a pair, because a launch
resolves a stored effort only under a stored model — so an effort-only
pick adopts the model it was chosen against, otherwise the remembered
effort never reaches a launch at all.

Two things the persist path deliberately avoids: it writes what the
provider committed rather than what was requested, since Codex
reconciles an effort the newly selected model cannot run; and it never
writes the provider readback, which is the CLI's own default and would
pin a `-m` the user never chose.

* fix(native-chat): persist session option picks atomically

---------

Co-authored-by: Merge Sim <sim@local>
2026-09-06 20:28:17 -07:00

101 lines
3.7 KiB
TypeScript

import type { AgentType } from './agent-status-types'
export type SessionOptionValue = string | boolean
export type SessionOptionSelectChoice = {
value: string
label: string
description?: string
}
/** `default` is the catalog's own value shown before anything is observed —
* truthful to display, but never evidence about a running agent. `dispatched`
* is sent-but-unread: the pill shows it, and a later report that disagrees is
* what corrects it. Both transports emit it, so it alone names neither — see
* `transport` on the descriptor. */
export type SessionOptionValueSource = 'applied' | 'dispatched' | 'reported' | 'default' | 'unknown'
/** How a live value reaches the agent. `catalog` types the catalog's command into
* the agent's terminal and can only learn the outcome by parsing the screen back;
* `agent-session` writes over the structured protocol, which reports every turn. */
export type NativeChatLiveOptionTransport = 'catalog' | 'agent-session'
/** Closed set of reasons an option is not settable in the current mode. A key
* (not free English) so the producer and the localized label stay in sync —
* an exhaustive switch turns any drift into a type error instead of leaking
* untranslated text. */
export type SessionOptionDisabledReason =
| 'available-after-session-start'
| 'set-when-session-starts'
export type SessionOptionDescriptor = {
id: string
label: string
description?: string
category?: 'model' | 'thought_level' | 'model_config' | 'mode'
kind:
| {
type: 'select'
currentValue?: string
choices: SessionOptionSelectChoice[]
}
| {
type: 'boolean'
currentValue?: boolean
}
valueSource: SessionOptionValueSource
/** Required so a new producer cannot inherit the wrong lane's rendering by
* omission — `dispatched` is emitted identically by both and cannot discriminate. */
transport: NativeChatLiveOptionTransport
settable: boolean
disabledReason?: SessionOptionDisabledReason
/** Why: picker-only and toggle-only PTY commands cannot be represented as
* a truthful radio/checkbox state, so the producer exposes an action row. */
action?: { type: 'agent-picker' | 'toggle-command' }
}
/** A value we typed at the agent and have never read back. Only the terminal
* transport can be in this state: the structured lane's own per-turn report is
* what moves a value off `dispatched`, and until it lands nothing else has. */
export function sessionOptionDispatchUnconfirmed(
descriptor: Pick<SessionOptionDescriptor, 'valueSource' | 'transport'>
): boolean {
return descriptor.valueSource === 'dispatched' && descriptor.transport === 'catalog'
}
export type SessionOptionSetResult = {
snapshot: SessionOptionDescriptor[]
}
export type PersistedNativeChatSessionOptions = Partial<
Record<
string,
{
model?: string
valuesByModel?: Record<string, Record<string, SessionOptionValue>>
}
>
>
export type NativeChatSessionOptionSettingsMutation =
| {
type: 'apply-picks'
agent: AgentType
picks: readonly {
modelId: string
optionId: string
value: SessionOptionValue
adoptModelAsLaunchDefault?: boolean
}[]
}
| { type: 'clear-model-if-missing'; agent: AgentType; availableModelIds: readonly string[] }
export type SessionOptionsSurface = {
getSnapshot(): SessionOptionDescriptor[]
/** Apply an absolute target; known flip-only options use their tracked baseline. */
setOption(id: string, value: SessionOptionValue): Promise<SessionOptionSetResult>
/** Invoke the value-less action exposed by the current descriptor. */
invokeAction(id: string): Promise<SessionOptionSetResult>
subscribe(listener: (snapshot: SessionOptionDescriptor[]) => void): () => void
}