fix: resolve draft project for worktree paths (#722)

This commit is contained in:
Iuliia Ivashko
2026-03-20 16:50:13 +02:00
committed by GitHub
parent fd31a8cc2d
commit 683d0c1798
2 changed files with 140 additions and 8 deletions
@@ -1,5 +1,6 @@
import { substituteCommandVariables } from '@/lib/openchamberConfig';
import type { WorktreeMetadata } from '@/types/worktree';
import { execCommand } from '@/lib/execCommands';
import {
deleteRemoteBranch,
git,
@@ -19,6 +20,66 @@ const normalizePath = (value: string): string => {
return replaced.length > 1 ? replaced.replace(/\/+$/, '') : replaced;
};
const toAbsolutePath = (baseDir: string, maybeRelativePath: string): string => {
const normalizedBase = normalizePath(baseDir);
const normalizedInput = normalizePath(maybeRelativePath);
if (!normalizedInput) return normalizedBase;
if (normalizedInput.startsWith('/')) return normalizedInput;
const stack = normalizedBase.split('/').filter(Boolean);
const parts = normalizedInput.split('/').filter(Boolean);
for (const part of parts) {
if (part === '.') continue;
if (part === '..') {
stack.pop();
continue;
}
stack.push(part);
}
return `/${stack.join('/')}`;
};
const derivePrimaryWorktreeRootFromGitDir = (gitDir: string): string | null => {
const normalized = normalizePath(gitDir);
if (!normalized) return null;
if (normalized.endsWith('/.git')) {
return normalized.slice(0, -'/.git'.length) || null;
}
const worktreesMarker = '/.git/worktrees/';
const markerIndex = normalized.indexOf(worktreesMarker);
if (markerIndex > 0) {
return normalized.slice(0, markerIndex) || null;
}
return null;
};
const resolvePrimaryWorktreeDirectory = async (directory: string): Promise<string> => {
const normalizedDirectory = normalizePath(directory);
const absoluteGitDirResult = await execCommand('git rev-parse --absolute-git-dir', normalizedDirectory);
const absoluteGitDir = normalizePath((absoluteGitDirResult.stdout || '').trim());
if (absoluteGitDirResult.success && absoluteGitDir) {
const rootFromAbsoluteGitDir = derivePrimaryWorktreeRootFromGitDir(absoluteGitDir);
if (rootFromAbsoluteGitDir) {
return rootFromAbsoluteGitDir;
}
}
const commonDirResult = await execCommand('git rev-parse --git-common-dir', normalizedDirectory);
const rawCommonDir = normalizePath((commonDirResult.stdout || '').trim());
if (!commonDirResult.success || !rawCommonDir) {
return normalizedDirectory;
}
const commonDir = toAbsolutePath(normalizedDirectory, rawCommonDir);
const rootFromCommonDir = derivePrimaryWorktreeRootFromGitDir(commonDir);
if (rootFromCommonDir) {
return rootFromCommonDir;
}
return normalizedDirectory;
};
const slugifyWorktreeName = (value: string): string => {
return value
.trim()
@@ -113,7 +174,8 @@ const toCreatePayload = (args: {
};
export async function listProjectWorktrees(project: ProjectRef): Promise<WorktreeMetadata[]> {
const projectDirectory = project.path;
const projectDirectory = normalizePath(project.path);
const metadataProjectDirectory = await resolvePrimaryWorktreeDirectory(projectDirectory).catch(() => projectDirectory);
const normalizedProjectDirectory = normalizePath(projectDirectory);
const worktrees = await git.worktree.list(projectDirectory).catch(() => []);
@@ -127,7 +189,7 @@ export async function listProjectWorktrees(project: ProjectRef): Promise<Worktre
source: 'sdk' as const,
name: name || deriveSdkWorktreeNameFromDirectory(worktreePath),
path: worktreePath,
projectDirectory,
projectDirectory: metadataProjectDirectory,
branch,
label: branch || name || deriveSdkWorktreeNameFromDirectory(worktreePath),
};
@@ -157,7 +219,8 @@ export type CreateWorktreeArgs = {
};
export async function createWorktree(project: ProjectRef, args: CreateWorktreeArgs): Promise<WorktreeMetadata> {
const projectDirectory = project.path;
const projectDirectory = normalizePath(project.path);
const metadataProjectDirectory = await resolvePrimaryWorktreeDirectory(projectDirectory).catch(() => projectDirectory);
const payload = toCreatePayload(args, projectDirectory);
const created = await git.worktree.create(projectDirectory, payload);
@@ -173,7 +236,7 @@ export async function createWorktree(project: ProjectRef, args: CreateWorktreeAr
source: 'sdk',
name: returnedName,
path: normalizePath(returnedPath),
projectDirectory,
projectDirectory: metadataProjectDirectory,
branch: returnedBranch,
label: returnedBranch || returnedName,
};
+73 -4
View File
@@ -23,6 +23,7 @@ import { markPendingUserSendAnimation } from "@/lib/userSendAnimation";
import { flattenAssistantTextParts } from "@/lib/messages/messageText";
import { normalizeMessageRecordsForProjection } from "./utils/messageProjectors";
import type { ProjectEntry } from "@/lib/api/types";
import type { WorktreeMetadata } from "@/types/worktree";
export type { AttachedFile, EditPermissionMode };
export { MEMORY_LIMITS, ACTIVE_SESSION_WINDOW } from "./types/sessionTypes";
@@ -106,6 +107,69 @@ const resolveProjectForDirectory = (projects: ProjectEntry[], directory: string
return bestMatch;
};
const resolveProjectFromWorktreeDirectory = (
projects: ProjectEntry[],
availableWorktreesByProject: Map<string, WorktreeMetadata[]>,
directory: string | null,
): ProjectEntry | null => {
const normalizedDirectory = normalizePath(directory);
if (!normalizedDirectory) {
return null;
}
let matchedWorktree: WorktreeMetadata | null = null;
let matchedProjectPath: string | null = null;
let bestPathLength = -1;
for (const [projectPath, worktrees] of availableWorktreesByProject.entries()) {
for (const worktree of worktrees) {
const worktreePath = normalizePath(worktree.path);
if (!worktreePath) {
continue;
}
const isExact = normalizedDirectory === worktreePath;
const isNested = normalizedDirectory.startsWith(`${worktreePath}/`);
if (!isExact && !isNested) {
continue;
}
if (worktreePath.length > bestPathLength) {
bestPathLength = worktreePath.length;
matchedWorktree = worktree;
matchedProjectPath = normalizePath(projectPath);
}
}
}
if (!matchedWorktree) {
return null;
}
const normalizedMetadataProjectPath = normalizePath(matchedWorktree.projectDirectory);
const candidates = [normalizedMetadataProjectPath, matchedProjectPath].filter((value): value is string => Boolean(value));
for (const candidatePath of candidates) {
const exact = projects.find((project) => normalizePath(project.path) === candidatePath) ?? null;
if (exact) {
return exact;
}
const nested = resolveProjectForDirectory(projects, candidatePath);
if (nested) {
return nested;
}
}
return null;
};
const resolveDraftProjectForDirectory = (
projects: ProjectEntry[],
availableWorktreesByProject: Map<string, WorktreeMetadata[]>,
directory: string | null,
): ProjectEntry | null => {
return resolveProjectFromWorktreeDirectory(projects, availableWorktreesByProject, directory)
?? resolveProjectForDirectory(projects, directory);
};
const buildSessionChoiceAnalysisSignature = (messages: Array<{ info: Message; parts: Part[] }>): string => {
const lastMessage = messages[messages.length - 1];
const lastMessageId = typeof lastMessage?.info?.id === 'string' ? lastMessage.info.id : '';
@@ -209,6 +273,7 @@ export const useSessionStore = create<SessionStore>()(
openNewSessionDraft: (options) => {
const projectsState = useProjectsStore.getState();
const projects = projectsState.projects;
const availableWorktreesByProject = get().availableWorktreesByProject;
const activeProject = projectsState.getActiveProject();
const currentDirectory = normalizePath(useDirectoryStore.getState().currentDirectory ?? null);
const persistedTarget = readPersistedDraftTarget();
@@ -220,7 +285,7 @@ export const useSessionStore = create<SessionStore>()(
? projects.find((project) => project.id === options.projectId) ?? null
: null;
const inferredProjectFromDirectory = resolveProjectForDirectory(projects, explicitDirectory);
const inferredProjectFromDirectory = resolveDraftProjectForDirectory(projects, availableWorktreesByProject, explicitDirectory);
const fallbackProject = (() => {
if (activeProject) {
return activeProject;
@@ -234,8 +299,8 @@ export const useSessionStore = create<SessionStore>()(
const persistedProjectById = persistedTarget?.projectId
? projects.find((project) => project.id === persistedTarget.projectId) ?? null
: null;
const persistedProjectByDirectory = resolveProjectForDirectory(projects, persistedTarget?.directory ?? null);
const currentDirectoryProject = resolveProjectForDirectory(projects, currentDirectory);
const persistedProjectByDirectory = resolveDraftProjectForDirectory(projects, availableWorktreesByProject, persistedTarget?.directory ?? null);
const currentDirectoryProject = resolveDraftProjectForDirectory(projects, availableWorktreesByProject, currentDirectory);
const selectedProject = (() => {
if (explicitProject || explicitDirectory !== null) {
@@ -1209,7 +1274,11 @@ useDirectoryStore.subscribe((state, prevState) => {
}
const projects = useProjectsStore.getState().projects;
const resolvedProject = resolveProjectForDirectory(projects, nextDirectory);
const resolvedProject = resolveDraftProjectForDirectory(
projects,
useSessionStore.getState().availableWorktreesByProject,
nextDirectory,
);
useSessionStore.setState((store) => ({
newSessionDraft: {