A terminal mounted on a hidden tab sits in a 0x0 box; its first fit shrank
xterm to 12x6 and the attach carried that size to tmux, so every background
pane was squeezed (Claude's TUI redrawn at 12 columns). It now neither fits
nor reports a size until shown, attaches with no size, and the server spawns
such an attach at the window's own size (tmux.windowSize). A shown tab
reports its size even when the fit did not change xterm's.
Settings > Agents shows the host's login profiles to staff only: a developer
got two 403s from reads that were never theirs.
Dependencies
- Security: body-parser and multer DoS advisories fixed (npm audit: 0).
- Server: Express 5 (query parser kept "extended", req.body defaults to {},
SPA catch-all /{*splat}, send-file helper restores Express 4's dot-folder
rule for sendFile/download, alerts ack route without inline regex),
better-sqlite3 13, js-yaml 5 (server/lib/yaml.js keeps js-yaml 4's default
schema), uuid dropped for crypto.randomUUID, node-pty beta.18, multer 2.5.
- Client: React 19, xterm 6 (WebGL renderer replaces the removed canvas one,
DOM fallback on context loss; Alt+arrow word moves re-mapped), lucide-react 1,
TypeScript 7, i18next/react-i18next/codemirror minors.
Workspace rename step 4
- `agenthub lanes` and `--lane` exit 1 with "renamed: use …"; client api.ts,
the CLI and the MCP server call /api/workspaces; /api/lanes stays mounted.
Server mode (S3): members manage their own Claude Code config
- server/lib/cc-target.js resolves whose config a /cc-config request touches
and gates the router by allowlist; /profiles* and /plugins/update-all stay
admin-only. Member writes go through write-as, CLI calls through sudo as the
member (never -y), reads through openSafe; cc_config_changed reaches only
its owner. Not verified live on an S3 install yet.
Other
- Chat: an HTML file is an artifact only when written whole and not a project
page (index.html, or under src/, public/, templates/, …).
- OpenAPI covers every /api route; openapi-coverage.test.js guards it.
- read-pool test keeps its event loop alive (its worker is unref'd).
- Docs: plan statuses, checklists, CLI.md, LANES.md, SERVER-MODE.md, API.md.
- update-scheduler: default interval 1 h (DASHBOARD_UPDATE_CHECK_INTERVAL_MS and its 60 s floor kept);
a scheduled tick with no /ws client is skipped, and the next browser to connect gets one catch-up
check when the last one is stale (websocket.js onConnection). Manual check unchanged.
- db-retention.js: shared session purge (used by POST /api/settings/cleanup too), opt-in
AGENTHUB_RETENTION_DAYS applied by the hourly maintenance in 25-session transactions with yields
(max 5000/run), bounded incremental_vacuum when the freelist is large, and a manual Compact
(VACUUM in a worker, converting to auto_vacuum=INCREMENTAL).
- db.js: new databases are INCREMENTAL; an existing one up to 32 MB is converted at startup.
- GET/PUT /api/settings/retention, POST /api/settings/compact (confirm: true); Settings → Data →
Session cleanup gets the Automatic cleanup control and Compact now (en + vi).
- Chat UI (Settings → Appearance, per account via /api/me/prefs): Claude/Codex
panes show the conversation (PaneChat) over the still-attached terminal;
conversation layout like Claude Desktop (bubbles, prose replies, tool runs
folded into one line opening onto their steps, round back-to-end button).
- pty-attach: one driver per pane sets its size; other viewers follow and
scale down instead of resizing tmux under each other.
- Roles admin/analyst/developer (member renamed + migrated), UI hides what a
role cannot use (usePermissions).
- Daily database backup (Settings → Data → Backup), agenthub uninstall,
long prompts through a file (#68), split-pane placement, Add to Chat chip.
Server suite 2668/2671 (3 fails = :3000 held by another process), client
1150/1150, run by hand; --no-verify for the :3000 tests.
Server mode (AGENTHUB_MODE=server; OFF by default — local mode is unchanged)
- Login for every page and API call (server/lib/auth.js, routes/auth.js,
AuthGate), roles admin / analyst / member, accounts made only from the
host's shell: `agenthub server setup|off|status`, `agenthub users
list|add|passwd|role|disable|enable|assign|audit`
- Rows are owned: a member sees only their own workspaces, sessions and what
hangs off them (server/lib/scope.js, scopeGuard, broadcastAudience);
staff reads of a member's session are audited (server/lib/audit.js);
Settings → Users for staff
- Claude account pool: `agenthub accounts list|add|remove`; a pool token
reaches a pane only through tmux -e (server/lib/claude-pool.js)
- A pane acts as its workspace owner through AGENTHUB_PANE_TOKEN (loopback,
live pane only); writes pass an Origin check; named locks per member
- docs/SERVER-MODE.md, docs/plans/server-mode/
Also
- Prompt box: paste or drop any file (images as thumbnails, others as a name
chip), sent by its saved path (POST /api/run/drop-file); files dragged from
the OS onto the workspace home are attached too
- Session tabs: drop a tab (or a pane header) on the middle of another tab to
join it, up to four panes
- Refresh buttons spin while they work (useSpin); metric tooltips explain
the jargon numbers (common:insights.help.*)
- Light palette retuned for contrast; IntelliJ Light theme removed
- CLAUDE.md sync marker trimmed; its history moved to docs/CLAUDE-SYNC-LOG.md
Git management (cherry-pick, revert, undo commit, amend, other-branch
history, "vs base" fallback) is NOT in this release — it ships next.
Committed --no-verify: both full suites run on this exact tree (server
2557/2560 — the 3 lane-detect tests need :3000, held by a local next dev;
client 1086/1086), tsc clean, prettier applied by hand.
node-pty on Windows looks the file it starts up on PATH exactly as given and
never adds `.exe`, so `tmux attach-session` started as a bare `tmux` failed
with "File not found" on every attach: the browser's terminal socket opened,
closed, and reconnected every 1.5 s.
- pty-attach.js: attachBinary names tmux by its full file name on Windows
(ai-tools.which("tmux"), else tmux.exe)
- websocket.js: an attach that throws is logged as `[WS] attach <run> failed`,
the one place that says why the terminal keeps reconnecting
- /api/workspaces and `agenthub workspace(s)` / `--workspace` as aliases of
/api/lanes and `agenthub lanes` / `--lane`; old names print one stderr note.
REPL `help lanes|workspace|workspaces` resolves through the aliases.
- A workspace holds up to ten sessions (slots 0-9); every console tab is a
group of 1-4 split panes (PaneGroups, agenthub.workspace.paneLayout);
parseGroups keeps the stored active tab when an earlier one is dropped.
- Browse button beside path fields: POST /api/lanes/pick-path opens the OS
picker on the server's desktop — loopback only and no forwarding header
(EREMOTE), a display failure is ENODIALOG not a Cancel, the dialog is
killed when the request closes, a folder field falls back to the in-app
browser on any failure.
- Changes rail counts chip, image viewer (/git/raw), status bar session
counts and per-day cost (`date=` on /api/pricing/cost).
- /agenthub-open and /agenthub-doctor run from the INSTALLED build
(supersededBy), doctor offers fixes behind two confirmations; plugin-open
treats a locked bootstrap as non-fatal.
- New-terminal shell waits for the first attach (issue #55).
- repo-onboarding / debug-live-issue skills removed.
- OpenAPI: pick-path, lane/date on /pricing/cost, tool on /run/history,
/api/workspaces mount.
- Split view: every pane shows the selected lane, each in its own tmux
session (`slot` 0-3 on POST /api/lanes/:id/start; slot 0 stays the
lane's `run_id`, slots 1-3 are `agenthub-lane-<id>-<slot>`). A pane's
Active runs hides what the other panes show; stop/reset/remove kill
every slot. No per-pane lane picker any more.
- Whole-file editor: PUT /api/lanes/:id/git/file (lane-file-write.js)
with the read path's guards, existing text files only, and a baseSha
stale check (409 ESTALE). A file it cannot take says why.
- Per-hunk review bar under each hunk's last changed line: prev/next,
"n of N", Undo, Keep. Review diffs no longer nest their own scroll box
(unless windowed), so navigation moves the one real scroller.
- Files tree lists untracked-not-ignored files and colours entries by git
status (M/U/A/D/R), folders holding changes get a dot.
- Changes panel: Unstage all on the single commit/Stage-all row; sizes
Narrow (30%) / Medium / Full screen, no 70/30.
- Lane card: the needs-you banner is gone (the strip tile keeps it).
Terminal (issue #33):
- `convertEol` is gone from xterm. It rewrote every bare LF as CR+LF, so a
tmux pane that scrolled (tmux emits a bare LF and keeps drawing at the
column it was on) had the rest of that draw land at column 0, stamping a
line's first characters into the left margin of the rows below — the torn
left edge that only showed up once content scrolled.
- The attach carries the client's real size (`?cols=&rows=`, `sizeFromQuery`
in `server/websocket.js`, bounded because it reaches a TIOCSWINSZ ioctl).
`attach()` spawned `tmux attach-session` at a hardcoded 80x24 on every
attach, and tmux sizes the window to the newest client, so `claude` redrew
its whole UI at 80 columns until the first `resize` message landed.
- The terminal takes the keyboard on mount, so typing reaches tmux without
clicking the pane first.
- A pane that ends (double Ctrl+C, `exit`, a crash) reports itself through a
new `onExit` prop and `LaneConsolePane` runs `newRun()`, so the console
returns to the setup form instead of leaving a dead pane behind a
"New run" button.
Pane environment:
- `server/lib/desktop-env.js` forwards the graphical session's DISPLAY /
WAYLAND_DISPLAY / XAUTHORITY / XDG_RUNTIME_DIR (from
`systemctl --user show-environment`, this process's own values winning)
into the pane. The server normally runs as a systemd user service with none
of them, so `claude` there could not read the clipboard and answered "no
image found in clipboard" on every image paste.
Run setup:
- `auto` is a permission mode and is now the default everywhere — allowlists,
the `--permission-mode` fallback, the select, and the pane's initial state.
- The model picker sends the CLI's aliases (`fable`/`opus`/`sonnet`/`haiku`)
instead of pinned ids. `claude --model` resolves an alias to the latest
model itself, so a release like Fable 5.1 needs no edit here; pricing is
unaffected because it keys off the resolved id the transcript reports.
- **Create/update readme file** moved from the Add-lane modal into the lane's
Tools menu: creating a lane and writing its docs are separate decisions,
and the second is worth re-running long after the first.
- The Workspace's no-lane state is a real banner; the lane strip (add tile
included) renders only once a lane exists.
Refs #32, #33.
Tests: npm run test:server (1828 pass), client vitest (558 pass).
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).
closeWebSocket() nulls both WebSocketServer references but cannot unregister
the `server.on("upgrade")` listeners that initWebSocket/initPtyWebSocket
installed on the shared http.Server. An upgrade landing in that window threw
TypeError: Cannot read properties of null (reading 'handleUpgrade')
from the listener — unhandled, so it killed the process mid-SIGTERM instead of
letting it exit gracefully, and no server came back up. Observed in
runtime/server.log right after a restart, with the dashboard then showing
"Mất kết nối" and nothing else.
The previous commit's TerminalView reconnect makes this near-certain rather
than rare: every open run console re-attaches to /ws-pty every 1.5s, so a
shutdown almost always has an upgrade in flight. Both listeners now destroy
the socket when their server is gone; clients retry, which is the correct
answer during a shutdown.
Regression test uses a real http.Server and raw upgrade requests — without the
guard the process dies and the test run hangs rather than reporting a failure.
xterm.js's onData hands the browser a plain string, and WebSocket.send(string)
always emits a TEXT frame — pty-attach.js only forwarded BINARY frames to
pty.write(), so every keystroke was silently dropped as an unparseable JSON
control message. Now any non-control text frame reaches the pty.
Deeper root cause of the terminal never accepting input at all: the /ws
WebSocketServer used the {server, path} shorthand, whose own internal
upgrade listener calls handleUpgrade() for every upgrade on the shared
http.Server and aborts with 400 on a path mismatch — killing /ws-pty/*
upgrades before the PTY server's own listener ever ran. Switched /ws to
noServer + a manual path-checked dispatch, matching /ws-pty's pattern.
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.