fix: use mainRepoRoot for agent creation in Paseo-owned worktrees
This commit is contained in:
@@ -121,14 +121,17 @@ function SectionHeader({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createAgentWorkingDir =
|
||||||
|
(checkout?.isPaseoOwnedWorktree ? checkout.mainRepoRoot : null) ?? section.workingDir;
|
||||||
|
|
||||||
const handleCreatePress = useCallback(
|
const handleCreatePress = useCallback(
|
||||||
(e: { stopPropagation: () => void }) => {
|
(e: { stopPropagation: () => void }) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
if (section.workingDir) {
|
if (createAgentWorkingDir) {
|
||||||
onCreateAgent(section.workingDir);
|
onCreateAgent(createAgentWorkingDir);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[onCreateAgent, section.workingDir]
|
[onCreateAgent, createAgentWorkingDir]
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -171,7 +174,7 @@ function SectionHeader({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
<View style={styles.sectionHeaderRight}>
|
<View style={styles.sectionHeaderRight}>
|
||||||
{section.workingDir && (
|
{createAgentWorkingDir && (
|
||||||
<Pressable
|
<Pressable
|
||||||
style={styles.createAgentButton}
|
style={styles.createAgentButton}
|
||||||
onPress={handleCreatePress}
|
onPress={handleCreatePress}
|
||||||
|
|||||||
@@ -1218,7 +1218,8 @@ function assertHydratedReplica(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function sanitizeClaudeProjectName(cwd: string): string {
|
function sanitizeClaudeProjectName(cwd: string): string {
|
||||||
return cwd.replace(/[\\/]/g, "-").replace(/_/g, "-");
|
// Match Claude CLI's path sanitization: replace slashes, dots, and underscores with dashes
|
||||||
|
return cwd.replace(/[\\/\.]/g, "-").replace(/_/g, "-");
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveClaudeHistoryPath(cwd: string, sessionId: string): string {
|
function resolveClaudeHistoryPath(cwd: string, sessionId: string): string {
|
||||||
|
|||||||
@@ -1260,7 +1260,8 @@ class ClaudeAgentSession implements AgentSession {
|
|||||||
private resolveHistoryPath(sessionId: string): string | null {
|
private resolveHistoryPath(sessionId: string): string | null {
|
||||||
const cwd = this.config.cwd;
|
const cwd = this.config.cwd;
|
||||||
if (!cwd) return null;
|
if (!cwd) return null;
|
||||||
const sanitized = cwd.replace(/[\\/]/g, "-").replace(/_/g, "-");
|
// Match Claude CLI's path sanitization: replace slashes, dots, and underscores with dashes
|
||||||
|
const sanitized = cwd.replace(/[\\/\.]/g, "-").replace(/_/g, "-");
|
||||||
const configDir = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
const configDir = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
||||||
const dir = path.join(configDir, "projects", sanitized);
|
const dir = path.join(configDir, "projects", sanitized);
|
||||||
return path.join(dir, `${sessionId}.jsonl`);
|
return path.join(dir, `${sessionId}.jsonl`);
|
||||||
|
|||||||
@@ -2503,6 +2503,7 @@ export class Session {
|
|||||||
cwd,
|
cwd,
|
||||||
isGit: true,
|
isGit: true,
|
||||||
repoRoot: status.repoRoot ?? null,
|
repoRoot: status.repoRoot ?? null,
|
||||||
|
mainRepoRoot: status.mainRepoRoot,
|
||||||
currentBranch: status.currentBranch ?? null,
|
currentBranch: status.currentBranch ?? null,
|
||||||
isDirty: status.isDirty ?? null,
|
isDirty: status.isDirty ?? null,
|
||||||
baseRef: status.baseRef,
|
baseRef: status.baseRef,
|
||||||
|
|||||||
@@ -1124,6 +1124,7 @@ const CheckoutStatusGitPaseoSchema = CheckoutStatusCommonSchema.extend({
|
|||||||
isGit: z.literal(true),
|
isGit: z.literal(true),
|
||||||
isPaseoOwnedWorktree: z.literal(true),
|
isPaseoOwnedWorktree: z.literal(true),
|
||||||
repoRoot: z.string(),
|
repoRoot: z.string(),
|
||||||
|
mainRepoRoot: z.string(),
|
||||||
currentBranch: z.string().nullable(),
|
currentBranch: z.string().nullable(),
|
||||||
isDirty: z.boolean(),
|
isDirty: z.boolean(),
|
||||||
baseRef: z.string(),
|
baseRef: z.string(),
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ describe("checkout git utilities", () => {
|
|||||||
expect(status.isGit).toBe(true);
|
expect(status.isGit).toBe(true);
|
||||||
expect(status.repoRoot).toBe(result.worktreePath);
|
expect(status.repoRoot).toBe(result.worktreePath);
|
||||||
expect(status.isDirty).toBe(true);
|
expect(status.isDirty).toBe(true);
|
||||||
|
expect(status.isPaseoOwnedWorktree).toBe(true);
|
||||||
|
expect(status.mainRepoRoot).toBe(repoDir);
|
||||||
|
|
||||||
const diff = await getCheckoutDiff(result.worktreePath, { mode: "uncommitted" }, { paseoHome });
|
const diff = await getCheckoutDiff(result.worktreePath, { mode: "uncommitted" }, { paseoHome });
|
||||||
expect(diff.diff).toContain("-hello");
|
expect(diff.diff).toContain("-hello");
|
||||||
@@ -134,6 +136,29 @@ describe("checkout git utilities", () => {
|
|||||||
expect(message).toBe("worktree update");
|
expect(message).toBe("worktree update");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("returns mainRepoRoot pointing to first non-bare worktree for bare repos", async () => {
|
||||||
|
const bareRepoDir = join(tempDir, "bare-repo");
|
||||||
|
execSync(`git clone --bare ${repoDir} ${bareRepoDir}`);
|
||||||
|
|
||||||
|
const mainCheckoutDir = join(tempDir, "main-checkout");
|
||||||
|
execSync(`git -C ${bareRepoDir} worktree add ${mainCheckoutDir} main`);
|
||||||
|
execSync("git config user.email 'test@test.com'", { cwd: mainCheckoutDir });
|
||||||
|
execSync("git config user.name 'Test'", { cwd: mainCheckoutDir });
|
||||||
|
|
||||||
|
const worktree = await createWorktree({
|
||||||
|
branchName: "feature",
|
||||||
|
cwd: mainCheckoutDir,
|
||||||
|
baseBranch: "main",
|
||||||
|
worktreeSlug: "feature-worktree",
|
||||||
|
paseoHome,
|
||||||
|
});
|
||||||
|
|
||||||
|
const status = await getCheckoutStatus(worktree.worktreePath, { paseoHome });
|
||||||
|
expect(status.isGit).toBe(true);
|
||||||
|
expect(status.isPaseoOwnedWorktree).toBe(true);
|
||||||
|
expect(status.mainRepoRoot).toBe(mainCheckoutDir);
|
||||||
|
});
|
||||||
|
|
||||||
it("merges the current branch into base from a worktree checkout", async () => {
|
it("merges the current branch into base from a worktree checkout", async () => {
|
||||||
const worktree = await createWorktree({
|
const worktree = await createWorktree({
|
||||||
branchName: "main",
|
branchName: "main",
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { exec, execFile } from "child_process";
|
import { exec, execFile } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
import { resolve } from "path";
|
import { resolve, dirname, basename } from "path";
|
||||||
|
import { realpathSync } from "fs";
|
||||||
import type { ParsedDiffFile } from "../server/utils/diff-highlighter.js";
|
import type { ParsedDiffFile } from "../server/utils/diff-highlighter.js";
|
||||||
import { parseAndHighlightDiff } from "../server/utils/diff-highlighter.js";
|
import { parseAndHighlightDiff } from "../server/utils/diff-highlighter.js";
|
||||||
import { isPaseoOwnedWorktreeCwd } from "./worktree.js";
|
import { isPaseoOwnedWorktreeCwd } from "./worktree.js";
|
||||||
@@ -79,6 +80,7 @@ export type CheckoutStatusGitNonPaseo = {
|
|||||||
export type CheckoutStatusGitPaseo = {
|
export type CheckoutStatusGitPaseo = {
|
||||||
isGit: true;
|
isGit: true;
|
||||||
repoRoot: string;
|
repoRoot: string;
|
||||||
|
mainRepoRoot: string;
|
||||||
currentBranch: string | null;
|
currentBranch: string | null;
|
||||||
isDirty: boolean;
|
isDirty: boolean;
|
||||||
baseRef: string;
|
baseRef: string;
|
||||||
@@ -156,9 +158,37 @@ async function getWorktreeRoot(cwd: string): Promise<string | null> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function getMainRepoRoot(cwd: string): Promise<string> {
|
||||||
|
const { stdout: commonDirOut } = await execAsync(
|
||||||
|
"git rev-parse --path-format=absolute --git-common-dir",
|
||||||
|
{ cwd, env: READ_ONLY_GIT_ENV }
|
||||||
|
);
|
||||||
|
const commonDir = commonDirOut.trim();
|
||||||
|
const normalized = realpathSync(commonDir);
|
||||||
|
|
||||||
|
if (basename(normalized) === ".git") {
|
||||||
|
return dirname(normalized);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { stdout: worktreeOut } = await execAsync("git worktree list --porcelain", {
|
||||||
|
cwd,
|
||||||
|
env: READ_ONLY_GIT_ENV,
|
||||||
|
});
|
||||||
|
const worktrees = parseWorktreeList(worktreeOut);
|
||||||
|
const nonBareNonPaseo = worktrees.filter(
|
||||||
|
(wt) => !wt.isBare && !wt.path.includes("/.paseo/worktrees/")
|
||||||
|
);
|
||||||
|
const childrenOfBareRepo = nonBareNonPaseo.filter((wt) =>
|
||||||
|
wt.path.startsWith(normalized + "/")
|
||||||
|
);
|
||||||
|
const mainChild = childrenOfBareRepo.find((wt) => basename(wt.path) === "main");
|
||||||
|
return mainChild?.path ?? childrenOfBareRepo[0]?.path ?? nonBareNonPaseo[0]?.path ?? normalized;
|
||||||
|
}
|
||||||
|
|
||||||
type GitWorktreeEntry = {
|
type GitWorktreeEntry = {
|
||||||
path: string;
|
path: string;
|
||||||
branchRef?: string;
|
branchRef?: string;
|
||||||
|
isBare?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function parseWorktreeList(output: string): GitWorktreeEntry[] {
|
function parseWorktreeList(output: string): GitWorktreeEntry[] {
|
||||||
@@ -179,6 +209,9 @@ function parseWorktreeList(output: string): GitWorktreeEntry[] {
|
|||||||
if (current && trimmed.startsWith("branch ")) {
|
if (current && trimmed.startsWith("branch ")) {
|
||||||
current.branchRef = trimmed.slice("branch ".length).trim();
|
current.branchRef = trimmed.slice("branch ".length).trim();
|
||||||
}
|
}
|
||||||
|
if (current && trimmed === "bare") {
|
||||||
|
current.isBare = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (current) {
|
if (current) {
|
||||||
entries.push(current);
|
entries.push(current);
|
||||||
@@ -460,9 +493,11 @@ export async function getCheckoutStatus(
|
|||||||
hasRemote && currentBranch ? await getAheadOfOrigin(cwd, currentBranch) : null;
|
hasRemote && currentBranch ? await getAheadOfOrigin(cwd, currentBranch) : null;
|
||||||
|
|
||||||
if (configured.isPaseoOwnedWorktree) {
|
if (configured.isPaseoOwnedWorktree) {
|
||||||
|
const mainRepoRoot = await getMainRepoRoot(cwd);
|
||||||
return {
|
return {
|
||||||
isGit: true,
|
isGit: true,
|
||||||
repoRoot: worktreeRoot,
|
repoRoot: worktreeRoot,
|
||||||
|
mainRepoRoot,
|
||||||
currentBranch,
|
currentBranch,
|
||||||
isDirty,
|
isDirty,
|
||||||
baseRef: configured.baseRef,
|
baseRef: configured.baseRef,
|
||||||
|
|||||||
Reference in New Issue
Block a user