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.
This commit is contained in:
+3
-3
File diff suppressed because one or more lines are too long
@@ -64,8 +64,9 @@ Internal build — all rights reserved.
|
||||
Instructions, Memory, Context files and extra Folders, which Claude and Codex
|
||||
sessions start with.
|
||||
- **Settings as one page per section** (`/settings/<section>`): a left nav
|
||||
grouped General / Agents / Routing / Git, a search box (Ctrl+F) over every
|
||||
section, a page per installed AI tool, per-vendor model pricing, and which
|
||||
grouped General (General / Data / About / Appearance / Pet / Notifications) /
|
||||
Agents / Routing / Cost / Git, a search box (Ctrl+F) over every section whose
|
||||
results deep-link a row's sub-tab and anchor, a page per installed AI tool, per-vendor model pricing, and which
|
||||
untracked worktrees a workspace's Add folder suggests.
|
||||
- **Claude Code + Codex configuration explorer.** Settings hosts one page per
|
||||
tool: every plugin, skill, subagent, slash command, MCP server, hook, settings
|
||||
|
||||
+6
-1
@@ -58,7 +58,8 @@ The client is a single-page application (SPA) built with modern web technologies
|
||||
|
||||
Other rules that fall out of this:
|
||||
- **Light mode's hue family is warm, dark mode's is cool.** The light ramp is Claude's own ivory/terracotta (`#e8e5da` → `#fcfbf7`, warm neutrals, warm `--shadow-card`), not Radix slate: a cool near-white page reads as glare at any lightness, because the brightest thing on screen is also the bluest. Dark mode stays slate + blue — a warm accent on a near-black page is a different problem with a different answer.
|
||||
- Light `--accent` is **not** the dark one, in hue or in step. Dark's `#0090ff` is 3.3:1 on white; Claude's brand terracotta `#cc785c` is 3.1:1 on ivory and 3.3:1 under white text, so it works as a large fill and fails as a link or a button label. Light mode uses it two steps down, `#a34a28` — 5.3:1 as text on a card, 5.9:1 white-on-accent.
|
||||
- Light `--accent` is **not** the dark one, in hue or in step. Dark's `#0090ff` is 3.3:1 on white; Claude's brand terracotta `#cc785c` is 3.1:1 on ivory and 3.3:1 under white text, so it works as a large fill and fails as a link or a button label. Light mode uses it three steps down, `#924224` (it was `#a34a28`, which missed 4.5:1 as text on its own selected chip).
|
||||
- **White text sits on `accent-solid`, never on `accent`.** `--accent` is a TEXT colour; in the dark themes it is the light step (Default `#70b8ff`, Orca Dark `#6aa0f8`) that reads on a near-black card and is far too light to carry white. Every theme also defines `--accent-solid`/`--accent-solid-hover` (Tailwind `bg-accent-solid`) for the fill behind white text — `.btn-primary`, a Switch's on state, a Checkbox tick — darkened until white clears 4.5:1 (Default `#0076d1`). In the light themes the text accent is already dark enough, so `--accent-solid` is simply `var(--accent)`.
|
||||
- `--fg-muted` carries real content (timestamps, counts), so it is held at ≥4.5:1 (`#686359`, 5.4:1 on a card), not at the decorative value the Radix step suggests.
|
||||
- Code colouring goes through the `syntax.*` tokens (`--syn-*`), never fixed Tailwind steps: the dark set is pastel (300-level), the light set is the same hues four steps darker, because a pastel keyword on a light code block is unreadable.
|
||||
- Anything that assumes a dark background — a hardcoded near-black panel, `hover:text-white`, `filter: invert(1)` on a form control glyph — is a light-mode bug. Use tokens, or scope the rule under `.dark`.
|
||||
@@ -518,6 +519,8 @@ function App() {
|
||||
}
|
||||
```
|
||||
|
||||
The heavy pages — `Workspace` (xterm, the diff viewer), `SettingsHub` and `SessionDetail` — are `React.lazy` imports in `App.tsx`, each route wrapped in `<Suspense>` (the `Page` helper), so they load on first visit instead of in the initial bundle. `App.tsx` also mounts the app-wide `<Toaster />` and `<ConfirmHost />` once, beside `<GlobalTooltip />`.
|
||||
|
||||
### Navigation Flow
|
||||
|
||||
```mermaid
|
||||
@@ -627,6 +630,8 @@ graph TB
|
||||
style Success fill:#10B981
|
||||
```
|
||||
|
||||
A thrown error carries the server's `error.code` as `code` and the HTTP `status` (plus `runId` from a `409 ESESSIONLIVE` start, `auth` from `409 EGITAUTH`). A `200` that is not JSON — the SPA's `index.html` from a proxy or a dev server that missed the route — throws `code: "EBADRESPONSE"` with the translated `errors:badResponse` message instead of `Unexpected token '<'`. Pages surface a failure through `toast` (`lib/toast.ts`, rendered by `components/Toaster.tsx`; an error toast stays until dismissed) rather than `alert()`, and ask for confirmation through `confirmDialog`/`confirmDiscard` (`lib/confirm.ts`, rendered by `components/ConfirmHost.tsx`) rather than `window.confirm`.
|
||||
|
||||
---
|
||||
|
||||
## UI Components
|
||||
|
||||
+50
-2
@@ -12,6 +12,8 @@ Complete REST API and WebSocket documentation for Agent Dashboard.
|
||||
- [REST API](#rest-api)
|
||||
- [Sessions](#sessions)
|
||||
- [Agents](#agents)
|
||||
- [Events](#events)
|
||||
- [Data cleanup](#data-cleanup)
|
||||
- [Tools](#tools)
|
||||
- [Metrics](#metrics)
|
||||
- [Pricing](#pricing)
|
||||
@@ -279,11 +281,13 @@ When the session's tool is Codex, three fields control Codex-specific behavior:
|
||||
- **`router: "<name>"`** — the AI Router profile THIS session runs on (omitted = native, or for a resume the router its session last ran on); see § AI Router profiles. `400 EBADROUTER` for an unknown name, `400 EUNSUPPORTED` on a tool other than Claude/Codex.
|
||||
- **`worktree: true`** (Claude workspaces, no `resumeSessionId`) — a NEW session in a git worktree of its own, Claude Code's way: the pane runs `claude --worktree=ah-<id>-<base36 time>` (the `=` form, since `-w [name]` would take the prompt as the name; the name is made by the server), so Claude itself creates `<repo>/.claude/worktrees/<name>` on branch `worktree-<name>`, copies `.worktreeinclude`, runs the user's `WorktreeCreate` hook and cleans up on exit. `/.claude/worktrees/` is added to the repo root's `info/exclude`; the run is recorded with `worktree: 1` (and the run handle carries `worktree: true`). `400 EUNSUPPORTED` on any other tool. A `resumeSessionId` (or fork) of a session that ran in such a worktree starts the pane IN that worktree; one whose worktree is gone is `400 ENOWORKTREE`.
|
||||
|
||||
- **`slot`** — which Workspace tab's session to start, `0`–`3` (default `0`, else `400 EBADSLOT`). Slot 0 is the workspace's own `agenthub-lane-<id>` and is what `run_id` records; slots 1–3 are extra sessions on the same workspace, `agenthub-lane-<id>-<slot>`, and never touch `run_id`/`status`. The response adds `run_id`: the session this start opened. `stop`, `reset` and `remove` kill every slot's session, not only `run_id`.
|
||||
- **`slot`** — which Workspace tab's session to start, `0`–`3` or `"auto"` (default `0`, else `400 EBADSLOT`). `"auto"` lets the server pick the first slot with no live session, under the workspace lock (a client's own pick may come from a stale list); when all four are busy it answers `409 ERUNLIVE` "every tab of this workspace is busy", while a specific busy slot answers `409 ERUNLIVE` "that tab already has a live session". Slot 0 is the workspace's own `agenthub-lane-<id>` and is what `run_id` records; slots 1–3 are extra sessions on the same workspace, `agenthub-lane-<id>-<slot>`, and never touch `run_id`/`status`. The response adds `run_id`: the session this start opened. `stop`, `reset` and `remove` kill every slot's session, not only `run_id`.
|
||||
- **`shell`** — `true` opens a plain shell in that slot instead of an AI tool (the Workspace's **New terminal** tab): tmux's default shell in the workspace's folder, recorded as tool `shell`. Every other field is ignored, nothing comes from the request but the slot, and the workspace's `run_id`/`status` stay as they are (no agent is at work). A slot with a live session answers `409 ERUNLIVE`.
|
||||
|
||||
Error `503 ETMUXMISSING` is returned when the host has no `tmux` binary. Every run the dashboard starts is a tmux session, so this is a missing dependency rather than a bad request — the message carries the install command, and the same code comes back from `POST /api/run`. Nothing else in the dashboard is affected.
|
||||
|
||||
Error `409 ESESSIONLIVE` is returned when `resumeSessionId` (or `resume`) names a session a live pane already holds — two `--resume`s of one session would be two agents writing one transcript. `error.run_id` names that pane; open it instead. A fork (`forkSession: true`) makes a new session and is allowed. A `resumeSessionId` on a catalog tool (anything but Claude/Codex) is `400 EUNSUPPORTED`, since it would silently start fresh.
|
||||
|
||||
Error `409 ERUNLIVE` is returned if the workspace already has a live run; stop it first with the `stop` action. The same code and status come back when the workspace's tmux pane is already running an agent while a `prompt` or `resumeSessionId` was sent — that argv cannot be typed over a live agent, so the request is refused instead of answered 200 with nothing done. A start that asks for neither still adopts the existing pane silently.
|
||||
|
||||
#### Preflight a destructive workspace action
|
||||
@@ -1233,7 +1237,9 @@ Returns all sessions, ordered by most recent activity.
|
||||
|-----------|------|---------|-------------|
|
||||
| `limit` | integer | 50 | Maximum sessions to return (1-1000) |
|
||||
| `offset` | integer | 0 | Pagination offset |
|
||||
| `status` | string | - | Filter by persisted status: `active`, `completed`, `error`, `abandoned`. The UI **Waiting** state is derived from the `awaiting_input_since` column and is not a queryable enum — filter `status=active` and inspect `awaiting_input_since` (non-null = Waiting) |
|
||||
| `status` | string | - | Filter by persisted status: `active`, `completed`, `error`, `abandoned`. The UI **Waiting** state is derived from the `awaiting_input_since` column and is not a `status` value — use `awaiting=1` |
|
||||
| `awaiting` | `1` | - | Only sessions blocked on the user: `awaiting_input_since IS NOT NULL AND status = 'active'` (the UI's **Waiting** filter) |
|
||||
| `from` / `to` | ISO 8601 | - | Inclusive bounds on `started_at`; an unparseable value is ignored. Like every filter here, applied before `limit`/`offset`, so `total` stays exact |
|
||||
| `cwd` | string | - | Exact working directory |
|
||||
| `tool` | string | - | One CLI's sessions: `claude`, `codex`, `antigravity` |
|
||||
| `lane` | integer | - | The sessions a workspace OWNS (`sessions.lane_id`): the ones its panes held. `400 INVALID_INPUT` when not an id |
|
||||
@@ -1397,6 +1403,8 @@ curl http://localhost:4820/api/sessions/sess_abc123/stats
|
||||
{ "tool_name": "Bash", "count": 1842 },
|
||||
{ "tool_name": "Read", "count": 1340 }
|
||||
],
|
||||
"tool_call_count": 5210,
|
||||
"distinct_tool_count": 18,
|
||||
"error_count": 12,
|
||||
"first_event_at": "2026-04-26T18:59:00.000Z",
|
||||
"last_event_at": "2026-04-29T21:30:14.000Z",
|
||||
@@ -1419,6 +1427,8 @@ curl http://localhost:4820/api/sessions/sess_abc123/stats
|
||||
}
|
||||
```
|
||||
|
||||
`tools_used` is the top 15 only, so its sum is not the session's tool-call count. `tool_call_count` is exact: the session's `PreToolUse` events with a `tool_name`, or its `PostToolUse` events when it has none (an imported session). `distinct_tool_count` is the number of different tool names.
|
||||
|
||||
**Error Responses:**
|
||||
|
||||
| Code | Description |
|
||||
@@ -1482,6 +1492,20 @@ curl http://localhost:4820/api/sessions/sess_abc123/agents
|
||||
|
||||
### Agents
|
||||
|
||||
#### List Agents
|
||||
|
||||
```http
|
||||
GET /api/agents
|
||||
```
|
||||
|
||||
Returns `{ agents, limit, offset }`, newest `started_at` first, each agent with its own `cost` (see the note above). Query parameters: `limit` (default 10000), `offset`, `status`, `session_id`, `sources`, and:
|
||||
|
||||
- **`session_ids`** — comma-separated session ids: every agent of those sessions in ONE call (the Dashboard's active trees). Duplicates and blanks are dropped and at most 200 ids are read; `limit` still applies, `offset` does not. An empty list answers `{ "agents": [] }`. When present it takes precedence over `session_id`, `status` and `sources`.
|
||||
|
||||
```bash
|
||||
curl "http://localhost:4820/api/agents?session_ids=sess_abc123,sess_def456"
|
||||
```
|
||||
|
||||
#### Get Agent
|
||||
|
||||
```http
|
||||
@@ -1595,6 +1619,30 @@ sequenceDiagram
|
||||
|
||||
---
|
||||
|
||||
### Events
|
||||
|
||||
#### List Events
|
||||
|
||||
```http
|
||||
GET /api/events
|
||||
```
|
||||
|
||||
Returns `{ events, limit, offset, total }`, newest first, filtered by `event_type`, `tool_name`, `agent_id`, `session_id` (each comma-separated), `q`, `from`/`to` (on `created_at`) and `sources`. Each row carries its labels, so a list page need not fetch every session and agent to name them: `session_name`, `session_cwd`, `session_lane_id`, `agent_name` and `agent_subagent_type` (each `null` when the session/agent row is missing). These columns are REST-only — the WebSocket `new_event` message is unchanged.
|
||||
|
||||
---
|
||||
|
||||
### Data cleanup
|
||||
|
||||
#### Clean up old data
|
||||
|
||||
```http
|
||||
POST /api/settings/cleanup
|
||||
```
|
||||
|
||||
Body `{ abandon_hours?, purge_days?, dry_run? }`. `abandon_hours` marks active sessions silent that long as `abandoned` (and completes their working agents); `purge_days` deletes ended sessions (with their events and agents) that started more than that many days ago. `purge_days` must be a whole number ≥ 1, else `400 INVALID_PURGE_DAYS` — a fraction would purge almost everything. `dry_run: true` answers the same counts (`abandoned`, `purged_sessions`, `purged_events`, `purged_agents`, plus `dry_run: true`) and changes nothing.
|
||||
|
||||
---
|
||||
|
||||
### Tools
|
||||
|
||||
#### List All Tools
|
||||
|
||||
+6
-3
@@ -404,8 +404,9 @@ The dashboard web UI merges workspaces and runs into a single **Workspace** page
|
||||
- **No header row** — the session view has no header of its own: the console's tab bar is it (status, the workspace's name and the back arrow are gone — the name is on the sidebar row and the home). Only a workspace with an installed pipeline template shows its time-on-stage, pipeline picker and feature picker above the console; there are no stage chips — the declared stage and the `auto: <stage>` detection both render in the PipelineMap. While one pane is shown, its split right/down menus sit at the right end of the tab bar; each offers **New terminal** and the same AI tools as the `+` menu. The detail block (the workspace card, the PipelineMap, the proof gallery) is always shown, and Delete lives in the sidebar row menu. There is no Tools menu any more: MCP sync and the sync-base check are the CLI's `agenthub lanes mcp sync` and `agenthub lanes sync-base`, and a readme refresh is a skill typed into a session.
|
||||
- **Detail panel** — the selected workspace's declared stage, its inferred stage when detection leads, a full-width pipeline map, and a legend naming all five node states plus the dashed-amber inferred treatment Hidden while no pipeline template is installed (see "Pipeline stages and the five node states").
|
||||
- **Terminal** — a real interactive terminal (xterm.js) displaying the tmux session's PTY output, with full support for interactive commands, editors, and pagers. A live run keeps its rendered history and scroll position when scrolling. It grabs the keyboard the moment it appears, so typing goes into the pane without clicking it first. A pane has **no run form of its own**: a session with a prompt starts from the workspace home's prompt box. There is no **New run** button: when a pane's process ends (a double Ctrl+C out of `claude`, `exit`, a crash) its pane closes; the last one shows the next open tab, or the home when none is left. A pane with nothing to show offers **New session**, which opens the home too.
|
||||
Keyboard: the terminal keeps Tab and Shift+Tab (`claude` cycles its permission mode on Shift+Tab), so **Ctrl+M** toggles *Tab moves focus* — VS Code's way out of its terminal — and Tab then leaves the pane; **Cmd+F** (macOS) / **Ctrl+Shift+F** opens a find bar. URLs open in a new tab; a selection tmux copies (OSC 52) lands in the browser clipboard, and that path is write-only — nothing in the pane can read your clipboard. `agenthub.terminal.screenReader` = `"1"` in `localStorage` turns on xterm's screen-reader mode. While the socket is down a **Reconnecting…** overlay says keystrokes are not reaching the pane; it re-attaches by itself. In the session view, outside a terminal or a text field, `Alt+1`–`Alt+4` focuses pane 1–4, `Alt+\` splits the focused pane right with a new terminal, and `Alt+W` closes the focused pane (its session stays a tab).
|
||||
- **Clipboard in a pane** — the workspace's tmux session is started with the graphical session's own `DISPLAY`/`WAYLAND_DISPLAY`/`XAUTHORITY`/`XDG_RUNTIME_DIR` (read from `systemctl --user show-environment`, this process's own values winning; `server/lib/desktop-env.js`). The dashboard normally runs as a systemd *user* service, which has none of them, so a pane it started had no clipboard at all and `claude` answered "no image found in clipboard" on every image paste while the same CLI pasted fine in a terminal. Only those four names are forwarded, and only into panes this dashboard spawns — an already-running pane keeps the environment it was started with, so it needs a restart to gain one.
|
||||
- **Tabs** — the session view's console is a tab bar (`client/src/components/run/ConsoleTabs.tsx`), one tab per live tmux session of the workspace — at most four, its slots 0–3: slot 0 is the workspace's `agenthub-lane-<id>` (its `run_id`), slots 1–3 are `agenthub-lane-<id>-<slot>`. A tab shows its AI tool's icon and name (or **Terminal**); the active tab is the focused pane's, which is the URL's `?run=`, so the Changes panel and Analyst follow it, and a tab another pane shows carries a grey underline. A click shows a tab in the focused pane (or focuses the pane already showing it), **Ctrl/Cmd+click** — on a tab or on a `+` item — opens it in a split, and a tab drags onto the panes. Every tab with a live run stays mounted and those no pane shows are hidden with CSS, so switching tabs never drops a terminal. A tab's **×** stops its session (`DELETE /api/run/<runId>`) and closes its pane; with no pane left it moves to the next tab, or the home. The **+** menu opens a new tab in the first free slot: **New terminal** (your own default shell in the workspace folder — `POST /api/lanes/:id/start {slot, shell: true}`, recorded as tool `shell`; it is not an agent, so the workspace's `run_id`/`status` are left alone), each installed, enabled AI tool (started bare, on its default model, no prompt), and **Agent settings…** (Settings → Agents). With four tabs open **+** is disabled. EVERY tab is bound to the sidebar's selection. A tab has no header of its own (no title, no **Active runs** list, no **New run**): other sessions are opened from the sidebar or the home's Recents. (The old fixed 1/2/4 split layout and its `agenthub.workspace.splitView` key are gone; **Split panes** replaces them.)
|
||||
- **Tabs** — the session view's console is a tab bar (`client/src/components/run/ConsoleTabs.tsx`), one tab per live tmux session of the workspace — at most four, its slots 0–3: slot 0 is the workspace's `agenthub-lane-<id>` (its `run_id`), slots 1–3 are `agenthub-lane-<id>-<slot>`. A tab shows its AI tool's icon and name (or **Terminal**); the active tab is the focused pane's, which is the URL's `?run=`, so the Changes panel and Analyst follow it, and a tab another pane shows carries a grey underline. A click shows a tab in the focused pane (or focuses the pane already showing it), **Ctrl/Cmd+click** — on a tab or on a `+` item — opens it in a split, and a tab drags onto the panes. Every tab with a live run stays mounted and those no pane shows are hidden with CSS, so switching tabs never drops a terminal. A tab's **×** stops its session (`DELETE /api/run/<runId>`) and closes its pane; with no pane left it moves to the next tab, or the home. The **+** menu opens a new tab in the first free slot, which the server picks (`slot: "auto"`, under the workspace lock): **New terminal** (your own default shell in the workspace folder — `POST /api/lanes/:id/start {slot, shell: true}`, recorded as tool `shell`; it is not an agent, so the workspace's `run_id`/`status` are left alone), each installed, enabled AI tool (started bare, on its default model, no prompt), and **Agent settings…** (Settings → Agents). With four tabs open **+** is disabled. EVERY tab is bound to the sidebar's selection. A tab has no header of its own (no title, no **Active runs** list, no **New run**): other sessions are opened from the sidebar or the home's Recents. (The old fixed 1/2/4 split layout and its `agenthub.workspace.splitView` key are gone; **Split panes** replaces them.)
|
||||
|
||||
- **Split panes** — the console splits like VS Code (`client/src/components/run/PaneGrid.tsx`, model in `client/src/lib/paneLayout.ts`): a binary tree of splits, each side by side or stacked at a ratio, whose leaves are the workspace's slots — at most four panes, one per live session, a session never shown twice. **Ctrl/Cmd+click** a tab, a `+` item or a sidebar session row opens it to the right of the focused pane (the intent rides on the navigation's state, never in the URL); with more than one pane each gets a slim header: **drag** it (or a tab) onto another pane — its edge splits there, its middle swaps the two —, its **▾** shows another live session in it (swapping with the pane that showed it), **Split right / Split down** puts a not-shown session or a **New terminal** beside it, and **×** closes the pane only (its session stays a tab). Dividers drag (or move by arrow keys) between 10% and 90%. Clicking in (or tabbing into) a pane focuses it, `?run=` follows — so the right panel, Analyst and the sidebar read the focused session — and the keyboard goes with it: only the focused pane's terminal takes it when it appears. A pane closes when its terminal reports that its session ended (exited, closed from another browser); the run list itself only prunes a remembered layout on a workspace's first read, and a pane opened here whose session never got listed — one empty read, which the server returns on any tmux error, never wipes a split. A session ending while the home or a replay is shown only closes its pane; the URL stays. The layout is a viewer preference kept per workspace in `localStorage` (`agenthub.workspace.paneLayout`), never on the server. Every pane is an absolutely placed sibling keyed by its slot, so splitting, swapping or resizing never re-parents — and so never remounts — a live terminal; the session view itself fills the page edge to edge, its tab bar and the right panel's activity bar one header strip.
|
||||
- **Workspace grid** — one card per workspace, 1 column, 2 at `md`, 3 at `xl`. Each card carries the workspace id, liveness dot and status, title, declared stage and time-on-stage, the `auto:` chip when detection leads, the kind and CI tags, the working-copy facts from `GET /api/lanes/:id/git`, and the action row. The needs-you message (`needs_action`) is on the workspace list tile only — the detail card no longer repeats it as a banner.
|
||||
@@ -416,7 +417,9 @@ Run history is per workspace, queryable via `GET /api/run/history?laneId=<n>`.
|
||||
|
||||
**Start and Resume are create-or-reuse, and never silently swallow the request.** When the workspace's `agenthub-lane-<id>` tmux session does not exist, it is created with the full argv. When it exists but its pane is sitting at a **shell prompt** (a `agenthub lanes shell` you opened, or a `claude` that has since exited), the argv is typed into that pane — so `--resume` really runs, and an initial prompt really lands, in the session you are already looking at. Only when the pane is running something (a live `claude`, an editor, a build) is the request adopted as-is: attaching shows you what is running rather than typing over it. Before this, an existing session was always adopted silently, so the first Resume after a `agenthub lanes shell` answered `200` while doing nothing at all.
|
||||
|
||||
**A workspace whose `run_id` still points at a live run refuses a second Start/Resume outright** — `POST /api/lanes/:id/start` answers `409 ERUNLIVE` rather than typing a new prompt or `--resume` over a session that is still in flight. The dashboard starts a session only in a slot with no live run (the home, a resume and the tab bar's `+` all take the first free one), and a tab's **×** is what frees a slot (`DELETE /api/run/:id`); the workspace's own `run_id`/`status` self-heal to idle the next time anything reads the workspace (`healRunId` in `routes/lanes.js`), so no separate workspace-side call is needed.
|
||||
**A workspace whose `run_id` still points at a live run refuses a second Start/Resume outright** — `POST /api/lanes/:id/start` answers `409 ERUNLIVE` rather than typing a new prompt or `--resume` over a session that is still in flight. The dashboard starts a session only in a slot with no live run (the home, a resume and the tab bar's `+` all send `slot: "auto"`, so the SERVER picks the first free one under the workspace lock; `409 ERUNLIVE` when all four are busy), and a tab's **×** is what frees a slot (`DELETE /api/run/:id`); the workspace's own `run_id`/`status` self-heal to idle the next time anything reads the workspace (`healRunId` in `routes/lanes.js`), so no separate workspace-side call is needed.
|
||||
|
||||
**A session is open in at most one tab.** A resume (`resumeSessionId`, or `resume`) of a session a live pane already holds answers `409 ESESSIONLIVE` with that pane's `run_id` instead of starting a second agent on the same transcript — open that pane instead. A **Fork** makes a new session and is allowed. A catalog tool other than Claude/Codex cannot resume a session by id (`400 EUNSUPPORTED`).
|
||||
|
||||
The UI operates on a working directory (`cwd`), not a workspace id. Starting a run in a `cwd` that no workspace owns calls `POST /api/lanes/ensure` first, to idempotently find or adopt a workspace for that path; a `cwd` an existing workspace already owns is matched from the loaded workspace list without a round trip. Either way the run is then started through `POST /api/lanes/:id/start` rather than directly through `POST /api/run`.
|
||||
|
||||
@@ -601,7 +604,7 @@ the editor (Whole file toggles back to its diff; a commit still opens as a diff)
|
||||
Edit click, **Save**/**Cancel** stay and wake up once the text differs, Cancel
|
||||
restores the saved text) → closed (`agenthub.workspace.changesSize`
|
||||
alongside `agenthub.workspace.changesOpen`); the collapsed rail reopens it
|
||||
narrow. A stored size from an older build that no longer exists (`wide`) falls back to narrow. Reading a diff and watching a console want opposite amounts of room and
|
||||
narrow. Between the console and the open (not full-screen) panel sits a **splitter**: drag it, or move it with the arrow keys, to any width between 15% and 85%; that fraction is stored as a NUMBER in the same `agenthub.workspace.changesSize` key, and the header button still steps through the presets. A stored size from an older build that no longer exists (`wide`) falls back to narrow. Reading a diff and watching a console want opposite amounts of room and
|
||||
which one is "work" changes several times an hour, so the width is a step, not
|
||||
a setting — and one button beats a width button wedged beside a collapse button
|
||||
in the panel's tightest corner. It is icon-only, and the icon it draws is the
|
||||
|
||||
+69
-6
@@ -636,6 +636,14 @@ components:
|
||||
count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
tool_call_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: 'Exact tool calls: PreToolUse events with a tool_name, or PostToolUse ones when the session has no PreToolUse (an imported session). tools_used is a top-15 list, so its sum is not this.'
|
||||
distinct_tool_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
description: Number of different tool names this session called.
|
||||
error_count:
|
||||
type: integer
|
||||
minimum: 0
|
||||
@@ -1778,9 +1786,12 @@ components:
|
||||
minimum: 0
|
||||
description: Mark active sessions abandoned if stale for this many hours
|
||||
purge_days:
|
||||
type: number
|
||||
minimum: 0
|
||||
description: Delete old completed/error/abandoned sessions older than this many days
|
||||
type: integer
|
||||
minimum: 1
|
||||
description: Delete old completed/error/abandoned sessions older than this many days. A whole number ≥ 1, else 400 INVALID_PURGE_DAYS (a fraction would purge almost everything).
|
||||
dry_run:
|
||||
type: boolean
|
||||
description: true = answer the same counts and change nothing.
|
||||
CleanupResponse:
|
||||
type: object
|
||||
required:
|
||||
@@ -1802,6 +1813,11 @@ components:
|
||||
type: integer
|
||||
purged_agents:
|
||||
type: integer
|
||||
dry_run:
|
||||
type: boolean
|
||||
enum:
|
||||
- true
|
||||
description: 'Present (true) only on a dry run: the counts were not applied.'
|
||||
CcConfigScope:
|
||||
type: string
|
||||
enum:
|
||||
@@ -4773,6 +4789,28 @@ paths:
|
||||
- antigravity
|
||||
description: Only the sessions of one CLI. Applied before `limit`, so a busy Codex lane cannot push Claude sessions off a page.
|
||||
example: claude
|
||||
- name: awaiting
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
enum:
|
||||
- '1'
|
||||
description: '`1` = only sessions blocked on the user (`awaiting_input_since IS NOT NULL AND status = ''active''`), the UI''s Waiting filter. Applied before `limit`/`offset`, so `total` stays exact.'
|
||||
example: '1'
|
||||
- name: from
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Inclusive lower bound on `started_at` (ISO 8601). An unparseable value is ignored. Applied before `limit`/`offset`.
|
||||
example: '2026-10-01T00:00:00.000Z'
|
||||
- name: to
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
format: date-time
|
||||
description: Inclusive upper bound on `started_at` (ISO 8601). An unparseable value is ignored. Applied before `limit`/`offset`.
|
||||
example: '2026-10-05T23:59:59.999Z'
|
||||
- $ref: '#/components/parameters/SourcesQuery'
|
||||
example: local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
|
||||
- $ref: '#/components/parameters/LimitQuery'
|
||||
@@ -5039,7 +5077,7 @@ paths:
|
||||
tags:
|
||||
- Sessions
|
||||
summary: Get aggregated session stats
|
||||
description: 'Returns aggregated counts for the SessionOverview panel: total events, events-by-type, the top 15 tools by usage, an error count (events whose `event_type`/`summary` match /error/i or /failed/i), the event time range, agent type/status counts, the subagent-type breakdown (excluding the special `compaction` type, which is surfaced under `agents.compaction`), and token totals. All aggregation runs in SQL, so it stays cheap even for sessions with tens of thousands of events; the endpoint is read-only with no side effects. The frontend debounces calls on `new_event` / `agent_*` / `session_updated` websocket frames so the counters track a running session. Returns 404 with code `NOT_FOUND` when the session does not exist.'
|
||||
description: 'Returns aggregated counts for the SessionOverview panel: total events, events-by-type, the top 15 tools by usage, the exact `tool_call_count` (PreToolUse events with a `tool_name`, or PostToolUse when the session has none, e.g. an imported one) and `distinct_tool_count` (different tool names) — `tools_used` is a top-15 list, so its sum is not the count —, an error count (events whose `event_type`/`summary` match /error/i or /failed/i), the event time range, agent type/status counts, the subagent-type breakdown (excluding the special `compaction` type, which is surfaced under `agents.compaction`), and token totals. All aggregation runs in SQL, so it stays cheap even for sessions with tens of thousands of events; the endpoint is read-only with no side effects. The frontend debounces calls on `new_event` / `agent_*` / `session_updated` websocket frames so the counters track a running session. Returns 404 with code `NOT_FOUND` when the session does not exist.'
|
||||
operationId: getSessionStats
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/SessionIdPath'
|
||||
@@ -5072,6 +5110,8 @@ paths:
|
||||
count: 121
|
||||
- tool_name: Grep
|
||||
count: 77
|
||||
tool_call_count: 612
|
||||
distinct_tool_count: 9
|
||||
error_count: 6
|
||||
first_event_at: '2026-06-25T14:02:11.052Z'
|
||||
last_event_at: '2026-06-25T14:31:50.119Z'
|
||||
@@ -5252,13 +5292,19 @@ paths:
|
||||
tags:
|
||||
- Agents
|
||||
summary: List agents
|
||||
description: 'Returns agents, most recent first. Filters are applied with precedence rather than composition: when `session_id` is supplied it wins and returns every agent for that session (ignoring `status` and pagination); otherwise a `status` filter returns paginated agents in that lifecycle state; otherwise all agents are returned paginated. `limit` defaults to 10000 when not a positive integer. Read-only, no side effects. Each agent''s `metadata` is returned as a raw JSON-encoded string, not a parsed object.'
|
||||
description: 'Returns agents, most recent first. Filters are applied with precedence rather than composition: `session_ids` (comma-separated, duplicates and blanks dropped, at most 200 ids, `limit` applied but not `offset`, newest `started_at` first; an empty list answers no agents) wins over everything else and returns every agent of those sessions in one call; next, when `session_id` is supplied it wins and returns every agent for that session (ignoring `status` and pagination); otherwise a `status` filter returns paginated agents in that lifecycle state; otherwise all agents are returned paginated. `limit` defaults to 10000 when not a positive integer. Read-only, no side effects. Each agent''s `metadata` is returned as a raw JSON-encoded string, not a parsed object.'
|
||||
operationId: listAgents
|
||||
parameters:
|
||||
- $ref: '#/components/parameters/AgentStatusQuery'
|
||||
example: working
|
||||
- $ref: '#/components/parameters/SessionFilterQuery'
|
||||
example: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d
|
||||
- name: session_ids
|
||||
in: query
|
||||
schema:
|
||||
type: string
|
||||
description: 'Comma-separated session ids: every agent of those sessions in one call (the Dashboard''s active trees). Duplicates and blanks are dropped, at most 200 ids are read, `limit` still applies and `offset` does not; ordered by `started_at` DESC. Takes precedence over `session_id`, `status` and `sources`. An empty list answers `{agents: []}`.'
|
||||
example: b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d,0c4e9a1b-2d3f-4a5b-8c6d-7e8f9a0b1c2d
|
||||
- $ref: '#/components/parameters/SourcesQuery'
|
||||
example: local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11
|
||||
- $ref: '#/components/parameters/LimitQuery'
|
||||
@@ -5499,6 +5545,8 @@ paths:
|
||||
`q` performs a case-insensitive substring (`LIKE %q%`) search across the `summary`, `tool_name`, and the JSON-encoded `data` columns. `from`/`to` are inclusive ISO-8601 datetime bounds on `created_at`; unparseable values are ignored rather than rejected. `limit` is clamped to 1–500 (default 50) and `offset` is clamped to >= 0 (default 0).
|
||||
|
||||
Note: each returned event's `data` field is a **JSON-encoded string**, not a nested object — callers must `JSON.parse` it to inspect the payload.
|
||||
|
||||
Each row also carries its labels, so a list page need not fetch every session and agent to name them: `session_name`, `session_cwd`, `session_lane_id`, `agent_name` and `agent_subagent_type` (nullable; `null` when the session or agent row is missing). They are REST-only — the WebSocket `new_event` frame is unchanged.
|
||||
parameters:
|
||||
- in: query
|
||||
name: event_type
|
||||
@@ -6648,7 +6696,7 @@ paths:
|
||||
tags:
|
||||
- Settings
|
||||
summary: Abandon stale sessions and optionally purge old history
|
||||
description: 'Two-phase maintenance. Phase 1 (`abandon_hours`, non-destructive): marks any still-`active` session with no events newer than that many hours as `abandoned`, and completes its lingering agents — a tidy-up of crashed/orphaned sessions. Phase 2 (`purge_days`) is ⚠ DESTRUCTIVE and IRREVERSIBLE: it permanently DELETES completed/error/abandoned sessions (and their events, agents, and token_usage) whose `started_at` is older than that many days. Active sessions are NEVER purged. Both fields are optional and independent — send only `abandon_hours` for a safe tidy-up, or include `purge_days` to also reclaim disk. Export first if the purged history matters. The response reports counts for each phase.'
|
||||
description: 'Two-phase maintenance. Phase 1 (`abandon_hours`, non-destructive): marks any still-`active` session with no events newer than that many hours as `abandoned`, and completes its lingering agents — a tidy-up of crashed/orphaned sessions. Phase 2 (`purge_days`) is ⚠ DESTRUCTIVE and IRREVERSIBLE: it permanently DELETES completed/error/abandoned sessions (and their events, agents, and token_usage) whose `started_at` is older than that many days. Active sessions are NEVER purged. Both fields are optional and independent — send only `abandon_hours` for a safe tidy-up, or include `purge_days` to also reclaim disk. Export first if the purged history matters. `purge_days` must be a whole number ≥ 1, else 400 `INVALID_PURGE_DAYS` (a fraction like 0.1 would purge almost everything). `dry_run: true` answers the same counts (plus `dry_run: true`) and changes nothing — preview a purge before running it. The response reports counts for each phase.'
|
||||
operationId: cleanupData
|
||||
requestBody:
|
||||
required: true
|
||||
@@ -6666,6 +6714,11 @@ paths:
|
||||
value:
|
||||
abandon_hours: 12
|
||||
purge_days: 90
|
||||
preview:
|
||||
summary: 'Dry run: count what a 90-day purge would delete, change nothing'
|
||||
value:
|
||||
purge_days: 90
|
||||
dry_run: true
|
||||
responses:
|
||||
'200':
|
||||
description: Cleanup result
|
||||
@@ -6679,6 +6732,16 @@ paths:
|
||||
purged_sessions: 57
|
||||
purged_events: 14820
|
||||
purged_agents: 241
|
||||
'400':
|
||||
description: purge_days is not a whole number of days, 1 or more
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: '#/components/schemas/ErrorResponse'
|
||||
example:
|
||||
error:
|
||||
code: INVALID_PURGE_DAYS
|
||||
message: purge_days must be a whole number of days, 1 or more
|
||||
/api/import/guide:
|
||||
get:
|
||||
tags:
|
||||
|
||||
+4
-4
@@ -448,17 +448,17 @@ The OpenAPI spec is generated from `server/openapi.js` (`createOpenApiSpec()`),
|
||||
| Method | Path | Description |
|
||||
| ------- | ------------------- | ------------------------------------------------ |
|
||||
| `GET` | `/api/health` | Server health check (`status`, `version`, `timestamp`) |
|
||||
| `GET` | `/api/sessions` | List sessions (`status`, `limit`, `offset`; `tool=claude\|codex\|antigravity` filters by CLI before the limit) |
|
||||
| `GET` | `/api/sessions` | List sessions (`status`, `limit`, `offset`; `tool=claude\|codex\|antigravity`, `awaiting=1` and `from`/`to` on `started_at` filter before the limit) |
|
||||
| `GET` | `/api/sessions/:id` | Session detail (includes `agents` + `events`) |
|
||||
| `POST` | `/api/sessions` | Create session (idempotent by `id`) |
|
||||
| `PATCH` | `/api/sessions/:id` | Update session |
|
||||
| `GET` | `/api/sessions/:id/transcripts` | List the session's transcript files (main + sub-agents) |
|
||||
| `GET` | `/api/sessions/:id/transcript` | Cursor-paginated message stream for one transcript |
|
||||
| `GET` | `/api/agents` | List agents (`status`, `session_id`, pagination)|
|
||||
| `GET` | `/api/agents` | List agents (`status`, `session_id`, pagination; `session_ids=a,b` — ≤ 200 — wins over the rest)|
|
||||
| `GET` | `/api/agents/:id` | Agent detail |
|
||||
| `POST` | `/api/agents` | Create agent (idempotent by `id`) |
|
||||
| `PATCH` | `/api/agents/:id` | Update agent |
|
||||
| `GET` | `/api/events` | List events (`session_id`, `limit`, `offset`) |
|
||||
| `GET` | `/api/events` | List events (`session_id`, `limit`, `offset`); each row also carries `session_name`/`session_cwd`/`session_lane_id`/`agent_name`/`agent_subagent_type` (REST only, `new_event` unchanged) |
|
||||
| `GET` | `/api/stats` | Dashboard aggregate counters |
|
||||
| `GET` | `/api/analytics` | Analytics aggregates for charts/trends |
|
||||
| `GET` | `/api/metrics` | Prometheus / OpenMetrics exposition (text; v0.0.4) |
|
||||
@@ -693,7 +693,7 @@ Because sync runs non-interactively (`ssh -o BatchMode=yes`), the connection mus
|
||||
| `DELETE` | `/api/settings/antigravity-hooks` | Remove the `agenthub` hook only (`sameOriginGuard`) |
|
||||
| `GET` | `/api/settings/export` | Export all data (sessions, agents, events, token_usage, workflows, dashboard_runs, alert_rules, model_pricing) as one versioned JSON attachment |
|
||||
| `POST` | `/api/settings/import` | Restore a bundle from `/export`. Multipart `file`, or JSON `{ path }` (server reads it). Idempotent + non-destructive: sessions already present are skipped whole |
|
||||
| `POST` | `/api/settings/cleanup` | Abandon stale sessions and purge old data |
|
||||
| `POST` | `/api/settings/cleanup` | Abandon stale sessions and purge old data (`purge_days` whole ≥ 1, else `400 INVALID_PURGE_DAYS`; `dry_run: true` counts only) |
|
||||
|
||||
### Claude Config Explorer (`/api/cc-config`)
|
||||
|
||||
|
||||
@@ -55,7 +55,12 @@ const paths = {
|
||||
"ignored rather than rejected. `limit` is clamped to 1–500 (default 50) and " +
|
||||
"`offset` is clamped to >= 0 (default 0).\n\n" +
|
||||
"Note: each returned event's `data` field is a **JSON-encoded string**, not a " +
|
||||
"nested object — callers must `JSON.parse` it to inspect the payload.",
|
||||
"nested object — callers must `JSON.parse` it to inspect the payload.\n\n" +
|
||||
"Each row also carries its labels, so a list page need not fetch every session " +
|
||||
"and agent to name them: `session_name`, `session_cwd`, `session_lane_id`, " +
|
||||
"`agent_name` and `agent_subagent_type` (nullable; `null` when the session or " +
|
||||
"agent row is missing). They are REST-only — the WebSocket `new_event` frame " +
|
||||
"is unchanged.",
|
||||
parameters: [
|
||||
{
|
||||
in: "query",
|
||||
|
||||
@@ -1205,7 +1205,7 @@ const paths = {
|
||||
tags: ["Settings"],
|
||||
summary: "Abandon stale sessions and optionally purge old history",
|
||||
description:
|
||||
"Two-phase maintenance. Phase 1 (`abandon_hours`, non-destructive): marks any still-`active` session with no events newer than that many hours as `abandoned`, and completes its lingering agents — a tidy-up of crashed/orphaned sessions. Phase 2 (`purge_days`) is ⚠ DESTRUCTIVE and IRREVERSIBLE: it permanently DELETES completed/error/abandoned sessions (and their events, agents, and token_usage) whose `started_at` is older than that many days. Active sessions are NEVER purged. Both fields are optional and independent — send only `abandon_hours` for a safe tidy-up, or include `purge_days` to also reclaim disk. Export first if the purged history matters. The response reports counts for each phase.",
|
||||
"Two-phase maintenance. Phase 1 (`abandon_hours`, non-destructive): marks any still-`active` session with no events newer than that many hours as `abandoned`, and completes its lingering agents — a tidy-up of crashed/orphaned sessions. Phase 2 (`purge_days`) is ⚠ DESTRUCTIVE and IRREVERSIBLE: it permanently DELETES completed/error/abandoned sessions (and their events, agents, and token_usage) whose `started_at` is older than that many days. Active sessions are NEVER purged. Both fields are optional and independent — send only `abandon_hours` for a safe tidy-up, or include `purge_days` to also reclaim disk. Export first if the purged history matters. `purge_days` must be a whole number ≥ 1, else 400 `INVALID_PURGE_DAYS` (a fraction like 0.1 would purge almost everything). `dry_run: true` answers the same counts (plus `dry_run: true`) and changes nothing — preview a purge before running it. The response reports counts for each phase.",
|
||||
operationId: "cleanupData",
|
||||
requestBody: {
|
||||
required: true,
|
||||
@@ -1222,6 +1222,10 @@ const paths = {
|
||||
"Abandon stale sessions, then purge history older than 90 days (destructive)",
|
||||
value: { abandon_hours: 12, purge_days: 90 },
|
||||
},
|
||||
preview: {
|
||||
summary: "Dry run: count what a 90-day purge would delete, change nothing",
|
||||
value: { purge_days: 90, dry_run: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1236,6 +1240,20 @@ const paths = {
|
||||
},
|
||||
},
|
||||
},
|
||||
400: {
|
||||
description: "purge_days is not a whole number of days, 1 or more",
|
||||
content: {
|
||||
"application/json": {
|
||||
schema: { $ref: "#/components/schemas/ErrorResponse" },
|
||||
example: {
|
||||
error: {
|
||||
code: "INVALID_PURGE_DAYS",
|
||||
message: "purge_days must be a whole number of days, 1 or more",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -146,6 +146,30 @@ const paths = {
|
||||
"Only the sessions of one CLI. Applied before `limit`, so a busy Codex lane cannot push Claude sessions off a page.",
|
||||
example: "claude",
|
||||
},
|
||||
{
|
||||
name: "awaiting",
|
||||
in: "query",
|
||||
schema: { type: "string", enum: ["1"] },
|
||||
description:
|
||||
"`1` = only sessions blocked on the user (`awaiting_input_since IS NOT NULL AND status = 'active'`), the UI's Waiting filter. Applied before `limit`/`offset`, so `total` stays exact.",
|
||||
example: "1",
|
||||
},
|
||||
{
|
||||
name: "from",
|
||||
in: "query",
|
||||
schema: { type: "string", format: "date-time" },
|
||||
description:
|
||||
"Inclusive lower bound on `started_at` (ISO 8601). An unparseable value is ignored. Applied before `limit`/`offset`.",
|
||||
example: "2026-10-01T00:00:00.000Z",
|
||||
},
|
||||
{
|
||||
name: "to",
|
||||
in: "query",
|
||||
schema: { type: "string", format: "date-time" },
|
||||
description:
|
||||
"Inclusive upper bound on `started_at` (ISO 8601). An unparseable value is ignored. Applied before `limit`/`offset`.",
|
||||
example: "2026-10-05T23:59:59.999Z",
|
||||
},
|
||||
{
|
||||
$ref: "#/components/parameters/SourcesQuery",
|
||||
example: "local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11",
|
||||
@@ -334,7 +358,7 @@ const paths = {
|
||||
tags: ["Sessions"],
|
||||
summary: "Get aggregated session stats",
|
||||
description:
|
||||
"Returns aggregated counts for the SessionOverview panel: total events, events-by-type, the top 15 tools by usage, an error count (events whose `event_type`/`summary` match /error/i or /failed/i), the event time range, agent type/status counts, the subagent-type breakdown (excluding the special `compaction` type, which is surfaced under `agents.compaction`), and token totals. All aggregation runs in SQL, so it stays cheap even for sessions with tens of thousands of events; the endpoint is read-only with no side effects. The frontend debounces calls on `new_event` / `agent_*` / `session_updated` websocket frames so the counters track a running session. Returns 404 with code `NOT_FOUND` when the session does not exist.",
|
||||
"Returns aggregated counts for the SessionOverview panel: total events, events-by-type, the top 15 tools by usage, the exact `tool_call_count` (PreToolUse events with a `tool_name`, or PostToolUse when the session has none, e.g. an imported one) and `distinct_tool_count` (different tool names) — `tools_used` is a top-15 list, so its sum is not the count —, an error count (events whose `event_type`/`summary` match /error/i or /failed/i), the event time range, agent type/status counts, the subagent-type breakdown (excluding the special `compaction` type, which is surfaced under `agents.compaction`), and token totals. All aggregation runs in SQL, so it stays cheap even for sessions with tens of thousands of events; the endpoint is read-only with no side effects. The frontend debounces calls on `new_event` / `agent_*` / `session_updated` websocket frames so the counters track a running session. Returns 404 with code `NOT_FOUND` when the session does not exist.",
|
||||
operationId: "getSessionStats",
|
||||
parameters: [
|
||||
{
|
||||
@@ -363,6 +387,8 @@ const paths = {
|
||||
{ tool_name: "Read", count: 121 },
|
||||
{ tool_name: "Grep", count: 77 },
|
||||
],
|
||||
tool_call_count: 612,
|
||||
distinct_tool_count: 9,
|
||||
error_count: 6,
|
||||
first_event_at: "2026-06-25T14:02:11.052Z",
|
||||
last_event_at: "2026-06-25T14:31:50.119Z",
|
||||
@@ -580,7 +606,7 @@ const paths = {
|
||||
tags: ["Agents"],
|
||||
summary: "List agents",
|
||||
description:
|
||||
"Returns agents, most recent first. Filters are applied with precedence rather than composition: when `session_id` is supplied it wins and returns every agent for that session (ignoring `status` and pagination); otherwise a `status` filter returns paginated agents in that lifecycle state; otherwise all agents are returned paginated. `limit` defaults to 10000 when not a positive integer. Read-only, no side effects. Each agent's `metadata` is returned as a raw JSON-encoded string, not a parsed object.",
|
||||
"Returns agents, most recent first. Filters are applied with precedence rather than composition: `session_ids` (comma-separated, duplicates and blanks dropped, at most 200 ids, `limit` applied but not `offset`, newest `started_at` first; an empty list answers no agents) wins over everything else and returns every agent of those sessions in one call; next, when `session_id` is supplied it wins and returns every agent for that session (ignoring `status` and pagination); otherwise a `status` filter returns paginated agents in that lifecycle state; otherwise all agents are returned paginated. `limit` defaults to 10000 when not a positive integer. Read-only, no side effects. Each agent's `metadata` is returned as a raw JSON-encoded string, not a parsed object.",
|
||||
operationId: "listAgents",
|
||||
parameters: [
|
||||
{ $ref: "#/components/parameters/AgentStatusQuery", example: "working" },
|
||||
@@ -588,6 +614,14 @@ const paths = {
|
||||
$ref: "#/components/parameters/SessionFilterQuery",
|
||||
example: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d",
|
||||
},
|
||||
{
|
||||
name: "session_ids",
|
||||
in: "query",
|
||||
schema: { type: "string" },
|
||||
description:
|
||||
"Comma-separated session ids: every agent of those sessions in one call (the Dashboard's active trees). Duplicates and blanks are dropped, at most 200 ids are read, `limit` still applies and `offset` does not; ordered by `started_at` DESC. Takes precedence over `session_id`, `status` and `sources`. An empty list answers `{agents: []}`.",
|
||||
example: "b7f3a2c1-4e5d-4a8b-9c2f-1d6e8a0b3c4d,0c4e9a1b-2d3f-4a5b-8c6d-7e8f9a0b1c2d",
|
||||
},
|
||||
{
|
||||
$ref: "#/components/parameters/SourcesQuery",
|
||||
example: "local,4d1f0e2a-7b9c-4c33-8a21-9e0f7b6d4c11",
|
||||
|
||||
+23
-3
@@ -554,6 +554,17 @@ function createOpenApiSpec() {
|
||||
},
|
||||
},
|
||||
},
|
||||
tool_call_count: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
description:
|
||||
"Exact tool calls: PreToolUse events with a tool_name, or PostToolUse ones when the session has no PreToolUse (an imported session). tools_used is a top-15 list, so its sum is not this.",
|
||||
},
|
||||
distinct_tool_count: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
description: "Number of different tool names this session called.",
|
||||
},
|
||||
error_count: {
|
||||
type: "integer",
|
||||
minimum: 0,
|
||||
@@ -1363,10 +1374,14 @@ function createOpenApiSpec() {
|
||||
description: "Mark active sessions abandoned if stale for this many hours",
|
||||
},
|
||||
purge_days: {
|
||||
type: "number",
|
||||
minimum: 0,
|
||||
type: "integer",
|
||||
minimum: 1,
|
||||
description:
|
||||
"Delete old completed/error/abandoned sessions older than this many days",
|
||||
"Delete old completed/error/abandoned sessions older than this many days. A whole number ≥ 1, else 400 INVALID_PURGE_DAYS (a fraction would purge almost everything).",
|
||||
},
|
||||
dry_run: {
|
||||
type: "boolean",
|
||||
description: "true = answer the same counts and change nothing.",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -1379,6 +1394,11 @@ function createOpenApiSpec() {
|
||||
purged_sessions: { type: "integer" },
|
||||
purged_events: { type: "integer" },
|
||||
purged_agents: { type: "integer" },
|
||||
dry_run: {
|
||||
type: "boolean",
|
||||
enum: [true],
|
||||
description: "Present (true) only on a dry run: the counts were not applied.",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user