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).
A codex lane's declared stage has always depended entirely on the session
reading AGENTS.md and calling `ccam stage` on its own — no hook, no wrapper,
no enforcement. Two additions make that more reliable without touching the
declared-wins / inferred-never-done invariants:
- LaneConsolePane pre-fills a codex pane's opening prompt with a plain-text
stage-declaration checklist built from the lane's own pipeline_nodes —
the same up-front priming a Claude lane gets from its pre-filled
/ccam:ship-feature-lane skill command, just as prose since Codex has no
slash commands.
- server/lib/codex-nudge.js sweeps every 60s and types a one-line reminder
into a lane's live tmux pane (tmux.sendText, a new literal-text primitive
alongside sendCommand) when its fresh detected_stage has sat undeclared
for over two minutes. It never writes stage/detected_stage itself — any
resulting declaration is still the codex session's own real ccam stage
call, just server-prompted instead of relying on memory alone.
server 1354/1354, client 449/449, tsc clean.
xterm left fontFamily unset, so it used the browser/OS default
(courier-new/monospace). That face is missing Vietnamese precomposed
diacritics on many systems, so the browser silently substitutes those
glyphs from another face with a different advance width — corrupting
the cell even under the canvas renderer, which only stops a glyph from
shoving its neighbours, not from overflowing its own cell. Pin the
terminal to the app's own bundled JetBrains Mono and import its
vietnamese subset (previously only latin was loaded).
Scrolling a console left stray characters glued to the left edge of blank
lines. Copying an affected line yields clean text, so the buffer was always
correct — only the painting drifted, which no CSS can correct.
xterm's default DOM renderer aligns each row with a single `letter-spacing`
value. That holds only while every glyph comes from one monospace font, and
TerminalView passed no fontFamily, so xterm used its default
`courier-new, courier, monospace`: Vietnamese diacritics and CJK fall
through to a fallback face whose advance width differs, glyphs shove their
neighbours along, and the row's tail spills onto the row below.
The canvas renderer draws every cell at a fixed position, so a wide glyph
can no longer displace its neighbours. Loaded best-effort — a browser that
cannot hand back a 2D context keeps the DOM renderer rather than losing the
terminal entirely.
Ruled out first, each by replaying the real byte stream into xterm.js and
diffing against `tmux capture-pane`: wrapped Vietnamese at the margin, mixed
CJK, wheel scrolling through copy-mode, and a live 131->95->131 resize all
render byte-identically at matched widths.
client 448/448, tsc clean.
Scrolling a lane console repainted history with stray characters glued to
the left edge of blank lines. Reproduced against a real tmux attach: a pane
WIDER than xterm makes every over-long row autowrap its tail onto the next
row's left margin, and tmux — believing that row unchanged — never repaints
over it. One column of difference leaves one stray character per wrapped
line, two columns leave two; matched widths render clean.
TerminalView only reported its size on WS open and on the container's
ResizeObserver, so nothing corrected:
- cols moving with no container resize (webfont load, page zoom, scrollbar)
- tmux's `window-size latest` letting a second attach client (another tab,
`ccam lanes shell`) resize the window out from under this one
Size is now reported from xterm's own onResize, re-asserted when the
terminal regains focus, and never reported as zero — the console collapses
with CSS, and fitting a 0x0 container would resize the real pane to nothing.
client 447/447, tsc clean.
A lane on the 16-node `ship-feature` template showed an untouched pipeline
map and never declared a stage. Four independent causes, fixed at their
shared choke points:
1. Codex emits no hooks, so `touchLaneFromHook` — the only caller of
`stage-detect.detect()`, and correctly pinned to `claude` — could never
see a `codex` lane. New `server/lib/codex-ingest.js` tails Codex's own
rollout journals (`$CODEX_HOME/sessions/**/rollout-*.jsonl`), maps its
tool calls onto the hook-shaped `{tool_name, tool_input}` the matcher
already understands, and feeds the existing detect path plus an
idle/running status mirror under the same guard hooks use. Polled every
`DASHBOARD_CODEX_POLL_MS` ms (default 15000, `0` disables).
2. `resolveLaneArg` matched on cwd alone, so with a claude and a codex lane
at one directory (the composite `idx_lanes_cwd_tool`) a `ccam stage`
call could drive the sibling lane's card. It now resolves by
`--lane` → `CCAM_LANE_ID` → `--tool`/harness marker → owning lane, and
errors naming both candidates rather than picking one. Every pane the
dashboard starts carries `CCAM_LANE_ID` (`tmux new-session -e`, or an
assignment prefix when typing into a live shell pane).
3. `recordDetection`'s declared-wins veto had no exit: a lane left at the
pipeline's last node rejected every detection for the rest of its life.
A declaration on the LAST node no longer vetoes — the pipeline is over
there, so a later detection is new work. Nothing else is relaxed, and
detection still never writes `stage` nor renders `done`.
4. The run console pre-filled the pipeline's Claude slash command into
codex panes, which Codex reads as prose — so the skill that would have
called `ccam stage` never ran. Claude lanes only now.
A codex lane's *declared* stage still depends on the session calling
`ccam stage`; `AGENTS.md` is the only lever CCAM has, so it now says so.
Verified: server 1345/1345, client 441/441, and end-to-end against a real
codex lane row (detected_stage/detected_signal set, declared stage
untouched, no node rendered done).
Model dropdown now shows curated Claude vs Codex models per lane's tool,
and the run panel title/subtitle/binary-missing text switch between
"Chạy Claude"/"Chạy Codex" accordingly.
Repo/Adopt mode (POST /api/lanes/ensure) treated lanes.cwd as globally
unique, so adopting a path already driven by one CLI silently ignored a
different tool choice. Uniqueness moves to the composite (cwd, tool):
one directory can now back a claude lane and a codex lane side by side.
- server/db.js: drop the column-level UNIQUE on cwd, migrate existing
DBs via table rebuild, add idx_lanes_cwd_tool.
- resolveLaneByCwd gains an optional tool filter; hook ingestion always
pins to "claude" since Codex has no hook mechanism here, so a hook
event never touches a codex lane sharing the path.
- POST /ensure resolves the existing owner by (cwd, tool) instead of
cwd alone, so a different tool creates a sibling lane.
- LaneConsolePane's cwd-only lane lookups now also match tool, so a
shared cwd doesn't attach a run to the wrong sibling lane.
Worktree mode is unaffected (already allocates a unique directory per
lane). docs/LANES.md and CLAUDE.md updated to describe the new
composite uniqueness.
A pipeline template may now declare the slash command that drives it
(`skill`), so the choice of pipeline is the choice of workflow rather
than just a picture on the dashboard:
- `ccam ship-full "<req>"` runs the 16-node `ship-feature` template's
skill (`/ccam:ship-feature-lane`) — through gates, QC and the senior
gate to a published PR, watched until a human merges it.
- `ccam ship-quick "<req>"` runs the 8-node `default` template's skill
(`/ccam:ship-feature`) — in-repo feature work that stops at the gate.
Each command puts the lane on its own template before spawning, so the
dashboard renders the right node count from the first stage instead of
after the skill self-corrects. `--print` shows the command, switches
nothing and runs nothing. The prompt reaches `claude` as one argv
element, never through a shell.
The same field feeds the Workspace run console, which pre-fills its
prompt with the lane's driving skill and never overwrites a prompt the
user has already edited.
Add detect rules for plan/review/gate/tests/intake nodes (Agent subagent
matches, TaskCreate, broader test-runner regex) so more of the pipeline
lights up without an explicit ccam stage call, and fix a review/gate
detect overlap where senior-gate-reviewer also matched the generic
reviewer pattern. Also fix TerminalView's TextDecoder dropping the
`stream: true` flag, which corrupted multi-byte UTF-8 chars split across
WebSocket frames.
The previous `attachCustomWheelEventHandler(() => false)` killed scrolling
outright. xterm's `bindMouse` consults that handler in BOTH wheel paths — the
one that emits a real SGR mouse report and the fallback that turns a notch on
an alt-screen buffer into ESC[A/ESC[B — so a blanket false also blocked tmux's
own wheel scrolling (tmux runs with `mouse on`, i.e. tracking is active).
Now the handler returns false only for the exact bad case: no wheel tracking
AND an alternate buffer. `x10` counts as untracked because that protocol
reports button presses only, never the wheel, so xterm takes the emulation
path for it too.
xterm emits ESC[Z for Shift+Tab but, unlike plain Tab, never marks the
keyboard event cancelled, so the browser ran its default action and moved
focus out of the terminal. The pane saw only the first press, which made
`shift+tab to cycle` (claude's permission mode) look dead on /run while
working fine in a real terminal.
A custom key handler now calls preventDefault() on that one combo and still
returns true, so xterm's own handling — including the ESC[Z it sends — is
untouched; only the focus-moving default is suppressed. Verified against a
scratch tmux session: writing ESC[Z into the attach pty cycles claude's
status line from `bypass permissions on` to `auto mode on`.
Trade-off: Shift+Tab no longer tab-reverses out of the terminal; click
elsewhere to move focus.
Three separate reasons the Workspace terminal looked unscrollable:
- xterm falls back, on an alt-screen buffer whose program has not enabled
mouse tracking, to translating each wheel notch into a cursor-key press.
Inside a `claude` pane that reads as arrow up/down, so scrolling walked the
prompt history. A custom wheel handler returning false suppresses only that
emulation branch; real mouse reports come from a separate listener xterm
registers when the program does request wheel events.
- TerminalView never reconnected, so a server restart left the pane frozen on
its last painted frame — still live-looking, but swallowing every keystroke
and mouse report. It now re-attaches 1.5s after an unexpected close and
stops only on the server's `exit` frame; tmux keeps the session, so the
re-attach repaints in full.
- tmux repaints the whole pane, so xterm's own scrollback is always empty and
its viewport scrollbar renders as a groove with no thumb that cannot move.
Hidden, since scrolling there goes through mouse reports, not the DOM.
The lane-switch effect only synced `handle` from whatever activeRuns
snapshot was already loaded, so navigating away and back to /run (a
fresh mount with activeRuns still null) left a running lane's console
stuck on the setup form until the user switched lanes and back.
Without a lane, the picker had no cwd filter and listed every session
across every repo. Fall back to the free-typed cwd field so resume
suggestions stay scoped to the working directory in view.
Picking a session in the setup form's resume picker only staged the
selection: the user still had to type a prompt and press Run before the
lane's tmux session was started with `--resume`. A resume carries its own
transcript, so there was nothing to type.
The picker now fires the start directly with the picked session (passed
explicitly, since the parent's state has not landed on that tick), sends the
session's own cwd — which is what the locked cwd field already displays —
and the Run button no longer requires a prompt while a resume is selected.
Fresh runs still require one.
The cwd was synced only when laneId changed, but a pane can render before
GET /api/lanes has answered — split view restores its pane lanes from
localStorage, and layout 1 paints before the list loads. With no lane
resolved the field fell back to the home suggestion, and since laneId never
changed afterwards it stayed there. RunSetup submits that string verbatim to
POST /api/lanes/:id/start, so the run was started in the wrong folder.
Track the cwd in its own effect keyed on the resolved lane path rather than
on laneId, so it re-syncs as soon as the lane is known. The home default now
applies only while no lane is selected at all.
LaneConsolePane kept handle/cwd/prompt/runHistory in local state that was
never reset when the laneId prop changed, so selecting another lane swapped
the header and detail panel while the terminal stayed attached to the
previous lane's tmux session.
Reset the pane on lane switch and re-attach immediately to the new lane's
live run from GET /api/run when it has one, falling back to that lane's
setup form when it does not. `lanes`/`activeRuns` are read through a ref so
the page's 5s poll cannot wipe a half-typed prompt.
The Workspace active-runs list only knew about runs this dashboard spawned,
so two `claude` sessions started by hand in terminal tabs showed up nowhere —
the list read "no active runs" while two agents were working.
Poll GET /api/sessions?status=active alongside the run list and merge those
sessions in as live rows, deduped against dashboard runs by session_id and
filtered to local sources with a cwd (a remote-source or cwd-less session
cannot be resumed on this machine).
External rows get no Attach action: the dashboard owns no tmux session for
them, so there is no PTY to bridge. They offer Resume, which reuses the
existing ensure-lane + start-with-resumeSessionId path to spawn a new
tmux-backed `claude --resume` in that folder — a second process on the same
transcript, not a view of the original terminal.
Fixes regression from Tasks 1-2:
1. Run.defaultCwd.test.tsx was failing because LaneConsolePane wasn't
rendering when no lane was selected (in layout-1 with zero lanes).
2. Added fallback rendering of LaneConsolePane in Workspace when
!currentLane, so RunSetup stays reachable on fresh install.
3. Fixed early-return condition in LaneConsolePane to only show
dropdown-only placeholder when showLaneSelector=true && laneId=null
(split-view mode), not in layout-1 mode.
4. Added useEffect to sync cwd state with defaultCwd when it changes
asynchronously from parent.
5. Updated LaneConsolePane tests to reflect correct behavior.
6. Regenerated snapshot to match layout-1 output.
The wsConnected prop was destructured but unused. This restores the
live/offline status pill next to the page title, matching the original
Header function design and consuming the prop properly.
Server streams PTY output as binary WS frames, but the browser's default
binaryType ("blob") handed onmessage a Blob that never matched the
`typeof === "string"` check — every keystroke response was silently
dropped and the terminal stayed blank despite the backend streaming
correctly (verified via a raw ws client against the live tmux session).
Fix two issues identified in code review:
1. Critical: onViewFromHistory was a silent no-op. Now navigates to the
SessionDetail page using the same route pattern as the external link in
RunHistory, allowing users to view a finished run's transcript.
2. Important: Removed dead slashCommands={[]} prop from RunSetup invocation.
Made slashCommands optional in RunSetupProps to maintain type safety while
reflecting that the discovery logic was removed.
All tests pass (396 client, 1152 server).
`npm run test:client` (Vitest/esbuild) doesn't type-check, so several
tasks' incomplete cleanup of the old RunConsole-era code in
Workspace.tsx (SlashCommand/BUILTIN_SLASH_COMMANDS references, old
RunStatus values, a stray `mode` field, a wrong `prompt` vs
`initialPrompt` key) went unnoticed through every review until `tsc
--noEmit` was run directly. Also fixes a stale `RunStatusPayload.exitCode`
read in Tabby's brain.ts and dangling RunStreamPayload/RunInputAckPayload
references left in types.ts.
- pty-run.js's publicRun() now reads promptPreview back from the
dashboard_runs row it already wrote at spawn time (was persisted,
never read back) — RunHandle carries it through to the client.
- Workspace.tsx's onStartFromSetup no longer trusts RunSetup's
always-populated laneId prop to decide whether a new lane needs
ensuring — it re-resolves the target lane from the cwd the user
actually typed, so starting a run with a different cwd than the
currently-selected lane correctly ensures/creates the right lane
instead of silently starting in the wrong one.
Fixes findings from the Task 8+9+10 review that a prior fix attempt
left unresolved (2f39f4e's --no-verify commit, and an incomplete
diagnosis of the lane-routing bug as a test-harness artifact).
Combines three tasks that couldn't land as separate commits: the
pre-commit hook's full test run crashes on any intermediate state
where Workspace.tsx still imports the files being deleted, so the
deletion (old RunConsole/useRunStream/run-spawner/stream-json-parser),
the RunSetup/RunHistory type adjustments, and this file's own
TerminalView wiring had to be staged together and committed as one
hook-passable unit.
- Delete RunConsole.tsx, useRunStream.ts, server/lib/run-spawner.js,
server/lib/stream-json-parser.js and their tests (Task 8).
- Adjust RunSetup.tsx/RunHistory.tsx to the tmux-backed RunHandle/
RunStartArgs/DashboardRunHistoryItem shapes, remove mode selection
UI (Task 9).
- Swap Workspace.tsx's chat-bubble run console for TerminalView
(xterm.js over /ws-pty/:runId), drop the stream-json envelope
plumbing, update Start/Resume to the new RunStartArgs payload.
Create onStartFromSetup handler to work with RunSetup's new callback
shape. Remove mode state and related plumbing. Remove send/followUp
state (no longer using old RunConsole chat interface).
- Add promptPlaceholderTerminal i18n key to support RunSetup's new
placeholder text (Task 10).
- Update Workspace.test.tsx to mock TerminalView component.
- Regenerate screens.snapshot.test.tsx snapshot (only Workspace run
panel changes: terminal container instead of chat bubbles).
Adds a working Dark/Light toggle (next to the language switcher, same row
as EN/VI) and re-themes the whole dashboard, not just the handful of
components that already used semantic tokens.
- Tailwind darkMode:"class" + CSS-variable color tokens (client/src/index.css,
tailwind.config.js): surface.0-5, border/border-light, accent/accent-hover,
fg.primary/secondary/muted, status.success/danger/warning. One class flip
on <html> re-themes everything — no per-element dark: variant pairs.
- useTheme() hook: localStorage-persisted, defaults to dark, no
prefers-color-scheme fallback (client/src/hooks/useTheme.ts).
- Mechanical, table-driven migration (scripts/migrate-color-tokens.mjs,
scripts/tokenize-status-colors.mjs, scripts/darken-status-colors.mjs) of
every raw neutral/gray/slate + emerald/red/amber Tailwind utility across
client/src onto the new tokens, so every badge/button/component pulls the
same shade per status/role instead of each picking its own.
- Palette values are the literal Radix Colors (radix-ui.com/colors) scale
constants — slate/blue/green/red/amber steps 1-12 — adopted after three
rounds of hand-picked values that kept overshooting (flat, then too dark,
then glaring); see docs/superpowers/specs/2026-07-31-color-redesign-
dark-light-mode-design.md for the full history and role mapping.
- PipelineMap: done/current/failed/passed-no-evidence/detected share one
visual language (border + text + translucent wash of the same status
color); `current` alone stays a solid accent fill, the one state that
gets to look bolder ("you are here").
- LaneCard: removed the stage/kind/auto-stage chips that duplicated the
Workspace lane-detail header already showing them.
Categorical/decorative hues (violet, indigo, cyan, teal, sky, rose, pink,
orange, yellow, and blue where it plays a role-coloring part e.g. message
bubbles) are deliberately out of scope — collapsing those onto shared
tokens would erase the distinction between different kinds of thing, not
a status.
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.