feat(sessions): move sessions to existing worktrees

This commit is contained in:
mattv8
2026-08-27 18:42:57 -06:00
parent b8465ae133
commit 9babd19d9b
29 changed files with 1967 additions and 98 deletions
@@ -374,7 +374,7 @@ export const partitionWorktreesByRegisteredProject = (
// Cache worktree listings to avoid repeated git worktree list + rev-parse calls
const _worktreeListCache = new Map<string, { value: WorktreeMetadata[]; at: number }>();
const _worktreeListInflight = new Map<string, Promise<WorktreeMetadata[]>>();
const _worktreeListInflight = new Map<string, { generation: number; promise: Promise<WorktreeMetadata[]> }>();
const _worktreeListGeneration = new Map<string, number>();
const WORKTREE_LIST_CACHE_TTL = 30_000; // 30 seconds
@@ -391,7 +391,7 @@ const readProjectWorktrees = async (projectDirectory: string): Promise<WorktreeM
const metadataProjectDirectory = await resolveProjectRoot(projectDirectory).catch(() => projectDirectory);
const normalizedProjectDirectory = normalizePath(projectDirectory);
const worktrees = await git.worktree.list(projectDirectory).catch(() => []);
const worktrees = await git.worktree.list(projectDirectory);
const results: WorktreeMetadata[] = worktrees
.filter((entry) => typeof entry.path === 'string' && entry.path.trim().length > 0)
.map((entry) => {
@@ -424,38 +424,48 @@ const readProjectWorktrees = async (projectDirectory: string): Promise<WorktreeM
});
};
const readStableProjectWorktrees = async (projectDirectory: string): Promise<WorktreeMetadata[]> => {
const readStableProjectWorktrees = async (
projectDirectory: string,
minimumGeneration = getWorktreeListGeneration(projectDirectory),
): Promise<WorktreeMetadata[]> => {
while (true) {
const generation = getWorktreeListGeneration(projectDirectory);
const worktrees = await readProjectWorktrees(projectDirectory);
if (generation === getWorktreeListGeneration(projectDirectory)) {
if (generation >= minimumGeneration && generation === getWorktreeListGeneration(projectDirectory)) {
_worktreeListCache.set(projectDirectory, { value: worktrees, at: Date.now() });
return worktrees;
}
}
};
export async function listProjectWorktrees(project: ProjectRef): Promise<WorktreeMetadata[]> {
export async function listProjectWorktrees(project: ProjectRef, options?: { force?: boolean }): Promise<WorktreeMetadata[]> {
const projectDirectory = normalizePath(project.path);
const force = options?.force === true;
if (force) {
invalidateWorktreeList(projectDirectory);
}
const generation = getWorktreeListGeneration(projectDirectory);
// Return cached if fresh
const cached = _worktreeListCache.get(projectDirectory);
if (cached && Date.now() - cached.at < WORKTREE_LIST_CACHE_TTL) {
if (!force && cached && Date.now() - cached.at < WORKTREE_LIST_CACHE_TTL) {
return cached.value;
}
// Dedup in-flight requests
const inflight = _worktreeListInflight.get(projectDirectory);
if (inflight) return inflight;
if (inflight && inflight.generation === generation) return inflight.promise;
const promise = readStableProjectWorktrees(projectDirectory).finally(() => {
if (_worktreeListInflight.get(projectDirectory) === promise) {
const promise = readStableProjectWorktrees(projectDirectory, generation).finally(() => {
if (_worktreeListInflight.get(projectDirectory)?.promise === promise) {
_worktreeListInflight.delete(projectDirectory);
}
});
_worktreeListInflight.set(projectDirectory, promise);
_worktreeListInflight.set(projectDirectory, { generation, promise });
return promise;
}