Skip to content

fix: refresh a placeholder's recorded size when it is collapsed - #15733

Open
mattmillerai wants to merge 2 commits into
matt/be-8848-placeholder-decorationsfrom
matt/be-8851-collapse-refresh-placeholder-size
Open

fix: refresh a placeholder's recorded size when it is collapsed#15733
mattmillerai wants to merge 2 commits into
matt/be-8848-placeholder-decorationsfrom
matt/be-8851-collapse-refresh-placeholder-size

Conversation

@mattmillerai

@mattmillerai mattmillerai commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

STACKED — merging lands on matt/be-8848-placeholder-decorations (owned by @mattmillerai, PR #15719), NOT main. That branch is itself stacked on #15713. Do not merge this until both land; it is not "ready to merge" on its own.

ELI-5

If a node in your workflow comes from an extension you do not have installed, the frontend draws a placeholder in its place. Because it has no node definition to work from, saving the workflow replays whatever the file said about that node instead of regenerating it. #15713 made an expanded placeholder save its live size, but a collapsed one falls back to the size recorded in the file — and that record was written once when the workflow loaded and never updated. So: resize a placeholder, collapse it, save — and the resize was thrown away. This makes collapsing and expanding both refresh that record, so the size you last gave the placeholder is the one that gets saved.

Summary

collapse() now keeps a missing-node placeholder's recorded serialization size and its live size in sync in both directions, so a resize made while expanded survives a collapse-then-save round trip and is not lost again on the way back out.

Changes

  • Collapsing copies the live size into last_serialization.size. fix: preserve a live resize when serializing missing-node placeholders #15713's rule is kept intact — a collapsed placeholder still serializes the recorded size, since it has no definition to re-derive an expanded one from; what changes is that "recorded" now means the last known expanded size rather than the load-time one.
  • Expanding restores the live size from last_serialization.size. In Vue nodes mode node.size is a DOM measurement of whichever card is rendered (useLayoutSync.ts:55 writes the ResizeObserver's bounds into liteNode.size), so immediately after an expand it still holds the collapsed card's dimensions until the next measurement lands a frame later. Without the restore, a serialize() inside that window persisted the collapsed dimensions, and a second collapse inside it re-recorded them — destroying the value the refresh exists to preserve.
  • Both halves run before the flags.collapsed flip, so each reads node.size while it still measures the card the node is leaving.
  • Two guards remain: this.constructor === LGraphNode (mirrors serialize()'s own placeholder test) and last_serialization?.size != null (preserves fix: preserve a live resize when serializing missing-node placeholders #15713's "does not invent a size when the recorded serialization has none" rule). The !this.flags.collapsed guard is gone — the flag now selects the direction instead of gating the write.
  • The record is replaced rather than mutated in place, because LGraph.configure() assigns last_serialization by reference straight out of the caller's data.nodes (LGraph.ts:2626; the dedup clone at LGraph.ts:2599 only kicks in for workflows with subgraph definitions). Writing through it would corrupt a retained caller snapshot, and would throw if the input were frozen — aborting the collapse before the flag flipped.
  • Six tests in the missing-node placeholder serialization block in LGraphNode.test.ts.

Review Focus

  • The direction switch in collapse(). It is the whole change. Canvas mode never overwrites node.size on collapse, so there the restore rewrites the values it just recorded and is a no-op; the asymmetry only exists under Vue nodes.
  • Restoring through the size setter (this.size = recordedSerialization.size) rather than setSize(): the setter copies element-wise (no aliasing with last_serialization), guards length < 2, and mirrors into the layout store, but does not fire onResize. Since the restore happens while the node is still flagged collapsed, LGraphNode.vue's handleLayoutChange bails out and the DOM is restored by the existing isCollapsed watch's --node-width-x stash instead.
  • Reassigning last_serialization. No reader caches the object identity — useNodeReplacement.ts:178, missingNodeScan.ts:28, useErrorClearingHooks.ts:236 and the rest read fields at call time — and the load-time writes that do rely on the aliasing (app.ts:2222, app.ts:2270, LGraph.ts:2007) all run long before any collapse.
  • Second consumer of the refreshed field. useNodeReplacement.ts uses node.last_serialization ?? node.serialize() when swapping a placeholder for a real node, so a replacement now inherits the last expanded size rather than the load-time one. That is the intended direction, but it is worth a look.
  • Incidental fix. Expanding a placeholder that was loaded collapsed and then saving used to persist the collapsed card measurement. The restore fixes that too; it predates this PR.

Sweep

The hook is in collapse() only, on the premise that nothing else flips the flag. Sweeping flags.collapsed = across src/, browser_tests/ and apps/: 1 non-test source assignment — the line this change sits in front of — and 17 assignments across 7 test files, none of which are product paths and none of which are touched here. All 5 non-test .collapse( call sites (useNodeEventHandlers.ts, useCoreCommands.ts, useSelectedNodeActions.ts, and two in LGraphCanvas.ts) route through the hook. So the portion deliberately not covered is the direct-flag path, which exists only in tests.

Notes and judgment calls

Unexercised

  • Verification is unit-level. I did not run the Playwright suite or drive Vue nodes mode in a browser. The Vue-mode measurement timing that motivates both the before-the-flip capture and the restore is read off useLayoutSync.ts and LGraphNode.vue rather than observed live, so the width of the re-measure window is inferred, not bounded empirically. The fix does not depend on that width — it closes the window synchronously rather than racing it.
  • The read-only investigation this implementation came from lives in an internal tracker this environment cannot reach, so its findings were taken as given from the plan text rather than re-derived.

Provenance

  • Authored by: agent-work loop
  • Verified: pnpm test:unit src/lib/litegraph src/platform/nodeReplacement src/renderer/core/layout src/renderer/extensions/vueNodes — 178 files, 2330 passed / 3 expected-fail / 12 skipped, 0 failed; pnpm test:unit src/lib/litegraph/src/LGraphNode.test.ts — 51 passed; call-site suites (useCoreCommands, useLoad3d) — 134 passed; pnpm typecheck — clean; oxlint, oxfmt --check and lint on both changed files — clean. Red/green checked on all three new tests: deleting the restore-on-expand line fails 2 of them with expected [ 80, 30 ] to deeply equal [ 777, 666 ], and reverting the replace to an in-place mutation fails the third.
  • Deviations: none of the plan's acceptance criteria were skipped. Two departures from the plan as written, both explained under "Notes and judgment calls": the existing-test premise no longer matches, and the plan scoped only the collapse direction — review found the expand direction was load-bearing for the same guarantee, so it is included here rather than deferred.

A collapsed missing-node placeholder serializes the size recorded in the
file it was loaded with, which was set once at load and never refreshed.
Resizing an expanded placeholder, collapsing it, then saving therefore
wrote the load-time dimensions and dropped the resize on reload.

collapse() now refreshes that record with the size the node is being
collapsed from, captured before the flag flips so Vue nodes mode's
collapsed-card measurement cannot overwrite it.
@mattmillerai mattmillerai added the agent-coded Opened by the agent-work code loop label Aug 23, 2026
@mattmillerai
mattmillerai marked this pull request as ready for review August 23, 2026 09:41
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ee7367eb-6330-474e-8da2-27365c0491a9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@dosubot dosubot Bot added the size:S This PR changes 10-29 lines, ignoring generated files. label Aug 23, 2026
@github-actions

github-actions Bot commented Aug 23, 2026

Copy link
Copy Markdown

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 08/23/2026, 10:20:41 AM UTC

Links

🎭 Playwright: ✅ 1975 passed, 0 failed · 5 flaky

📊 Browser Reports
  • chromium: View Report (✅ 1954 / ❌ 0 / ⚠️ 5 / ⏭️ 5)
  • chromium-2x: View Report (✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • chromium-0.5x: View Report (✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0)
  • mobile-chrome: View Report (✅ 18 / ❌ 0 / ⚠️ 0 / ⏭️ 0)

📦 Bundle Size

⏳ Size data collection in progress…

⚡ Performance Report

canvas-idle: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 68.9 MB heap
canvas-mouse-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 76.9 MB heap
canvas-zoom-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 61.9 MB heap
dom-widget-clipping: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 69.9 MB heap
large-graph-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 70.9 MB heap
large-graph-pan: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 67.7 MB heap
large-graph-zoom: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 65.6 MB heap
legacy-node-drag: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 61.2 MB heap
minimap-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 69.2 MB heap
subgraph-dom-widget-clipping: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 70.8 MB heap
subgraph-idle: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 55.3 MB heap
subgraph-mouse-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 54.3 MB heap
subgraph-transition-enter: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 137ms TBT · 86.1 MB heap
viewport-pan-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 76.8 MB heap
vue-large-graph-idle: · 56.2 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 177.7 MB heap
vue-large-graph-pan: · 56.3 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 34ms TBT · 183.7 MB heap
workflow-execution: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 63.0 MB heap

ℹ️ No baseline found — significance unavailable.

Absolute values
Metric Value
canvas-idle: avg frame time 17ms
canvas-idle: p95 frame time 17ms
canvas-idle: layout duration 0ms
canvas-idle: style recalc duration 7ms
canvas-idle: layout count 0
canvas-idle: style recalc count 8
canvas-idle: task duration 542ms
canvas-idle: script duration 9ms
canvas-idle: TBT 0ms
canvas-idle: heap used 68.9 MB
canvas-idle: DOM nodes -284
canvas-idle: event listeners -166
canvas-mouse-sweep: avg frame time 17ms
canvas-mouse-sweep: p95 frame time 17ms
canvas-mouse-sweep: layout duration 3ms
canvas-mouse-sweep: style recalc duration 39ms
canvas-mouse-sweep: layout count 12
canvas-mouse-sweep: style recalc count 74
canvas-mouse-sweep: task duration 903ms
canvas-mouse-sweep: script duration 115ms
canvas-mouse-sweep: TBT 0ms
canvas-mouse-sweep: heap used 76.9 MB
canvas-mouse-sweep: DOM nodes -281
canvas-mouse-sweep: event listeners -153
canvas-zoom-sweep: avg frame time 17ms
canvas-zoom-sweep: p95 frame time 17ms
canvas-zoom-sweep: layout duration 1ms
canvas-zoom-sweep: style recalc duration 16ms
canvas-zoom-sweep: layout count 6
canvas-zoom-sweep: style recalc count 31
canvas-zoom-sweep: task duration 356ms
canvas-zoom-sweep: script duration 10ms
canvas-zoom-sweep: TBT 0ms
canvas-zoom-sweep: heap used 61.9 MB
canvas-zoom-sweep: DOM nodes 76
canvas-zoom-sweep: event listeners 19
dom-widget-clipping: avg frame time 17ms
dom-widget-clipping: p95 frame time 17ms
dom-widget-clipping: layout duration 0ms
dom-widget-clipping: style recalc duration 8ms
dom-widget-clipping: layout count 0
dom-widget-clipping: style recalc count 12
dom-widget-clipping: task duration 358ms
dom-widget-clipping: script duration 53ms
dom-widget-clipping: TBT 0ms
dom-widget-clipping: heap used 69.9 MB
dom-widget-clipping: DOM nodes 19
dom-widget-clipping: event listeners 1
large-graph-idle: avg frame time 17ms
large-graph-idle: p95 frame time 17ms
large-graph-idle: layout duration 0ms
large-graph-idle: style recalc duration 10ms
large-graph-idle: layout count 0
large-graph-idle: style recalc count 9
large-graph-idle: task duration 613ms
large-graph-idle: script duration 16ms
large-graph-idle: TBT 0ms
large-graph-idle: heap used 70.9 MB
large-graph-idle: DOM nodes -273
large-graph-idle: event listeners -149
large-graph-pan: avg frame time 17ms
large-graph-pan: p95 frame time 17ms
large-graph-pan: layout duration 0ms
large-graph-pan: style recalc duration 15ms
large-graph-pan: layout count 0
large-graph-pan: style recalc count 70
large-graph-pan: task duration 1205ms
large-graph-pan: script duration 328ms
large-graph-pan: TBT 0ms
large-graph-pan: heap used 67.7 MB
large-graph-pan: DOM nodes -276
large-graph-pan: event listeners -149
large-graph-zoom: avg frame time 17ms
large-graph-zoom: p95 frame time 17ms
large-graph-zoom: layout duration 8ms
large-graph-zoom: style recalc duration 16ms
large-graph-zoom: layout count 60
large-graph-zoom: style recalc count 66
large-graph-zoom: task duration 1366ms
large-graph-zoom: script duration 378ms
large-graph-zoom: TBT 0ms
large-graph-zoom: heap used 65.6 MB
large-graph-zoom: DOM nodes -137
large-graph-zoom: event listeners -88
legacy-node-drag: avg frame time 17ms
legacy-node-drag: p95 frame time 17ms
legacy-node-drag: layout duration 0ms
legacy-node-drag: style recalc duration 13ms
legacy-node-drag: layout count 0
legacy-node-drag: style recalc count 46
legacy-node-drag: task duration 1509ms
legacy-node-drag: script duration 481ms
legacy-node-drag: TBT 0ms
legacy-node-drag: heap used 61.2 MB
legacy-node-drag: DOM nodes -257
legacy-node-drag: event listeners 33
minimap-idle: avg frame time 17ms
minimap-idle: p95 frame time 17ms
minimap-idle: layout duration 0ms
minimap-idle: style recalc duration 8ms
minimap-idle: layout count 0
minimap-idle: style recalc count 8
minimap-idle: task duration 629ms
minimap-idle: script duration 16ms
minimap-idle: TBT 0ms
minimap-idle: heap used 69.2 MB
minimap-idle: DOM nodes -278
minimap-idle: event listeners -150
subgraph-dom-widget-clipping: avg frame time 17ms
subgraph-dom-widget-clipping: p95 frame time 17ms
subgraph-dom-widget-clipping: layout duration 0ms
subgraph-dom-widget-clipping: style recalc duration 9ms
subgraph-dom-widget-clipping: layout count 0
subgraph-dom-widget-clipping: style recalc count 46
subgraph-dom-widget-clipping: task duration 369ms
subgraph-dom-widget-clipping: script duration 111ms
subgraph-dom-widget-clipping: TBT 0ms
subgraph-dom-widget-clipping: heap used 70.8 MB
subgraph-dom-widget-clipping: DOM nodes 18
subgraph-dom-widget-clipping: event listeners 8
subgraph-idle: avg frame time 17ms
subgraph-idle: p95 frame time 17ms
subgraph-idle: layout duration 0ms
subgraph-idle: style recalc duration 9ms
subgraph-idle: layout count 0
subgraph-idle: style recalc count 9
subgraph-idle: task duration 489ms
subgraph-idle: script duration 7ms
subgraph-idle: TBT 0ms
subgraph-idle: heap used 55.3 MB
subgraph-idle: DOM nodes -281
subgraph-idle: event listeners -150
subgraph-mouse-sweep: avg frame time 17ms
subgraph-mouse-sweep: p95 frame time 17ms
subgraph-mouse-sweep: layout duration 4ms
subgraph-mouse-sweep: style recalc duration 38ms
subgraph-mouse-sweep: layout count 16
subgraph-mouse-sweep: style recalc count 77
subgraph-mouse-sweep: task duration 818ms
subgraph-mouse-sweep: script duration 87ms
subgraph-mouse-sweep: TBT 0ms
subgraph-mouse-sweep: heap used 54.3 MB
subgraph-mouse-sweep: DOM nodes -281
subgraph-mouse-sweep: event listeners -151
subgraph-transition-enter: avg frame time 17ms
subgraph-transition-enter: p95 frame time 17ms
subgraph-transition-enter: layout duration 15ms
subgraph-transition-enter: style recalc duration 31ms
subgraph-transition-enter: layout count 14
subgraph-transition-enter: style recalc count 19
subgraph-transition-enter: task duration 950ms
subgraph-transition-enter: script duration 16ms
subgraph-transition-enter: TBT 137ms
subgraph-transition-enter: heap used 86.1 MB
subgraph-transition-enter: DOM nodes 13673
subgraph-transition-enter: event listeners 2373
viewport-pan-sweep: avg frame time 17ms
viewport-pan-sweep: p95 frame time 17ms
viewport-pan-sweep: layout duration 0ms
viewport-pan-sweep: style recalc duration 36ms
viewport-pan-sweep: layout count 0
viewport-pan-sweep: style recalc count 251
viewport-pan-sweep: task duration 4147ms
viewport-pan-sweep: script duration 1023ms
viewport-pan-sweep: TBT 0ms
viewport-pan-sweep: heap used 76.8 MB
viewport-pan-sweep: DOM nodes -259
viewport-pan-sweep: event listeners -133
vue-large-graph-idle: avg frame time 18ms
vue-large-graph-idle: p95 frame time 17ms
vue-large-graph-idle: layout duration 0ms
vue-large-graph-idle: style recalc duration 0ms
vue-large-graph-idle: layout count 0
vue-large-graph-idle: style recalc count 0
vue-large-graph-idle: task duration 16343ms
vue-large-graph-idle: script duration 109ms
vue-large-graph-idle: TBT 0ms
vue-large-graph-idle: heap used 177.7 MB
vue-large-graph-idle: DOM nodes -8312
vue-large-graph-idle: event listeners -16391
vue-large-graph-pan: avg frame time 18ms
vue-large-graph-pan: p95 frame time 17ms
vue-large-graph-pan: layout duration 0ms
vue-large-graph-pan: style recalc duration 16ms
vue-large-graph-pan: layout count 0
vue-large-graph-pan: style recalc count 165
vue-large-graph-pan: task duration 19569ms
vue-large-graph-pan: script duration 389ms
vue-large-graph-pan: TBT 34ms
vue-large-graph-pan: heap used 183.7 MB
vue-large-graph-pan: DOM nodes -8312
vue-large-graph-pan: event listeners -16388
workflow-execution: avg frame time 17ms
workflow-execution: p95 frame time 17ms
workflow-execution: layout duration 0ms
workflow-execution: style recalc duration 17ms
workflow-execution: layout count 2
workflow-execution: style recalc count 11
workflow-execution: task duration 94ms
workflow-execution: script duration 7ms
workflow-execution: TBT 0ms
workflow-execution: heap used 63.0 MB
workflow-execution: DOM nodes 117
workflow-execution: event listeners 74
Raw data
{
  "timestamp": "2026-08-23T10:40:14.669Z",
  "gitSha": "ea7da0b94221bc3927aa0cf6eb8ad915553d5d51",
  "branch": "matt/be-8851-collapse-refresh-placeholder-size",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2121.255000000019,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 6.393,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 562.369,
      "heapDeltaBytes": 2088832,
      "heapUsedBytes": 64024844,
      "domNodes": -285,
      "jsHeapTotalBytes": 3923968,
      "scriptDurationMs": 9.264999999999999,
      "eventListeners": -181,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-idle",
      "durationMs": 2035.2829999999358,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 8.573,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 521.884,
      "heapDeltaBytes": 19305768,
      "heapUsedBytes": 80468196,
      "domNodes": -283,
      "jsHeapTotalBytes": 3923968,
      "scriptDurationMs": 8.192000000000002,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 2042.002000000025,
      "styleRecalcs": 76,
      "styleRecalcDurationMs": 42.615,
      "layouts": 12,
      "layoutDurationMs": 3.8890000000000002,
      "taskDurationMs": 950.3790000000001,
      "heapDeltaBytes": 21125992,
      "heapUsedBytes": 82452172,
      "domNodes": -281,
      "jsHeapTotalBytes": 5234688,
      "scriptDurationMs": 122.90500000000002,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1804.9270000000206,
      "styleRecalcs": 72,
      "styleRecalcDurationMs": 35.515,
      "layouts": 12,
      "layoutDurationMs": 3.0360000000000005,
      "taskDurationMs": 854.9780000000001,
      "heapDeltaBytes": 15972496,
      "heapUsedBytes": 78760936,
      "domNodes": -281,
      "jsHeapTotalBytes": 6021120,
      "scriptDurationMs": 107.833,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1708.4029999999757,
      "styleRecalcs": 31,
      "styleRecalcDurationMs": 14.937,
      "layouts": 6,
      "layoutDurationMs": 0.5830000000000002,
      "taskDurationMs": 336.913,
      "heapDeltaBytes": 2466604,
      "heapUsedBytes": 63753292,
      "domNodes": 75,
      "jsHeapTotalBytes": 4456448,
      "scriptDurationMs": 8.715,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1721.5589999999565,
      "styleRecalcs": 31,
      "styleRecalcDurationMs": 17.114,
      "layouts": 6,
      "layoutDurationMs": 0.5070000000000001,
      "taskDurationMs": 375.18300000000005,
      "heapDeltaBytes": 3158040,
      "heapUsedBytes": 66054480,
      "domNodes": 77,
      "jsHeapTotalBytes": 4980736,
      "scriptDurationMs": 11.225999999999999,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 582.4049999999943,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 7.815,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 365.098,
      "heapDeltaBytes": 10892672,
      "heapUsedBytes": 73228632,
      "domNodes": 18,
      "jsHeapTotalBytes": 4718592,
      "scriptDurationMs": 52.187999999999995,
      "eventListeners": 0,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 578.1220000000076,
      "styleRecalcs": 12,
      "styleRecalcDurationMs": 8.084000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 350.397,
      "heapDeltaBytes": 10798408,
      "heapUsedBytes": 73361940,
      "domNodes": 20,
      "jsHeapTotalBytes": 4980736,
      "scriptDurationMs": 53.209999999999994,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2070.409999999981,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 11.546999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 609.143,
      "heapDeltaBytes": -1379212,
      "heapUsedBytes": 75321844,
      "domNodes": -269,
      "jsHeapTotalBytes": -1052672,
      "scriptDurationMs": 15.709000000000001,
      "eventListeners": -147,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2054.4550000000754,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 7.549,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 616.218,
      "heapDeltaBytes": -3339384,
      "heapUsedBytes": 73304372,
      "domNodes": -277,
      "jsHeapTotalBytes": -4096,
      "scriptDurationMs": 17.243,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2180.1550000000134,
      "styleRecalcs": 70,
      "styleRecalcDurationMs": 14.546000000000003,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1178.59,
      "heapDeltaBytes": -7767504,
      "heapUsedBytes": 69911076,
      "domNodes": -275,
      "jsHeapTotalBytes": 745472,
      "scriptDurationMs": 317.213,
      "eventListeners": -147,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2179.121000000009,
      "styleRecalcs": 70,
      "styleRecalcDurationMs": 15.040000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1232.361,
      "heapDeltaBytes": -5676876,
      "heapUsedBytes": 71990148,
      "domNodes": -276,
      "jsHeapTotalBytes": 221184,
      "scriptDurationMs": 338.65000000000003,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3129.7500000000014,
      "styleRecalcs": 65,
      "styleRecalcDurationMs": 14.254,
      "layouts": 60,
      "layoutDurationMs": 7.177,
      "taskDurationMs": 1316.0700000000002,
      "heapDeltaBytes": -7752788,
      "heapUsedBytes": 69946424,
      "domNodes": 12,
      "jsHeapTotalBytes": 6852608,
      "scriptDurationMs": 376.238,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3197.5609999999506,
      "styleRecalcs": 67,
      "styleRecalcDurationMs": 17.533,
      "layouts": 60,
      "layoutDurationMs": 7.940000000000001,
      "taskDurationMs": 1416.588,
      "heapDeltaBytes": -9879900,
      "heapUsedBytes": 67622528,
      "domNodes": -285,
      "jsHeapTotalBytes": 6287360,
      "scriptDurationMs": 379.77500000000003,
      "eventListeners": -183,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "legacy-node-drag",
      "durationMs": 2207.763,
      "styleRecalcs": 45,
      "styleRecalcDurationMs": 13.128999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1510.9460000000001,
      "heapDeltaBytes": -22542324,
      "heapUsedBytes": 64301444,
      "domNodes": -264,
      "jsHeapTotalBytes": 8122368,
      "scriptDurationMs": 492.811,
      "eventListeners": 33,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "legacy-node-drag",
      "durationMs": 2369.927999999959,
      "styleRecalcs": 46,
      "styleRecalcDurationMs": 13.083,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1506.9189999999999,
      "heapDeltaBytes": -16282260,
      "heapUsedBytes": 63949896,
      "domNodes": -250,
      "jsHeapTotalBytes": 258048,
      "scriptDurationMs": 469.395,
      "eventListeners": 33,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "minimap-idle",
      "durationMs": 2056.861999999967,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 9.001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 619.3549999999999,
      "heapDeltaBytes": -12292668,
      "heapUsedBytes": 70817220,
      "domNodes": -279,
      "jsHeapTotalBytes": 5238784,
      "scriptDurationMs": 15.361000000000002,
      "eventListeners": -149,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "minimap-idle",
      "durationMs": 2045.4799999999977,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 7.573999999999997,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 638.3019999999999,
      "heapDeltaBytes": -3942024,
      "heapUsedBytes": 74372992,
      "domNodes": -277,
      "jsHeapTotalBytes": -1314816,
      "scriptDurationMs": 17.29,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 555.5679999999938,
      "styleRecalcs": 46,
      "styleRecalcDurationMs": 9.114,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 365.68199999999996,
      "heapDeltaBytes": 11481100,
      "heapUsedBytes": 73951888,
      "domNodes": 18,
      "jsHeapTotalBytes": 5505024,
      "scriptDurationMs": 110.736,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 546.5509999999085,
      "styleRecalcs": 46,
      "styleRecalcDurationMs": 9.322,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 373.27600000000007,
      "heapDeltaBytes": 11469336,
      "heapUsedBytes": 74456084,
      "domNodes": 18,
      "jsHeapTotalBytes": 4980736,
      "scriptDurationMs": 110.587,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2021.8619999999987,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 8.648,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 486.12999999999994,
      "heapDeltaBytes": -4833800,
      "heapUsedBytes": 57734140,
      "domNodes": -281,
      "jsHeapTotalBytes": 4186112,
      "scriptDurationMs": 6.7410000000000005,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2006.287000000043,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 8.697000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 492.05800000000005,
      "heapDeltaBytes": -4214716,
      "heapUsedBytes": 58338028,
      "domNodes": -281,
      "jsHeapTotalBytes": 4448256,
      "scriptDurationMs": 7.5680000000000005,
      "eventListeners": -149,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1775.5769999999984,
      "styleRecalcs": 76,
      "styleRecalcDurationMs": 37.733000000000004,
      "layouts": 16,
      "layoutDurationMs": 4.511,
      "taskDurationMs": 826.4350000000001,
      "heapDeltaBytes": -5884276,
      "heapUsedBytes": 56509896,
      "domNodes": -280,
      "jsHeapTotalBytes": 5496832,
      "scriptDurationMs": 87.89,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1792.5940000000082,
      "styleRecalcs": 77,
      "styleRecalcDurationMs": 38.312,
      "layouts": 16,
      "layoutDurationMs": 4.142,
      "taskDurationMs": 810.3860000000001,
      "heapDeltaBytes": -5188208,
      "heapUsedBytes": 57305664,
      "domNodes": -282,
      "jsHeapTotalBytes": 4186112,
      "scriptDurationMs": 85.652,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66999999999998,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "subgraph-transition-enter",
      "durationMs": 1401.1639999999943,
      "styleRecalcs": 19,
      "styleRecalcDurationMs": 31.01899999999999,
      "layouts": 14,
      "layoutDurationMs": 14.573000000000002,
      "taskDurationMs": 950.2620000000002,
      "heapDeltaBytes": -5882520,
      "heapUsedBytes": 90277348,
      "domNodes": 13673,
      "jsHeapTotalBytes": 12058624,
      "scriptDurationMs": 16.188000000000002,
      "eventListeners": 2373,
      "totalBlockingTimeMs": 137,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.80000000000109
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8164.852999999994,
      "styleRecalcs": 250,
      "styleRecalcDurationMs": 34.971000000000004,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 3999.275,
      "heapDeltaBytes": 943412,
      "heapUsedBytes": 77753020,
      "domNodes": -278,
      "jsHeapTotalBytes": -1089536,
      "scriptDurationMs": 995.588,
      "eventListeners": -135,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.669999999999952,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8297.001000000022,
      "styleRecalcs": 251,
      "styleRecalcDurationMs": 37.258,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 4295.3910000000005,
      "heapDeltaBytes": 6648376,
      "heapUsedBytes": 83212716,
      "domNodes": -239,
      "jsHeapTotalBytes": -40960,
      "scriptDurationMs": 1051.1029999999998,
      "eventListeners": -131,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 16860.22699999995,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 16329.349,
      "heapDeltaBytes": -18442120,
      "heapUsedBytes": 193900624,
      "domNodes": -8312,
      "jsHeapTotalBytes": -12099584,
      "scriptDurationMs": 115.374,
      "eventListeners": -16389,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 16935.524999999983,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 16356.704,
      "heapDeltaBytes": -33159612,
      "heapUsedBytes": 178828220,
      "domNodes": -8312,
      "jsHeapTotalBytes": -16076800,
      "scriptDurationMs": 102.507,
      "eventListeners": -16393,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.780000000000047,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 20014.808000000015,
      "styleRecalcs": 161,
      "styleRecalcDurationMs": 16.029000000000014,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 19574.483999999997,
      "heapDeltaBytes": -33040964,
      "heapUsedBytes": 192617492,
      "domNodes": -8312,
      "jsHeapTotalBytes": -17149952,
      "scriptDurationMs": 389.992,
      "eventListeners": -16387,
      "totalBlockingTimeMs": 31,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 20048.043000000005,
      "styleRecalcs": 169,
      "styleRecalcDurationMs": 16.629000000000005,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 19563.538,
      "heapDeltaBytes": -33255992,
      "heapUsedBytes": 192672460,
      "domNodes": -8312,
      "jsHeapTotalBytes": -17416192,
      "scriptDurationMs": 388.954,
      "eventListeners": -16389,
      "totalBlockingTimeMs": 37,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 16.80000000000291
    },
    {
      "name": "workflow-execution",
      "durationMs": 122.07300000000032,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 14.139999999999999,
      "layouts": 2,
      "layoutDurationMs": 0.503,
      "taskDurationMs": 81.86699999999999,
      "heapDeltaBytes": 3166600,
      "heapUsedBytes": 64856572,
      "domNodes": 113,
      "jsHeapTotalBytes": 524288,
      "scriptDurationMs": 5.802000000000001,
      "eventListeners": 49,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "workflow-execution",
      "durationMs": 452.20499999993535,
      "styleRecalcs": 13,
      "styleRecalcDurationMs": 19.785999999999998,
      "layouts": 2,
      "layoutDurationMs": 0.33600000000000013,
      "taskDurationMs": 105.712,
      "heapDeltaBytes": 5001464,
      "heapUsedBytes": 67190208,
      "domNodes": 121,
      "jsHeapTotalBytes": 0,
      "scriptDurationMs": 8.369,
      "eventListeners": 99,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    }
  ]
}

@github-actions github-actions Bot added the risk:R3 PR risk grade (advisory shadow check; grader-owned) label Aug 23, 2026
@mattmillerai mattmillerai added the cursor-review Trigger multi-model Cursor agent review on this PR label Aug 23, 2026
@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 33.33333% with 4 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/lib/litegraph/src/LGraphNode.ts 33.33% 3 Missing and 1 partial ⚠️
@@                          Coverage Diff                           @@
##           matt/be-8848-placeholder-decorations   #15733    +/-   ##
======================================================================
  Coverage                                 81.89%   81.89%            
======================================================================
  Files                                      1888     1888            
  Lines                                    107287   107293     +6     
  Branches                                  30087    29622   -465     
======================================================================
+ Hits                                      87858    87865     +7     
+ Misses                                    19093    19083    -10     
- Partials                                    336      345     +9     
Flag Coverage Δ
e2e 67.42% <33.33%> (+<0.01%) ⬆️
unit 73.54% <ø> (ø)

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/lib/litegraph/src/LGraphNode.ts 64.26% <33.33%> (-0.14%) ⬇️

... and 18 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Cursor Review — Consolidated panel

Triggered by @mattmillerai.

Found 4 finding(s).

Severity Count
🟠 High 1
🟡 Medium 3

Panel: 8/8 reviewers contributed findings.

Comment thread src/lib/litegraph/src/LGraphNode.ts
Comment thread src/lib/litegraph/src/LGraphNode.ts Outdated
Comment thread src/lib/litegraph/src/LGraphNode.ts
Comment thread src/lib/litegraph/src/LGraphNode.test.ts Outdated
@dosubot dosubot Bot added size:M This PR changes 30-99 lines, ignoring generated files. and removed size:S This PR changes 10-29 lines, ignoring generated files. labels Aug 23, 2026
collapse() refreshed last_serialization.size on the expanded->collapsed
transition only. In Vue nodes mode node.size is a DOM measurement of the
card that is currently rendered, so right after an expand it still holds
the collapsed card's dimensions until the ResizeObserver write-back lands
a frame later. Serializing inside that window persisted the collapsed
size over the recorded one, and a second collapse inside it re-recorded
the collapsed size, destroying the value the refresh exists to keep.

Make the refresh symmetric: expanding now restores node.size from the
recorded size, so both the window's serialize and its re-collapse see the
expanded dimensions. The record is also replaced rather than mutated in
place, since LGraph.configure() assigns it by reference out of the
caller's workflow data.
@mattmillerai
mattmillerai force-pushed the matt/be-8851-collapse-refresh-placeholder-size branch from fe2cc5c to 6bb328c Compare August 23, 2026 10:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent-coded Opened by the agent-work code loop cursor-review Trigger multi-model Cursor agent review on this PR risk:R3 PR risk grade (advisory shadow check; grader-owned) size:M This PR changes 30-99 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant