diff --git a/packages/app/src/stores/session-store-hooks/selectors.test.ts b/packages/app/src/stores/session-store-hooks/selectors.test.ts index 772c44c7e..516eb594c 100644 --- a/packages/app/src/stores/session-store-hooks/selectors.test.ts +++ b/packages/app/src/stores/session-store-hooks/selectors.test.ts @@ -16,7 +16,11 @@ import { workspaceEqualityFns, type SidebarOrderSnapshot, } from "./selectors"; -import { useSessionStore, type WorkspaceDescriptor } from "../session-store"; +import { + useSessionStore, + type EmptyProjectDescriptor, + type WorkspaceDescriptor, +} from "../session-store"; const SERVER_ID = "test-server"; @@ -87,6 +91,12 @@ function emptySidebarOrder(): SidebarOrderSnapshot { }; } +function selectWorkspaceStructureProjectKeys( + state: Parameters[0], +): string[] { + return selectWorkspaceStructureProjects(state, SERVER_ID).map((project) => project.projectKey); +} + afterEach(() => { useSessionStore.getState().clearSession(SERVER_ID); }); @@ -208,6 +218,38 @@ describe("workspace structure composition", () => { }); } + it("keeps a project parent visible throughout the last workspace archive transition", () => { + const workspace = createWorkspace({ + id: "workspace-a", + projectId: "project-a", + projectDisplayName: "Project A", + projectRootPath: "/repo/a", + workspaceDirectory: "/repo/a", + }); + const emptyProject: EmptyProjectDescriptor = { + projectId: "project-a", + projectDisplayName: "Project A", + projectCustomName: null, + projectRootPath: "/repo/a", + projectKind: "git", + }; + initializeWorkspaces([workspace]); + + const emittedProjectKeys = [selectWorkspaceStructureProjectKeys(useSessionStore.getState())]; + const stop = useSessionStore.subscribe((state) => { + emittedProjectKeys.push(selectWorkspaceStructureProjectKeys(state)); + }); + + try { + useSessionStore.getState().removeWorkspace(SERVER_ID, workspace.id); + useSessionStore.getState().addEmptyProject(SERVER_ID, emptyProject); + } finally { + stop(); + } + + expect(emittedProjectKeys).toEqual([["project-a"], ["project-a"]]); + }); + 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" }); diff --git a/packages/app/src/stores/session-store.ts b/packages/app/src/stores/session-store.ts index 5c6c93e2d..734f700b1 100644 --- a/packages/app/src/stores/session-store.ts +++ b/packages/app/src/stores/session-store.ts @@ -229,6 +229,30 @@ function preserveWorkspaceMapIdentity( return changed ? next : existing; } +function emptyProjectDescriptorFromWorkspace( + workspace: WorkspaceDescriptor, +): EmptyProjectDescriptor { + return { + projectId: workspace.projectId, + projectDisplayName: workspace.projectDisplayName, + projectCustomName: workspace.projectCustomName ?? null, + projectRootPath: workspace.projectRootPath, + projectKind: workspace.projectKind, + }; +} + +function hasWorkspaceInProject( + workspaces: ReadonlyMap, + projectId: string, +): boolean { + for (const workspace of workspaces.values()) { + if (workspace.projectId === projectId) { + return true; + } + } + return false; +} + export type ExplorerEntryKind = "file" | "directory"; export type ExplorerFileKind = "text" | "image" | "binary"; export type ExplorerEncoding = "utf-8" | "base64" | "none"; @@ -1393,13 +1417,31 @@ export const useSessionStore = create()( if (!session || !workspaceKey) { return prev; } + const removedWorkspace = session.workspaces.get(workspaceKey); + if (!removedWorkspace) { + return prev; + } const next = new Map(session.workspaces); next.delete(workspaceKey); + let nextEmptyProjects = session.emptyProjects; + if (hasWorkspaceInProject(next, removedWorkspace.projectId)) { + if (nextEmptyProjects.has(removedWorkspace.projectId)) { + nextEmptyProjects = new Map(nextEmptyProjects); + nextEmptyProjects.delete(removedWorkspace.projectId); + } + } else { + const emptyProject = emptyProjectDescriptorFromWorkspace(removedWorkspace); + const existing = nextEmptyProjects.get(emptyProject.projectId); + if (!existing || !equal(existing, emptyProject)) { + nextEmptyProjects = new Map(nextEmptyProjects); + nextEmptyProjects.set(emptyProject.projectId, emptyProject); + } + } return { ...prev, sessions: { ...prev.sessions, - [serverId]: { ...session, workspaces: next }, + [serverId]: { ...session, workspaces: next, emptyProjects: nextEmptyProjects }, }, }; });