- psmux prints `name: 1 windows (created …)` whatever `list-sessions -F`
asks for. Read as names, every live pane looked gone and the heal beat
closed its run (and its tab) within seconds while the session kept
running. tmux.js (and the Update all helper) now take the text before the
first `:` — a session name never holds one.
- `npm run setup` used `(cd client && npm install)`; cmd.exe's ( ) is no
subshell, so `npm run link-cli` ran inside client/ ("Missing script") and
Update now failed on Windows. Now `npm --prefix client install`.
- Update now: an npm step names itself in its error and shows the END of
npm's output (the error, not the warnings), says when it hit the time
limit, and gets 20 min instead of 5.
psmux (the WinGet `tmux` on Windows) has no `=name` exact-match targets: it
answered "no server running on session '=agenthub-lane-1'" for a live pane,
so every Windows start reported "the session's command exited as soon as it
started" while the session ran on, and the browser could not attach.
- server/lib/tmux.js: `session`/`pane` targets probe the first live session
once — `=name` failing where the bare name answers means bare names from
then on; real tmux keeps `=name` / `=name:` (it matches by prefix otherwise)
- pty-attach.js and cc-update-all-helper.js use the same targets
- Windows panes: a WinGet tmux runs native programs and there is no `sh`, so
every pane's `sh -c` wrapper died at once ("Command failed: tmux set-option
… mouse on"). paneArgv runs the CLI itself on win32, and a Terminal gets
tmux's own default shell (newSession drops the `--` for an empty argv).
- Plugin UI build: client deps install with --ignore-scripts and tsc/vite run
by the running node instead of `npm run build`. On Windows esbuild's
postinstall went through cmd.exe with no `node` on the server's PATH, both
installs failed, and Update now kept the previous UI. A test keeps the two
build steps equal to client/package.json's build script.
- Update all: Atomics.wait instead of a `sleep` binary Windows lacks.
A workspace could not be created on Windows: "cwd must be an absolute path"
for D:\…, because createLane/canonicalCwd tested for a leading "/".
- lanes.js: path.isAbsolute/path.resolve; canonicalCwd stops at any root (a
missing drive looped forever); resolveLaneByCwd and /ensure accept "\" too
- native-dialog.js: the Windows picker gets an invisible TopMost owner form,
shown first — an ownerless dialog from the background server opened behind
the browser, or hidden
- windows-hide.js: every child process on Windows starts with windowsHide,
required first by the server and the detached scripts (a console window
flashed per git/npm call)
- Confirm/update/clear-data modals and the quota panel scroll instead of
overflowing a short window
- v2.3.0's tree reported 2.2.1, so every install saw an update forever;
package.json, the lockfile, openapi.yaml, the CLAUDE.md marker and the
snapshot now read 2.3.1
- A failed plugin-update step records the tail of the command's stdout and
stderr plus its exit code or signal (failureText); plugin-bootstrap.js
prints its refusal on stdout, which the verdict used to drop. Update all
uses the same text
- updates.test.js never applies an update: it skips when the checkout is
behind a release (apply ran npm run setup and pruned dev dependencies)
- Files tree lists gitignored files and folders, named in orange
(addIgnored in git-diff.js); change-tree rows indent in whole steps
Right-click menus: one portal shell (components/ContextMenu.tsx) for every
item with actions - sidebar/Recents/archived session rows (SessionMenu),
console tabs, pane headers, the Changes panel's files/tree/commits, the
workspace home, sessions outside a workspace (SessionRowMenu: Sessions
table, Dashboard, Activity, Workflow runs), prompt images, every Settings
list and every Claude Code/Codex config list (FileItemMenu). Buttons stay;
a delete keeps its button's confirm; the shell stops portal clicks from
reaching the card that opened it.
Status bar: Workspace $ and Session $ on the Workspace page, from
?lane= on GET /api/pricing/cost and the focused/replayed session's cost.
Database, no data dropped: synchronous=NORMAL, journal_size_limit 64 MB,
hourly PASSIVE checkpoint + PRAGMA optimize, idx_events_session_created
(replacing the redundant idx_events_session), watchdog reads
sessions.transcript_path, transcript parsed before the hook's write
transaction, GET /api/sessions/:id?events=0 for the web page (reloads only
on its own session, debounced), streamed export, transactional clear-data
and chunked cleanup, subagent import reads tool_use_ids once, runtime logs
rotated past 5 MB.
- tmux targets are exact (`=name`, `=name:` for panes/options): a bare
`-t agenthub-lane-1` resolves by prefix to agenthub-lane-12 once lane 1
is gone, so killing a finished lane could kill another.
- getRun/killRun refuse ids that are not `agenthub-lane-<n>`, so
DELETE /api/run/main can no longer kill a user's own tmux session.
- router/apply reports restart_required only for a running pane; getRun
returns a `gone` handle, so it was always true.
- needs_action is cleared only by the session that raised it (new nullable
lanes.needs_action_session); a second session in the same cwd used to
clear it on its second hook.
An older Claude Code has no -y on `plugin install|update` and exits with
"error: unknown option '-y'", so the dashboard's Update now chain failed at
its plugin_update step (and so did Update all, the plugin buttons and the
superpowers auto-install). Those versions also predate the confirmation
prompt -y answers, so every wrapper now retries the same argv without it
when, and only when, the CLI rejects the flag. /agenthub-update, which the
failure message points users to, falls back the same way.
Closes#26, #25.
`runs` is the denominator every pass rate on the Workflows page is read
against, and any backwards move opened a new one. Stage detection bounces by
design — one feature reads `implement → tests → implement` as the session
edits, runs the suite and edits again — so a single feature was recorded as
five or six runs, and a lane in constant use reported a handful of passes
spread across dozens of phantom runs. A backwards move now opens a run only
when it is DECLARED; a backwards detection continues the run in flight.
Reaching the final node still closes the run for either, since work after
`done` is new work however it was observed. Replayed over this repo's own lane
the rule takes 26 recorded runs down to the 5 features that actually shipped
through it. The panel itself is unchanged: passes are still declarations only.
`/reload-plugins` and `/reload-skills` were typed back to back. Claude Code
opens its slash-command menu the moment `/` is typed and the Enter that follows
lands on that menu, so the second command was appended to the line still in the
composer and the pane submitted `/reload-plugins/reload-skills` — which Claude
Code runs as `/reload-plugins` with an argument, never reloading skills, and
leaving a stray prompt turn in the scrollback. Reproduced and fixed against a
live pane: SETTLE_MS (1s) between the two sends, which makes
`reloadOpenSessions` async rather than block the event loop, and the same pause
in the detached update-all worker.
The plugins tab could already update one plugin or one marketplace at a time. It gains an install-wide batch and, separately, the reload step that actually makes an update take effect.
Update all runs in a DETACHED worker rather than the request handler, for a structural reason: the batch ends with the plugin the dashboard ITSELF runs from, and that step's plugin-bootstrap.js --force SIGTERMs this very server. An in-process loop would die partway through with the remaining plugins unrun and nothing left to report. Order is marketplaces (one call, no name = all) -> every other installed plugin -> AgentHub last -> one reload. Each transition is journalled to cc-update-all.json in the runtime dir, outside the plugin cache the update replaces, so whichever server is alive serves progress from it; the POST's response is only an acknowledgement.
readUpdateAll DERIVES an abandoned status for a running journal older than an hour, because a detached worker killed by a reboot leaves nobody to write the terminal entry and the button would stay wedged forever. isRunning gates a concurrent batch: two claude plugin update chains over one config dir race.
Reload is its own capability (server/lib/cc-reload.js, POST /api/cc-config/reload). /reload-plugins and /reload-skills are scoped BY CLAUDE CODE to the session that runs them, so nothing can reload a session from outside - typing into a session already running is the only real mechanism, and that is what this does, over tmux send-keys into the dashboard's live lane panes. It spawns NO claude process on purpose: a one-shot claude -p "/reload-plugins" would reload only its own short-lived session, exit, and leave a junk session in the dashboard's history - a headless run gets a full sessions row and there is no filter anywhere to suppress it. Reusing live panes means no session is created at all. Panes at a shell prompt are skipped (the text would run as a shell command) and only claude lanes are addressed, since Codex has no such command.
Update-all reloads once at the end; updating a single plugin passes reload:true and reloads immediately, and never after a failed action.
Fixes a real pre-existing bug on the way: pluginUpdate omitted -y while runClaude closes stdin, and the CLI requires it whenever stdin is not a TTY - so the per-plugin Update button failed for any plugin whose marketplace declares an install command. Also completes CcConfigChangedPayload's action union, which claimed write|delete while nine other values were already being emitted.
Verified: server 1683/1683, client 486/486 (27 new tests), build, prettier, file-header audit, openapi.yaml regenerated (117 paths). Update all was NOT executed against this machine - it would update 7 real plugins and restart the running dashboard.