57dc91585d
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.
141 lines
6.5 KiB
Markdown
141 lines
6.5 KiB
Markdown
# 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.
|