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:
@@ -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"> = {
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user