Skip to content

feat(web): guided picker for subagent model + provider (Dashboard half of #67347) - #67557

Open
DavidMetcalfe wants to merge 6 commits into
NousResearch:mainfrom
DavidMetcalfe:feat/delegation-picker-dashboard
Open

DavidMetcalfe wants to merge 6 commits into
NousResearch:mainfrom
DavidMetcalfe:feat/delegation-picker-dashboard

Conversation

@DavidMetcalfe

Copy link
Copy Markdown
Contributor

What does this PR do?

Closes the Dashboard half of the delegation.model / delegation.provider
picker UX gap originally reported in #67347. Mirrors the Desktop
implementation in #67523 (same design, different React app).

The Dashboard's ConfigPage (web/src/pages/ConfigPage.tsx) was
surfacting both fields as bare free-text inputs via the generic
schema-driven AutoField renderer — same UX problem Desktop just fixed.
This PR replaces them with a paired provider + model picker sourced from
/api/model/options (the same endpoint the chat model picker and MoA
preset picker already use).

Key behaviors (matched to the Desktop implementation):

  • "Inherit from main agent" is a first-class dropdown option that
    writes "" to both keys — the documented default at
    hermes_cli/config.py:2297-2298 and the resolver at
    tools/delegate_tool.py:3056-3170.
  • Provider switch clears the model so a new provider never pairs with
    the old provider's model.
  • Custom-endpoint handling: providers with an empty models list (user-
    defined endpoints without probed models) fall back to a free-text input.
  • Out-of-catalog persisted models stay selectable so an existing valid
    pick renders instead of blank.
  • Gateway-unreachable degradation: falls back to free-text inputs with
    a warning banner; the settings page is never locked.

The atomic two-key write happens inside setConfig at the parent — one
combined update, never a half-pair.

Related Issue

Fixes #67347 (Dashboard half — Desktop half landed in #67523)

Type of Change

  • ✨ New feature (non-breaking change that adds functionality)

Changes Made

  • web/src/components/DelegationModelProviderField.tsx (new, +201 lines)
    — the picker. Uses the Dashboard's existing useState + useEffect
    fetch pattern (no react-query in web/src/), wired to api.getModelOptions().
  • web/src/pages/ConfigPage.tsx — hooks the picker into renderFields.
    delegation.model row renders <DelegationModelProviderField>, the
    delegation.provider row returns null (avoiding a duplicate free-text
    input below the picker). Both keys written atomically via two
    setNestedValue calls inside one setConfig.
  • web/src/i18n/en.ts — 7 new keys plus a function-typed
    delegationCurrentlyInheriting: (model: string) => string (matches
    the Desktop i18n pattern).
  • web/src/i18n/types.ts — type counterparts.
  • web/src/i18n/{ja,zh,zh-hant,af,de,es,fr,ga,hu,it,ko,pt,ru,tr,uk}.ts
    — 14 non-English locales stubbed with English values (all Dashboard
    locales declare Translations directly; no defineLocale shortcut
    available).

How to Test

  1. Open the Dashboard at /config (or whatever the route is for the
    config editor — see web/src/pages/ConfigPage.tsx).
  2. Search/filter to find delegation.model. The row should render the
    new picker (provider dropdown + model dropdown or free-text fallback)
    instead of a free-text input.
  3. With both fields blank, verify "Inherit from main agent" is the
    default option and a helper line shows the inherited main model.
  4. Select a provider — the model dropdown should populate with that
    provider's catalog models (or show a free-text input if the catalog
    is empty).
  5. Select a model — verify both delegation.model and delegation.provider
    are written to the config (no half-pair persisted).
  6. Switch providers — verify the model clears.
  7. Stop the gateway and refresh — verify the page falls back to text
    inputs + a warning banner instead of locking.

Checklist

Code

Documentation & Housekeeping

  • I've updated relevant documentation — N/A (no docs change needed; UI improvement to existing fields).
  • I've updated cli-config.yaml.example if I added/changed config keys — N/A (no new config keys).
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — N/A.
  • I've considered cross-platform impact — N/A (web frontend, no platform-specific code).
  • I've updated tool descriptions/schemas — N/A.

Test gap (explicit)

The Dashboard has no existing React component tests, no
@testing-library/react, and no jsdom configured in vitest.config.ts.
Adding test infra for one component is scope creep. Cross-vendor review
on the diff is included in the PR description; if reviewers want a
test, happy to add it as a follow-up (would require installing
@testing-library/react + jsdom and updating vitest.config.ts).

Screenshots / Logs

N/A — UI change, no logs to capture. The fix is verifiable by opening
the Dashboard's config page and observing the picker replace the
free-text input.

Notes

  • The Dashboard's picker is structurally the same as the Desktop one
    (apps/desktop/src/app/settings/delegation-model-provider-field.tsx)
    but uses useState/useEffect instead of useQuery because the
    Dashboard's web/src/ has no react-query setup.
  • i18n keys live under t.config.* (matching the Desktop's
    t.settings.config.* convention from feat(desktop): add guided subagent model/provider picker (#67347) #67523) since these are
    config-editor affordances, not model-picker affordances.
  • Locale stubs use English values; translators can fill in real
    translations in a follow-up.

Mirrors the Desktop implementation in NousResearch#67523 to close the Dashboard
half of the UX gap reported in NousResearch#67347. The Dashboard's
`ConfigPage` surfaced both fields as bare free-text inputs via the
generic schema-driven `AutoField` renderer — same problem Desktop
just fixed.

Replaces the schema-driven rendering for `delegation.model` and
`delegation.provider` with a paired provider + model picker sourced
from `/api/model/options` (the same endpoint the chat model picker
and MoA preset picker use).

Key behaviors (matched to the Desktop implementation):

- "Inherit from main agent" is a first-class dropdown option that
  writes '' to both keys — the documented default at
  hermes_cli/config.py:2297-2298 and the resolver at
  tools/delegate_tool.py:3056-3170.
- Provider switch clears the model so the new provider never pairs
  with the old provider's model.
- Custom endpoints with empty model catalogs fall back to a free-text
  input.
- Out-of-catalog persisted models stay selectable.
- Gateway-unreachable degrades to free-text inputs + warning banner,
  never locks the page.
- Atomic write at the parent via two `setNestedValue` calls inside
  one `setConfig` — never persists a half-pair.

Implementation notes:

- Uses the Dashboard's existing `useState` + `useEffect` fetch
  pattern (no react-query setup in web/src/), wired to
  `api.getModelOptions()`.
- i18n keys live under `t.config.*` (matching the Desktop's
  `t.settings.config.*` from NousResearch#67523) since these are config-editor
  affordances, not model-picker affordances.
- All 14 non-English locales stubbed with English values — translators
  fill in real translations in a follow-up.

Test gap (explicit, called out in PR description): the Dashboard has
no React component test infrastructure (@testing-library/react, jsdom)
and adding it for one component is scope creep. Cross-vendor review
on the diff is the verification path; a follow-up test can land
separately if reviewers want it.

Fixes NousResearch#67347 (Dashboard half)

Local verification:
- pnpm tsc clean (apps/desktop equivalent: `tsc --noEmit -p
  web/tsconfig.app.json`)
- All 89 existing web vitest tests still pass.
Two SHOULD-FIX items from cross-vendor review of the prior commit:

1. **Atomic state merge in ConfigPage.** Replaced two sequential
   `setNestedValue` calls with a single functional `setConfig` update
   that builds the merged object before commit. Eliminates the brief
   window where one of the two keys could be updated in isolation.

2. **Loading state in the picker.** Added `isLoading` state, set
   `disabled={isLoading}` on the provider <Select>, and rendered a
   "Loading subagent catalog…" hint below while the gateway fetch is
   in flight. `isLoading` toggles inside the existing `useEffect` via
   `.finally()` so the cancelled-guard still applies.

Also adds `delegationLoading` to all 16 locale files (English +
15 non-English stubs; translators fill in real values in a follow-up).

Items intentionally not changed (with rationale recorded in the
Stage 2 brief): inline `key ===` check (matches Desktop impl),
`isDelegationModelPickerKey` helper (testable surface), hand-rolled
fetch (no react-query in web/src/), Input import (already correct).
Two NIT/SHOULD-FIX items from the second-round cross-vendor review:

1. **Race on unmount (NIT, GPT-OSS NousResearch#5).** Moved the explanatory comment
   that documents the `cancelled` gating pattern; the gate was already
   in place but now has the intent documented inline.

2. **A11y on loading hint (SHOULD-FIX, GPT-OSS NousResearch#7).** Added
   `aria-live="polite"` to the loading hint so screen readers announce
   when the catalog finishes loading.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks for carrying the Dashboard half of #67347 and for preserving the existing model-options endpoint and profile-aware config flow.

Problems

  • web/src/components/DelegationModelProviderField.tsx:135 drops a supported configuration mode: delegation.model may be set while delegation.provider remains empty. Current resolution deliberately preserves parent credentials in that state (tools/delegate_tool.py:3139-3149), and the documented contract calls it out at website/docs/user-guide/configuration.md:2023. The new provider Select has no empty/provider-inherit option when isInherit is false, so users cannot intentionally create or reliably edit this model-only override.

Suggested changes

  • Preserve model-only/provider-inherit as a distinct picker state, then add a focused interaction test covering it alongside concrete-provider and full-inherit transitions.

Automated hermes-sweeper review.

Comment thread web/src/components/DelegationModelProviderField.tsx
Two cleanups caught by the post-merge eslint pass on the push:

1. **Inline `isDelegationModelPickerKey`.** The helper exported from the
   component file was used at exactly one call site (ConfigPage.tsx),
   exported to dodge the `react-refresh/only-export-components` rule.
   Inlining as `key === "delegation.provider"` is shorter, matches
   surrounding ConfigPage style, and removes the export entirely.

2. **Drop redundant `setIsLoading(true)`.** `useState(true)` already
   initializes the loading flag; the explicit `setIsLoading(true)` at
   the top of the effect was duplicating that and triggering the
   `react-hooks/set-state-in-effect` lint rule (intentionally a
   warning in the web codebase until patterns get refactored —
   see commit 02613a4). Removed with a comment explaining why.
@teknium1 teknium1 added sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 19, 2026
Two real BLOCKERs from the sweeper + Stage 3 round 1 review on PR NousResearch#67557
(via the same review on NousResearch#67523):

1. **Circular deadlock**: the picker derived its active state from
   `draftProvider` + `draftModel` alone. Selecting MODEL_ONLY_VALUE from
   clean state ("", "") snapped the dropdown back to INHERIT_VALUE
   because the derivation maps blank+blank → inherit.

2. **Input unmount on backspace**: clearing the model field mid-edit
   unmounted the input because the derivation flipped back to inherit.

The fix introduces explicit local state for the dropdown's selected
value (`providerSelectValue`) — user INTENT — separately from the
draft model/provider pair. The dropdown no longer depends on the model
being non-empty to stay on MODEL_ONLY_VALUE; clearing the model keeps
the user in the model-only branch with an empty input ready for typing.

Provider-switch and profile-switch paths resync `providerSelectValue`
from the persisted config via the existing `useEffect` with the
echo-ref pattern (mirrors `FallbackModelsField` on Desktop).

Also shortened the dropdown option label from "Custom model (use
parent credentials)" (38 chars) to "Custom model" (12 chars); the full
explanation moves to a helper line below the input.
@DavidMetcalfe

Copy link
Copy Markdown
Contributor Author

Addressed the sweeper finding about delegation.model set without delegation.provider (the same gap flagged on #67523). Two real BLOCKERs caught in cross-vendor review and fixed:

  1. Circular deadlock: deriving the active picker branch from draftProvider + draftModel alone caused selecting "Custom model" from clean state to snap back to "Inherit" (because blank+blank → inherit). The dropdown's selected value is now its own local state (providerSelectValue), separate from the persisted model/provider pair. Selecting MODEL_ONLY_VALUE from any state stays there.

  2. Input unmount on backspace: clearing the model field mid-edit unmounted the input for the same reason. The dropdown selection is the source of truth for which branch renders; the model field can be empty without the input disappearing.

The resolver at tools/delegate_tool.py:3139-3149 preserves parent credentials in the provider="", model="…" state, so the new "Custom model" dropdown option lets users create and edit that state explicitly instead of getting it stripped.

Also shortened the dropdown option label from "Custom model (use parent credentials)" (38 chars) to "Custom model" — the full explanation now lives in the helper line below the input.

Cross-vendor review (Flash + GPT-OSS in parallel) returned unanimous ACCEPTABLE on both substantive findings after the fix; the only flagged SHOULD-FIX items were already addressed in the implementation (verified by source-tracing). Diff: +189 / -31 across 18 files.

Same approach should fix the equivalent gap on #67523 if the maintainer wants to apply it there too.

…t OR

Three cleanups caught during the post-merge walkthrough on the
state-machine refactor:

1. **Extract `selectValueFor(provider, model)` helper.** The
   `(provider, model) → dropdown value` mapping was duplicated in
   the `useState` seed and the `useEffect` resync. Now both call
   sites use the same helper.

2. **Drop the defensive `isInherit` OR-clause.** Removed the
   `|| (draftProvider === "" && draftModel === "")` fallback from
   the `isInherit` derivation — the helper covers all three cases
   at construction, so the OR is dead code.

3. **Type the helper parameters explicitly.** `selectValueFor` is
   declared with `string` parameters, so TypeScript guarantees no
   `undefined` can reach the truthy checks (the GPT-OSS round-3
   BLOCKER "could crash on undefined" is a TypeScript-narrowing
   misread — verified by source trace).

tsc + vitest + eslint all clean:
- tsc --noEmit -p web/tsconfig.app.json: 0 errors
- vitest run: 89/89 passing
- eslint src/components/DelegationModelProviderField.tsx: 0 warnings

Diff: +16 / -23 (one file).
@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/dashboard Web dashboard / control panel UI (dashboard/, landing) tool/delegate Subagent delegation area/config Config system, migrations, profiles labels Jul 19, 2026
@alt-glitch

Copy link
Copy Markdown
Contributor

This was generated by AI during triage.
Related: this is the Dashboard implementation of #67347; #67523 implements the corresponding Desktop surface.

@alt-glitch alt-glitch removed the sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades label Jul 19, 2026
@DavidMetcalfe

DavidMetcalfe commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

@OutThisLife @teknium1 — Checking in on #67557. All required checks are passing, the PR is mergeable, and no concerns have been raised since the July 19 review.

Status

  • All required checks pass (JS/TS typecheck and tests across workspaces, Docker builds, uv.lock check).
  • The one review finding — the delegation.model-without-delegation.provider state being unreachable — is addressed in my July 19 comment: the picker now exposes a dedicated "Custom model" dropdown option (local providerSelectValue state, separate from the persisted pair), so the documented model-only override can be created and edited explicitly, with resolver behavior unchanged.

Scope
This is a targeted UI change (~580 lines): one new component plus ConfigPage wiring, replacing two free-text inputs in the existing Dashboard config editor with a guided picker for the already-documented delegation.model/delegation.provider keys. No new config keys, no CLI/RPC surface. It pairs with the Desktop half (#67523), which is also still open.

If everything looks good, would it be okay to merge — and #67523 as well, if you agree?

@DavidMetcalfe

Copy link
Copy Markdown
Contributor Author

@OutThisLife @teknium1 — Following up on my August 10 check-in with one piece of context that may be relevant here.

Since then, #74375 (unified subagent model/reasoning controls) was closed without merging, while the related surfaces remain open: the CLI picker (#76480), the Desktop half (#67523), and this Dashboard half. If the direction for #67347 has shifted toward a single unified surface, I'm happy to reshape or close this PR to fit. Otherwise it's ready for review: all required checks are green on the current head, it's mergeable with no conflicts, and the single review finding (the model-only override state) was addressed in the July 19 comment above.

If anything specific is blocking review — scope, the test-gap note in the description, or anything else — let me know and I'll address it.

andrexibiza added a commit to andrexibiza/hermes-agent that referenced this pull request Sep 7, 2026
Assemble the existing guided picker and pin/config fallback matrix on
NousResearch/hermes-agent main e9bccc9.
Expose default delegation provider/model and ordered backups together on
the Models screen, using the same scoped editor for the Advanced entry.
Keep partial selections local, preserve model-only overrides, pin catalog
and config operations to one connection/profile, and confirm persisted
writes. Negotiate backend support so older runtimes cannot accept inert
fallback settings through the new editor.

Port the worker fallback decision into the post-decomposition runtime
owner. Declared chains work with explicit pins; [] disables recovery;
absent/null retains pin-aware defaults. Explicit inherit/parent is opt-in,
canonical empty suppresses aliases, and returned route metadata is deeply
child-owned. Reuse the canonical normalizer and existing recovery loop.

Source implementation and provenance:
- webtecnica: NousResearch#67523, source head
  fd6e822; omit its unrelated runner change.
- Ayush Nangia: NousResearch#80479, source head
  a0a8d8d; preserve the pin/config matrix
  and spfcraze's model-only-pin correction.
- Earlier fallback work: Ayush Nangia NousResearch#65052, Axl Ibiza NousResearch#80421,
  wz-heng NousResearch#80438, and Teknium NousResearch#80465 pin protection already on main.
- devatnull NousResearch#81072 supplies explicit inheritance semantics, not its
  incompatible automatic inheritance under a pin.
- TurgutKural NousResearch#101017 supplies alias compatibility, not its [] default.
- Reports/design: DavidMetcalfe NousResearch#67347, mlahatte NousResearch#65038,
  and ScotterMonk NousResearch#94629.

This is a co-authored current-main recomposition, not an unchanged
cherry-pick of the historical commits. Original branches and review
threads remain untouched. Dashboard NousResearch#67557 remains a separate surface.

Fixes NousResearch#65038
Refs NousResearch#94629
Refs NousResearch#67347
Refs NousResearch#80450

Co-authored-by: webtecnica <75556242+webtecnica@users.noreply.github.com>
Co-authored-by: Ayush Nangia <ayushnangia16@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/dashboard Web dashboard / control panel UI (dashboard/, landing) P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform tool/delegate Subagent delegation type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Guided picker for Subagent Model + Provider in Advanced Settings (Desktop + Dashboard)

3 participants