The title and the four counters said a second time what the lane strip
already shows lane by lane (liveness dot, ⚠ marker), so they only cost a row
of height on a screen whose point is the terminal. `GET /api/lanes` still
returns `counts` for other callers.
Add lane becomes the strip's FIRST tile — creating a lane is the same kind of
act as picking one — and the per-lane Tools menu (agents install / mcp sync /
sync-base check) moves onto the lane-detail header row beside the pipeline
picker and the Model select, next to the lane it acts on.
Four more spots that spent height saying what was already visible or
saying too much of it.
**LaneCard** drops the progress bar and percent entirely — the pipeline
map right below the card already draws the same number as named,
coloured nodes, and a bar beside it was a worse second telling of it.
**SplitLayoutToggle** (Workspace) swaps "1 pane" / "2 pane" / "4 pane"
text buttons for the conventional square / two-column / 2x2-grid icons
(`Square`, `Columns2`, `Grid2x2`) most apps use for this. The words move
to `aria-label` and `title` rather than disappearing.
**RunSetup** merges the fresh/resume source picker onto the same row as
the Working directory / Model / Permission mode / Thinking effort fields
instead of a second row above them — settings left, New/Resume session
right. The session picker for an active resume still gets its own row
below, since it only appears in that one state.
**LaneConsolePane**'s "Run Claude" header goes from a two-row title +
wrapped blurb to one row (smaller icon and title), and the blurb itself
is trimmed to one short sentence, hidden below `sm` where it would wrap
again anyway.
`docs/LANES.md` updated to match (split-view description, lane-card
field list, and `agenthub stage`'s progress-bar wording).
Verification: `tsc --noEmit` clean, `npx vitest run` 508/508 (2 stale
progress-bar assertions in LaneCard.test.tsx replaced with one asserting
the bar is gone), `npm run build` clean, screens.snapshot diff reviewed
before regenerating (icon buttons + new blurb + reordered DOM only), file
headers audit clean.
openapi.yaml embeds info.version and the Settings screen snapshot pins the UI build string, so both are regenerated in the same commit - repo-links.test.js and the snapshot test each fail otherwise.
The Codex tab strip wrapped onto a second row while Claude Code's stayed
one scrollable row, so two panels that are meant to be twins read as
different components. Both now render the same ConfigTabBar — the
scroller lifted out of CcConfig, with the fade edges and chevrons that
only appear when there is somewhere to scroll. The deferral note in
CodexConfig that predicted this is gone with it.
The bar also carries role="tablist"/"tab" and aria-selected now: Codex
had them, Claude Code did not, and one of the two had to win.
Cursor's placeholder tab goes away with the machinery that served only
it — the optional Panel, the "soon" badge, the coming-soon card, and
their four i18n keys.
Element.scrollBy and friends move to test-setup: jsdom has none of them,
and the tab bar now calls them from both pages.
Neither panel drove a decision: the detected-pattern ranking restated what
the orchestration DAG already shows, and the delegation flow's main→subagent
model pairing is visible in the per-session drill-in. Removes both components
and their i18n blocks, promotes the error-propagation map to full width, and
renumbers the remaining sections 4-7.
Adds a "Switch profile" button left of Backups on /settings?tool=claude-code.
A *profile* is one Anthropic account - one quota - saved under a name, so the
local `claude` CLI can be pointed at a different account without disturbing
anything else.
A switch swaps exactly two identity artifacts and nothing more: `claudeAiOauth`
inside `<CLAUDE_HOME>/.credentials.json`, and `oauthAccount` + `userID` inside
the `.claude.json` config file(s). Those files are key-merged in place, and only
where they already exist, so `projects`/`numStartups`/everything else survives.
Skills, subagents, commands, memory, plugins, marketplaces, MCP servers and
settings are never read or written here - that is what makes a switch keep the
whole workspace and change only the account. `mcpOAuth` stays put too: those are
the machine's MCP server tokens, not account identity.
Profiles are one JSON file each under `<CLAUDE_HOME>/agenthub-profiles/` (dir
0700, files 0600, atomic temp+rename) and hold live OAuth tokens, so no route
ever returns token material - `GET /profiles` answers identity metadata only.
The dashboard cannot log an account in; only `claude auth login` mints tokens.
Adding a profile is therefore always "log in from a terminal, then snapshot what
the CLI wrote". `activateProfile` re-snapshots the outgoing account first so
tokens Claude Code refreshed since its last save are not lost, and refuses
(`EUNSAVED`) to switch away from an account that was never saved - that would
discard the only copy of its credentials. A switch applies to new sessions only,
since a running session rewrites the config on exit.
Routes: GET/POST/DELETE /api/cc-config/profiles, POST /profiles/activate.
Verified: server 1420/1420 (14 new, incl. token-free responses and 0600/0700
permissions), client 459/459.
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 plugin card showed "7 skills, 5 agents" and nothing more — the counts
were the whole story, so finding out WHICH skills a plugin ships meant
opening its install directory by hand. The contribution chips are now
buttons: clicking one expands a list naming each artifact with its
description, one surface open at a time. Commands render with their
slash. Hooks stay a plain chip — a plugin's hooks are matcher/command
pairs declared inline in plugin.json, not named artifacts.
`contributes.items` carries the four surfaces server-side; the existing
counts keep their type and are now derived from those lists, so a chip
reading "3 commands" can never sit beside a list holding two.
That also fixes a real blind spot. Each surface was read only from the
conventional install subdirectory, but plugin.json may declare its own
paths — this repo's own `ccam` lists three files under
plugins/ccam/commands/ and keeps commands/ empty, so it reported zero
commands. Both sources are now read and de-duplicated. A declared path
resolving outside the plugin's install directory is dropped: a manifest
is third-party data and must not turn the dashboard into an arbitrary
file reader.
Also pins the Refresh button's width: both labels share one grid cell so
the button is always as wide as "Refreshing…" / "Đang làm mới…" and no
longer resizes the toolbar when the state flips. Measuring the longest
real string beats a min-width that would be wrong in the next language.
Verified: npm run test:server (1370 tests, +6 for the new discovery),
npm run test:client (457, +3 for the expand behaviour), tsc --noEmit,
prettier and check-headers clean. Docs synced: docs/API.md, openapi.yaml
(new CcConfigPluginItem schema), ARCHITECTURE.md, docs/PLUGINS.md.
The Workspace page rendered `ConsoleArea` from two sites, picked by
`currentLane`. Every flip of that value unmounted the whole console
subtree and mounted it again at the other site: the terminal died, the
run history went with it, and `LaneConsolePane`'s lane-switch effect
wiped a half-typed prompt. It flipped on every page load (lanes arrive,
`refreshLanes` selects `lanes[0]`), on a lane being removed, and on any
refetch that re-picked the selection.
`lane-detail` is now always rendered and holds the only `ConsoleArea`;
the header and info block are the conditional parts. Same rule the
RunConsole already followed one level down — collapse with CSS, never by
unmounting — now enforced above it too.
Second half: every poller fed `setState` a brand-new object graph each
tick, re-rendering the lane strip, the card and the console area several
times a minute on a completely idle dashboard. `keepIfSame` keeps the
previous reference when the payload is value-identical, applied to the
5 s run/session poll, the lane list and counts, the `lane_update` WS
branch, and LaneCard's git/runtime/locks pollers.
Verified: npm run test:client (44 files / 454 tests), tsc --noEmit
clean, check-headers.sh clean. The Run screen snapshot was regenerated —
the diff is the added `<section>` wrapper and one indent level, no
content change.
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.
The panel aggregated every lane at once, which answers "does this stage get
proved across the project" but never "does THIS lane prove it". A lane picker
now narrows the same aggregation through ?lane=<id> on
GET /api/lanes/pipeline-stats, counting that lane plus its own archived
features — dropping the features would hide most of a lane's history the moment
it starts its next one.
The response carries a `lanes` array that the filter never narrows: it
populates the picker, so it has to keep offering every other lane even while
the counts show one. For the same reason the picker renders above the empty
branch rather than inside the populated one — narrowing to a lane that has
recorded nothing would otherwise be a one-way trip, with no control left to
select anything else. A lane filter that is not a positive integer is rejected
with 400 EBADLANE rather than silently aggregating everything.
The d3-sankey tool-transition chart was the last of the deep harness-internals
visualizations issue #6 set out to remove: a wall of bands showing that Read
tends to precede Edit, with nothing a reader could act on. It goes, along with
its i18n keys in both locales and the now-orphaned d3-sankey dependency.
The workflows API response is untouched — `toolFlow` still ships in the
aggregate and still feeds the Dashboard's top-tools list. Remaining sections
renumber 4-9.
Restores the five analytics sections that turned out to be worth keeping
(patterns, model delegation, error propagation, session complexity, compaction
impact) and reworks what the page opens on.
The lane panel no longer mirrors the Workspace's live lane strip. It now
answers the question the strip cannot: how often each pipeline node actually
passes. GET /api/lanes/pipeline-stats aggregates every stage record of every
lane AND every archived feature — a lane's own `stages` only holds its current
feature, so without the archive the history resets each time a lane starts new
work. Evidence-backed passes are counted apart from bare claims, the same line
the pipeline map draws between `done` and `passed-no-evidence`.
Beside it, GET /api/workflows/skill-usage reports which skills and plugins the
sessions actually reach for. Only PreToolUse rows are counted: every call also
emits a PostToolUse, and counting both would double every number. A plugin's
total includes calls to the MCP tools it ships, so one used purely through MCP
does not read as unused.
Still dropped from issue #6: the agent collaboration network and the subagent
effectiveness scorecards.
The Workflows page rendered eleven charts of Claude Code harness internals
(collaboration network, subagent effectiveness, patterns, model delegation,
error propagation, concurrency, complexity, compaction). Too specialized to
read and nothing to act on, so they are gone along with their i18n keys.
What is left is what a reader can act on: headline stats, Workflow-tool runs,
the spawning DAG, the tool flow and the per-session drill-in — plus a new
first section for this dashboard's OWN pipelines. LanePipelinePanel renders a
horizontally scrolling lane strip and the selected lane's PipelineMap,
reusing the Workspace components so a stage looks identical on both pages.
The panel is read-only: the console-never-writes-a-stage boundary applies
here too, so every lane action stays on the Workspace page. The workflows API
response shape is untouched.
Closes#6
Dashboard now opens on the Analytics tab for first-time visitors (localStorage
still wins once a user picks a tab). Also unbreaks the client test suite in
this environment: Node 22+'s built-in --experimental-webstorage localStorage
stub was shadowing jsdom's working one, failing every test that touches
localStorage. Disabled via a Node-major-gated poolOptions.forks.execArgv so
Node 20/21 test runs are unaffected.
Closes issue #5 (Dashboard | Update menu & page):
- Analytics content (heatmap, sparkline, token/cost/workflow/productivity
charts) moves into a new "Analytics" tab on the Dashboard page, alongside
the existing Monitor/Health tabs. The standalone Analytics page, its
/analytics route, and its sidebar menu entry are removed.
- The Kanban Board page, its /kanban route, and its sidebar menu entry are
removed entirely (page content deleted, not just hidden).
- Updated ARCHITECTURE.md, README.md, client/README.md, docs/API.md, and
the affected client tests/snapshots to match.
Move the rare destructive lane actions behind a LaneToolsMenu next to
the lane header, and show a liveness dot + status label + stage age
inline in the header instead of only in the card grid.
Full rename across files, identifiers, CSS classes, and user-facing strings
(en/vi locales, ARCHITECTURE.md). Replaces the hand-drawn animated cat
avatar with the pig-superhero mascot artwork, split into parts to keep
per-eye cursor tracking and blink animation working; whole-mascot mood
transforms (breathe/bob/shake/tilt) and new zzz/bang/sparkle overlays
carry the rest of the mood expression since the traced art has no shared
palette to key off of.
The lane strip was a fixed-width vertical column beside the detail panel.
Flipped it to a horizontally scrolling row on top so the detail/console
area gets the full page width below it.
The detail panel (LaneCard/PipelineMap/proof gallery/console) could
grow tall enough to visually crowd out the lane-strip carousel above
it once a run was attached in split view. Move the lane list into its
own scrolling vertical column beside the detail panel instead of
stacking it above, so neither can cover the other; also collapse the
info block by default in 2/4-pane split view (toggle to expand) and
move the pane-count control into the detail header.
Added comprehensive tests for the split-terminal-view feature:
- Default 1-pane layout (no multi-pane UI)
- Switching to 2-pane and 4-pane layouts
- Persisting layout and lane selections to localStorage
- Fallback behavior when persisted lane IDs no longer exist
Fixed test expectation: when a persisted lane ID no longer exists and falls back
to unselected (null), both panes render as empty (pane-empty), not just one.
Updated Run snapshot to reflect the new layout toggle buttons.
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.
- 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).