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:
Bohdan Triapitsyn
2026-04-18 16:05:55 +03:00
parent 4aa0dcdc1c
commit f04d732e15
4 changed files with 89 additions and 128 deletions
+2 -53
View File
@@ -28,6 +28,7 @@ import { flattenAssistantTextParts } from "@/lib/messages/messageText"
import { EXECUTION_FORK_META_TEXT } from "@/lib/messages/executionMeta"
import { waitForWorktreeBootstrap } from "@/lib/worktrees/worktreeBootstrap"
import { waitForPendingDraftWorktreeRequest } from "@/lib/worktrees/pendingDraftWorktree"
import { resolveProjectForSessionDirectory } from "@/lib/projectResolution"
import type { ProjectEntry } from "@/lib/api/types"
import {
getSyncSessions,
@@ -292,59 +293,7 @@ const persistDraftTarget = (target: PersistedDraftTarget): void => {
} catch { /* ignored */ }
}
const resolveProjectForDirectory = (projects: ProjectEntry[], directory: string | null): ProjectEntry | null => {
const nd = normalizePath(directory)
if (!nd) return null
let best: ProjectEntry | null = null
for (const p of projects) {
const pp = normalizePath(p.path)
if (!pp) continue
if (nd !== pp && !nd.startsWith(`${pp}/`)) continue
if (!best || pp.length > (normalizePath(best.path)?.length ?? 0)) best = p
}
return best
}
const resolveProjectFromWorktreeDirectory = (
projects: ProjectEntry[],
availableWorktreesByProject: Map<string, WorktreeMetadata[]>,
directory: string | null,
): ProjectEntry | null => {
const nd = normalizePath(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 = normalizePath(wt.path)
if (!wp) continue
if (nd !== wp && !nd.startsWith(`${wp}/`)) continue
if (wp.length > bestLen) {
bestLen = wp.length
matchedWorktree = wt
matchedProjectPath = normalizePath(projectPath)
}
}
}
if (!matchedWorktree) return null
const candidates = [normalizePath(matchedWorktree.projectDirectory), matchedProjectPath].filter((v): v is string => Boolean(v))
for (const c of candidates) {
const exact = projects.find((p) => normalizePath(p.path) === c) ?? null
if (exact) return exact
const nested = resolveProjectForDirectory(projects, c)
if (nested) return nested
}
return null
}
const resolveDraftProjectForDirectory = (
projects: ProjectEntry[],
availableWorktreesByProject: Map<string, WorktreeMetadata[]>,
directory: string | null,
): ProjectEntry | null =>
resolveProjectFromWorktreeDirectory(projects, availableWorktreesByProject, directory) ??
resolveProjectForDirectory(projects, directory)
const resolveDraftProjectForDirectory = resolveProjectForSessionDirectory
const getAttachmentForSession = (sessionId: string | null | undefined): SessionWorktreeAttachment | undefined => {
if (!sessionId) return undefined