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
@@ -9,6 +9,7 @@ import {
useSessionStore,
type WorkspaceDescriptor,
} from "./session-store";
import { patchWorkspaceScripts } from "../contexts/session-workspace-scripts";
function createWorkspace(
input: Partial<WorkspaceDescriptor> & Pick<WorkspaceDescriptor, "id">,
@@ -32,6 +33,23 @@ afterEach(() => {
useSessionStore.getState().clearSession("test-server");
});
function initializeTestSession(): void {
useSessionStore.getState().initializeSession("test-server", null as unknown as DaemonClient);
}
function getTestSessionReferences() {
const state = useSessionStore.getState();
const session = state.sessions["test-server"];
if (!session) {
throw new Error("test session is not initialized");
}
return {
sessions: state.sessions,
session,
workspaces: session.workspaces,
};
}
describe("normalizeWorkspaceDescriptor", () => {
it("normalizes workspace scripts and invalid activity timestamps", () => {
const scripts = [
@@ -138,6 +156,126 @@ describe("mergeWorkspaces", () => {
},
]);
});
it("preserves identity when merging content-equal workspace descriptors", () => {
const store = useSessionStore.getState();
initializeTestSession();
const workspace = createWorkspace({ id: "/repo/main" });
store.mergeWorkspaces("test-server", [workspace]);
const first = getTestSessionReferences();
store.mergeWorkspaces("test-server", [{ ...workspace, scripts: [...workspace.scripts] }]);
const second = getTestSessionReferences();
expect(second.sessions).toBe(first.sessions);
expect(second.session).toBe(first.session);
expect(second.workspaces).toBe(first.workspaces);
expect(second.workspaces.get("/repo/main")).toBe(first.workspaces.get("/repo/main"));
});
it("preserves unaffected workspace entry identity when one workspace changes", () => {
const store = useSessionStore.getState();
initializeTestSession();
const workspaceA = createWorkspace({ id: "/repo/a", name: "main" });
const workspaceB = createWorkspace({ id: "/repo/b", name: "feature" });
store.mergeWorkspaces("test-server", [workspaceA, workspaceB]);
const before = getTestSessionReferences();
const beforeA = before.workspaces.get("/repo/a");
const beforeB = before.workspaces.get("/repo/b");
store.mergeWorkspaces("test-server", [{ ...workspaceA, status: "running" }]);
const after = getTestSessionReferences();
expect(after.sessions).not.toBe(before.sessions);
expect(after.session).not.toBe(before.session);
expect(after.workspaces).not.toBe(before.workspaces);
expect(after.workspaces.get("/repo/a")).not.toBe(beforeA);
expect(after.workspaces.get("/repo/b")).toBe(beforeB);
});
});
describe("setWorkspaces", () => {
it("preserves identity when replacing workspaces with content-equal entries", () => {
const store = useSessionStore.getState();
initializeTestSession();
const workspace = createWorkspace({ id: "/repo/main" });
store.setWorkspaces("test-server", new Map([[workspace.id, workspace]]));
const before = getTestSessionReferences();
store.setWorkspaces(
"test-server",
new Map([[workspace.id, { ...workspace, scripts: [...workspace.scripts] }]]),
);
const after = getTestSessionReferences();
expect(after.sessions).toBe(before.sessions);
expect(after.session).toBe(before.session);
expect(after.workspaces).toBe(before.workspaces);
expect(after.workspaces.get(workspace.id)).toBe(before.workspaces.get(workspace.id));
});
it("stores replacement descriptors as-is when diff stat is cleared", () => {
const store = useSessionStore.getState();
initializeTestSession();
const workspace = createWorkspace({
id: "/repo/main",
diffStat: { additions: 2, deletions: 1 },
});
store.setWorkspaces("test-server", new Map([[workspace.id, workspace]]));
const before = getTestSessionReferences();
store.setWorkspaces("test-server", new Map([[workspace.id, { ...workspace, diffStat: null }]]));
const after = getTestSessionReferences();
expect(after.sessions).not.toBe(before.sessions);
expect(after.session).not.toBe(before.session);
expect(after.workspaces).not.toBe(before.workspaces);
expect(after.workspaces.get(workspace.id)?.diffStat).toBeNull();
});
});
describe("removeWorkspace", () => {
it("preserves identity when removing a missing workspace", () => {
const store = useSessionStore.getState();
initializeTestSession();
const workspace = createWorkspace({ id: "/repo/main" });
store.setWorkspaces("test-server", new Map([[workspace.id, workspace]]));
const before = getTestSessionReferences();
store.removeWorkspace("test-server", "/repo/missing");
const after = getTestSessionReferences();
expect(after.sessions).toBe(before.sessions);
expect(after.session).toBe(before.session);
expect(after.workspaces).toBe(before.workspaces);
});
});
describe("patchWorkspaceScripts", () => {
it("preserves workspace entry identity when scripts are content-equal", () => {
const script = {
scriptName: "web",
type: "service" as const,
hostname: "web.paseo.localhost",
port: 3000,
proxyUrl: "http://web.paseo.localhost:6767",
lifecycle: "running" as const,
health: "healthy" as const,
exitCode: null,
};
const workspace = createWorkspace({ id: "/repo/main", scripts: [script] });
const current = new Map([[workspace.id, workspace]]);
const next = patchWorkspaceScripts(current, {
workspaceId: workspace.id,
scripts: [{ ...script }],
});
expect(next).toBe(current);
expect(next.get(workspace.id)).toBe(workspace);
});
});
describe("mergeWorkspaceSnapshotWithExisting", () => {