Issue #62: a tmux client whose LC_ALL/LC_CTYPE/LANG names no UTF-8 prints `_`
for every non-ASCII character, and a macOS LaunchAgent starts the dashboard
with no locale at all — Vietnamese text and Claude Code's logo came out as
underscores in every browser terminal.
- shell-path.js: ensureUtf8Locale() defaults LC_CTYPE (en_US.UTF-8 on macOS,
C.UTF-8 elsewhere) at start, before any tmux call; an explicit LC_ALL and
Windows are left alone
- RunSetup.tsx: the `/` list now offers Claude Code's bundled skills
(/code-review, /security-review, /simplify, /loop…) and newer built-ins, and
every enabled plugin's commands and skills as /<plugin>:<name>
- Claude login decided by the TOKEN's owner (GET /api/oauth/profile, cached
per token), not by .claude.json: a running session can leave the
credentials store holding one account's token under another account's
name. The Profiles modal, its Check quota and the status-bar quota now show
the account requests are really billed to and warn on the mismatch; a
switch saves the outgoing token into its OWNER's profile (credentials only)
instead of overwriting the profile .claude.json names; saving a profile
while they disagree is refused (EMISMATCH); a saved profile holding another
account's token reports EWRONGACCOUNT instead of that account's usage
- Running Claude Code sessions follow a switch on their own (they re-read the
credentials store when it changes), so the "close your sessions first"
warning is gone
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
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.
AI Proxy (was AI Router):
- server/lib/ai-proxies.js, proxy-apply.js, routes/ai-proxies.js mounted at
/api/ai-proxies and the old /api/ai-routers; `router` still accepted on the
start routes; profiles move to <CLAUDE_HOME>/agenthub-proxies/
- dashboard_runs.router renamed in place to proxy: an older build on the same
DB fails on that column, so dev and prod sharing one DB update together
- a proxy's baseUrl stored as its root (a typed /v1 made /v1/v1/messages);
Test probes Claude /v1/messages and Codex /v1/responses
- Settings → AI Proxy (/settings/ai-router redirects), ProxyPicker
Updates:
- every tab reloads onto the new UI build when its WebSocket reconnects to a
server of another version (lib/newBuildReload.ts, once per version)
- Update now stops waiting on a plugin chain that failed or pulled nothing
(the old server never goes down), and reloads as soon as a new version answers
Also:
- a pane whose command exits at once says so (EPANEEXITED) instead of
"Command failed: tmux set-option … mouse on"
- the pane drag preview draws where the pane actually lands
- Sessions page tabs: Sessions · Events · Agents. QueryExplorer.tsx became
AgentsTable.tsx (agents only — sessions and events already had tabs);
/query redirects to /sessions?view=agents and leaves the sidebar.
GET /api/query is unchanged and still answers all three entities.
- Windows picker opens in front: -EncodedCommand script spends the hidden
first ShowWindow that windowsHide gives the process, then brings the TopMost
owner forward via AttachThreadInput (v2.5.3's owner was itself hidden).
- Session page Resume/Fork/Open pane: owningLane and sessionWorktreeName
match Windows folders (either separator), with a test.
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.5.1's update died at "bootstrap" on every platform: boot.npmCommand is
not a function. Run as the entry, plugin-bootstrap.js calls bootstrap() →
plugin-open.js, which requires plugin-bootstrap back and got the module.exports
of that moment — {} — because it was assigned at the bottom, below the entry
block. plugin-open.js already exported first for the same reason.
- module.exports moved above the entry block in plugin-bootstrap.js
- A test checks both entry scripts assign module.exports before their entry
block (it fails on v2.5.1's file); requiring the module, as the other tests
do, never takes the entry path
A Windows self-update failed at "bootstrap": 'npm.cmd' is not recognized.
The update chain runs with the dashboard server's environment, whose PATH
need not reach npm.cmd, and the bootstrap looked npm up through cmd.exe
(shell: true, which also printed DEP0190).
- npmCommand() in scripts/plugin-bootstrap.js runs the npm bundled with the
running node (npm-cli.js beside process.execPath, Windows and Unix
layouts) with no shell, and puts that node's directory first on PATH (kept
under the env's own Path/PATH key) so npm scripts find the same node; npm
on PATH only when that node ships none
- Used by the runtime install, the UI build (plugin-open.js) and the
doctor's npm row (plugin-doctor.js)
- Settings → About → "Start AgentHub automatically" (AutostartSettings.tsx,
GET/PUT /api/settings/autostart, server/lib/autostart.js). The switch is
the systemd --user unit's / LaunchAgent's enabled state: on writes a unit
for the running server when none exists and enables it, off disables it;
neither starts or stops the server. A foreign unit is never touched
(409 EFOREIGN)
- The plugin bootstrap no longer installs a unit on every install: it
installs or starts one only while the user has it enabled, and a rewrite
keeps the enabled state (writeUserService({ enable })). An install that
already had the unit enabled stays on. A stale-but-enabled unit beside a
live detached server is repointed, no longer started onto the bound port
- unitIsEnabled / setUnitEnabled in systemd-service.js and launchd-service.js
Also:
- fix(workspace): status bar no longer covers the chat box below md (#61)
Analytics parity phases 2–7 (docs/plans/analytics-parity/), built on
138e706 and ported onto v2.3.1:
- Cost by AI tool (?tool= on cost/analytics/stats/workflows, by_tool,
Codex fast/priority and flex tiers, agenthub_cost_usd_total{tool,model})
- Date ranges (from/to) on cost/analytics/workflows; Analytics range select
with Today / This week (Monday) / This month (1st) / 7d / 30d / 90d / All /
Custom; workflow filters carried into workflow-stats.js (read worker)
- Budgets and cost alerts as alert rule types (alert_rules rebuilt without
its rule_type CHECK), GET /api/budgets/status, rule preview, spend pace,
status-bar budget chip; rules stay editable in place
- Query Explorer (/query, allowlisted sort, CSV/JSON export) and scheduled
reports (report_definitions/report_runs, Settings → Reports)
- Session Replay tab and immutable snapshots (session_snapshots, /snapshot/:token)
- Remote Codex sync, push ingest with a hashed token, privacy redaction and
ignored folders (Settings → Data), Cursor sessions (no cost)
- Read-only MCP server, statusline, Grafana/Prometheus files
Also: resizable sidebar, pet run/wave animations, the workspace home keeps a
half-typed prompt, and the pre-commit hook runs only the related tests for a
small commit.
- 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
- Changes panel: a working file opens in a CodeMirror editor with its diff
inline (DiffEditor.tsx, @codemirror/merge, lazy-loaded); Changes folds the
unchanged lines, Whole file shows them; side by side at 50/50 and full
width; a Revert per block; original from HEAD or the branch's merge base
(`mergeBase` on GET /git/changes?range=branch)
- Every diff: green = line added whole, red = removed whole, blue = edited
in place (--diff-mod)
- History drawn as a commit graph (#58: `parents` on GET /git/log,
--date-order; lib/commitGraph.ts)
- Per-workspace never-stage list, drawn orange in the Files tree (--kept);
closed Terminal rows past three deleted
- Status bar plan quota for Claude (per account) and Codex
(GET /api/pricing/quota, server/lib/quota.js); compact hover panel that
stays open, Refresh button with a spinner
- Settings → Appearance holds language, theme and pet; Ctrl+Shift+C copies;
compact sidebar navigation and short session times
- Changes panel: Files and Changes merged into one panel; the overview's
groups drawn as a folder tree (compacted chains, file counts, fold,
stage/unstage a whole folder)
- AI-suggested commit message for what is staged
(POST /api/lanes/:id/git/commit-message, claude -p --safe-mode, haiku)
- AI Router: optional "Behaves as" Claude model id, written as
modelPicker options into the router's --settings file (issue #56)
- Sidebar: session rows show last activity, live rows their start time;
a closed Terminal row can be removed (DELETE /api/run/history/:id)
- Every pane's CLI starts only once a client is attached; claude panes
no longer forced inline; tmux scrollbar one column wide
- Update notifier: "Check for updates" reopens a closed notice
- /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.