From f887c1630994b46038e278538599580dc9791e10 Mon Sep 17 00:00:00 2001 From: nntrivi2001 Date: Thu, 1 Oct 2026 13:39:17 +0700 Subject: [PATCH] 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 ` ` (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 (-migrate.lock), so two dashboards started at once on a fresh database both come up. --- ARCHITECTURE.md | 8 +- CLAUDE.md | 5 +- client/README.md | 2 +- .../lanes/__tests__/AddLaneModal.test.tsx | 4 + client/src/components/run/LaneConsolePane.tsx | 6 + client/src/components/run/RunSetup.tsx | 64 ++++- .../run/__tests__/LaneConsolePane.test.tsx | 31 +- .../run/__tests__/RunSetup.test.tsx | 85 +++++- .../components/settings/PricingSettings.tsx | 19 +- .../__tests__/AiToolsSettings.test.tsx | 26 ++ .../__tests__/PricingSettings.test.tsx | 12 + client/src/hooks/useAiTools.ts | 4 +- client/src/i18n/locales/en/settings.json | 23 +- client/src/i18n/locales/vi/settings.json | 23 +- client/src/lib/api.ts | 68 ++++- client/src/lib/types.ts | 5 + client/src/pages/AiToolConfig.tsx | 267 ++++++++++++++++++ client/src/pages/AntigravityConfig.tsx | 22 -- client/src/pages/SettingsHub.tsx | 43 ++- .../src/pages/__tests__/AiToolConfig.test.tsx | 141 +++++++++ docs/API.md | 9 +- docs/DATABASE.md | 8 +- docs/HOOKS.md | 4 +- docs/LANES.md | 8 +- openapi.yaml | 8 + server/README.md | 8 +- server/__tests__/ai-tool-config.test.js | 201 +++++++++++++ server/__tests__/ai-tool-models.test.js | 187 ++++++++++++ server/__tests__/ai-tools-route.test.js | 19 ++ server/__tests__/ai-tools.test.js | 4 + server/__tests__/db-concurrent-start.test.js | 42 +++ server/__tests__/lanes-api.test.js | 84 ++++++ server/__tests__/pricing-sync.test.js | 208 +++++++++++++- server/__tests__/pty-run.test.js | 14 +- server/data/ai-tools/antigravity.json | 14 +- server/data/ai-tools/cursor.json | 10 +- server/data/ai-tools/gemini.json | 15 +- server/data/ai-tools/opencode.json | 14 +- server/data/ai-tools/qwen-code.json | 14 +- server/db.js | 61 +++- server/index.js | 15 + server/lib/ai-tool-config.js | 151 ++++++++++ server/lib/ai-tool-models.js | 115 ++++++++ server/lib/ai-tools.js | 82 +++++- server/lib/pricing-sync.js | 180 +++++++++--- server/lib/pty-run.js | 12 +- server/openapi.js | 7 + server/routes/ai-tools.js | 113 +++++++- server/routes/lanes.js | 70 ++++- server/routes/pricing.js | 6 +- server/routes/settings.js | 5 + 51 files changed, 2343 insertions(+), 203 deletions(-) create mode 100644 client/src/pages/AiToolConfig.tsx delete mode 100644 client/src/pages/AntigravityConfig.tsx create mode 100644 client/src/pages/__tests__/AiToolConfig.test.tsx create mode 100644 server/__tests__/ai-tool-config.test.js create mode 100644 server/__tests__/ai-tool-models.test.js create mode 100644 server/__tests__/db-concurrent-start.test.js create mode 100644 server/lib/ai-tool-config.js create mode 100644 server/lib/ai-tool-models.js diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index fdc3e296..c5c9b741 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -336,9 +336,11 @@ graph TD | `bin/agenthub.js` | Dependency-free umbrella CLI (`agenthub `) exposing the full dashboard surface in the terminal: monitoring (health / stats / kanban / tail via short-interval event polling), data browsing (sessions, per-session detail with an indented agent tree + cost + recent events, agents, events), insights (analytics, workflow intelligence, dynamic Workflow-tool runs, per-model cost), alerts + rules + webhook test probes, pricing CRUD, imports (rescan / scan-path), and administration (doctor, info, export, cleanup, reinstall-hooks, clear-data). Linked globally by `npm run setup` via a fail-soft `npm link` (`link-cli` script). Discovers the live server through `server/lib/server-info.js` (`~/.claude/.agenthub.json`, PID-liveness-checked) with `CLAUDE_DASHBOARD_PORT` / `DASHBOARD_PORT` env overrides and a 4820 fallback; renders a full terminal UI — box-drawn width-fitted tables with right-aligned numeric columns, status icons, inline bar charts (stats / analytics / cost), `├─`/`└─` agent trees, and a TTY spinner for `start` — whose ANSI styling degrades to plain text when piped (`--no-color` / `NO_COLOR` / `FORCE_COLOR` / `AGENTHUB_COLOR` respected); the one destructive command (`clear-data`) refuses to run without `--yes`. Server lifecycle: `agenthub status` shows a ●/○ up/down indicator and `agenthub start` boots a detached production server (waits for /api/health, logs to `data/agenthub-server.log`). `agenthub repl` (aliases `shell` / `i`) opens an interactive shell — a readline prompt with tab-completion (commands / subcommands / flags), persisted arrow-key history (`data/.agenthub_repl_history`), and a live server-status prompt; each entered line runs as a short-lived child `agenthub` process (via `runCommand` dispatch), so an offline refusal or a blocking `tail` cannot take the shell down, and piped input runs each line in order then exits at EOF. When the server is down, read-only commands (sessions / session / agents / events / kanban / stats / pricing list / alerts list / rules / export / doctor) fall back to direct SQLite reads of `data/dashboard.db` under a ⚠ Offline-mode banner — the connection is opened without SQLite readonly mode so a live WAL stays visible, and is SELECT-only by construction — while server-only commands (tail, analytics, workflows, runs, cost, mutations) refuse with the specific reason | | `scripts/import-history.js` | Batch history importer used by (a) server startup auto-import, (b) the `/api/import/*` routes, (c) the `import-history` CLI, and (d) live `SubagentStop` ingestion via the exported `scanAndImportSubagents(dbModule, sessionId, transcriptPath)`. Exposes `importAllSessions(dbModule)` for the default `~/.claude/projects` tree, `syncDefaultProjects(dbModule, {mtimeCache})` (the incremental, mtime-fingerprinted re-sweep that backs the continuous background sync — parses only new/changed files and reports `[{sessionId, isNew}]`), and the generalized `importFromDirectory(dbModule, rootDir, {onProgress})` which walks any directory recursively, classifies each `.jsonl` as session vs subagent (with `findSessionSubagents` probing both `//subagents/*` and `/subagents//*` layouts), and funnels everything through the shared `parseSessionFile` + `importSession` pipeline. The durable transcript snapshot (`snapshotTranscript`) additionally preserves **nested** Workflow-tool inner-agent transcripts (`subagents/workflows//agent-*.jsonl`) via the separate `findSessionWorkflowSubagents` probe — mirroring the run subpath so the read route resolves the snapshot identically to the live file, without pulling those nested agents into the flat sub-agent import (no double-count). After each batch imports, `importAllSessions` / `importFromDirectory` also call `ingestWorkflowsForSession` (from `server/lib/workflow-ingest.js`) per session — outside the SQLite transaction, since the ingest is async — so a **Workflow-tool** run whose journal never reached a live server (a headless `claude -p` run, a CI job, or an HPC/cluster node emits no hooks) still links its inner agents to their `run_id` on a plain `agenthub import rescan` / `agenthub import path`, instead of leaving them orphaned (`workflow_run_id = NULL`) with the run stuck at 1 agent — see "Workflow-Tool Run Ingestion". `parseSubagentFile` extracts ordered `toolEvents` (tool_use + tool_result paired by `tool_use_id`) so `importSubagentFromJsonl` can emit per-tool `PreToolUse` + `PostToolUse` rows under each subagent's own `agent_id`. The importer dedups against live hook-created subagent rows via `findLiveSubagentForJsonl` (session + subagent_type + start-time within 30 s) so backfill never produces parallel `-jsonl-*` rows. It also **skips `importSubagents` entirely when subagent transcripts exist** — the main-transcript `Agent`-block rows (`-subagent-N`) and the transcript rows (`-jsonl-*`) would otherwise both be created and only deduped by a fragile type+timing match, doubling every subagent; when transcripts are present the richer `-jsonl-` rows are authoritative and the `-subagent-N` fallback runs only when there are none. `importSession` now also **persists `transcript_path`** on the session row (via `setSessionTranscriptPath`) so the abandon sweep, compaction scanner, and per-agent cost backfill can locate the transcript later, and **stamps each subagent's own token buckets into `metadata.tokens`** (used for per-agent cost). `backfillSubagentTokenMetadata` (a deferred, self-limiting startup pass) fills `metadata.tokens` on subagents that predate per-agent cost, deriving the transcript path from `//.jsonl` when `transcript_path` is null and covering both subagent-dir layouts — metadata-only, so it never touches session `token_usage`. `classifyJsonl` treats any file under a `subagents/` ancestor at any depth (including the `subagents/workflows//` tree) as a subagent, so workflow inner-agent transcripts are never misimported as top-level sessions. **Nested-subagent hierarchy** is rebuilt by `reconcileSubagentParents`: every subagent is inserted flat under the main agent (no single hook/JSONL carries the spawner id), then `parseSubagentFile` also returns `spawnedChildren` — the child agent ids named on each Task tool result (`toolUseResult.agentId`) — which are inverted to a child→parent map and used to repoint `parent_agent_id` (via the `setAgentParent` statement) so a subagent that spawns its own subagents nests under its true spawner instead of collapsing flat under main; any subagent no other subagent claims stays under main. It resolves both the child and parent to their live-or-jsonl DB id (mirroring `importSubagentFromJsonl`), so it also corrects the live hook heuristic's guesses once the transcripts land. Idempotent and additive (only rewrites `parent_agent_id`), it runs in all three group-import paths (`importSession` ×2, live `scanAndImportSubagents`); `scanAndImportSubagents` returns a `reparented` count alongside `created`. **Re-import is fully incremental**: for each existing session a per-event-type high-water mark (`MAX(created_at) GROUP BY event_type`) is read up-front and only JSONL entries with `ts > cutoff[type]` are inserted for Stop / PostToolUse / TurnDuration / ToolError — so long-running sessions whose transcripts grow across multiple days continue to receive new events on every re-run instead of being blocked by the old "if zero of type X then dump all" check. `sessions.ended_at` is rolled forward to the JSONL's last activity when it surpasses the stored value, and `metadata.user_messages` / `assistant_messages` / `turn_count` are refreshed on every pass. `parseSessionFile` also captures the transcript title (`custom-title` / `ai-title`) and `importSession` prefers it for `sessions.name` over the cwd-folder fallback, backfilling existing auto/placeholder names on re-import (same precedence as the live hook sync). Other idempotency keys are unchanged: `data LIKE '%"tool_use_id":"X"%'` skips any tool event already inserted, compaction agents/events dedup by uuid, API errors dedup by summary, and `baseline_*` columns preserve pre-compaction token totals. Token totals, per-model cost, compactions, subagents, tool events, API errors, and turn durations are identical to live ingestion. Creates `APIError`, `TurnDuration`, and `ToolError` event types during import; subagent tool events carry `imported: true, source: "subagent_jsonl"` in their data payload so analytics can distinguish backfilled rows when needed | | `server/routes/import.js` | Express router for the Import History feature. Three endpoints funnel into the same pipeline: `POST /api/import/rescan` (default projects dir), `POST /api/import/scan-path` (arbitrary absolute dir with `~` expansion), `POST /api/import/upload` (multer multipart accepting `.jsonl`, `.meta.json`, `.zip`, `.tar`, `.tar.gz`, `.tgz`, `.gz`). `GET /api/import/guide` returns OS-aware instructions + archive command + default-dir stats. Each request uses a per-request temp dir (`req._agenthubUploadDir` for multer staging, a separate `workDir` for extraction) that is reclaimed in `finally`. Progress is broadcast as `import.progress` websocket messages throttled at ~150 ms. Limits configurable via `AGENTHUB_IMPORT_MAX_BYTES` / `AGENTHUB_IMPORT_MAX_FILES` | -| `server/lib/pricing-sync.js` | Automatic pricing: a Claude, OpenAI (`gpt-…`, `o…`, `codex-…`, from Codex sessions) or Gemini (`gemini-…`, from Antigravity sessions) model seen in `token_usage` with no `%` row of its own gets one INSERTED (never updated) from LiteLLM's public price list — fetched only when something is missing, cached 24 h, run from `startBackgroundServices` every 15 min, off with `AGENTHUB_PRICING_SYNC=0`; each entry only from its own vendor (`anthropic`/`openai`, `vertex_ai-language-models`/`gemini`), OpenAI and Gemini rows with no cache-write charge unless the list names one, Gemini's >200k rate not modelled. `POST /api/settings/reset-pricing` syncs straight after reseeding (up to 3 s); `{scope: "claude"}` resets only `claude-…` rows. See docs/DATABASE.md § model_pricing | +| `server/lib/pricing-sync.js` | Automatic pricing from LiteLLM's public price list, fetched at most daily: a row (`source = 'sync'`) for every Claude, OpenAI (`gpt-…`, `o…`, `codex-…`) or Gemini (`gemini-…`) family used in `token_usage` with no `%` row of its own AND for every current-generation chat model of those vendors before it is used; its own rows follow a rate change; a row edited in Settings (`source = 'user'`) or deleted (`model_pricing_dismissed`) is never touched again. Off with `AGENTHUB_PRICING_SYNC=0`. | | `server/lib/ai-tools.js` | The AI Tools catalog: one JSON per AI coding CLI (`server/data/ai-tools/`, the synced newer-release copy, `DASHBOARD_AI_TOOLS_DIR`, rising precedence), PATH detection, enable/default state (`/ai-tools.json`). `launcher`/`install`/`update` are honoured only in built-in and release files. | | `server/lib/shell-path.js` | Merges the login shell's PATH into the server's (`$SHELL -ilc`, 5 s) and keeps the shell's own order (`shellPath()`) for install jobs. | +| `server/lib/ai-tool-models.js` | `GET /api/ai-tools/:id/models`: each tool's model list from its own source — Codex's `models_cache.json` (listed slugs only; `identity` is never read), Claude's aliases labelled with the newest priced version plus pinned versions from `model_pricing`, a catalog tool's vendor-priced ids (`model.vendor`) plus distinct `token_usage` models of its sessions. | +| `server/lib/ai-tool-config.js` | A catalog tool's own config files behind `GET/PUT /api/ai-tools/:id/config[/:index]`: only the `~/`-relative paths a trusted catalog file lists in `config`, existing regular text files ≤ 1 MB, never a credential-looking name (also after a symlink), saved over the opened sha (`ESTALE`), JSON/JSONC syntax-checked, backed up under `/ai-tool-config-backups//` (newest 10), written atomically keeping the mode. | | `server/lib/ai-tool-jobs.js` | The one-at-a-time install/update runner behind `POST /api/ai-tools/:id/install|update` and `/update-all`: argv only, stdin closed, login-shell npm, vendor scripts downloaded and run with `bash`, a tool's own updater; log in memory. | | `server/lib/ai-tool-versions.js` | npm-managed check (binary under `npm prefix -g`, used by update-all to skip an npm updater for a tool installed another way) and `GET /api/ai-tools/versions`, cached 24 h; dropped on Refresh and after each job. The UI asks for no version: every updater gets Update. | | `server/lib/ai-tools-sync.js` | Daily copy of the newest release's AI Tools catalog into `/runtime/ai-tools-remote/`, used only while that release is newer than the build. Off with `AGENTHUB_AI_TOOLS_SYNC=0`. | @@ -617,7 +619,7 @@ graph LR | `/workflows` | Workflows | `GET /api/workflows?status=active\|completed`, `GET /api/workflows/session/:id` + WebSocket auto-refresh (3s debounce) | | `/cc-config` | *(redirect)* | Redirects to `/settings?tool=claude-code` — the config explorer moved under the Settings hub as one of its per-tool panels (see `/settings` below) | | `/run` | Workspace | Merged workspace page combining lanes and runs. Attaches to a tmux-backed pseudoterminal tied to a lane: the UI selects a lane, calls `POST /api/run` with that lane's `id`, and receives a `runId` + tmux session name. The Workspace displays a horizontal lane strip at the top, the selected lane's pipeline map (only while a pipeline template is installed — none ships), and a real interactive terminal (xterm.js) fed by `/ws-pty/:runId` binary frames below. `TerminalView` takes the keyboard on mount (`term.focus()`), so typing in a lane reaches tmux without clicking the pane first, and it reports the server's `exit` frame to its owner through `onExit` — `LaneConsolePane` runs `newRun()` there, so a pane that ends (a double Ctrl+C out of `claude`, `exit`, a crash) swaps straight back to the run setup form instead of leaving a dead terminal behind a "New run" button. It re-attaches 1.5 s after any unexpected socket close (server restart included) and stops only on that `exit` frame — a dead socket used to leave the pane frozen on its last frame, looking live while swallowing every keystroke and mouse report. It also calls `preventDefault()` on Shift+Tab (keeping xterm's own handling): xterm emits `ESC[Z` for that combo but, unlike plain Tab, never marks the event cancelled, so the browser's default moved focus out of the terminal and the pane saw only the first press — `shift+tab to cycle` (claude's permission mode) looked dead in the browser while working in a real terminal. It also installs a custom wheel handler that returns `false`: without it, xterm falls back on an alt-screen buffer to translating each wheel notch into a cursor-key press (`ESC[A`/`ESC[B`), which a `claude` pane reads as arrow up/down and uses to walk the prompt history instead of scrolling. That handler suppresses only the emulation branch — real mouse reports are emitted by a separate listener xterm registers when the pane's program enables mouse tracking. Its xterm viewport scrollbar is hidden by design (`.xterm-viewport` rule in `client/src/index.css`): tmux repaints the whole pane, so xterm's scrollback is always empty and scrolling happens via mouse reports forwarded to the pane's program, not the DOM. Pre-flight: `GET /api/run/{tmux,binary,cwds,files}` for tmux availability + `claude` binary check + `@`-file autocomplete. Start/resume: `POST /api/run` (requires `laneId`; optionally accepts `prompt` to send immediately); `GET /api/run/:id` (returns handle); `DELETE /api/run/:id` (stops). History: `GET /api/run/history?laneId=` lists only that lane's runs. PTY streaming: `/ws-pty/:runId` delivers raw PTY frames as binary WebSocket frames — no JSON envelope overhead, direct to xterm.js for live rendering; the same tmux session can have multiple simultaneous clients (browser Workspace, `agenthub lanes shell` CLI, other tools), all synced live. Lane self-heal: `GET /api/lanes/:id` auto-corrects `run_id`/`status` if the tmux session has been killed externally. Tier 1 TUI parity: tmux session is a real shell, not headless — supports editors, pagers, interactive subcommands. **The console never writes a lane's stage** — stage moves only through `agenthub stage` commands. Live / Offline indicator next to the title | -| `/settings` | SettingsHub | Tabbed shell (`?tool=` query param picks the active tab, `PageTabs`) hosting the dashboard's own settings plus one configuration panel per AI tool. An **AI Tools** button at the tab bar's right edge (`PageTabs` `actions`) opens `components/settings/AiToolsModal.tsx` around `AiToolsSettings.tsx`; `/settings#ai-tools` (the Add-lane modal's link) opens it too, and closing drops the hash. **dashboard** tab (default, no `?tool=`) → `Settings`: an **Appearance** section first (mode + named theme, `localStorage` only), `GET /api/settings/info`, `GET /api/pricing/cost` + `localStorage` for notification prefs; hosts the **Remote Data Sources** panel (`components/RemoteSources.tsx`) — CRUD + test + sync over `/api/remote-sources`, live status from `remote_source.status` WS messages. **claude-code** tab (`?tool=claude-code`) → `CcConfig`: 12-tab Claude Code configuration explorer whose **Overview** also renders `components/settings/ClaudeAgentHubSettings.tsx` (there is no AgentHub sub-tab; the overview's path list leaves CLAUDE_HOME to that editor) holding the dashboard's own Claude Code settings — Model Pricing (`GET/PUT/DELETE /api/pricing`, `POST /api/settings/reset-pricing`), hook status + Reinstall (`GET /api/settings/info`, `POST /api/settings/reinstall-hooks`) and CLAUDE_HOME (`GET/PUT /api/settings/claude-home`; a PUT answers `routers.missing`, the lanes naming an AI Router profile the new home's store lacks, and the UI warns); its **Reset Defaults** sends `{scope: "claude"}`. They were on the dashboard tab until #40 part B, and a tool's own settings go in that tool's tab from now on. Both editable home directories render through `components/settings/HomeDirSetting.tsx` and are saved to AgentHub's settings file (`server/lib/settings-env.js`: `~/.config/agenthub/settings.env`, never the plugin cache's `.env`; load order real environment > the checkout's `.env` > that file), with a warning when the real environment or `.env` owns the variable (`env_owned`); both PUTs carry `sameOriginGuard`. Reads via `GET /api/cc-config/{overview,skills,agents,commands,output-styles,plugins,marketplaces,mcp,hooks,hook-scripts,keybindings,statusline,settings,memory}`. Mutations for skills/agents/commands/output-styles/memory — including the per-project file-based auto-memory store (`*.md` under `~/.claude/projects//memory/`, grouped by project and searchable in the Memory tab, with clickable `MEMORY.md` index links that scroll to + highlight the matching fact file) — via `PUT /api/cc-config/file` + `DELETE /api/cc-config/file` (timestamped backups, atomic writes). The Keybindings tab additionally offers a structured inline editor that persists via `PUT /api/cc-config/keybindings` (same backup-first, atomic-write guarantees). `GET /api/cc-config/file?path=…` for single-file viewer. `GET /api/cc-config/backups` for the recovery modal. A **Switch profile** button left of Backups opens the auth-profile modal (`/api/cc-config/profiles*`): save the logged-in Anthropic account, rename it by clicking its name, then switch between accounts — only the login is swapped, so skills / subagents / memory / plugins / marketplaces / settings stay put. There is no name field (the server derives the identifier) and the save button shows only while the live account is unsaved, since one account is one profile. It cannot log in for you (`claude auth login` does that) and a switch applies to new sessions only. Subscribes to `cc_config_changed` WS messages for live refresh on both dashboard mutations and external file edits picked up by `cc-watcher`. The Settings tab (within CcConfig) leads with a client-side **Current configuration** summary that resolves the `/config` options (model, verbose, theme, output style, effort, auto-compact, notifications, …) across user / project / project-local scopes, showing defaults when unset. **codex** tab (`?tool=codex`) → `CodexConfig`: the Codex CLI counterpart — 8 tabs (overview, skills, agents, rules, memory, config, mcp, plugins; marketplaces are folded into the plugins tab rather than given their own, since the CLI treats them as one small add/remove surface). Codex's scope union is `"user"` \| `"project"` only — no `"project-local"`, no per-project file-based memory. Reads/mutates via `GET/PUT/DELETE /api/codex-config/*` (skills/agents/rules/memory/config are file-backed with the same backup-first write path as `cc-config`; mcp/plugins/marketplaces are CLI-owned add/remove only, no artifact editor). Subscribes to `codex_config_changed` WS messages. Its **Overview** also holds (no AgentHub sub-tab) Model Pricing for OpenAI models (`components/settings/PricingSettings.tsx`, the same table Claude Code's Overview shows filtered to `claude-…` rows, each with a **Show all models** switch) CODEX_HOME (`GET/PUT /api/settings/codex-home`; a save re-writes every codex lane's AI Router layer into the new home via `reapplyCodexLayers` in `server/lib/router-apply.js`) and the **Codex hooks** card (`components/settings/CodexHooksSettings.tsx`, `GET/POST/DELETE /api/settings/codex-hooks`). **antigravity** tab (`?tool=antigravity`) → `AntigravityConfig`: Model Pricing for Gemini models (`PricingSettings vendor="google"`) and the **Antigravity hooks** card (`components/settings/AntigravityHooksSettings.tsx`, `GET/POST/DELETE /api/settings/antigravity-hooks`). A tool's tab is hidden while that tool is disabled in AI Tools. Both config panels share a tool-agnostic component set under `client/src/components/config/` (`EditorModal`, `ConfirmDeleteModal`, `FileViewer`, `ToastNotice`, `MdItemList`, `ScopeBadge`, `configPrimitives`) generic over an artifact-type parameter, so the two tools' editors stay visually and behaviorally consistent without duplicating the modal code. Every tab shows the Live / Offline indicator next to the page title | +| `/settings` | SettingsHub | Tabbed shell (`?tool=` query param picks the active tab, `PageTabs`) hosting the dashboard's own settings plus one configuration panel per AI tool. An **AI Tools** button at the tab bar's right edge (`PageTabs` `actions`) opens `components/settings/AiToolsModal.tsx` around `AiToolsSettings.tsx`; `/settings#ai-tools` (the Add-lane modal's link) opens it too, and closing drops the hash. **dashboard** tab (default, no `?tool=`) → `Settings`: an **Appearance** section first (mode + named theme, `localStorage` only), `GET /api/settings/info`, `GET /api/pricing/cost` + `localStorage` for notification prefs; hosts the **Remote Data Sources** panel (`components/RemoteSources.tsx`) — CRUD + test + sync over `/api/remote-sources`, live status from `remote_source.status` WS messages. **claude-code** tab (`?tool=claude-code`) → `CcConfig`: 12-tab Claude Code configuration explorer whose **Overview** also renders `components/settings/ClaudeAgentHubSettings.tsx` (there is no AgentHub sub-tab; the overview's path list leaves CLAUDE_HOME to that editor) holding the dashboard's own Claude Code settings — Model Pricing (`GET/PUT/DELETE /api/pricing`, `POST /api/settings/reset-pricing`), hook status + Reinstall (`GET /api/settings/info`, `POST /api/settings/reinstall-hooks`) and CLAUDE_HOME (`GET/PUT /api/settings/claude-home`; a PUT answers `routers.missing`, the lanes naming an AI Router profile the new home's store lacks, and the UI warns); its **Reset Defaults** sends `{scope: "claude"}`. They were on the dashboard tab until #40 part B, and a tool's own settings go in that tool's tab from now on. Both editable home directories render through `components/settings/HomeDirSetting.tsx` and are saved to AgentHub's settings file (`server/lib/settings-env.js`: `~/.config/agenthub/settings.env`, never the plugin cache's `.env`; load order real environment > the checkout's `.env` > that file), with a warning when the real environment or `.env` owns the variable (`env_owned`); both PUTs carry `sameOriginGuard`. Reads via `GET /api/cc-config/{overview,skills,agents,commands,output-styles,plugins,marketplaces,mcp,hooks,hook-scripts,keybindings,statusline,settings,memory}`. Mutations for skills/agents/commands/output-styles/memory — including the per-project file-based auto-memory store (`*.md` under `~/.claude/projects//memory/`, grouped by project and searchable in the Memory tab, with clickable `MEMORY.md` index links that scroll to + highlight the matching fact file) — via `PUT /api/cc-config/file` + `DELETE /api/cc-config/file` (timestamped backups, atomic writes). The Keybindings tab additionally offers a structured inline editor that persists via `PUT /api/cc-config/keybindings` (same backup-first, atomic-write guarantees). `GET /api/cc-config/file?path=…` for single-file viewer. `GET /api/cc-config/backups` for the recovery modal. A **Switch profile** button left of Backups opens the auth-profile modal (`/api/cc-config/profiles*`): save the logged-in Anthropic account, rename it by clicking its name, then switch between accounts — only the login is swapped, so skills / subagents / memory / plugins / marketplaces / settings stay put. There is no name field (the server derives the identifier) and the save button shows only while the live account is unsaved, since one account is one profile. It cannot log in for you (`claude auth login` does that) and a switch applies to new sessions only. Subscribes to `cc_config_changed` WS messages for live refresh on both dashboard mutations and external file edits picked up by `cc-watcher`. The Settings tab (within CcConfig) leads with a client-side **Current configuration** summary that resolves the `/config` options (model, verbose, theme, output style, effort, auto-compact, notifications, …) across user / project / project-local scopes, showing defaults when unset. **codex** tab (`?tool=codex`) → `CodexConfig`: the Codex CLI counterpart — 8 tabs (overview, skills, agents, rules, memory, config, mcp, plugins; marketplaces are folded into the plugins tab rather than given their own, since the CLI treats them as one small add/remove surface). Codex's scope union is `"user"` \| `"project"` only — no `"project-local"`, no per-project file-based memory. Reads/mutates via `GET/PUT/DELETE /api/codex-config/*` (skills/agents/rules/memory/config are file-backed with the same backup-first write path as `cc-config`; mcp/plugins/marketplaces are CLI-owned add/remove only, no artifact editor). Subscribes to `codex_config_changed` WS messages. Its **Overview** also holds (no AgentHub sub-tab) Model Pricing for OpenAI models (`components/settings/PricingSettings.tsx`, the same table Claude Code's Overview shows filtered to `claude-…` rows, each with a **Show all models** switch) CODEX_HOME (`GET/PUT /api/settings/codex-home`; a save re-writes every codex lane's AI Router layer into the new home via `reapplyCodexLayers` in `server/lib/router-apply.js`) and the **Codex hooks** card (`components/settings/CodexHooksSettings.tsx`, `GET/POST/DELETE /api/settings/codex-hooks`). Every other installed, enabled catalog tool (no `launcher`) gets a tab of its own, `?tool=` → `AiToolConfig`: where the CLI lives, its homepage, an **Update…** link into the AI Tools modal, its session count and cost (`GET /api/ai-tools/:id/usage`) and its lanes (links to `/workspace?lane=`), a **Default model** picker (`PUT /api/ai-tools/:id {defaultModel}`) for a tool with a model flag, Model Pricing for its `model.vendor` (Antigravity: Gemini), and the **Config files** its catalog file lists (`GET/PUT /api/ai-tools/:id/config[/:index]`, `server/lib/ai-tool-config.js`). Antigravity's tab (`?tool=antigravity`) adds the **Antigravity hooks** card (`components/settings/AntigravityHooksSettings.tsx`, `GET/POST/DELETE /api/settings/antigravity-hooks`). A tool's tab is hidden while that tool is disabled in AI Tools. Both config panels share a tool-agnostic component set under `client/src/components/config/` (`EditorModal`, `ConfirmDeleteModal`, `FileViewer`, `ToastNotice`, `MdItemList`, `ScopeBadge`, `configPrimitives`) generic over an artifact-type parameter, so the two tools' editors stay visually and behaviorally consistent without duplicating the modal code. Every tab shows the Live / Offline indicator next to the page title | | `/*` | NotFound | None (static 404 page) | ### Activity Feed Interaction Model @@ -1051,7 +1053,7 @@ flowchart TD - Uses Node.js `http` module directly — no dependencies. - Resolution order is **env override → discovery file → default**: - `CLAUDE_DASHBOARD_PORT` forces a single target (no fan-out, no discovery). - - Otherwise `server/lib/server-info.js` reads `~/.claude/.agenthub.json`, prunes dead-PID entries, and returns one live `port` per unique `dataDir` (lowest port wins when two instances share `~/.claude/agenthub`). The handler POSTs to each returned port in parallel. + - Otherwise `server/lib/server-info.js` reads `~/.claude/.agenthub.json`, prunes dead-PID entries, and returns one live `port` per unique `dataDir` (lowest port wins when two instances share `~/.claude/agenthub`). The handler POSTs to each returned port in parallel. The other instance learns of a lane the hooks moved through `broadcastForeignLaneChanges` (`server/routes/lanes.js`): every `DASHBOARD_LANE_SYNC_MS` (2 s) it reads `PRAGMA data_version`, which moves only on another connection's commit, and re-broadcasts each lane whose `updated_at` changed (and `removed` for a vanished one) to its own browsers. Sessions and events are not re-broadcast this way — a page on that instance shows them on its next fetch. - If neither yields anything, the handler falls back to `4820`. - Per-target promises never reject — a dead listener can't starve the others, and the handler can wait on `Promise.all` for clean exit timing. diff --git a/CLAUDE.md b/CLAUDE.md index 2bbaabb5..b0cfabbe 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,6 +1,6 @@ # Claude Code Working Guide -_Last synced: v1.8.12 (2026-09-28) + a plugin build installed BESIDE the running one (a terminal `claude plugin update` starts nothing) being reported: `installed_not_running` + `update_available` in `pluginStatus` (`server/lib/update-check.js`, via `supersededBy`, no network), the sidebar's ** installed, not running**, the update modal keyed on it, `plugin-update-helper.js` handing over to the recorded install first; and a plugin install whose release lookup failed reading **Could not check for updates** instead of **Up to date** (`Sidebar.tsx`) + a markdown PREVIEW (`client/src/components/MarkdownPreview.tsx`: react-markdown + remark-gfm, raw HTML never run, frontmatter as a code block) behind a **Preview**/**Raw** toggle in the config `FileViewer` and the Changes panel's whole-file view + AI Tools moving from a Settings → Dashboard section to a MODAL behind a button at the Settings tab bar's right edge (`client/src/components/settings/AiToolsModal.tsx`, `actions` on `PageTabs`, opened by `/settings#ai-tools` too); its rows an enable switch + Default badge/Set default + **Update** for EVERY tool whose catalog names an updater (no version lookup on open — `api.aiTools.versions` left the client, `GET /api/ai-tools/versions` stays), the not-installed tools folded behind a disclosure, a finished job's log folded to one line; `codex` updating through its own `codex update` (standalone or npm) and `openclaw`/`hermes` gaining `update --yes`; update-all running every installed updater, skipping an npm one only when npm does not manage the binary, with no `npm view`; the shared `client/src/components/AiToolIcon.tsx`; the Add-lane tool picker being ONE button showing the chosen tool that opens the enabled tools as buttons (no Default/Other… select); and the Claude Code / Codex **AgentHub** sub-tab folded into their **Overview** (home editor, hooks, pricing under the overview card; the overview's path list drops CLAUDE_HOME/CODEX_HOME; `HomeDirSetting` shows the saved value only while the field differs) + ANTIGRAVITY PARITY (#40 part D: `server/lib/antigravity-usage.js` reads agy's own per-conversation SQLite files `~/.gemini/antigravity-cli/conversations/.db` read-only every `DASHBOARD_ANTIGRAVITY_POLL_MS` — tokens from `gen_metadata`'s protobuf `ModelUsageStats`, cwd from `trajectory_metadata_blob`, name/start from `brain//…/transcript_full.jsonl` — into `tool = 'antigravity'` sessions, mtime per file in the new `antigravity_conversations` table, and sets idle the lane of a session gone quiet that the hooks left running; `server/lib/antigravity-hooks.js` installs on a click the named hook `agenthub` (PreInvocation, PostToolUse, Stop — NEVER PreToolUse) into the global `~/.gemini/config/hooks.json` through `~/.gemini/config/agenthub-hook.sh`, `GET/POST/DELETE /api/settings/antigravity-hooks`; `routes/hooks.js` `antigravityHookInput` maps a CLI hook — `conversationId` → session, `workspacePaths[0]` → cwd, first PreInvocation → UserPromptSubmit — and ignores the IDE's; `server/lib/antigravity-home.js` (`AGENTHUB_GEMINI_DIR`); Gemini families in `pricing-sync.js`; the new Settings → **Antigravity** tab `client/src/pages/AntigravityConfig.tsx` with `PricingSettings vendor="google"` and `AntigravityHooksSettings.tsx`. Its review fixed: an empty `-wal` (our own read-only open leaves one) is not activity; activity counts the session's newest hook event, with a 10-minute window mid-turn; an agy Stop carrying an error lands as `error`, one with `fullyIdle: false` is dropped, and the first PreInvocation after a Stop is a prompt; `agyCliDirs()` for a symlinked `~/.gemini`; both tools' hook installers refuse to overwrite or remove a forwarder that is not theirs (`EFOREIGN`) and keep hooks.json's mode. The C2 review also fixed: Codex hooks matched by exact command, one hook at a time, so a user's hook in our group or with a similar name survives; `sameOriginGuard` on the codex-hooks writes; `codexHomes()` in `codex-home.js` for a symlinked CODEX_HOME; a missing main agent created for a poll-imported Codex session; `refreshScript` leaving a foreign file alone; Refresh on the card. The whole-change review also fixed: settings load real env > repo `.env` > the settings file (located after `.env` loads, so `AGENTHUB_SETTINGS_FILE` may live there), a `.env` key `env_owned` too, `writeSetting` replacing every earlier line for its key and writing through a symlink, `validateHomeDir` comparing device+inode with the home and each ancestor, `sameOriginGuard` on both home PUTs, a claude-home PUT answering `routers.missing` (`lanesWithMissingRouter` in `router-apply.js`), CLAUDE_HOME-derived paths read per call in the hook status, `install-hooks.js` and `import-history.js`; `reset-pricing` `{scope: "claude"}` from the Claude tab; `GET /api/sessions?tool=` before the limit; `clear-data` emptying `codex_rollouts` and `antigravity_conversations`, plus `idx_codex_rollouts_thread`; Compaction tagging for claude only, a Codex sub-agent's folded tool event (`_codexChild`) moving no session, agent or lane status, and hooks owning a Codex session's status only while current (`hooksCurrent`: newest event ≥ newest journal write − 2 min, also in `settleActive`); `scriptIsOurs` treating other content, an unreadable file or a directory as foreign, a symlinked hooks.json emptied through the link; agy's quiet window 10 min for any session with hook events (2 min without), a settle completing its agents and clearing waiting flags, a PreInvocation a prompt also with no prior event, a non-active session or a last event >2 min old, a `terminationReason` matching /error|fail/ an error Stop, a file that is not agy's recorded with `session_id ''` and retried on change or at start (anything else next pass), and the session id taken from the file name) + CODEX HOOKS (#40 part C2: `server/lib/codex-hooks.js` installs — only on a click in Settings → Codex → AgentHub → **Codex hooks**, `GET/POST/DELETE /api/settings/codex-hooks` — one group per event (SessionStart, UserPromptSubmit, PreToolUse, PermissionRequest, PostToolUse, Stop, Interrupt, SessionEnd) into `$CODEX_HOME/hooks.json`, all through the stable forwarder `$CODEX_HOME/agenthub-hook.sh` (content rewritten each start by `refreshScript`, command never changes, because Codex trust is keyed on the definition); `scripts/hook-handler.js codex` posts `tool: "codex"`; `routes/hooks.js` creates `tool = 'codex'` sessions with no `transcript_path`, skips every Claude-transcript read for them, maps `PermissionRequest` → `Notification` and `Interrupt` → `Stop`, folds a sub-agent thread's hooks into the root session (`threadOf` in `codex-usage.js`), and `touchLaneFromHook` resolves the lane by (cwd, tool) with detection for claude only; a Codex session with events keeps its hook-given status in `codex-usage.js`, and `codex-ingest.js` skips a lane whose hooks are current. Also from the C1 review: one `codex_rollouts` row per thread, the resume picker lists Claude sessions only, a failed price fetch waits 15 min, a fork skipped until its first own total, 16 MB per read, usage rewritten whole, root-thread cwd/start/model, the startup cleanup and a lane purge scoped by tool, and the codex lane mirror deciding once per lane with a second of future-mtime slack) + CODEX SESSIONS + TOKEN COST (#40 part C1: `server/lib/codex-usage.js` reads every Codex rollout journal into `sessions` (new column `tool`, `'claude'` default, `'codex'` here) and `token_usage` — offset + parse state per file in the new `codex_rollouts` table, a fork's copied parent history skipped by total vector, sub-agent threads summed into the root session, input = input − cached, exact totals via `setTokenUsage`, no `transcript_path`; `startCodexUsagePoll` on the `DASHBOARD_CODEX_POLL_MS` beat; `allRolloutFiles()` exported from `codex-ingest.js`; `pricing-sync.js` also prices `gpt-…`/`o…`/`codex-…` from LiteLLM's `openai` entries and `reset-pricing` syncs right after reseeding; `AGENTHUB_PRICING_SYNC=0` in `scripts/test-server.js`; `client/src/components/settings/PricingSettings.tsx` = the pricing table with a `vendor` filter + **Show all models**, in both tools' AgentHub sub-tabs; `SessionToolBadge.tsx`; the Workspace attach/resume list offering `tool = 'claude'` rows only) + per-tool SETTINGS (#40 part B: Model Pricing, Hook Configuration and CLAUDE_HOME moved from Settings → Dashboard to the Claude Code tab's **AgentHub** sub-tab, `client/src/components/settings/ClaudeAgentHubSettings.tsx` — a tool's own settings go in that tool's tab; `setClaudeHome`/`setCodexHome` refuse any dir that is or contains the OS home (`EBADHOME`, `validateHomeDir` in `server/lib/settings-env.js`) and persist to AgentHub's settings file `~/.config/agenthub/settings.env` (`AGENTHUB_SETTINGS_FILE`; real env > repo `.env` > that file, loaded only when `server/index.js` is main — never the plugin cache's `.env`, which an update wipes), a first CLAUDE_HOME change pinning `DASHBOARD_DATA_DIR` so the DB stays put; CODEX_HOME editable in Codex → AgentHub (`/api/settings/codex-home`, `HomeDirSetting.tsx`, `reapplyCodexLayers` re-writing codex lanes' router layers into the new home); a codex pane is started with the dashboard's `CODEX_HOME` through tmux `-e`, in `server/lib/pty-run.js`) + AI Tools INSTALL/UPDATE (#40 part A2: catalog fields `install`/`update` honoured only in built-in and release files; `server/lib/ai-tool-jobs.js` runs ONE job at a time as an argv — the login shell's npm, a downloaded vendor script via `bash `, or a tool's own updater; `server/lib/ai-tool-versions.js` offers an npm update only when the binary resolves under `npm prefix -g`; `server/lib/ai-tools-sync.js` copies the newest release's catalog daily when it is newer than the build, off with `AGENTHUB_AI_TOOLS_SYNC=0`; `server/lib/ai-tool-icons.js` caches raster favicons server-side; `shellPath()` in `shell-path.js`; `repoUrlFor` in `update-check.js`; six `/api/ai-tools` routes; Install/Update/Update tools in `AiToolsSettings.tsx`) + the pipeline layer going DORMANT (no template ships: `server/data/pipelines/` deleted, `EMPTY_PIPELINE`, `ENOPIPELINE`, test templates under `server/__tests__/fixtures/pipelines/`) and the workflow skills leaving the plugin (`skills/ship-feature`, `skills/ship-feature-lane`, the 18 stage/ship commands, all four `agents/`, `agenthub skills install`, `agenthub lanes agents install`, `agenthub ship-full|ship-quick`, `POST /api/skills/install`, `POST /api/lanes/:id/agents/install`, and the Superpowers auto-install with `server/lib/skill-plugins.js` and its doctor row) + the AI TOOLS catalog (#40 part A1: `server/lib/ai-tools.js` loads one JSON per AI coding CLI from `server/data/ai-tools/` — 38 tools, data from Orca, MIT — plus overrides in `DASHBOARD_AI_TOOLS_DIR`, detects each by walking PATH, and keeps enable/default in `/ai-tools.json`; `server/lib/shell-path.js` merges the login shell's PATH (`$SHELL -ilc`, 5 s, off with `AGENTHUB_SHELL_PATH=0`) at startup and on Refresh; `/api/ai-tools` GET/refresh/PUT; **AI Tools** (`client/src/components/settings/AiToolsSettings.tsx`, now a modal opened from the Settings tab bar's right edge, `AiToolsModal.tsx`) and the shared `client/src/hooks/useAiTools.ts`, which replaced `LANE_TOOL_LABEL`; the Add-lane modal's **Default ()** + **Other…** picker; the built-in router profile now labelled plain `Native`, rendered **Native ()**; the Claude Code/Codex settings tabs hidden while that tool is disabled; a per-run **Bypass permissions** switch; `POST /api/run` now forwarding `sandbox`/`approval`/`resumeLast`, which it had dropped) + `antigravity` lanes (a third `lanes.tool`, running Antigravity's `agy` CLI; no pipeline, no AI Router, no model/effort picker; the lane card no longer renders a bare tool chip, and the prompt box names the lane's CLI via `LANE_TOOL_LABEL` in `client/src/lib/types.ts`) + a lane whose pane EXITED being released on a server beat (`healAllRunIds` in `server/routes/lanes.js`, every `DASHBOARD_RUN_HEAL_MS`=10 s from `server/index.js`) — a codex/antigravity pane fires no hook, so nothing else re-read it and the lane stayed `running` + a codex Start no longer sending `resumeLast` (only **Resume last session** does) + per-CHANGE-BLOCK review bars (issue #50: `stage-hunk`/`discard-hunk` take an optional `block` — the index of a run of consecutive +/- lines inside the `@@` hunk — and `onlyBlock` in `server/lib/git-diff.js` cuts git's fresh hunk down to it, folding every other block into the side being checked; `changeBlocks`/`LaneDiffChange` in `LaneDiffView.tsx` hang one bar per block, and the arrows land on `data-change-index`) + `codex` lanes losing their pipeline (`codex-ingest.js` mirrors only idle↔running — no stage detection or auto-advance, no `keepDetection`; `codex-nudge.js` deleted; every server start runs `clearCodexPipelines` (`server/lib/lanes.js`: a codex lane's stage columns, `lane_stage_events` and `lane_features` wiped) and `removeStageDoc` (`server/lib/lane-agents.js`: the old `` block taken out of each codex lane's `AGENTS.md`, the file deleted when nothing else is left); `agenthub lanes agents install` installs nothing on a codex lane; the Workspace hides stage, time-on-stage, pipeline/feature pickers and PipelineMap for one) + `info/exclude` entries AgentHub writes into a lane going to the COMMON git dir with the lane's path prefix (`server/lib/git-exclude.js`, used by `router-apply.js` and `lane-mcp.js` — a linked worktree's own `info/exclude` is never read by git, and a subdir lane has no `.git`) + macOS start-at-login (`scripts/launchd-service.js`: the LaunchAgent `~/Library/LaunchAgents/io.smartgift.agenthub.plist`, same API as `systemd-service.js`, chosen by `scripts/service-manager.js` — which the bootstrap, `agenthub service` and `/agenthub-doctor` now require instead of `systemd-service` directly) + v1.8.6 (2026-09-24) + four fixes found by the #37 evaluation, shipped without #37 itself: tmux targets are EXACT (`=name` for has-/kill-/attach-session, `=name:` for pane and option targets, in `server/lib/tmux.js`, `pty-attach.js` and `scripts/cc-update-all-helper.js`) because a bare `-t agenthub-lane-1` resolves by prefix to `agenthub-lane-12` once lane 1 is gone; `getRun`/`killRun` in `server/lib/pty-run.js` refuse any id that is not `agenthub-lane-`, so `DELETE /api/run/main` can no longer kill a user's own tmux session; `POST /api/lanes/:id/router/apply` reports `restart_required` only when the pane's status is `running` (it was always true, since `getRun` returns a `gone` handle); and a lane's `needs_action` is cleared only by the session that raised it (new nullable `lanes.needs_action_session`, `server/routes/hooks.js`) + macOS support, code-audited and untested on a Mac (#22): `server/index.js` deletes a DENYLIST of parent-session variables at startup (`PARENT_SESSION_VARS`: CLAUDECODE, CLAUDE_CODE_SESSION_ID, CLAUDE_CODE_MESSAGING_*, CLAUDE_PLUGIN_ROOT, AI_AGENT, …, plus GIT_EDITOR only when it is Claude Code's `true`; user config such as CLAUDE_CONFIG_DIR/CLAUDE_CODE_USE_BEDROCK/ANTHROPIC_* passes through), because a server started from a Claude Code session handed them through tmux to every lane pane; the `~/.local/bin/agenthub` launcher falling back to `node` on PATH when its pinned node is gone (`linkCli` in `scripts/plugin-bootstrap.js`); `liveServers` ignoring a discovery entry whose `startedAt` predates this boot, so a recycled pid is never a live dashboard; lane cwds stored by real path (`canonicalCwd` in `server/lib/lanes.js`, used by `createLane`, `POST /api/lanes/ensure` and the liveness reaper — `/tmp` becomes `/private/tmp`, a not-yet-existing path through its deepest existing ancestor, existing rows untouched); an `__setExecImpl` seam in `server/lib/session-liveness.js`; and the `INSTALL.md` § macOS section with the Mac checklist + issue #34's empty-repo half — `publish` as a git write (`server/lib/git-ops.js`: `remote add origin -- ` + `push -u origin HEAD`, only in a repo with a commit and NO remote, URL allowlisted by `validateRemoteUrl` to https-without-userinfo/ssh:///scp-like, `origin` removed again on a failed push; `ENOCOMMITS`/`EREMOTEEXISTS`/`EBADURL`), an unborn repo being readable (`gitFacts`/`statusCounts` in `server/lib/worktree.js`: `head: null`, new `unborn`, branch via `symbolic-ref`), `POST /api/lanes/worktree` answering `400 EEMPTYREPO` instead of leaving a `failed` row, and `LaneGitBar.tsx` gaining **Publish…** (no remote), **Publish branch** (remote but no upstream) and a commit-first hint (unborn) while `AddLaneModal.tsx` steers an empty repo to Repo mode + a dashboard fetch/pull/push that needs a git LOGIN asking for one in the git bar (issue #45: `409 EGITAUTH` with `auth`, resent with `credentials`; AgentHub's own shared store `server/lib/git-credentials.js` + helper `server/lib/git-credential-helper.js`, `/api/git-credentials`, Settings → **Git credentials** = `client/src/pages/GitCredentialsConfig.tsx`) + MERGE and conflict resolution from the dashboard (issue #45 part B: `merge`/`resolve`/`merge-commit`/`merge-abort` in `server/lib/git-ops.js`, `gitFacts.merging`/`conflicts`, the git bar's **Merge into current** and diverged-upstream **Merge**, the Changes panel's merge banner and **Merge conflicts** group, and the per-block conflict view `client/src/components/lanes/LaneConflictView.tsx` over the pure parser `client/src/lib/conflicts.ts` — keep mine/theirs/both per block, saved through the whole-file editor's `PUT /git/file` + `resolve done`, no new API) + drag-and-drop of an image onto the lane terminal (`TerminalView.tsx` uploads to `POST /api/run/drop-image` in `server/routes/run.js` — raw body, png/jpeg/gif/webp only, random name under `/agenthub-drops-/`, never a name from the request — then `term.paste()`s the returned path, which is how `claude` attaches it) + Claude Opus 5.5 getting its own pricing row (`claude-opus-5-5%` at 4/20/0.2/5/8, fast 8/40 — `claude-opus-5%` had been pricing it as Opus 5) + AUTOMATIC pricing (`server/lib/pricing-sync.js`, started in `startBackgroundServices`: a model family used in `token_usage` with no `%` row of its own is INSERTED from LiteLLM's public price list, never updated, off with `AGENTHUB_PRICING_SYNC=0`) + the Changes panel at **Full screen** moving the whole Files view (finder + expand-all + tree, one `browsePane`) into a left SIDEBAR and dropping the Files tab and in-place editing (`full` prop on `LaneChangesPanel` from `Workspace.tsx`; a whole file opens for READING and enters the editor only on a click on its text — `onEditAt` on `LaneFileView`, caret at the clicked line, a drag-selection is not a click; Cancel returns to reading, and only a diff keeps an Edit button) and height-locking the page (`viewportLocked` also true at full, else the panel shrank to content with no live run) + the overview splitting into **Staged Changes** / **Changes** groups in the `work` range + a changed tree row tinted whole (`treeMark` tone carries a `bg-status-*/10`) + the Files tree listing untracked-not-ignored files and carrying each entry's git status (`treeStatus`/`treeEntries`/`TREE_LS` in `server/lib/git-diff.js`: file `status` = porcelain code, folder `changed: true`, from ONE `laneStatus` per tree read; `treeMark` in `LaneChangesPanel.tsx` paints M/U/A/D/R) + split panes on ONE lane (every Workspace pane shows the strip's selected lane, each in its own tmux session: `slot` 0–3 on `POST /api/lanes/:id/start`, `agenthub-lane-` for slot 0 = `run_id`, `agenthub-lane--` for 1–3 — `runIdForLane(id, slot)`/`MAX_PANE_SLOT` in `server/lib/pty-run.js`, the `-[1-3]` suffix accepted by `pty-attach.js` and `websocket.js`; `stop`/`reset`/`remove` kill every slot via `laneRunIds`; a pane's Active runs hides what the other panes show; no per-pane lane picker) + the server suite running on a PRIVATE tmux server (`npm run test:server` = `scripts/test-server.js`: drops `TMUX`, sets `TMUX_TMPDIR` to a temp dir, kills that server after — suites spawn and kill real `agenthub-lane-` sessions with ids from their own DB, and on the default server that killed the user's real lane panes, including the one running the tests) + the whole-file EDITOR (`PUT /api/lanes/:id/git/file`, `server/lib/lane-file-write.js`: the read path's guards plus existing-text-file-only and a `baseSha` stale check → `409 ESTALE`; `readFileAt` returns that `sha`) + the per-hunk controls moving into an editor-style review bar under each hunk's last changed line (`HunkBar` in `LaneDiffView.tsx`: ^/v, `n of N`, Undo, Keep) + **Unstage all** on the ONE commit/Stage-all row + Changes sizes Narrow (30%) / Medium (50/50) / Full screen, no 70/30 + the lane card's needs-you banner gone (the strip tile keeps it) + per-HUNK Stage/Undo in the Changes panel's Review and single-file views (`stage-hunk`/`discard-hunk` in `server/lib/git-ops.js`: the hunk is named by its `@@` line and re-matched against a FRESH `git diff -U HEAD` by `hunkPatch` in `git-diff.js`, then `git apply --cached` / `git apply -R` from a temp file — `409 EHUNKGONE` when it moved, `400 EUNTRACKED` on an untracked file; `discard-hunk` is the SECOND irreversible git action and carries the same `confirm: true` gate and two-click UI; the controls reach every `HunkHeader` through a context in `LaneDiffView.tsx`), each Review file header sticky with its own stage/unstage/discard (`FileActions`, shared with the overview row) + the Changes panel gaining a FULL-width step (`CHANGES_SIZES` in `Workspace.tsx`: narrow → 50/50 → 70/30 → full → closed; at full the `lane-detail` section gets `hidden` — CSS, so the one console render site stays mounted), **Stage all** plus the staged-only commit box ABOVE the overview list and sticky (as is the Files finder), and **Expand all folders** being ONE `GET /api/lanes/:id/git/tree-all` (`listTreeAll` in `server/lib/git-diff.js`: one `ls-files`, every level shaped like `listTree`'s) instead of a request — and a `git ls-files` — per folder + the lane's git section MOVING from the lane card to the top of the Changes panel (`client/src/components/lanes/LaneGitHeader.tsx`: `useLaneGitFacts`, the working-copy line, `LaneGitFilesPanel` and `LaneGitBar`, sharing ONE facts read with `LaneChangesPanel`, which refreshes it after every stage/unstage/discard/commit while a git bar action re-reads the diff; `onNewLane` comes from `Workspace.tsx`, and `LaneCard` has no git row and no `onNewLaneFromRepo` any more) and the git bar losing its commit UI — the overview's staged-only box is the ONE commit path, the server's `commit` without `only` is unchanged; the git line and its icon-only actions are ONE row in one card, the panel toolbar ONE wrapping row, and `LaneCard` hides itself (`empty:hidden`) when it has nothing to show + the run form's `/` and `@` autocomplete being REAL again (`PromptEditor` in `client/src/components/run/RunSetup.tsx` — the `2f39f4e` rewrite had left a bare textarea: `/` offers the run cwd's project + user commands and skills, via `api.ccConfig.commands|skills(scope, cwd)`, plus the CLI built-ins, claude lanes only; `@` offers `GET /api/run/files`; Enter picks while the list is open and sends otherwise) + the console's **Active runs** listing only the CURRENT lane's runs and external sessions (`LaneConsolePane.tsx`: runs by `laneId`, sessions by the longest path-boundary lane cwd, `owningLaneId`) + v1.8.3 (2026-09-23) + `GET /api/settings/info` reporting `server.memory.heapLimit` (V8's `heap_size_limit`), which the Dashboard's V8 Heap bar and Health Score now divide by instead of `heapTotal` + every `claude plugin install|update -y` (`cc-plugin-cli.js`, `skill-plugins.js`, both detached update helpers, `/agenthub-update`) retrying WITHOUT `-y` when an older Claude Code answers `unknown option '-y'` + `LaneDiffView.tsx` WINDOWING a diff above `WINDOW_THRESHOLD` (500) items at a fixed `ROW_PX` row height (`visibleRange`, spacers, a `ch` min-width off the longest line; at or below the threshold the DOM is unchanged) and clamping its mirrored split scroll (`clampScroll`/`follow`) + a git action that git itself refuses (a numeric exec exit status) answering 409 `EGIT` instead of 500 with a number for a code (`gitErrorCode` in `server/routes/lanes.js`), and one `parseNumstat` (`server/lib/git-diff.js`) shared by the Changes panel and `gitFacts.files` + the Dashboard heatmap ramp mixing `--accent` into `--surface-0` so it reads on every theme + the lane git surface being HARDENED (`server/lib/git-diff.js`, `server/lib/git-ops.js`, `server/lib/worktree.js`: every lane git call runs with `GIT_LITERAL_PATHSPECS=1` and `-z` porcelain, is scoped to the LANE's cwd — a subdir lane sees and acts on only its own paths, `gitFacts.files` included — and reads through `lanePath` at every rev; any file whose basename is `settings.local.json`, the lane router key file, is refused as `ESECRET`/403 on disk and at every rev; `commit` refuses with `ESTAGEDOUTSIDE` while anything outside the lane is staged, checked with `--no-renames`; `push` without a resolvable remote is `ENOREMOTE`; git writes serialize on the repo's absolute git dir, not the lane id) + the xterm terminal following the selected DARK theme (`client/src/lib/terminalTheme.ts`, live via `term.options.theme`) and chart colours using structural tokens + the Add-lane modal asking `POST /api/lanes/ensure` with `exact: true` (#42) — without it a directory NESTED inside an existing lane resolves to that parent lane with `created: false`, so the modal closed on the parent and no new card ever appeared; `exact` counts only a lane on that very directory, and a `created: false` answer now names the lane that owns the folder instead of closing. The CLI, the run console and Sagi keep the parent lookup + Settings → **Appearance** (`client/src/components/settings/AppearanceSettings.tsx`: mode Dark/Light/**System** — `prefers-color-scheme`, followed live — plus one NAMED theme remembered per mode: dark Default/Darcula/Apple Dark, light Default/Apple Light/IntelliJ Light/Canvas). A theme is a CSS-variable override block in `client/src/index.css` keyed by `data-theme` on `` (`.dark[data-theme=…]` / `[data-mode="light"][data-theme=…]`, `data-mode` is set on `` too), and `--tint-base`/light `--syn-plain` are now `var(--surface-3)`/`var(--fg-primary)` so no theme restates them; `hooks/useTheme.ts` is a `useSyncExternalStore` reader over `localStorage` (`theme` = `dark|light|system`, `agenthub.theme.dark|light`) that caches NOTHING in the module, so the Sidebar and Settings can never disagree — do not reintroduce per-instance `useState` there; an inline `