diff --git a/apps/desktop/src/main/main.ts b/apps/desktop/src/main/main.ts index a3c24c742..9d6a59a25 100644 --- a/apps/desktop/src/main/main.ts +++ b/apps/desktop/src/main/main.ts @@ -78,6 +78,7 @@ import { import { inspectRecentProject, type RecentProjectInspection } from "./services/projects/recentProjectSummary"; import { resolveProjectIcon } from "./services/projects/projectIconResolver"; import { normalizeStartupProjectState, resolveStartupProject } from "./services/projects/startupProjectResolver"; +import { collectRootsBoundToWindows } from "./services/projects/projectContextRoots"; import { createAdeProjectService } from "./services/projects/adeProjectService"; import { createConfigReloadService } from "./services/projects/configReloadService"; import { IPC } from "../shared/ipc"; @@ -1191,16 +1192,13 @@ app.whenReady().then(async () => { } : null; - const rootsBoundToWindows = (): Set => { - const roots = new Set(); - for (const root of windowProjectRoots.values()) { - if (root) roots.add(root); - } - for (const tabRoots of windowProjectTabRoots.values()) { - for (const root of tabRoots) roots.add(root); - } - return roots; - }; + const rootsBoundToWindows = (): Set => + collectRootsBoundToWindows({ + windowProjectRoots: windowProjectRoots.values(), + windowProjectTabRoots: windowProjectTabRoots.values(), + windowPendingProjectRoots: windowPendingProjectRoots.values(), + projectInitPromises: projectInitPromises.keys(), + }); const emitProjectChangedToWindow = ( windowId: number | null, diff --git a/apps/desktop/src/main/services/projects/projectContextRoots.test.ts b/apps/desktop/src/main/services/projects/projectContextRoots.test.ts new file mode 100644 index 000000000..f60c58c03 --- /dev/null +++ b/apps/desktop/src/main/services/projects/projectContextRoots.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it } from "vitest"; +import { collectRootsBoundToWindows } from "./projectContextRoots"; + +describe("collectRootsBoundToWindows", () => { + it("includes pending and in-flight init roots so rebalance cannot evict mid-open", () => { + const roots = collectRootsBoundToWindows({ + windowProjectRoots: ["/old-repo"], + windowProjectTabRoots: [new Set(["/old-repo"])], + windowPendingProjectRoots: [new Map([["/new-repo", 1]])], + projectInitPromises: ["/warming-repo"], + }); + + expect([...roots].sort()).toEqual(["/new-repo", "/old-repo", "/warming-repo"].sort()); + }); + + it("deduplicates the same root across binding sources", () => { + const roots = collectRootsBoundToWindows({ + windowProjectRoots: ["/repo"], + windowProjectTabRoots: [new Set(["/repo"])], + windowPendingProjectRoots: [new Map([["/repo", 2]])], + projectInitPromises: ["/repo"], + }); + + expect(roots.size).toBe(1); + expect(roots.has("/repo")).toBe(true); + }); +}); diff --git a/apps/desktop/src/main/services/projects/projectContextRoots.ts b/apps/desktop/src/main/services/projects/projectContextRoots.ts new file mode 100644 index 000000000..3adfbfb25 --- /dev/null +++ b/apps/desktop/src/main/services/projects/projectContextRoots.ts @@ -0,0 +1,25 @@ +/** + * Roots that must be treated as "in use" for project-context retention and rebalance. + * Includes window bindings plus in-flight opens (pending IPC authorization and init promises). + */ +export function collectRootsBoundToWindows(args: { + windowProjectRoots: Iterable; + windowProjectTabRoots: Iterable>; + windowPendingProjectRoots: Iterable>; + projectInitPromises: Iterable; +}): Set { + const roots = new Set(); + for (const root of args.windowProjectRoots) { + if (root) roots.add(root); + } + for (const tabRoots of args.windowProjectTabRoots) { + for (const root of tabRoots) roots.add(root); + } + for (const pendingRoots of args.windowPendingProjectRoots) { + for (const root of pendingRoots.keys()) roots.add(root); + } + for (const root of args.projectInitPromises) { + roots.add(root); + } + return roots; +}