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
+49 -7
View File
@@ -1,3 +1,4 @@
import equal from "fast-deep-equal";
import { create } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
import type { DaemonClient } from "@server/client/daemon-client";
@@ -159,6 +160,41 @@ export function mergeWorkspaceSnapshotWithExisting(input: {
};
}
function preserveWorkspaceDescriptorIdentity(
incoming: WorkspaceDescriptor,
existing?: WorkspaceDescriptor | null,
): WorkspaceDescriptor {
if (existing && equal(existing, incoming)) {
return existing;
}
return incoming;
}
function preserveWorkspaceMapIdentity(
existing: Map<string, WorkspaceDescriptor>,
incoming: Map<string, WorkspaceDescriptor>,
): Map<string, WorkspaceDescriptor> {
if (existing === incoming) {
return existing;
}
const next = new Map<string, WorkspaceDescriptor>();
let changed = existing.size !== incoming.size;
const existingEntries = existing.entries();
for (const [key, workspace] of incoming) {
const existingWorkspace = existing.get(key);
const nextWorkspace = preserveWorkspaceDescriptorIdentity(workspace, existingWorkspace);
next.set(key, nextWorkspace);
const existingEntry = existingEntries.next().value;
if (!existingEntry || existingEntry[0] !== key || existingEntry[1] !== nextWorkspace) {
changed = true;
}
}
return changed ? next : existing;
}
export type ExplorerEntryKind = "file" | "directory";
export type ExplorerFileKind = "text" | "image" | "binary";
export type ExplorerEncoding = "utf-8" | "base64" | "none";
@@ -938,14 +974,18 @@ export const useSessionStore = create<SessionStore>()(
}
const nextWorkspaces =
typeof workspaces === "function" ? workspaces(session.workspaces) : workspaces;
if (session.workspaces === nextWorkspaces) {
const preservedWorkspaces = preserveWorkspaceMapIdentity(
session.workspaces,
nextWorkspaces,
);
if (session.workspaces === preservedWorkspaces) {
return prev;
}
return {
...prev,
sessions: {
...prev.sessions,
[serverId]: { ...session, workspaces: nextWorkspaces },
[serverId]: { ...session, workspaces: preservedWorkspaces },
},
};
});
@@ -962,13 +1002,15 @@ export const useSessionStore = create<SessionStore>()(
let changed = false;
for (const workspace of nextEntries) {
const existing = next.get(workspace.id);
if (existing === workspace) {
const mergedWorkspace = mergeWorkspaceSnapshotWithExisting({
incoming: workspace,
existing,
});
const nextWorkspace = preserveWorkspaceDescriptorIdentity(mergedWorkspace, existing);
if (existing === nextWorkspace) {
continue;
}
next.set(
workspace.id,
mergeWorkspaceSnapshotWithExisting({ incoming: workspace, existing }),
);
next.set(workspace.id, nextWorkspace);
changed = true;
}
if (!changed) {