feat(lanes,ui): per-lane git surface — Changes panel, VS Code-style diff, branch-aware Add lane, redesigned tooltips (#34, #35)

- Changes card beside lane detail with five modes: Files tree, overview,
  Review (whole diff, per-file collapse), single file, commit history.
- LaneDiffView: unified / side-by-side with independent scroll on both axes
  and mirrored scrollLeft, expand/collapse context, intra-line emphasis.
- Staged-only commit box; git actions (fetch/pull/push/stage/unstage/
  discard/commit/switch/branch-create/branch-delete) guarded in git-ops.js.
- Server: git-diff.js + git-ops.js behind GET /api/lanes/:id/git/* and
  POST /api/lanes/:id/git/:action; gitFacts gains upstream/ahead/behind.
- Add lane: new/existing branch, current branch, worktree list, prefill
  from the git bar's "New lane here"; /api/run/cwds gains a repo kind.
- GlobalTooltip upgrades every native title into a themed anchored bubble;
  chart/info popovers use theme tokens.
This commit is contained in:
2026-09-23 09:18:17 +07:00
parent 32a8887419
commit 1b561c22d9
47 changed files with 7354 additions and 895 deletions
+249 -2
View File
@@ -3,7 +3,7 @@
openapi: 3.0.3
info:
title: Agent Dashboard for Claude Code API
version: 1.7.0
version: 1.8.1
description: HTTP API for real-time Claude Code session monitoring, agent lifecycle tracking, analytics, pricing, hooks ingestion, and workflow intelligence.
contact:
name: Nguyễn Ngọc Trí Vĩ
@@ -10977,9 +10977,256 @@ paths:
description: Absolute path to an existing git repository.
responses:
'200':
description: '{branches: string[], current: string | null}.'
description: '{branches: string[], current: string | null, worktrees: {path, branch?, locked?}[]}. The worktrees ride along so the picker can grey out branches git would refuse to check out twice.'
'400':
description: repo is missing, relative, does not exist, or is not a git repo.
/api/lanes/{id}/git/changes:
get:
tags:
- Lanes
summary: A lane's code changes, at one of three depths
description: Backs the Workspace Changes panel. No extra params = a summary of every changed file; full=1 adds every file's hunks (review mode); path=<file> returns that one file's hunks. range=work (default) is HEAD vs the working tree with untracked files included; range=branch is <base_branch>...HEAD; range=commit needs sha= and shows that one commit. context= widens each hunk (default 3, clamped to 5000) — the viewer's expand button. Lines arrive with both gutters resolved. Capped at 300 files, 200 KB of patch per file and 1 MB per full read; a cap that bit sets `truncated`. Read-only, no same-origin guard.
operationId: getLaneChanges
parameters:
- name: id
in: path
required: true
schema:
type: integer
- name: range
in: query
schema:
type: string
enum:
- work
- branch
- commit
default: work
- name: sha
in: query
schema:
type: string
description: 'range=commit only: the commit to show.'
- name: context
in: query
schema:
type: integer
default: 3
maximum: 5000
description: Unchanged lines around each hunk.
- name: path
in: query
schema:
type: string
description: Return only this file, with its hunks.
- name: full
in: query
schema:
type: string
enum:
- '1'
- 'true'
description: Include every file's hunks (review mode).
responses:
'200':
description: '{available, range, base, baseRef, sha, context, files, truncated}, or {available:false} for a cwd git cannot read.'
'400':
description: ENOBASE (range=branch on a lane with no base branch), ENOFILECHANGE (path has no changes in this range), EBADRANGE or EBADREV.
'404':
description: Lane not found.
/api/lanes/{id}/git/log:
get:
tags:
- Lanes
summary: The lane's recent commits
description: Newest first, up to limit (default 30, max 200). file= narrows it to the commits touching one path. Reading a commit's diff is GET /api/lanes/{id}/git/changes?range=commit&sha=. Read-only, no same-origin guard.
operationId: listLaneCommits
parameters:
- name: id
in: path
required: true
schema:
type: integer
- name: limit
in: query
schema:
type: integer
default: 30
maximum: 200
- name: file
in: query
schema:
type: string
responses:
'200':
description: '{available, commits:[{sha, short, subject, author, date, refs}]}.'
'404':
description: Lane not found.
/api/lanes/{id}/git/file:
get:
tags:
- Lanes
summary: One file's whole content at a revision
description: rev=work (the file on disk, untracked included), HEAD, or a commit-ish. The `work` revision is the only one that reads the filesystem and it is realpath-confined to the lane directory. Binary content is reported, never returned. Capped at 1 MB. Read-only, no same-origin guard.
operationId: readLaneFile
parameters:
- name: id
in: path
required: true
schema:
type: integer
- name: path
in: query
required: true
schema:
type: string
- name: rev
in: query
schema:
type: string
default: work
responses:
'200':
description: '{available, path, rev, binary, truncated, lines}.'
'400':
description: EBADPATH, EBADREV or ENOFILE.
'404':
description: Lane not found.
/api/lanes/{id}/git/ls:
get:
tags:
- Lanes
summary: Tracked paths matching a substring (quick-open)
description: Reads `git ls-files`, so ignored files never appear and no skip-list is maintained. Basename hits rank first. Capped at 500 paths. Read-only, no same-origin guard.
operationId: listLanePaths
parameters:
- name: id
in: path
required: true
schema:
type: integer
- name: q
in: query
schema:
type: string
responses:
'200':
description: '{available, paths, truncated, total}.'
'404':
description: Lane not found.
/api/lanes/{id}/git/tree:
get:
tags:
- Lanes
summary: One directory level of tracked files
description: 'The file explorer''s expand-a-folder read: tracked entries under path=, folded into directories at that level. Read-only, no same-origin guard.'
operationId: listLaneTree
parameters:
- name: id
in: path
required: true
schema:
type: integer
- name: path
in: query
schema:
type: string
default: ''
responses:
'200':
description: '{available, path, entries:[{name, path, dir}], truncated}.'
'400':
description: EBADPATH (the path escapes the lane).
'404':
description: Lane not found.
/api/lanes/{id}/git/branches:
get:
tags:
- Lanes
summary: The lane repo's own local branches
description: Feeds the lane's branch switcher. The repository comes from the lane row, never from the query — that is what GET /api/lanes/branches is for. Read-only, no same-origin guard.
operationId: listLaneOwnBranches
parameters:
- name: id
in: path
required: true
schema:
type: integer
responses:
'200':
description: '{available, branches, current, worktrees}.'
'404':
description: Lane not found.
/api/lanes/{id}/git/{action}:
post:
tags:
- Lanes
summary: One git write on the lane's working copy
description: 'fetch (--all --prune), pull (ALWAYS --ff-only — a diverged branch is reported, never merged), push (-u origin HEAD, so only the branch the lane is on), stage/unstage (per file), discard (per file; irreversible, so it REQUIRES confirm:true), commit (git add -A then commit -m <message>, or only:"staged" to take the index as it stands), switch (git switch, -c to create; refuses a dirty tree so an agent''s half-finished work is never carried onto another branch), and branch-create / branch-delete (create never switches; delete is `git branch -d`, refuses main/master/development/develop, the current branch and one held by another worktree). A path in the body must be one `git status` is reporting as changed, so no path outside the lane is reachable. Guarded and serialized per lane under the lane lock. Deliberately no merge, rebase, reset or stash: reset stays behind the destroy guard on POST /api/lanes/{id}/reset. Network actions are killed after 60s.'
operationId: runLaneGitAction
parameters:
- name: id
in: path
required: true
schema:
type: integer
- name: action
in: path
required: true
schema:
type: string
enum:
- fetch
- pull
- push
- stage
- unstage
- discard
- commit
- switch
- branch-create
- branch-delete
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
message:
type: string
description: commit only; required, max 4000 chars.
only:
type: string
enum:
- staged
description: commit only; commit the index as it stands, with no add -A.
branch:
type: string
description: switch, branch-create and branch-delete.
base:
type: string
description: branch-create only; defaults to HEAD.
create:
type: boolean
description: switch only; create the branch.
paths:
type: array
items:
type: string
description: stage, unstage, discard; each must be a path git reports changed.
confirm:
type: boolean
description: discard only; the call is refused without it.
responses:
'200':
description: '{action, ok:true, output} — git''s own stdout+stderr.'
'400':
description: EBADACTION, EBADMESSAGE, EBADBRANCH, EBADPATH, ECONFIRM, EPROTECTED, ENOBRANCH, EBRANCHEXISTS, EBRANCHBUSY, EDIRTY or ENOCHANGES.
'404':
description: Lane not found.
'500':
description: git failed; its stderr is appended to error.message.
/api/lanes/{id}/{action}:
post:
tags: