Files
Claude-Code-Monitor/docs/superpowers/specs/2026-07-28-workspace-page-design.md
T
nntrivi2001 57dc91585d feat: Claude Code Monitor — lanes, pipelines and a merged workspace
Internal SmartGift build of a Claude Code monitoring dashboard.

Lanes: a durable unit of parallel agent work, one per working directory,
tracked across session restarts. Managed lanes are git worktrees the
dashboard provisions and can reset or remove behind a three-check destroy
guard and a counted preflight; adopted lanes are directories you already
own and are never destroyable.

Pipelines: a lane moves through pipeline stages. A stage the agent declares
with evidence renders green; a stage inferred from the tool-event stream
renders dashed amber and never counts as done. Detection is forward-only
within a 30-minute window, and never writes the declared stage.

Workspace: one page at /run with a lane grid, the selected lane's pipeline,
and a full Claude console behind a disclosure.
2026-07-30 14:39:03 +07:00

5.6 KiB

Merged Workspace page — design

Status: approved 2026-07-28. Sub-project A of three (C = worktree lanes, shipped; B = stage detection, planned). Built last because it consumes both.

Problem

Lanes and Run are two pages that describe the same activity. Run spawns a claude process in a directory and streams its output; Lanes shows what a lane is doing and where it is in its pipeline. A user watching an agent work has to hold both in their head, and the lane card cannot even send a prompt — start opens a promptless conversation run and message has no input field.

Goal

One page. A lane strip across the top, the selected lane's pipeline beneath it, and that lane's Claude console below — with every capability the Run page has today.

Decisions already taken

  • The merged page lives at /run. /lanes redirects there. One sidebar entry.
  • Every run belongs to a lane. Choosing a working directory that no lane owns creates one (kind='adopted') rather than running loose. A lane is, after all, just a working directory the dashboard is watching.
  • Everything from Run survives: slash-command autocomplete in the prompt editor, model / permission-mode / effort selectors, the token meter with cost, run history, and attach-to-a-live-run.
  • Layout: lane strip (horizontal, scrollable, with the counters and Add) → pipeline map of the selected lane → console. Selecting a lane switches both the pipeline and the console.

Architecture

client/src/pages/Run.tsx is 3658 lines holding an envelope model, a merge/typewriter engine, a slash-autocomplete prompt editor, a token meter, cwd suggestions, run history and the page shell. It is extracted into pieces that the new page composes:

Unit Responsibility
client/src/hooks/useRunStream.ts envelope state for one run id: subscribe run_stream / run_status / run_input_ack, merge envelopes, typewriter
client/src/components/run/RunConsole.tsx render the envelope stream, the prompt editor with slash autocomplete, the token meter, stop/clear
client/src/components/run/RunSetup.tsx mode / model / permission / effort / cwd / resume pickers, binary status, the limitations banner
client/src/components/run/RunHistory.tsx past runs, live runs, attach
client/src/pages/Workspace.tsx lane strip + PipelineMap + the three above

The extraction is mechanical and must not change behaviour. Each unit moves in its own commit with the existing Run tests passing untouched except for import paths. Only once Run.tsx is a thin composition does the new page get built. Extraction and composition never share a commit — that is the difference between a reviewable refactor and an unreviewable rewrite.

Server glue

Four small pieces, each independently useful:

  1. Runs start through the lane. The UI always calls POST /api/lanes/:id/start, which already exists, sits behind the same-origin guard, and records run_id on the lane. POST /api/run stays for the CLI and other callers; the UI simply stops using it. Lane start gains mode so a headless one-shot is still possible.
  2. POST /api/lanes/ensure{cwd, title?} returns the lane owning that path or creates an adopted one. Avoids the UI having to catch a 409 and re-read, and keeps the create-then-start pair from racing.
  3. dashboard_runs.lane_id — one additive column, set when a run is started through a lane, so history can be filtered per lane instead of guessed at by cwd.
  4. A finished run releases its lane. Today nothing clears lanes.run_id when a run ends on its own: the lane reads running forever and message keeps targeting a dead run. The run-spawner already knows the moment of exit (actualExitedAt, added on the worktree branch); on that event, clear the owning lane's run_id and set its status back to idle. This is a bug the merge exposes rather than causes.

What the console must not do

It never touches the lane's stage. Typing /code-review in the UI does not move the lane to review; only ccam stage declares, and only detection (sub-project B) infers. The console is a window onto a process, not a driver of the pipeline. Keeping that boundary is what stops the pipeline from becoming a lie.

Risks and how they are contained

  • The extraction is the whole risk. 3658 lines, one of them the typewriter engine, with a screens snapshot over the page. Containment: one unit per commit, tests untouched but for imports, snapshot diffs read rather than regenerated, and the composition deferred until the last extraction is green.
  • Two consoles for one lane. Only one run is live per lane (start 409s when one exists), so the console shows exactly one stream.
  • A lane created just to try a command leaves an adopted lane behind. Acceptable: adopted lanes are never destroyable, forgetting one is a click, and the alternative — runs that belong to nothing — is what this design set out to remove.

Testing

  • Each extraction: the existing Run tests pass with only import changes, and the screens snapshot for /run is unchanged until the page itself changes.
  • useRunStream: envelopes merge in order; a run_status terminal event stops the stream; the subscription is disposed on unmount.
  • POST /api/lanes/ensure: returns the existing lane for a path already owned, for a path nested inside one, and creates exactly one lane under concurrent calls.
  • Run-exit releases the lane: after a run ends by itself, the lane's run_id is null and its status is idle.
  • The console does not move the stage: after a full run through the console, the lane's stage is what it was.