mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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(
|
||||
(e: { stopPropagation: () => void }) => {
|
||||
e.stopPropagation();
|
||||
if (section.workingDir) {
|
||||
onCreateAgent(section.workingDir);
|
||||
if (createAgentWorkingDir) {
|
||||
onCreateAgent(createAgentWorkingDir);
|
||||
}
|
||||
},
|
||||
[onCreateAgent, section.workingDir]
|
||||
[onCreateAgent, createAgentWorkingDir]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -171,7 +174,7 @@ function SectionHeader({
|
||||
</Text>
|
||||
</View>
|
||||
<View style={styles.sectionHeaderRight}>
|
||||
{section.workingDir && (
|
||||
{createAgentWorkingDir && (
|
||||
<Pressable
|
||||
style={styles.createAgentButton}
|
||||
onPress={handleCreatePress}
|
||||
|
||||
@@ -1218,7 +1218,8 @@ function assertHydratedReplica(
|
||||
}
|
||||
|
||||
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 {
|
||||
|
||||
@@ -1260,7 +1260,8 @@ class ClaudeAgentSession implements AgentSession {
|
||||
private resolveHistoryPath(sessionId: string): string | null {
|
||||
const cwd = this.config.cwd;
|
||||
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 dir = path.join(configDir, "projects", sanitized);
|
||||
return path.join(dir, `${sessionId}.jsonl`);
|
||||
|
||||
@@ -2503,6 +2503,7 @@ export class Session {
|
||||
cwd,
|
||||
isGit: true,
|
||||
repoRoot: status.repoRoot ?? null,
|
||||
mainRepoRoot: status.mainRepoRoot,
|
||||
currentBranch: status.currentBranch ?? null,
|
||||
isDirty: status.isDirty ?? null,
|
||||
baseRef: status.baseRef,
|
||||
|
||||
@@ -1124,6 +1124,7 @@ const CheckoutStatusGitPaseoSchema = CheckoutStatusCommonSchema.extend({
|
||||
isGit: z.literal(true),
|
||||
isPaseoOwnedWorktree: z.literal(true),
|
||||
repoRoot: z.string(),
|
||||
mainRepoRoot: z.string(),
|
||||
currentBranch: z.string().nullable(),
|
||||
isDirty: z.boolean(),
|
||||
baseRef: z.string(),
|
||||
|
||||
@@ -117,6 +117,8 @@ describe("checkout git utilities", () => {
|
||||
expect(status.isGit).toBe(true);
|
||||
expect(status.repoRoot).toBe(result.worktreePath);
|
||||
expect(status.isDirty).toBe(true);
|
||||
expect(status.isPaseoOwnedWorktree).toBe(true);
|
||||
expect(status.mainRepoRoot).toBe(repoDir);
|
||||
|
||||
const diff = await getCheckoutDiff(result.worktreePath, { mode: "uncommitted" }, { paseoHome });
|
||||
expect(diff.diff).toContain("-hello");
|
||||
@@ -134,6 +136,29 @@ describe("checkout git utilities", () => {
|
||||
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 () => {
|
||||
const worktree = await createWorktree({
|
||||
branchName: "main",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { exec, execFile } from "child_process";
|
||||
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 { parseAndHighlightDiff } from "../server/utils/diff-highlighter.js";
|
||||
import { isPaseoOwnedWorktreeCwd } from "./worktree.js";
|
||||
@@ -79,6 +80,7 @@ export type CheckoutStatusGitNonPaseo = {
|
||||
export type CheckoutStatusGitPaseo = {
|
||||
isGit: true;
|
||||
repoRoot: string;
|
||||
mainRepoRoot: string;
|
||||
currentBranch: string | null;
|
||||
isDirty: boolean;
|
||||
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 = {
|
||||
path: string;
|
||||
branchRef?: string;
|
||||
isBare?: boolean;
|
||||
};
|
||||
|
||||
function parseWorktreeList(output: string): GitWorktreeEntry[] {
|
||||
@@ -179,6 +209,9 @@ function parseWorktreeList(output: string): GitWorktreeEntry[] {
|
||||
if (current && trimmed.startsWith("branch ")) {
|
||||
current.branchRef = trimmed.slice("branch ".length).trim();
|
||||
}
|
||||
if (current && trimmed === "bare") {
|
||||
current.isBare = true;
|
||||
}
|
||||
}
|
||||
if (current) {
|
||||
entries.push(current);
|
||||
@@ -460,9 +493,11 @@ export async function getCheckoutStatus(
|
||||
hasRemote && currentBranch ? await getAheadOfOrigin(cwd, currentBranch) : null;
|
||||
|
||||
if (configured.isPaseoOwnedWorktree) {
|
||||
const mainRepoRoot = await getMainRepoRoot(cwd);
|
||||
return {
|
||||
isGit: true,
|
||||
repoRoot: worktreeRoot,
|
||||
mainRepoRoot,
|
||||
currentBranch,
|
||||
isDirty,
|
||||
baseRef: configured.baseRef,
|
||||
|
||||
Reference in New Issue
Block a user