mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
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).
153 lines
3.9 KiB
TypeScript
153 lines
3.9 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import {
|
|
buildSidebarProjectRowModel,
|
|
isSidebarProjectFlattened,
|
|
} from "./sidebar-project-row-model";
|
|
import type {
|
|
SidebarProjectEntry,
|
|
SidebarWorkspaceEntry,
|
|
} from "@/hooks/use-sidebar-workspaces-list";
|
|
|
|
function workspace(overrides: Partial<SidebarWorkspaceEntry> = {}): SidebarWorkspaceEntry {
|
|
return {
|
|
workspaceKey: "srv:ws-root",
|
|
serverId: "srv",
|
|
workspaceId: "ws-root",
|
|
workspaceDirectory: "/repo",
|
|
projectKind: "git",
|
|
workspaceKind: "checkout",
|
|
name: "paseo",
|
|
statusBucket: "done",
|
|
diffStat: null,
|
|
scripts: [],
|
|
hasRunningScripts: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function project(overrides: Partial<SidebarProjectEntry> = {}): SidebarProjectEntry {
|
|
return {
|
|
projectKey: "project-1",
|
|
projectName: "paseo",
|
|
projectKind: "git",
|
|
iconWorkingDir: "/repo",
|
|
workspaces: [workspace()],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("buildSidebarProjectRowModel", () => {
|
|
it("flattens non-git projects with one workspace into a direct workspace row model", () => {
|
|
const flattenedWorkspace = workspace({
|
|
workspaceId: "ws-non-git",
|
|
workspaceKind: "checkout",
|
|
statusBucket: "running",
|
|
});
|
|
|
|
const result = buildSidebarProjectRowModel({
|
|
project: project({
|
|
projectKind: "directory",
|
|
workspaces: [flattenedWorkspace],
|
|
}),
|
|
collapsed: false,
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
kind: "workspace_link",
|
|
workspace: flattenedWorkspace,
|
|
selected: false,
|
|
chevron: null,
|
|
trailingAction: "none",
|
|
});
|
|
});
|
|
|
|
it("marks flattened non-git project rows as selected when their workspace is active", () => {
|
|
const flattenedWorkspace = workspace({
|
|
serverId: "srv-2",
|
|
workspaceId: "ws-non-git",
|
|
});
|
|
|
|
const result = buildSidebarProjectRowModel({
|
|
project: project({
|
|
projectKind: "directory",
|
|
workspaces: [flattenedWorkspace],
|
|
}),
|
|
collapsed: false,
|
|
serverId: "srv-2",
|
|
activeWorkspaceSelection: {
|
|
serverId: "srv-2",
|
|
workspaceId: "ws-non-git",
|
|
},
|
|
});
|
|
|
|
expect(result).toMatchObject({
|
|
kind: "workspace_link",
|
|
selected: true,
|
|
});
|
|
});
|
|
|
|
it("keeps single-workspace git projects as sections with the new worktree action", () => {
|
|
const onlyWorkspace = workspace({
|
|
workspaceId: "ws-main",
|
|
workspaceKind: "checkout",
|
|
});
|
|
|
|
const result = buildSidebarProjectRowModel({
|
|
project: project({
|
|
projectKind: "git",
|
|
workspaces: [onlyWorkspace],
|
|
}),
|
|
collapsed: true,
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
kind: "project_section",
|
|
chevron: "expand",
|
|
trailingAction: "new_worktree",
|
|
});
|
|
});
|
|
|
|
it("keeps multi-workspace git projects as expandable sections with a new worktree action", () => {
|
|
const result = buildSidebarProjectRowModel({
|
|
project: project({
|
|
projectKind: "git",
|
|
workspaces: [
|
|
workspace({ workspaceId: "ws-main", workspaceKind: "checkout" }),
|
|
workspace({ workspaceId: "ws-feature", workspaceKind: "worktree" }),
|
|
],
|
|
}),
|
|
collapsed: true,
|
|
});
|
|
|
|
expect(result).toEqual({
|
|
kind: "project_section",
|
|
chevron: "expand",
|
|
trailingAction: "new_worktree",
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("isSidebarProjectFlattened", () => {
|
|
it("returns true only for single-workspace non-git projects", () => {
|
|
expect(
|
|
isSidebarProjectFlattened(project({ projectKind: "git", workspaces: [workspace()] })),
|
|
).toBe(false);
|
|
expect(
|
|
isSidebarProjectFlattened(project({ projectKind: "directory", workspaces: [workspace()] })),
|
|
).toBe(true);
|
|
});
|
|
|
|
it("returns false for multi-workspace projects", () => {
|
|
expect(
|
|
isSidebarProjectFlattened(
|
|
project({
|
|
workspaces: [
|
|
workspace({ workspaceId: "ws-main" }),
|
|
workspace({ workspaceId: "ws-feat" }),
|
|
],
|
|
}),
|
|
),
|
|
).toBe(false);
|
|
});
|
|
});
|