From a3919faed4a125f333fa4db6b23e0cdbd9dfcccd Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Fri, 16 Oct 2026 15:18:08 +0200 Subject: [PATCH] perf(app): preserve unchanged sidebar workspace projections Reopening a retained sidebar rebuilt its entry collection and invalidated every project row despite unchanged workspace data. Preserve collection identity as well as row identity, while keeping membership and ordering changes observable. --- .../sidebar-workspaces-view-model.test.ts | 51 +++++++++++++++++++ .../hooks/sidebar-workspaces-view-model.ts | 21 +++++--- 2 files changed, 66 insertions(+), 6 deletions(-) diff --git a/packages/app/src/hooks/sidebar-workspaces-view-model.test.ts b/packages/app/src/hooks/sidebar-workspaces-view-model.test.ts index 637d3a035b..1bc4cf2135 100644 --- a/packages/app/src/hooks/sidebar-workspaces-view-model.test.ts +++ b/packages/app/src/hooks/sidebar-workspaces-view-model.test.ts @@ -403,6 +403,57 @@ describe("shared sidebar workspace model", () => { expect(model.projectNamesByViewKey).toEqual(new Map([["getpaseo/paseo", "getpaseo/paseo"]])); }); + it("preserves the collection when a retained sidebar reconnects to unchanged entries", () => { + const model = buildSidebarWorkspacePlacementModel({ + projects: [project({ projectKey: "project", workspaceKeys: ["srv:one", "srv:two"] })], + }); + const one = workspace({ + id: "one", + name: "one", + projectId: "project", + projectDisplayName: "project", + }); + const two = workspace({ + id: "two", + name: "two", + projectId: "project", + projectDisplayName: "project", + }); + const sessions = [ + { + serverId: "srv", + workspaceAgentActivity: new Map(), + workspaces: new Map([ + ["one", one], + ["two", two], + ]), + }, + ]; + const previousEntries = buildSidebarWorkspaceEntries({ + placements: model.workspaces, + sessions, + }); + expect( + buildSidebarWorkspaceEntries({ + placements: model.workspaces, + sessions, + previousEntries, + }), + ).toBe(previousEntries); + const reordered = buildSidebarWorkspaceEntries({ + placements: model.workspaces.toReversed(), + sessions, + previousEntries, + }); + expect([...reordered.keys()]).toEqual(["srv:two", "srv:one"]); + const removed = buildSidebarWorkspaceEntries({ + placements: model.workspaces.slice(0, 1), + sessions, + previousEntries, + }); + expect([...removed.keys()]).toEqual(["srv:one"]); + }); + it("preserves unchanged row identities when another workspace updates", () => { const model = buildSidebarWorkspacePlacementModel({ projects: [project({ projectKey: "project", workspaceKeys: ["srv:one", "srv:two"] })], diff --git a/packages/app/src/hooks/sidebar-workspaces-view-model.ts b/packages/app/src/hooks/sidebar-workspaces-view-model.ts index 24ecc34461..e004c3fb89 100644 --- a/packages/app/src/hooks/sidebar-workspaces-view-model.ts +++ b/packages/app/src/hooks/sidebar-workspaces-view-model.ts @@ -369,13 +369,15 @@ export function buildSidebarWorkspaceEntries(input: { sessions: SidebarWorkspaceSession[]; pendingCreateAttempts?: Record; previousEntries?: ReadonlyMap; -}): Map { +}): ReadonlyMap { if (input.placements.length === 0 || input.sessions.length === 0) { return new Map(); } const sessionByServerId = new Map(input.sessions.map((session) => [session.serverId, session])); const entries = new Map(); + const previousKeys = input.previousEntries?.keys(); + let unchanged = input.previousEntries !== undefined; for (const placement of input.placements) { const session = sessionByServerId.get(placement.serverId); @@ -395,15 +397,22 @@ export function buildSidebarWorkspaceEntries(input: { workspaceAgentActivity: session.workspaceAgentActivity, }); const previousEntry = input.previousEntries?.get(placement.workspaceKey); - entries.set( - placement.workspaceKey, + const nextEntry = previousEntry && areSidebarWorkspaceEntriesEqual(previousEntry, entry) ? previousEntry - : entry, - ); + : entry; + entries.set(placement.workspaceKey, nextEntry); + unchanged = + previousKeys?.next().value === placement.workspaceKey && + previousEntry === nextEntry && + unchanged; } - return entries; + // Resuming a retained sidebar must not invalidate its project/filter projections + // when the directory has not changed while it was hidden. + return unchanged && input.previousEntries?.size === entries.size + ? input.previousEntries + : entries; } function areSidebarWorkspaceEntriesEqual(