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:
@@ -53,6 +53,11 @@ vi.mock("../../lib/api", async (importOriginal) => {
|
||||
list: vi.fn(async () => ({ lanes, counts: {} })),
|
||||
update: vi.fn(async () => ({ lane: lanes[0] })),
|
||||
action: vi.fn(async () => ({ ok: true })),
|
||||
removeSessionWorktree: vi.fn(async () => ({
|
||||
removed: true,
|
||||
branch: "x",
|
||||
branchKept: false,
|
||||
})),
|
||||
routerList: vi.fn(async () => ({ routers: [] })),
|
||||
},
|
||||
sessions: {
|
||||
@@ -158,6 +163,50 @@ describe("WorkspaceTree", () => {
|
||||
expect(api.sessions.patch).toHaveBeenCalledWith("s-live", { archived: true });
|
||||
});
|
||||
|
||||
it("archiving a worktree session removes its worktree first, after asking", async () => {
|
||||
vi.mocked(api.sessions.list).mockImplementation(async () => ({
|
||||
sessions: [
|
||||
{ ...session("s-wt", "In a worktree"), cwd: "/r/.claude/worktrees/ah-1-x" } as Session,
|
||||
],
|
||||
total: 1,
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
}));
|
||||
const user = userEvent.setup();
|
||||
renderTree();
|
||||
await user.click(await screen.findByTestId("workspace-toggle-1"));
|
||||
expect(await screen.findByTestId("workspace-session-worktree-s-wt")).toBeInTheDocument();
|
||||
await user.click(screen.getByTestId("workspace-session-archive-s-wt"));
|
||||
expect(api.sessions.patch).not.toHaveBeenCalled();
|
||||
expect(screen.getByRole("dialog")).toHaveTextContent(".claude/worktrees/ah-1-x");
|
||||
await user.click(screen.getByRole("button", { name: "Archive and remove" }));
|
||||
await waitFor(() =>
|
||||
expect(api.sessions.patch).toHaveBeenCalledWith("s-wt", { archived: true })
|
||||
);
|
||||
expect(api.lanes.removeSessionWorktree).toHaveBeenCalledWith(1, "s-wt");
|
||||
});
|
||||
|
||||
it("keeps a worktree session when git refuses to remove its worktree", async () => {
|
||||
vi.mocked(api.sessions.list).mockImplementation(async () => ({
|
||||
sessions: [
|
||||
{ ...session("s-wt", "In a worktree"), cwd: "/r/.claude/worktrees/ah-1-x" } as Session,
|
||||
],
|
||||
total: 1,
|
||||
limit: 20,
|
||||
offset: 0,
|
||||
}));
|
||||
vi.mocked(api.lanes.removeSessionWorktree).mockRejectedValueOnce(
|
||||
Object.assign(new Error("contains modified or untracked files"), { code: "EGIT" })
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
renderTree();
|
||||
await user.click(await screen.findByTestId("workspace-toggle-1"));
|
||||
await user.click(await screen.findByTestId("workspace-session-archive-s-wt"));
|
||||
await user.click(screen.getByRole("button", { name: "Archive and remove" }));
|
||||
expect(await screen.findByRole("alert")).toHaveTextContent("modified or untracked");
|
||||
expect(api.sessions.patch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("dots a session waiting for the user amber and a failed one red", async () => {
|
||||
vi.mocked(api.sessions.list).mockImplementation(async () => ({
|
||||
sessions: [
|
||||
|
||||
Reference in New Issue
Block a user