fix: resolve draft project for worktree paths (#722)
This commit is contained in:
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user