refactor(app): reshape session store reads around narrow hook surface

Eliminate sidebar re-render cascades by fixing both sides of the
subscription contract:

- Session-store writes preserve identity: setWorkspaces, mergeWorkspaces,
  removeWorkspace, and patchWorkspaceScripts return the previous Map/entry
  reference when content is unchanged.
- HostRuntimeController.updateSnapshot only notifies when a patched field
  actually changed; idle probe ticks no longer replace the snapshot.
- New canonical hook surface in stores/session-store-hooks.ts
  (useWorkspace, useWorkspaceFields, useWorkspaceStructure,
  useWorkspaceKeys, useResolveWorkspaceIdByCwd,
  useWorkspaceStatusesForBadges, useWorkspaceExecutionAuthority,
  useRecommendedProjectPaths, useHasWorkspaces). These are the only
  place to subscribe to workspaces.
- 15 greedy subscribers migrated to the hook surface. The sidebar is now
  driven by useWorkspaceStructure for identity/ordering; each row
  hydrates its own descriptor via useWorkspaceFields, so a workspace
  update re-renders only its row. Archive-redirect callbacks switched to
  event-time useSessionStore.getState().
- Deleted dead code: use-session-directory.ts, workspace-fetch-debug.ts,
  and the unused SidebarProjectEntry aggregates (activeCount,
  totalWorkspaces, aggregated statusBucket).

No user-visible behavior change. Targeted tests green (79 tests across
host-runtime, session-store, session-store-hooks, sidebar list, sidebar
row model, sidebar shortcuts, workspace source of truth).
This commit is contained in:
Mohamed Boudra
2026-04-19 20:19:45 +07:00
parent 8991d34f78
commit bd1fb311ea
27 changed files with 1797 additions and 623 deletions
@@ -31,9 +31,6 @@ function project(overrides: Partial<SidebarProjectEntry> = {}): SidebarProjectEn
projectName: "paseo",
projectKind: "git",
iconWorkingDir: "/repo",
statusBucket: "done",
activeCount: 0,
totalWorkspaces: 1,
workspaces: [workspace()],
...overrides,
};
@@ -33,9 +33,6 @@ function project(projectKey: string, workspaces: SidebarWorkspaceEntry[]): Sideb
projectName: projectKey,
projectKind: "git",
iconWorkingDir: workspaces[0]?.workspaceDirectory ?? "",
statusBucket: "done",
activeCount: 0,
totalWorkspaces: workspaces.length,
workspaces,
};
}
@@ -1,104 +0,0 @@
type WorkspaceLike = {
id: string;
projectId: string;
projectDisplayName?: string;
name: string;
status: string;
workspaceKind: string;
};
type SidebarWorkspaceLike = {
workspaceId: string;
name: string;
statusBucket: string;
workspaceKind: string;
};
type SidebarProjectLike = {
projectKey: string;
projectName: string;
statusBucket: string;
activeCount: number;
totalWorkspaces: number;
workspaces: SidebarWorkspaceLike[];
};
function countByValue(values: Iterable<string>): Record<string, number> {
const counts = new Map<string, number>();
for (const value of values) {
counts.set(value, (counts.get(value) ?? 0) + 1);
}
return Object.fromEntries(counts);
}
export function summarizeWorkspaceCollection(
workspaces: Iterable<WorkspaceLike> | null | undefined,
): {
count: number;
projectIds: string[];
statusCounts: Record<string, number>;
workspaces: Array<{
id: string;
projectId: string;
projectDisplayName: string | null;
name: string;
status: string;
workspaceKind: string;
}>;
} {
const entries = Array.from(workspaces ?? [], (workspace) => ({
id: workspace.id,
projectId: workspace.projectId,
projectDisplayName: workspace.projectDisplayName?.trim() || null,
name: workspace.name,
status: workspace.status,
workspaceKind: workspace.workspaceKind,
}));
return {
count: entries.length,
projectIds: [...new Set(entries.map((workspace) => workspace.projectId))],
statusCounts: countByValue(entries.map((workspace) => workspace.status)),
workspaces: entries,
};
}
export function summarizeSidebarProjects(
projects: Iterable<SidebarProjectLike> | null | undefined,
): {
count: number;
projectKeys: string[];
projects: Array<{
projectKey: string;
projectName: string;
statusBucket: string;
activeCount: number;
totalWorkspaces: number;
workspaces: Array<{
workspaceId: string;
name: string;
statusBucket: string;
workspaceKind: string;
}>;
}>;
} {
const entries = Array.from(projects ?? [], (project) => ({
projectKey: project.projectKey,
projectName: project.projectName,
statusBucket: project.statusBucket,
activeCount: project.activeCount,
totalWorkspaces: project.totalWorkspaces,
workspaces: project.workspaces.map((workspace) => ({
workspaceId: workspace.workspaceId,
name: workspace.name,
statusBucket: workspace.statusBucket,
workspaceKind: workspace.workspaceKind,
})),
}));
return {
count: entries.length,
projectKeys: entries.map((project) => project.projectKey),
projects: entries,
};
}