# 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: ` 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 ` · 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: ` 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.