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
@@ -6,7 +6,7 @@ import { useQuery } from "@tanstack/react-query";
import { usePathname } from "expo-router";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { shortenPath } from "@/utils/shorten-path";
import { useSessionStore } from "@/stores/session-store";
import { useRecommendedProjectPaths } from "@/stores/session-store-hooks";
import { useHosts, useHostRuntimeClient, useHostRuntimeIsConnected } from "@/runtime/host-runtime";
import { useOpenProject } from "@/hooks/use-open-project";
import { parseServerIdFromPathname } from "@/utils/host-routes";
@@ -29,9 +29,7 @@ export function ProjectPickerModal() {
const client = useHostRuntimeClient(serverId ?? "");
const isConnected = useHostRuntimeIsConnected(serverId ?? "");
const workspaces = useSessionStore((state) =>
serverId ? state.sessions[serverId]?.workspaces : undefined,
);
const recommendedPaths = useRecommendedProjectPaths(serverId);
const inputRef = useRef<TextInput>(null);
const [query, setQuery] = useState("");
@@ -39,13 +37,6 @@ export function ProjectPickerModal() {
const [isSubmitting, setIsSubmitting] = useState(false);
const openProject = useOpenProject(serverId);
const recommendedPaths = useMemo(() => {
if (!workspaces) return [];
return Array.from(workspaces.values())
.map((workspace) => workspace.projectRootPath)
.filter((path) => path.length > 0);
}, [workspaces]);
const directorySuggestionsQuery = useQuery({
queryKey: ["project-picker-directory-suggestions", serverId, query],
queryFn: async () => {
@@ -0,0 +1,337 @@
/**
* @vitest-environment jsdom
*/
import { act } from "@testing-library/react";
import type { DaemonClient } from "@server/client/daemon-client";
import type { WorkspaceScriptPayload } from "@server/shared/messages";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { createRoot, type Root } from "react-dom/client";
import React from "react";
import type { ReactElement } from "react";
vi.hoisted(() => {
(globalThis as unknown as { __DEV__: boolean }).__DEV__ = false;
});
import {
createSidebarWorkspaceEntry,
type SidebarProjectEntry,
} from "@/hooks/use-sidebar-workspaces-list";
import { useSidebarWorkspacesList } from "@/hooks/use-sidebar-workspaces-list";
import { patchWorkspaceScripts } from "@/contexts/session-workspace-scripts";
import {
getHostRuntimeStore,
type HostRuntimeController,
type HostRuntimeSnapshot,
} from "@/runtime/host-runtime";
import type { HostProfile } from "@/types/host-connection";
import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store";
import { useSidebarOrderStore } from "@/stores/sidebar-order-store";
import { useWorkspaceFields } from "@/stores/session-store-hooks";
vi.mock("@react-native-async-storage/async-storage", () => ({
default: {
getItem: vi.fn(),
setItem: vi.fn(),
removeItem: vi.fn(),
},
}));
const SERVER_ID = "sidebar-render-count";
type RenderCounts = {
frame: number;
headers: Record<string, number>;
rows: Record<string, number>;
};
const runningScript: WorkspaceScriptPayload = {
scriptName: "web",
type: "service",
hostname: "web.paseo.localhost",
port: 3000,
proxyUrl: "http://web.paseo.localhost:6767",
lifecycle: "running",
health: "healthy",
exitCode: null,
};
function workspace(input: {
id: string;
projectId: string;
projectDisplayName: string;
name: string;
status?: WorkspaceDescriptor["status"];
scripts?: WorkspaceDescriptor["scripts"];
}): WorkspaceDescriptor {
return {
id: input.id,
projectId: input.projectId,
projectDisplayName: input.projectDisplayName,
projectRootPath: `/repo/${input.projectId}`,
workspaceDirectory: `/repo/${input.projectId}/${input.id}`,
projectKind: "git",
workspaceKind: input.name === "main" ? "local_checkout" : "worktree",
name: input.name,
status: input.status ?? "done",
diffStat: null,
scripts: input.scripts ?? [],
};
}
function createWorkspaces(): WorkspaceDescriptor[] {
return [
workspace({
id: "a-main",
projectId: "project-a",
projectDisplayName: "Project A",
name: "main",
scripts: [runningScript],
}),
workspace({
id: "a-one",
projectId: "project-a",
projectDisplayName: "Project A",
name: "one",
}),
workspace({
id: "a-two",
projectId: "project-a",
projectDisplayName: "Project A",
name: "two",
}),
workspace({
id: "b-main",
projectId: "project-b",
projectDisplayName: "Project B",
name: "main",
}),
workspace({
id: "b-one",
projectId: "project-b",
projectDisplayName: "Project B",
name: "one",
}),
workspace({
id: "b-two",
projectId: "project-b",
projectDisplayName: "Project B",
name: "two",
}),
];
}
function makeHost(): HostProfile {
const now = "2026-04-19T00:00:00.000Z";
return {
serverId: SERVER_ID,
label: "Render Count Host",
lifecycle: {},
connections: [],
preferredConnectionId: null,
createdAt: now,
updatedAt: now,
};
}
function initializeSidebarState(workspaces: WorkspaceDescriptor[]): void {
act(() => {
getHostRuntimeStore().syncHosts([makeHost()]);
useSessionStore.getState().initializeSession(SERVER_ID, null as unknown as DaemonClient);
useSessionStore
.getState()
.setWorkspaces(SERVER_ID, new Map(workspaces.map((entry) => [entry.id, entry])));
useSessionStore.getState().setHasHydratedWorkspaces(SERVER_ID, true);
useSidebarOrderStore.setState({
projectOrderByServerId: {
[SERVER_ID]: ["project-a", "project-b"],
},
workspaceOrderByServerAndProject: {
[`${SERVER_ID}::project-a`]: [
`${SERVER_ID}:a-main`,
`${SERVER_ID}:a-one`,
`${SERVER_ID}:a-two`,
],
[`${SERVER_ID}::project-b`]: [
`${SERVER_ID}:b-main`,
`${SERVER_ID}:b-one`,
`${SERVER_ID}:b-two`,
],
},
});
});
}
function resetCounts(counts: RenderCounts): void {
counts.frame = 0;
counts.headers = {};
counts.rows = {};
}
function incrementRecord(record: Record<string, number>, key: string): void {
record[key] = (record[key] ?? 0) + 1;
}
function ProjectHeaderProbe({
project,
counts,
}: {
project: SidebarProjectEntry;
counts: RenderCounts;
}): null {
incrementRecord(counts.headers, project.projectKey);
return null;
}
function WorkspaceRowProbe({
serverId,
workspaceId,
counts,
}: {
serverId: string;
workspaceId: string;
counts: RenderCounts;
}): null {
const workspace = useWorkspaceFields(serverId, workspaceId, (entry) =>
createSidebarWorkspaceEntry({ serverId, workspace: entry }),
);
if (workspace) {
incrementRecord(counts.rows, workspace.workspaceId);
}
return null;
}
function SidebarFrameProbe({ counts }: { counts: RenderCounts }): ReactElement {
counts.frame += 1;
const { projects } = useSidebarWorkspacesList({ serverId: SERVER_ID });
return (
<>
{projects.map((project) => (
<div key={project.projectKey}>
<ProjectHeaderProbe project={project} counts={counts} />
{project.workspaces.map((workspace) => (
<WorkspaceRowProbe
key={workspace.workspaceKey}
serverId={workspace.serverId}
workspaceId={workspace.workspaceId}
counts={counts}
/>
))}
</div>
))}
</>
);
}
function getHostController(): HostRuntimeController {
const controllers = (
getHostRuntimeStore() as unknown as {
controllers: Map<string, HostRuntimeController>;
}
).controllers;
const controller = controllers.get(SERVER_ID);
if (!controller) {
throw new Error("Host runtime controller was not initialized");
}
return controller;
}
function updateControllerSnapshot(
patch: Partial<Omit<HostRuntimeSnapshot, "serverId" | "clientGeneration">>,
): void {
(
getHostController() as unknown as {
updateSnapshot: (
patch: Partial<Omit<HostRuntimeSnapshot, "serverId" | "clientGeneration">>,
) => void;
}
).updateSnapshot(patch);
}
async function renderProbe(counts: RenderCounts): Promise<{ root: Root; container: HTMLElement }> {
const container = document.createElement("div");
document.body.appendChild(container);
const root = createRoot(container);
await act(async () => {
root.render(<SidebarFrameProbe counts={counts} />);
});
resetCounts(counts);
return { root, container };
}
describe("sidebar workspace render isolation", () => {
let root: Root | null = null;
let container: HTMLElement | null = null;
beforeEach(async () => {
initializeSidebarState(createWorkspaces());
});
afterEach(() => {
if (root) {
act(() => {
root?.unmount();
});
}
root = null;
container?.remove();
container = null;
act(() => {
getHostRuntimeStore().syncHosts([]);
useSessionStore.getState().clearSession(SERVER_ID);
useSidebarOrderStore.setState({
projectOrderByServerId: {},
workspaceOrderByServerAndProject: {},
});
});
});
it("re-renders only the changed workspace row for a status update", async () => {
const counts: RenderCounts = { frame: 0, headers: {}, rows: {} };
({ root, container } = await renderProbe(counts));
act(() => {
useSessionStore.getState().mergeWorkspaces(SERVER_ID, [
{
...createWorkspaces()[1]!,
status: "running",
},
]);
});
expect(counts.frame).toBe(0);
expect(counts.headers).toEqual({});
expect(counts.rows).toEqual({ "a-one": 1 });
});
it("does not re-render the sidebar for a host-runtime probe tick with no content change", async () => {
const counts: RenderCounts = { frame: 0, headers: {}, rows: {} };
({ root, container } = await renderProbe(counts));
act(() => {
const probeByConnectionId = getHostController().getSnapshot().probeByConnectionId;
updateControllerSnapshot({
probeByConnectionId: new Map(probeByConnectionId),
});
});
expect(counts).toEqual({ frame: 0, headers: {}, rows: {} });
});
it("does not re-render for a deep-equal scripts patch", async () => {
const counts: RenderCounts = { frame: 0, headers: {}, rows: {} };
({ root, container } = await renderProbe(counts));
act(() => {
useSessionStore.getState().setWorkspaces(SERVER_ID, (current) =>
patchWorkspaceScripts(current, {
workspaceId: "a-main",
scripts: [{ ...runningScript }],
}),
);
});
expect(counts).toEqual({ frame: 0, headers: {}, rows: {} });
});
});
@@ -56,6 +56,7 @@ import {
} from "@/utils/host-routes";
import { prepareWorkspaceTab } from "@/utils/workspace-navigation";
import {
createSidebarWorkspaceEntry,
type SidebarProjectEntry,
type SidebarWorkspaceEntry,
} from "@/hooks/use-sidebar-workspaces-list";
@@ -92,7 +93,12 @@ import { useKeyboardActionHandler } from "@/hooks/use-keyboard-action-handler";
import { type PrHint, useWorkspacePrHint } from "@/hooks/use-checkout-pr-status-query";
import { buildSidebarProjectRowModel } from "@/utils/sidebar-project-row-model";
import { useNavigationActiveWorkspaceSelection } from "@/stores/navigation-active-workspace-store";
import { normalizeWorkspaceDescriptor, useSessionStore } from "@/stores/session-store";
import {
normalizeWorkspaceDescriptor,
useSessionStore,
type WorkspaceDescriptor,
} from "@/stores/session-store";
import { useWorkspaceFields } from "@/stores/session-store-hooks";
import { buildWorkspaceArchiveRedirectRoute } from "@/utils/workspace-archive-navigation";
import { openExternalUrl } from "@/utils/open-external-url";
import {
@@ -114,7 +120,6 @@ function toProjectIconDataUri(icon: { mimeType: string; data: string } | null):
const workspaceKeyExtractor = (workspace: SidebarWorkspaceEntry) => workspace.workspaceKey;
const projectKeyExtractor = (project: SidebarProjectEntry) => project.projectKey;
const EMPTY_WORKSPACES = new Map();
const WORKSPACE_STATUS_DOT_WIDTH = 14;
const DEFAULT_STATUS_DOT_SIZE = 7;
const EMPHASIZED_STATUS_DOT_SIZE = 9;
@@ -194,6 +199,19 @@ interface WorkspaceRowInnerProps {
archiveShortcutKeys?: ShortcutKey[][] | null;
}
function useSidebarWorkspaceEntry(
serverId: string | null,
workspaceId: string | null,
): SidebarWorkspaceEntry | null {
const projectWorkspaceEntry = useCallback(
(workspace: WorkspaceDescriptor): SidebarWorkspaceEntry =>
createSidebarWorkspaceEntry({ serverId: serverId ?? "", workspace }),
[serverId],
);
return useWorkspaceFields(serverId, workspaceId, projectWorkspaceEntry);
}
export function PrBadge({ hint }: { hint: PrHint }) {
const { theme } = useUnistyles();
const [isHovered, setIsHovered] = useState(false);
@@ -1151,9 +1169,6 @@ function WorkspaceRowWithMenu({
const toast = useToast();
const activeWorkspaceSelection = useNavigationActiveWorkspaceSelection();
const archiveWorktree = useCheckoutGitActionsStore((state) => state.archiveWorktree);
const sessionWorkspaces = useSessionStore(
(state) => state.sessions[workspace.serverId]?.workspaces ?? EMPTY_WORKSPACES,
);
const [isArchivingWorkspace, setIsArchivingWorkspace] = useState(false);
const workspaceDirectory = resolveWorkspaceExecutionDirectory({
workspaceDirectory: workspace.workspaceDirectory,
@@ -1181,10 +1196,11 @@ function WorkspaceRowWithMenu({
buildWorkspaceArchiveRedirectRoute({
serverId: workspace.serverId,
archivedWorkspaceId: workspace.workspaceId,
workspaces: sessionWorkspaces.values(),
workspaces:
useSessionStore.getState().sessions[workspace.serverId]?.workspaces.values() ?? [],
}),
);
}, [activeWorkspaceSelection, sessionWorkspaces, workspace.serverId, workspace.workspaceId]);
}, [activeWorkspaceSelection, workspace.serverId, workspace.workspaceId]);
const handleArchiveWorktree = useCallback(() => {
if (isArchiving) {
@@ -1378,9 +1394,6 @@ function NonGitProjectRowWithMenuContent({
const toast = useToast();
const contextMenu = useContextMenu();
const activeWorkspaceSelection = useNavigationActiveWorkspaceSelection();
const sessionWorkspaces = useSessionStore(
(state) => state.sessions[workspace.serverId]?.workspaces ?? EMPTY_WORKSPACES,
);
const [isArchivingWorkspace, setIsArchivingWorkspace] = useState(false);
const redirectAfterArchive = useCallback(() => {
if (
@@ -1394,10 +1407,11 @@ function NonGitProjectRowWithMenuContent({
buildWorkspaceArchiveRedirectRoute({
serverId: workspace.serverId,
archivedWorkspaceId: workspace.workspaceId,
workspaces: sessionWorkspaces.values(),
workspaces:
useSessionStore.getState().sessions[workspace.serverId]?.workspaces.values() ?? [],
}) as any,
);
}, [activeWorkspaceSelection, sessionWorkspaces, workspace.serverId, workspace.workspaceId]);
}, [activeWorkspaceSelection, workspace.serverId, workspace.workspaceId]);
const handleArchiveWorkspace = useCallback(() => {
if (isArchivingWorkspace) {
@@ -1542,13 +1556,19 @@ function FlattenedProjectRow({
onRemoveProject?: () => void;
removeProjectStatus?: "idle" | "pending";
}) {
const workspace = useSidebarWorkspaceEntry(serverId, rowModel.workspace.workspaceId);
if (!workspace) {
return null;
}
if (project.projectKind === "directory") {
return (
<NonGitProjectRowWithMenu
project={project}
displayName={displayName}
iconDataUri={iconDataUri}
workspace={rowModel.workspace}
workspace={workspace}
selected={rowModel.selected}
onPress={onPress}
shortcutNumber={shortcutNumber}
@@ -1565,7 +1585,7 @@ function FlattenedProjectRow({
project={project}
displayName={displayName}
iconDataUri={iconDataUri}
workspace={rowModel.workspace}
workspace={workspace}
selected={rowModel.selected}
chevron={rowModel.chevron}
onPress={onPress}
@@ -1609,9 +1629,15 @@ function WorkspaceRow({
canCopyBranchName: boolean;
isCreating?: boolean;
}) {
const hydratedWorkspace = useSidebarWorkspaceEntry(workspace.serverId, workspace.workspaceId);
if (!hydratedWorkspace) {
return null;
}
return (
<WorkspaceRowWithMenu
workspace={workspace}
workspace={hydratedWorkspace}
selected={selected}
shortcutNumber={shortcutNumber}
showShortcutBadge={showShortcutBadge}