feat: v2.5.12 — plan and quota for every AI tool, Grok Build and Kimi Code, Quit AgentHub (#65)
Usage panel (status bar): - every account shows its plan (Claude subscriptionType, Codex chatgpt_plan_type, Cursor membershipType, Grok subscriptionTier) - new quota rows: Cursor CLI (cursor.com usage-summary), Antigravity (`agy --print /usage`, two model pools; AGENTHUB_AGY_USAGE=0 never runs agy), Grok Build, Kimi Code, OpenCode Go — each login read from the tool's own file, sent only to its own host while valid, never refreshed - in-flight quota reads are shared between tabs AI Tools catalog: Grok Build (`grok`) and Kimi Code (`kimi`) added; Gemini CLI, Hermes and OpenClaw removed. Also: Quit AgentHub (#65, `agenthub stop`), status-area icon, Add to Chat on an editor selection, Settings in three groups, JetBrains-style Changes panel (ticked files/hunks, ignore, blame, file history), terminal font. --no-verify: :3000 is taken by another dev server (lane-detect tests); both suites run by hand — server 2636/2639 (the 3 are :3000), client 1131/1131.
This commit is contained in:
+80
-24
@@ -409,7 +409,7 @@ The dashboard web UI merges workspaces and runs into a single **Workspace** page
|
||||
- **Scrollbar in a pane** — every pane gets tmux's `mouse on` and `pane-scrollbars on` with a visible style (`pane-scrollbars-style fg=brightwhite,bg=black,width=1`, `enableMouse` in `server/lib/tmux.js`; tmux 3.7+ — one column, since tmux 3.7c reserves `width` columns but paints only the first and a wider bar left a column of stale cells that read as a second scrollbar). tmux keeps no history and draws no scrollbar on the alternate screen, so a `claude` pane under `"tui": "fullscreen"` (`~/.claude/settings.json`) scrolls inside Claude's own UI instead. AgentHub no longer forces it inline (`CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1`): inline, every time the pane narrowed tmux re-wrapped Claude's full-width rules and pushed lines into history that its redraw could not clear, duplicating the banner and transcript. A pane started before this change keeps inline mode until it is restarted. Every pane's CLI (and a Terminal tab's shell) also starts only once a client is attached — 2 s at most — so nothing is drawn at tmux's 80×24 birth size and re-wrapped on the attach (`WAIT_FOR_CLIENT` in `server/lib/pty-run.js`, issue #55).
|
||||
- **Tabs** — the session view's console is a tab bar (`client/src/components/run/ConsoleTabs.tsx`) over the workspace's live tmux sessions — at most ten, its slots 0–9: slot 0 is the workspace's `agenthub-lane-<id>` (its `run_id`), slots 1–9 are `agenthub-lane-<id>-<slot>`. Every tab is a **combo**: a group of one to four split panes, each session in exactly one tab (`PaneGroups` in `client/src/lib/paneLayout.ts`). A one-pane tab shows its AI tool's icon and name (or **Terminal**); a combo tab shows every pane's icon and name, and the most urgent state dot. The active tab is the one on screen and its focused pane is the URL's `?run=`, so the Changes panel and Analyst follow it. A click shows a tab; **Ctrl/Cmd+click** on a one-pane tab or a `+` item brings it into the tab on screen, split to the right (a full tab — four panes — opens it as a tab of its own instead), and a one-pane tab dragged onto a pane joins that tab there. Any tab drags along the bar to reorder it; a pane's header dragged onto the bar takes that pane out into a tab of its own at that place. Dropped on the MIDDLE of another tab (it highlights), a tab — or a pane's header — joins that tab instead, in a fixed order: the second pane goes to the right (left | right), the third under the right pane, the fourth under the left pane (2×2); a drop that would pass four panes reorders instead (`joinGroup`). Every live session stays mounted and those of other tabs are hidden with CSS, so switching tabs never drops a terminal. A tab's **×** stops every session in it (`DELETE /api/run/<runId>`, after a confirm when one is an AI tool); with no tab left it moves to 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 ten sessions 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`): one binary tree of splits PER TAB, each side by side or stacked at a ratio, whose leaves are the workspace's slots — at most four panes in a tab, a session never shown twice. **Ctrl/Cmd+click** a one-pane tab, a `+` item or a sidebar session row brings it into the tab on screen, 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 **×** takes the pane out of this tab into a tab of its own (its session keeps running). A session dropped or swapped in from another tab leaves that 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 tabs and their layouts are a viewer preference kept per workspace in `localStorage` (`agenthub.workspace.paneLayout`, `{groups, active}`; a stored pre-tab single layout reads as one tab), 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.
|
||||
- **Split panes** — the console splits like VS Code (`client/src/components/run/PaneGrid.tsx`, model in `client/src/lib/paneLayout.ts`): one binary tree of splits PER TAB, each side by side or stacked at a ratio, whose leaves are the workspace's slots — at most four panes in a tab, a session never shown twice. **Ctrl/Cmd+click** a one-pane tab, a `+` item or a sidebar session row brings it into the tab on screen, 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, its **⤡** takes the pane out of this tab into a tab of its own (its session keeps running), and its **×** (also in the header's right-click menu) stops just that pane's session — the same `DELETE /api/run/<runId>` and AI-tool confirm as a tab's **×** — without taking it out first. A session dropped or swapped in from another tab leaves that 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 tabs and their layouts are a viewer preference kept per workspace in `localStorage` (`agenthub.workspace.paneLayout`, `{groups, active}`; a stored pre-tab single layout reads as one tab), 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.
|
||||
|
||||
Run history is per workspace, queryable via `GET /api/run/history?laneId=<n>`.
|
||||
@@ -560,8 +560,8 @@ to spawn a sibling worktree workspace on it, and moving this pane's HEAD under a
|
||||
working agent is not. **New workspace here** goes through the normal Add-workspace flow
|
||||
with the fields prefilled — one provisioning path for every workspace, not a second
|
||||
one hidden in the git bar. There is no **Commit** here: committing is the
|
||||
overview's staged-only box below, the one commit path, and it only ever takes
|
||||
what was staged.
|
||||
overview's commit box below, the one commit path, and it only ever takes the
|
||||
files ticked there.
|
||||
|
||||
**Publish…** is the one button whose destination the user types, so the server
|
||||
keeps it narrow: it only gives a remote to a repo that has none (it never
|
||||
@@ -687,10 +687,16 @@ reads the workspace's git surface in five modes:
|
||||
that block alone. The single-file view offers the same bar.
|
||||
A new line's NUMBER is a button too: it opens a comment box under the diff,
|
||||
and **Paste into pane** pastes `<path>:<line> — <text>` into this
|
||||
workspace's pane focused last (`client/src/lib/panePaste.ts`, the same
|
||||
workspace's pane focused last — clicked into, or focused through a tab,
|
||||
Alt+1–4 or the sidebar (`client/src/lib/panePaste.ts`, the same
|
||||
bracketed `term.paste()` an image drop uses) — nothing is sent until the
|
||||
user presses Enter in the pane; with no pane of the workspace open the box
|
||||
says so and keeps the text.
|
||||
In the file editor, a text selection shows Cursor's **Add to Chat** chip
|
||||
above it, which pastes `@<path>#L<from>-<to>` (Claude Code's line-range
|
||||
mention; `#L<n>` for one line — without the `L` Claude Code attaches the
|
||||
whole file) the same way. Other tools (Codex, …) get the same text as a
|
||||
plain hint.
|
||||
- **One file** — clicking a file's path in any list opens just that file.
|
||||
**All files** returns to the list it was opened from — the same mode, at the
|
||||
same scroll offset — because reading a change-set is a walk down a long list
|
||||
@@ -743,27 +749,67 @@ reason — *where you are* (mode, refresh) above, *how you are looking at it*
|
||||
(range, layout, context, whole file) below — because one row wrapped mid-group
|
||||
in every panel narrow enough to sit beside a console.
|
||||
|
||||
**Per-file writes are the panel's, per-repo writes are the git bar's.** Staging
|
||||
one file means nothing unless a commit can take the index as it stands, so the
|
||||
overview grows a commit box the moment anything is staged — above the list and
|
||||
pinned while it scrolls: the message (and its suggest button), the commit button, a **Stage all** that stages every listed file
|
||||
the index lacks in one call and an **Unstage all** that unstages every staged one — one row when the
|
||||
panel has room, the buttons wrapping under the message when it is narrow. The commit sends
|
||||
`only: "staged"`, never `add -A`, and the box is absent with a clean index because
|
||||
there is nothing it could mean. A file you never want committed (a local note, a scratch folder) can be marked **Never stage** (its row's eye-off button, or right-click): **Stage all** and a folder's stage button then skip it, the row is struck through, and the file is drawn orange in the Files tree (`--kept`, apart from the amber of a modified file). The list is kept in this browser per workspace (`agenthub.changes.neverStage.<lane id>`) and git is never told — nothing is written to the repo, and the file's own Stage still works. It is the dashboard's ONLY commit: the git bar
|
||||
above it has none, so no click can sweep the rest of the tree in with `add -A`. **Discard** is the one
|
||||
irreversible action in the panel: it asks twice in the UI, and the server
|
||||
refuses it without `confirm: true` and accepts only paths `git status` is
|
||||
already reporting as changed. In the `work` range the overview splits like VS
|
||||
Code's Source Control: **Staged Changes** (what the next commit takes, each row
|
||||
offering Unstage) above **Changes** (unstaged and untracked, each offering
|
||||
Stage); a partly staged file appears in both. Each group is a folder tree like
|
||||
**Per-file writes are the panel's, per-repo writes are the git bar's.** The
|
||||
commit is JetBrains' (PhpStorm's Commit window without its staging area): in
|
||||
the `work` range the overview lists **Changes** (every tracked change, staged
|
||||
or not) above **Unversioned Files** (untracked), and every file, folder and
|
||||
group header has a checkbox. **Commit** takes exactly the ticked files'
|
||||
working-tree content — `paths`, `git commit --only` on the server — whatever
|
||||
the index holds, so a staged but unticked file stays staged and uncommitted. A
|
||||
tracked change arrives ticked, an unversioned one unticked, and a file the
|
||||
user unticked stays unticked across polls. The commit box sits above the list
|
||||
and stays pinned while it scrolls: the message (and its ✨ suggest button,
|
||||
which reads the ticked files), **Amend** (folds the ticked files into the last
|
||||
commit; nothing ticked rewords it), **Commit (n)** and **Commit and Push**
|
||||
(the same commit, then the git bar's `push`; a failed push leaves the commit
|
||||
made and says so). It is hidden while a merge, cherry-pick or revert is open —
|
||||
a partial commit cannot finish one, **Complete** does. It is the dashboard's
|
||||
ONLY commit: the git bar above it has none, so no click can sweep the rest of
|
||||
the tree in with `add -A`.
|
||||
|
||||
**A commit can take part of a file**, like JetBrains' diff viewer: in Review
|
||||
(and in the editor's bar under each block) every change block of a tracked
|
||||
file has its own checkbox beside its Undo. Unticking one makes the file's box
|
||||
partial (indeterminate) everywhere it is drawn; ticking a block of an unticked
|
||||
file takes just that block; unticking every block unticks the file. A block
|
||||
is remembered by its content (its -/+ lines and how many identical blocks come
|
||||
before it), so a re-read or an edit elsewhere in the file keeps the choice. At
|
||||
Commit the panel re-reads each partial file at the default context and sends
|
||||
its ticked blocks as `hunks` (`{"<path>": [{hunk, blocks}]}`); the server builds
|
||||
the commit in a temporary index from git's own fresh diff and resets the real
|
||||
index entries of those paths to the new HEAD, so what was left out stays an
|
||||
unstaged change in the working tree. The review header of each file carries
|
||||
the same checkbox and **Rollback** (discard) instead of stage/unstage.
|
||||
|
||||
Files are coloured like JetBrains in the Changes list and the Files tree:
|
||||
unversioned orange (`--kept`), added green, modified and renamed blue
|
||||
(`--diff-mod`), deleted grey (struck through in the tree), conflicted red,
|
||||
ignored dimmed.
|
||||
|
||||
An unversioned row offers **Add to VCS** (`stage`, and the file is ticked).
|
||||
Right-click on a file adds **Commit File…** (that file alone ticked, the
|
||||
message box focused), **Jump to Source** (the whole file), **Copy relative
|
||||
path**, **Copy as Patch to Clipboard** (the file's diff as `git apply` takes
|
||||
it), and for an unversioned file **Add to .gitignore** (`ignore`: one anchored,
|
||||
escaped `/path` line appended to the workspace's own `.gitignore`), **Unstage**
|
||||
for a file something else left staged in the index, **Show History** (also on
|
||||
a Files-tree file: `/git/log?file=`, parents rewritten so the graph joins up;
|
||||
a commit opens that file as that commit changed it, and **Whole branch** goes
|
||||
back to the branch's history), and **Refresh**. In Review and in an open file
|
||||
the toolbar's ▲/▼ (and **F7** / **Shift+F7**) jump to the previous / next
|
||||
difference — the editor's own changed blocks, else each block's first diff
|
||||
row — and in an open file ◀/▶ open the previous / next file of the list it
|
||||
was opened from. **Annotate** on an open working file adds a gutter naming
|
||||
each line's last commit (`GET /git/blame`; date and author, the subject on
|
||||
hover, a click opens that commit; a line not committed yet says so).
|
||||
**Discard** is the one irreversible action in the panel: it asks twice in the
|
||||
UI, and the server refuses it without `confirm: true` and accepts only paths
|
||||
`git status` is already reporting as changed. Each group is a folder tree like
|
||||
JetBrains' commit view (`client/src/lib/changeTree.ts`): folders first, a
|
||||
chain of folders holding only one sub-folder compacted into one row
|
||||
(`src/lib`), each folder row showing its file count, folding on a click and —
|
||||
in Staged Changes / Changes — carrying one button that unstages / stages every
|
||||
file under it; the group headers fold too. The fold state is not stored. In the tree, a changed file's
|
||||
whole row takes its status tint, not only its letter.
|
||||
(`src/lib`), each folder row showing its file count and folding on a click;
|
||||
the group headers fold too. The fold state is not stored. In the tree, a
|
||||
changed file's whole row takes its status tint, not only its letter.
|
||||
|
||||
Review, single-file, history and browse do **not** poll: re-fetching under the
|
||||
reader's scroll position is worse than a slightly stale view, and those are the
|
||||
@@ -1499,6 +1545,16 @@ curl -X POST http://localhost:4820/api/lanes/5/git/commit \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message": "wip: staged only", "only": "staged"}'
|
||||
|
||||
# Exactly these files (the Changes panel's ticked ones), untracked included
|
||||
curl -X POST http://localhost:4820/api/lanes/5/git/commit \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"message": "fix: two files", "paths": ["src/a.ts", "notes.md"]}'
|
||||
|
||||
# Add an untracked file to the workspace's .gitignore
|
||||
curl -X POST http://localhost:4820/api/lanes/5/git/ignore \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"paths": ["scratch.log"]}'
|
||||
|
||||
# Irreversible, so `confirm` is required and the path must be one git
|
||||
# already reports as changed
|
||||
curl -X POST http://localhost:4820/api/lanes/5/git/discard \
|
||||
@@ -1753,7 +1809,7 @@ agenthub stage <stage> [--evidence "..."] # Report the current stage
|
||||
|
||||
## Workspaces on any AI tool
|
||||
|
||||
A workspace can run any AI coding CLI in the AI Tools catalog (`server/data/ai-tools/*.json` — ten built in: Claude Code, Codex, Gemini CLI, Antigravity, GitHub Copilot CLI, OpenCode, Cursor CLI (`cursor-agent`), Oh My Pi, Hermes and OpenClaw —, plus any `*.json` in `DASHBOARD_AI_TOOLS_DIR`, which replaces a built-in file with the same `id` but may not set `launcher`). Settings → Agents → **AI tools** (`/settings/agents`; the old `/settings#ai-tools` redirects there) lists what is installed (found on PATH; **Refresh** re-reads the login shell's PATH first) with an enable switch, **Set default** and **Update** (for every tool whose catalog file names an updater — no version lookup, the updater itself reports "up to date"), and, folded behind a disclosure, what is available to install with a homepage link. The Add-workspace modal shows the chosen tool as one button (the default until changed); clicking it lists every installed, enabled tool as buttons to pick from; `agenthub workspace add --tool <id>` does the same from a terminal. The run form's **Model** picker lists the workspace tool's own models (`GET /api/ai-tools/:id/models`; Claude, Codex, and any catalog tool whose file declares `model: { flag }` — the id is validated and passed as `<flag> <id>`), plus **Custom…**, and starts on the tool's default model when one is set.
|
||||
A workspace can run any AI coding CLI in the AI Tools catalog (`server/data/ai-tools/*.json` — nine built in: Claude Code, Codex, Antigravity, GitHub Copilot CLI, OpenCode, Cursor CLI (`cursor-agent`), Grok Build (`grok`), Kimi Code (`kimi`) and Oh My Pi —, plus any `*.json` in `DASHBOARD_AI_TOOLS_DIR`, which replaces a built-in file with the same `id` but may not set `launcher`). Settings → Agents → **AI tools** (`/settings/agents`; the old `/settings#ai-tools` redirects there) lists what is installed (found on PATH; **Refresh** re-reads the login shell's PATH first) with an enable switch, **Set default** and **Update** (for every tool whose catalog file names an updater — no version lookup, the updater itself reports "up to date"), and, folded behind a disclosure, what is available to install with a homepage link. The Add-workspace modal shows the chosen tool as one button (the default until changed); clicking it lists every installed, enabled tool as buttons to pick from; `agenthub workspace add --tool <id>` does the same from a terminal. The run form's **Model** picker lists the workspace tool's own models (`GET /api/ai-tools/:id/models`; Claude, Codex, and any catalog tool whose file declares `model: { flag }` — the id is validated and passed as `<flag> <id>`), plus **Custom…**, and starts on the tool's default model when one is set.
|
||||
|
||||
A catalog file:
|
||||
|
||||
|
||||
Reference in New Issue
Block a user