feat(lanes): split panes on one lane, in-place file editing, hunk review bar

- Split view: every pane shows the selected lane, each in its own tmux
  session (`slot` 0-3 on POST /api/lanes/:id/start; slot 0 stays the
  lane's `run_id`, slots 1-3 are `agenthub-lane-<id>-<slot>`). A pane's
  Active runs hides what the other panes show; stop/reset/remove kill
  every slot. No per-pane lane picker any more.
- Whole-file editor: PUT /api/lanes/:id/git/file (lane-file-write.js)
  with the read path's guards, existing text files only, and a baseSha
  stale check (409 ESTALE). A file it cannot take says why.
- Per-hunk review bar under each hunk's last changed line: prev/next,
  "n of N", Undo, Keep. Review diffs no longer nest their own scroll box
  (unless windowed), so navigation moves the one real scroller.
- Files tree lists untracked-not-ignored files and colours entries by git
  status (M/U/A/D/R), folders holding changes get a dot.
- Changes panel: Unstage all on the single commit/Stage-all row; sizes
  Narrow (30%) / Medium / Full screen, no 70/30.
- Lane card: the needs-you banner is gone (the strip tile keeps it).
This commit is contained in:
2026-09-24 15:49:39 +07:00
parent 67210bcd61
commit d397d3a1f6
29 changed files with 1289 additions and 254 deletions
+15 -1
View File
@@ -2447,6 +2447,16 @@ export const api = {
request<LaneFileContent>(
`/lanes/${id}/git/file?path=${encodeURIComponent(path)}&rev=${encodeURIComponent(rev)}`
),
/**
* PUT /api/lanes/:id/git/file — save the editor's text over the working
* file. `baseSha` is the `sha` it was opened at; a file that changed on
* disk since answers 409 ESTALE rather than being overwritten.
*/
gitSaveFile: (id: number, path: string, content: string, baseSha: string) =>
request<LaneFileContent>(`/lanes/${id}/git/file`, {
method: "PUT",
body: JSON.stringify({ path, content, baseSha }),
}),
/**
* GET /api/lanes/:id/git/ls — tracked paths matching a substring, for
* quick-open. Ignored files never appear (it reads `git ls-files`).
@@ -2611,7 +2621,9 @@ export const api = {
* @returns `{ lane, ok }` — the updated lane (if applicable) or a boolean result.
*/
action: (id: number, action: string, body: Record<string, unknown> = {}) =>
request<{ lane?: Lane; ok?: boolean }>(`/lanes/${id}/${action}`, {
// `run_id` answers a `start`: the session it opened, which for a split
// pane's slot (`body.slot` 1–3) is not the lane's own `run_id`.
request<{ lane?: Lane; ok?: boolean; run_id?: string }>(`/lanes/${id}/${action}`, {
method: "POST",
body: JSON.stringify(body),
}),
@@ -3604,6 +3616,8 @@ export interface RunStartArgs {
export interface RunHandle {
id: string;
laneId: number | null;
/** Which Workspace split pane's session this is: 0 is the lane's own. */
slot?: number | null;
status: RunStatus;
cwd: string | null;
model: string | null;