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:
@@ -493,6 +493,11 @@ export function Workspace() {
|
||||
});
|
||||
}, []);
|
||||
|
||||
// The session the opened run holds, when that run works in a worktree of
|
||||
// its own (`claude --worktree`): the Changes panel shows that tree.
|
||||
const openedRun = runParam ? activeRuns?.items.find((r) => r.id === runParam) : undefined;
|
||||
const gitSession = openedRun?.worktree && openedRun.sessionId ? openedRun.sessionId : undefined;
|
||||
|
||||
// A pane the layout does not show yet (`&pane=1` on one pane) widens the
|
||||
// split so the run lands where it was asked for — once per request, so the
|
||||
// user can still go back to one pane afterwards.
|
||||
@@ -1065,7 +1070,11 @@ export function Workspace() {
|
||||
</button>
|
||||
</div>
|
||||
<LaneChangesPanel
|
||||
// A new tree is a new panel: no read for the previous one
|
||||
// can land in it.
|
||||
key={`${currentLane.id}:${gitSession ?? ""}`}
|
||||
laneId={currentLane.id}
|
||||
session={gitSession}
|
||||
tab={panelTab}
|
||||
full={changesSize === "full"}
|
||||
// A sibling worktree forks from the lane's SOURCE repo when
|
||||
|
||||
@@ -722,6 +722,34 @@ describe("Workspace — home and session view", () => {
|
||||
await userEvent.setup().click(screen.getByTestId("workspace-back-home"));
|
||||
expect(await screen.findByTestId("workspace-home")).toBeInTheDocument();
|
||||
});
|
||||
it("points the Changes panel at the opened run's worktree session", async () => {
|
||||
localStorage.setItem("agenthub.workspace.changesOpen", "1");
|
||||
const original = vi.mocked(api.run.list).getMockImplementation();
|
||||
vi.mocked(api.run.list).mockResolvedValue({
|
||||
runs: [],
|
||||
items: [
|
||||
{
|
||||
id: "agenthub-lane-1-1",
|
||||
laneId: 1,
|
||||
slot: 1,
|
||||
status: "running",
|
||||
sessionId: "s-wt",
|
||||
worktree: true,
|
||||
},
|
||||
],
|
||||
} as never);
|
||||
render(
|
||||
<MemoryRouter initialEntries={["/workspace?lane=1&run=agenthub-lane-1-1"]}>
|
||||
<Workspace />
|
||||
</MemoryRouter>
|
||||
);
|
||||
await settle();
|
||||
await settle();
|
||||
await waitFor(() => expect(api.lanes.git).toHaveBeenCalledWith({ lane: 1, session: "s-wt" }));
|
||||
vi.mocked(api.run.list).mockImplementation(original!);
|
||||
localStorage.removeItem("agenthub.workspace.changesOpen");
|
||||
});
|
||||
|
||||
it("widens one pane to two for &pane=1, so the run opens beside the first", async () => {
|
||||
localStorage.removeItem("agenthub.workspace.splitView");
|
||||
render(
|
||||
|
||||
Reference in New Issue
Block a user