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
+22
View File
@@ -106,6 +106,8 @@ router.get("/history", (req, res) => {
* - "home": $HOME (the Run page's default cwd and first autocomplete group —
* a neutral spawn location that doesn't inherit this repo's project
* context, issue #202)
* - "repo": source repos existing lanes were provisioned from — the paths
* the Add-lane form's repo field is actually asking for (issue #34)
* - "recent": distinct cwds Claude Code has been used in, sourced from the
* dashboard's own sessions table. Filtered to dirs that still exist.
*
@@ -126,6 +128,26 @@ router.get("/cwds", (_req, res) => {
push("dashboard", process.cwd(), "Dashboard server");
push("home", require("node:os").homedir(), "Home");
// Source repos lanes have already been provisioned from, newest lane first.
// The Add-lane form's repo field used to offer only "recent" session cwds,
// which for a worktree lane is the WORKTREE, never the repo it was cut from
// — so the one path that form actually wants had to be retyped every time.
// These sit above the recents below because a repo is a deliberate choice a
// user made once, not a directory a session happened to run in.
try {
const { db } = require("../db");
const rows = db
.prepare(
`SELECT source_repo AS p, MAX(id) AS last_id FROM lanes
WHERE source_repo IS NOT NULL AND source_repo <> ''
GROUP BY source_repo ORDER BY last_id DESC LIMIT 20`
)
.all();
for (const row of rows) push("repo", row.p, path.basename(row.p));
} catch {
/* ignore — same best-effort contract as the recents below */
}
// Pull recent cwds from the sessions DB (best-effort; if the DB isn't
// ready or has a different schema, just return what we have).
try {