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
+28 -7
View File
@@ -1,5 +1,6 @@
import { useSyncExternalStore, useCallback, useMemo } from "react";
import AsyncStorage from "@react-native-async-storage/async-storage";
import equal from "fast-deep-equal/es6";
import {
DaemonClient,
type ConnectionState,
@@ -67,6 +68,16 @@ export type HostRuntimeSnapshot = {
clientGeneration: number;
};
type HostRuntimeSnapshotPatch = Partial<Omit<HostRuntimeSnapshot, "serverId" | "clientGeneration">>;
function setSnapshotPatchField<Key extends keyof HostRuntimeSnapshotPatch>(
patch: HostRuntimeSnapshotPatch,
key: Key,
value: HostRuntimeSnapshot[Key],
): void {
patch[key] = value;
}
export function isHostRuntimeConnected(snapshot: HostRuntimeSnapshot | null): boolean {
return snapshot?.connectionStatus === "online";
}
@@ -831,15 +842,25 @@ export class HostRuntimeController {
});
}
private updateSnapshot(
patch: Partial<Omit<HostRuntimeSnapshot, "serverId" | "clientGeneration">>,
): void {
const next: HostRuntimeSnapshot = {
private updateSnapshot(patch: HostRuntimeSnapshotPatch): void {
const preservedPatch: HostRuntimeSnapshotPatch = { ...patch };
let hasChanged = this.host.serverId !== this.snapshot.serverId;
for (const key of Object.keys(patch) as Array<keyof HostRuntimeSnapshotPatch>) {
const incomingValue = patch[key];
if (equal(this.snapshot[key], incomingValue)) {
setSnapshotPatchField(preservedPatch, key, this.snapshot[key]);
continue;
}
hasChanged = true;
}
if (!hasChanged) {
return;
}
this.snapshot = {
...this.snapshot,
...patch,
...preservedPatch,
serverId: this.host.serverId,
};
this.snapshot = next;
for (const listener of this.listeners) {
listener();
}
@@ -1021,7 +1042,7 @@ export class HostRuntimeController {
state,
lastError: client.lastError,
});
const patch: Partial<Omit<HostRuntimeSnapshot, "serverId" | "clientGeneration">> = {
const patch: HostRuntimeSnapshotPatch = {
...toSnapshotConnectionPatch(this.connectionMachineState),
};