feat(dashboard): per-profile skills toggle UI - #25116
Conversation
Adds a profile selector to the dashboard's Skills page so each installed
profile's skills.disabled list can be managed from the same dashboard
daemon. Until now, /api/skills only knew the active profile (whichever
HERMES_HOME the dashboard process was launched under), so toggling skills
for a non-active profile required spinning up a second dashboard daemon
bound to that profile's HERMES_HOME — operationally awkward for users
running multiple profiles (e.g. a default + a worker/specialist profile).
Backend
-------
- Add GET /api/profiles/{name}/skills — list a profile's skills
- Add PUT /api/profiles/{name}/skills/toggle — toggle for one profile
- Add is_active to ProfileInfo so the UI can identify the daemon's
resident profile (the one served by the legacy /api/skills routes).
- Reads/writes go directly against the profile's config.yaml
(skills.disabled). The load_config/save_config helpers are bound to
the process-level HERMES_HOME via get_config_path(), so they can't
be reused for cross-profile mutation without invasive global state
changes.
- v1 omits skills.external_dirs scanning for non-active profiles. The
dropdown targets profile-installed skills; external dirs are still
respected by the gateway at runtime.
Frontend
--------
- SkillsPage gains a Select dropdown next to the enabled-of count
(hidden when there's only one installed profile, so default-only
installs are unchanged).
- Default selection is the dashboard's own profile (is_active, or
is_default for older gateways that don't emit the field).
- Switching profile refetches the skills list from the profile-scoped
endpoint; the active-profile selection still uses the legacy
/api/skills route to stay in sync with the gateway's skill index.
- Toggles route through the appropriate endpoint based on selection.
The legacy /api/skills and /api/skills/toggle routes are untouched and
remain the canonical path for the active profile.
|
CI failed but the diff vs That test exercises The other 25 failures all reproduce on recent
Diff summary: main run = 25 failed / 22342 passed; this PR = 26 failed / 22343 passed — the +1 pass is from the 5 new tests in this PR less the +1 jiter flake. Could a maintainer re-run the failed job to clear the flake? (No admin rights here.) Happy to debug further if it reproduces. |
|
This has been implemented on current Automated hermes-sweeper review evidence:
The route shape differs from this PR's proposed |
What does this PR do?
Adds a profile selector to the dashboard's Skills page so each installed
profile's
skills.disabledlist can be managed from one dashboard daemon.Today,
GET /api/skillsandPUT /api/skills/toggleonly know the activeprofile (whichever
HERMES_HOMEthe dashboard process was launchedunder) — they go through
load_config()/save_config(), which resolveto
get_config_path()=get_hermes_home() / config.yaml. Togglingskills for a non-active profile therefore requires spinning up a second
dashboard daemon per profile (one
--portper profile) which isoperationally awkward once you have a default + worker/specialist
profiles colocated.
This PR adds the missing
/api/profiles/{name}/skillsroutes, surfacesis_activeonProfileInfoso the UI can recognise the daemon'sresident profile, and renders a Select dropdown in the Skills sidebar
that scopes the page to whatever profile you pick. The legacy
/api/skills*routes are untouched.Related Issue
Fixes #
(Filed alongside this PR — happy to link if there's an existing tracking
issue I missed.)
Type of Change
Changes Made
Backend —
hermes_cli/web_server.pyGET /api/profiles/{name}/skills— list a profile's installed skills with their enabled statePUT /api/profiles/{name}/skills/toggle— toggle one skill for one profileis_active: boolonProfileInfo— true when the profile's path resolves toget_hermes_home(). Lets the UI mark "this dashboard's own profile" without a magic profile name. Backward-compatible: optional on the frontend type.config.yaml(skills.disabledkey) usingatomic_yaml_write—load_config/save_configcan't be reused because they're bound to the process-levelHERMES_HOME.Frontend —
web/src/lib/api.ts,web/src/pages/SkillsPage.tsxapi.getProfileSkillsandapi.toggleProfileSkillhelpers.ProfileSelect on top, defaulting to the dashboard's own profile (is_active, falling back tois_defaultfor older gateways). Hidden for single-profile installs so default-only setups are unchanged./api/skillsroute so it stays in sync with the gateway's in-process skill index.Tests —
tests/hermes_cli/test_web_server.pytest_profiles_list_marks_default_profile_activetest_profile_skills_list_picks_up_profile_dir(verifies category derivation from a SKILL.md dropped under<profile>/skills/productivity/<name>/)test_profile_skill_toggle_persists_to_profile_config(round-trip including re-enable removes the entry)test_profile_skills_unknown_profile_404test_profile_skills_invalid_name_400How to Test
Smoke-tested live on macOS 26.3 (Apple Silicon) against Hermes
v2026.5.7 + 126 commits, with 7 profiles. Full round-trip (UI toggle →profile's
config.yamlupdated → other profile'sconfig.yamlunchanged) confirmed via curl + filesystem inspection.
Checklist
Code
feat(dashboard):)pytest tests/hermes_cli/test_web_server.pyand all relevant tests pass (3 pre-existingTestPtyWebSocketfailures are unrelated and reproduce on upstreammain)test_web_server.py)Documentation & Housekeeping
skills.disabledis the existing storageKnown limitations
skills.external_dirsfor non-active profiles in v1. The gateway still respects external_dirs at runtime; this only affects the dashboard UI's listing for non-resident profiles. Easy follow-up if anyone needs it.@nous-research/uiSelectdoesn't expose a portal prop, so the dropdown is placed in the Skills sidebar rather than the page header (the page header'soverflow: hidden+z-1would clip a listbox there). Sidebar placement also reads better — the selector visually scopes the entire view including the category filters.Screenshots
Open dropdown (sidebar) listing all 7 profiles installed on the test
machine, with
defaultannotated(active):