feat(console): model switcher in chat composer#609
Open
Conversation
Adds a portal-based model picker next to the existing agent switcher in
the chat composer. Wires it to the gateway's /model set slash command,
re-seeded from the gateway default whenever the session changes (since
/model set is session-scoped). Surfaces fetch and switch failures via
the existing setError banner so users get feedback when the dropdown
silently snaps back.
Includes new shared Select and ScrollArea primitives (portal positioning,
typeahead, vertical-rail filter, search) and a ChatModel type that the
admin catalog DTO now extends, so the chat surface no longer imports
admin-specific shapes. Model rows show a prettified display name
("Claude Haiku 4.5", "GPT-4.1 Mini") with a quiet subtitle pulled from
parameterSize / family / vendor and a context-window count on the right.
…G, use shadow token - Remove the auto-insert-scrollbar branch in <ScrollArea>; the only consumer (the new <Select> popup) renders the scrollbar explicitly, and the child- type sniffing it relied on is the same fragile pattern we already removed from <SelectContent>. - Replace the inline Codex SVG in model-switch-select with an <img> pointing at console/public/icons/codex.svg (which already shipped in this PR), so the brand mark has a single source of truth. - Replace the hardcoded popup box-shadow with var(--shadow-popover) (defined in theme.css for both light and dark) so the picker matches the rest of the popover surfaces in the console.
Adds per-row capability icons (Vision / Reasoning / Tool calling / Image gen), a colored cost-tier indicator ($/$$/$$$/$$$+), and an info button that opens a side detail panel with feature pills, a metadata grid (provider, developer, knowledge cutoff, context window, parameter size, cost tier), and the raw model id. Capability flags, cost ($/M-token bucketed into low/medium/high/highest), and knowledge-cutoff dates are sourced live from the MIT-licensed models.dev catalog (https://models.dev/api.json), fetched once at gateway startup with a 24h TTL and cached in memory. Failures degrade silently — picker just doesn't show the new badges/fields, identical to the previous behavior. Adds shared Select primitives for the new affordances: - SelectItemCapability (icon + tooltip per capability) - SelectItemCostTier (semantic dollar-sign tier with label) - SelectItemDetailButton(stops bubbling so it doesn't commit selection) - SelectDetailPanel (slot in <SelectContent detail={…}>) Plus four new icons (Eye, Brain, Image, Info) using the existing icons module.
Numeric segments of 5+ digits in a model id are date or build stamps
(YYYYMMDD on Anthropic's claude-opus-4-1-20250805, claude-opus-4-5-20251101,
claude-sonnet-4-5-20250929 etc.), not user-facing version components. Drop
them at the segment-split step so the prettifier doesn't glue them onto the
version number ("Claude Opus 4.1.20250805" -> "Claude Opus 4.1").
The bare-slug HybridAI default `gpt-4.1-mini` was grouping under
"Local · OpenAI" in the chat model picker because the picker fell back
to a hardcoded `'Local'` literal when the id had no `provider/` prefix
and `backend` (only set for `ollama`/`lmstudio`/`vllm`) was null. Add an
explicit `provider` field on the gateway catalog row (resolved from id
prefix, defaulting to `'hybridai'`) and have the picker's bare-slug
branch use it for grouping. The id stays unchanged so `/model set
gpt-4.1-mini` and the admin Models page keep working.
Drops the Vision / Reasoning / Tools / ImageGen capability badges,
cost-tier indicator, info button, and side detail panel from the
picker, plus the models.dev catalog fetch that backed them. Picker
rows now show display name + subtitle + context window only. Also
removes the now-unused `Select{Capability,CostTier,DetailButton,
DetailPanel}` primitives, their CSS, and the four icon files
(Brain/Eye/Image/Info) that only the detail UI used.
…ng-candy # Conflicts: # console/src/api/types.ts
The previous commit's `PROVIDER_LABELS` only mapped `hybridai` and
`openai-codex`, leaving every other gateway-tagged provider (`codex`,
`anthropic`, `mistral`, `gemini`, `ollama`, …) to the kebab-to-Title
fallback in `pretty()`. That produced wrong casings like "Lmstudio",
"Vllm", "Xai" and silently grouped each one at rank 50 in insertion
order. Worse, after the simplify pass renamed `'openai-codex'` to
`'codex'` to align with the providerHealth key, the picker's two-segment
branch — which still keyed on the id prefix `parts[0]` ("openai-codex")
— stopped finding a label entry for Codex models.
Type `PROVIDER_LABELS` as `Record<GatewayModelProviderKey, KnownProvider>`
so the compiler now refuses to ship a new provider without an explicit
label, and route every branch in `parseModel()` through `entry.provider`
(the gateway-tagged providerHealth key) so the lookup matches regardless
of id structure. `PROVIDER_RANK` and `PROVIDER_ICONS` stay sparse via
`Partial<Record<…>>` since their fallbacks (rank 50, ServerIcon) are
deliberate. Adds the missing `anthropic/` entry to the gateway prefix
table, and warn-logs when a `/`-bearing id falls through unmatched —
silent miscategorization was the regression class that caused the
original "Local · OpenAI" bug.
Adds direct test coverage for both surfaces: `parseModel` now has unit
tests for bare-slug HybridAI defaults, Ollama-tagged backends, and the
Codex two-segment id; `getGatewayAdminModels` asserts each row carries
the right providerHealth key including the `'openai-codex/'` →
`'codex'` translation.
…lect on top Aligns the console with the shadcn/baseui pattern of a generic Popover primitive that menus and listboxes both compose, instead of two parallel popover implementations. Adds focus-visible rings on the listbox/popup, splits the Select focus effect so arrow-key navigation no longer thrashes the search input, and restores the date-stamp filter in prettifyModelName so Anthropic model IDs render as "Claude Opus 4.1" rather than "Claude Opus 4 20250805". - New components/popover: portal, viewport-flip positioning, click-outside, document Escape, configurable focusOnOpen strategy. - Dropdown rebuilt on Popover (~110 LOC, was 281); items carry role=menuitem. - Select rebuilt on Popover (~620 LOC, was 861); SelectRail/SelectSearch/ SelectBadge dropped from public exports; product chrome moved to routes/chat/model-switch-select with sibling CSS module. - Search icon extracted to components/icons/Search.tsx.
…used openai.svg Inline stroke SVGs for Local and Server in model-switch-select belong with the rest of the monochrome icon set. The openai.svg in public/icons was added but never referenced. - New components/icons/Local.tsx and Server.tsx using the shared Icon base - Deleted public/icons/openai.svg (dead asset)
furukama
requested changes
Apr 28, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Summary
/model setslash command, hidden from the user-visible thread (hideUser: true). Re-seeds from the gateway default whenever the session changes (since/model setis session-scoped on the gateway). Surfaces fetch and switch failures via the existingsetErrorbanner so users get feedback when the dropdown silently snaps back.SelectandScrollAreaprimitives underconsole/src/components/(portal positioning, viewport flipping, click-outside, typeahead, keyboard navigation with aria-activedescendant, vertical-rail filter, search input slot, item subtitle slot). Built consciously narrow to this picker today; designed so future consumers can adopt the same primitives.ChatModelinterface inconsole/src/api/types.tsso the chat surface no longer imports admin-DTO shapes;AdminModelCatalogEntrynowextends ChatModel.claude-haiku-4-5→ Claude Haiku 4.5,gpt-4.1-mini→ GPT-4.1 Mini,deepseek-r1→ DeepSeek r1,o3stays o3) with a quiet subtitle pulled fromparameterSize/family/vendor, a Reasoning badge when applicable, and a context-window count on the right. Selected row is bolded; provider rail relabels to Clear filter when a filter is active.Test plan
pnpm typecheckclean fromconsole/vitest run— 242/242 passing (added afetchModelsmock to the existing chat-page tests so the newmodelsQueryresolves; existing assertions unchanged)Could not switch model — stop the current run and try again.)Failed to load the model list. Model switching is unavailable.)Notes
console/package), so manual verification was done against the source and against an existing preview build of the prior native<select>. A reviewer with a working dev server should run through the manual list above before merge.Dropdown/Selectportal-positioning shared hook — both real overlap, but out of scope for this PR; happy to follow up.Reasoning(vision / tool-use / image-gen) are not added because the gateway model catalog doesn't expose those flags yet. TheChatModelshape leaves room to add them without further client wiring.