import type { Session } from '@opencode-ai/sdk/v2'; import { normalizePath } from '@/lib/pathNormalization'; type Project = { id: string; normalizedPath: string; }; type Worktree = { path: string; }; export type DirectoryOwner = { projectId: string; projectRoot: string; scopeDirectory: string; kind: 'project' | 'worktree'; }; export type SessionOwnershipIndex = { bySessionId: Map; sessionsByProject: Map; archivedSessionsByProject: Map; sessionsByScope: Map>; directoryResolutions: number; }; const shouldReplaceOwner = (existing: DirectoryOwner | undefined, candidate: DirectoryOwner): boolean => { if (!existing) return true; if (candidate.kind !== existing.kind) { return candidate.kind === 'project'; } if (candidate.projectRoot.length !== existing.projectRoot.length) { return candidate.projectRoot.length > existing.projectRoot.length; } return candidate.projectId.localeCompare(existing.projectId) < 0; }; const setOwner = (owners: Map, directory: string, candidate: DirectoryOwner): void => { if (shouldReplaceOwner(owners.get(directory), candidate)) { owners.set(directory, candidate); } }; const resolveSessionDirectory = (session: Session): string | null => { const record = session as Session & { directory?: string | null; project?: { worktree?: string | null } | null; }; return normalizePath(record.directory) ?? normalizePath(record.project?.worktree); }; const getParentDirectory = (directory: string): string | null => { if (directory === '/' || /^[A-Z]:$/.test(directory)) { return null; } const separator = directory.lastIndexOf('/'); if (separator < 0) return null; if (separator === 0) return '/'; if (separator === 2 && /^[A-Z]:\//.test(directory)) return directory.slice(0, 2); return directory.slice(0, separator); }; export const createSessionOwnershipIndex = ( sessions: Session[], projects: Project[], availableWorktreesByProject: Map, isVSCode: boolean, archivedSessions: Session[] = [], ): SessionOwnershipIndex => { const ownerByDirectory = new Map(); const projectByRoot = new Map(); for (const project of projects) { const projectRoot = normalizePath(project.normalizedPath); if (!projectRoot) continue; const existingProject = projectByRoot.get(projectRoot); if (!existingProject || project.id.localeCompare(existingProject.id) < 0) { projectByRoot.set(projectRoot, project); } setOwner(ownerByDirectory, projectRoot, { projectId: project.id, projectRoot, scopeDirectory: projectRoot, kind: 'project', }); } if (!isVSCode) { for (const [projectPath, worktrees] of availableWorktreesByProject) { const projectRoot = normalizePath(projectPath); const project = projectRoot ? projectByRoot.get(projectRoot) : undefined; if (!project || !projectRoot) continue; for (const worktree of worktrees) { const directory = normalizePath(worktree.path); if (!directory) continue; setOwner(ownerByDirectory, directory, { projectId: project.id, projectRoot, scopeDirectory: directory, kind: 'worktree', }); } } } const resolvedOwners = new Map(); const bySessionId = new Map(); const sessionsByProject = new Map(); const archivedSessionsByProject = new Map(); const sessionsByScope = new Map>(); const resolveOwner = (directory: string | null): DirectoryOwner | null => { if (!directory) return null; if (resolvedOwners.has(directory)) { return resolvedOwners.get(directory) ?? null; } if (isVSCode) { const owner = ownerByDirectory.get(directory) ?? null; resolvedOwners.set(directory, owner); return owner; } const visited: string[] = []; let current: string | null = directory; let owner: DirectoryOwner | null = null; while (current) { if (resolvedOwners.has(current)) { owner = resolvedOwners.get(current) ?? null; break; } visited.push(current); owner = ownerByDirectory.get(current) ?? null; if (owner) break; current = getParentDirectory(current); } for (const visitedDirectory of visited) { resolvedOwners.set(visitedDirectory, owner); } return owner; }; const bucket = ( input: Session[], target: Map, scopeTarget?: Map>, ): void => { for (const session of input) { const owner = resolveOwner(resolveSessionDirectory(session)); if (!owner) continue; bySessionId.set(session.id, owner); const projectSessions = target.get(owner.projectId); if (projectSessions) { projectSessions.push(session); } else { target.set(owner.projectId, [session]); } if (!scopeTarget) continue; const scopeSessions = scopeTarget.get(owner.scopeDirectory); if (scopeSessions) { scopeSessions.add(session.id); } else { scopeTarget.set(owner.scopeDirectory, new Set([session.id])); } } }; bucket(sessions, sessionsByProject, sessionsByScope); bucket(archivedSessions, archivedSessionsByProject); return { bySessionId, sessionsByProject, archivedSessionsByProject, sessionsByScope, directoryResolutions: resolvedOwners.size, }; };