Files
AgentHub/docs/superpowers/specs/2026-07-28-workspace-page-design.md
T
nntrivi2001 17d56d0e01 refactor!: rebrand CCAM to AgentHub
Clean break, no compatibility alias. Renames the product name, the CLI
binary (`ccam` -> `agenthub`), the env var prefix (`CCAM_` ->
`AGENTHUB_`), the Prometheus metric namespace (`ccam_*` ->
`agenthub_*`), the tmux session prefix (`ccam-lane-<id>` ->
`agenthub-lane-<id>`), the per-repo runtime profile directory
(`.ccam/profile` -> `.agenthub/profile`), the lanes state root
(`~/.claude/ccam-lanes` -> `~/.claude/agenthub-lanes`), the bundled
plugins (`plugins/ccam*` -> `plugins/agenthub*`) and their slash
commands (`/ccam-doctor` -> `/agenthub-doctor`, `/ccam:*` ->
`/agenthub:*`).

Because there is no alias, an existing install has to be updated by
hand: rename its `.ccam/profile` directory and any `CCAM_*` variables,
and reinstall the plugin under its new name.

Two other name families are deliberately left alone here, since they are
not just strings: the `Claude-Code-Monitor` repository/marketplace name,
and the `agent-dashboard` npm package plus its `~/.claude/agent-dashboard`
runtime directory, which holds the live database and needs a migration
rather than a rename.

Verified: server 1406/1406, client 459/459, `grep -i ccam` clean across
tracked files, `openapi.yaml` regenerated (95 paths, unchanged count).
2026-09-07 12:09:15 +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 agenthub 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.