- GET /api/workflows: one LEAD() window pass in a read worker (read-pool.js,
workflow-stats.js); idx_events_session restored, idx_events_tool added
- compat-sqlite gains function/pluck/readonly so a plugin install without a
better-sqlite3 build boots again; antigravity-usage uses db.js's Database
- install-hooks adds no checkout hooks while the agenthub plugin is enabled
(events were doubled)
- terminal attach runs the live tmux server's own binary; the pane scrollbar
gets a visible 2-column style, and claude panes start with
CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1 so tmux keeps history to scroll
- Codex sessions' conversation in GET /api/sessions/:id/transcript
- Codex/Antigravity sessions claimed by their workspace without hooks
- UI-audit follow-ups, docs and OpenAPI
New params and errors (agents session_ids, sessions awaiting/from/to,
stats tool counts, event labels, cleanup dry_run/INVALID_PURGE_DAYS, lane
start slot auto/ESESSIONLIVE/EUNSUPPORTED), terminal and pane shortcuts,
the shared UI primitives, accent vs accent-solid, lazy routes and the
Settings groups. openapi.yaml regenerated.
The UI audit changed roles, aria attributes, theme tokens and layout on
most screens; the snapshots are regenerated after review. General split
into General / Data / About, so the moved content gets its own two
snapshots.
- A resume of a session a live pane already holds is refused (409
ESESSIONLIVE with error.run_id) and the client opens that pane, pasting
any typed message there; catalog tools refuse resumeSessionId
(EUNSUPPORTED) instead of silently starting fresh.
- POST /api/lanes/:id/start takes slot: "auto": the server picks the free
tab under the lane lock (components/lanes/startSession.ts on the client,
kept out of WorkspaceHome so the sidebar does not pull react-markdown
into the main bundle).
- Unsaved whole-file edits are guarded on every exit (Back, another file,
tab switch, close, lane/session change, beforeunload).
- Closing an AI tab asks first; hiding a pane has its own icon. Tabs and
pane headers show the session name, a state dot, rename by double-click.
- Terminal: Ctrl+M toggles Tab-moves-focus (no keyboard trap), Cmd+F /
Ctrl+Shift+F find bar, clickable links (noopener), a write-only OSC 52
clipboard, a Reconnecting overlay, debounced resizes, optional screen
reader mode. Alt+1-4 / Alt+\ / Alt+W focus, split and hide panes.
- Changes panel: draggable splitter, wrapping git bar, history paging,
disarming discard/undo/abort, inline diff comments pasted only into a
visible pane; drag MIME guard on sidebar rows; IME-safe Enter; Show
more in the sidebar and Recents; idle dots readable with sr-only text.
- Adds @xterm/addon-clipboard, @xterm/addon-search, @xterm/addon-web-links.
- EditorModal loads once per path (a parent re-render no longer reloads the
file over the user's typing), refuses Save after a failed load, opens a
truncated (>256 KB) file read-only, and asks before overwriting a file
that changed on disk or could not be re-read.
- Settings/Hooks editors unset only keys from the snapshot taken at Edit,
warn when the file changed underneath, keep a renamed key's value, and
refuse a new key named like a redacted one (env) or a read-only hook
event, which used to replace the secret/hooks.
- Project scope shows its resolved folder and is disabled inside the
plugin cache. Account-profile, pricing-row and pet deletes confirm;
two-click actions disarm after 3 s; unsaved drafts ask before closing.
- Search indexes every Claude Code / Codex sub-tab, opens it via ?tab= and
scrolls to + highlights the row; the active config tab is in the URL.
- General splits into General / Data / About; Pricing moves to a Cost
group with one shared load; config tabs group as Content / Extensions /
Config with arrow-key roving; Promise.allSettled per tab.
- Data: browser-preferences export/import (allowlisted keys), per-section
reset, housekeeping dry-run first, clear-all behind a typed phrase.
Server: purge days must be a whole number >= 1 (INVALID_PURGE_DAYS) and
dry_run returns exactly what the real run would delete.
- Alerts can be edited; webhooks/alerts/remote-source failures toast;
copies go through copyText; modals get dialog roles and useDialog.
- Sagi: Ctrl/Cmd+B only when shown and not typing, default spot clear of
row controls, arrow keys move it, reduced motion honoured; the dead
/run launch link goes to the workspace.
- Workspace, SettingsHub, SessionDetail and the Workflow sub-tab are lazy
(main bundle 2,166 kB -> ~920 kB), wrapped in RouteErrorBoundary: a stale
chunk after a self-update reloads once (vite:preloadError too), any other
error shows a Reload panel instead of a blank page.
- Live refresh: lib/throttle.ts + hooks/useLiveRefresh.ts replace trailing
debounces that froze under a steady event stream; refresh skips hidden
tabs and refetches after a WebSocket reconnect. Monitor loads only while
shown, with one batched GET /api/agents?session_ids= instead of N+1; a
failing endpoint is an inline error, not a blank Dashboard.
- NeedsYouStrip on Monitor and above Analytics. Tabs, filters, page, sort
and search live in the URL (Dashboard, Sessions, Activity, SessionDetail).
- Sessions: tool / workspace / date-range filters, CSV/JSON export
(formula-neutralised), focusable row links, Open in workspace (the dead
/run links are gone), stale responses dropped, error + Retry.
- Fixes: "NaNs" durations, 30/7-day charts skipping idle days, weekday
labels west of UTC (now averages), cache hit rate denominator, billable
tokens vs cache reads, exact tool-call counts, "To" date = end of day,
invalid ?from= no longer throws, recent list no longer shrinks to 15,
stable message keys, transcript switch races.
- Light-theme-unreadable bg-black code panels and dark-only DAG fills use
tokens; status configs and chart colours use theme tokens; Health tab
and other literals translated.
- Server (additive): GET /api/agents?session_ids=, GET /api/sessions
awaiting=1&from=&to=, stats tool_call_count/distinct_tool_count, event
rows carry session/agent labels.
- Contrast: --fg-muted reaches 4.5:1 on every surface in all nine themes
(dark default was 2.55:1); status chips and accent text clear 4.5:1 on
their own tints. New --accent-solid(/-hover) + tailwind `accent-solid`
is the fill behind white text (.btn-primary, Switch on, Checkbox tick);
--accent is now a text colour. Switch off / unchecked Checkbox get a
>=3:1 edge.
- <html lang> follows the chosen language; a global prefers-reduced-motion
rule; index.html body uses a token instead of text-gray-200.
- Layout: the sidebar starts collapsed below lg and becomes an off-canvas
drawer below md (menu button, backdrop, Escape, useDialog).
- ContextMenu: focus moves in, arrows/Home/End, Escape/Tab close, focus
returns, a keyboard-opened menu anchors to its target. Select is a real
listbox button (aria-expanded, activedescendant, type-ahead). PageTabs
implements APG tabs with a tabpanel. New .btn-secondary/.btn-danger/
.btn-icon and a .select class for native selects, all with focus rings.
- StatusBar announces connection and "N need you" politely; Sidebar's
connection modal uses useDialog; EmptyState/Skeleton/AiToolIcon a11y.
- api.ts request(): a non-JSON 200 throws a translated EBADRESPONSE
instead of "Unexpected token '<'".
One implementation each, for every page to use instead of its own copy:
- lib/toast.ts + components/Toaster.tsx: toast.success|error|info from
anywhere, rendered in polite/assertive live regions; an error stays until
dismissed, a success fades, a repeat collapses.
- lib/confirm.ts + components/ConfirmHost.tsx: await confirmDialog({...})
and confirmDiscard() through the themed ConfirmModal, replacing
window.confirm and the actions that asked nothing.
- hooks/useDialog.ts: the modal focus contract (focus in, Tab trapped,
Escape, focus restored), extracted from ConfirmModal; dialogs stack so
only the top one answers keys. ConfirmModal now uses it.
- hooks/useArmed.ts: two-click confirmation that disarms after 3 s.
- hooks/useUnsavedGuard.ts: beforeunload while a draft is dirty.
- lib/clipboard.ts copyText: Clipboard API with an execCommand fallback for
non-secure origins, outcome toasted.
- lib/keys.ts isSubmitEnter: ignores the Enter that confirms an IME
candidate (Vietnamese Telex, CJK).
Analytics parity, phase 1 (data hygiene), from the analytics-parity session.
events.data is now stored through the SQL function trim_event_data(?)
(server/lib/event-payload.js, registered in server/db.js) on every event
INSERT and in the duplicate guard: Edit/MultiEdit/NotebookEdit/Write drop
the whole-file mirrors (originalFile, oldString/newString, Write content),
Read drops file.content/base64, and any string in tool_input/tool_response
over 8 KB keeps a 6 KB head + 2 KB tail. structuredPatch and every field
outside those two stay whole; _trimmed names what was dropped. The full
text stays in the transcript and its snapshot. Rows stored before this are
trimmed once in the background (sweepEventPayloads, 200 rows per write
transaction, user_version 4). No VACUUM, no row pruned.
The session list now prices every token column /api/pricing/cost/:id does
(speed, geo, tier, the 1h cache write, server-tool fees) through one
tokensForSessions helper, so a row's cost equals the session's own.
A pane's split right/down menus offered only New terminal. They now list
the same AI tools as the tab bar's + menu (newTabTools/newTabBody,
shared from ConsoleTabs), started bare in the first free slot on that
side of the pane.
The tab bar's Tools menu is gone: its readme run hit 409 ERUNLIVE
whenever slot 0 was live (always, in the session view) and depended on
a personal skill; MCP sync and the dev-sync check served the dormant
ship-feature-lane pipeline. The sidebar row menu's readme item goes too.
The routes stay, reached by `agenthub lanes mcp sync` and
`agenthub lanes sync-base`.
Also: the home's Memory header and file tabs no longer shrink (a long
file squeezed the tab row and clipped it).
Settings -> Pet (/settings/pet, PetSettings.tsx): show/hide, speech
bubbles, size with a live preview, and the pet grid - Sagi, featured
codex-pets.net pets (downloaded on first pick), your own (deletable) -
with image upload and .codex-pet.zip import. The Sagi block leaves
General; the mascot menu's Pet settings opens the page. PetMenu and the
page share components/Sagi/usePets.ts.
Right-click menus: one portal shell (components/ContextMenu.tsx) for every
item with actions - sidebar/Recents/archived session rows (SessionMenu),
console tabs, pane headers, the Changes panel's files/tree/commits, the
workspace home, sessions outside a workspace (SessionRowMenu: Sessions
table, Dashboard, Activity, Workflow runs), prompt images, every Settings
list and every Claude Code/Codex config list (FileItemMenu). Buttons stay;
a delete keeps its button's confirm; the shell stops portal clicks from
reaching the card that opened it.
Status bar: Workspace $ and Session $ on the Workspace page, from
?lane= on GET /api/pricing/cost and the focused/replayed session's cost.
Database, no data dropped: synchronous=NORMAL, journal_size_limit 64 MB,
hourly PASSIVE checkpoint + PRAGMA optimize, idx_events_session_created
(replacing the redundant idx_events_session), watchdog reads
sessions.transcript_path, transcript parsed before the hook's write
transaction, GET /api/sessions/:id?events=0 for the web page (reloads only
on its own session, debounced), streamed export, transactional clear-data
and chunked cleanup, subagent import reads tool_use_ids once, runtime logs
rotated past 5 MB.
token_usage holds a session's cumulative totals with no time, so
/api/pricing/cost booked a multi-day session's whole cost to its start
date and the status bar's Today read ~$0. TranscriptCache now also splits
usage by each message's UTC hour (tokensByHour); the hook writer stores it
in the new token_usage_hourly table (MAX per hour) and the server
back-fills existing sessions at start. The daily split books each total
over its hourly rows' local days and the rest to the start date, so the
days still add up to the total.
The mascot can wear a pet. Right-click it for Hide pet, Size (48-250
px), Choose pet (Sagi, featured, your own - deletable), Upload your
own..., Import .codex-pet bundle..., Find more on codex-pets.net and
Pet settings.... The worn pet and its size are viewer prefs
(agenthub.pet, agenthub.pet.size); the pets live on the server under
<data dir>/pets/<id>/ (server/lib/pets.js, /api/pets).
A Codex pet is the .codex-pet.zip format: pet.json + a 1536x1872
spritesheet.webp, 8x9 frames of 192x208, one row per state.
PetSprite.tsx plays the row of the mascot's mood; an uploaded image
bobs. Only the two bundle files are read, sizes are checked from the
zip directory before inflating, the sheet's WebP header must match,
uploads are raster only (no SVG), ids are made by the server, every
write is same-origin.
The four featured pets (Goku, Bac Ho, Border Collie, 11) are
codex-pets.net uploads with no redistribution licence, so none is
bundled: a pick downloads it from codex-pets.net by a fixed id.
A tab opened bare from + had no session row until its first hook, and
the sidebar re-read its live panes only when a workspace's status or
session moved, which a tab in slots 1-3 never does: the new tab showed
nowhere. The sidebar now lists a live AI tab whose session has no row
yet as "New session" straight from the run (its session id is known
from the start) and follows the Workspace page's own run-list poll
(LIVE_RUNS_EVENT).
A sidebar workspace row now opens the workspace's live tabs on the pane
it last focused, and its home only when it has none; a Home button at
the left end of the tab bar reaches the home of a workspace whose tabs
keep running.
The console splits into up to four panes: a binary split tree of tmux
slots (client/src/lib/paneLayout.ts) drawn by PaneGrid.tsx, every pane an
absolutely placed sibling keyed by slot so splitting, swapping, resizing
or closing never remounts a live terminal. Ctrl/Cmd+click on a tab, a +
item or a sidebar session row opens it beside the focused pane (router
state, never the URL); a pane header drags onto another pane's edge to
split there or onto its middle to swap, picks another live session,
splits right/down (a not-shown session or a new terminal), or closes the
pane only. Dividers drag or take arrow keys. ?run= is the focused pane:
every layout change goes through change(), which pushes the URL with
focusOnly instead of an effect watching the focus (that ping-ponged with
open requests). The layout is kept per workspace in localStorage.
Only the focused pane's terminal takes the keyboard when it appears, and
the keyboard follows a focus change. A pane closes when its terminal
reports the exit; the run list prunes only a remembered layout on a
workspace's first read and a just-opened slot that never got listed, so
one empty read cannot wipe a split. A run ending on the home or a replay
closes its pane without leaving the page.
The session view now fills the page edge to edge: no page padding, the
tab bar and the right panel's activity bar one h-10 strip, and the tab
bar, pane headers and right panel on the sidebar/status bar colour
(surface-1) around terminals on the page colour.
The session section and the Files/Changes panel drop their cards; one
vertical divider separates them (none at full width), and the collapsed
rail keeps only that divider. The terminal goes back to the page colour
(--surface-0), and the panel's two sticky bars cover scrolled content in
the page colour instead of the card's.
Inside, the git row loses its bordered box for a bottom divider, and the
ended-session prompt box drops its own fill and shadow.
The tab bar and terminal lose their bordered box: the bar keeps one
divider with an accent underline on the active tab, and the terminal is
painted in the card's colour (--surface-3) instead of the page's.
That made a long-standing overflow visible: xterm re-measures its cell
only on a font option change or a resize, so a webfont landing after
open left the fit on the fallback font's cell and the rows/cols spilled
out of the pane. Re-set the family and refit on document.fonts
loadingdone.
The rule hiding .xterm-viewport's scrollbar lived in @layer utilities,
where Tailwind purges any class that never appears in the source. xterm
creates that element at runtime, so the rule never shipped and an empty
scrollbar groove showed beside every terminal. Move it out of the layer.
The status/name/back-arrow row is gone; Tools sits at the tab bar's right
end and the pipeline/feature pickers show only with a template.
LaneConsolePane is reduced to the terminal (no title, no Active runs, no
New run): RunHistory.tsx, api.run.history and the page's active-sessions
poll are removed.
The 1/2/4 split view becomes a tab bar (ConsoleTabs.tsx): one tab per live
tmux slot (0-3) of the workspace, the active one being ?run=, every live tab's
pane kept mounted and hidden with CSS. The + menu opens a new tab in the first
free slot: New terminal (a plain shell - shell: true on POST
/api/lanes/:id/start, spawnShell, tool 'shell', no run_id/status change), any
installed enabled AI tool started bare, or Settings -> Agents. A tab's x stops
its session. splitViewStorage, &pane= and the sidebar's Ctrl/Cmd+click are gone.
The workspace home's composer takes ONE Model chip (ModelMenu: model list
plus Effort) and moves AI Router / Permission / Local-New worktree into the
row under it; Settings drops the General-page cost card and export row,
PricingSettings and the empty states are compacted for settings panels.
- A session picks its own AI tool: `tool` on POST /api/lanes/:id/start
(a catalog id, known + enabled, else 400 EBADTOOL/EDISABLED); a resume
keeps the tool its session ran on (`sessions.tool`). The workspace's
`lanes.tool` is now only the default.
- The workspace home's prompt box is the one place a session starts. Its
fields run in order: AI → AI Router (Native or a profile) → Model (the
tool's own list, or the router's models) → Permission (Claude mode /
Codex sandbox+approval / catalog bypass) → Effort.
- The pane's run form (RunSetup) is gone: New run, a single pane's exit and
an empty split pane's New session all go to the home. RunSetup.tsx keeps
PromptEditor, CwdAutocomplete and ModelPicker (new `choices` prop).
- The tool icon moved from the sidebar's workspace row to each session row
(right edge, `session.tool`); the split-pane header shows the run's tool.
- An ended session opens in its workspace (`&session=`, SessionReplay.tsx):
its conversation read-only plus a prompt box; only a SENT message resumes
it (resumeSessionId + prompt, first free slot). The console is only
hidden behind it.
- Analyst button beside Files/Changes in the right panel opens
/sessions/:id for the opened session.
- Gemini CLI ships its own icon (client/public/ai-tools/gemini.svg, from
zonalogo.com).
- RouterPicker always shows (Native disabled for tools without router
support); useAiRouters() exported.
- Tests: server start-with-tool cases; SessionReplay, home field order/tool/
router models, empty pane, Analyst; dropped the cwd-field suites
(Run.defaultCwd, Workspace.laneCwd). Docs: CLAUDE.md rules, API.md,
LANES.md, README, ARCHITECTURE, INSTALL.
AI Router is a session's choice, not a workspace's:
- an AI Router select beside the model picker in the workspace home and a
pane's run form (RouterPicker.tsx; hidden off Claude/Codex or with only
Native; last pick remembered in agenthub.session.router)
- `router` on POST /api/lanes/:id/start and POST /api/run; Claude gets
--settings=<data dir>/router-settings/<name>.json (0600, outranks project
and user settings, key never in argv), Codex -p <name> + key via tmux -e
- dashboard_runs.router; a resume naming none goes back to it
- profile PATCH rewrites the session files, DELETE removes them
- gone: POST /api/lanes/:id/router/apply, LaneRouterRow, the Add-workspace
router radio, the claude-/codex-home `routers` answers; clearLaneRouters
takes the old keys out of each workspace's settings.local.json at start
Sidebar: Settings pinned to the foot, the collapse toggle beside it (stacked
on the rail) instead of in the brand's corner.
Icons: shipped SVG marks for claude, codex, openclaw and hermes (a
black/white pair swapped by theme) in client/public/ai-tools/, used by
AiToolIcon before the server's favicon.
Desktop-parity phase 6, following Claude Desktop:
- the workspace home's Worktree checkbox starts the pane with
`claude --worktree=<name>`, so Claude Code makes, populates
(.worktreeinclude, WorktreeCreate hook) and cleans up
<repo>/.claude/worktrees/<name>; AgentHub only excludes
/.claude/worktrees/ at the repo root (Claude does not)
- server/lib/session-worktree.js resolves a session's worktree from the
session row only (owned by the workspace, registered by git worktree
list); every lane git route takes ?session= and answers ENOWORKTREE
instead of falling back to the main folder
- the session view's Changes panel works in the opened run's worktree
(dashboard_runs.worktree, GitTarget in the client api)
- Resume/Fork start in the session's worktree; a gone one is ENOWORKTREE
- Archive of a worktree session removes the worktree after asking:
git worktree remove without --force, git branch -d (unmerged work keeps
its branch), a lock held by a live pid refused, a stale one cleared
- listWorktrees reads `locked <reason>` locks
- tests scrub GIT_* env so the pre-commit suite cannot act on this repo
Workspace sessions (docs/plans/desktop-parity, phases 1-5 and 7):
- sessions.lane_id: a workspace lists only the sessions its panes held,
stamped by bindRunSession (first writer wins, nested `claude -p`
excluded); GET /api/sessions?lane=&archived=
- session row state dot, double-click rename, archive (sessions.archived_at,
hides only); the activity bar's duplicate Sessions tab removed
- Fork beside Resume (--fork-session, Claude only), Ctrl/Cmd+click a live
session opens it in a second pane, a diff line's comment is pasted into
the workspace's last-focused pane (lib/panePaste.ts)
dashboard_runs stability:
- a pane's previous run is archived as <pane>@<started_at> instead of being
overwritten by INSERT OR REPLACE
- a run is flagged abandoned only once its tmux session is gone, at boot and
on the run-heal beat (panes outlive a server restart)
UI:
- sidebar: no footer, language or theme block; small collapse button in the
brand corner; version + update check in the connection details modal, a
status-bar dot while a release is out; language in Settings -> General
- one Switch component for every switch, the themed Checkbox for every
checkbox (no native checkboxes)
Also: AI Tools catalog trimmed to 10 built-ins (custom-file prompt modes
tested through fixtures), and the workspace plans under docs/plans/ (the
multi-user server plan lives in the AgentHub-Server repo).
- Sidebar: a tool filter (shown once workspaces of two tools exist, kept in
agenthub.sidebar.projects → tool; reordering under it keeps the hidden
workspaces' places), and search that also finds a workspace by one of its
session names, showing only the matching sessions.
- Workspace home: the repository it belongs to (root, main branch, worktree
count) under the title, and each recent's cost.
- Transcript page: an explicit Resume for a Claude session started in a
workspace's own folder (SessionResumeButton). It warns that a resume re-reads
the whole transcript, re-reads the live panes, then starts in the first free
pane and opens it; a live session offers Open pane instead.
/workspace?lane=<id> opens on the workspace's home (WorkspaceHome): a prompt
box whose Start opens a new session in the first free pane slot, Recents (a
live session attaches its pane, any other opens its transcript, nothing
resumes), and Instructions / Memory / Context / Folder. Instructions live in
lanes.instructions, extra folders and uploaded files in the new lane_sources
table (copies under <data dir>/workspace-context/<lane id>/); spawnRun reads
them from the lane's rows and hands Claude --add-dir + an appended system
prompt file, Codex --add-dir + developer_instructions. Memory is read-only.
Every dashboard pane carries AGENTHUB_RUN_ID; hooks forward it and the run
records which session the pane holds (a Claude pane follows /clear and
/resume but not a nested claude -p), so the sidebar and notifications open
the right pane (&run=). Settings loses its Projects group; Worktrees now
decides which worktrees Add folder suggests.
The sidebar's project tree becomes a flat workspace list (WorkspaceTree):
one row per workspace in the stored order, each expanding to its sessions.
A session that still has a live pane opens /workspace?lane=<id>&run=<runId>,
which only attaches pane 0 to that tmux session; any other session opens
its read-only transcript. Nothing in the sidebar starts or resumes a run.
The workflow analytics now render under the Analytics tab's Workflow
sub-tab; ?tab= and ?sub= deep-link the Dashboard, /workflows redirects there,
and the sidebar drops its Workflows item.
Two optional named themes from Orca's neutral palette join the Appearance
picker (Orca Dark keeps a blue accent so white-on-accent buttons stay
legible; Orca Light's status colours are darkened to clear 4.5:1 on their
chips). A shown notification can play a sound — None (default), Chime, Pop
or Alert — chosen globally and per notification type in Settings →
Notifications; the WAVs are generated by scripts/gen-notification-sounds.js.
Settings moves from a tab bar to /settings/<section>: a left nav grouped
General / Agents / Routing / Git / Projects (a select on narrow screens), a
search box over every section (Ctrl/Cmd+F, Enter opens the first match), and
row primitives. Each AI tool has its page under agents/<id>, the AI Tools
modal becomes the Agents page, Pricing shows every vendor, Git → Worktrees
sets which untracked worktrees the sidebar lists, and each project gets a
page with its own worktree override and Forget all. ?tool= and #ai-tools
links redirect to the path form.
The Workspace page's lane strip moves into the sidebar as a project tree:
workspaces grouped by git repository (new GET /api/projects, cached in
memory, POST /api/projects/refresh), untracked worktrees with Adopt, inline
rename, drag/Alt+arrow ordering and a right-click row menu. The selected
workspace is the URL's ?lane=, one shared lane list feeds the tree, the new
bottom status bar and the page. The Changes panel gains an activity bar
(Files / Changes / Sessions, with resume), and split panes get a header.
Copy-only rename (workspace redesign, phase 0). The word a user reads
changes — "Lane" → "Workspace", "Add lane" → "Add workspace" — in both
locales, in agenthub CLI prose and help, in the server's error messages
and in the docs. Every identifier stays: /api/lanes, `agenthub lanes`,
--lane, AGENTHUB_LANE_ID, error codes, tmux names, DB tables, file,
component and i18n key names. docs/LANES.md keeps its name, retitled
"Workspaces (lanes)".
Pricing: the LiteLLM sync now adds every current Claude / GPT (>= 5) /
Gemini (>= 3) chat model before it is used and keeps its own rows
(model_pricing.source = 'sync', the Auto badge) at the published rate. A
user's row (any PUT) and a deleted pattern (model_pricing_dismissed) are
never touched; a deleted built-in row is not re-seeded at the next start; a
reset re-applies the list at once; retired Claude models are priced only
once used.
Models: GET /api/ai-tools/:id/models reads each tool's own list (Codex's
models_cache.json, Claude's aliases named after the newest version plus
priced versions, a catalog tool's vendor rows plus the models its sessions
used). A catalog `model: {flag, vendor?}` field passes `<flag> <id>`
(validated: never a flag or shell text); a per-tool default model starts
each run, and a resume keeps the session's own model.
Settings: every installed, enabled catalog tool without its own tab gets
one (AiToolConfig.tsx): path, homepage, Update, sessions/cost/lanes,
default model, vendor pricing, and the config files its release catalog
lists, edited over the opened sha, syntax-checked, backed up, written
atomically; credential-looking names refused. Antigravity keeps its hooks
card there.
Lanes: a just-started run no longer reads "dead" (age counts the run's
start while the old session is still bound); a second dashboard on the
same database re-broadcasts the lanes the other changed (PRAGMA
data_version, every 2 s); a lane start with a bad model answers 400.
DB: every migration runs under a cross-process lock (<db>-migrate.lock), so
two dashboards started at once on a fresh database both come up.
- AI Tools moves from a Settings section into a modal behind a button at the
Settings tab bar's right edge (AiToolsModal, `actions` on PageTabs,
/settings#ai-tools); rows get an enable switch, Default badge, and an
Update button for every tool whose catalog names an updater (no version
lookup on open); not-installed tools fold behind a disclosure, a finished
job's log folds to one line; shared AiToolIcon
- codex updates through its own `codex update`, openclaw/hermes gain
`update --yes`; update-all runs every installed updater and skips an npm
one only when npm does not manage the binary
- Add-lane tool picker is one button showing the chosen tool that opens the
enabled tools as buttons
- Claude Code / Codex AgentHub sub-tab folded into Overview (home editor,
hooks, pricing); the path list drops CLAUDE_HOME/CODEX_HOME;
HomeDirSetting shows the saved value only while the field differs
- Preview/Raw toggle for markdown files in the config FileViewer and the
Changes panel's whole-file view (MarkdownPreview: react-markdown +
remark-gfm, raw HTML never run, frontmatter as a code block)
- docs and the CLAUDE.md marker updated
A `claude plugin update` run from a terminal (or with only the Claude desktop
app open) unpacks the new build and starts nothing: no SessionStart fires, so
the dashboard kept running the old cache dir across reboots, and with the
forge unreachable the sidebar read "Up to date" (seen on a machine stuck at
1.8.10).
- pluginStatus asks supersededBy for its own root: a recorded sibling install
sets `installed_not_running: {root, version}` and `update_available`, with
no network needed
- the sidebar reads "<version> installed, not running"; the update modal keys
on it, so Update now is offered even with no release tag or sha
- plugin-update-helper hands over to the recorded install first, the newest
sibling dir only as a fallback
- a plugin install whose release lookup failed reads "Could not check for
updates" instead of "Up to date"
- docs/PLUGINS.md: the case and the manual hand-over command
Part B: Model Pricing, Hook Configuration and CLAUDE_HOME move from
Settings → Dashboard to the Claude Code tab's AgentHub sub-tab
(ClaudeAgentHubSettings.tsx); CODEX_HOME is editable in Codex → AgentHub.
A home that is or contains the OS home is refused (EBADHOME).
Settings persistence: dashboard-saved settings go to
~/.config/agenthub/settings.env (AGENTHUB_SETTINGS_FILE), outside the plugin
cache an update wipes. Load order: real env > the checkout's .env > that file.
The first CLAUDE_HOME change pins DASHBOARD_DATA_DIR so the DB stays put.
C1: codex-usage.js reads Codex rollout journals into sessions (new column
tool) and token_usage, tracked per file in codex_rollouts; forks deduped by
total vector, sub-agent threads folded into the root; pricing-sync also
prices OpenAI models; PricingSettings with a vendor filter.
C2: codex-hooks.js installs AgentHub's hooks into $CODEX_HOME/hooks.json on
a click, through a stable forwarder; Codex trust is never written. The hook
route creates tool='codex' sessions and folds sub-agent hooks into the root.
D: antigravity-usage.js reads agy's per-conversation SQLite files (tokens
from ModelUsageStats) into tool='antigravity' sessions; antigravity-hooks.js
installs the named hook `agenthub` (PreInvocation, PostToolUse, Stop — never
PreToolUse) into ~/.gemini/config/hooks.json; Settings → Antigravity tab;
Gemini pricing.
Whole-change review fixes: sameOriginGuard on both home PUTs, routers.missing
on a CLAUDE_HOME change, reset-pricing scope, GET /api/sessions?tool=,
clear-data covering the new tables, Codex child-thread events moving no
status, hooks owning status only while current, EFOREIGN for a forwarder
that is not ours, agy quiet window and turn rules, AGENTHUB_SETTINGS_FILE
honoured from .env.
Docs, OpenAPI and the CLAUDE.md marker updated. Server 2141, client 779.
Settings → Dashboard gains an AI Tools section (installed tools with
Enabled/Disabled and Set default, available ones with a homepage link,
Refresh). useAiTools shares one catalog copy across the app and replaces
LANE_TOOL_LABEL. The Add-lane modal picks Default (<tool>) or Other…; the
router row reads Native (<tool>) and disables AI Router profiles for tools
that cannot use one. The run form gives catalog tools resume and a red
per-run Bypass permissions switch (Codex too). Claude Code / Codex settings
tabs hide while that tool is disabled. The Workspace probes each lane tool's
CLI fresh.
Docs: CLAUDE.md tool/bypass rule, AGENTS.md, LANES.md, API.md, CLI.md,
server/README.md, OpenAPI fragment + regenerated openapi.yaml.
server/lib/ai-tools.js loads server/data/ai-tools/*.json (38 tools, data from
Orca, MIT) plus DASHBOARD_AI_TOOLS_DIR, detects each CLI by walking PATH, and
keeps enable/default in <data dir>/ai-tools.json. shell-path.js merges the
login shell's PATH at startup and on Refresh. /api/ai-tools lists, refreshes
and toggles; /api/run/binary resolves through the catalog.
Lanes accept any catalog id (EDISABLED for a disabled one on creation, ENOTOOL
at start once a tool left the catalog). pty-run keeps the Claude/Codex
builders and builds every other tool's argv from its catalog file, pasting an
after-start prompt through a tmux buffer; every pane gets the hydrated PATH.
A per-run bypass switch appends the catalog's bypass args (Codex included).
POST /api/run now forwards sandbox/approval/resumeLast, which it dropped.
agenthub lanes add gains --tool.
The plugin stops shipping its workflow layer:
- skills/ship-feature and skills/ship-feature-lane, the 18 stage/ship slash
commands (plugin.json keeps agenthub-doctor/update/open), and all four
agents/ (qc-local, senior-gate-reviewer, backend-reviewer, frontend-reviewer).
- CLI `agenthub skills install`, `agenthub lanes agents install`,
`agenthub ship-full|ship-quick`; routes POST /api/skills/install and
POST /api/lanes/:id/agents/install; the Settings button, the lane Tools
menu item and the Add-lane setup step that called them.
- The Superpowers auto-install: server/lib/skill-plugins.js, the bootstrap's
autoInstallSkillPlugins / skillPluginsSettled gate and the doctor row.
Pipelines are kept but dormant: server/data/pipelines/ is deleted, so no
template ships. `default` resolves to EMPTY_PIPELINE (no nodes, progress 0),
createLane still accepts it, POST /api/lanes/:id/stage answers 400
ENOPIPELINE, and the Workspace hides the stage timer, pickers, PipelineMap
and strip stage chip for a lane with no nodes. A template dropped into
DASHBOARD_PIPELINES_DIR turns it all back on. The server suite loads its
templates from server/__tests__/fixtures/pipelines/.
Docs: CLAUDE.md (marker, retired rules), README(.vi), INSTALL, ARCHITECTURE,
docs/LANES|PLUGINS|CLI|API, agenthub-introduction.html.
- antigravity: a third lane tool running Antigravity's `agy` CLI. `-i`
carries the first prompt, `--continue` resumes; `agy` has no model,
effort or permission flag, and `--dangerously-skip-permissions` is never
emitted. No pipeline, no agents install, and no AI Router (native only,
else EUNSUPPORTED). Add lane gains an Antigravity choice and the binary
probe maps it to `agy`.
- UI: the lane card no longer renders a frame holding only a "Codex" chip,
and the prompt box names the lane's CLI ("Ask Codex anything…"), via
LANE_TOOL_LABEL.
- runs: a pane that exits is released on a 10 s server beat
(healAllRunIds, DASHBOARD_RUN_HEAL_MS). Codex and Antigravity fire no
hook, so their lanes stayed "running" until something re-read them.
- codex: Start no longer sends resumeLast — only "Resume last session"
continues the previous thread.