fix(chat): resolve parent project for worktree-session messages
Save-as-plan and Add-to-notes actions were disabled on messages whose session ran in a worktree, because session.directory alone does not reflect the worktree attachment — the authoritative cwd lives in session-worktree-store. Use useEffectiveDirectory (attachment cwd > worktree metadata > session.directory) and resolve the parent project via the worktree map the sidebar already builds, extracted into lib/projectResolution so session-ui-store and chat actions share one resolver. No extra git calls.
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
import type { ProjectEntry } from "@/lib/api/types";
|
||||
import type { WorktreeMetadata } from "@/types/worktree";
|
||||
|
||||
export const normalizeProjectPath = (value?: string | null): string | null => {
|
||||
if (typeof value !== "string") return null;
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return null;
|
||||
const replaced = trimmed.replace(/\\/g, "/");
|
||||
if (replaced === "/") return "/";
|
||||
return replaced.length > 1 ? replaced.replace(/\/+$/, "") : replaced;
|
||||
};
|
||||
|
||||
export const resolveProjectForDirectory = (
|
||||
projects: ProjectEntry[],
|
||||
directory: string | null,
|
||||
): ProjectEntry | null => {
|
||||
const nd = normalizeProjectPath(directory);
|
||||
if (!nd) return null;
|
||||
let best: ProjectEntry | null = null;
|
||||
for (const p of projects) {
|
||||
const pp = normalizeProjectPath(p.path);
|
||||
if (!pp) continue;
|
||||
if (nd !== pp && !nd.startsWith(`${pp}/`)) continue;
|
||||
if (!best || pp.length > (normalizeProjectPath(best.path)?.length ?? 0)) best = p;
|
||||
}
|
||||
return best;
|
||||
};
|
||||
|
||||
export const resolveProjectFromWorktreeDirectory = (
|
||||
projects: ProjectEntry[],
|
||||
availableWorktreesByProject: Map<string, WorktreeMetadata[]>,
|
||||
directory: string | null,
|
||||
): ProjectEntry | null => {
|
||||
const nd = normalizeProjectPath(directory);
|
||||
if (!nd) return null;
|
||||
let matchedWorktree: WorktreeMetadata | null = null;
|
||||
let matchedProjectPath: string | null = null;
|
||||
let bestLen = -1;
|
||||
for (const [projectPath, worktrees] of availableWorktreesByProject.entries()) {
|
||||
for (const wt of worktrees) {
|
||||
const wp = normalizeProjectPath(wt.path);
|
||||
if (!wp) continue;
|
||||
if (nd !== wp && !nd.startsWith(`${wp}/`)) continue;
|
||||
if (wp.length > bestLen) {
|
||||
bestLen = wp.length;
|
||||
matchedWorktree = wt;
|
||||
matchedProjectPath = normalizeProjectPath(projectPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!matchedWorktree) return null;
|
||||
const candidates = [normalizeProjectPath(matchedWorktree.projectDirectory), matchedProjectPath]
|
||||
.filter((v): v is string => Boolean(v));
|
||||
for (const c of candidates) {
|
||||
const exact = projects.find((p) => normalizeProjectPath(p.path) === c) ?? null;
|
||||
if (exact) return exact;
|
||||
const nested = resolveProjectForDirectory(projects, c);
|
||||
if (nested) return nested;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
export const resolveProjectForSessionDirectory = (
|
||||
projects: ProjectEntry[],
|
||||
availableWorktreesByProject: Map<string, WorktreeMetadata[]>,
|
||||
directory: string | null,
|
||||
): ProjectEntry | null =>
|
||||
resolveProjectFromWorktreeDirectory(projects, availableWorktreesByProject, directory) ??
|
||||
resolveProjectForDirectory(projects, directory);
|
||||
Reference in New Issue
Block a user