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
+7 -15
View File
@@ -1,12 +1,10 @@
import { useMemo } from "react";
import { Text, View } from "react-native";
import { FileText } from "lucide-react-native";
import invariant from "tiny-invariant";
import { FilePane } from "@/components/file-pane";
import { usePaneContext } from "@/panels/pane-context";
import type { PanelRegistration } from "@/panels/panel-registry";
import { useSessionStore } from "@/stores/session-store";
import { resolveWorkspaceExecutionAuthority } from "@/utils/workspace-execution";
import { useWorkspaceExecutionAuthority } from "@/stores/session-store-hooks";
function useFilePanelDescriptor(target: { kind: "file"; path: string }) {
const fileName = target.path.split("/").filter(Boolean).pop() ?? target.path;
@@ -21,25 +19,19 @@ function useFilePanelDescriptor(target: { kind: "file"; path: string }) {
function FilePanel() {
const { serverId, workspaceId, target } = usePaneContext();
const workspace = useSessionStore(
(state) => state.sessions[serverId]?.workspaces.get(workspaceId) ?? null,
);
const authority = useMemo(() => resolveWorkspaceExecutionAuthority({ workspace }), [workspace]);
const workspaceAuthority = useWorkspaceExecutionAuthority(serverId, workspaceId);
const workspaceDirectory = workspaceAuthority?.ok
? workspaceAuthority.authority.workspaceDirectory
: null;
invariant(target.kind === "file", "FilePanel requires file target");
if (!authority) {
if (!workspaceDirectory) {
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center", padding: 16 }}>
<Text>Workspace execution directory not found.</Text>
</View>
);
}
return (
<FilePane
serverId={serverId}
workspaceRoot={authority.workspaceDirectory}
filePath={target.path}
/>
);
return <FilePane serverId={serverId} workspaceRoot={workspaceDirectory} filePath={target.path} />;
}
export const filePanelRegistration: PanelRegistration<"file"> = {
+3 -8
View File
@@ -8,7 +8,7 @@ import { TerminalPane } from "@/components/terminal-pane";
import { usePaneContext } from "@/panels/pane-context";
import type { PanelDescriptor, PanelRegistration } from "@/panels/panel-registry";
import { useSessionStore } from "@/stores/session-store";
import { getWorkspaceExecutionAuthority } from "@/utils/workspace-execution";
import { useWorkspaceExecutionAuthority } from "@/stores/session-store-hooks";
type ListTerminalsPayload = ListTerminalsResponse["payload"];
@@ -25,11 +25,7 @@ function useTerminalPanelDescriptor(
context: { serverId: string; workspaceId: string },
): PanelDescriptor {
const client = useSessionStore((state) => state.sessions[context.serverId]?.client ?? null);
const workspaces = useSessionStore((state) => state.sessions[context.serverId]?.workspaces);
const workspaceAuthority = getWorkspaceExecutionAuthority({
workspaces,
workspaceId: context.workspaceId,
});
const workspaceAuthority = useWorkspaceExecutionAuthority(context.serverId, context.workspaceId)!;
const workspaceDirectory = workspaceAuthority.ok
? workspaceAuthority.authority.workspaceDirectory
: null;
@@ -63,8 +59,7 @@ function useTerminalPanelDescriptor(
function TerminalPanel() {
const isFocused = useIsFocused();
const { serverId, workspaceId, target, isPaneFocused } = usePaneContext();
const workspaces = useSessionStore((state) => state.sessions[serverId]?.workspaces);
const workspaceAuthority = getWorkspaceExecutionAuthority({ workspaces, workspaceId });
const workspaceAuthority = useWorkspaceExecutionAuthority(serverId, workspaceId)!;
const workspaceDirectory = workspaceAuthority.ok
? workspaceAuthority.authority.workspaceDirectory
: null;