Skip to content

feat(site/src/pages/AgentsPage): redesign chat model selector dropdown#26355

Draft
tracyjohnsonux wants to merge 9 commits into
mainfrom
designer/model-selector-redesign
Draft

feat(site/src/pages/AgentsPage): redesign chat model selector dropdown#26355
tracyjohnsonux wants to merge 9 commits into
mainfrom
designer/model-selector-redesign

Conversation

@tracyjohnsonux

Copy link
Copy Markdown
Contributor

Summary

Redesign of the agents-chatbox model dropdown to match the new layout: a searchable list grouped by provider, a clear "selected" treatment with a trailing checkmark, and an Effort row at the bottom of the panel. The trigger is a subtle inline pill with a chevron that fits next to the existing chat-footer controls.

This is a draft opened on @tracyjohnsonux's behalf by the Coder Agents bot for design review. Backend was intentionally not touched.

Visual reference

Screenshots of the implementation will be added as a follow-up comment on this PR.

What changed (frontend only)

  • ModelSelector.tsx: replaced Radix Select with Popover + Command (cmdk).
    • Search input at the top of the panel with a strict, substring-based filter so typing a model name hides unrelated rows cleanly.
    • Provider name renders as a section heading (e.g. "Anthropic", "OpenAI").
    • Each row shows the display name with a compact context-window chip, e.g. Opus 4.7 (1M).
    • The selected row gets a darker background plus a trailing checkmark icon.
    • Trigger button restyled as a subtle inline pill. Mouse-focus ring suppressed, keyboard-focus ring preserved (same intent as before the rewrite).
    • Tooltip on each row preserved for screen-reader/visual users (full name + provider + context window).
    • aria-label on each row is the model display name, so existing getByRole("option", { name: ... }) lookups keep matching even though the visible label now includes the context chip.
  • Effort row at the bottom of the panel renders the admin-configured reasoning effort for the selected model as a read-only slider plus value chip. Hidden when the selected model has no effort configured. Important: see the open question below before merging.
  • ModelSelectorOption gains an optional effort?: string field.
  • modelOptions.ts: getModelOptionsFromConfigs now reads the reasoning effort from each provider's options block (provider_options.openai.reasoning_effort, provider_options.openaicompat.reasoning_effort, provider_options.anthropic.effort) and passes it through so the Effort row shows real configuration values.
  • ModelSelector.stories.tsx: rewritten around the new design. New stories cover OpenSingleProvider, OpenMultipleProviders, OpenWithoutEffort, SearchFiltersOptions, plus trigger/disabled/empty states.

Open question (decision needed before this can become real)

The design shows an interactive Effort slider with low / medium / high / xhigh values. Today, the backend stores reasoning effort only on the admin-managed ChatModelConfig: CreateChatRequest and the message-send payloads have no reasoning_effort field. So a user-controllable per-chat effort slider needs a backend change.

This PR ships the row as a read-only display of the admin-configured value so the layout is real and not faking interactivity. Three options for what comes next:

  1. Add backend support for a per-chat effort override (new optional field on CreateChatRequest and the chat-send websocket payload, plumbed through ChatModelConfig.model_config.provider_options.{openai,openaicompat,anthropic} at request time). Then make the slider here interactive.
  2. Keep it read-only as a quick way to see the model's configured effort from the chatbox, and move "change effort" into the existing admin model-config UI.
  3. Drop the Effort row from this PR and ship just the search / provider headings / checkmark redesign.

Need your call here, @tracyjohnsonux.

Tests

  • pnpm exec tsc --noEmit clean.
  • pnpm exec biome check src/ clean.
  • Unit tests (vitest --project unit) for AgentsPage: 1370 passed.
  • Storybook interaction tests for AgentsPage: 1059 passed. (The single failing test Tool.stories.tsx > MCP Tool Completed reproduces on main without these changes; it is a pre-existing xterm flake.)
  • make lint/emdash clean.
Implementation notes for reviewers
  • The previous implementation used Radix Select, which forces a listbox-only popover and does not support the bottom Effort row. Switching to Popover + Command gives us the search input "for free" via cmdk and lets us attach the Effort footer to the same popover surface.
  • cmdk's default fuzzy filter kept loosely matching rows visible (e.g. typing opus would not fully hide Sonnet). The picker overrides filter with a strict value.includes(needle) so typing narrows cleanly.
  • The Slider primitive applies data-[disabled]:opacity-40 to its Track when disabled. The Effort row explicitly overrides that with [&_[data-disabled]]:opacity-100 so the read-only display stays at full visual weight; it's communicating real configuration, not a disabled control.
  • The trigger button preserves the existing focus-ring intent: focus:ring-0 suppresses the mouse-focus ring, focus-visible:ring-2 focus-visible:ring-content-link keeps the keyboard-focus ring. The existing trigger test (ModelSelector.test.tsx) still passes against the new markup.
  • role="combobox" + aria-haspopup="listbox" + aria-expanded are set on the trigger so the test helpers in AgentSettingsAgentsPageView.stories.tsx (getByRole("combobox", { name: ... })) keep working.

This pull request was prepared by the Coder Agents bot at @tracyjohnsonux's direction.

Replace the Radix Select-based ModelSelector with a Popover + Command
(cmdk) implementation that matches the new agents-chatbox design:

- Search input at the top of the panel for fast model lookup with
  strict, substring-based filtering.
- Provider names render as section headings ("Anthropic", "OpenAI").
- Each model row shows the display name with a compact context
  window chip, for example "Opus 4.7 (1M)".
- The selected row gets a darker background and a trailing checkmark.
- The trigger is a subtle inline pill with chevron that keeps the
  keyboard-focus ring and suppresses the mouse-focus ring.
- A read-only Effort row at the bottom renders the admin-configured
  reasoning effort for the selected model. Per-chat effort selection
  is a backend feature; this PR ships the layout without faking
  interactivity. See the PR description for the open question.

ModelSelectorOption gains an optional `effort` field, and
getModelOptionsFromConfigs reads the reasoning effort from each
provider's options block so the new row shows real data.

Co-authored-by: Coder Agents <agents@coder.com>
tracyjohnsonux and others added 8 commits June 12, 2026 22:43
- Loosen row spacing in the popover so each row's hover background has
  visible breathing room from its neighbours (gap-1 between items,
  h-10 row height, px-2.5 horizontal padding, bumped heading padding).
- Left-align the dropdown with the trigger instead of centering it.
  Radix's collision detection nudges the popover back into view when
  the trigger is close to the viewport edge.
- Turn the Effort row into an interactive slider, capped at the
  admin-configured value, so designers can demo lowering the model's
  reasoning effort. The selected value is local-only state and is not
  sent to the backend; this is a UI preview until the per-chat effort
  override is wired through the API. Tooltip text updated to spell
  that out.

Co-authored-by: Coder Agents <agents@coder.com>
…icker scrollbar

- Drop the `border-t-0` override on the model picker's `CommandList`
  so the theme-aware top border from `Command` draws a clean rule
  between the search input and the first model row.
- Style the picker's scrollbar narrow: `scrollbar-width: thin` for
  Firefox plus an 8px webkit scrollbar with a rounded
  `surface-quaternary` thumb on a transparent track. 8px keeps a
  usable hit area for the thumb while shedding the visual weight of
  the default browser scrollbar.

Co-authored-by: Coder Agents <agents@coder.com>
… every model

- Hide the round slider thumb in the Effort row so the visual
  indicator is purely the end of the filled track. The thumb element
  stays in the DOM at full size, so pointer drag and keyboard
  interaction still work; only its paint is suppressed via arbitrary
  Tailwind selectors on [role=slider].
- Always render the Effort row when a model is selected, falling
  back to `xhigh` when the admin has not configured an effort. This
  is a demo-only fallback so every model in the picker tells the
  same story; remove it once the backend lands and the row should
  hide for models that do not support reasoning.

Co-authored-by: Coder Agents <agents@coder.com>
- Lock the effort badge to a fixed width (`w-[4.5rem]`) with
  `justify-start` so the slider track keeps a constant length as
  the label flips between `Low`, `Medium`, `High`, etc.
- Animate the slider's transform, position, and width over 150ms so
  stepping between effort levels glides instead of snapping. The
  duration is short enough that pointer drags still feel snappy at
  each step boundary.
- Replace the hidden-thumb treatment with a small (10px) solid-white
  thumb, no border or shadow, with a grab/grabbing cursor so the
  handle reads as draggable without dominating the row.

Co-authored-by: Coder Agents <agents@coder.com>
…n opened

Pass the currently-selected model's cmdk value as the Command's
`defaultValue` so cmdk highlights and scrolls the row into view on
mount, instead of the dropdown opening at the top of the list with
the user's choice hidden below the fold. Extract the cmdk-value
shape into `getCmdkValue` so the same string is used both when
rendering a row and when telling cmdk which row is active.

Co-authored-by: Coder Agents <agents@coder.com>
Remove the border, background, rounded corners, and h-6 box around
the effort label so it reads as plain text next to the slider. The
fixed 4.5rem width and `justify-start` are preserved so the slider
track length still stays constant as the label flips between values
like "Low" and "Xhigh".

Co-authored-by: Coder Agents <agents@coder.com>
… picker

cmdk applies `data-selected=true` to its active row, which can be the
hovered row, the keyboard cursor row, or whichever row cmdk
auto-advanced to after a search. Painting that with the same
`bg-surface-secondary` used for the persistent-selected row made the
two read as identical, so typing into the search appeared to select
a different model than the one with the checkmark.

Drop the active state to a faint white overlay
(`bg-content-primary/[0.08]`) so the cursor stays visible for
keyboard users without competing with the real selection.

Co-authored-by: Coder Agents <agents@coder.com>
Override the Slider primitive's default 8px track height down to 6px
(`h-1.5`) for the effort slider in the model picker. The Track is
the slider's only descendant with `bg-surface-secondary`, so the
class selector targets it stably; the parent-scoped override wins
on specificity over the primitive's own `h-2`. The 10px thumb stays
the same size so the proportions read like a control dot riding a
slim track.

Co-authored-by: Coder Agents <agents@coder.com>
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.

1 participant