perf(sidebar): index session ownership and narrow live subscriptions

Replace repeated project-by-session directory matching across sidebar hooks with a shared ownership index that resolves each unique directory once and exposes direct project and folder-scope buckets.

Gate destructive folder reconciliation on authoritative session data and topology readiness, preserve last-known worktrees after discovery failures, and retain nested-project, VS Code, active/archive dedupe, and Windows drive-root semantics.

Narrow cross-directory subscriptions to session and status slices so streaming deltas no longer trigger global aggregation. Reuse a cached session ID index for permission lineage checks instead of rebuilding it on every session switch.

On the reported 15-project, 67-worktree, 14,561-session shape, ownership indexing averages 3.81 ms versus roughly 450 ms for the cache-only hotfix.

Validation: 28 targeted tests, UI type-check, UI lint, and dead-code analysis.
This commit is contained in:
Bohdan Triapitsyn
2026-07-13 23:18:12 +03:00
parent 799904f0f4
commit b36afbf5ee
19 changed files with 619 additions and 511 deletions
@@ -1,16 +1,12 @@
import React from 'react';
import type { Session } from '@opencode-ai/sdk/v2';
import type { WorktreeMetadata } from '@/types/worktree';
import {
collectKnownProjectDirectories,
dedupeSessionsById,
getArchivedScopeKey,
isSessionRelatedToProject,
normalizePath,
resolveArchivedFolderName,
} from '../utils';
import type { SessionOwnershipIndex } from '../sessionOwnership';
type ProjectForArchivedFolders = {
id: string;
normalizedPath: string;
};
@@ -22,83 +18,42 @@ type FolderEntry = {
type Args = {
normalizedProjects: ProjectForArchivedFolders[];
sessions: Session[];
archivedSessions: Session[];
availableWorktreesByProject: Map<string, WorktreeMetadata[]>;
isVSCode: boolean;
ownership: SessionOwnershipIndex;
isSessionsLoading: boolean;
hasAuthoritativeGlobalSessions: boolean;
isWorktreeTopologyLoading: boolean;
unresolvedWorktreeProjectPaths: ReadonlySet<string>;
foldersMap: Record<string, FolderEntry[]>;
createFolder: (scopeKey: string, name: string, parentId?: string | null) => FolderEntry;
addSessionToFolder: (scopeKey: string, folderId: string, sessionId: string) => void;
cleanupSessions: (scopeKey: string, existingSessionIds: Set<string>) => void;
};
const getArchivedSessionsForProject = (
project: ProjectForArchivedFolders,
params: Pick<Args, 'sessions' | 'archivedSessions' | 'availableWorktreesByProject' | 'isVSCode'> & {
knownProjectDirectories: Set<string>;
},
): Session[] => {
const worktreesForProject = params.isVSCode ? [] : (params.availableWorktreesByProject.get(project.normalizedPath) ?? []);
const validDirectories = new Set<string>([
project.normalizedPath,
...worktreesForProject
.map((meta) => normalizePath(meta.path) ?? meta.path)
.filter((value): value is string => Boolean(value)),
]);
const collect = (input: Session[]): Session[] => input.filter((session) =>
isSessionRelatedToProject(session, project.normalizedPath, validDirectories, params.knownProjectDirectories),
);
const archived = collect(params.archivedSessions);
const unassignedLive = params.sessions.filter((session) => {
if (session.time?.archived) {
return false;
}
const sessionDirectory = normalizePath((session as Session & { directory?: string | null }).directory ?? null);
if (sessionDirectory) {
return false;
}
return isSessionRelatedToProject(session, project.normalizedPath, validDirectories, params.knownProjectDirectories);
});
return dedupeSessionsById([...archived, ...unassignedLive]);
};
export const useArchivedAutoFolders = (args: Args): void => {
const {
normalizedProjects,
sessions,
archivedSessions,
availableWorktreesByProject,
isVSCode,
ownership,
isSessionsLoading,
hasAuthoritativeGlobalSessions,
isWorktreeTopologyLoading,
unresolvedWorktreeProjectPaths,
foldersMap,
createFolder,
addSessionToFolder,
cleanupSessions,
} = args;
const knownProjectDirectories = React.useMemo(
() => collectKnownProjectDirectories(normalizedProjects, availableWorktreesByProject, isVSCode),
[normalizedProjects, availableWorktreesByProject, isVSCode],
);
React.useEffect(() => {
if (isSessionsLoading) {
if (isSessionsLoading || !hasAuthoritativeGlobalSessions || isWorktreeTopologyLoading) {
return;
}
normalizedProjects.forEach((project) => {
if (unresolvedWorktreeProjectPaths.has(project.normalizedPath)) {
return;
}
const scopeKey = getArchivedScopeKey(project.normalizedPath);
const projectArchivedSessions = getArchivedSessionsForProject(project, {
sessions,
archivedSessions,
availableWorktreesByProject,
isVSCode,
knownProjectDirectories,
});
const projectArchivedSessions = ownership.archivedSessionsByProject.get(project.id) ?? [];
const sessionIds = new Set(projectArchivedSessions.map((session) => session.id));
const existingFolders = foldersMap[scopeKey] ?? [];
@@ -122,12 +77,11 @@ export const useArchivedAutoFolders = (args: Args): void => {
});
}, [
normalizedProjects,
sessions,
archivedSessions,
availableWorktreesByProject,
knownProjectDirectories,
isVSCode,
ownership,
isSessionsLoading,
hasAuthoritativeGlobalSessions,
isWorktreeTopologyLoading,
unresolvedWorktreeProjectPaths,
foldersMap,
createFolder,
addSessionToFolder,