feat: v2.5.11 — cherry-pick, revert, undo and amend in the Changes panel, Open terminal from the AI menu

Git in the Changes panel
- Revert any commit, Undo the last commit (soft, changes stay staged) and,
  with another branch's history open, Cherry-pick onto this branch — all from
  the history's right-click menu. Amend folds staged changes into the last
  commit. Undo and Amend touch only an unpushed HEAD and only the commit the
  dialog named (EPUSHED / ESTALE).
- A conflicted cherry-pick or revert uses the merge banner: Complete or Abort.
  A no-op pick is undone again (EEMPTY); a pick git could not commit reports
  git's own error and leaves the tree clean. A rebase started in a terminal
  blocks every history action.
- Push, Publish, branch delete, cherry-pick, revert and amend ask first. An
  armed discard reads as a red "Discard?" chip. There is no bulk discard.
- History can show another branch (?ref=). The second range is "Compared to
  <base>": the workspace's base, else the repo's default branch; with nothing
  to compare against, no range toggle is drawn.
- The diff editor gets each change block's Undo / Keep bar.

Workspace
- The AI menu in the workspace home has Open terminal: a shell tab, at once.
- "+ Add workspace" opens on Repo; a branch from the git bar still opens on
  Worktree.
- The sidebar no longer keeps a list of closed Terminal tabs.
- Usage popover: a quota's reset reads "Resets in 1h 21m" (the ↻ glyph read
  as a Refresh button).

Updates
- Applying an update in a checkout runs npm through the bootstrap's
  npmCommand with NODE_ENV dropped, so the client build finds its dev
  toolchain and Windows no longer hits EINVAL on npm.cmd.

Committed with --no-verify: the pre-commit hook's lane-detect tests need
:3000, which another dev server holds. Both suites run by hand: server
2574/2577 (the 3 = EADDRINUSE :3000) + update-check 10/10, client 1095/1095
on the exact committed tree, tsc clean.
This commit is contained in:
2026-10-07 15:05:46 +07:00
parent 9a54978eef
commit f0656603c3
41 changed files with 1762 additions and 535 deletions
+36 -8
View File
@@ -333,7 +333,7 @@ When the session's tool is Codex, three fields control Codex-specific behavior:
- **`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 session to start, `0`–`9` (ten per workspace) 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 ten 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–9 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): the login shell tmux would run (`$SHELL -l`) in the workspace's folder, started only once a client is attached, so at the browser's size (`#{session_attached}` polled, at most 2 s — a prompt printed at tmux's 80 columns and then resized left the cursor on top of it, issue #55), 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`.
- **`shell`** — `true` opens a plain shell in that slot instead of an AI tool (the Workspace's **New terminal** tab, or **Open terminal** in the workspace home's AI menu): the login shell tmux would run (`$SHELL -l`) in the workspace's folder, started only once a client is attached, so at the browser's size (`#{session_attached}` polled, at most 2 s — a prompt printed at tmux's 80 columns and then resized left the cursor on top of it, issue #55), 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.
@@ -572,13 +572,14 @@ that file.
file, **1 MB** of patch in a `full` read. A cap that bit sets `truncated` on
the response and on the file it cut.
- Read-only, no same-origin guard. `available: false` (HTTP **200**) for a cwd
git cannot read; `400 ENOBASE` when `range=branch` on a workspace with no base
branch, and `400 ENOFILECHANGE` for a `path` with no changes in that range.
git cannot read; `range=branch` compares against the workspace's base branch,
else the repo's default branch (what `origin/HEAD` names, else `main`/`master`)
— `400 ENOBASE` when neither exists — and `400 ENOFILECHANGE` for a `path` with no changes in that range.
#### Read a workspace's commits, a file, or the file tree
```http
GET /api/lanes/:id/git/log?limit=30[&file=<path>]
GET /api/lanes/:id/git/log?limit=30[&file=<path>][&ref=<branch>]
GET /api/lanes/:id/git/file?path=<path>[&rev=work|HEAD|<sha>]
PUT /api/lanes/:id/git/file {path, content, baseSha}
GET /api/lanes/:id/git/raw?path=<image>[&rev=work|HEAD|<sha>]
@@ -595,7 +596,9 @@ answer `{available:false}` with HTTP **200** for a cwd git cannot read.
refs, parents}]}`, newest first in `--date-order` (no parent before its
children; `parents` = parent shas, first parent first), `limit` clamped to 200. `file=` narrows it to the
commits touching that path — relative, no `..` segment, else
`400 EBADPATH`. Reading one commit's diff is
`400 EBADPATH`. `ref=` reads another branch's history instead of HEAD's
(where the panel's Cherry-pick finds its commits): a local or
remote-tracking branch that exists, else `400 ENOBRANCH`. Reading one commit's diff is
`/git/changes?range=commit&sha=`.
- **`PUT /git/file`** saves the whole-file editor (same-origin guarded;
`server/lib/lane-file-write.js`). Same path guards as the read, plus: the
@@ -723,7 +726,11 @@ cannot collide on `index.lock`.
| `merge` | `git merge --no-edit <ref>` — a local or remote-tracking branch that exists | `{"branch": "origin/main"}` |
| `resolve` | a conflicted file: `checkout --ours\|--theirs` + `add`, or `add` once hand-edited (`done`) | `{"path": "...", "side": "ours\|theirs\|done"}` |
| `merge-commit` | `git commit --no-edit` — concludes the merge | — |
| `merge-abort` | `git merge --abort` | `{"confirm": true}` |
| `merge-abort` | `git merge --abort` (or `cherry-pick --abort` / `revert --abort`, whichever is open) | `{"confirm": true}` |
| `cherry-pick` | `git cherry-pick <sha>` — a non-merge commit, onto a clean repo-root workspace | `{"sha": "9b3e74a"}` |
| `revert` | `git revert --no-edit <sha>` — same guards as `cherry-pick` | `{"sha": "9b3e74a"}` |
| `undo-commit` | `git reset --soft HEAD~1` — only an UNPUSHED, non-merge, non-root HEAD that is still `sha`; its changes stay staged | `{"confirm": true, "sha": "<HEAD the user saw>"}` |
| `amend` | `git commit --amend -m <message>`, or `--no-edit` with no message — only an UNPUSHED HEAD that is still `sha`; takes the index as it stands | `{"message": "...", "sha": "<HEAD>"}` |
```json
{ "action": "commit", "ok": true, "output": "[feat/x 9b3e74a] fix: the thing" }
@@ -731,8 +738,29 @@ cannot collide on `index.lock`.
- `pull` is **always** `--ff-only` and `switch` refuses a dirty tree
(`400 EDIRTY`): neither can invent a merge commit or a conflict underneath a
running agent. There is deliberately no rebase, reset or stash here —
`reset` stays behind the destroy guard on `POST /api/lanes/:id/reset`.
running agent. There is deliberately no rebase, hard reset or stash here —
a hard `reset` stays behind the destroy guard on `POST /api/lanes/:id/reset`.
- **Cherry-pick and revert** follow merge: a commit named by hex sha that
exists (`400 EBADSHA`) and is not a merge commit (`400 EMERGECOMMIT`), a
clean tree, a repo-root workspace. A conflict is a **200** with
`conflicts[]`, resolved with `resolve` and finished by `merge-commit` (which
keeps a pick's original author) or dropped by `merge-abort`; `GET /git` then
reports `merging: true` and `operation: "cherry-pick"|"revert"`. A pick or
revert whose changes are already there is aborted again and answers
`409 EEMPTY`, never left open; one git could not commit (no identity, a
failing signer) is undone the same way and answers git's own error. A
rebase stopped in a terminal makes every history action, `merge-commit` and
`merge-abort` `409 EMERGING` — it is finished where it started.
- **Undo commit and amend** rewrite the last commit, so both are
`409 EPUSHED` once any remote-tracking branch contains HEAD, `409 ESTALE`
when HEAD is no longer the `sha` the request names (`400 EBADSHA` without
one — an agent may have committed since the UI drew the commit), and
`400 ESUBDIR` outside a repo-root workspace. `undo-commit` needs
`confirm: true` and refuses
the first commit (`400 ENOPARENT`) and a merge commit (`400 EMERGECOMMIT`);
it never touches the working tree. `amend` with neither a message nor a
staged change is `400 ENOCHANGES`. All four are `409 EMERGING` while a
merge, cherry-pick or revert is open.
- **Merge** (issue #45) happens only when asked for by name: on a clean tree
(`400 EDIRTY`), from a workspace at the repo root (`400 ESUBDIR` — a merge is
repo-wide), for a branch listed under `refs/heads` or `refs/remotes`