diff --git a/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.test.ts b/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.test.ts index b5650cc16bc..2ddb78374a5 100644 --- a/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.test.ts +++ b/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.test.ts @@ -98,7 +98,7 @@ describe('native chat session option enrichment', () => { ) }) - it('uses only discovered Claude rows and capabilities per host', async () => { + it('merges seed Claude rows with discovered capabilities per host', async () => { mocks.discoverRuntimeCommitMessageModels.mockResolvedValue({ success: true, catalogOrigin: 'probe', @@ -135,7 +135,10 @@ describe('native chat session option enrichment', () => { await vi.waitFor(() => expect(listener).toHaveBeenCalledOnce()) const models = readNativeChatEnrichedModels('claude', 'ssh:host')! - expect(models.map(({ id }) => id)).toEqual(['opus[1m]', 'sonnet']) + // Why: seed rows the CLI omits (e.g. fable, which needs a one-time consent + // before list_models advertises it) must stay selectable, so the merge keeps + // every seed id and overlays discovered rows on top. + expect(models.map(({ id }) => id)).toEqual(['fable', 'opus', 'sonnet', 'haiku', 'opus[1m]']) const sonnetEffort = models.find(({ id }) => id === 'sonnet')?.options[0] expect(sonnetEffort?.kind).toMatchObject({ type: 'select', diff --git a/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.ts b/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.ts index 1bb49699050..dc089caa573 100644 --- a/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.ts +++ b/src/renderer/src/components/native-chat/native-chat-session-option-enrichment.ts @@ -101,12 +101,9 @@ export function ensureNativeChatModelEnrichment(args: { if (!discovered || discovered.length === 0) { return } - entry.models = - args.agent === 'claude' - ? [...discovered] - : catalog.discoveredModelsAreAuthoritative - ? mergeDiscoveredAuthoritativeModels(catalog.models, discovered) - : mergeCatalogModels(catalog.models, discovered) + entry.models = catalog.discoveredModelsAreAuthoritative + ? mergeDiscoveredAuthoritativeModels(catalog.models, discovered) + : mergeCatalogModels(catalog.models, discovered) for (const listener of entry.listeners) { listener([...entry.models]) } diff --git a/src/renderer/src/components/native-chat/use-native-chat-session-options.test.ts b/src/renderer/src/components/native-chat/use-native-chat-session-options.test.ts index a24d1b8f87f..d966777c13f 100644 --- a/src/renderer/src/components/native-chat/use-native-chat-session-options.test.ts +++ b/src/renderer/src/components/native-chat/use-native-chat-session-options.test.ts @@ -82,10 +82,14 @@ describe('useNativeChatSessionOptions model reporting', () => { await waitFor(() => expect(modelDescriptor(result.current.snapshot).currentValue).toBe('opus[1m]') ) - // The invented family row is gone: every choice is one the host listed. + // Seed rows the host omitted (e.g. fable) stay selectable; discovered rows + // overlay the matching seed ids and append any new ones. expect(modelDescriptor(result.current.snapshot).choices.map((choice) => choice.value)).toEqual([ - 'opus[1m]', - 'haiku' + 'fable', + 'opus', + 'sonnet', + 'haiku', + 'opus[1m]' ]) }) @@ -130,7 +134,7 @@ describe('useNativeChatSessionOptions model reporting', () => { await waitFor(() => expect( modelDescriptor(result.current.snapshot).choices.map((choice) => choice.value) - ).toEqual(['opus[1m]', 'haiku']) + ).toEqual(['fable', 'opus', 'sonnet', 'haiku', 'opus[1m]']) ) expect(modelDescriptor(result.current.snapshot).currentValue).toBeUndefined() }) @@ -171,7 +175,7 @@ describe('useNativeChatSessionOptions model reporting', () => { await waitFor(() => expect( modelDescriptor(result.current.snapshot).choices.map((choice) => choice.value) - ).toEqual(['opus[1m]', 'haiku']) + ).toEqual(['fable', 'opus', 'sonnet', 'haiku', 'opus[1m]']) ) expect(modelDescriptor(result.current.snapshot).currentValue).toBeUndefined() }) diff --git a/src/shared/agent-session-option-catalog.ts b/src/shared/agent-session-option-catalog.ts index b242b6b27c9..8ef10a18abf 100644 --- a/src/shared/agent-session-option-catalog.ts +++ b/src/shared/agent-session-option-catalog.ts @@ -54,7 +54,11 @@ export function findCatalogOption( return model?.options.find((option) => option.id === optionId) } -/** Merge live rows over the static seed while retaining cataloged option mappings. */ +/** Merge live rows over the static seed while retaining cataloged option mappings. + * Why: for agents whose discovered rows carry their own option menus (Claude's + * per-model effort levels and fast-mode flag come from the CLI probe), the live + * options win; for agents whose discovery returns no options, the seed's menus + * are kept so the picker does not go blank. */ export function mergeCatalogModels( seed: readonly CatalogModel[], discovered: readonly CatalogModel[] @@ -66,7 +70,7 @@ export function mergeCatalogModels( return model } discoveredById.delete(model.id) - return { ...model, ...live, options: model.options } + return { ...model, ...live, options: live.options.length > 0 ? live.options : model.options } }) return [...merged, ...discoveredById.values()] } diff --git a/src/shared/commit-message-agent-spec.ts b/src/shared/commit-message-agent-spec.ts index c65850ba08a..0ffdefb1e3a 100644 --- a/src/shared/commit-message-agent-spec.ts +++ b/src/shared/commit-message-agent-spec.ts @@ -357,6 +357,12 @@ export const COMMIT_MESSAGE_AGENT_SPECS: Partial