Skip to content

feat(tui): browse and install Skill Stacks with /stacks - #366

Merged
quickbeard merged 1 commit into
mainfrom
feat/skill-stack-tui
Sep 14, 2026
Merged

quickbeard merged 1 commit into
mainfrom
feat/skill-stack-tui

Conversation

@quickbeard

Copy link
Copy Markdown
Owner

P2 of the Skill Stack integration (P0 #361 → CLI, P1 #364 → server routes). The TUI's first way onto the CoDev SkillHub.

What it does

/stacks (also the prompt_stacks keybind, unbound by default like prompt_skills) opens a browser for the hub's Skill Stacks beside /skills. Pick one → This project or Global → an agent preset → the dialog calls prepare, and only if the hub reports skills already installed does it stop to ask skip them or overwrite them (naming the collisions). Then it installs, refreshes, and toasts Installed N of M skill(s) from <stack>, K skipped.

Escaping the conflict prompt discards the staging rather than leaving it for the server's TTL. Escaping during download does the same once the download returns; the flow never installs into a dialog the user has already left.

Why it's shaped this way

  • All hub traffic goes through the server (feat(server): serve Skill Stack browse and install over the v2 HttpApi #364's v2.skillhub routes). The install lands on the host that runs the agent — the same .claude/skills + .agents/skills folders codev skill stack pull writes.
  • The model sees the new skills immediately. Skill discovery is cached per server instance and nothing invalidates it (the QUESTION(Dax) in core/src/skill.ts), so after a successful install the dialog calls instance.dispose — exactly what a config update already does — and the next request rebuilds it. Without this a /stacks install would be invisible until restart.
  • Flow state lives in an awaited controller, not components (the dialog-configure.tsx pattern): dialog.replace stores the element factory and may re-invoke it. And since replace() fires every prior step's onClose even when this flow advances itself, "did the user escape mid-step?" is answered by checking whether the dialog still shows the factory we put up (stillShowing), never via onClose.
  • Agent picker is a preset list (CoDev only / + Claude Code / all four). DialogSelect is single-choice; a per-row toggle needs a registered keybind like dialog.mcp.toggle, which is more upstream surface than the choice warrants today. CoDev Code is in every preset and folded in server-side regardless.

Tests

packages/tui/test/skill-stack.view.test.tsx boots the real app against a fetch fixture and drives the whole flow through rendered frames: browse → scope → agents → conflict prompt → install → toast, asserting /instance/dispose was called exactly once and nothing was discarded; a second test escapes at the conflict prompt and asserts the staging was discarded and nothing installed.

Full TUI suite 271 pass / 0 fail; keymap test and opencode's test/cli/run (which pins keybind defaults) pass; bun typecheck clean.

Bookkeeping

  • DIVERGENCES.md row for prompt/index.tsx + config/keybind.ts (the two upstream files touched; the dialog and test are fork-only).
  • CHANGELOG.md → Unreleased → Skills.
  • User docs live in codev-landing-page (content/docs/code) — /stacks and prompt_stacks need a line there once this ships.

Next: P3, the desktop app's Stacks tab on the same routes.

🤖 Generated with Claude Code

https://claude.ai/code/session_01P1MfYkJD3tQZRgqBKB4gig

Adds the TUI's first way onto the CoDev SkillHub: `/stacks` (and the
unbound `prompt_stacks` keybind) opens a browser for the hub's Skill
Stacks, then walks scope (this project or global), an agent preset, and
— only when prepare reports collisions — a skip-or-overwrite choice
before installing. Escaping the conflict prompt discards the staging
rather than leaving it to the server's TTL.

Everything goes through the server's v2.skillhub routes (#364), so the
install lands on the host that runs the agent, in the same folders
`codev skill stack pull` writes. After a successful install the dialog
disposes the server instance — what a config update does too — because
skill discovery is cached per instance and nothing else invalidates it;
without that the model would not see the new skills until a restart.

Flow state lives in an awaited controller rather than components, as in
dialog-configure.tsx: dialog.replace stores the element factory and can
re-invoke it. And since replace() fires every prior step's onClose even
when this flow advances itself, "did the user escape mid-download?" is
answered by checking whether the dialog still shows the step we put up.

The agent picker is a preset list because DialogSelect is single-choice
(a per-row toggle needs its own keybind, like dialog.mcp.toggle); CoDev
Code is in every preset and is folded in server-side regardless.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01P1MfYkJD3tQZRgqBKB4gig
@quickbeard
quickbeard merged commit b47d8ea into main Sep 14, 2026
0 of 5 checks passed
@quickbeard
quickbeard deleted the feat/skill-stack-tui branch September 14, 2026 03:55
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.

1 participant