Skip to content

feat(ui): interactive Skills dialog with live enable/disable toggle - #3329

Open
joestump-agent wants to merge 14 commits into
charmbracelet:mainfrom
joestump-agent:feat/skills-dialog
Open

feat(ui): interactive Skills dialog with live enable/disable toggle#3329
joestump-agent wants to merge 14 commits into
charmbracelet:mainfrom
joestump-agent:feat/skills-dialog

Conversation

@joestump-agent

Copy link
Copy Markdown
Contributor

Summary

Adds an interactive Skills management dialog that lets users browse all discovered skills and toggle individual skills on/off live — no config-file editing, no restart.

Builds on the ReloadSkills / SkillSnapshot infrastructure from #3319.

What it does

  • New "Skills" command in the command palette opens a full-screen dialog
  • Lists all skills — active, disabled, and errored — with their source (builtin/user/project), state indicator (on/off/error), and user-invocable flag
  • enter / ctrl+d: toggle the selected skill's enabled state by updating options.disabled_skills in config, then triggers an immediate ReloadSkills so the change takes effect instantly
  • ctrl+r: reload skills from disk (picks up new/edited/deleted skill files)
  • Type to filter, / to navigate

Why

Currently the only way to disable a skill is to hand-edit options.disabled_skills in the config file and restart the session. A live, per-skill toggle in a browsable dialog is a significant UX improvement — users can experiment with skills without leaving the TUI.

Changes

File What
internal/ui/dialog/skills.go New. The dialog: filterable list of all skills with toggle/reload keybindings
internal/ui/dialog/actions.go Add ActionSkillToggle action type
internal/ui/dialog/commands.go Add "Skills" command palette entry that opens the dialog
internal/ui/model/ui.go openSkillsDialog() + toggleSkill() handler + ActionReloadSkills also closes Skills dialog
internal/workspace/workspace.go Add GetSkillStates() to the Workspace interface
internal/workspace/app_workspace.go Implement GetSkillStates() via Manager.States()
internal/workspace/client_workspace.go Implement GetSkillStates() via local Manager.States()

Dependencies

Test plan

  • go build ./... passes
  • go test ./internal/ui/dialog/... ./internal/workspace/... ./internal/skills/... passes
  • go vet clean
  • Manual: open Skills dialog, toggle a builtin skill off, verify it disappears from <available_skills>, toggle it back on
  • Manual: add a skill file to disk, ctrl+r in the dialog, verify it appears

💘 Generated with Crush

@joestump-agent

Copy link
Copy Markdown
Contributor Author

Pushed one more commit: builtin skills (crush-config, crush-hooks, jq) now declare user-invocable: true so they show up in the command palette alongside discovered user skills — a natural companion to the dialog. Includes catalog-filter test coverage.

LarsArtmann and others added 14 commits August 16, 2026 07:12
Skill discovery was re-run from scratch on every prompt build, ignoring
the already-built Manager. Combined with symlink-unaware dedup (keyed by
raw path string), a single invalid skill could log "Skill validation
failed" up to 10× per invocation.

Three changes:

1. DiscoverWithStates: resolve symlinks before keying the seen map so
   the same file reached via two paths is parsed/validated/warned once.
2. Prompt: add WithSkills option so callers pass pre-discovered skills
   instead of re-walking the filesystem on every Build.
3. Coordinator + agentic-fetch: pass activeSkills via WithSkills at both
   prompt construction sites.

Closes charmbracelet#2683 (symlink UI duplicate — same root cause).
See charmbracelet#3318 for the full analysis.
Lets users pick up skill changes (new files, edits, deletions) without
restarting the session. Available as "Reload Skills" in the command
palette. The reload chain mirrors UpdateAgentModel: Manager.Reload
re-runs discovery and publishes, coordinator.ReloadSkills propagates to
the system prompt + tools + tracker, and the full Workspace interface
(local + client-server) is wired.

Wires WithDiscoveryConfig into both Manager construction sites so Reload
has the config it needs.
Discovery involves filesystem I/O that could hang on a stalled NFS
mount or broken symlink. 10s timeout matches the pattern used by
UpdateAgentModel and other UI handlers.
logDiscoveryStats was only called from discoverSkills (the coordinator's
fallback path that no production caller hits). Move the discovery-level
logging into DiscoverFromConfig itself so startup AND reloads are logged.

Split the coordinator-specific prompt/token stats into
logPromptSkillStats and call it from NewCoordinator and ReloadSkills.
- Manager.Reload now takes context.Context and returns error
  (cancellation checked after discovery, before swap)
- Add swagger annotations to /skills/reload endpoint
- Add tests: Manager.Reload (discovery + cancellation),
  WithSkills (stores + nil fallback), ReloadSkills (nil manager guard)
- Fix misleading skillsMgr comment
- gofmt clean on all changed files
Both production callers (root.go, backend.go) always pass a Manager.
The fallback path was dead code that existed only to handle a case
that never happens in production. Removing it makes the Manager
requirement explicit.
- agenttest.NewCoordinator now passes an empty skills.Manager instead
  of nil, matching the requirement that NewCoordinator receives one.
- Fix gofumpt formatting on all one-liner ReloadSkills test stubs.
Two separate calls to AllSkills() + ActiveSkills() were a torn read —
Reload could swap one slice between them. Reload now returns both,
and SkillSnapshot provides the same atomic read without reload.
Three fixes from code review:

1. ReloadSkills now builds models+prompt BEFORE swapping coordinator
   state, so a mid-reload error leaves the agent on the old skills
   instead of a half-committed mix.

2. InitializePrompt now accepts pre-discovered skills via WithSkills,
   eliminating the filesystem walk that caused duplicate "Skill
   validation failed" warnings on /init.

3. Manager.Reload no longer sets m.states before PublishStates
   overwrites it — PublishStates is now the single mutation point as
   documented.

Added TestManager_SkillSnapshot and TestManager_SkillSnapshot_AfterReload
to verify atomic reads of both skill slices.

💘 Generated with Crush

Assisted-by: Crush:glm-5.2
Adds a full-screen Skills management dialog accessible from the command
palette. Lists all discovered skills — including disabled and errored
ones — with their source, state, and user-invocable flag.

Key bindings:
  - enter / ctrl+d: toggle a skill on/off (writes options.disabled_skills)
  - ctrl+r: reload skills from disk
  - type to filter, up/down to navigate

The toggle updates the config and triggers an immediate skill reload, so
changes take effect without restarting the session or editing config files
by hand.

Builds on the ReloadSkills / SkillSnapshot infrastructure from charmbracelet#3319.
Builtin skills (crush-config, crush-hooks, jq) now declare
user-invocable: true so they surface in the palette like discovered
user skills, with coverage for the catalog filter.

Assisted-by: Claude Fable 5
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants