Commit Graph

215 Commits

Author SHA1 Message Date
nntrivi2001 442f133930 docs: UI-audit fixes in CLAUDE.md, API, OpenAPI, LANES and READMEs
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.
2026-10-05 13:22:13 +07:00
nntrivi2001 efa417488a feat(workspace): no double resume, server-picked tabs, terminal a11y
- 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.
2026-10-05 13:22:12 +07:00
nntrivi2001 6715fbd971 feat(settings): data-safe editors, deep-linked search, Data/About/Cost pages
- 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.
2026-10-05 13:22:08 +07:00
nntrivi2001 03f1673fce feat(dashboard): lazy routes, Needs-you strip, URL state, exact numbers
- 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.
2026-10-05 13:22:04 +07:00
nntrivi2001 215cae7393 feat(db): trim stored hook payloads; price the session list like a session
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.
2026-10-05 13:13:02 +07:00
nntrivi2001 ad6e80192d feat: right-click menus everywhere, workspace/session cost, long-term DB safety
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.
2026-10-05 09:37:48 +07:00
nntrivi2001 65fe680d38 fix(pricing): book daily cost to the day tokens were spent
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.
2026-10-05 08:03:05 +07:00
nntrivi2001 ed1bf20e6b feat(sagi): custom pets — Codex pet bundles, images, featured downloads
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.
2026-10-03 16:25:42 +07:00
nntrivi2001 d9511ea446 feat(workspace): session view console as tabs, with New terminal
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.
2026-10-02 18:09:31 +07:00
nntrivi2001 da1b17778c fix(workspace): a new session shows in its workspace 2026-10-02 16:18:00 +07:00
nntrivi2001 4802684d2f feat(workspace): AI tool per session, one place to start a session, ended sessions open like Claude Desktop
- 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.
2026-10-02 15:29:38 +07:00
nntrivi2001 c507d37baf feat: AI Router per session; Settings at the sidebar's foot; shipped tool icons
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.
2026-10-02 14:19:02 +07:00
nntrivi2001 5084334d81 feat(workspace): a session in its own worktree, Claude's way
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
2026-10-02 13:22:29 +07:00
nntrivi2001 a6bd33bcff feat: workspaces own their sessions; desktop parity; sidebar and settings cleanup
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).
2026-10-02 13:19:50 +07:00
nntrivi2001 d0535af626 feat(ui): a Claude-Desktop-style home for every workspace
/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.
2026-10-02 01:10:58 +07:00
nntrivi2001 ba2c4e1914 feat(ui): sidebar project tree, status bar and a tabbed workbench for workspaces
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.
2026-10-01 16:33:25 +07:00
nntrivi2001 5b3884b309 feat(ui): call lanes "workspaces" in the UI, CLI output and docs
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)".
2026-10-01 16:03:24 +07:00
nntrivi2001 f887c16309 feat: model lists and prices that follow each AI tool, a Settings tab per tool, live lane status (#40)
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.
2026-10-01 13:39:17 +07:00
nntrivi2001 c5aba094bd feat(ui): AI Tools modal, one-button lane tool picker, AgentHub settings in Overview, markdown preview
- 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
2026-10-01 10:11:43 +07:00
nntrivi2001 058c574138 fix(updates): report a plugin build installed beside the running one, never "up to date" on a failed check
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
2026-10-01 10:09:12 +07:00
nntrivi2001 969b06253c feat: per-tool settings, persistent settings file, Codex and Antigravity sessions, tokens and hooks (#40 B, C1, C2, D)
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.
2026-09-29 16:09:19 +07:00
nntrivi2001 7ec5a7cd85 feat(ai-tools): install/update AI CLIs, remote catalog sync, cached favicons (#40 A2) 2026-09-29 08:25:37 +07:00
nntrivi2001 fcfebe44f1 feat(ai-tools): Settings → AI Tools, Default/Other lane picker, per-run bypass switch (#40 A1)
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.
2026-09-29 08:21:05 +07:00
nntrivi2001 77917203fd feat(ai-tools): catalog of AI coding CLIs, detection, enable/default, lanes for any tool (#40 A1)
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.
2026-09-29 08:18:40 +07:00
nntrivi2001 2b8f83d2dd refactor(lanes): drop the workflow skills and Superpowers link; pipelines go dormant
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.
2026-09-28 17:49:05 +07:00
nntrivi2001 9d096b61f7 feat(lanes): Antigravity lanes, release exited panes, fresh Codex starts
- 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.
2026-09-28 10:22:07 +07:00
nntrivi2001 952e8584f0 feat: per-change-block hunk actions (#50), codex lanes without a pipeline, macOS LaunchAgent
- git (#50): stage-hunk/discard-hunk take an optional `block` — one run of
  consecutive +/- lines inside an @@ hunk. `onlyBlock` (server/lib/git-diff.js)
  cuts git's fresh hunk down to it, folding the other blocks into the side
  being checked; LaneDiffView hangs one review bar per block (unified + split)
  and its arrows walk blocks.
- git exclude: entries AgentHub writes for a lane go to the COMMON git dir with
  the lane's path prefix (server/lib/git-exclude.js, used by router-apply and
  lane-mcp) — a linked worktree's own info/exclude is never read by git, and a
  subdir lane has no .git. MCP sync no longer fails with ENOTDIR on a worktree
  lane or creates a stray .git in a non-repo lane.
- codex lanes carry no pipeline: codex-ingest mirrors only idle/running,
  codex-nudge is removed, agents install writes nothing for codex, the
  Workspace hides stage/pipeline/feature pickers/PipelineMap for them, and
  every server start clears leftover codex stage data and the old AGENTS.md
  stage block.
- Changes panel: a whole file opens for reading and enters the editor on a
  click on its text (caret at the clicked line), not automatically.
- macOS: LaunchAgent start-at-login (scripts/launchd-service.js, picked by
  scripts/service-manager.js).
- Adds the AgentHub introduction slides and their image-embedding script.
2026-09-25 17:30:28 +07:00
nntrivi2001 1da46f10f4 feat(git): per-block conflict view with mine/theirs choices (#45)
Opening a conflicted file in the Changes panel now shows each conflict
block as its two sides, labelled from git's markers and tinted apart,
with Keep mine / Take theirs / Keep both per block (undoable, ^/v to
jump). Save and mark resolved writes the chosen text through the
whole-file editor's PUT /git/file (baseSha stale check) then runs
resolve done. The Merge conflicts group's buttons are text, and a
conflicted file is marked with a red ! in the Files tree.

Fix: ls-files --cached lists an unmerged path once per stage, so the
tree showed a conflicted file three times; lsFiles now dedupes.
2026-09-25 13:35:50 +07:00
nntrivi2001 40fe41bb77 fix(db): keep router_profile through the legacy lanes rebuild
The (cwd, tool) rebuild lists its columns by hand and omitted
router_profile, which its probe adds earlier in the same start — a legacy
DB lost the column (and every lane's router) until the next restart.
2026-09-25 11:30:10 +07:00
nntrivi2001 99a2a8277c fix(tmux,run,hooks): the four pre-existing bugs the #37 evaluation found
- tmux targets are exact (`=name`, `=name:` for panes/options): a bare
  `-t agenthub-lane-1` resolves by prefix to agenthub-lane-12 once lane 1
  is gone, so killing a finished lane could kill another.
- getRun/killRun refuse ids that are not `agenthub-lane-<n>`, so
  DELETE /api/run/main can no longer kill a user's own tmux session.
- router/apply reports restart_required only for a running pane; getRun
  returns a `gone` handle, so it was always true.
- needs_action is cleared only by the session that raised it (new nullable
  lanes.needs_action_session); a second session in the same cwd used to
  clear it on its second hook.
2026-09-25 11:30:02 +07:00
nntrivi2001 04b6b98d97 fix(git,macos,runs): close the review findings on #22, #34 and #38
- validateRemoteUrl checks the literal scheme prefix git will receive and
  refuses %/backslash in the authority (WHATWG normalises https:/host and
  percent-escapes that git reads differently); route test for EBADURL.
- Parent-session scrub also drops AI_AGENT, and GIT_EDITOR when it is
  Claude Code's `true`.
- canonicalCwd resolves a not-yet-existing path through its deepest
  existing ancestor, so a symlinked parent still yields one lane.
- Note the clock-step ceiling on liveServers' boot-time filter.
- Docs/header wording: the Active runs count is lane-scoped.
2026-09-25 11:26:31 +07:00
nntrivi2001 478418f602 fix(macos): scrub parent-session env, survive node upgrades and reboots, store lanes by real path (#22)
What broke (code audit, untested on a Mac):
- Without systemd the server is always spawned from inside a Claude Code
  session, so it inherited CLAUDECODE, CLAUDE_CODE_SESSION_ID,
  CLAUDE_PLUGIN_ROOT, ... and tmux handed them to every lane pane (a codex
  pane even resolved as `claude` in `agenthub`'s harness check).
- The ~/.local/bin/agenthub launcher pinned a versioned node path that
  `brew upgrade`/nvm deletes, leaving the CLI dead until the next bootstrap.
- A discovery entry left over from before a reboot could name a recycled pid,
  so the bootstrap's fast path believed a dashboard was live and
  stopDashboard could signal an unrelated process.
- Lanes were stored under the typed cwd; macOS reports /private/tmp and the
  on-disk case, so sessions never bound to a /tmp or differently-cased lane.

What changed:
- server/index.js deletes a DENYLIST of parent-session variables at startup;
  user config (CLAUDE_CONFIG_DIR, CLAUDE_CODE_USE_BEDROCK, ANTHROPIC_*) stays.
- linkCli's launcher falls back to `node` on PATH when the pinned one is gone.
- liveServers ignores entries whose startedAt predates this boot (60 s margin).
- createLane and POST /api/lanes/ensure store/look up fs.realpathSync.native
  of an existing cwd; the liveness reaper also tries the realpath. Existing
  rows are untouched.
- session-liveness gains an exec seam so the darwin ps+lsof branch is tested.
- plugin-doctor names ~/.zshrc on darwin; INSTALL.md gains a macOS section
  with the Mac verification checklist, mirrored in both READMEs and PLUGINS.md.
No launchd, no macOptionIsMeta, engines stays >=20.
2026-09-25 11:23:59 +07:00
nntrivi2001 aff79a787c feat(lanes,git): publish a repo with no remote, and read a repo with no commits
What broke: a lane on a fresh `git init` repo had no working git surface.
`statusCounts` ran `rev-parse --short HEAD` unguarded, so `gitFacts` threw
and `GET /api/lanes/:id/git` answered `{available:false}` — the Changes
panel showed no header, and `commit`/`switch` 500'd through `isDirty`.
Add lane in Worktree mode accepted such a repo and left a `failed` row
behind, and a repo with commits but no remote had a permanently disabled
Push with no way to give it one short of a terminal.

What changed:
- worktree.js: an unborn HEAD is a normal state. `statusCounts` reports
  `head: null`; `gitFacts` reads the branch via `symbolic-ref`, an empty
  subject, and a new `unborn` field.
- git-ops.js: an 11th write, `publish` — `remote add origin -- <url>` then
  `push -u origin HEAD`, only with a commit (ENOCOMMITS) and NO remote at
  all (EREMOTEEXISTS, so it never re-points one), the url allowlisted by
  `validateRemoteUrl` (https without userinfo, ssh://, scp-like; refuses
  local paths, file://, ext::, fd::, git://, http://, leading `-`:
  EBADURL), and `origin` removed again when the push fails.
- POST /api/lanes/worktree answers 400 EEMPTYREPO for a repo with no
  commits instead of creating a row that can only fail.
- LaneGitBar: unborn repos show "no commits yet" and a commit-first hint;
  no remote offers Publish… with an inline URL box; a remote but no
  upstream relabels Push as "Publish branch". AddLaneModal disables Add
  on an empty repo and offers "Use as a Repo lane".
- The two tests that used an unborn repo to force a provisioning failure
  now use a `feat` ref clash, since that repo is refused up front.
- CLAUDE.md rule reworded to "the boring eleven" with publish's guards;
  docs/API.md, docs/LANES.md, ARCHITECTURE.md, server/README.md, openapi.
2026-09-25 11:21:30 +07:00
nntrivi2001 f514602697 feat(lanes): git login from the dashboard, merge and conflict resolution (#45)
Part A — a fetch/pull/push against a remote that needs a login no longer
dies on "terminal prompts disabled": the server answers 409 EGITAUTH with
the host, the git bar asks for a username + password/access token and
resends the same action. AgentHub keeps its OWN credential store
(<CLAUDE_HOME>/agenthub-git-credentials/, 0700/0600), one login per host,
shared by every user of the install, managed from Settings -> Git
credentials. A login is only sent to one of the repo's own remote hosts,
saved only after git accepted it, never returned by any route, and reaches
git through an exclusive credential helper (never argv).

Part B — merge a branch (or the diverged upstream) from the git bar; a
merge that stops on conflicts lists them in the Changes panel with Keep
mine / Take theirs / Mark resolved, then Complete merge or Abort merge
(confirm gate). Clean tree, repo-root lane and an existing ref only;
pull/switch/hunk actions are blocked mid-merge; staging a conflicted file
with markers left is refused. Rebase/reset/stash stay out.
2026-09-25 10:44:40 +07:00
nntrivi2001 158783d5d5 feat(lanes,run,pricing): Changes sidebar editor, staged groups, image drop, auto pricing
- Changes panel at full screen: the whole Files view (finder, expand-all,
  tree) becomes a left sidebar and the Files tab is dropped; a working-copy
  file opens straight into the editor (Save/Cancel live once the text
  differs, Cancel restores it, Whole file toggles back to the diff), and the
  page is height-locked so the editor no longer collapses with no live run.
- Overview splits into Staged Changes / Changes groups in the work range.
- A changed file's whole tree row takes its status tint.
- Drag an image onto the lane terminal: POST /api/run/drop-image saves it
  (png/jpeg/gif/webp, random name, 0600, pruned after a day) and the path
  is pasted into the pane, which is how claude attaches it.
- Claude Opus 5.5 gets its own pricing row (4/20/0.2/5/8, fast 8/40);
  claude-opus-5% had been pricing it as Opus 5.
- server/lib/pricing-sync.js: a model family used in token_usage with no
  row of its own is inserted from LiteLLM's public price list, never
  updated; off with AGENTHUB_PRICING_SYNC=0. repo-links allows exactly
  that one third-party GitHub raw URL.
2026-09-24 17:05:15 +07:00
nntrivi2001 d397d3a1f6 feat(lanes): split panes on one lane, in-place file editing, hunk review bar
- Split view: every pane shows the selected lane, each in its own tmux
  session (`slot` 0-3 on POST /api/lanes/:id/start; slot 0 stays the
  lane's `run_id`, slots 1-3 are `agenthub-lane-<id>-<slot>`). A pane's
  Active runs hides what the other panes show; stop/reset/remove kill
  every slot. No per-pane lane picker any more.
- Whole-file editor: PUT /api/lanes/:id/git/file (lane-file-write.js)
  with the read path's guards, existing text files only, and a baseSha
  stale check (409 ESTALE). A file it cannot take says why.
- Per-hunk review bar under each hunk's last changed line: prev/next,
  "n of N", Undo, Keep. Review diffs no longer nest their own scroll box
  (unless windowed), so navigation moves the one real scroller.
- Files tree lists untracked-not-ignored files and colours entries by git
  status (M/U/A/D/R), folders holding changes get a dot.
- Changes panel: Unstage all on the single commit/Stage-all row; sizes
  Narrow (30%) / Medium / Full screen, no 70/30.
- Lane card: the needs-you banner is gone (the strip tile keeps it).
2026-09-24 15:49:39 +07:00
nntrivi2001 67210bcd61 fix(test): run the server suite on a private tmux server
Several suites start real `agenthub-lane-<id>` tmux sessions and then kill
or reset them, with lane ids from their own throwaway database. On the
user's default tmux server those names collide with the dashboard's real
lane panes, so a test run — including the one the pre-commit hook runs on
every commit — killed whichever live session shared an id, the pane the
commit was made from included.

`npm run test:server` now goes through scripts/test-server.js, which drops
TMUX, points TMUX_TMPDIR at a fresh directory and kills that server after.
Also fixes the ERUNTIMEOUT test's tmux mock, which had the wrong (async,
cmd+args) signature and only passed by accident.
2026-09-24 15:48:19 +07:00
nntrivi2001 cfe832383c feat(lanes): stage/undo one hunk at a time, sticky review file headers
Review and the single-file view gain per-hunk Stage and Undo
(stage-hunk / discard-hunk: the hunk is named by its @@ line and
re-matched against a fresh diff, then git apply --cached / -R; undo
needs confirm: true and two clicks). Each review file header stays
pinned while its diff scrolls and carries the file's own
stage/unstage/discard.
2026-09-24 14:40:31 +07:00
nntrivi2001 0737786faf feat(lanes): full-width Changes, Stage all, one-read expand-all
- The Changes panel's width control gains a full-width step (narrow →
  50/50 → 70/30 → full → closed); the console beside it is hidden with
  CSS, never unmounted.
- "Expand all folders" is one GET /api/lanes/:id/git/tree-all (one
  `git ls-files`, every level shaped like /git/tree) instead of one
  request and one git process per folder.
- Overview: a Stage all button and the staged-only commit box sit above
  the file list and stay pinned while it scrolls; the Files finder is
  pinned too.
2026-09-24 14:40:31 +07:00
nntrivi2001 7bc4594b94 fix(lanes,git): harden the lane git surface and fix client races in the Changes panel
Server (git-diff.js, git-ops.js, worktree.js, routes/lanes.js):
- Every lane git call runs with GIT_LITERAL_PATHSPECS=1 and -z porcelain, so a
  file name is a file name, never a glob, and odd names parse correctly.
- A lane that is a subdirectory of its repo sees and acts on ONLY its own
  paths (reads, stage/unstage/discard/commit, and gitFacts.files).
- Any file named settings.local.json (the lane's router API key) is refused as
  403 ESECRET on disk (symlinks resolved) and at every committed rev.
- commit refuses with 400 ESTAGEDOUTSIDE while anything outside the lane is
  staged (checked with --no-renames, so a move into the lane is caught).
- push resolves its remote (upstream → only remote → origin), else 400
  ENOREMOTE; unstaging a rename unstages both halves.
- Git writes serialize on the repo's git dir, not the lane id, so two lanes on
  one index cannot collide on index.lock.
- git refusing an action (a numeric exit status) answers 409 EGIT with git's
  stderr instead of a 500 carrying a numeric code; one shared parseNumstat.

Client:
- Changes panel: no stale answer from a previous lane or an older quick-open
  query repaints the panel; discard disarms on lane switch; Enter on a file
  path opens it instead of folding; bounded tree expand-all.
- Large diffs (> 500 rows) render only the visible rows, with stable width
  and in-sync split columns; mirrored split scroll is clamped.
- Git bar: no "in sync" without counts, a clear no-remote message, branch list
  re-read after a switch, and no previous-lane facts after a lane switch.
- Tooltips no longer flicker or close on unrelated scrolls.
2026-09-24 10:30:25 +07:00
nntrivi2001 6cf8a82889 feat(ui): Appearance settings — dark/light/system mode and named themes; fix Add lane silently doing nothing (#42)
- Settings → Appearance: mode Dark / Light / System (follows the OS live) and
  a named theme remembered per mode — dark: Default, Darcula, Apple Dark;
  light: Default, Apple Light, IntelliJ Light, Canvas. Each theme is a
  CSS-variable override block keyed by `data-theme` on <html>; the sidebar
  quick toggle gains System and swaps between the two remembered themes.
- An inline boot script in index.html applies the stored mode and theme
  before first paint, so a reload no longer flashes the default colours.
- Diff +/- rows are easier to tell apart on every theme: a /20 status tint,
  a 3px status bar and a bold coloured marker; the changed-span emphasis no
  longer vanishes on light themes.
- #42: the Add-lane modal (Repo mode / "Current branch") asks
  POST /api/lanes/ensure with `exact: true`. Without it a folder nested inside
  an existing lane resolved to that parent lane with created:false, so the
  modal closed and no new card appeared. When the folder really already has a
  lane, the modal now says which one instead of closing.

Closes #42
2026-09-24 09:44:42 +07:00
nntrivi2001 aff863c69f fix(plugins): retry claude plugin install|update without -y on an old CLI
An older Claude Code has no -y on `plugin install|update` and exits with
"error: unknown option '-y'", so the dashboard's Update now chain failed at
its plugin_update step (and so did Update all, the plugin buttons and the
superpowers auto-install). Those versions also predate the confirmation
prompt -y answers, so every wrapper now retries the same argv without it
when, and only when, the CLI rejects the flag. /agenthub-update, which the
failure message points users to, falls back the same way.
2026-09-23 16:46:20 +07:00
nntrivi2001 764b967856 fix(dashboard): measure V8 heap against its limit, not its current size
The Health Score's heap component divided heapUsed by heapTotal, which is
only what V8 has grown the heap to so far, so a healthy server read 70-90%
and dragged the score down. GET /api/settings/info now also reports
server.memory.heapLimit (v8 heap_size_limit, the ceiling the process dies
at); the V8 Heap bar and the Health Score divide by it, falling back to
heapTotal against an older server.
2026-09-23 16:46:00 +07:00
nntrivi2001 92cee94431 fix(hooks): store a hook delivered twice only once
A doubled hook registration (plugin + leftover checkout install) POSTs every
event twice with a byte-identical payload, so the timeline showed each row
twice and token/alert/lane side effects ran twice. POST /api/hooks/event now
skips a payload identical to the session's LATEST event from the last 10s
(findRecentDuplicateEvent). Latest-only keeps two legitimately identical
Stop events of consecutive turns, which always have other events between.
2026-09-23 09:18:17 +07:00
nntrivi2001 1b561c22d9 feat(lanes,ui): per-lane git surface — Changes panel, VS Code-style diff, branch-aware Add lane, redesigned tooltips (#34, #35)
- Changes card beside lane detail with five modes: Files tree, overview,
  Review (whole diff, per-file collapse), single file, commit history.
- LaneDiffView: unified / side-by-side with independent scroll on both axes
  and mirrored scrollLeft, expand/collapse context, intra-line emphasis.
- Staged-only commit box; git actions (fetch/pull/push/stage/unstage/
  discard/commit/switch/branch-create/branch-delete) guarded in git-ops.js.
- Server: git-diff.js + git-ops.js behind GET /api/lanes/:id/git/* and
  POST /api/lanes/:id/git/:action; gitFacts gains upstream/ahead/behind.
- Add lane: new/existing branch, current branch, worktree list, prefill
  from the git bar's "New lane here"; /api/run/cwds gains a repo kind.
- GlobalTooltip upgrades every native title into a themed anchored bubble;
  chart/info popovers use theme tokens.
2026-09-23 09:18:17 +07:00
nntrivi2001 f77049c238 feat(run,ui): make the lane console usable — terminal focus, live-exit recovery, auto permission mode, model aliases
Terminal (issue #33):
- `convertEol` is gone from xterm. It rewrote every bare LF as CR+LF, so a
  tmux pane that scrolled (tmux emits a bare LF and keeps drawing at the
  column it was on) had the rest of that draw land at column 0, stamping a
  line's first characters into the left margin of the rows below — the torn
  left edge that only showed up once content scrolled.
- The attach carries the client's real size (`?cols=&rows=`, `sizeFromQuery`
  in `server/websocket.js`, bounded because it reaches a TIOCSWINSZ ioctl).
  `attach()` spawned `tmux attach-session` at a hardcoded 80x24 on every
  attach, and tmux sizes the window to the newest client, so `claude` redrew
  its whole UI at 80 columns until the first `resize` message landed.
- The terminal takes the keyboard on mount, so typing reaches tmux without
  clicking the pane first.
- A pane that ends (double Ctrl+C, `exit`, a crash) reports itself through a
  new `onExit` prop and `LaneConsolePane` runs `newRun()`, so the console
  returns to the setup form instead of leaving a dead pane behind a
  "New run" button.

Pane environment:
- `server/lib/desktop-env.js` forwards the graphical session's DISPLAY /
  WAYLAND_DISPLAY / XAUTHORITY / XDG_RUNTIME_DIR (from
  `systemctl --user show-environment`, this process's own values winning)
  into the pane. The server normally runs as a systemd user service with none
  of them, so `claude` there could not read the clipboard and answered "no
  image found in clipboard" on every image paste.

Run setup:
- `auto` is a permission mode and is now the default everywhere — allowlists,
  the `--permission-mode` fallback, the select, and the pane's initial state.
- The model picker sends the CLI's aliases (`fable`/`opus`/`sonnet`/`haiku`)
  instead of pinned ids. `claude --model` resolves an alias to the latest
  model itself, so a release like Fable 5.1 needs no edit here; pricing is
  unaffected because it keys off the resolved id the transcript reports.
- **Create/update readme file** moved from the Add-lane modal into the lane's
  Tools menu: creating a lane and writing its docs are separate decisions,
  and the second is worth re-running long after the first.
- The Workspace's no-lane state is a real banner; the lane strip (add tile
  included) renders only once a lane exists.

Refs #32, #33.

Tests: npm run test:server (1828 pass), client vitest (558 pass).
2026-09-21 17:03:27 +07:00
nntrivi2001 64017c6ca5 feat(run,notifications,profiles): stop sending prompts nobody typed, and fix notification + profile delivery
Run console:
- Picking a pipeline no longer pre-writes anything into the prompt box
  (`promptPrefill` in `LaneConsolePane.tsx` is gone — it wrote the driving
  slash command on a claude lane and a stage checklist on a codex one).
  Whatever sits in that box is sent as a real prompt the moment the run
  starts, so the box is the user's alone now.
- Resuming a session sends NO prompt at all (`handleStart` in `RunSetup.tsx`):
  the resumed session carries its own transcript, and leftover text in the box
  fired an extra turn into it.

Notifications:
- `client/src/lib/notify.ts` is now the ONE sender, used by both
  `useNotifications` and Settings' test button, and it reports where the
  notification landed: `POST /api/push/send` answering `{native:false,pushed:0}`
  means it went NOWHERE, so the client falls back to a local notification
  instead of counting the 200 as delivered.
- A notification carries the lane that raised it (`/workspace?lane=<id>`), and
  Workspace selects that lane on arrival.

Profiles:
- `server/lib/cc-profiles.js` reads and writes the macOS login Keychain item
  `Claude Code-credentials`, where the `claude` CLI actually keeps
  `claudeAiOauth` on darwin — a switch used to rewrite a file nothing reads.
  The file stays the fallback, is kept in step only when it already exists, and
  `AGENTHUB_CC_KEYCHAIN=0` pins it (tests set it).

Tests: npm run test:server (1820 pass), client vitest (553 pass).
2026-09-21 15:15:22 +07:00
nntrivi2001 8d48a038c7 fix(ingest,ui): count each assistant message once, each turn once, and each event once
Three defects that all showed up as "the dashboard's numbers are wrong", plus
the notification and brand work asked for alongside them.

Tokens: Claude Code writes ONE JSONL line per content block of an assistant
message, and every one of those lines repeats the same message-level `usage`.
Both parsers summed them, so a tool-heavy session's tokens — and its cost — ran
1.5-2x high (one 4h session: $45.82 stored against $27.72 real). `lastUsageMsgId`
counts a message once, carried across the transcript cache's incremental chunk
boundaries. `replaceTokenUsage`'s high-water baselines mean the fixed parser
cannot correct what is already stored, so `setTokenUsage` (exact, baselines
cleared) is added for the deliberate re-parse path and `reconcileTokens` now
covers every session with a transcript on disk rather than imported ones only.

Turns: every `TurnDuration` row was stamped NOW while its dedup guard matched on
the transcript's timestamp, so each hook re-inserted the session's whole turn
history — 587k rows, 75% of a real events table, one session holding 68,747
copies of the same turns. The row now carries the transcript timestamp, which
fixes the guard and the feed's ordering together. Same class of bug in the
session metadata: `thinking_blocks` / `turn_count` / `total_turn_duration_ms`
are assigned from the full parse instead of `+=`, since `extract()` returns
whole-transcript totals on every call.

Events: leftover checkout hook entries in `~/.claude/settings.json` double every
event, every subagent row and every token figure. `stripLegacyHooks` has always
removed them — but only inside `bootstrap()`, which `fastPathOk` skips on
exactly the healthy installs where nothing else forces a full pass, so they
survived indefinitely. `hasLegacyHooks` (pure fs) now blocks the fast path.

A one-time repair at the first start after upgrading (`user_version` 3) deletes
the duplicated `TurnDuration` rows and re-parses every transcript with exact
writes. Marker 3 and not 2: an install that ran the earlier pass had its
corrected rows re-inflated by whatever old build was still ingesting hooks.

Router: switching a lane's AI Router while a pane is live only half-applies —
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 with "model not found". The apply route reports
`restart_required` / `run_id`, and the picker offers a Restart that respawns the
pane from the lane id alone (replaying the old `--model` would re-create the
mismatch).

Notifications (#29): the two moments Claude is BLOCKED on the user now have
their own switches and default to on — `onAwaitingInput` (a permission prompt or
a question) and `onTurnComplete` (end of turn). Before, the first was unswitchable
and the second hung off a preference that defaults to off, so the one thing a
user walks away from went unannounced. Prefs shape/defaults/storage move to
`client/src/lib/notifPrefs.ts`; Settings and the hook had private copies.

Brand: the sidebar shows the SmartGift wordmark with the AgentHub caption under
it, the collapsed rail shows the mark, and the favicon is vendored to match.
2026-09-18 16:52:05 +07:00
nntrivi2001 eab7c437df feat(skills,pipelines): make the review node review, and make a long run followable and affordable
Closes #24.

**The review node ran nothing.** Both templates declared
`/superpowers:requesting-code-review` — the skill about how to HAND OVER a
review, not one that reviews anything — so `agenthub lanes node review`
performed no review at all. They now declare the bare `/code-review`, Claude
Code's own. It carries no `plugin:` prefix, so `requiredSkillPlugins` skips it
and the doctor's required-plugin row is unchanged (verified against the live
function: still exactly the five superpowers skills the templates name).

**The rest is the brainstorm → implement → review span, made cheaper to run and
harder to lose track of.** `ship-feature` gains four sections it never had:

- **Right-size the run.** The pipeline is not free — every dispatching phase
  costs a model call and minutes, and the fix-loop pays it again each lap. A
  typo does not enter the pipeline at all; a small single-layer change skips the
  plan-debate, independent-test and gate agents and lets the user's acceptance
  be the gate; medium/large runs everything. Sizing is a claim made out loud,
  and a task that grows re-sizes upward.
- **Dispatching a judgement.** The fresh-context/FOREGROUND/never-`fork`/
  hand-only-verifiable-artifacts/one-parseable-line contract, written once
  instead of re-argued in three phases — plus the rule that was missing
  entirely: never dispatch what a command answers. An agent is for judgement,
  not for typing.
- **Resuming after a compaction.** Long runs outlive the context window; the
  lane skill had this and this one did not. Re-derive position from the declared
  stage and the diff, and treat a phase whose evidence you cannot find as one
  that did not happen.
- **A `Leave when:` line on every phase.** Objective exit criteria are what stop
  both the silent skip and the endless polish: intake leaves when the answers
  are in hand, review leaves when the latest pass over the CURRENT diff is
  clean, the gate leaves when verification really ran AND the user said yes.

And, narrower: intake asks only what the repo cannot answer (a question the code
could have answered spends the user's round and earns a worse answer);
implement works the approved plan task by task, re-checks each acceptance
criterion, heartbeats on long work, and treats a plan that turns out wrong as a
return to `plan` rather than a quiet substitution; tests runs the suite the diff
needs rather than both by reflex.
2026-09-18 10:39:01 +07:00
nntrivi2001 26b0fe53c7 feat(skills,pipelines): ask the user at the two points being wrong is cheap, and stop paying for reviews the diff does not need
The `default` 8-node pipeline walked past its own intake: the node declared
`agenthub stage intake`, a stage report with no work attached, so "intake" was
a label the run passed through without ever asking a question. It now declares
`/superpowers:brainstorming` (and detects it — that rule moves off `plan`),
and both driving skills make the brainstorm mandatory, asked with
AskUserQuestion and waited on rather than answered on the user's behalf.

Three stops replace three silent slides:

- After the plan is written and adversarially challenged, it goes to the USER
  for approval. Implementation does not start without it, and work that turns
  out to need a different approach comes back for a fresh one.
- Review picks its reviewers from `git diff --name-only`: Claude Code's own
  `code-review` always, a domain reviewer only for a layer the diff actually
  touched, and — new — only once the change is big enough to have seams. Three
  agents re-reading twenty lines buys one opinion at three times the tokens, on
  every fix-loop lap. Review is also a loop now: it ends on a clean pass over
  the current diff, not on one pass having happened.
- The gate asks the user to accept before anything is claimed done (and in the
  lane pipeline, before the PR is published — publishing is outward-facing).
  A NO-GO re-enters by SIZE: a defect goes back to implement, a wrong approach
  back to plan. Nothing carries a NO-GO forward with a note about it.

Also here, from a real stall seen on a lane: `ship-feature` had no waiting rule
at all, and a session at the gate invented one with `ScheduleWakeup` — a
/loop-only primitive that paces nothing here and is refused outright when
`auto` mode cannot reach its safety classifier. The rule the lane skill already
had now exists in both.

Cost, measured in what each agent is actually for: the suite run leaves its
agent (nothing is judged, so nothing is delegated — an agent there buys a
second model's tokens to read output the session can read for free), and
`backend-reviewer` drops from opus to sonnet like its frontend twin; both read
a scoped diff against a fixed checklist, and the senior gate above them is
still opus.

Skill bugs found in the same sweep: doc-map told agents not to recreate the
removed translations and then, three lines later, to mirror every README change
into them; the docs mapping still carried `+VN+CN` and the punctuation left by
their removal; Procedure jumped 2 → 6; Verify required `scripts/doc-coverage.sh`,
which does not exist in this repo.
2026-09-18 10:21:03 +07:00