diff --git a/src/shared/structured-agent-session-options.test.ts b/src/shared/structured-agent-session-options.test.ts index 542cf80e294..a00cf82abe9 100644 --- a/src/shared/structured-agent-session-options.test.ts +++ b/src/shared/structured-agent-session-options.test.ts @@ -1,5 +1,8 @@ import { describe, expect, it } from 'vitest' -import { CODEX_SESSION_OPTION_CATALOG } from './agent-session-option-catalog-claude-codex' +import { + CLAUDE_SESSION_OPTION_CATALOG, + CODEX_SESSION_OPTION_CATALOG +} from './agent-session-option-catalog-claude-codex' import { buildNativeChatSessionOptionSnapshot } from './native-chat-session-option-snapshot' import { createNativeChatSessionOptionRecord } from './native-chat-session-option-state' import { @@ -93,6 +96,33 @@ describe('structured agent session options', () => { }) }) + it.each([ + { agent: 'codex' as const, seed: CODEX_SESSION_OPTION_CATALOG }, + { agent: 'claude' as const, seed: CLAUDE_SESSION_OPTION_CATALOG } + ])( + 'keeps the options row when an older host publishes an empty list: $agent', + ({ agent, seed }) => { + // Wire case, not a unit case: `structuredAgentSessionOptionCatalog` runs on the client over + // whatever a host published. A host that predates the readers' own seed floor still sends + // `models: []` beside a current model for a restored thread, and the row must survive it. + const state = applyStructuredAgentSessionOptions( + createStructuredAgentSessionOptionState(agent), + seed, + { models: [], current: { model: 'unlisted-from-an-old-host' } } + ) + + const snapshot = structuredAgentSessionOptionSnapshot(state) + expect(snapshot.map((descriptor) => descriptor.id)).toEqual(['model', 'effort']) + const model = snapshot[0]! + // Only official names reach the pill; the raw id is neither offered nor shown. + expect(model).toMatchObject({ valueSource: 'unknown' }) + expect(model.kind.type === 'select' ? model.kind.choices.map((c) => c.value) : []).toEqual( + seed.models.map((seeded) => seeded.id) + ) + expect(snapshot[1]).toMatchObject({ id: 'effort', settable: true, kind: { type: 'select' } }) + } + ) + it('projects live options as directly settable descriptors', () => { const state = applyStructuredAgentSessionOptions( createStructuredAgentSessionOptionState('codex'), diff --git a/src/shared/structured-agent-session-options.ts b/src/shared/structured-agent-session-options.ts index 529f6567b56..febb890bea0 100644 --- a/src/shared/structured-agent-session-options.ts +++ b/src/shared/structured-agent-session-options.ts @@ -52,7 +52,12 @@ export function structuredAgentSessionOptionCatalog( // Why: only ids the provider lists may be offered. An unlisted `current.model` still // reaches the snapshot through the record, which draws it as tracked-but-unmatched — // a neutral pill plus effort from `unknownModelOptions` — instead of a raw-id row. - const models: CatalogModel[] = result.models.map(discoveredModel) + const discovered: CatalogModel[] = result.models.map(discoveredModel) + // Why here and not only in each host-side reader: this runs on the client, on a result a host + // of any version published. An empty list offers nothing to pick and carries no options, so the + // snapshot drops the whole row — and a host that predates the readers' own seed floor publishes + // exactly that for a restored thread whose provider listed nothing. The seed is agent-agnostic. + const models = discovered.length > 0 ? discovered : [...seed.models] return { ...seed, models, defaultModelIsCliDefault: true } }