Files
Claude-Code-Monitor/docs/superpowers/specs/2026-08-06-lane-actions-ui-design.md
nntrivi2001 5f114d7c6f docs(lanes): design F4 — surface E1-F3c lane actions in the UI
LaneCard gets agents-install/mcp-sync buttons, integration status
badges, and a sync-base --check button (read-only preflight only — no
merge button, that stays a session/skill action). Settings gets
skills-install and housekeeping (gc) buttons, machine-wide. Two new
routes needed (POST /api/skills/install, POST /api/lanes/gc) since
those two primitives were CLI-only until now.
2026-08-06 09:09:30 +07:00

8.5 KiB

F4 — Surfacing E1-F3c's lane actions in the UI

Status: approved 2026-08-06.

Problem

E1 through F3c (this session) built six CLI-only primitives — ccam lanes sync-base, ccam lanes agents install, ccam lanes mcp sync, ccam lanes integration <name>, ccam lanes gc, ccam skills install — with no UI surface. The user wants the lane-scoped, day-to-day-useful ones clickable from the dashboard instead of terminal-only.

Scope

In scope — LaneCard.tsx (per-lane, gated on runtime?.available, same condition the existing up/down button already uses since all four need a .ccam/profile):

  1. "Install agents" buttonPOST /lanes/:id/agents/install (exists). Shows the installed file list on success.
  2. "Sync MCP" buttonPOST /lanes/:id/mcp/sync (exists). Shows synced servers + seeded profiles.
  3. Integration status badges (tracker / dev_qc / ci_wait), read-only, on/off — reads GET /lanes/:id/integrations/:name (exists) for each of the three fixed names.
  4. "Check dev sync" buttonPOST /lanes/:id/sync-base with {mode: "check"} (exists — the route already supports mode). Shows DEV_DELTA/DEV_OVERLAP or a collision list. Read-only preflight only — no merge button. Merging is a deliberate, evidence-recording action the driving skill/session takes (ccam lanes sync-base bare, or --continue); a UI click is the wrong place for a git merge with fix-loop implications. This matches the project's own boundary: "the console never writes a lane's stage" — extending that spirit, the console doesn't perform git merges either.

In scope — Settings.tsx (machine-wide, new "Lanes" section near the existing Hooks section — modeled on "Reinstall Hooks"/"Cleanup"): 5. "Install ship-feature-lane skill" button → new route POST /api/skills/install (doesn't exist yet — needs building; wraps the same fs.cpSync logic ccam skills install's CLI already has, factored into server/lib/lane-agents.js or a new tiny server/lib/skills-install.js so the CLI and route share one implementation instead of duplicating the copy logic). 6. "Run housekeeping" button, with a "dry run" checkbox → new route POST /api/lanes/gc (doesn't exist yet — wraps server/lib/lane-gc.js's reapOrphanMcp/capOversizedLogs, already built and tested in F3c). Shows reaped pids + capped log paths.

Explicitly out of scope:

  • A merge button for sync-base (see #4's reasoning).
  • Any UI for ccam lanes sync-base --continue (conflict resolution is a session/skill action with real judgment calls about which side to keep — not a button).
  • Refactoring the CLI (bin/ccam.js) to call the new HTTP routes instead of its current direct require() of the lib modules — the CLI's local-only shape (no server round-trip needed when you're already on the machine) stays as-is; only the browser needs an HTTP path, so the routes are additive, not a replacement for the CLI's existing implementation.

Design

Two new backend routes

  • POST /api/skills/install — same sameOriginGuard as every other mutating route. Extracts the copy logic (REPO_ROOT/.claude/skills/ship-feature-lane~/.claude/skills/ship-feature-lane, fs.cpSync with force: true) out of bin/ccam.js's cmdSkillsInstall into a small exported function in server/lib/skills-install.js, so bin/ccam.js and the route both call the same code (no duplication). Returns {installed: true, path: string}.
  • POST /api/lanes/gc — same guard. Body {dryRun?: boolean}. Calls reapOrphanMcp/capOversizedLogs from the already-built server/lib/lane-gc.js (no changes needed there — Task 2 of F3c already exports both). Returns {reaped: number[], capped: {path: string, sizeBefore: number}[]}.

Both routes are machine-wide (no :id), registered at the router's top level (not under /lanes/:id/...) — /api/skills/install sits alongside other non-lane-scoped routes; /api/lanes/gc sits in the lanes router but before any :id param route (order matters: Express would otherwise try to resolve gc as a lane id on a route like /lanes/:id).

Client API additions (client/src/lib/api.ts)

Inside the existing lanes object:

agentsInstall: (id: number) =>
  request<{ installed: string[] }>(`/lanes/${id}/agents/install`, { method: "POST", body: "{}" }),
mcpSync: (id: number) =>
  request<{ servers: string[]; profilesSeeded: string[] }>(`/lanes/${id}/mcp/sync`, { method: "POST", body: "{}" }),
integration: (id: number, name: string) =>
  request<{ enabled: boolean }>(`/lanes/${id}/integrations/${encodeURIComponent(name)}`),
syncBaseCheck: (id: number, branch?: string) =>
  request<{ code: number; devDelta?: string[] | null; overlap?: string[] | null; collisions?: { file: string; collidesWith: string; suggestion: string }[] }>(
    `/lanes/${id}/sync-base`,
    { method: "POST", body: JSON.stringify({ mode: "check", branch }) }
  ),

A new top-level settings object addition (the existing api.settings.reinstallHooks/cleanup group):

skillsInstall: () => request<{ installed: true; path: string }>("/skills/install", { method: "POST", body: "{}" }),
gc: (body: { dryRun?: boolean } = {}) =>
  request<{ reaped: number[]; capped: { path: string; sizeBefore: number }[] }>("/lanes/gc", {
    method: "POST",
    body: JSON.stringify(body),
  }),

LaneCard.tsx

Four additions, all gated on runtime?.available (same condition the up/down button already uses — every one of these four needs a .ccam/profile):

  • Install agents / Sync MCP buttons: same visual pattern as the up/down toggle (rounded px-2 py-1 text-fg-secondary transition-colors hover:bg-surface-2 disabled:opacity-50), a useState<"agents" | "mcp" | "sync-check" | null> busy flag (one shared state covering all three new button actions plus check, mirroring runtimeBusy's shape), result shown as a transient inline line below the button (reuse the laneActionError-style banner pattern, but for success text too — a new actionResult: {label: string; message: string} | null state scoped to LaneCard).
  • Integration badges: a small row of three pill-shaped badges (tracker / dev_qc / ci_wait), fetched once when the card mounts and runtime.available is true (parallel Promise.all of the three api.lanes.integration calls, same effect-on-mount shape the existing git/runtime fetches already use), green/gray styling for on/off, no click handler — read-only.
  • Check dev sync button: on click, calls syncBaseCheck; renders the result inline — DEV_DELTA: N files / DEV_OVERLAP: none or the file list, OR on code: 5 the collision list with each suggested rename. No merge action anywhere in this UI path.

Settings.tsx

New "Lanes" section (after the existing Hooks section, matching its heading/TOC-entry style), two buttons following the exact runAction(key, asyncFn) / actionBanner([key]) pattern the Hooks/Cleanup buttons already use:

  • "Install ship-feature-lane skill" → api.settings.skillsInstall().
  • "Run housekeeping" with a dry-run checkbox (state alongside the existing abandonHours/purgeDays inputs) → api.settings.gc({dryRun}).

i18n

New keys in client/src/i18n/locales/{en,vi}/lanes.json (agents-install/mcp-sync/integration/sync-check labels + result messages) and client/src/i18n/locales/{en,vi}/settings.json (skills-install/housekeeping labels + result messages) — both locales updated together, matching every existing key's bilingual completeness.

Testing

  • Backend: unit tests for the two new routes' handler logic is thin (both just call already-tested lib functions) — a manual curl smoke check per this repo's established "no HTTP test harness" precedent is the verification, same as every other E/F route this session.
  • Frontend: LaneCard.test.tsx gets new test cases for the four additions (button renders when runtime.available, hidden when not; click calls the right api.lanes.* method; busy state disables the button during the call). screens.snapshot.test.tsx will need its snapshot regenerated (cd client && npx vitest run -u) after the Settings/Workspace screens change — per this repo's own testing policy, review the diff before accepting it, don't blindly update.

Verify

npm run test:server (route smoke-tested manually, but the full suite must stay green), npm run test:client including the regenerated snapshot, and a manual click-through in the dev UI: open Workspace, confirm the four LaneCard additions appear only for a profile-having lane and each button's result renders; open Settings, confirm both new buttons run and show a result banner.