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
@@ -0,0 +1,364 @@
/**
* @vitest-environment jsdom
*/
import { act, renderHook } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import type { DaemonClient } from "@server/client/daemon-client";
import {
useHasWorkspaces,
useRecommendedProjectPaths,
useResolveWorkspaceIdByCwd,
useWorkspace,
useWorkspaceExecutionAuthority,
useWorkspaceFields,
useWorkspaceKeys,
useWorkspaceStatusesForBadges,
useWorkspaceStructure,
} from "./session-store-hooks";
import { useSidebarOrderStore } from "./sidebar-order-store";
import { useSessionStore, type WorkspaceDescriptor } from "./session-store";
const SERVER_ID = "test-server";
function createWorkspace(
input: Partial<WorkspaceDescriptor> & Pick<WorkspaceDescriptor, "id">,
): WorkspaceDescriptor {
return {
id: input.id,
projectId: input.projectId ?? "project-1",
projectDisplayName: input.projectDisplayName ?? "Project 1",
projectRootPath: input.projectRootPath ?? "/repo",
workspaceDirectory: input.workspaceDirectory ?? "/repo",
projectKind: input.projectKind ?? "git",
workspaceKind: input.workspaceKind ?? "local_checkout",
name: input.name ?? "main",
status: input.status ?? "done",
diffStat: input.diffStat ?? null,
scripts: input.scripts ?? [],
};
}
function initializeWorkspaces(workspaces: WorkspaceDescriptor[]): void {
act(() => {
useSessionStore.getState().initializeSession(SERVER_ID, null as unknown as DaemonClient);
useSessionStore
.getState()
.setWorkspaces(SERVER_ID, new Map(workspaces.map((workspace) => [workspace.id, workspace])));
});
}
afterEach(() => {
act(() => {
useSessionStore.getState().clearSession(SERVER_ID);
useSidebarOrderStore.setState({
projectOrderByServerId: {},
workspaceOrderByServerAndProject: {},
});
});
});
describe("useWorkspace", () => {
it("resolves a descriptor when the route id matches workspace identity but not the map key", () => {
const workspace = createWorkspace({ id: "workspace-a" });
act(() => {
useSessionStore.getState().initializeSession(SERVER_ID, null as unknown as DaemonClient);
useSessionStore.getState().setWorkspaces(SERVER_ID, new Map([["map-key-a", workspace]]));
});
const { result } = renderHook(() => useWorkspace(SERVER_ID, workspace.id));
expect(result.current).toBe(workspace);
});
it("keeps the descriptor reference for unrelated workspace updates", () => {
const workspaceA = createWorkspace({ id: "workspace-a", name: "A" });
const workspaceB = createWorkspace({ id: "workspace-b", name: "B" });
initializeWorkspaces([workspaceA, workspaceB]);
const { result } = renderHook(() => useWorkspace(SERVER_ID, workspaceA.id));
const before = result.current;
act(() => {
useSessionStore.getState().mergeWorkspaces(SERVER_ID, [{ ...workspaceB, status: "running" }]);
});
expect(result.current).toBe(before);
act(() => {
useSessionStore
.getState()
.mergeWorkspaces(SERVER_ID, [{ ...workspaceA, status: "attention" }]);
});
expect(result.current).not.toBe(before);
});
it("keeps the descriptor reference when the observed workspace is rewritten with content-equal data", () => {
const workspace = createWorkspace({ id: "workspace-a", scripts: [] });
initializeWorkspaces([workspace]);
const { result } = renderHook(() => useWorkspace(SERVER_ID, workspace.id));
const before = result.current;
act(() => {
useSessionStore
.getState()
.setWorkspaces(SERVER_ID, new Map([[workspace.id, { ...workspace, scripts: [] }]]));
});
expect(result.current).toBe(before);
});
});
describe("useWorkspaceExecutionAuthority", () => {
it("preserves the old missing-workspace message with the requested id", () => {
initializeWorkspaces([]);
const { result } = renderHook(() => useWorkspaceExecutionAuthority(SERVER_ID, "missing-id"));
expect(result.current).toMatchObject({
ok: false,
message: "Workspace not found: missing-id",
});
});
it("keeps deep-equal authority references under unrelated workspace updates", () => {
const workspaceA = createWorkspace({ id: "workspace-a", name: "A" });
const workspaceB = createWorkspace({ id: "workspace-b", name: "B" });
initializeWorkspaces([workspaceA, workspaceB]);
const { result } = renderHook(() => useWorkspaceExecutionAuthority(SERVER_ID, workspaceA.id));
const before = result.current;
act(() => {
useSessionStore.getState().mergeWorkspaces(SERVER_ID, [{ ...workspaceB, status: "running" }]);
});
expect(result.current).toBe(before);
});
});
describe("useWorkspaceFields", () => {
it("keeps deep-equal projection references until projected fields change", () => {
const workspace = createWorkspace({ id: "workspace-a", name: "A", status: "done" });
initializeWorkspaces([workspace]);
const { result } = renderHook(() =>
useWorkspaceFields(SERVER_ID, workspace.id, (current) => ({
identity: {
id: current.id,
name: current.name,
},
})),
);
const before = result.current;
act(() => {
useSessionStore.getState().mergeWorkspaces(SERVER_ID, [{ ...workspace, status: "running" }]);
});
expect(result.current).toBe(before);
act(() => {
useSessionStore.getState().mergeWorkspaces(SERVER_ID, [
{
...workspace,
name: "A renamed",
status: "running",
},
]);
});
expect(result.current).not.toBe(before);
});
});
describe("useWorkspaceStructure", () => {
it("changes for membership updates but not status-only updates", () => {
const workspaceA = createWorkspace({ id: "workspace-a", name: "A" });
const workspaceB = createWorkspace({ id: "workspace-b", name: "B" });
initializeWorkspaces([workspaceA]);
const { result } = renderHook(() => useWorkspaceStructure(SERVER_ID));
const before = result.current;
act(() => {
useSessionStore.getState().mergeWorkspaces(SERVER_ID, [workspaceB]);
});
const afterAdd = result.current;
expect(afterAdd).not.toBe(before);
expect(afterAdd.projects[0]?.workspaceKeys).toEqual(["workspace-a", "workspace-b"]);
act(() => {
useSessionStore.getState().mergeWorkspaces(SERVER_ID, [{ ...workspaceA, status: "running" }]);
});
expect(result.current).toBe(afterAdd);
});
it("changes when a structure-relevant project identity field changes", () => {
const workspace = createWorkspace({
id: "workspace-a",
projectDisplayName: "Project 1",
});
initializeWorkspaces([workspace]);
const { result } = renderHook(() => useWorkspaceStructure(SERVER_ID));
const before = result.current;
act(() => {
useSessionStore
.getState()
.mergeWorkspaces(SERVER_ID, [{ ...workspace, projectDisplayName: "Project Renamed" }]);
});
expect(result.current).not.toBe(before);
});
it("changes when persisted sidebar project order changes", () => {
const workspaceA = createWorkspace({
id: "workspace-a",
projectId: "project-a",
projectDisplayName: "Project A",
});
const workspaceB = createWorkspace({
id: "workspace-b",
projectId: "project-b",
projectDisplayName: "Project B",
});
initializeWorkspaces([workspaceA, workspaceB]);
const { result } = renderHook(() => useWorkspaceStructure(SERVER_ID));
const before = result.current;
act(() => {
useSidebarOrderStore.getState().setProjectOrder(SERVER_ID, ["project-b", "project-a"]);
});
expect(result.current).not.toBe(before);
});
});
describe("useWorkspaceKeys", () => {
it("changes for reorder updates but not content-only updates", () => {
const workspaceA = createWorkspace({ id: "workspace-a", name: "A" });
const workspaceB = createWorkspace({ id: "workspace-b", name: "B" });
initializeWorkspaces([workspaceA, workspaceB]);
const { result } = renderHook(() => useWorkspaceKeys(SERVER_ID));
const before = result.current;
expect(before).toEqual(["workspace-a", "workspace-b"]);
act(() => {
useSessionStore.getState().setWorkspaces(
SERVER_ID,
new Map([
[workspaceB.id, workspaceB],
[workspaceA.id, workspaceA],
]),
);
});
const afterReorder = result.current;
expect(afterReorder).not.toBe(before);
expect(afterReorder).toEqual(["workspace-b", "workspace-a"]);
act(() => {
useSessionStore.getState().mergeWorkspaces(SERVER_ID, [{ ...workspaceA, status: "running" }]);
});
expect(result.current).toBe(afterReorder);
});
});
describe("useRecommendedProjectPaths", () => {
it("updates when an existing workspace project root changes", () => {
const workspace = createWorkspace({ id: "workspace-a", projectRootPath: "/repo/a" });
initializeWorkspaces([workspace]);
const { result } = renderHook(() => useRecommendedProjectPaths(SERVER_ID));
act(() => {
useSessionStore
.getState()
.mergeWorkspaces(SERVER_ID, [{ ...workspace, projectRootPath: "/repo/b" }]);
});
expect(result.current).toEqual(["/repo/b"]);
});
it("keeps the path list reference under unrelated workspace updates", () => {
const workspace = createWorkspace({ id: "workspace-a", projectRootPath: "/repo/a" });
initializeWorkspaces([workspace]);
const { result } = renderHook(() => useRecommendedProjectPaths(SERVER_ID));
const before = result.current;
act(() => {
useSessionStore.getState().mergeWorkspaces(SERVER_ID, [{ ...workspace, status: "running" }]);
});
expect(result.current).toBe(before);
});
});
describe("useHasWorkspaces", () => {
it("stays stable when workspace membership changes without flipping the boolean", () => {
const workspaceA = createWorkspace({ id: "workspace-a" });
const workspaceB = createWorkspace({ id: "workspace-b" });
initializeWorkspaces([workspaceA]);
const { result } = renderHook(() => useHasWorkspaces(SERVER_ID));
const before = result.current;
act(() => {
useSessionStore.getState().mergeWorkspaces(SERVER_ID, [workspaceB]);
});
expect(result.current).toBe(before);
});
});
describe("useResolveWorkspaceIdByCwd", () => {
it("resolves by cwd and stays stable under unrelated updates", () => {
const workspaceA = createWorkspace({
id: "workspace-a",
workspaceDirectory: "/repo/a",
});
const workspaceB = createWorkspace({
id: "workspace-b",
workspaceDirectory: "/repo/b",
});
initializeWorkspaces([workspaceA, workspaceB]);
const { result } = renderHook(() => useResolveWorkspaceIdByCwd(SERVER_ID, "/repo/a"));
const before = result.current;
expect(before).toBe("workspace-a");
act(() => {
useSessionStore.getState().mergeWorkspaces(SERVER_ID, [{ ...workspaceB, status: "running" }]);
});
expect(result.current).toBe(before);
});
});
describe("useWorkspaceStatusesForBadges", () => {
it("tracks status changes without changing for no-ops or unrelated descriptor updates", () => {
const workspaceA = createWorkspace({ id: "workspace-a", status: "done" });
const workspaceB = createWorkspace({ id: "workspace-b", status: "attention" });
initializeWorkspaces([workspaceA, workspaceB]);
const { result } = renderHook(() => useWorkspaceStatusesForBadges());
const before = result.current;
expect(before).toEqual(["done", "attention"]);
act(() => {
useSessionStore
.getState()
.mergeWorkspaces(SERVER_ID, [{ ...workspaceA, scripts: [...workspaceA.scripts] }]);
});
expect(result.current).toBe(before);
act(() => {
useSessionStore.getState().mergeWorkspaces(SERVER_ID, [{ ...workspaceB, name: "Renamed" }]);
});
expect(result.current).toBe(before);
act(() => {
useSessionStore.getState().mergeWorkspaces(SERVER_ID, [{ ...workspaceA, status: "failed" }]);
});
expect(result.current).not.toBe(before);
expect(result.current).toEqual(["failed", "attention"]);
});
});
@@ -0,0 +1,384 @@
import equal from "fast-deep-equal";
import { useMemo } from "react";
import { useStoreWithEqualityFn } from "zustand/traditional";
import { useSidebarOrderStore } from "@/stores/sidebar-order-store";
import { projectDisplayNameFromProjectId } from "@/utils/project-display-name";
import type { DesktopBadgeWorkspaceStatus } from "@/utils/desktop-badge-state";
import {
getWorkspaceExecutionAuthority,
resolveWorkspaceIdByExecutionDirectory,
resolveWorkspaceMapKeyByIdentity,
type WorkspaceExecutionAuthorityResult,
} from "@/utils/workspace-execution";
import { useSessionStore, type WorkspaceDescriptor } from "./session-store";
// These are the ONLY supported ways to read workspaces from the session store.
// Do not write raw `useSessionStore` selectors that return the workspaces Map, a session object,
// or the sessions dict — it breaks re-render isolation.
export type { DesktopBadgeWorkspaceStatus } from "@/utils/desktop-badge-state";
export interface WorkspaceStructureProject {
projectKey: string;
projectName: string;
projectKind: WorkspaceDescriptor["projectKind"];
iconWorkingDir: string;
workspaceKeys: string[];
}
export interface WorkspaceStructure {
projects: WorkspaceStructureProject[];
}
type SessionStoreSnapshot = ReturnType<typeof useSessionStore.getState>;
const EMPTY_WORKSPACE_KEYS: string[] = [];
const EMPTY_WORKSPACE_STRUCTURE: WorkspaceStructure = { projects: [] };
function getWorkspaceOrderScopeKey(serverId: string, projectKey: string): string {
return `${serverId.trim()}::${projectKey.trim()}`;
}
function compareWorkspaceStructureItems(
left: { workspaceId: string; workspaceName: string },
right: { workspaceId: string; workspaceName: string },
): number {
const nameDelta = left.workspaceName.localeCompare(right.workspaceName, undefined, {
numeric: true,
sensitivity: "base",
});
if (nameDelta !== 0) {
return nameDelta;
}
return left.workspaceId.localeCompare(right.workspaceId, undefined, {
sensitivity: "base",
});
}
function compareWorkspaceStructureProjects(
left: WorkspaceStructureProject,
right: WorkspaceStructureProject,
): number {
return left.projectName.localeCompare(right.projectName, undefined, {
numeric: true,
sensitivity: "base",
});
}
function applyStoredOrdering<T>(input: {
items: T[];
storedOrder: readonly string[];
getKey: (item: T) => string;
}): T[] {
if (input.items.length <= 1 || input.storedOrder.length === 0) {
return input.items;
}
const itemByKey = new Map<string, T>();
for (const item of input.items) {
itemByKey.set(input.getKey(item), item);
}
const prunedOrder: string[] = [];
const seen = new Set<string>();
for (const key of input.storedOrder) {
if (!itemByKey.has(key) || seen.has(key)) {
continue;
}
seen.add(key);
prunedOrder.push(key);
}
if (prunedOrder.length === 0) {
return input.items;
}
const orderedSet = new Set(prunedOrder);
const ordered: T[] = [];
let orderedIndex = 0;
for (const item of input.items) {
const key = input.getKey(item);
if (!orderedSet.has(key)) {
ordered.push(item);
continue;
}
const targetKey = prunedOrder[orderedIndex] ?? key;
orderedIndex += 1;
ordered.push(itemByKey.get(targetKey) ?? item);
}
return ordered;
}
function selectWorkspace(
state: SessionStoreSnapshot,
serverId: string | null,
workspaceId: string | null,
): WorkspaceDescriptor | null {
if (!serverId || !workspaceId) {
return null;
}
const workspaces = state.sessions[serverId]?.workspaces;
const workspaceKey = resolveWorkspaceMapKeyByIdentity({
workspaces,
workspaceId,
});
return workspaceKey ? (workspaces?.get(workspaceKey) ?? null) : null;
}
export function useWorkspace(
serverId: string | null,
workspaceId: string | null,
): WorkspaceDescriptor | null {
return useStoreWithEqualityFn(
useSessionStore,
(state) => selectWorkspace(state, serverId, workspaceId),
Object.is,
);
}
export function useWorkspaceFields<T>(
serverId: string | null,
workspaceId: string | null,
project: (w: WorkspaceDescriptor) => T,
): T | null {
return useStoreWithEqualityFn(
useSessionStore,
(state) => {
const workspace = selectWorkspace(state, serverId, workspaceId);
return workspace ? project(workspace) : null;
},
equal,
);
}
export function useWorkspaceExecutionAuthority(
serverId: string | null,
workspaceId: string | null,
): WorkspaceExecutionAuthorityResult | null {
return useStoreWithEqualityFn(
useSessionStore,
(state) => {
if (serverId === null || workspaceId === null) {
return null;
}
return getWorkspaceExecutionAuthority({
workspaces: state.sessions[serverId]?.workspaces,
workspaceId,
});
},
equal,
);
}
function selectWorkspaceStructureProjects(
state: SessionStoreSnapshot,
serverId: string | null,
): WorkspaceStructureProject[] {
if (!serverId) {
return EMPTY_WORKSPACE_STRUCTURE.projects;
}
const workspaces = state.sessions[serverId]?.workspaces;
if (!workspaces || workspaces.size === 0) {
return EMPTY_WORKSPACE_STRUCTURE.projects;
}
const byProject = new Map<
string,
WorkspaceStructureProject & {
workspaces: Array<{ workspaceId: string; workspaceName: string; workspaceKey: string }>;
}
>();
for (const workspace of workspaces.values()) {
const project =
byProject.get(workspace.projectId) ??
({
projectKey: workspace.projectId,
projectName:
workspace.projectDisplayName || projectDisplayNameFromProjectId(workspace.projectId),
projectKind: workspace.projectKind,
iconWorkingDir: workspace.projectRootPath,
workspaceKeys: [],
workspaces: [],
} satisfies WorkspaceStructureProject & {
workspaces: Array<{ workspaceId: string; workspaceName: string; workspaceKey: string }>;
});
project.workspaces.push({
workspaceId: workspace.id,
workspaceName: workspace.name,
workspaceKey: `${serverId}:${workspace.id}`,
});
byProject.set(workspace.projectId, project);
}
const projects = Array.from(byProject.values()).map(
({ workspaces: projectWorkspaces, ...project }) => {
const sortedWorkspaces = [...projectWorkspaces].sort(compareWorkspaceStructureItems);
return {
...project,
workspaceKeys: sortedWorkspaces.map((workspace) => workspace.workspaceId),
};
},
);
projects.sort(compareWorkspaceStructureProjects);
return projects;
}
function selectWorkspaceOrderByScopeForServer(
workspaceOrderByScope: Record<string, string[]>,
serverId: string | null,
): Record<string, string[]> {
if (!serverId) {
return {};
}
const prefix = `${serverId.trim()}::`;
const relevantOrderByScope: Record<string, string[]> = {};
for (const [scopeKey, order] of Object.entries(workspaceOrderByScope)) {
if (scopeKey.startsWith(prefix)) {
relevantOrderByScope[scopeKey] = order;
}
}
return relevantOrderByScope;
}
export function useWorkspaceStructure(serverId: string | null): WorkspaceStructure {
const projects = useStoreWithEqualityFn(
useSessionStore,
(state) => selectWorkspaceStructureProjects(state, serverId),
equal,
);
const projectOrder = useStoreWithEqualityFn(
useSidebarOrderStore,
(state) =>
serverId
? (state.projectOrderByServerId[serverId] ?? EMPTY_WORKSPACE_KEYS)
: EMPTY_WORKSPACE_KEYS,
equal,
);
const workspaceOrderByScope = useStoreWithEqualityFn(
useSidebarOrderStore,
(state) =>
selectWorkspaceOrderByScopeForServer(state.workspaceOrderByServerAndProject, serverId),
equal,
);
return useMemo(() => {
if (!serverId || projects.length === 0) {
return EMPTY_WORKSPACE_STRUCTURE;
}
const orderedProjects = applyStoredOrdering({
items: projects.map((project) => {
const workspaceOrder =
workspaceOrderByScope[getWorkspaceOrderScopeKey(serverId, project.projectKey)] ??
EMPTY_WORKSPACE_KEYS;
const workspaceItems = project.workspaceKeys.map((workspaceId) => ({
workspaceId,
workspaceKey: `${serverId}:${workspaceId}`,
}));
return {
...project,
workspaceKeys: applyStoredOrdering({
items: workspaceItems,
storedOrder: workspaceOrder,
getKey: (workspace) => workspace.workspaceKey,
}).map((workspace) => workspace.workspaceId),
};
}),
storedOrder: projectOrder,
getKey: (project) => project.projectKey,
});
return { projects: orderedProjects };
}, [projectOrder, projects, serverId, workspaceOrderByScope]);
}
export function useWorkspaceKeys(serverId: string | null): string[] {
return useStoreWithEqualityFn(
useSessionStore,
(state) => {
if (!serverId) {
return EMPTY_WORKSPACE_KEYS;
}
const workspaces = state.sessions[serverId]?.workspaces;
return workspaces ? Array.from(workspaces.keys()) : EMPTY_WORKSPACE_KEYS;
},
equal,
);
}
export function useRecommendedProjectPaths(serverId: string | null): string[] {
return useStoreWithEqualityFn(
useSessionStore,
(state) => {
if (!serverId) {
return EMPTY_WORKSPACE_KEYS;
}
const workspaces = state.sessions[serverId]?.workspaces;
if (!workspaces) {
return EMPTY_WORKSPACE_KEYS;
}
return Array.from(workspaces.values())
.map((workspace) => workspace.projectRootPath)
.filter((path) => path.length > 0);
},
equal,
);
}
export function useHasWorkspaces(serverId: string | null): boolean {
return useStoreWithEqualityFn(
useSessionStore,
(state) => {
if (!serverId) {
return false;
}
return (state.sessions[serverId]?.workspaces?.size ?? 0) > 0;
},
Object.is,
);
}
export function useResolveWorkspaceIdByCwd(
serverId: string | null,
cwd: string | null | undefined,
): string | null {
return useStoreWithEqualityFn(
useSessionStore,
(state) => {
if (!serverId || !cwd) {
return null;
}
const workspaces = state.sessions[serverId]?.workspaces;
return resolveWorkspaceIdByExecutionDirectory({
workspaces: workspaces?.values(),
workspaceDirectory: cwd,
});
},
Object.is,
);
}
export function useWorkspaceStatusesForBadges(): DesktopBadgeWorkspaceStatus[] {
return useStoreWithEqualityFn(
useSessionStore,
(state) => {
const statuses: DesktopBadgeWorkspaceStatus[] = [];
for (const session of Object.values(state.sessions)) {
for (const workspace of session.workspaces.values()) {
statuses.push(workspace.status);
}
}
return statuses;
},
equal,
);
}
@@ -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", () => {
+49 -7
View File
@@ -1,3 +1,4 @@
import equal from "fast-deep-equal";
import { create } from "zustand";
import { subscribeWithSelector } from "zustand/middleware";
import type { DaemonClient } from "@server/client/daemon-client";
@@ -159,6 +160,41 @@ export function mergeWorkspaceSnapshotWithExisting(input: {
};
}
function preserveWorkspaceDescriptorIdentity(
incoming: WorkspaceDescriptor,
existing?: WorkspaceDescriptor | null,
): WorkspaceDescriptor {
if (existing && equal(existing, incoming)) {
return existing;
}
return incoming;
}
function preserveWorkspaceMapIdentity(
existing: Map<string, WorkspaceDescriptor>,
incoming: Map<string, WorkspaceDescriptor>,
): Map<string, WorkspaceDescriptor> {
if (existing === incoming) {
return existing;
}
const next = new Map<string, WorkspaceDescriptor>();
let changed = existing.size !== incoming.size;
const existingEntries = existing.entries();
for (const [key, workspace] of incoming) {
const existingWorkspace = existing.get(key);
const nextWorkspace = preserveWorkspaceDescriptorIdentity(workspace, existingWorkspace);
next.set(key, nextWorkspace);
const existingEntry = existingEntries.next().value;
if (!existingEntry || existingEntry[0] !== key || existingEntry[1] !== nextWorkspace) {
changed = true;
}
}
return changed ? next : existing;
}
export type ExplorerEntryKind = "file" | "directory";
export type ExplorerFileKind = "text" | "image" | "binary";
export type ExplorerEncoding = "utf-8" | "base64" | "none";
@@ -938,14 +974,18 @@ export const useSessionStore = create<SessionStore>()(
}
const nextWorkspaces =
typeof workspaces === "function" ? workspaces(session.workspaces) : workspaces;
if (session.workspaces === nextWorkspaces) {
const preservedWorkspaces = preserveWorkspaceMapIdentity(
session.workspaces,
nextWorkspaces,
);
if (session.workspaces === preservedWorkspaces) {
return prev;
}
return {
...prev,
sessions: {
...prev.sessions,
[serverId]: { ...session, workspaces: nextWorkspaces },
[serverId]: { ...session, workspaces: preservedWorkspaces },
},
};
});
@@ -962,13 +1002,15 @@ export const useSessionStore = create<SessionStore>()(
let changed = false;
for (const workspace of nextEntries) {
const existing = next.get(workspace.id);
if (existing === workspace) {
const mergedWorkspace = mergeWorkspaceSnapshotWithExisting({
incoming: workspace,
existing,
});
const nextWorkspace = preserveWorkspaceDescriptorIdentity(mergedWorkspace, existing);
if (existing === nextWorkspace) {
continue;
}
next.set(
workspace.id,
mergeWorkspaceSnapshotWithExisting({ incoming: workspace, existing }),
);
next.set(workspace.id, nextWorkspace);
changed = true;
}
if (!changed) {