From 6f76ffba0e1ba846e71088db6154bb14baa1adf5 Mon Sep 17 00:00:00 2001 From: PannenetsF <45866836+PannenetsF@users.noreply.github.com> Date: Wed, 19 Aug 2026 12:11:12 +0800 Subject: [PATCH] fix(native-chat): keep Claude Fable selectable when CLI probe omits it The Claude CLI's list_models control request does not advertise Fable until the user has completed its one-time usage-credit consent. Because the native-chat enrichment replaced the seed model list with the probe result for Claude, Fable vanished from the model picker on any host whose CLI had not yet listed it. Switch Claude to the same additive merge used by other non-authoritative agents: seed rows (fable, opus, sonnet, haiku) are retained, and discovered rows overlay matching ids and append new ones. For matching ids the discovered option menus (effort levels, fast mode) win when present, so per-host capabilities still drive the picker. Also add Fable to the commit-message Claude agent's fallback model list so it is available for non-interactive generation too. --- .../native-chat-session-option-enrichment.test.ts | 7 +++++-- .../native-chat-session-option-enrichment.ts | 9 +++------ .../use-native-chat-session-options.test.ts | 14 +++++++++----- src/shared/agent-session-option-catalog.ts | 8 ++++++-- src/shared/commit-message-agent-spec.ts | 6 ++++++ 5 files changed, 29 insertions(+), 15 deletions(-) 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