Files
Claude-Code-Monitor/docs/superpowers/specs/2026-07-29-workspace-ui-design.md
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

6.5 KiB

Workspace UI rebuild — design

Status: approved 2026-07-29. Sub-project D, built on top of A (merged Workspace page). Reference: the Shipyard "Feature Harness" screen the user supplied.

Problem

The merged Workspace page shipped with the right information and the wrong shape. Everything a lane knows — declared stage, inferred stage, progress, liveness, needs-you — is already on the card (client/src/components/lanes/LaneCard.tsx) and already correct on the wire. None of it is legible: the lane strip is a horizontal scroller of cramped cards, the pipeline sits above a console that dominates the viewport, and the auto: <stage> chip that proves detection works is 10px of amber text nobody sees.

Measured on the live install while writing this: lane 5 carried detected_stage: "tests" with a real signal, and the user's report was "the lane does not auto-detect". Detection was never broken. The display was.

Two facts also make lanes look emptier than they are:

  • branch and ci_status are columns nobody writes, so those rows are always blank even for a managed worktree sitting on a real branch.
  • Detection is forward-only with no expiry, so a lane parks at the highest stage it ever touched. Lane 5 reached tests and can never show implement again, even while the agent is editing code.

Goal

The reference screen's legibility, on CCAM's real data: a lane's state readable from across the room, the pipeline large enough to trace, and the console present but out of the way until wanted.

Decisions taken

  • Card grid, not a strip. Responsive 1 / 2 / 3 columns.
  • The console collapses. It keeps every capability from A; it starts collapsed and opens for the selected lane. Watching lanes is the default posture, driving one is the exception.
  • Only real data. No placeholder tiles for facts CCAM does not have (tickets, preview ports, per-lane credentials). Branch/commit/CI are added because they can be read for real — see below.
  • Detection expires. A detection older than a TTL stops holding the floor.

Layout

Top to bottom, one column:

header:    title · [N lanes][N running][N need you][N dead] · [+ Add lane]
detail:    selected lane · declared + inferred headline · large PipelineMap · legend
console:   collapsed by default; expands to RunSetup + RunConsole + RunHistory
grid:      lane cards, 1/2/3 columns

Selecting a card switches the detail panel and the console together, exactly as A wired it. The console is unchanged behind its new disclosure — no prop of RunConsole, RunSetup or RunHistory moves.

The card

Reference layout, CCAM's fields, nothing invented:

Row Content Source
header LANE <id> · liveness dot · status id, liveness, status
title title, falling back to cwd existing
progress declared stage chip · bar · % · time on stage stage, progress, stage_seconds
inferred dashed amber auto: <stage> with the signal as tooltip detected_stage, detected_signal
tags kind (adopted/managed), CI when known kind, ci_status
git branch · short head · last commit subject · dirty/untracked counts new, see below
alert needs-you banner needs_action
actions start · stop · clear · reset · remove existing lane actions

reset and remove keep their preflight + expect echo through DestructiveLaneModal. This redesign does not touch the destroy guard.

Git facts

A new read-only endpoint, GET /api/lanes/:id/git, returning {branch, head, subject, dirty, untracked} or {available: false} when the lane's cwd is not a git repo or is unreadable.

Deliberately not folded into GET /api/lanes: that payload is polled and broadcast, and shelling out to git once per lane on the hot path would put a subprocess burst behind every hook-driven lane_update. The card fetches its own facts when it mounts and on a slow interval, and renders without them until they arrive.

server/lib/worktree.js already has statusCounts(dir) returning {dirty, untracked, head} and a git() wrapper that scrubs the inherited GIT_* environment. Both are reused as-is; the endpoint adds only the branch name and the commit subject. No second git helper, no shell strings.

Detection expiry

recordDetection gains one rule: a detected_stage whose detected_at is older than DETECTION_TTL_MS (default 30 minutes) no longer blocks a new detection — the forward-only comparison is skipped and the fresh signal wins. Within the window nothing changes: forward-only and declared-wins hold exactly as they do today.

This keeps the anti-flapping property that motivated forward-only (a Read right after an Edit must not drag the lane backwards) while admitting the thing it got wrong: a work session ends, and the next one starts somewhere else in the pipeline.

Unchanged, and not negotiable: detection still never writes lanes.stage, and an inferred node still never renders done.

Signal legibility

detected_signal currently captures the whole flattened tool input, so the chip's tooltip reads cd /very/long/path && npm run test:server 2>&1 | grep …. The matcher already knows which regex fired; the signal becomes the matched span plus a little context rather than the entire command. Cosmetic, but it is the text the tooltip exists to show.

Risks

  • The console's disclosure is the only structural risk. Mounting it inside a collapsed container must not unmount useRunStream and lose a live stream. The subscription stays mounted; only the visual container collapses.
  • Git calls per card. Bounded by the number of lanes on screen and a slow refresh; failure is silent and the card renders without those rows.
  • The screens snapshot over /run will change. It is read, not regenerated blindly.

Testing

  • The card renders every field from a fixture lane, and renders without the git block when the endpoint reports available: false.
  • A detected node still never carries data-state="done" — the premise guard from sub-project B is re-asserted at the new layout.
  • Collapsing and expanding the console does not tear down the run subscription: a stream envelope delivered while collapsed is present when it re-expands.
  • GET /api/lanes/:id/git returns the facts for a real repo fixture and available: false for a plain directory, and never shells out through a shell.
  • A detection older than the TTL is accepted even when it is behind the current detected_stage; one inside the window is still refused.