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,13 +6,15 @@ import {
useSessionStore,
type WorkspaceDescriptor,
} from "@/stores/session-store";
import {
useWorkspaceStructure,
type WorkspaceStructureProject,
} from "@/stores/session-store-hooks";
import { getHostRuntimeStore } from "@/runtime/host-runtime";
import { useSidebarOrderStore } from "@/stores/sidebar-order-store";
import { projectDisplayNameFromProjectId } from "@/utils/project-display-name";
const EMPTY_ORDER: string[] = [];
const EMPTY_PROJECTS: SidebarProjectEntry[] = [];
const EMPTY_WORKSPACE_ORDER_SCOPE: Record<string, string[]> = {};
export type SidebarStateBucket = WorkspaceDescriptor["status"];
@@ -36,9 +38,6 @@ export interface SidebarProjectEntry {
projectName: string;
projectKind: WorkspaceDescriptor["projectKind"];
iconWorkingDir: string;
statusBucket: SidebarStateBucket;
activeCount: number;
totalWorkspaces: number;
workspaces: SidebarWorkspaceEntry[];
}
@@ -50,119 +49,68 @@ export interface SidebarWorkspacesListResult {
refreshAll: () => void;
}
const SIDEBAR_BUCKET_PRIORITY: Record<SidebarStateBucket, number> = {
done: 0,
attention: 1,
running: 2,
failed: 3,
needs_input: 4,
};
function aggregateBucket(
current: SidebarStateBucket,
candidate: SidebarStateBucket,
): SidebarStateBucket {
return SIDEBAR_BUCKET_PRIORITY[candidate] > SIDEBAR_BUCKET_PRIORITY[current]
? candidate
: current;
}
function compareWorkspaceBaseline(
left: SidebarWorkspaceEntry,
right: SidebarWorkspaceEntry,
): number {
const nameDelta = left.name.localeCompare(right.name, undefined, {
numeric: true,
sensitivity: "base",
});
if (nameDelta !== 0) {
return nameDelta;
}
return left.workspaceId.localeCompare(right.workspaceId, undefined, {
sensitivity: "base",
});
}
function compareProjectBaseline(left: SidebarProjectEntry, right: SidebarProjectEntry): number {
return left.projectName.localeCompare(right.projectName, undefined, {
numeric: true,
sensitivity: "base",
});
}
export function buildSidebarProjectsFromWorkspaces(input: {
function createStructuralWorkspaceEntry(input: {
serverId: string;
workspaces: Iterable<WorkspaceDescriptor>;
projectOrder: string[];
workspaceOrderByScope: Record<string, string[]>;
project: WorkspaceStructureProject;
workspaceId: string;
}): SidebarWorkspaceEntry {
return {
workspaceKey: `${input.serverId}:${input.workspaceId}`,
serverId: input.serverId,
workspaceId: input.workspaceId,
projectRootPath: input.project.iconWorkingDir,
workspaceDirectory: undefined,
projectKind: input.project.projectKind,
workspaceKind: "checkout",
name: input.workspaceId,
statusBucket: "done",
diffStat: null,
scripts: [],
hasRunningScripts: false,
};
}
export function createSidebarWorkspaceEntry(input: {
serverId: string;
workspace: WorkspaceDescriptor;
}): SidebarWorkspaceEntry {
return {
workspaceKey: `${input.serverId}:${input.workspace.id}`,
serverId: input.serverId,
workspaceId: input.workspace.id,
projectRootPath: input.workspace.projectRootPath,
workspaceDirectory: input.workspace.workspaceDirectory,
projectKind: input.workspace.projectKind,
workspaceKind: input.workspace.workspaceKind,
name: input.workspace.name,
statusBucket: input.workspace.status,
diffStat: input.workspace.diffStat,
scripts: input.workspace.scripts,
hasRunningScripts: input.workspace.scripts.some((script) => script.lifecycle === "running"),
};
}
export function buildSidebarProjectsFromStructure(input: {
serverId: string;
projects: WorkspaceStructureProject[];
}): SidebarProjectEntry[] {
const byProject = new Map<string, SidebarProjectEntry>();
for (const workspace of input.workspaces) {
const project =
byProject.get(workspace.projectId) ??
({
projectKey: workspace.projectId,
projectName:
workspace.projectDisplayName || projectDisplayNameFromProjectId(workspace.projectId),
projectKind: workspace.projectKind,
iconWorkingDir: workspace.projectRootPath,
statusBucket: "done",
activeCount: 0,
totalWorkspaces: 0,
workspaces: [],
} satisfies SidebarProjectEntry);
const row: SidebarWorkspaceEntry = {
workspaceKey: `${input.serverId}:${workspace.id}`,
serverId: input.serverId,
workspaceId: workspace.id,
projectRootPath: workspace.projectRootPath,
workspaceDirectory: workspace.workspaceDirectory,
projectKind: workspace.projectKind,
workspaceKind: workspace.workspaceKind,
name: workspace.name,
statusBucket: workspace.status,
diffStat: workspace.diffStat,
scripts: workspace.scripts,
hasRunningScripts: workspace.scripts.some((script) => script.lifecycle === "running"),
};
project.workspaces.push(row);
project.totalWorkspaces += 1;
if (workspace.status !== "done") {
project.activeCount += 1;
}
project.statusBucket = aggregateBucket(project.statusBucket, workspace.status);
byProject.set(workspace.projectId, project);
if (input.projects.length === 0) {
return EMPTY_PROJECTS;
}
const baselineProjects = Array.from(byProject.values()).map((project) => {
const baselineWorkspaces = [...project.workspaces];
baselineWorkspaces.sort(compareWorkspaceBaseline);
const workspaceOrderScopeKey = getWorkspaceOrderScopeKey(input.serverId, project.projectKey);
const orderedWorkspaces = applyStoredOrdering({
items: baselineWorkspaces,
storedOrder: input.workspaceOrderByScope[workspaceOrderScopeKey] ?? EMPTY_ORDER,
getKey: (workspace) => workspace.workspaceKey,
});
return {
...project,
workspaces: orderedWorkspaces,
};
});
baselineProjects.sort(compareProjectBaseline);
return applyStoredOrdering({
items: baselineProjects,
storedOrder: input.projectOrder,
getKey: (project) => project.projectKey,
});
return input.projects.map((project) => ({
projectKey: project.projectKey,
projectName: project.projectName,
projectKind: project.projectKind,
iconWorkingDir: project.iconWorkingDir,
workspaces: project.workspaceKeys.map((workspaceId) =>
createStructuralWorkspaceEntry({
serverId: input.serverId,
project,
workspaceId,
}),
),
}));
}
export function applyStoredOrdering<T>(input: {
@@ -229,10 +177,6 @@ export function appendMissingOrderKeys(input: {
return [...input.currentOrder, ...missingKeys];
}
function getWorkspaceOrderScopeKey(serverId: string, projectKey: string): string {
return `${serverId.trim()}::${projectKey.trim()}`;
}
function toWorkspaceDescriptor(payload: WorkspaceDescriptorPayload): WorkspaceDescriptor {
return normalizeWorkspaceDescriptor(payload);
}
@@ -250,17 +194,12 @@ export function useSidebarWorkspacesList(options?: {
const persistedProjectOrder = useSidebarOrderStore((state) =>
serverId ? (state.projectOrderByServerId[serverId] ?? EMPTY_ORDER) : EMPTY_ORDER,
);
const persistedWorkspaceOrderByScope = useSidebarOrderStore((state) =>
serverId ? state.workspaceOrderByServerAndProject : EMPTY_WORKSPACE_ORDER_SCOPE,
);
const isActive = Boolean(serverId);
const sessionWorkspaces = useSessionStore((state) =>
isActive && serverId ? (state.sessions[serverId]?.workspaces ?? null) : null,
);
const hasHydratedWorkspaces = useSessionStore((state) =>
isActive && serverId ? (state.sessions[serverId]?.hasHydratedWorkspaces ?? false) : false,
);
const workspaceStructure = useWorkspaceStructure(isActive ? serverId : null);
const connectionStatus = useSyncExternalStore(
(onStoreChange) =>
@@ -282,22 +221,20 @@ export function useSidebarWorkspacesList(options?: {
);
const projects = useMemo(() => {
if (!sessionWorkspaces || sessionWorkspaces.size === 0 || !serverId) {
if (!serverId || workspaceStructure.projects.length === 0) {
return EMPTY_PROJECTS;
}
return buildSidebarProjectsFromWorkspaces({
return buildSidebarProjectsFromStructure({
serverId,
workspaces: sessionWorkspaces.values(),
projectOrder: persistedProjectOrder,
workspaceOrderByScope: persistedWorkspaceOrderByScope,
projects: workspaceStructure.projects,
});
}, [persistedProjectOrder, persistedWorkspaceOrderByScope, serverId, sessionWorkspaces]);
}, [serverId, workspaceStructure]);
useEffect(() => {
if (!serverId) {
return;
}
}, [connectionStatus, hasHydratedWorkspaces, projects, serverId, sessionWorkspaces]);
}, [connectionStatus, hasHydratedWorkspaces, projects, serverId]);
useEffect(() => {
if (!serverId || projects.length === 0) {
@@ -313,9 +250,9 @@ export function useSidebarWorkspacesList(options?: {
}
for (const project of projects) {
const workspaceOrderScopeKey = getWorkspaceOrderScopeKey(serverId, project.projectKey);
const persistedWorkspaceOrder =
persistedWorkspaceOrderByScope[workspaceOrderScopeKey] ?? EMPTY_ORDER;
const persistedWorkspaceOrder = useSidebarOrderStore
.getState()
.getWorkspaceOrder(serverId, project.projectKey);
const nextWorkspaceOrder = appendMissingOrderKeys({
currentOrder: persistedWorkspaceOrder,
visibleKeys: project.workspaces.map((workspace) => workspace.workspaceKey),
@@ -326,7 +263,7 @@ export function useSidebarWorkspacesList(options?: {
.setWorkspaceOrder(serverId, project.projectKey, nextWorkspaceOrder);
}
}
}, [persistedProjectOrder, persistedWorkspaceOrderByScope, projects, serverId]);
}, [persistedProjectOrder, projects, serverId]);
const refreshAll = useCallback(() => {
if (!isActive || !serverId || connectionStatus !== "online") {