feat(webui): model selector upgrade — provider-aware groups, thinking effort, modality badges - #38
Merged
Conversation
Ticket 04 — model selector upgrade (final slice of the
provider-configuration feature). Built on the schema (01), presets
(02), and management UI (03) already merged on `main`.
Server (engine ↔ webui contract):
- GET /api/models now surfaces `currentThinking` from the engine's
`thinkingEffort` configOption (falling back to `cs.model.thinking`,
then `null`) and routes the per-model `thinkingLevels` /
`modalities` through the response — the new selector reads them
directly without a second round-trip.
- POST /api/set-model accepts `{model?, thinking?}` independently:
a thinking-only update leaves the model alone, a model-only update
carries the recorded effort with it, and `thinking: ""` clears the
recorded override. When a session exists, both pushes go through
`session/set_config_option{configId: "thinkingEffort"}` — the
engine's `thinkingEffort` configId (see packages/tui/src/acp/
control-state.ts#ACP_CONFIG_THINKING_EFFORT) maps 1:1 to the
webui level; the engine validates the level against the selected
model's `effortOptions` and answers invalidParams on a mismatch,
which the route surfaces as `thinkingSynced: false` + `warning`.
Pre-session the recorded effort also flows through the same path on
the next `applyRecordedModel`, which now pushes model first then
effort (engine contract: "Select a Session model before changing
thinking effort.", agent.ts#1003).
- applyConfigOptionUpdate propagates the engine's
`thinkingEffort.currentValue` into `cs.model.thinking` so a TUI
change propagates back through the SSE snapshot.
- state-bus default for `model.thinking` switched from the cosmetic
"On" to "" (the runtime "no override" sentinel); the snapshot is
built through the existing `...cs` spread, so the new value rides
on every push without further wiring.
Frontend (composer ModelSelect + ThinkingEffortSelect):
- ModelSelect now consumes the `groups[]` shape with `auth.{hasKey,
type}` and a per-model `modalities[]`. Groups with `hasKey ===
false` render greyed with a "configure in Settings" hint, models
carry modality badges (text/image/audio/video/file → i18n). The
engine session group (`__engine`) has no `auth` and stays usable.
- ThinkingEffortSelect is mounted next to the model selector only
when the active model declares a non-empty `thinkingLevels`; the
menu enumerates exactly the model's levels plus an "engine
default" option (sends `thinking: ""`). Disabled while a run is
active — the documented "running → next turn" semantic.
- Chip label appends `· <level>` when `cs.model.thinking` is set.
- composer.tsx wires both selectors through the new
`api.setModel({model, thinking})` payload, threading the recorded
effort across mid-session model changes.
Tests:
- model.check.mjs: handleGetModels surfaces `currentThinking`
(engine / pre-session / null paths); handleSetModel persists,
pushes, and surfaces `thinkingSynced` for the 6 new branches
(both, model-only, thinking-only, clear, empty-payload 400,
engine rejection, in-place configOptions mirror).
- mcode-acp-note.test.js: applyRecordedModel for the 6 new effort
paths (both / effort-only / model-only / empty / no modelOption /
engine-rejected-effort) plus applyConfigOptionUpdate for the 3 new
propagation paths.
- composer-models.test.ts: 11 new tests covering `isGroupDisabled`,
`modalityBadgeKey`, `thinkingLevelKey`, and the setModel payload
shape the composer sends (model+effort together, model-only,
effort-only, clear).
Gates: webapp typecheck 0 errs, webapp tests 264/264 pass (+11),
unit tests 825/825 pass (no regressions), `pnpm build` green,
`pnpm check:source` 4585 files clean.
Live self-check (isolated 18108/18109, own data dir, fresh
providers.json with one byok + two no-key providers): open model
selector → no-key groups greyed with "请在设置中配置 API Key" hint,
modality badges visible on every model row, pick `openai_compat/
gpt-4o` (thinkingLevels [low, medium, high]) → effort picker mounts,
pick "高" → chip becomes "GPT-4o · 高", reload → both selections
round-trip via `currentThinking` (engine-side pre-session record).
Screenshots under /tmp/dev-ms/shot-ms{1,2,3}-*.png.
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.
What
The final slice of the provider-configuration feature.
auth.hasKey === falserender greyed with a config hint (clicks no-op); modality badges (文本/图像/文件) per model row.thinkingLevelsmounts a level picker; levels map 1:1 to the engine'sthinkingEffortconfig option with the engine's model-first-then-effort ordering enforced in bothhandleSetModelandapplyRecordedModel; the engine'sconfig_option_updatemirrors back intocs.model.thinking. Pre-session record round-trips (currentThinking).Acceptance (independent agent)
PASS-WITH-CONCERNS, engine contract verified LIVE in both directions: recorded "high" on a brand-new session hit the engine's advertisement check (
Invalid params: Thinking effort is not advertised…) and degraded exactly as designed (logged, engine default stands, currentThinking reflects engine truth); "medium" accepted mid-session and round-tripped. First-turn guard / applyRecordedModel ticket-01 semantics intact. The "6 pre-existing failures" claim was confirmed properly this time (clean main clone WITH deps: 6/6 fail on main, 6/6 pass on this branch — incidental fix). Gates: typecheck 0 · test:webapp 264/264 · test:unit 825/825 · build ✓ · check:source ✓.Non-blocking: webui thinkingLevels metadata (preset-authored) can disagree with the engine's per-model effortOptions — the engine rejects unknown levels safely; surfacing rejected picks in the UI is a possible follow-up.
Full
pnpm verifydeferred to CI.