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:
@@ -230,6 +230,43 @@ describe("pty-run", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("spawnRun starts a Claude session in its own worktree, Claude's way", () => {
|
||||
let newSessionArgv = null;
|
||||
tmux.__setExecImpl((args) => {
|
||||
if (args[0] === "has-session") {
|
||||
const e = new Error("gone");
|
||||
e.status = 1;
|
||||
throw e;
|
||||
}
|
||||
if (args[0] === "new-session") newSessionArgv = args;
|
||||
return "";
|
||||
});
|
||||
const repo = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "pty-wt-")));
|
||||
// Inside a git hook GIT_DIR names the repo being committed; drop it.
|
||||
const env = Object.fromEntries(
|
||||
Object.entries(process.env).filter(([k]) => !k.startsWith("GIT_"))
|
||||
);
|
||||
require("node:child_process").execFileSync("git", ["init", "-q", repo], { env });
|
||||
try {
|
||||
pty.spawnRun({ laneId: 41, cwd: repo, worktree: true, initialPrompt: "fix it" });
|
||||
// `=` form: `-w [name]` takes an optional value and would eat the prompt.
|
||||
const flag = newSessionArgv.find((a) => a.startsWith("--worktree="));
|
||||
assert.match(flag, /^--worktree=ah-41-[a-z0-9]+$/);
|
||||
assert.equal(newSessionArgv.at(-1), "fix it");
|
||||
// Claude does not exclude its worktrees; the main repo would list them.
|
||||
const exclude = fs.readFileSync(path.join(repo, ".git", "info", "exclude"), "utf8");
|
||||
assert.ok(exclude.split("\n").includes("/.claude/worktrees/"), exclude);
|
||||
const dashboardRuns = require("../lib/dashboard-runs");
|
||||
assert.equal(dashboardRuns.getRun("agenthub-lane-41").worktree, 1);
|
||||
assert.throws(
|
||||
() => pty.spawnRun({ laneId: 42, cwd: repo, tool: "codex", worktree: true }),
|
||||
(e) => e.code === "EUNSUPPORTED"
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(repo, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("spawnRun offers bypass in the pane's shift+tab cycle without starting in it", () => {
|
||||
let newSessionArgv = null;
|
||||
tmux.__setExecImpl((args) => {
|
||||
|
||||
Reference in New Issue
Block a user