Skip to content

feat(serve): local web UI + read-only API for capshelf projects - #1

Open
genged wants to merge 6 commits into
mainfrom
claude/web-ui-design-9o3rb7
Open

feat(serve): local web UI + read-only API for capshelf projects#1
genged wants to merge 6 commits into
mainfrom
claude/web-ui-design-9o3rb7

Conversation

@genged

@genged genged commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Install Anthropic's frontend-design skill and Vercel Labs'
vercel-react-best-practices skill via skills.sh for the web UI work.
Tracked in skills-lock.json; .claude/skills symlinks point at the
real directories under .agents/skills.

Claude-Session: https://claude.ai/code/session_01GqDEcrivurtwK3oW8STB9Y

claude added 6 commits June 27, 2026 09:53
Install Anthropic's frontend-design skill and Vercel Labs'
vercel-react-best-practices skill via skills.sh for the web UI work.
Tracked in skills-lock.json; .claude/skills symlinks point at the
real directories under .agents/skills.

Claude-Session: https://claude.ai/code/session_01GqDEcrivurtwK3oW8STB9Y
Add `capshelf serve`: a Bun.serve server exposing a read-only JSON API and
a React (Vite) single-page UI for inspecting a project's reconcile state.

- src/status-report.ts: extract the status computation from the status
  command into a shared buildStatusReport() so the CLI and the API share one
  source of truth (status --json output is unchanged).
- src/serve-api.ts: /api/health, /api/status, /api/catalog over the existing
  core; GET-only (writes return 405).
- src/commands/serve.ts: Bun.serve wiring, static SPA serving with traversal
  guard and a not-built hint page; --port/--host/--no-open.
- web/: React + Vite app in the Patina theme (OKLCH; color reserved for drift
  status, mono for SHAs). App shell, Overview, Items master-detail with a live
  diff drawer, Shelf, and Bundles screens. Light + dark.

Read-only v1: data-repo/network writes stay deliberate in the CLI.

Claude-Session: https://claude.ai/code/session_01GqDEcrivurtwK3oW8STB9Y
Binary embedding (1):
- scripts/embed-web.ts generates src/web-embed.ts (base64 of web/dist) so
  `bun build --compile` bakes the UI into the single binary.
- src/web-embed.ts: committed empty stub; serve falls back to web/dist on
  disk when empty (dev path), serves embedded assets when populated.
- Makefile `web` target builds + embeds; `build` compiles then restores the
  stub so the working tree stays clean.

New screens (3):
- /api/config + /api/activity (git.ts recentCommits) over existing core.
- Settings: data-repo binding, paths, install mode, tracked-item counts.
- Activity: data-repo commit timeline (empty-state when unbound).
- Command palette (⌘K): fuzzy nav + items + shelf; ?cmdk=1 deep-link.

Verified: typecheck, lint, compiled binary serves the embedded UI, all
screens render against live data.

Claude-Session: https://claude.ai/code/session_01GqDEcrivurtwK3oW8STB9Y
Keep the server read-only and the data-repo-write boundary intact, but make
every write action discoverable: clicking a write affordance opens a shared
command dialog showing the exact `capshelf …` to copy and run.

- CommandDialog: provider + useCommand() hook + dialog (copy button, note).
  ?cmd=<args> deep-links it open for shareable "run this" links.
- Wired: topbar Apply, Overview row menu (update/revert/keep-local/status by
  state), Shelf Add (add <kind>/<name>), Bundles Add (add bundles/<name>),
  Item drawer actions (update/revert/keep-local). Drawer's inline command is
  replaced by contextual action buttons that open the dialog.

Claude-Session: https://claude.ai/code/session_01GqDEcrivurtwK3oW8STB9Y
Correctness:
- serve.ts: path-traversal boundary now requires a trailing separator so a
  sibling dir like "<web/dist>-evil" can't satisfy the startsWith() check.
- serve-api.ts /api/activity: parse `n` as a positive integer (was
  `Number(x) || 25`, which swallowed 0 and accepted floats fed to `git -n`).
- serve-api.ts catalog: drop unsafe `as ItemKind` cast that defeated the
  runtime guard; use ITEM_KINDS.find for a real ItemKind | undefined.
- web useFetch: shared fetch hook with an unmount guard; Activity/Settings no
  longer setState after unmount (also dedups their load/error boilerplate).

Cleanup:
- Share metadataJsonFields from metadata.ts (ls.ts + serve-api.ts) instead of
  two copies; merge the duplicate ./git import.
- api.ts STATE_LABEL: drop the duplicate kept_local key and the redundant cast.
- ItemScreen: remove the pass-through VersionInline wrapper; include row.state
  in the diff effect deps.

Verified: typecheck, biome lint, web build, 478/479 tests (the one failure is
a pre-existing environmental init test), serve smoke incl. traversal attempt.

Claude-Session: https://claude.ai/code/session_01GqDEcrivurtwK3oW8STB9Y
The serve feature's API and helpers had no direct tests (and were where the
review found bugs). Add meaningful coverage:

- serve-api.test.ts: drives handleApiRequest in-process against a real bound
  project + data repo — routing (null/405/404/500), and the shape/behavior of
  health, status, catalog (incl. kind filter + invalid-kind ignore), config,
  and activity (newest-first, head match, n cap + junk fallback).
- serve-static.test.ts: staticTarget path confinement — the sibling-prefix
  bypass and parent traversal that the review fix closed.
- recent-commits.test.ts: ordering, field parsing, limit, separator-unsafe
  subjects, and [] on a non-repo.

Refactor: extract staticTarget() from serve.ts's serveStatic so the
confinement check is unit-testable as a pure function.

500 tests, 1 pre-existing environmental failure (init bootstrap) unchanged.

Claude-Session: https://claude.ai/code/session_01GqDEcrivurtwK3oW8STB9Y
@genged genged closed this Jun 29, 2026
@genged genged reopened this Jun 29, 2026
@genged genged changed the title chore: add frontend-design and vercel-react-best-practices skills feat(serve): local web UI + read-only API for capshelf projects Jun 29, 2026
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.

2 participants