diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index f013a76..ab073ff 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -70,6 +70,7 @@ import { mergeCollectorEnrichment, mergeCollectorPackages, removeCollectorLinks, + reconcileCollectorCollapsedPackageIds, selectCollectorPackageLinks, type CollectorWorkspaceFilter } from "./views/collector/collector-model"; @@ -3596,13 +3597,19 @@ export function App(): ReactElement { }; const mergeCollectorResult = (result: CollectorInspectionResult, enrichment = false): void => { - setCollectorPackages((current) => { - const merged = enrichment - ? mergeCollectorEnrichment(current, result.packages) - : mergeCollectorPackages(current, result.packages); - collectorPackagesRef.current = merged.packages; - return merged.packages; - }); + const current = collectorPackagesRef.current; + const merged = enrichment + ? mergeCollectorEnrichment(current, result.packages) + : mergeCollectorPackages(current, result.packages); + collectorPackagesRef.current = merged.packages; + setCollectorPackages(merged.packages); + setCollapsedCollectorPackageIds((collapsed) => reconcileCollectorCollapsedPackageIds( + collapsed, + current, + merged.packages, + result.packages, + !enrichment + )); }; const enrichCollectorResult = (packages: CollectorPackage[]): void => { diff --git a/src/renderer/views/collector/collector-model.ts b/src/renderer/views/collector/collector-model.ts index 886c132..24235b9 100644 --- a/src/renderer/views/collector/collector-model.ts +++ b/src/renderer/views/collector/collector-model.ts @@ -186,6 +186,37 @@ export function mergeCollectorEnrichment(current: CollectorPackage[], incoming: return mergeCollectorPackages(current, retained); } +export function reconcileCollectorCollapsedPackageIds( + current: Set, + previousPackages: CollectorPackage[], + packages: CollectorPackage[], + incoming: CollectorPackage[], + collapseImported: boolean +): Set { + const incomingUrls = new Set(incoming.flatMap((pkg) => pkg.links.map((link) => collectorUrlKey(link.url)))); + const previousPackageByUrl = new Map(); + const collapsedUrls = new Set(); + for (const pkg of previousPackages) { + for (const link of pkg.links) { + const key = collectorUrlKey(link.url); + previousPackageByUrl.set(key, pkg.id); + if (current.has(pkg.id)) collapsedUrls.add(key); + } + } + const next = new Set(); + for (const pkg of packages) { + const keys = pkg.links.map((link) => collectorUrlKey(link.url)); + const preservesCollapsedState = current.has(pkg.id) || keys.some((key) => collapsedUrls.has(key)); + const receivesImportedContent = collapseImported && keys.some((key) => { + if (!incomingUrls.has(key)) return false; + const previousPackageId = previousPackageByUrl.get(key); + return !previousPackageId || previousPackageId !== pkg.id; + }); + if (preservesCollapsedState || receivesImportedContent) next.add(pkg.id); + } + return next; +} + export function selectCollectorPackageLinks(current: Set, pkg: CollectorPackage, selected: boolean): Set { const next = new Set(current); for (const link of pkg.links) { diff --git a/tests/collector-view.test.tsx b/tests/collector-view.test.tsx index 78ec4a6..1073028 100644 --- a/tests/collector-view.test.tsx +++ b/tests/collector-view.test.tsx @@ -8,6 +8,7 @@ import { mergeCollectorEnrichment, mergeCollectorPackages, removeCollectorLinks, + reconcileCollectorCollapsedPackageIds, selectCollectorPackageLinks, type CollectorPackage } from "../src/renderer/views/collector/collector-model"; @@ -335,6 +336,26 @@ describe("collector disclosure virtualization", () => { }); describe("CollectorView", () => { + it("collapses newly imported packages while preserving existing disclosure state", () => { + const incoming = [{ ...packages[0], id: "temporary-import-package" }]; + const collapsed = reconcileCollectorCollapsedPackageIds(new Set(["package-mixed"]), [packages[1]], packages, incoming, true); + + expect(collapsed).toEqual(new Set(["package-mixed", "package-sbs"])); + }); + + it("does not collapse an expanded package for a duplicate-only import", () => { + const incoming = [{ ...packages[0], id: "temporary-import-package" }]; + + expect([...reconcileCollectorCollapsedPackageIds(new Set(), packages, packages, incoming, true)]).toEqual([]); + }); + + it("moves collapsed state to regrouped package ids during enrichment", () => { + const before = [{ ...packages[0], id: "package-before" }]; + const after = [{ ...packages[0], id: "package-after" }]; + + expect([...reconcileCollectorCollapsedPackageIds(new Set(["package-before"]), before, after, after, false)]).toEqual(["package-after"]); + }); + it("reserves the exact package height while content visibility skips offscreen rows", () => { const expanded = buildCollectorWorkspaceViewModel([packages[0]], "all", "", false, [], [], "", true).packages[0]; const collapsed = { ...expanded, collapsed: true };