Skip to content

feat(console): model switcher in chat composer#609

Open
maxnoller wants to merge 10 commits intomainfrom
worktree-cozy-shimmying-candy
Open

feat(console): model switcher in chat composer#609
maxnoller wants to merge 10 commits intomainfrom
worktree-cozy-shimmying-candy

Conversation

@maxnoller
Copy link
Copy Markdown
Member

Summary

  • Adds a portal-based model picker next to the existing agent switcher in the chat composer. Wired to the gateway's /model set slash command, hidden from the user-visible thread (hideUser: true). Re-seeds from the gateway default whenever the session changes (since /model set is session-scoped on the gateway). Surfaces fetch and switch failures via the existing setError banner so users get feedback when the dropdown silently snaps back.
  • Introduces shared Select and ScrollArea primitives under console/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.
  • Extracts a ChatModel interface in console/src/api/types.ts so the chat surface no longer imports admin-DTO shapes; AdminModelCatalogEntry now extends ChatModel.
  • Model rows show a prettified display name (claude-haiku-4-5Claude Haiku 4.5, gpt-4.1-miniGPT-4.1 Mini, deepseek-r1DeepSeek r1, o3 stays o3) with a quiet subtitle pulled from parameterSize / 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 typecheck clean from console/
  • vitest run — 242/242 passing (added a fetchModels mock to the existing chat-page tests so the new modelsQuery resolves; existing assertions unchanged)
  • Manual: open the picker on a fresh session, confirm grouping by provider/vendor, search filters by both raw slug and pretty name
  • Manual: switch sessions and confirm the picker re-seeds to the gateway default rather than carrying the previous session's choice
  • Manual: click a provider in the rail, then click "Clear filter" (renamed "All providers"), confirm filter clears
  • Manual: trigger a switch while a stream is running and confirm the inline error banner appears (Could not switch model — stop the current run and try again.)
  • Manual: stop the gateway and reload — confirm the model-load error banner shows (Failed to load the model list. Model switching is unavailable.)

Notes

  • The dev server in this worktree wouldn't bring up the SPA cleanly (Vite version mismatch between root and 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.
  • Skipped a wider Composer prop-restructure (switchers array) and a Dropdown/Select portal-positioning shared hook — both real overlap, but out of scope for this PR; happy to follow up.
  • Capability badges beyond Reasoning (vision / tool-use / image-gen) are not added because the gateway model catalog doesn't expose those flags yet. The ChatModel shape leaves room to add them without further client wiring.

maxnoller and others added 10 commits April 27, 2026 13:10
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)
Copy link
Copy Markdown
Contributor

@furukama furukama left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Die Icons sollten schon die Providerlogos sein.

Image

Und das Dropdown bitte analog zu dem Agent-Chooser daneben. Evtl. das design wiederverwenden?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants