Skip to content

feat(dashboard): per-profile skills toggle UI - #25116

Closed
cypres0099 wants to merge 1 commit into
NousResearch:mainfrom
cypres0099:feat/dashboard-per-profile-skills
Closed

feat(dashboard): per-profile skills toggle UI#25116
cypres0099 wants to merge 1 commit into
NousResearch:mainfrom
cypres0099:feat/dashboard-per-profile-skills

Conversation

@cypres0099

Copy link
Copy Markdown
Contributor

What does this PR do?

Adds a profile selector to the dashboard's Skills page so each installed
profile's skills.disabled list can be managed from one dashboard daemon.

Today, GET /api/skills and PUT /api/skills/toggle only know the active
profile (whichever HERMES_HOME the dashboard process was launched
under) — they go through load_config() / save_config(), which resolve
to get_config_path() = get_hermes_home() / config.yaml. Toggling
skills for a non-active profile therefore requires spinning up a second
dashboard daemon per profile (one --port per profile) which is
operationally awkward once you have a default + worker/specialist
profiles colocated.

This PR adds the missing /api/profiles/{name}/skills routes, surfaces
is_active on ProfileInfo so the UI can recognise the daemon's
resident 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

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

Changes Made

Backendhermes_cli/web_server.py

  • GET /api/profiles/{name}/skills — list a profile's installed skills with their enabled state
  • PUT /api/profiles/{name}/skills/toggle — toggle one skill for one profile
  • is_active: bool on ProfileInfo — true when the profile's path resolves to get_hermes_home(). Lets the UI mark "this dashboard's own profile" without a magic profile name. Backward-compatible: optional on the frontend type.
  • Per-profile reads/writes go directly against the profile's config.yaml (skills.disabled key) using atomic_yaml_writeload_config/save_config can't be reused because they're bound to the process-level HERMES_HOME.

Frontendweb/src/lib/api.ts, web/src/pages/SkillsPage.tsx

  • New api.getProfileSkills and api.toggleProfileSkill helpers.
  • Skills sidebar gets a Profile Select on top, defaulting to the dashboard's own profile (is_active, falling back to is_default for older gateways). Hidden for single-profile installs so default-only setups are unchanged.
  • Switching profile refetches skills via the profile-scoped endpoint; the active selection still goes through the legacy /api/skills route so it stays in sync with the gateway's in-process skill index.
  • Toggles route through the matching endpoint based on the current selection.

Teststests/hermes_cli/test_web_server.py

  • test_profiles_list_marks_default_profile_active
  • test_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_404
  • test_profile_skills_invalid_name_400

How to Test

# 1. Start the dashboard against your default profile
hermes dashboard --host 127.0.0.1 --port 9120 --insecure

# 2. Create a second profile (or use one you have)
hermes profile create scratch-worker --clone

# 3. Open http://127.0.0.1:9120/skills — the new "Profile" dropdown
#    appears above Filters in the sidebar. Default shows "default (active)".

# 4. Pick "scratch-worker" — the skill list reloads with that profile's
#    enabled state. Toggle a skill; the change writes to
#    ~/.hermes/profiles/scratch-worker/config.yaml under skills.disabled,
#    leaving ~/.hermes/config.yaml untouched.

# 5. From the CLI you can verify:
cat ~/.hermes/profiles/scratch-worker/config.yaml | grep -A2 '^skills:'

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.yaml updated → other profile's config.yaml
unchanged) confirmed via curl + filesystem inspection.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (feat(dashboard):)
  • I searched for existing PRs — none cover this gap
  • My PR contains only changes related to this feature
  • I've run pytest tests/hermes_cli/test_web_server.py and all relevant tests pass (3 pre-existing TestPtyWebSocket failures are unrelated and reproduce on upstream main)
  • I've added tests for my changes (5 new tests in test_web_server.py)
  • I've tested on my platform: macOS 26.3 (Apple Silicon)

Documentation & Housekeeping

  • No new config keys — skills.disabled is the existing storage
  • No new env vars
  • No tool description changes
  • No CONTRIBUTING.md / AGENTS.md updates needed
  • Cross-platform: pure Python + React, no OS-specific code

Known limitations

  • The profile-scoped scan does not follow skills.external_dirs for 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.
  • The @nous-research/ui Select doesn't expose a portal prop, so the dropdown is placed in the Skills sidebar rather than the page header (the page header's overflow: hidden + z-1 would 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 default annotated (active):

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.
@alt-glitch alt-glitch added type/feature New feature or request comp/cli CLI entry point, hermes_cli/, setup wizard tool/skills Skills system (list, view, manage) P3 Low — cosmetic, nice to have labels May 13, 2026
@cypres0099

Copy link
Copy Markdown
Contributor Author

CI failed but the diff vs main is a single unrelated test:

FAILED tests/hermes_cli/test_timeouts.py::test_anthropic_adapter_honors_timeout_kwarg
  → NameError: name 'jiter' is not defined

That test exercises agent/anthropic_adapter.py's timeout config — nothing I touched in this PR. The failure is a runtime import-time issue with the anthropic SDK's Rust JSON parser (jiter), even though jiter==0.14.0 installs successfully in the dependency step. Looks flaky/environmental rather than a real regression.

The other 25 failures all reproduce on recent main runs (e.g. run 25815578471):

  • ModuleNotFoundError: No module named 'botocore' (bedrock optional dep)
  • ModuleNotFoundError: No module named 'numpy' (kittentts)
  • ModuleNotFoundError: No module named 'faster_whisper'
  • 10× Dingtalk SDK 'NoneType' object has no attribute 'from_dict' / RuntimeOptions
  • 3× WeCom / Weixin cffi OpenSSL (Cryptography_HAS_OP_NO_RENEGOTIATION)
  • 1× Matrix test_check_requirements_with_token
  • 1× Feishu KeyError: 'uri'
  • 1× run_agent test_switch_model_preserves_config_context_length
  • 1× bedrock model picker

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.

@teknium1

Copy link
Copy Markdown
Contributor

This has been implemented on current main by the dashboard's profile-scoped management flow.

Automated hermes-sweeper review evidence:

  • hermes_cli/web_server.py:8630 documents that Skills & Tools endpoints accept an optional profile query param so one dashboard can manage any profile's skills/toolsets.
  • hermes_cli/web_server.py:8690 and hermes_cli/web_server.py:8703 show GET /api/skills and PUT /api/skills/toggle both scope reads/writes through _profile_scope(...).
  • web/src/lib/api.ts:44 adds the machine-level management profile scope, and web/src/lib/api.ts:600 exposes profile-aware getSkills / toggleSkill helpers.
  • web/src/pages/SkillsPage.tsx:137 consumes the selected profile and passes it into skills/toolsets loads and skill toggles.
  • web/src/components/ProfileSwitcher.tsx:7 provides the dashboard UI selector for the profile write target, hidden for single-profile installs.
  • tests/hermes_cli/test_web_server_skills_profiles.py:65 covers listing/toggling against a requested profile and verifies the dashboard's own config stays untouched.
  • The main implementation landed in 914befa9aaae46f2709373bc3d7352e813a18c54 (feat(dashboard): profile-scoped skills & toolsets management) and the global switcher in 875aa8f162aa40f07b19b2ca229720da70193d41.

The route shape differs from this PR's proposed /api/profiles/{name}/skills endpoints, but the behavior requested here is covered on main.

@teknium1 teknium1 closed this Jun 12, 2026
@teknium1 teknium1 added the sweeper:implemented-on-main Sweeper: behavior already present on current main label Jun 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have sweeper:implemented-on-main Sweeper: behavior already present on current main tool/skills Skills system (list, view, manage) type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants