Make ProviderModelSelector preset-first with a Custom-combination compose popover and harness-keyed effort
- Dominant language
- JavaScript
- Stars
- 38
- Forks
- 31
- Avg merge
- 22m
- Merged PRs (30d)
- 988
Description
Part of #7561 (epic).
Blocked by #7564
## Problem / Goal
~99 client surfaces pick a provider/model/effort (63 files import `ProviderModelSelector`, 50 import `useProviderModels`, 7 use bespoke ``s). The request is explicit: do not add a wall of selects everywhere. Make the shared selector **preset-first** — one dropdown of enabled presets — with a single "Custom combination…" entry that opens a compact compose flow (harness → method → service → model → effort) yielding a composite id, plus "Save as preset". Effort becomes harness-keyed.
## Context
- `client/src/components/ProviderModelSelector.jsx` (307): `selectableProviders()` + `filterHardwareCompatibleProviderModels()` + `effortLevelsForProvider(selectedProvider, effectiveModel)`; three callbacks `onProviderChange/onModelChange/onEffortChange`; `''` = inherit; unavailable saved values stay visible with a reason (#6368).
- `client/src/hooks/useProviderModels.js` (242): loads `getProviders()`, returns `{providers, activeProviderId, selectedProviderId, selectedModel, availableModels, …}`; no effort state.
- `EffortSelect.jsx`, `AgentJobProviderFields.jsx`, `AppProviderPin.jsx`, `FeatureProviderPicker.jsx`, `StagePromptModelPicker.jsx` (tier-in-model-field), `VisionProviderPicker.jsx`, `CaptionModelPicker.jsx`, `ShellProviderLauncher.jsx` (TUI-only launch).
- Bespoke selects to migrate: `cos/PersistentMindTaskModelAllowlistControls.jsx`, `cos/PersistentMindTemporaryRoute.jsx`, `settings/VoiceTab.jsx`, `providers/ProviderRouteRow.jsx`, `pages/AIProviders.jsx` (run panel), `meatspace/post/PostSessionLauncher.jsx`, `pages/Loops.jsx`, `pages/RunnerPage.jsx`.
- Client mirrors: `client/src/utils/providerModels.js` re-exports server ladders and `clampEffortToLadder`; `providerTypes.js` re-exports predicates. `client/src/AGENTS.md`: no `alert/confirm`, `htmlFor`/`id` pairing, mobile responsive, selection in the URL where it opens a record, silent-vs-toasting errors.
- Server contract from child 3: `GET /api/providers/catalog` (harnesses, services, compatibility, effortLevels, effortLevelsByModel, presets); composite ids valid wherever `providerRefSchema` is used.
## Proposed approach
1. **`useProviderCatalog()`** (new hook, `client/src/hooks/`): fetches `/api/providers/catalog` once per mount tree (shared cache like `useProviderModels`), exposes `presets`, `harnesses`, `services`, `compatiblePairs(harnessId)`, `methodsFor(harnessId)`, `modelsFor(serviceSlug)`, `effortLevelsFor(harnessId, model)`, `resolveRef(id)` (preset record or a synthesized display record for a composite: label `Pi · TUI · NVIDIA NIM (free)`, models from the service catalog). `useProviderModels` keeps its contract and gains `resolveRef` pass-through; its `providers` list = presets ∪ the currently saved composite (so an existing pin renders).
2. **`ProviderModelSelector`.** Default render: one `` grouped by harness label listing enabled presets; last group "Custom" with one option "Custom combination…" (opens `ProviderComposePopover`) and, when the saved value is a composite, that composite rendered as its own option. Model and effort selects follow as today, sourced from `resolveRef`. `ProviderComposePopover` (new, `client/src/components/providers/`): four stacked selects (harness → method → service; model + effort reuse the existing controls), each narrowing the next by compatibility, plus an optional "Credential bootstrap" select (None | configured apps) rendered only when the method is `cli`/`tui` AND at least one bootstrap app exists — and required, with a hint, when the chosen service's credential is `via: 'bootstrap'` — an inline readiness line (harness detected? service credential/daemon?), buttons "Use once" (emits the composite id) and "Save as preset…" (name field → `POST /api/providers/presets`, then selects the new preset). Keyboard-operable, labelled, mobile-stacked, no `confirm()`.
3. **Effort.** `EffortSelect` and `effortLevelsForProvider` on the client take `harnessId` + `model` first, falling back to record sniffing for legacy presets; the server-published `effortLevels`/`effortLevelsByModel` replace the sanitized-record fallback rung.
4. **Callers.** `AgentJobProviderFields`, `AppProviderPin`, `FeatureProviderPicker`, `VisionProviderPicker` (api method only), `ShellProviderLauncher` (tui method only: presets with `method: 'tui'` plus compose restricted to TUI) adopt the preset-first selector; caller mode policy (`callerModeList`) filters both presets and compose methods. The 8 bespoke selects switch to the shared selector or `useProviderCatalog`; `StagePromptModelPicker` keeps its Tier/Specific toggle, Specific mode uses the shared selector.
5. **Tests.** Rendered-interaction tests for: preset-first list grouped by harness; composing `pi.tui@nvidia-nim-free` and emitting it; "Save as preset" round trip; a saved composite whose service is now disabled rendering with its reason; effort ladder switching when the harness changes; `callerMode` restrictions. `client/src/numberFormattingConventions.test.js`-style tree-wide guard: no new bare provider `` outside the shared components.
## Acceptance criteria
- [ ] Every surface that used `ProviderModelSelector`/`useProviderModels` renders one dropdown by default with no additional selects until "Custom combination…" is chosen; the 8 bespoke selects are gone.
- [ ] A composite chosen through compose is saved in the caller's existing `{providerId, model, effort}` field with no caller schema change; choosing a bootstrap appends `+`, and the bootstrap select is absent when no app is configured.
- [ ] Unavailable saved presets and composites stay visible with a reason and are never auto-replaced.
- [ ] Effort options for `codex` change with the model and for `antigravity` disappear for a tier-less model, driven by the published ladders.
- [ ] a11y: labels paired, focus returns to the trigger on close, works at 360px width; `act()` warnings are failures.
- [ ] Client and server catalog barrels/READMEs updated for the new hook and component.
## Out of scope
The AI Providers page tabs (child 6); the tier-in-model-field convention of prompt stages (unchanged).
Contributor guide
Research direction
Start with #7564, then read client/src/components/ProviderModelSelector.jsx, client/src/hooks/useProviderModels.js, and the listed provider picker callers. Review the proposed catalog hook and rendered-interaction tests before changing the shared selector. Done means preset-first rendering, compose and save flows, harness-keyed effort, migrated bespoke selects, and the stated accessibility and tree-wide guard tests passing.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript, react
- Domain
- frontend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 25/100