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:
@@ -15,7 +15,7 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ChevronDown, ChevronUp } from "lucide-react";
|
||||
import { api } from "../../lib/api";
|
||||
import { api, gitTarget } from "../../lib/api";
|
||||
import {
|
||||
applyChoices,
|
||||
parseConflicts,
|
||||
@@ -37,11 +37,14 @@ function Lines({ lines, className = "" }: { lines: string[]; className?: string
|
||||
|
||||
export function LaneConflictView({
|
||||
laneId,
|
||||
session,
|
||||
path,
|
||||
onResolved,
|
||||
onEdit,
|
||||
}: {
|
||||
laneId: number;
|
||||
/** Work in that session's own worktree (see `gitTarget`). */
|
||||
session?: string;
|
||||
path: string;
|
||||
/** The file left the conflict (resolved, or a whole side taken). */
|
||||
onResolved: () => void;
|
||||
@@ -62,7 +65,7 @@ export function LaneConflictView({
|
||||
setChoices([]);
|
||||
setError(null);
|
||||
api.lanes
|
||||
.gitFile(laneId, path, "work")
|
||||
.gitFile(gitTarget(laneId, session), path, "work")
|
||||
.then((r) => {
|
||||
if (!live) return;
|
||||
setFile(
|
||||
@@ -75,7 +78,7 @@ export function LaneConflictView({
|
||||
return () => {
|
||||
live = false;
|
||||
};
|
||||
}, [laneId, path]);
|
||||
}, [laneId, session, path]);
|
||||
|
||||
const parts: ConflictPart[] | null = file?.usable ? parseConflicts(file.lines) : null;
|
||||
const total = parts ? parts.filter((p) => p.kind === "conflict").length : 0;
|
||||
@@ -98,11 +101,16 @@ export function LaneConflictView({
|
||||
const save = () =>
|
||||
run(async () => {
|
||||
if (!parts || !file?.sha) return;
|
||||
await api.lanes.gitSaveFile(laneId, path, applyChoices(parts, choices).join("\n"), file.sha);
|
||||
await api.lanes.gitAction(laneId, "resolve", { path, side: "done" });
|
||||
await api.lanes.gitSaveFile(
|
||||
gitTarget(laneId, session),
|
||||
path,
|
||||
applyChoices(parts, choices).join("\n"),
|
||||
file.sha
|
||||
);
|
||||
await api.lanes.gitAction(gitTarget(laneId, session), "resolve", { path, side: "done" });
|
||||
});
|
||||
const takeAll = (side: "ours" | "theirs") =>
|
||||
run(() => api.lanes.gitAction(laneId, "resolve", { path, side }));
|
||||
run(() => api.lanes.gitAction(gitTarget(laneId, session), "resolve", { path, side }));
|
||||
|
||||
const choose = (i: number, c: ConflictChoice | undefined) =>
|
||||
setChoices((prev) => {
|
||||
|
||||
Reference in New Issue
Block a user