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.
91 KiB
CLAUDE.md sync log (archive)
The old _Last synced:_ marker of CLAUDE.md, moved here verbatim on 2026-10-07 because it had grown to ~93k characters and was loaded into every session. Newest entries first, joined by +. Not loaded automatically; read it only for history. New history goes in commit messages, not here.
v2.5.8 (2026-10-07) + SERVER MODE, phase S1 of the AgentHub-Server plan, unreleased (AGENTHUB_MODE=server, off by default — one codebase, no fork: server/lib/auth.js + server/routes/auth.js at /api/auth (me/login/logout), users/auth_sessions tables, authGuard on /api after tokenGuard, isWebSocketLoggedIn on both WebSockets, client/src/components/AuthGate.tsx wrapping <App/> in main.tsx with the sidebar foot's sign-out, agenthub server setup|off|status and agenthub users … writing the DB directly, isLocalRequest moved into server/lib/same-origin.js and sameOriginGuard also accepting an Origin that IS the request's own allowed Host; docs/SERVER-MODE.md) + the CLAUDE ACCOUNT POOL, unreleased (server/lib/claude-pool.js: claude setup-token tokens under <CLAUDE_HOME>/agenthub-accounts/ 0700/0600 added by agenthub accounts add; spawnRun gives a Claude pane on no proxy the pickAccount() result through tmux -e CLAUDE_CODE_OAUTH_TOKEN and records dashboard_runs.account; ranking = quota ≥ 90% last, then live panes, then $ in 5 h / 7 d; GET /api/accounts) + the browser terminal ATTACHING on Windows (attachBinary in server/lib/pty-attach.js names tmux by its full file name there — ai-tools.which("tmux"), else tmux.exe —, because node-pty on Windows looks a file up on PATH exactly as given and answered "File not found" for a bare tmux: the socket opened, closed and the terminal reconnected every 1.5 s; websocket.js now logs [WS] attach <run> failed: … for any attach that throws) + v2.5.7 (2026-10-06) + tmux TARGETS that also work on psmux, the WinGet tmux on Windows (session/pane in server/lib/tmux.js, now exported and used by pty-attach.js and cc-update-all-helper.js: the first live session is probed once — has-session -t =<name> failing where the bare name answers means no exact-match syntax, and every target is then the bare name; real tmux keeps =name/=name:. psmux answered "no server running on session '=agenthub-lane-1'" for a live pane, so every Windows start reported EPANEEXITED while its session ran on; __setExecImpl(fn, {exact}) pins the answer in tests) + v2.5.6 (2026-10-06) + the plugin's UI build needing no node on PATH (buildClient in scripts/plugin-open.js: client deps installed with --ignore-scripts — esbuild, the only one with an install script, finds its binary without it — and tsc -b / vite build run by process.execPath itself instead of npm run build, kept equal to client/package.json's build by a test; on Windows a lifecycle script or .bin/*.cmd shim goes through cmd.exe, the server's PATH had no node, and every Update now kept the previous UI — "UI build v (rebuild client to match)") + a Windows pane started WITHOUT sh (paneArgv in server/lib/pty-run.js: on win32 the CLI's argv itself and, for a Terminal, nothing after -c <cwd> so tmux runs its default shell — newSession in server/lib/tmux.js drops the -- then; a WinGet tmux runs native programs and there is no sh, so every Windows pane died at once with "Command failed: tmux set-option … mouse on"; no wait-for-attach there, so a Windows pane may first draw at 80×24) + cc-update-all-helper.js pausing with Atomics.wait instead of a sleep binary Windows lacks + v2.5.5 (2026-10-06) + AI Router RENAMED to AI Proxy everywhere (server/lib/ai-proxies.js, server/lib/proxy-apply.js, server/routes/ai-proxies.js mounted at /api/ai-proxies AND the old /api/ai-routers; proxy on the start routes with router still accepted; EBADPROXY, proxyCapable, AGENTHUB_PROXY_KEY, <data dir>/proxy-settings/; profiles in <CLAUDE_HOME>/agenthub-proxies/, the old agenthub-routers/ moved on first read and symlinked back; dashboard_runs.router renamed in place to proxy — an older build on the SAME DB fails on that column, so dev and prod on one DB update together; client/src/pages/AiProxyConfig.tsx at /settings/ai-proxy, /settings/ai-router redirecting via MOVED; components/run/ProxyPicker.tsx, localStorage agenthub.session.proxy falling back to agenthub.session.router) with a proxy's baseUrl stored as its ROOT (a /v1 dropped; Claude gets the root, the Codex layer root + /v1 — a typed /v1 had made /v1/v1/messages), Test probing Claude's /v1/messages AND Codex's /v1/responses ({claude, codex}), and the built-in Native carrying no model ids + every tab RELOADING onto the new UI build after an update (client/src/lib/newBuildReload.ts, mounted once in App.tsx: on a WebSocket RE-connect, GET /api/health's version ≠ __APP_VERSION__ → location.reload(), once per version via sessionStorage agenthub.reloadedFor) + Update now no longer spinning forever on a plugin chain that left the old server up (pollUntilBack in UpdateNotifier.tsx stops on this chain's own last_plugin_update verdict — ok:false or already_current, finished_at after the click — and reloads as soon as a different local_version answers) + a pane whose command exits at once reported as such (newSession in server/lib/tmux.js: EPANEEXITED naming the command, instead of "Command failed: tmux set-option -t =agenthub-lane-: mouse on") + v2.5.4 (2026-10-06) + the Query Explorer FOLDED into the Sessions page (/sessions tabs Sessions · Events · Agents in client/src/pages/History.tsx; pages/QueryExplorer.tsx became pages/AgentsTable.tsx, agents only — sessions and events already had their tabs; /query redirects to /sessions?view=agents, its sidebar item is gone; GET /api/query still answers all three entities) + the session page's Resume/Fork/Open pane matching a Windows folder (owningLane in SessionResumeButton.tsx and sessionWorktreeName in WorkspaceSessionList.tsx take \ as a separator too) + the Windows picker made the FOREGROUND window (dialogCommands in server/lib/native-dialog.js, now -EncodedCommand: a throwaway ShowWindow spends the hide that windowsHide gives a process's first one, then the TopMost owner is brought forward by joining the foreground thread's input — AttachThreadInput —, since v2.5.3's owner was itself hidden and the dialog still opened at the bottom) + v2.5.3 (2026-10-06) + a Windows workspace folder (D:\…) being accepted (createLane/canonicalCwd in server/lib/lanes.js test path.isAbsolute instead of a leading /, canonicalCwd stops at any root — a missing drive looped forever —, resolveLaneByCwd and /ensure take \ as a separator too) and a TopMost owner for the Windows native picker (dialogCommands, server/lib/native-dialog.js) + every child process on Windows started with windowsHide: true (server/lib/windows-hide.js, required first by the server and the detached scripts — the console-less dashboard flashed a console window per git/npm call) + v2.5.2 (2026-10-06) + scripts/plugin-bootstrap.js assigning module.exports BEFORE its entry block, like plugin-open.js (run as the entry, bootstrap() → plugin-open.js requires it back and got {} — v2.5.1's every update died on "boot.npmCommand is not a function"; an order test in plugin-bootstrap.test.js guards both files) + v2.5.1 (2026-10-06) + every plugin npm run (the runtime install, the UI build in plugin-open.js, the doctor's npm row) going through npmCommand in scripts/plugin-bootstrap.js — the running node's bundled npm-cli.js, no shell, never a PATH lookup unless that node ships no npm: a Windows self-update failed with "'npm.cmd' is not recognized" because the server's PATH did not reach it + v2.5.0 (2026-10-06) + START-AT-LOGIN made OPT-IN, off by default (Settings → About → Start AgentHub automatically, AutostartSettings.tsx over GET/PUT /api/settings/autostart, server/lib/autostart.js: the switch IS the unit's enabled state — unitIsEnabled/setUnitEnabled in both service modules —, on writes a unit for the running server when none exists and enables it, off disables it, neither starts or stops anything; plugin-bootstrap.js installs or starts the unit only while it is enabled and rewrites it with writeUserService({ enable }) keeping that state — it used to install one on every plugin install) + the session view's height subtracting the drawer's 2.75rem menu bar below md (issue #61, Workspace.tsx) + v2.4.0 (2026-10-06) + ANALYTICS PARITY with the upstream agent monitor (hoangsonww) 523170e (docs/plans/analytics-parity/, phases 2–7, ported onto v2.3.1 and released as v2.4.0; phase 1 had shipped already): opt-in secret redaction chained in front of trim_event_data plus redact_text(?) on every events summary insert; ?tool= on cost / analytics / stats / workflows (parseTool/parseScope in server/lib/source-filter.js, scoped-stats.js taking a {where, params} predicate over sessions) with by_tool and an AI-tool select + Cost by AI tool card on Analytics, /api/analytics total_cost now equal to /api/pricing/cost, Codex fast/priority tier → speed = fast priced with LiteLLM *_priority rates synced into the fast columns, flex → service_tier = flex (half price), agenthub_cost_usd_total{tool,model} (long-context >200k/>272k rates deliberately NOT modelled — see pricing-sync.js); from/to date ranges (parseRange) on cost / analytics / workflows with the Analytics ?range= select (Today / This week from Monday / This month from the 1st / 7d / 30d / 90d / All / Custom, client/src/lib/dateRange.ts), the /query Query Explorer (server/routes/query.js, allowlisted sort, CSV/JSON export ≤ 50k rows) and scheduled reports (server/lib/reports.js, Settings → Reports, files under <data dir>/reports/); spend BUDGETS and dollar alerts as alert rule types budget / cost_threshold (server/lib/budgets.js, GET /api/budgets/status, each threshold once per period, alert_rules rebuilt without its rule_type CHECK), POST /api/alerts/rules/preview (dry run over history), Spend pace + Budgets cards and a status-bar budget chip; session REPLAY tab and immutable SNAPSHOTS (session_snapshots, /api/snapshots, /snapshot/:token, Settings → Snapshots); remote Codex sync, push ingest POST /api/hooks/ingest-batch (server/lib/push-token.js), privacy redaction + ignored folders (server/lib/privacy.js, Settings → Data → Privacy and → Push ingest); CURSOR sessions (server/lib/cursor-usage.js: ~/.cursor/projects/<slug>/agent-transcripts/<id>/<id>.jsonl — CLI and editor alike — into tool = 'cursor' sessions every DASHBOARD_CURSOR_POLL_MS, AGENTHUB_CURSOR_DIR, the cursor_transcripts table; NO tokens, model or cost — the transcripts record none and state.vscdb (which holds auth tokens) is never read; no Cursor hooks, whose before* events are permission gates); the read-only MCP server (mcp/server.js, agenthub mcp), the statusline (agenthub statusline install) and monitoring/ Prometheus/Grafana files + the pre-commit hook running only the RELATED tests for a small commit (.husky/pre-commit: ≤ 10 staged code files and no dependency/config/hook/deleted file → vitest related for client/ and the server/__tests__ files naming each other staged module; docs alone run nothing; AGENTHUB_FULL_TESTS=1 or a bigger commit runs both full suites, the frontend one with NODE_ENV=test) + a failed plugin-update step saying WHY (failureText in scripts/plugin-update-helper.js, also used by cc-update-all-helper.js: the tail of the command's stdout and stderr plus its exit code or signal — plugin-bootstrap.js --force prints its refusal, bootstrap: locked/superseded/…, on STDOUT, and the verdict used to keep only stderr, so it read "Command failed: …"; the helper runs main() only as a script) and updates.test.js never APPLYING an update (it skips when the checkout is behind a release — the route really fast-forwards, runs npm run setup, which under NODE_ENV=production pruned the checkout's dev dependencies, and restarts) + the Files tree listing GITIGNORED files and folders too, drawn ORANGE (addIgnored in server/lib/git-diff.js: each level's directory entries git does not list, read with readdir — ls-files --ignored cannot list inside an ignored folder — marked ignored: true, .git and a symlink out of the workspace left out; text-kept in TreeNodes; an ignored folder stays shut under Expand all) and the Changes overview's tree indenting a file one chevron (1rem) inside its folder + a working file's Changes view being the EDITOR too, folded to its changes (collapse on DiffEditor = collapseUnchanged; Whole file unfolds it and the toggle keeps the draft; each block's Revert edits the buffer only — the git Stage/Undo bars and line comments stay on the read-only diffs and in Review), and diffs + editor opening SIDE BY SIDE at 50/50 and full width (wide from Workspace.tsx = changesPreset !== "compact"; the editor's split is MergeView, original read-only on the left) + a file opening on its CHANGES (the diff; an unchanged Files pick as the whole file) and a Changes | Whole file toggle taking it to an EDITOR with its diff inline (client/src/components/lanes/DiffEditor.tsx: CodeMirror 6 + @codemirror/merge unifiedMergeView, lazy-loaded, syntax from our own lib/highlight.ts per line; original = the file at HEAD or at mergeBase, new on GET /api/lanes/:id/git/changes?range=branch; the read view's click-to-edit and LaneFileView's onEditAt gone, Cancel reverts, a save stays in the editor; Whole file off = the read-only diff with its review bars) and every diff's colours becoming green = line added whole / red = removed whole / BLUE = edited in place (keeps a letter or digit — keepsText there, emphasis in LaneDiffView.tsx; --diff-mod in index.css, bg-diff-mod tinted in tailwind.config.js) + the status bar's right-end PLAN QUOTA chips (GET /api/pricing/quota, server/lib/quota.js: Claude's /api/oauth/usage with the CLI's live token, NEVER refreshed, cached 5 min, plus every other saved auth profile asked with its own stored token only while it is valid — never refreshed, past expiry its last figure with asOf; Codex's newest rollout rate_limits, no network; hover lists every account per tool with each window's bar, percent, reset time, the account's email and sign-in expiry, in a compact 11px panel that STAYS OPEN under the pointer — leaving the chips closes it after 200 ms, entering it cancels that; only the chips and the panel's Refresh button refresh, the button spinning and reading Refreshing… while the forced read runs, never disabled, whose blur would close the panel) + the Changes panel's History drawn as a COMMIT GRAPH (issue #58: parents on GET /api/lanes/:id/git/log, now --date-order; lanes laid out by client/src/lib/commitGraph.ts, one SVG per fixed-height row in LaneChangesPanel.tsx) + Settings → Appearance holding the LANGUAGE, the theme and the PET (AppearanceSettings.tsx renders the language row, the theme card #theme and PetSettings under #pet; the General and Pet pages are gone — /settings, /settings/general, an unknown section and the legacy ?tool=dashboard land on appearance, /settings/pet on appearance#pet, MOVED in SettingsHub.tsx; Settings.tsx keeps data/about/notifications) + Ctrl+Shift+C as COPY, not the browser's inspector (copyOnCtrlShiftC in client/src/lib/keys.ts, a capture-phase keydown installed once in App.tsx: preventDefault + execCommand("copy"), which xterm answers with its own selection; F12/Ctrl+Shift+I still open DevTools) + a per-workspace NEVER-STAGE list in the Changes panel (agenthub.changes.neverStage.<lane id> in localStorage: a row's eye-off button or right-click toggles it, Stage all and a folder's stage skip those paths, the row is struck through and the Files tree draws the file ORANGE — --kept in index.css, text-kept/bg-kept in tailwind.config.js, kept on TreeNodes; git is never told — no info/exclude, no .gitignore write, and a single file's own Stage still works) + closed Terminal rows past the newest three per workspace DELETED on the run-heal beat (pruneShellsStmt in reconcileOrphans, server/lib/dashboard-runs.js; CLOSED_SHELLS_KEPT mirrors the sidebar's CLOSED_TERMINALS) — rows the list could not show resurfaced after a remove + an untracked FOLDER (dir/ from git status) drawn as a dir/ row in the change tree (changeTree.ts) + an AI-suggested COMMIT MESSAGE for what is staged (the overview's sparkle button → POST /api/lanes/:id/git/commit-message, same-origin, server/lib/commit-message.js: the user's claude -p --safe-mode --no-session-persistence --tools "" --model haiku on the lane-scoped staged diff via stdin, secret files left out, 60 KB diff cap, 120 s timeout; ENOSTAGED/ENOTOOL 400, EAI 502 — not a git write, it only reads the index) + the sidebar's session rows showing their last activity and a live New session/Terminal row its start time (WorkspaceSessionList.tsx, like the closed Terminal rows) + the right panel's Files and Changes tabs MERGED into one panel (the activity-bar tab buttons, PANEL_TABS and agenthub.workspace.panelTab gone from Workspace.tsx, LaneChangesPanel losing its tab prop; its mode switcher reads Changes · Files · Review · History, Files dropped at full width where it is the sidebar) and the overview's groups drawn as a JetBrains-style FOLDER TREE (client/src/lib/changeTree.ts: single-child folder chains compacted, per-folder file count, folders and group headers fold, a folder's stage/unstage-all button) + an AI Router profile's optional Behaves as (behavesAs, a claude-… id the user picks in Settings → AI Router; normalizeBehavesAs in server/lib/ai-routers.js) written by modelPicker in server/lib/router-apply.js as modelPicker.options rows { model, behavesAs } into the router's --settings file, one per distinct non-claude-* model name — issue #56: Claude Code 2.1.290 warned that a 9Router combo name like agent "isn't described by this version's model catalog" and compacted at an assumed 200k; nothing is written without it, because Claude Code takes the first modelPicker among policy → flag → user and ours would hide the user's own + a Browse button beside every path field (client/src/components/PathPickButton.tsx) opening the OS's own picker ON THE SERVER'S DESKTOP (POST /api/lanes/pick-path, server/lib/native-dialog.js: zenity → kdialog / osascript / WinForms, execFile argv with only the resolved start folder from the request; loopback socket AND no X-Forwarded-For/Forwarded header, else EREMOTE; a display failure is ENODIALOG, never a Cancel; the dialog is killed when the request closes; a folder field falls back to FolderBrowseModal on any failure) + /api/workspaces and agenthub workspace/workspaces as ALIASES of /api/lanes and agenthub lanes (the same two routers mounted at both paths in server/index.js; COMMAND_ALIASES/canonicalArgv in bin/agenthub.js, applied by runCommand and handleServerDown), --workspace <id> beside --lane <id> in resolveLaneArg; help, usage text, UI hints and user docs now say agenthub workspace …/--workspace, and the old agenthub lanes/--lane still run but print one stderr note (warnOldNames) — removing them is step 4 of docs/plans/workspace-rename/README.md, NOT before the third release after the one that ships the aliases, and it renames routing only: never a table, column, response key, tmux name, env var, data path or storage key + console TABS as COMBOS: a workspace holds up to TEN sessions (MAX_PANE_SLOT 9, run ids agenthub-lane-<id>-<1-9>), and every tab of the session view is a group of one to four split panes (PaneGroups in client/src/lib/paneLayout.ts, stored per workspace in agenthub.workspace.paneLayout as {groups, active} — a pre-tab single layout reads as one tab; agenthub.workspace.tabOrder is gone): Ctrl/Cmd+click or dropping a one-pane tab on a pane merges it into the tab on screen, a pane's close takes it out into its own tab, a pane header dropped on the bar detaches there, and a tab's × stops every session in it + the uncommitted-counts chip ON THE CLOSED Changes rail, vertical, with the per-file list on hover (LaneGitRailCounts/LaneGitCounts in client/src/components/lanes/LaneGitHeader.tsx, its own facts read only while the rail shows) and the OPEN panel's header showing no counts at all, since the panel lists the files below + /agenthub-doctor OFFERING to fix what it reports (asks, lists the exact steps, runs them only after a second yes — plugin-doctor.js stays read-only) and /agenthub-open running that doctor flow when plugin-open.js exits non-zero, which it now also does when its bootstrap does not answer ok (it printed the URL with exit 0 after a refused bootstrap) + /agenthub-update running plugin-bootstrap.js --force and the doctor from the INSTALLED build (supersededBy() of the session's root, else that root) — from the old root it always answered superseded right after pulling an update — and the repo-onboarding/debug-live-issue plugin skills deleted (with the .codex/.agents copies of repo-onboarding) + a New terminal pane's shell started only after the pane's first resize (spawnShell in server/lib/pty-run.js runs sh -c that polls #{session_attached} until a client is attached, ≤ 2 s, then exec "$SHELL" -l — not the first SIGWINCH, which enableMouse's scrollbar fires before any attach — issue #55: a prompt drawn at tmux's initial 80 columns and redrawn by readline after the attach resize left the cursor on top of the prompt) + AI CLI detection on WINDOWS (which in server/lib/ai-tools.js tries each PATHEXT extension — the bare name only when it already has one, since npm's extensionless shim beside codex.cmd is a sh script — so GET /api/ai-tools had answered installed: false for every tool; spawnTarget there gives a .cmd/.bat shell: true and a quoted path for ai-tool-jobs.js and ai-tool-versions.js, catalog argv only) + a dashboard pane's tmux SCROLLBAR that is actually there (pane-scrollbars-style fg=brightwhite,bg=black,width=1 in enableMouse, server/lib/tmux.js — the default track drew in the theme's border colour, and width=2 left a never-painted column of stale cells that read as a second scrollbar, since tmux 3.7c paints only the first column; and a claude pane no longer forced inline — CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 duplicated its banner and transcript on every pane narrowing (tmux re-wraps the full-width rules into history Claude's redraw cannot clear), so the user's "tui" decides, and every pane's CLI starts only once a client is attached (WAIT_FOR_CLIENT in server/lib/pty-run.js)) + a CODEX session's conversation in GET /api/sessions/:id/transcript (codexMessage in server/routes/sessions.js: its root thread's rollout from codex_rollouts, mapped to the Claude message shape; developer messages and injected context left out — the route had answered every Codex session with no messages) + server/compat-sqlite.js gaining function, stmt.pluck() and readonly/fileMustExist — a plugin install has no better-sqlite3 build, so HEAD's db.function("trim_event_data") crashed every such dashboard at boot — with db.js exporting its chosen Database for antigravity-usage.js (which required better-sqlite3 directly and skipped every agy file in production) and pbFields turning node:sqlite's Uint8Array blobs into Buffers; compat-sqlite.test.js boots db.js with better-sqlite3 blocked + the terminal ATTACH running the live tmux server's own binary (attachBinary in server/lib/pty-attach.js → /proc/<tmux.serverPid()>/exe, else PATH's tmux) — tmux 3.7c installed to /usr/local/bin beside a running 3.4 server failed every attach with open terminal failed: not a terminal while every other tmux command still worked + EVENT-LOOP FIXES (GET /api/workflows froze the server for minutes on a large session: its "next tool event" correlated subquery went quadratic once ad6e801 dropped idx_events_session — now one LEAD() window pass, server/lib/workflow-stats.js, run in the read worker server/lib/read-pool.js with ?sections= (Health asks for seven), shared in-flight runs, AGENTHUB_READ_WORKER/AGENTHUB_READ_TIMEOUT_MS, 503 EREADTIMEOUT; idx_events_session(session_id, id) restored for the per-hook duplicate guard and idx_events_tool(tool_name) added; installHooks adding nothing and removing its hook-handler.js entries while settings.json enables agenthub@…, which every checkout server start had re-added — ~40% of events doubled — and getHookStatus answering source: "plugin") + a Codex/Antigravity session tied to its workspace without hooks (claimToolSessions in server/lib/dashboard-runs.js: tool + cwd + the run's live window; also fills an empty dashboard_runs.session_id) — it had vanished from the workspace once its tab closed + UI-AUDIT FIXES (shared primitives replacing per-page copies: client/src/lib/toast.ts + components/Toaster.tsx — toast.success|error|info, an error stays until dismissed —, lib/confirm.ts confirmDialog/confirmDiscard + components/ConfirmHost.tsx replacing every window.confirm, lib/clipboard.ts copyText (falls back to execCommand off a secure origin, toasts the outcome), lib/keys.ts isSubmitEnter ignoring an IME-confirming Enter, hooks/useArmed.ts two-click actions that DISARM after 3 s, hooks/useDialog.ts the one modal focus contract — focus in, Tab trapped, Escape, focus back, dialogs STACK so only the top one answers —, hooks/useUnsavedGuard.ts (beforeunload + confirmDiscard), lib/throttle.ts + hooks/useLiveRefresh.ts a leading+trailing throttled WebSocket refresh that skips hidden tabs and refetches on visibility and reconnect; Toaster and ConfirmHost mounted once in App.tsx; the accent-solid token (--accent-solid/-hover in every theme, tailwind.config.js) as the fill behind white text, --accent now a TEXT colour — dark Default #70b8ff/solid #0076d1, Orca Dark #6aa0f8/#3472d8, warm light #924224 = its own solid —; Workspace/SettingsHub/SessionDetail lazy routes under <Suspense> inside components/RouteErrorBoundary.tsx (a stale chunk after a self-update reloads once, vite:preloadError too) — keep heavy page code out of anything the sidebar imports: the start helpers live in components/lanes/startSession.ts, not WorkspaceHome.tsx, or react-markdown lands in the main bundle; the Dashboard's components/NeedsYouStrip.tsx; Sessions filters (awaiting=1, from/to on GET /api/sessions, kept in the URL) and CSV/JSON export (lib/exportData.ts); Settings → General split into General / Data / About (Settings.tsx section; General since folded into Appearance), Pricing in its own Cost nav group, a browser-preferences backup (lib/prefsBackup.ts, allowlisted keys only), and the settings search deep-linking a row's ?tab= and #anchor, which the hub scrolls to and highlights; the terminal (TerminalView.tsx): Ctrl+M toggles Tab-moves-focus, Cmd+F (macOS)/Ctrl+Shift+F a find bar (@xterm/addon-search), @xterm/addon-web-links, a WRITE-only @xterm/addon-clipboard (OSC 52 in, never a read of the user's clipboard), a Reconnecting… overlay, agenthub.terminal.screenReader and agenthub.terminal.selectHintSeen; Alt+1–4 / Alt+\ / Alt+W focus, split right and close a pane (session view only, never from a terminal or a field); a draggable Changes splitter storing a NUMERIC fraction in agenthub.workspace.changesSize beside the presets; panePaste pasting only into a VISIBLE pane; POST /api/lanes/:id/start slot: "auto" (the server picks the free tab under the lane lock — every start from the home, a replay and Resume/Fork sends it), 409 ESESSIONLIVE + run_id for a resume of a session a live pane holds, 400 EUNSUPPORTED for a catalog tool's resumeSessionId; GET /api/agents?session_ids=, tool_call_count/distinct_tool_count on session stats, label columns on GET /api/events rows, POST /api/settings/cleanup refusing a non-whole purge_days (INVALID_PURGE_DAYS) and answering dry_run; the client's EBADRESPONSE for a non-JSON 200) + the tab bar's Tools menu GONE (LaneToolsMenu deleted with its MCP sync / dev-sync check / Create-update readme items, the sidebar row menu's readme item, api.lanes.syncBaseCheck and the actions.* strings; the routes stay, reached by agenthub lanes mcp sync / agenthub lanes sync-base) + a pane's split right/down menus offering the AI tools beside New terminal (newTabTools/newTabBody exported from ConsoleTabs.tsx, shared with the + menu) + Settings → Pet (since folded into Appearance as #pet; components/settings/PetSettings.tsx: show, speech bubbles, size with preview, the pet grid with download/upload/import/delete; the Sagi block left General; the mascot menu's Pet settings… opens it; list + actions shared with PetMenu through components/Sagi/usePets.ts) + LONG-TERM DB SAFETY with no data dropped (server/db.js: synchronous = NORMAL, journal_size_limit 64 MB, hourly maintainDatabase — PASSIVE checkpoint + PRAGMA optimize, never VACUUM —, idx_events_session_created replacing idx_events_session; the watchdog reading sessions.transcript_path; the hook route warming transcriptCache BEFORE processEvent's write transaction; GET /api/sessions/:id?events=0 for the web page, which reloads only on its own session's messages, debounced; GET /api/settings/export streamed in keyset pages; clear data in one transaction, cleanup chunked; the subagent importer reading an agent's tool_use_ids once; runtime logs rotated past 5 MB by scripts/log-rotate.js. events.data TRIMMED at every insert through the SQL function trim_event_data(?) (server/lib/event-payload.js, registered in server/db.js — never insert an event without it: Edit/Write drop originalFile/oldString/newString/Write content, Read drops file.content/base64, strings in tool_input/tool_response over 8 KB keep head + tail, structuredPatch and every other field kept, _trimmed names the drops; the full text stays in the transcript and its snapshot) plus a one-time chunked sweep of stored rows (sweepEventPayloads, user_version 4) — the user's choice of 2026-10-05, replacing "keep every payload"; still nothing prunes rows automatically, and the session list now prices every token column like /api/pricing/cost/:id (tokensForSessions)) + the status bar's Workspace $ and Session $ on the Workspace page (StatusBar.tsx: ?lane= on GET /api/pricing/cost — the sessions the workspace owns, same rule as GET /api/sessions?lane= — and GET /api/pricing/cost/:id for the URL's session or the focused pane's session from LIVE_RUNS_EVENT; selectedFrom exported from WorkspaceTree.tsx) + RIGHT-CLICK MENUS on every item with actions (client/src/components/ContextMenu.tsx — one portal shell + useContextMenu/menuPoint/MenuItem, which WorkspaceRowMenu now uses; lanes/SessionMenu.tsx for session rows in the sidebar, Recents and the archived fold; console tabs, pane headers, the Changes panel's file rows/review headers/tree entries/commits, the home's title and context rows; then components/SessionRowMenu.tsx for sessions outside a workspace — Sessions table, Dashboard agent cards/events, Activity, Workflow runs —, every Settings list and every Claude Code/Codex config list — FileItemMenu in config/MdItemList.tsx; the buttons stay, a menu repeats them, a delete there takes the button's own confirm, text areas keep the browser's menu, and the shell stops its clicks bubbling through the portal into the card that opened it) + the DAILY COST split by the day tokens were SPENT, not the day a session started (token_usage_hourly — one row per UTC hour per bucket, from each Claude transcript message's timestamp via TranscriptCache.tokensByHour, written by the hook writer and back-filled at start by server/lib/hourly-usage.js; dailyTokenRows in server/routes/pricing.js books the rest of a session's total to its start date, so the days still add up — the status bar's Today read ≈ $0 on a session started days earlier) + CUSTOM PETS for the mascot (server/lib/pets.js + server/routes/pets.js behind /api/pets, <data dir>/pets/<id>/; the .codex-pet.zip format — pet.json + a 1536×1872 spritesheet.webp, 8×9 frames of 192×208, one row per state — played by mood in client/src/components/Sagi/PetSprite.tsx; the right-click PetMenu.tsx: hide, size, choose, upload an image, import a bundle; three featured pets DOWNLOADED from codex-pets.net on a pick, never bundled; worn pet and size in agenthub.pet/agenthub.pet.size) + a sidebar workspace row opening the workspace's HOME, live tabs or not (a live tab opens from its session row; Home at the tab bar's left end) + drag-to-reorder dropping ANYWHERE by the pointer's position, with an insertion line (insertionIndex/moveBefore in client/src/lib/laneOrderStorage.ts): the sidebar's whole Workspaces section takes a row's drop (a row only starts the drag), and the console tab bar reorders its tabs, kept per workspace in agenthub.workspace.tabOrder, a bare tab listed at once as New session from its run (WorkspaceSessionList) and a live Terminal tab as Terminal, with the workspace's last three CLOSED Terminal tabs under its sessions (GET /api/run/history?laneId=&tool=shell; a click opens a NEW shell — a shell cannot be resumed, and its output is not recorded; its hover X or right-click Remove from list deletes that tool = 'shell' row through DELETE /api/run/history/:id, refused for a live pane or any other tool), and the sidebar's live panes following the Workspace page's poll (LIVE_RUNS_EVENT in WorkspaceTree.tsx) + the session view's console SPLITTING like VS Code (client/src/components/run/PaneGrid.tsx over client/src/lib/paneLayout.ts: a binary split tree of slots, ≤ 4 panes, each an absolutely placed sibling keyed by slot; Ctrl/Cmd+click on a tab, a + item or a sidebar session row splits — router state split, never the URL; pane headers drag to move/swap, pick another live session, split right/down, close the pane only; dividers drag or take arrow keys; ?run= = the focused pane, pushed with focusOnly; layout per workspace in agenthub.workspace.paneLayout) and the session view going EDGE TO EDGE (no page padding, no cards, the tab bar and the right panel's activity bar one h-10 header strip, the terminal on the page colour) + the session view's console as TABS instead of a 1/2/4 split (client/src/components/run/ConsoleTabs.tsx: one tab per live tmux slot 0–3, the active one = ?run=, inactive ones mounted and hidden with CSS; + opens New terminal — shell: true on POST /api/lanes/:id/start, spawnShell in server/lib/pty-run.js, tool shell, no run_id/status change — or a bare AI tool, and links Settings → Agents; splitViewStorage.ts and &pane= gone — the sidebar's Ctrl/Cmd+click came back as a split, see above; LaneConsolePane's homeOnExit → onRunExit), the tab bar then becoming the session view's ONLY header (the status/name/back-arrow row gone — Tools at the tab bar's right end, since removed, the pipeline/feature pickers only with a template —, LaneConsolePane reduced to the terminal: no title, no Active runs — RunHistory.tsx deleted with the client's api.run.history/DashboardRunHistoryItem and the page's active-sessions poll —, no New run) + an ENDED session opening in its workspace (&session=, SessionReplay.tsx: conversation + prompt box, sending resumes) instead of /sessions/<id>, which the right panel's Analyst button opens + a session's AI TOOL chosen per session (tool on POST /api/lanes/:id/start, the home's AI select; the sidebar's tool icon moved from the workspace row to each session row) and the pane's run form GONE (RunSetup keeps only PromptEditor/CwdAutocomplete/ModelPicker; New run/exit/an empty pane go to the workspace home) + the DESKTOP-PARITY plan (docs/plans/desktop-parity/, phases 1–7; the multi-user server plan lives in the AgentHub-Server repo): an AI Router chosen PER SESSION (the AI Router select beside the model picker in the workspace home and a pane's run form, client/src/components/run/RouterPicker.tsx, router on both start routes; Claude --settings=<data dir>/router-settings/<name>.json, Codex -p <name>; dashboard_runs.router for a resume; POST /api/lanes/:id/router/apply, LaneRouterRow, the Add-workspace router radio and the claude-/codex-home routers answers gone; clearLaneRouters takes the old keys out of settings.local.json at start) + Settings and the collapse toggle pinned to the sidebar's foot + shipped SVG marks for claude/codex/gemini/openclaw/hermes (Gemini's from zonalogo.com) (client/public/ai-tools/<id>.svg, BUNDLED in AiToolIcon.tsx; the server's favicon route stays raster-only for the rest) + a workspace OWNS its sessions and lists only those (sessions.lane_id, stamped by bindRunSession, backfilled only from dashboard_runs; GET /api/sessions?lane=&archived=0|1; PATCH /api/sessions/:id {archived|name}), dashboard_runs keeping a pane's previous run as <pane>@<started_at> instead of overwriting it and flagging a run abandoned only once its tmux session is gone (reconcileOrphans(hasSession) at boot and on the run-heal beat), a session row's state dot (sessionDot), double-click rename and archive (sessions.archived_at, hides only; the home folds the archived with Unarchive), the activity bar's Sessions tab and WorkspaceSessionsTab.tsx gone with LaneConsolePane's resumeRequest, Fork beside Resume (forkSession → --fork-session, Claude only, EUNSUPPORTED → 400 on start), Ctrl/Cmd+click a live session → &pane=1, and a diff line's number opening a comment pasted into the workspace's last-focused pane (client/src/lib/panePaste.ts) + the sidebar losing its footer, language and theme controls (the collapse toggle sits beside Settings at the sidebar's foot; the version and Check for updates moved into the connection details modal, the status bar showing a green dot while a release is out; the language is Settings → Appearance, SettingsSegmented) + a session in its OWN WORKTREE, Claude's way (desktop-parity phase 6: the home's Local / New worktree select → claude --worktree=<name>, dashboard_runs.worktree, ?session= on every lane git route via server/lib/session-worktree.js, Resume/Fork into the worktree, ENOWORKTREE when it is gone, Archive = POST /api/lanes/:id/session-worktree/remove never forced; listWorktrees now reads locked <reason>) + ONE client/src/components/Switch.tsx for every on/off switch and the themed Checkbox.tsx (now with tone/disabled) for every checkbox — no native <input type="checkbox">, no hand-rolled switch + the AI Tools catalog TRIMMED to 10 (server/data/ai-tools/: claude, codex, gemini, antigravity, copilot, opencode, cursor — labelled Cursor CLI, --continue resume, cursor-agent update — omp, hermes, openclaw; the other 28 deleted; the argv-separator / fixed-args / after-start+bypassEnv prompt modes no built-in uses any more stay supported for custom files and are tested through server/__tests__/fixtures/ai-tools/) + the workspace-sessions plan's leftovers (a sidebar TOOL FILTER, agenthub.sidebar.projects → tool; search also finding a workspace by a session's name; the home naming its repository from GET /api/projects and Recents showing cost; the transcript page's explicit Resume in lanes/SessionResumeButton.tsx) + a WORKSPACE HOME like a Claude Desktop project page (client/src/components/lanes/WorkspaceHome.tsx: /workspace?lane=<id> opens it and hides — CSS — the session view, which &run=<runId> opens; its prompt box starts a new session in the first free pane slot; Recents; Instructions / Memory / Context / Folder over server/lib/lane-sources.js + server/routes/lane-sources.js: lanes.instructions, the new lane_sources table, uploaded files under <data dir>/workspace-context/<lane id>/, read-only memory, and spawnRun adding them to Claude/Codex argv itself) + Settings losing its Projects group (pages/ProjectSettings.tsx and the per-project override gone; Worktrees now picks the home's folder suggestions) + notifications opening the lane's pane (&run=) + the sidebar becoming a Claude-Desktop-style WORKSPACE LIST (client/src/components/WorkspaceTree.tsx replaces ProjectTree.tsx: one flat row per workspace in the stored order, no repo grouping and no untracked-worktree rows; a row's chevron expands its sessions, lanes/WorkspaceSessionList.tsx; a session with a live pane opens /workspace?lane=<id>&run=<runId>, which only ATTACHES pane 0 — attachRequest on LaneConsolePane — and any other one its read-only transcript; the sidebar never starts a resume; expanded rows in agenthub.sidebar.projects → expanded) + the Workflows page folding into Dashboard → Analytics → Workflow (Dashboard.tsx renders pages/Workflows.tsx under that sub-tab, ?tab=/?sub= deep-link the Dashboard, /workflows redirects to /?tab=analytics&sub=workflow, no Workflows item in the sidebar) + the named themes gaining orca-dark/orca-light (Orca's neutral palette in client/src/index.css; Orca Dark's accent is two shades of #3b82f6 — --accent #6aa0f8 for text, --accent-solid #3472d8 behind white text —, not Orca's near-white primary) + a sound per notification type (sound + soundByType in client/src/lib/notifPrefs.ts, default "none"; playSound in lib/notify.ts, played by useNotifications only when a notification was actually shown; the WAVs in client/public/sounds/ are generated by scripts/gen-notification-sounds.js — edit the script, never the files) + Settings becoming /settings/:section with a grouped left nav (client/src/pages/SettingsHub.tsx, route settings/*: General / Appearance / Notifications — Settings.tsx now takes section, its TOC gone —, Agents with agents/<id> per tool — a tool's own settings live there, the AI Tools modal is now the agents page and AiToolsModal.tsx is gone —, Routing (AI Router, an all-vendor Pricing page), Git (credentials, Worktrees = WorktreeVisibilitySettings.tsx), Projects = pages/ProjectSettings.tsx per repo with a per-project projectHideSources override and Forget all; ?tool=<id>/#ai-tools redirect to the path form; the whole-page search index is SETTINGS_INDEX in client/src/lib/settingsSearch.ts, ranked by rankSettings, Ctrl/Cmd+F focuses it; row primitives in components/settings/SettingsRow.tsx) + the Workspace page losing its lane STRIP to a sidebar PROJECT TREE (client/src/components/ProjectTree.tsx + lanes/WorkspaceRow.tsx/WorkspaceRowMenu.tsx: workspaces grouped by git common dir from the new GET /api/projects (server/lib/projects.js, server/routes/projects.js, POST /api/projects/refresh same-origin), untracked worktrees with Adopt (ensure exact: true), double-click rename, drag/Alt+↑/↓ order still in agenthub.workspace.laneOrder, a right-click menu with Start/Stop/Rename/router/New here/Copy path/Reset/Remove; prefs in agenthub.sidebar.projects, client/src/lib/projectTreePrefs.ts), the selected lane being the URL's ?lane= (mirrored to agenthub.workspace.selected), ONE shared lane list (client/src/hooks/useLanes.ts), a fixed bottom StatusBar.tsx (connection, version, running/needs-you counts — of SESSIONS, GET /api/sessions?status=active minus ?awaiting=1, never lanes —, today's cost from GET /api/pricing/cost?date=<local date>; every cost figure shows its token total and lists tokens + cost per model on hover) taking the sidebar's connection footer, the Changes panel gaining an activity bar (Files / Changes / Sessions = lanes/WorkspaceSessionsTab.tsx, a session resumed through LaneConsolePane's resumeRequest; agenthub.workspace.panelTab), and a per-pane header in split view + the user-facing word "lane" becoming "workspace" in UI copy (en + vi), CLI prose, server error messages and docs, every identifier unchanged (/api/lanes, agenthub lanes, lanes.json, Lane* components; docs/LANES.md keeps its name) + server/db.js running every migration under a cross-process lock (BEGIN IMMEDIATE on <db>-migrate.lock, held from before the legacy-DB copy until just before stmts) — two dashboards started at once on a fresh DB raced the probe-then-ALTER and the second died on duplicate column name; never wrap those migrations in a transaction on the main DB instead, since several rebuilds open their own BEGIN and turn foreign_keys off + a second dashboard on the SAME DB re-broadcasting the lanes the other one changed (broadcastForeignLaneChanges in server/routes/lanes.js, PRAGMA data_version every DASHBOARD_LANE_SYNC_MS=2 s from server/index.js — hooks reach only the lowest port, so a browser on the other port saw a lane's status only after F5) + the pricing sync never adding a RETIRED Claude model in advance (RETIRED_PRICING_PATTERNS moved to server/lib/pricing-sync.js, imported by db.js; one is priced only once used) + a just-started run no longer reading dead (lastEventAge also counts the run's dashboard_runs.started_at) + a Settings TAB PER CATALOG TOOL (client/src/pages/AiToolConfig.tsx for every installed, enabled tool with no launcher, built in SettingsHub.tsx from useAiTools, ?tool=<id>; AntigravityConfig.tsx deleted, its pricing + hooks cards living on there): path, homepage, an Update… link into the AI Tools modal, GET /api/ai-tools/:id/usage sessions/cost, the tool's lanes, a Default model saved through PUT {defaultModel} (ModelPicker now exported from RunSetup.tsx), PricingSettings for the catalog's model.vendor (antigravity now google), and the catalog's trusted config files edited through server/lib/ai-tool-config.js behind GET/PUT /api/ai-tools/:id/config[/:index]; the view gains modelVendor/configFiles + MODEL LISTS from each tool itself (server/lib/ai-tool-models.js behind GET /api/ai-tools/:id/models: Codex's models_cache.json listed slugs — its identity never read, Claude's aliases labelled with the newest priced version plus pinned priced versions, a catalog tool's vendor-priced ids plus the models its sessions used), a catalog model: { flag, vendor? } field (antigravity, gemini, opencode, cursor) putting <flag> <id> before the prompt, validModel refusing a flag- or shell-shaped id (EBADMODEL) at the route AND in spawnRun, a per-tool defaultModel in ai-tools.json (PUT /api/ai-tools/:id {defaultModel}) that LaneConsolePane starts each run on, the run form's picker fetching that list (RUN_MODEL_CHOICES only as Claude's offline fallback, RUN_MODEL_CHOICES_CODEX deleted) + AUTOMATIC pricing that ADDS and FOLLOWS: pricing-sync.js fetches LiteLLM at most daily, inserts every current-generation Claude/OpenAI (gpt ≥ 5)/Gemini (≥ 3) chat model before it is used (isCurrent, MIN_GEN), keeps its own rows (model_pricing.source = 'sync', the Auto badge in PricingSettings) at the published rate, adopts a pre-column row only while it equals the list, and never touches a source = 'user' row (any PUT /api/pricing) or a deleted pattern (model_pricing_dismissed, emptied by a reset for its scope) + a plugin build installed BESIDE the running one (a terminal claude plugin update starts nothing) being reported: installed_not_running + update_available in pluginStatus (server/lib/update-check.js, via supersededBy, no network), the sidebar's installed, not running, the update modal keyed on it, plugin-update-helper.js handing over to the recorded install first; and a plugin install whose release lookup failed reading Could not check for updates instead of Up to date (Sidebar.tsx) + a markdown PREVIEW (client/src/components/MarkdownPreview.tsx: react-markdown + remark-gfm, raw HTML never run, frontmatter as a code block) behind a Preview/Raw toggle in the config FileViewer and the Changes panel's whole-file view + AI Tools moving from a Settings → Dashboard section to a MODAL behind a button at the Settings tab bar's right edge (client/src/components/settings/AiToolsModal.tsx, actions on PageTabs, opened by /settings#ai-tools too); its rows an enable switch + Default badge/Set default + Update for EVERY tool whose catalog names an updater (no version lookup on open — api.aiTools.versions left the client, GET /api/ai-tools/versions stays), the not-installed tools folded behind a disclosure, a finished job's log folded to one line; codex updating through its own codex update (standalone or npm) and openclaw/hermes gaining update --yes; update-all running every installed updater, skipping an npm one only when npm does not manage the binary, with no npm view; the shared client/src/components/AiToolIcon.tsx; the Add-lane tool picker being ONE button showing the chosen tool that opens the enabled tools as buttons (no Default/Other… select); and the Claude Code / Codex AgentHub sub-tab folded into their Overview (home editor, hooks, pricing under the overview card; the overview's path list drops CLAUDE_HOME/CODEX_HOME; HomeDirSetting shows the saved value only while the field differs) + ANTIGRAVITY PARITY (#40 part D: server/lib/antigravity-usage.js reads agy's own per-conversation SQLite files ~/.gemini/antigravity-cli/conversations/<id>.db read-only every DASHBOARD_ANTIGRAVITY_POLL_MS — tokens from gen_metadata's protobuf ModelUsageStats, cwd from trajectory_metadata_blob, name/start from brain/<id>/…/transcript_full.jsonl — into tool = 'antigravity' sessions, mtime per file in the new antigravity_conversations table, and sets idle the lane of a session gone quiet that the hooks left running; server/lib/antigravity-hooks.js installs on a click the named hook agenthub (PreInvocation, PostToolUse, Stop — NEVER PreToolUse) into the global ~/.gemini/config/hooks.json through ~/.gemini/config/agenthub-hook.sh, GET/POST/DELETE /api/settings/antigravity-hooks; routes/hooks.js antigravityHookInput maps a CLI hook — conversationId → session, workspacePaths[0] → cwd, first PreInvocation → UserPromptSubmit — and ignores the IDE's; server/lib/antigravity-home.js (AGENTHUB_GEMINI_DIR); Gemini families in pricing-sync.js; the new Settings → Antigravity tab client/src/pages/AntigravityConfig.tsx with PricingSettings vendor="google" and AntigravityHooksSettings.tsx. Its review fixed: an empty -wal (our own read-only open leaves one) is not activity; activity counts the session's newest hook event, with a 10-minute window mid-turn; an agy Stop carrying an error lands as error, one with fullyIdle: false is dropped, and the first PreInvocation after a Stop is a prompt; agyCliDirs() for a symlinked ~/.gemini; both tools' hook installers refuse to overwrite or remove a forwarder that is not theirs (EFOREIGN) and keep hooks.json's mode. The C2 review also fixed: Codex hooks matched by exact command, one hook at a time, so a user's hook in our group or with a similar name survives; sameOriginGuard on the codex-hooks writes; codexHomes() in codex-home.js for a symlinked CODEX_HOME; a missing main agent created for a poll-imported Codex session; refreshScript leaving a foreign file alone; Refresh on the card. The whole-change review also fixed: settings load real env > repo .env > the settings file (located after .env loads, so AGENTHUB_SETTINGS_FILE may live there), a .env key env_owned too, writeSetting replacing every earlier line for its key and writing through a symlink, validateHomeDir comparing device+inode with the home and each ancestor, sameOriginGuard on both home PUTs, a claude-home PUT answering routers.missing (lanesWithMissingRouter in router-apply.js), CLAUDE_HOME-derived paths read per call in the hook status, install-hooks.js and import-history.js; reset-pricing {scope: "claude"} from the Claude tab; GET /api/sessions?tool= before the limit; clear-data emptying codex_rollouts and antigravity_conversations, plus idx_codex_rollouts_thread; Compaction tagging for claude only, a Codex sub-agent's folded tool event (_codexChild) moving no session, agent or lane status, and hooks owning a Codex session's status only while current (hooksCurrent: newest event ≥ newest journal write − 2 min, also in settleActive); scriptIsOurs treating other content, an unreadable file or a directory as foreign, a symlinked hooks.json emptied through the link; agy's quiet window 10 min for any session with hook events (2 min without), a settle completing its agents and clearing waiting flags, a PreInvocation a prompt also with no prior event, a non-active session or a last event >2 min old, a terminationReason matching /error|fail/ an error Stop, a file that is not agy's recorded with session_id '' and retried on change or at start (anything else next pass), and the session id taken from the file name) + CODEX HOOKS (#40 part C2: server/lib/codex-hooks.js installs — only on a click in Settings → Codex → AgentHub → Codex hooks, GET/POST/DELETE /api/settings/codex-hooks — one group per event (SessionStart, UserPromptSubmit, PreToolUse, PermissionRequest, PostToolUse, Stop, Interrupt, SessionEnd) into $CODEX_HOME/hooks.json, all through the stable forwarder $CODEX_HOME/agenthub-hook.sh (content rewritten each start by refreshScript, command never changes, because Codex trust is keyed on the definition); scripts/hook-handler.js <Event> codex posts tool: "codex"; routes/hooks.js creates tool = 'codex' sessions with no transcript_path, skips every Claude-transcript read for them, maps PermissionRequest → Notification and Interrupt → Stop, folds a sub-agent thread's hooks into the root session (threadOf in codex-usage.js), and touchLaneFromHook resolves the lane by (cwd, tool) with detection for claude only; a Codex session with events keeps its hook-given status in codex-usage.js, and codex-ingest.js skips a lane whose hooks are current. Also from the C1 review: one codex_rollouts row per thread, the resume picker lists Claude sessions only, a failed price fetch waits 15 min, a fork skipped until its first own total, 16 MB per read, usage rewritten whole, root-thread cwd/start/model, the startup cleanup and a lane purge scoped by tool, and the codex lane mirror deciding once per lane with a second of future-mtime slack) + CODEX SESSIONS + TOKEN COST (#40 part C1: server/lib/codex-usage.js reads every Codex rollout journal into sessions (new column tool, 'claude' default, 'codex' here) and token_usage — offset + parse state per file in the new codex_rollouts table, a fork's copied parent history skipped by total vector, sub-agent threads summed into the root session, input = input − cached, exact totals via setTokenUsage, no transcript_path; startCodexUsagePoll on the DASHBOARD_CODEX_POLL_MS beat; allRolloutFiles() exported from codex-ingest.js; pricing-sync.js also prices gpt-…/o<n>…/codex-… from LiteLLM's openai entries and reset-pricing syncs right after reseeding; AGENTHUB_PRICING_SYNC=0 in scripts/test-server.js; client/src/components/settings/PricingSettings.tsx = the pricing table with a vendor filter + Show all models, in both tools' AgentHub sub-tabs; SessionToolBadge.tsx; the Workspace attach/resume list offering tool = 'claude' rows only) + per-tool SETTINGS (#40 part B: Model Pricing, Hook Configuration and CLAUDE_HOME moved from Settings → Dashboard to the Claude Code tab's AgentHub sub-tab, client/src/components/settings/ClaudeAgentHubSettings.tsx — a tool's own settings go in that tool's tab; setClaudeHome/setCodexHome refuse any dir that is or contains the OS home (EBADHOME, validateHomeDir in server/lib/settings-env.js) and persist to AgentHub's settings file ~/.config/agenthub/settings.env (AGENTHUB_SETTINGS_FILE; real env > repo .env > that file, loaded only when server/index.js is main — never the plugin cache's .env, which an update wipes), a first CLAUDE_HOME change pinning DASHBOARD_DATA_DIR so the DB stays put; CODEX_HOME editable in Codex → AgentHub (/api/settings/codex-home, HomeDirSetting.tsx, reapplyCodexLayers re-writing codex lanes' router layers into the new home); a codex pane is started with the dashboard's CODEX_HOME through tmux -e, in server/lib/pty-run.js) + AI Tools INSTALL/UPDATE (#40 part A2: catalog fields install/update honoured only in built-in and release files; server/lib/ai-tool-jobs.js runs ONE job at a time as an argv — the login shell's npm, a downloaded vendor script via bash <file>, or a tool's own updater; server/lib/ai-tool-versions.js offers an npm update only when the binary resolves under npm prefix -g; server/lib/ai-tools-sync.js copies the newest release's catalog daily when it is newer than the build, off with AGENTHUB_AI_TOOLS_SYNC=0; server/lib/ai-tool-icons.js caches raster favicons server-side; shellPath() in shell-path.js; repoUrlFor in update-check.js; six /api/ai-tools routes; Install/Update/Update tools in AiToolsSettings.tsx) + the pipeline layer going DORMANT (no template ships: server/data/pipelines/ deleted, EMPTY_PIPELINE, ENOPIPELINE, test templates under server/__tests__/fixtures/pipelines/) and the workflow skills leaving the plugin (skills/ship-feature, skills/ship-feature-lane, the 18 stage/ship commands, all four agents/, agenthub skills install, agenthub lanes agents install, agenthub ship-full|ship-quick, POST /api/skills/install, POST /api/lanes/:id/agents/install, and the Superpowers auto-install with server/lib/skill-plugins.js and its doctor row) + the AI TOOLS catalog (#40 part A1: server/lib/ai-tools.js loads one JSON per AI coding CLI from server/data/ai-tools/ — 38 tools at the time, data from Orca, MIT, trimmed since to 10 — plus overrides in DASHBOARD_AI_TOOLS_DIR, detects each by walking PATH, and keeps enable/default in <data dir>/ai-tools.json; server/lib/shell-path.js merges the login shell's PATH ($SHELL -ilc, 5 s, off with AGENTHUB_SHELL_PATH=0) at startup and on Refresh; /api/ai-tools GET/refresh/PUT; AI Tools (client/src/components/settings/AiToolsSettings.tsx, now a modal opened from the Settings tab bar's right edge, AiToolsModal.tsx) and the shared client/src/hooks/useAiTools.ts, which replaced LANE_TOOL_LABEL; the Add-lane modal's Default () + Other… picker; the built-in router profile now labelled plain Native, rendered Native (); the Claude Code/Codex settings tabs hidden while that tool is disabled; a per-run Bypass permissions switch; POST /api/run now forwarding sandbox/approval/resumeLast, which it had dropped) + antigravity lanes (a third lanes.tool, running Antigravity's agy CLI; no pipeline, no AI Router, no model/effort picker; the lane card no longer renders a bare tool chip, and the prompt box names the lane's CLI via LANE_TOOL_LABEL in client/src/lib/types.ts) + a lane whose pane EXITED being released on a server beat (healAllRunIds in server/routes/lanes.js, every DASHBOARD_RUN_HEAL_MS=10 s from server/index.js) — a codex/antigravity pane fires no hook, so nothing else re-read it and the lane stayed running + a codex Start no longer sending resumeLast (only Resume last session does) + per-CHANGE-BLOCK review bars (issue #50: stage-hunk/discard-hunk take an optional block — the index of a run of consecutive +/- lines inside the @@ hunk — and onlyBlock in server/lib/git-diff.js cuts git's fresh hunk down to it, folding every other block into the side being checked; changeBlocks/LaneDiffChange in LaneDiffView.tsx hang one bar per block, and the arrows land on data-change-index) + codex lanes losing their pipeline (codex-ingest.js mirrors only idle↔running — no stage detection or auto-advance, no keepDetection; codex-nudge.js deleted; every server start runs clearCodexPipelines (server/lib/lanes.js: a codex lane's stage columns, lane_stage_events and lane_features wiped) and removeStageDoc (server/lib/lane-agents.js: the old <!-- agenthub:stage:start --> block taken out of each codex lane's AGENTS.md, the file deleted when nothing else is left); agenthub lanes agents install installs nothing on a codex lane; the Workspace hides stage, time-on-stage, pipeline/feature pickers and PipelineMap for one) + info/exclude entries AgentHub writes into a lane going to the COMMON git dir with the lane's path prefix (server/lib/git-exclude.js, used by router-apply.js and lane-mcp.js — a linked worktree's own info/exclude is never read by git, and a subdir lane has no .git) + macOS start-at-login (scripts/launchd-service.js: the LaunchAgent ~/Library/LaunchAgents/io.smartgift.agenthub.plist, same API as systemd-service.js, chosen by scripts/service-manager.js — which the bootstrap, agenthub service and /agenthub-doctor now require instead of systemd-service directly) + v1.8.6 (2026-09-24) + four fixes found by the #37 evaluation, shipped without #37 itself: tmux targets are EXACT (=name for has-/kill-/attach-session, =name: for pane and option targets, in server/lib/tmux.js, pty-attach.js and scripts/cc-update-all-helper.js) because a bare -t agenthub-lane-1 resolves by prefix to agenthub-lane-12 once lane 1 is gone; getRun/killRun in server/lib/pty-run.js refuse any id that is not agenthub-lane-<n>, so DELETE /api/run/main can no longer kill a user's own tmux session; POST /api/lanes/:id/router/apply reports restart_required only when the pane's status is running (it was always true, since getRun returns a gone handle); and a lane's needs_action is cleared only by the session that raised it (new nullable lanes.needs_action_session, server/routes/hooks.js) + macOS support, code-audited and untested on a Mac (#22): server/index.js deletes a DENYLIST of parent-session variables at startup (PARENT_SESSION_VARS: CLAUDECODE, CLAUDE_CODE_SESSION_ID, CLAUDE_CODE_MESSAGING_, CLAUDE_PLUGIN_ROOT, AI_AGENT, …, plus GIT_EDITOR only when it is Claude Code's true; user config such as CLAUDE_CONFIG_DIR/CLAUDE_CODE_USE_BEDROCK/ANTHROPIC_ passes through), because a server started from a Claude Code session handed them through tmux to every lane pane; the ~/.local/bin/agenthub launcher falling back to node on PATH when its pinned node is gone (linkCli in scripts/plugin-bootstrap.js); liveServers ignoring a discovery entry whose startedAt predates this boot, so a recycled pid is never a live dashboard; lane cwds stored by real path (canonicalCwd in server/lib/lanes.js, used by createLane, POST /api/lanes/ensure and the liveness reaper — /tmp becomes /private/tmp, a not-yet-existing path through its deepest existing ancestor, existing rows untouched); an __setExecImpl seam in server/lib/session-liveness.js; and the INSTALL.md § macOS section with the Mac checklist + issue #34's empty-repo half — publish as a git write (server/lib/git-ops.js: remote add origin -- <url> + push -u origin HEAD, only in a repo with a commit and NO remote, URL allowlisted by validateRemoteUrl to https-without-userinfo/ssh:///scp-like, origin removed again on a failed push; ENOCOMMITS/EREMOTEEXISTS/EBADURL), an unborn repo being readable (gitFacts/statusCounts in server/lib/worktree.js: head: null, new unborn, branch via symbolic-ref), POST /api/lanes/worktree answering 400 EEMPTYREPO instead of leaving a failed row, and LaneGitBar.tsx gaining Publish… (no remote), Publish branch (remote but no upstream) and a commit-first hint (unborn) while AddLaneModal.tsx steers an empty repo to Repo mode + a dashboard fetch/pull/push that needs a git LOGIN asking for one in the git bar (issue #45: 409 EGITAUTH with auth, resent with credentials; AgentHub's own shared store server/lib/git-credentials.js + helper server/lib/git-credential-helper.js, /api/git-credentials, Settings → Git credentials = client/src/pages/GitCredentialsConfig.tsx) + MERGE and conflict resolution from the dashboard (issue #45 part B: merge/resolve/merge-commit/merge-abort in server/lib/git-ops.js, gitFacts.merging/conflicts, the git bar's Merge into current and diverged-upstream Merge, the Changes panel's merge banner and Merge conflicts group, and the per-block conflict view client/src/components/lanes/LaneConflictView.tsx over the pure parser client/src/lib/conflicts.ts — keep mine/theirs/both per block, saved through the whole-file editor's PUT /git/file + resolve done, no new API) + drag-and-drop of an image onto the lane terminal (TerminalView.tsx uploads to POST /api/run/drop-image in server/routes/run.js — raw body, png/jpeg/gif/webp only, random name under <tmpdir>/agenthub-drops-<uid>/, never a name from the request — then term.paste()s the returned path, which is how claude attaches it) + Claude Opus 5.5 getting its own pricing row (claude-opus-5-5% at 4/20/0.2/5/8, fast 8/40 — claude-opus-5% had been pricing it as Opus 5) + AUTOMATIC pricing (server/lib/pricing-sync.js, started in startBackgroundServices: a model family used in token_usage with no <family>% row of its own is INSERTED from LiteLLM's public price list — since followed daily for the rows it owns, see the entry above — off with AGENTHUB_PRICING_SYNC=0) + the Changes panel at Full screen moving the whole Files view (finder + expand-all + tree, one browsePane) into a left SIDEBAR and dropping the Files tab and in-place editing (full prop on LaneChangesPanel from Workspace.tsx; a whole file opens for READING and enters the editor only on a click on its text — onEditAt on LaneFileView, caret at the clicked line, a drag-selection is not a click; Cancel returns to reading, and only a diff keeps an Edit button) and height-locking the page (viewportLocked also true at full, else the panel shrank to content with no live run) + the overview splitting into Staged Changes / Changes groups in the work range + a changed tree row tinted whole (treeMark tone carries a bg-status-*/10) + the Files tree listing untracked-not-ignored files (and, since, ignored ones marked ignored) and carrying each entry's git status (treeStatus/treeEntries/TREE_LS in server/lib/git-diff.js: file status = porcelain code, folder changed: true, from ONE laneStatus per tree read; treeMark in LaneChangesPanel.tsx paints M/U/A/D/R) + split panes on ONE lane (every Workspace pane shows the strip's selected lane, each in its own tmux session: slot 0–3 on POST /api/lanes/:id/start, agenthub-lane-<id> for slot 0 = run_id, agenthub-lane-<id>-<slot> for 1–3 — runIdForLane(id, slot)/MAX_PANE_SLOT in server/lib/pty-run.js, the -[1-3] suffix accepted by pty-attach.js and websocket.js; stop/reset/remove kill every slot via laneRunIds; a pane's Active runs hides what the other panes show; no per-pane lane picker) + the server suite running on a PRIVATE tmux server (npm run test:server = scripts/test-server.js: drops TMUX, sets TMUX_TMPDIR to a temp dir, kills that server after — suites spawn and kill real agenthub-lane-<id> sessions with ids from their own DB, and on the default server that killed the user's real lane panes, including the one running the tests) + the whole-file EDITOR (PUT /api/lanes/:id/git/file, server/lib/lane-file-write.js: the read path's guards plus existing-text-file-only and a baseSha stale check → 409 ESTALE; readFileAt returns that sha) + the per-hunk controls moving into an editor-style review bar under each hunk's last changed line (HunkBar in LaneDiffView.tsx: ^/v, n of N, Undo, Keep) + Unstage all on the ONE commit/Stage-all row + Changes sizes Narrow (30%) / Medium (50/50) / Full screen, no 70/30 + the lane card's needs-you banner gone (the strip tile keeps it) + per-HUNK Stage/Undo in the Changes panel's Review and single-file views (stage-hunk/discard-hunk in server/lib/git-ops.js: the hunk is named by its @@ line and re-matched against a FRESH git diff -U<context> HEAD by hunkPatch in git-diff.js, then git apply --cached / git apply -R from a temp file — 409 EHUNKGONE when it moved, 400 EUNTRACKED on an untracked file; discard-hunk is the SECOND irreversible git action and carries the same confirm: true gate and two-click UI; the controls reach every HunkHeader through a context in LaneDiffView.tsx), each Review file header sticky with its own stage/unstage/discard (FileActions, shared with the overview row) + the Changes panel gaining a FULL-width step (CHANGES_SIZES in Workspace.tsx: narrow → 50/50 → 70/30 → full → closed; at full the lane-detail section gets hidden — CSS, so the one console render site stays mounted), Stage all plus the staged-only commit box ABOVE the overview list and sticky (as is the Files finder), and Expand all folders being ONE GET /api/lanes/:id/git/tree-all (listTreeAll in server/lib/git-diff.js: one ls-files, every level shaped like listTree's) instead of a request — and a git ls-files — per folder + the lane's git section MOVING from the lane card to the top of the Changes panel (client/src/components/lanes/LaneGitHeader.tsx: useLaneGitFacts, the working-copy line, LaneGitFilesPanel and LaneGitBar, sharing ONE facts read with LaneChangesPanel, which refreshes it after every stage/unstage/discard/commit while a git bar action re-reads the diff; onNewLane comes from Workspace.tsx, and LaneCard has no git row and no onNewLaneFromRepo any more) and the git bar losing its commit UI — the overview's staged-only box is the ONE commit path, the server's commit without only is unchanged; the git line and its icon-only actions are ONE row in one card, the panel toolbar ONE wrapping row, and LaneCard hides itself (empty:hidden) when it has nothing to show + the run form's / and @ autocomplete being REAL again (PromptEditor in client/src/components/run/RunSetup.tsx — the 2f39f4e rewrite had left a bare textarea: / offers the run cwd's project + user commands and skills, via api.ccConfig.commands|skills(scope, cwd), plus the CLI built-ins, claude lanes only; @ offers GET /api/run/files; Enter picks while the list is open and sends otherwise) + the console's Active runs listing only the CURRENT lane's runs and external sessions (LaneConsolePane.tsx: runs by laneId, sessions by the longest path-boundary lane cwd, owningLaneId) + v1.8.3 (2026-09-23) + GET /api/settings/info reporting server.memory.heapLimit (V8's heap_size_limit), which the Dashboard's V8 Heap bar and Health Score now divide by instead of heapTotal + every claude plugin install|update -y (cc-plugin-cli.js, skill-plugins.js, both detached update helpers, /agenthub-update) retrying WITHOUT -y when an older Claude Code answers unknown option '-y' + LaneDiffView.tsx WINDOWING a diff above WINDOW_THRESHOLD (500) items at a fixed ROW_PX row height (visibleRange, spacers, a ch min-width off the longest line; at or below the threshold the DOM is unchanged) and clamping its mirrored split scroll (clampScroll/follow) + a git action that git itself refuses (a numeric exec exit status) answering 409 EGIT instead of 500 with a number for a code (gitErrorCode in server/routes/lanes.js), and one parseNumstat (server/lib/git-diff.js) shared by the Changes panel and gitFacts.files + the Dashboard heatmap ramp mixing --accent into --surface-0 so it reads on every theme + the lane git surface being HARDENED (server/lib/git-diff.js, server/lib/git-ops.js, server/lib/worktree.js: every lane git call runs with GIT_LITERAL_PATHSPECS=1 and -z porcelain, is scoped to the LANE's cwd — a subdir lane sees and acts on only its own paths, gitFacts.files included — and reads through lanePath at every rev; any file whose basename is settings.local.json, the lane router key file, is refused as ESECRET/403 on disk and at every rev; commit refuses with ESTAGEDOUTSIDE while anything outside the lane is staged, checked with --no-renames; push without a resolvable remote is ENOREMOTE; git writes serialize on the repo's absolute git dir, not the lane id) + the xterm terminal following the selected DARK theme (client/src/lib/terminalTheme.ts, live via term.options.theme) and chart colours using structural tokens + the Add-lane modal asking POST /api/lanes/ensure with exact: true (#42) — without it a directory NESTED inside an existing lane resolves to that parent lane with created: false, so the modal closed on the parent and no new card ever appeared; exact counts only a lane on that very directory, and a created: false answer now names the lane that owns the folder instead of closing. The CLI, the run console and Sagi keep the parent lookup + Settings → Appearance (client/src/components/settings/AppearanceSettings.tsx: mode Dark/Light/System — prefers-color-scheme, followed live — plus one NAMED theme remembered per mode: dark Default/Darcula/Apple Dark, light Default/Apple Light/IntelliJ Light/Canvas). A theme is a CSS-variable override block in client/src/index.css keyed by data-theme on <html> (.dark[data-theme=…] / [data-mode="light"][data-theme=…], data-mode is set on <html> too), and --tint-base/light --syn-plain are now var(--surface-3)/var(--fg-primary) so no theme restates them; hooks/useTheme.ts is a useSyncExternalStore reader over localStorage (theme = dark|light|system, agenthub.theme.dark|light) that caches NOTHING in the module, so the Sidebar and Settings can never disagree — do not reintroduce per-instance useState there; an inline <script> in client/index.html reads the same keys before first paint, so a key rename touches BOTH + the diff's +/- rows (LaneDiffView.tsx rowTint/markerClass, highlight.ts diff-add/diff-del) taking a /20 status tint, a 3px status bar and a coloured marker, and the changed-span emphasis bg-fg-primary/15 instead of bg-white/15, which vanished on every light theme + the light theme going WARM (client/src/index.css: the whole light ramp is Claude's ivory/terracotta family — page #e8e5da, .card #f5f3ec (one notch below Claude's own #faf9f5 ivory), warm borders and a warm --shadow-card, --accent #924224 (was #a34a28) which is the brand #cc785c three steps down because the brand value is 3.1:1 on ivory and 3.3:1 under white text, and #a34a28 missed 4.5:1 on its own selected chip — dark mode is untouched and stays Radix slate + blue) + the Add-lane modal having NO pipeline picker and no Advanced disclosure (AddLaneModal.tsx: every lane is created on the server's own default, changed afterwards from the lane's detail header or at creation via agenthub lanes add --pipeline <id>; the router radio is the one per-lane extra left and it sits in the form) + the lane-detail header dropping its stage and auto: <stage> chips (the PipelineMap directly below already draws both) + the Changes panel's ONE resize control being icon-only, drawing the width the next click lands on + status/accent FILLS being pre-mixed instead of translucent (tint() in client/tailwind.config.js + --tint-base in both themes of client/src/index.css: bg-status-success/10 and border-status-danger/40 now compile to color-mix(…, rgb(var(--tint-base))), because rgb(… / 0.1) composites with whatever sits behind it — one "success" chip rendered as five different colours over the five surface steps and washed out on light cards. Scoped to backgroundColor/borderColor ONLY: text-status-warning/80 and ring-accent/40 sit on top of a fill and must stay translucent, and a translucent surface is what every scrim is made of. Light --status-* also dropped two steps below Radix 11 so a label clears 4.5:1 on its own densest chip, not just on the card) + the LIGHT theme being rebuilt around the rule that the surface ramp rises in lightness with its index in BOTH themes (client/src/index.css: page surface-0 #eef0f3 under a brighter .card surface-3 #f8f9fb, so hovers lift; light --accent #0068d6 because dark's #0090ff is 3.3:1 on white; --fg-muted held at ≥4.5:1; new --syn-* syntax tokens + syntax.* in tailwind.config.js consumed by tokenClass in client/src/lib/highlight.ts, dark keeping its pastels and light taking the same hues four steps darker) — a hardcoded near-black panel, a hover:text-white or a global filter: invert(1) is a light-mode BUG, token it or scope it under .dark + client/src/components/Tooltip.tsx — GlobalTooltip, mounted once in App.tsx, upgrades EVERY native title in the app into an anchored, themed bubble (parks the attribute in data-tip while the bubble is up and restores it after, so the accessible name and the no-JS fallback survive; data-tip-off opts out, Tip.tsx shares its TOOLTIP_SURFACE) — do NOT add a per-call-site tooltip wrapper, keep writing title= + the lane git surface of issues #34 and #35 — server/lib/git-diff.js (a lane's changes as parsed hunks, range=work|branch|commit, a caller-chosen context, three caps, plus listCommits/readFileAt/listPaths/listTree) behind GET /api/lanes/:id/git/changes|log|file|ls|tree, server/lib/git-ops.js (fetch/pull/push/stage/unstage/discard/commit/switch/branch-create/branch-delete — a per-file path must be one git status reports as changed, and discard needs confirm: true) behind POST /api/lanes/:id/git/:action, the Changes panel's five modes (Files first) with a staged-only commit box in its overview and per-file collapse in its review list (the header folds, only the PATH opens the file; plus collapse/expand-all) and a back that returns to the originating mode AND scroll offset, ONE header control that steps narrow/50-50/70-30/closed (agenthub.workspace.changesSize beside changesOpen), and its VS Code-shaped viewer (LaneDiffView.tsx: a diff bounded to its own box and scrolling on BOTH axes — a horizontal bar at the foot of a 2000-line container is only reachable at the end of the file — unified/side-by-side as two independently scrolling columns with MIRRORED scrollLeft — halves inside one scroller painted over each other with nothing to scroll — highlighting reused from client/src/lib/highlight.ts, intra-line emphasis, expand AND collapse context, whole-file), the Add-lane modal's third branch answer Current branch (it switches the modal to Repo mode — a lane that simply IS the repo on main, which a worktree could not do anyway because git holds one worktree per branch) and its prefill prop, driven by the git bar's New lane here, gitFacts gaining hasRemote/upstream/ahead/behind, GET /api/lanes/branches gaining worktrees, GET /api/run/cwds gaining a repo kind (the source_repo of existing lanes), the Add-lane modal's new/existing branch toggle + worktree list + Advanced disclosure (AddLaneModal.tsx), and the two new client components LaneGitBar.tsx (now in the Changes panel, via LaneGitHeader) and LaneChangesPanel.tsx (the collapsible Changes card beside lane-detail in Workspace.tsx, agenthub.workspace.changesOpen) + v1.6.17 (2026-09-18) + the run's permission mode defaulting to auto and the model picker sending ALIASES (ALLOWED_PERMISSION_MODES in server/lib/pty-run.js + server/routes/run.js gained auto, which is also the fallback buildClaudeArgv passes to --permission-mode, the RunSetup select's first option and LaneConsolePane's initial state; and RUN_MODEL_CHOICES in client/src/lib/api.ts now holds 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, and pricing is unaffected because it keys off the resolved id the transcript reports) + convertEol being GONE from the xterm terminal (TerminalView.tsx) — 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 of issue #33, which the attach-size fix alone did NOT cure + the lane pane inheriting the graphical session's DISPLAY/WAYLAND_DISPLAY/XAUTHORITY/XDG_RUNTIME_DIR (server/lib/desktop-env.js, merged into spawnRun's tmux -e env) — a systemd user service has none of them, so claude in a dashboard-started pane could not read the clipboard and answered "no image found in clipboard" on every image paste + the PTY attach carrying the client's REAL size (sizeFromQuery in server/websocket.js, ?cols=&rows= on the /ws-pty/:runId URL built in TerminalView.tsx) — attach() was spawning tmux attach-session at a hardcoded 80×24 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 and that width stayed in the scrollback as the torn left-margin fragments of issue #33 + the Workspace's no-lane state being a real banner (lane-empty in client/src/pages/Workspace.tsx, empty.title/empty.body in lanes.json replacing emptyState) — the lane strip, add tile included, now renders only when a lane exists + the Create/update readme file action living in the lane's Tools menu (both since removed; LaneToolsMenu in client/src/components/lanes/LaneCard.tsx, actions.writeDocs* in lanes.json) instead of being a checkbox in the Add-lane modal — same api.run.start({initialPrompt: "/sg-all:sg-md-writing"}), claude lanes only, but re-runnable long after creation + the lane terminal taking focus on mount and reporting its own death (TerminalView.tsx: term.focus() after the first fit, so typing reaches tmux without clicking the pane, and a new onExit prop — read through a ref so an inline parent callback never re-attaches the socket — fired on the server's exit frame; LaneConsolePane runs newRun() there, so a double Ctrl+C out of claude lands back on the run setup form instead of a dead pane behind a New run button) + the run console pre-filling NOTHING into the prompt box and a resume sending NO prompt (LaneConsolePane.tsx's promptPrefill is gone — picking a pipeline used to write its driving slash command into the box on a claude lane and a stage-declaration checklist on a codex one, and whatever sits there is sent as a real prompt the moment the run starts; handleStart in RunSetup.tsx now drops initialPrompt entirely when a session is being resumed, since a resume carries its own transcript and anything typed would fire an extra turn into it) + browser notifications falling back when the push landed NOWHERE (notify() in client/src/hooks/useNotifications.ts): POST /api/push/send answers 200 with {native:false, pushed:0} whenever this browser has no push subscription — no service worker, a non-secure origin like http://<lan-ip>:4820, an unreachable push service — and the client counted that as delivered, so nothing ever appeared; delivery is now native || pushed > 0 and anything else shows the notification locally (skipped when the push DID land, or every one would be doubled). The Notification API still needs a secure context, so a bare LAN IP can show nothing at all + auth-profile switching reading and writing the macOS login Keychain (readCredentials/writeCredentials in server/lib/cc-profiles.js): on darwin the claude CLI keeps claudeAiOauth in the generic-password item Claude Code-credentials (account $USER), never in <CLAUDE_HOME>/.credentials.json — so a switch rewrote a file nothing reads and the CLI kept running as the previous account. The Keychain wins when it holds an item, the file stays the fallback and is kept in step only when it already exists, and AGENTHUB_CC_KEYCHAIN=0 pins the file (the test suite sets it so a Mac dev run never touches the real Keychain) + turn bookkeeping being idempotent (stmts.insertEventAt in server/db.js, used for TurnDuration with the TRANSCRIPT's timestamp — its dedup guard matches on created_at, so a NOW stamp made every hook re-insert the session's whole turn history: 587k rows, 75% of the events table — and meta.thinking_blocks/turn_count/total_turn_duration_ms now ASSIGNED from the full parse instead of +=, since extract() returns whole-transcript totals every time) + hasLegacyHooks blocking fastPathOk (scripts/plugin-bootstrap.js): leftover checkout hook entries double EVERY event and subagent row, stripLegacyHooks only ever ran inside bootstrap(), and the fast path skipped it on exactly the healthy installs where it mattered + the one-time repair marker moving to user_version 3 (server/index.js) — it now also deletes the duplicated TurnDuration rows, and 2 could not be trusted because an old build still ingesting hooks re-inflated whatever that pass corrected + a router switch on a LIVE pane reporting itself (POST /api/lanes/:id/router/apply now answers restart_required/run_id when runs.getRun(runIdForLane(id)) is live, and LaneRouterRow renders a Restart button that kills that pane and restarts it from the lane id alone — Claude Code re-applies env to a running session but keeps the model it resolved at startup, so the old router's model name reaches the new endpoint and the next prompt fails) + the sidebar brand being the SmartGift wordmark with the AgentHub caption under it and the vendored client/public/favicon.ico (Sidebar.tsx, client/index.html, client/public/manifest.json) + the two "Claude is waiting for YOU" notification types (client/src/lib/notifPrefs.ts — the ONE definition of the prefs shape/defaults/storage, now imported by both Settings.tsx and useNotifications.ts; onAwaitingInput and onTurnComplete default ON, issue #29) + per-message token counting (lastUsageMsgId in server/lib/transcript-cache.js and both parsers in scripts/import-history.js: Claude Code writes ONE JSONL line per content block of an assistant message and each repeats the same message-level usage, so summing lines over-counted tokens and cost ~1.5–2× on tool-heavy sessions) + stmts.setTokenUsage (server/db.js) — the EXACT counterpart of replaceTokenUsage, used only by reconcileTokens, because the high-water baseline_* columns make a downward CORRECTION impossible; the server runs that reconcile once in the background at the first start after upgrading (user_version 2, server/index.js) + v1.6.12 (2026-09-18) + the same three rules in skills/ship-feature-lane/SKILL.md (three scheduled touchpoints now: Stage 0 Q&A, Stage 0b plan approval, Stage 7 acceptance before the PR is published — the publish rule and the turn-liveness rule name them) + the default 8-node pipeline asking the user twice and looping on a NO-GO (skills/ship-feature/SKILL.md + server/data/pipelines/default.json: intake now DECLARES /superpowers:brainstorming and detects it, the plan phase blocks on an explicit approval, ship-feature gained the four sections that make a long run followable and affordable (Right-size the run — trivial work does not enter the pipeline and a small change skips the plan-debate/independent-test/gate agents, Dispatching a judgement — the fresh-context/foreground/never-fork/never-dispatch-what-a-command-answers contract written ONCE instead of re-argued per phase, Resuming after a compaction, and a per-phase Leave when:), review DECLARES the bare /code-review (Claude Code's own, no plugin: prefix — requiredSkillPlugins skips it, so it adds no install requirement) and runs it always and a domain reviewer only for the layers git diff --name-only shows, and gate requires the user's acceptance — a NO-GO re-declares implementing or plan instead of proceeding) + KEEP_RETIRED_ROOTS dropping to 1 (scripts/plugin-bootstrap.js) + lane removal being record-only (the remove branch in server/routes/lanes.js no longer calls removeWorktree/removeLaneData, so a worktree, its branch and its database survive forgetting a lane — see the rule under Lanes) + v1.6.10 (2026-09-17) + per-profile Claude quota (server/lib/cc-usage.js, POST /api/cc-config/profiles/usage, the Check quota button in the Profiles modal) — a POST because a saved profile's token is normally expired, so answering refreshes it and the refresh ROTATES the credentials written back to disk, which is also why it never runs on a poll; the LIVE account is the exception and is read from .credentials.json via cc-profiles.liveCredentialsFor, never refreshed, because the CLI has already retired the refresh token its profile copy holds + the settings patch that changes nothing now writing nothing (patchSettings in server/lib/cc-mutate.js, plus .claude/.cc-config-backups/ in a lane's git exclude) + the Add-lane modal closing itself on a clean setup and offering a real Close when a step failed + a lane's router pick no longer landing on whichever lane the user switched to mid-apply (LaneRouterRow), nor reverting on the next lane switch — POST /api/lanes/:id/router/apply now broadcastLanes like every other mutating lane route, so the client's cached row stops disagreeing with what was written + a run being opened by a DECLARATION only — a backwards DETECTION now continues the run in flight (appendStageEvent in server/lib/lanes.js), because detection bounces within one feature and runs is what every pass rate on the Workflows page is divided by + SETTLE_MS in server/lib/cc-reload.js — the two reload commands are typed a second apart, and reloadOpenSessions is now async, because Claude Code swallows the Enter after a slash command and back-to-back sends merged into one composer line + v1.6.0 (2026-09-14) + the lane strip's drag/Alt-arrow reordering and inline rename (client/src/lib/laneOrderStorage.ts, LaneStripCard's index/onReorder props, the Workspace detail header's rename field, and normalizeTitle/bindable in server/lib/lanes.js) + the uncommitted-files hover panel (LaneGitFilesPanel, now in LaneGitHeader, and files from GET /api/lanes/:id/git now being {path,status,added,deleted} records off one extra git diff --numstat HEAD) + AI Router profiles (server/lib/ai-routers.js, server/lib/router-apply.js, the Settings tab /settings?tool=ai-router, its Test button / POST /api/ai-routers/test, the per-lane router select — labelled Model, LaneRouterRow exported from LaneCard and rendered in the Workspace lane-detail header row — and lanes.router_profile) + the bootstrap's superseded-build guard and plugin-cache pruner (supersededBy/pruneRetiredPluginRoots in scripts/plugin-bootstrap.js, unitRoot in scripts/systemd-service.js) + v1.5.6 (2026-09-11) + the systemd handover rule below (scripts/systemd-service.js's writeUserService/restartUserService split and plugin-bootstrap.js's deferred pendingSystemdRestart) + eb63569 (2026-09-10) + release-based update detection (server/lib/update-check.js reads the forge releases API — a checkout via its origin remote, a plugin-cache install via the marketplace manifest on disk, since an archive-sourced cache dir has no .git; a plugin_managed install self-updates via the detached plugin-update chain) + the install-wide Update all batch and session reload (server/lib/cc-update-all.js, server/lib/cc-reload.js)