fix: refresh a placeholder's recorded size when it is collapsed - #15733
fix: refresh a placeholder's recorded size when it is collapsed#15733mattmillerai wants to merge 2 commits into
Conversation
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.
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
🎨 Storybook: ✅ Built — View Storybook🎭 Playwright: ✅ 1975 passed, 0 failed · 5 flaky📊 Browser Reports
📦 Bundle Size
⚡ Performance Report
Absolute values
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
}
]
} |
Codecov Report❌ Patch coverage is
@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more.
... and 18 files with indirect coverage changes 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
🔍 Cursor Review — Consolidated panel
Triggered by @mattmillerai.
Found 4 finding(s).
| Severity | Count |
|---|---|
| 🟠 High | 1 |
| 🟡 Medium | 3 |
Panel: 8/8 reviewers contributed findings.
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.
fe2cc5c to
6bb328c
Compare
STACKED — merging lands on
matt/be-8848-placeholder-decorations(owned by @mattmillerai, PR #15719), NOTmain. 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
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.last_serialization.size. In Vue nodes modenode.sizeis a DOM measurement of whichever card is rendered (useLayoutSync.ts:55writes the ResizeObserver's bounds intoliteNode.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, aserialize()inside that window persisted the collapsed dimensions, and a second collapse inside it re-recorded them — destroying the value the refresh exists to preserve.flags.collapsedflip, so each readsnode.sizewhile it still measures the card the node is leaving.this.constructor === LGraphNode(mirrorsserialize()'s own placeholder test) andlast_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.collapsedguard is gone — the flag now selects the direction instead of gating the write.LGraph.configure()assignslast_serializationby reference straight out of the caller'sdata.nodes(LGraph.ts:2626; the dedup clone atLGraph.ts:2599only 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.missing-node placeholder serializationblock inLGraphNode.test.ts.Review Focus
collapse(). It is the whole change. Canvas mode never overwritesnode.sizeon collapse, so there the restore rewrites the values it just recorded and is a no-op; the asymmetry only exists under Vue nodes.sizesetter (this.size = recordedSerialization.size) rather thansetSize(): the setter copies element-wise (no aliasing withlast_serialization), guardslength < 2, and mirrors into the layout store, but does not fireonResize. Since the restore happens while the node is still flagged collapsed,LGraphNode.vue'shandleLayoutChangebails out and the DOM is restored by the existingisCollapsedwatch's--node-width-xstash instead.last_serialization. No reader caches the object identity —useNodeReplacement.ts:178,missingNodeScan.ts:28,useErrorClearingHooks.ts:236and 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.useNodeReplacement.tsusesnode.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.Sweep
The hook is in
collapse()only, on the premise that nothing else flips the flag. Sweepingflags.collapsed =acrosssrc/,browser_tests/andapps/: 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 inLGraphCanvas.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
keeps the recorded size while collapsedtest to still setflags.collapseddirectly, and said to leave it untouched. fix: preserve a live title, flags and colour when serializing missing-node placeholders (BE-8848) #15719 had already rewritten it to callcollapse(true). I left it untouched either way and it still passes — at collapse time the placeholder's live size is the recorded[140, 60], so the refresh is a no-op there — but it no longer documents the direct-flag edge the plan described. I did not add a replacement test for that edge, since no product code reaches it (see Sweep).last_serializationin place by citingapp.tsas precedent. That citation was wrong: the objectsapp.ts:2222andapp.ts:2270mutate are constructed atapp.ts:2195, so that code owns them. Nothing established the pattern for a borrowed object, hence the replace.matt/be-8848-placeholder-decorationsrather thanmainbecause the added tests assertflags.collapsedin the output (fix: preserve a live title, flags and colour when serializing missing-node placeholders (BE-8848) #15719) and reuse thecreatePlaceholderhelper (fix: preserve a live resize when serializing missing-node placeholders #15713). The production change itself also composes with today'smain, whereserialize()unconditionally replayslast_serialization.size.Unexercised
useLayoutSync.tsandLGraphNode.vuerather 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.Provenance
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 --checkand lint on both changed files — clean. Red/green checked on all three new tests: deleting the restore-on-expand line fails 2 of them withexpected [ 80, 30 ] to deeply equal [ 777, 666 ], and reverting the replace to an in-place mutation fails the third.