mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 00:02:10 +00:00
* 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>
142 lines
5.5 KiB
TypeScript
142 lines
5.5 KiB
TypeScript
import type { AgentType } from './agent-status-types'
|
|
import { sessionOptionValueIsValid } from './agent-session-option-catalog'
|
|
import type {
|
|
NativeChatSessionOptionSettingsMutation,
|
|
PersistedNativeChatSessionOptions,
|
|
SessionOptionValue
|
|
} from './native-chat-session-options'
|
|
|
|
export function resolveNativeChatSessionOptionDefaults(
|
|
persisted: PersistedNativeChatSessionOptions | null | undefined,
|
|
agent: AgentType
|
|
): Record<string, SessionOptionValue> | undefined {
|
|
const entry = persisted?.[agent]
|
|
// Why: untouched settings must preserve the agent CLI's configured defaults;
|
|
// only a model explicitly selected by the user authorizes launch flags.
|
|
const modelId = typeof entry?.model === 'string' && entry.model.trim() ? entry.model : undefined
|
|
if (!modelId) {
|
|
return undefined
|
|
}
|
|
const values: Record<string, SessionOptionValue> = { model: modelId }
|
|
const storedValues = entry?.valuesByModel?.[modelId]
|
|
if (storedValues && typeof storedValues === 'object') {
|
|
for (const [id, value] of Object.entries(storedValues)) {
|
|
if (sessionOptionValueIsValid(value)) {
|
|
values[id] = value
|
|
}
|
|
}
|
|
}
|
|
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. */
|
|
export 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
|
|
}
|
|
|
|
/** Fold a settled batch of picks onto the durable record. A surface that must send the
|
|
* whole object back — rather than merging key by key — applies them in one pass so a
|
|
* later pick in the batch cannot drop an earlier one. */
|
|
export function applyNativeChatSessionOptionPicks(args: {
|
|
persisted: PersistedNativeChatSessionOptions | null | undefined
|
|
agent: AgentType
|
|
picks: Extract<NativeChatSessionOptionSettingsMutation, { type: 'apply-picks' }>['picks']
|
|
}): PersistedNativeChatSessionOptions {
|
|
let persisted = args.persisted ?? {}
|
|
for (const pick of args.picks) {
|
|
persisted = updateNativeChatSessionOptionDefaults({ persisted, agent: args.agent, ...pick })
|
|
}
|
|
return persisted
|
|
}
|
|
|
|
/** Applies one host-owned delta to the latest record. Returning null means the
|
|
* authoritative model list found nothing to retire. */
|
|
export function applyNativeChatSessionOptionSettingsMutation(
|
|
persisted: PersistedNativeChatSessionOptions | null | undefined,
|
|
mutation: NativeChatSessionOptionSettingsMutation
|
|
): PersistedNativeChatSessionOptions | null {
|
|
if (mutation.type === 'apply-picks') {
|
|
return applyNativeChatSessionOptionPicks({
|
|
persisted,
|
|
agent: mutation.agent,
|
|
picks: mutation.picks
|
|
})
|
|
}
|
|
const modelId = persisted?.[mutation.agent]?.model
|
|
if (!modelId || mutation.availableModelIds.includes(modelId)) {
|
|
return null
|
|
}
|
|
return clearNativeChatSessionOptionModel(persisted, mutation.agent)
|
|
}
|
|
|
|
/** 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. */
|
|
export function clearNativeChatSessionOptionModel(
|
|
persisted: PersistedNativeChatSessionOptions | null | undefined,
|
|
agent: AgentType
|
|
): PersistedNativeChatSessionOptions {
|
|
const currentAgent = persisted?.[agent]
|
|
if (!currentAgent?.model) {
|
|
return { ...persisted }
|
|
}
|
|
const { model: _dropped, ...rest } = currentAgent
|
|
return { ...persisted, [agent]: rest }
|
|
}
|
|
|
|
export function updateNativeChatSessionOptionDefaults(args: {
|
|
persisted: PersistedNativeChatSessionOptions | null | undefined
|
|
agent: AgentType
|
|
modelId: string
|
|
optionId: string
|
|
value: SessionOptionValue
|
|
/** Defaults to adopting, since without a `model` no launch resolves the value at all.
|
|
* Only the picker surface, which can tell a probe-confirmed id from the seed's guess
|
|
* at the CLI default, withholds it: adopting a guess would emit `-m <guess>` on every
|
|
* later launch, fatal on an account without that model. */
|
|
adoptModelAsLaunchDefault?: boolean
|
|
}): PersistedNativeChatSessionOptions {
|
|
const currentAgent = args.persisted?.[args.agent]
|
|
const currentModelValues = currentAgent?.valuesByModel?.[args.modelId] ?? {}
|
|
const valuesByModel = {
|
|
...currentAgent?.valuesByModel,
|
|
...(args.optionId === 'model'
|
|
? {}
|
|
: {
|
|
[args.modelId]: { ...currentModelValues, [args.optionId]: args.value }
|
|
})
|
|
}
|
|
return {
|
|
...args.persisted,
|
|
[args.agent]: {
|
|
...currentAgent,
|
|
...(args.adoptModelAsLaunchDefault === false
|
|
? {}
|
|
: { model: args.optionId === 'model' ? String(args.value) : args.modelId }),
|
|
valuesByModel
|
|
}
|
|
}
|
|
}
|