feat(workspace): a session in its own worktree, Claude's way

Desktop-parity phase 6, following Claude Desktop:
- the workspace home's Worktree checkbox starts the pane with
  `claude --worktree=<name>`, so Claude Code makes, populates
  (.worktreeinclude, WorktreeCreate hook) and cleans up
  <repo>/.claude/worktrees/<name>; AgentHub only excludes
  /.claude/worktrees/ at the repo root (Claude does not)
- server/lib/session-worktree.js resolves a session's worktree from the
  session row only (owned by the workspace, registered by git worktree
  list); every lane git route takes ?session= and answers ENOWORKTREE
  instead of falling back to the main folder
- the session view's Changes panel works in the opened run's worktree
  (dashboard_runs.worktree, GitTarget in the client api)
- Resume/Fork start in the session's worktree; a gone one is ENOWORKTREE
- Archive of a worktree session removes the worktree after asking:
  git worktree remove without --force, git branch -d (unmerged work keeps
  its branch), a lock held by a live pid refused, a stale one cleared
- listWorktrees reads `locked <reason>` locks
- tests scrub GIT_* env so the pre-commit suite cannot act on this repo
This commit is contained in:
2026-10-02 13:22:29 +07:00
parent a6bd33bcff
commit 5084334d81
35 changed files with 1041 additions and 119 deletions
+22 -1
View File
@@ -58,4 +58,25 @@ function excludeFromGit(laneDir, relPaths) {
}
}
module.exports = { excludeFromGit };
/**
* The same, anchored at the REPOSITORY root rather than `dir` — for paths a
* tool creates at the root whatever subdirectory it was started in (Claude
* Code's `.claude/worktrees/`). Best effort, like `excludeFromGit`.
*/
function excludeFromRepoRoot(dir, relPaths) {
try {
const env = { ...process.env };
for (const name of REPO_VARS) delete env[name];
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
cwd: dir,
env,
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
}).trim();
if (root) excludeFromGit(root, relPaths);
} catch {
/* not a repo */
}
}
module.exports = { excludeFromGit, excludeFromRepoRoot };