the picker stopped overruling the default

a new chat offered fable. not displayed it — SELECTED it, sent it, and the
server honoured the choice it was given. the composer's fallback was
providerModels[0], the first entry in the catalogue, which is not a default but
an ordering accident. the file already carried a comment about the day fable was
added above opus and every resumed session came back claiming fable; the same
accident then quietly defeated the whole server-side default.

so the fallback prefers the platform's default and uses catalogue order only
when that model is not in the provider's list at all — which is every opencode
model, where there is no officer-wide default to prefer.

and a test, because a comment cannot hold a constant mirrored across two
packages. it asserts the client's copy equals the server's, and that the value
names a tier rather than the bare harness. both of the ways this default has
been defeated are now failures a test catches: a drifted mirror, and a stored
harness read as a choice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Officer-Session: 656f5c85-e49a-4f1f-b9f3-042a04b4d043
This commit is contained in:
2026-08-25 18:33:38 +00:00
co-authored by Claude Opus 5
parent 2ed47fa65e
commit 4adf96efa4
3 changed files with 82 additions and 1 deletions
@@ -3,6 +3,7 @@ import { CirclePlay, ClipboardList, FolderOpen } from 'lucide-react';
import { toneText } from '@/components/Data';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import type { ModelOption } from 'state/useModels';
import { CHAT_DEFAULT_MODEL } from '../types';
import { getProviderDisplayName } from 'state/useModels';
import type { ChatMessage } from '../types';
import { UsagePopover } from './UsagePopover';
@@ -88,7 +89,18 @@ export function ModelSelector({
const activeProvider = availableModels.find((m) => m.id === displayModel)?.provider ?? providers[0];
const providerModels = availableModels.filter((m) => m.provider === activeProvider);
const fallbackModelId = providerModels[0]?.id ?? null;
/*
The platform's default first, and the catalogue's order only as a last resort.
`providerModels[0]` is not a default — it is whichever entry happens to be listed first, and the
comment below records what that cost once already. It cost it again on 2026-08-25: with the server
defaulting to Sonnet, this line selected `fable`, sent it, and the server honoured the choice it was
given. A default the client can silently overrule is not a default.
Falls back to the first entry only when the platform default is not in this provider's list at all —
which is every OpenCode model, where there is no Officer-wide default to prefer.
*/
const fallbackModelId = providerModels.find((m) => m.id === CHAT_DEFAULT_MODEL)?.id ?? providerModels[0]?.id ?? null;
/*
Pick a default only for a chat that has not started.
@@ -1,3 +1,18 @@
/**
* What an unstarted chat offers when nothing else has been chosen.
*
* **Mirrors `CHAT_DEFAULT_MODEL` in `src/servers/api/chat/websocket.ts` and must not drift from it.** Same
* arrangement as `COMMANDER_DEFAULT_MODEL` in `apps/CommandHQ/types.ts`, and for the same reason: the
* server decides, but the picker has to show the same answer or it silently sends a different one.
*
* The composer used to fall back to `providerModels[0]` — the first entry in the catalogue. That is not a
* default, it is an ordering accident, and the file already carried a comment about the day `fable` was
* added above `opus` and every resumed session came back claiming fable. The same accident then defeated
* the server-side default entirely: the client selected fable, SENT fable, and the server honoured what it
* was told.
*/
export const CHAT_DEFAULT_MODEL = 'claude-code/sonnet';
export type Attachment =
| { type: 'webpage'; url: string; title: string; content: string; attachmentId: string; loading?: boolean }
| { type: 'image'; filename: string; dataUrl: string; attachmentId: string; loading?: boolean }