Skip to content

fix: preserve a live title, flags and colour when serializing missing-node placeholders (BE-8848) - #15719

Open
mattmillerai wants to merge 2 commits into
matt/fe-1817-serialize-placeholder-sizefrom
matt/be-8848-placeholder-decorations
Open

fix: preserve a live title, flags and colour when serializing missing-node placeholders (BE-8848)#15719
mattmillerai wants to merge 2 commits into
matt/fe-1817-serialize-placeholder-sizefrom
matt/be-8848-placeholder-decorations

Conversation

@mattmillerai

@mattmillerai mattmillerai commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

STACKED — merging lands on matt/fe-1817-serialize-placeholder-size (owned by @mattmillerai, PR #15713), NOT main. Review/merge #15713 first; this PR's diff is only the commit on top.

ELI-5

If ComfyUI can't find a node's code, it draws a grey stand-in and remembers the node's original text so nothing is lost when you install the missing pack later. You could rename that stand-in, colour it, reshape it, collapse it or pin it — and every one of those edits vanished the next time you opened the workflow, because saving replayed the remembered text verbatim. Now your edits stick, and a stand-in you never touched still saves what it loaded.

Summary

serialize()'s missing-node placeholder branch replays last_serialization and overrides only mode/pos (and, on the branch this stacks on, size). This extends that override to every user-decoration field — title, flags, color, bgcolor, boxcolor, shape — so a rename, recolour, reshape, collapse or pin on a placeholder survives save + reload.

Changes

  • What: In the placeholder branch of LGraphNode.serialize(), the live node now wins for title, flags, color, bgcolor, boxcolor and shape. configure() copies those from the file onto the live node at load, so they can only diverge from the file when the user deliberately edited them — live-wins can never lose information the file had.
  • How: the mirrored keys are omitted from the replayed serialization and the live values spread back over it, so "a cleared decoration cannot resurrect the file's recorded value" holds for every key by construction instead of one delete branch at a time. title reuses the normal path's this.title !== this.constructor.title guard so a file with no title does not gain one on round-trip.
  • flags is always emitted. zComfyNode.flags is required and zFlags is a non-nullable object, so round-tripping the absence of flags produced a node the workflow schema rejects — a validation alert, and tryFixLinks repair skipped on the next open. {} faithfully represents "no flags" and is schema-valid. o.flags is cloneObject(this.flags), so pinned and collapsed ride along and the replayed object no longer aliases last_serialization.flags.
  • Not changed: type, inputs, outputs, properties, widgets_values, widgets_values_named still replay from the file — no node definition exists to regenerate them. The widgets_values index asymmetry is separately tracked (Widget values shift by one slot on reload when an earlier widget has serialize === false #15669 / PR fix: restore widgets_values by full widget index (#15669) #15688) and is not bundled here.
  • Breaking: none for consumers. The one serialization difference for an untouched placeholder is that a file which recorded no flags key now round-trips as "flags": {} — deliberate, and the reason LGraph.test.ts.snap's v0.4 fixture gains that key.

Review Focus

Chesterton's-fence check on flags. This flips a field that previously always replayed from the file, so I read the history first. bdf6d4dea2 ("Allow updating position and mode on missing nodes", #6792, AustinMroz) introduced the replay to keep the "last known good" serialization so the node still works once the missing pack is installed, and added pos/mode as the "small and safe changes" that should not be discarded. The decoration fields are the same class — pure decoration that cannot affect whether the node resolves when the pack is reinstalled — so extending the same override is consistent with that intent rather than against it.

The flags-absence round-trip was dropped after review. An earlier revision emitted flags only when the file had one or the live node had a flag to record, to keep the v0.4 fixture snapshot byte-identical. That is the wrong trade: flags is schema-required, so preserving the absence preserves an invalid node. The snapshot now records "flags": {} and the placeholder emits a document the schema accepts.

shape and boxcolor were folded in after review. They were initially scoped out as a separate call about decoration-vs-geometry. They are serialized by the normal path and user-mutable on a placeholder — useNodeCustomization.applyShape (src/composables/graph/useNodeCustomization.ts:113) and LGraphCanvas.onMenuNodeShapes both act on any selected LGraphNode with no has_errors gate — so leaving them out kept the exact data-loss class this PR closes. They are decoration by the same Chesterton's-fence argument as the rest.

Known adjacent gap, not fixed here (follow-up proposed). last_serialization is a load-time snapshot that is never refreshed, and two consumers read it directly rather than through serialize(): useNodeReplacement.replaceWithMapping (src/platform/nodeReplacement/useNodeReplacement.ts:177) applies serialized.title from it, so a renamed placeholder reverts to the file's title once the missing pack is installed; and resizing an expanded placeholder then collapsing it serializes the stale recorded size. Both need a refresh policy that interacts with #15713's deliberate collapsed-size rule, so they are tracked separately rather than bundled.

Provenance

  • Authored by: agent-work loop
  • Verified: pnpm vitest run src/lib/litegraph src/platform src/scripts src/composables — 537 files, 8259 passed, 7 expected-fail, 5 skipped, 0 failed; pnpm typecheck — clean; pnpm lint — 0 errors (3 pre-existing warnings in unrelated files); pnpm format — no changes outside the two edited files. LGraph.test.ts.snap intentionally updated (v0.4 fixture gains "flags": {}); no other snapshot moved.
  • Deviations: Two decisions from the first revision were reversed on review evidence rather than carried forward — the flags-absence guard (it preserved a schema-invalid document) and the exclusion of shape/boxcolor (they are user-mutable on placeholders, so excluding them left the bug open). No test asserts clearing shape, because LGraphNodeProperties instruments shape as an own data property that shadows the prototype accessor, so node.shape = 'default' stores the raw string instead of undefined; that is a pre-existing repo-wide quirk of the legacy string API, and the Vue path (useNodeCustomization) only ever assigns RenderShape enum values, which are all truthy. No screenshots — this is a serialization change with no visual surface.

…-node placeholders

serialize() replays last_serialization for a missing-node placeholder and
previously overrode only mode, pos and size, so a rename, recolour, collapse
or pin on a placeholder was discarded on reload. configure() copies the
file title/flags/color/bgcolor onto the live node at load, so live-wins on
these four fields can only ever carry a deliberate user edit.

Unset decorations are deleted rather than conditionally spread, so clearing
a colour cannot resurrect the one the file recorded, and the flags key is
only emitted when the file had one or the live node has something to say,
which keeps an untouched round-trip byte-identical.
@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 08:30
@coderabbitai

coderabbitai Bot commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Important

Approval pending

CodeRabbit has no unresolved comments, but it has not reviewed the latest commit.

Use the checkbox below to review the latest commit. CodeRabbit will approve the changes if it finds no blocking issues.

  • 🔍 Trigger review

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

@dosubot dosubot Bot added the size:M This PR changes 30-99 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, 09:00:54 AM UTC

Links

🎭 Playwright: ✅ 1983 passed, 0 failed · 1 flaky

📊 Browser Reports
  • chromium: View Report (✅ 1962 / ❌ 0 / ⚠️ 1 / ⏭️ 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 · 76.9 MB heap
canvas-mouse-sweep: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 74.6 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 · 59.9 MB heap
large-graph-pan: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 69.1 MB heap
large-graph-zoom: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 69.0 MB heap
legacy-node-drag: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 64.0 MB heap
minimap-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 69.0 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.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 62.1 MB heap
subgraph-mouse-sweep: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 64.8 MB heap
subgraph-transition-enter: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 143ms TBT · 83.5 MB heap
viewport-pan-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 69.1 MB heap
vue-large-graph-idle: · 55.4 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 171.2 MB heap
vue-large-graph-pan: · 56.2 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 151ms TBT · 171.6 MB heap
workflow-execution: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 62.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 8ms
canvas-idle: layout count 0
canvas-idle: style recalc count 8
canvas-idle: task duration 595ms
canvas-idle: script duration 10ms
canvas-idle: TBT 0ms
canvas-idle: heap used 76.9 MB
canvas-idle: DOM nodes -284
canvas-idle: event listeners -152
canvas-mouse-sweep: avg frame time 17ms
canvas-mouse-sweep: p95 frame time 17ms
canvas-mouse-sweep: layout duration 4ms
canvas-mouse-sweep: style recalc duration 42ms
canvas-mouse-sweep: layout count 12
canvas-mouse-sweep: style recalc count 76
canvas-mouse-sweep: task duration 937ms
canvas-mouse-sweep: script duration 116ms
canvas-mouse-sweep: TBT 0ms
canvas-mouse-sweep: heap used 74.6 MB
canvas-mouse-sweep: DOM nodes -281
canvas-mouse-sweep: event listeners -151
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 18ms
canvas-zoom-sweep: layout count 6
canvas-zoom-sweep: style recalc count 32
canvas-zoom-sweep: task duration 397ms
canvas-zoom-sweep: script duration 12ms
canvas-zoom-sweep: TBT 0ms
canvas-zoom-sweep: heap used 61.9 MB
canvas-zoom-sweep: DOM nodes 77
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 11
dom-widget-clipping: task duration 372ms
dom-widget-clipping: script duration 59ms
dom-widget-clipping: TBT 0ms
dom-widget-clipping: heap used 69.9 MB
dom-widget-clipping: DOM nodes 18
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 8ms
large-graph-idle: layout count 0
large-graph-idle: style recalc count 9
large-graph-idle: task duration 678ms
large-graph-idle: script duration 17ms
large-graph-idle: TBT 0ms
large-graph-idle: heap used 59.9 MB
large-graph-idle: DOM nodes -281
large-graph-idle: event listeners -181
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 69
large-graph-pan: task duration 1301ms
large-graph-pan: script duration 352ms
large-graph-pan: TBT 0ms
large-graph-pan: heap used 69.1 MB
large-graph-pan: DOM nodes -274
large-graph-pan: event listeners -150
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 15ms
large-graph-zoom: layout count 60
large-graph-zoom: style recalc count 65
large-graph-zoom: task duration 1463ms
large-graph-zoom: script duration 401ms
large-graph-zoom: TBT 0ms
large-graph-zoom: heap used 69.0 MB
large-graph-zoom: DOM nodes -287
large-graph-zoom: event listeners -183
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 10ms
legacy-node-drag: layout count 0
legacy-node-drag: style recalc count 46
legacy-node-drag: task duration 1524ms
legacy-node-drag: script duration 481ms
legacy-node-drag: TBT 0ms
legacy-node-drag: heap used 64.0 MB
legacy-node-drag: DOM nodes -118
legacy-node-drag: event listeners 109
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 699ms
minimap-idle: script duration 17ms
minimap-idle: TBT 0ms
minimap-idle: heap used 69.0 MB
minimap-idle: DOM nodes -276
minimap-idle: event listeners -151
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 10ms
subgraph-dom-widget-clipping: layout count 0
subgraph-dom-widget-clipping: style recalc count 46
subgraph-dom-widget-clipping: task duration 400ms
subgraph-dom-widget-clipping: script duration 117ms
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 8ms
subgraph-idle: layout count 0
subgraph-idle: style recalc count 8
subgraph-idle: task duration 555ms
subgraph-idle: script duration 9ms
subgraph-idle: TBT 0ms
subgraph-idle: heap used 62.1 MB
subgraph-idle: DOM nodes -283
subgraph-idle: event listeners -181
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 36ms
subgraph-mouse-sweep: layout count 16
subgraph-mouse-sweep: style recalc count 75
subgraph-mouse-sweep: task duration 834ms
subgraph-mouse-sweep: script duration 91ms
subgraph-mouse-sweep: TBT 0ms
subgraph-mouse-sweep: heap used 64.8 MB
subgraph-mouse-sweep: DOM nodes -280
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 32ms
subgraph-transition-enter: layout count 14
subgraph-transition-enter: style recalc count 19
subgraph-transition-enter: task duration 994ms
subgraph-transition-enter: script duration 18ms
subgraph-transition-enter: TBT 143ms
subgraph-transition-enter: heap used 83.5 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 42ms
viewport-pan-sweep: layout count 0
viewport-pan-sweep: style recalc count 251
viewport-pan-sweep: task duration 4500ms
viewport-pan-sweep: script duration 1075ms
viewport-pan-sweep: TBT 0ms
viewport-pan-sweep: heap used 69.1 MB
viewport-pan-sweep: DOM nodes -277
viewport-pan-sweep: event listeners -134
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 17263ms
vue-large-graph-idle: script duration 122ms
vue-large-graph-idle: TBT 0ms
vue-large-graph-idle: heap used 171.2 MB
vue-large-graph-idle: DOM nodes -8312
vue-large-graph-idle: event listeners -16390
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 20ms
vue-large-graph-pan: layout count 0
vue-large-graph-pan: style recalc count 168
vue-large-graph-pan: task duration 20852ms
vue-large-graph-pan: script duration 451ms
vue-large-graph-pan: TBT 151ms
vue-large-graph-pan: heap used 171.6 MB
vue-large-graph-pan: DOM nodes -8312
vue-large-graph-pan: event listeners -16386
workflow-execution: avg frame time 17ms
workflow-execution: p95 frame time 17ms
workflow-execution: layout duration 1ms
workflow-execution: style recalc duration 15ms
workflow-execution: layout count 3
workflow-execution: style recalc count 7
workflow-execution: task duration 80ms
workflow-execution: script duration 7ms
workflow-execution: TBT 0ms
workflow-execution: heap used 62.0 MB
workflow-execution: DOM nodes 120
workflow-execution: event listeners 37
Raw data
{
  "timestamp": "2026-08-23T09:12:22.967Z",
  "gitSha": "900be043af14df76d97b4219881ef855a066dac4",
  "branch": "matt/be-8848-placeholder-decorations",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2107.8239999999937,
      "styleRecalcs": 6,
      "styleRecalcDurationMs": 6.058999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 644.0010000000001,
      "heapDeltaBytes": 20032064,
      "heapUsedBytes": 81708312,
      "domNodes": -286,
      "jsHeapTotalBytes": 4448256,
      "scriptDurationMs": 10.413000000000002,
      "eventListeners": -153,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-idle",
      "durationMs": 2053.9209999999457,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 9.011999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 545.49,
      "heapDeltaBytes": 17192312,
      "heapUsedBytes": 79590352,
      "domNodes": -282,
      "jsHeapTotalBytes": 4710400,
      "scriptDurationMs": 9.681999999999999,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1962.9620000000045,
      "styleRecalcs": 75,
      "styleRecalcDurationMs": 42.135,
      "layouts": 12,
      "layoutDurationMs": 3.701,
      "taskDurationMs": 942.4330000000001,
      "heapDeltaBytes": 15931648,
      "heapUsedBytes": 78241780,
      "domNodes": -281,
      "jsHeapTotalBytes": 5496832,
      "scriptDurationMs": 115.267,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1953.538999999978,
      "styleRecalcs": 76,
      "styleRecalcDurationMs": 42.729,
      "layouts": 12,
      "layoutDurationMs": 3.7820000000000005,
      "taskDurationMs": 932.279,
      "heapDeltaBytes": 15837744,
      "heapUsedBytes": 78248628,
      "domNodes": -281,
      "jsHeapTotalBytes": 5234688,
      "scriptDurationMs": 116.02100000000002,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1713.2689999999684,
      "styleRecalcs": 31,
      "styleRecalcDurationMs": 17.315,
      "layouts": 6,
      "layoutDurationMs": 0.7599999999999998,
      "taskDurationMs": 388.509,
      "heapDeltaBytes": 3100072,
      "heapUsedBytes": 64702416,
      "domNodes": 76,
      "jsHeapTotalBytes": 5242880,
      "scriptDurationMs": 12.297,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1737.1090000000322,
      "styleRecalcs": 32,
      "styleRecalcDurationMs": 18.993,
      "layouts": 6,
      "layoutDurationMs": 0.6759999999999999,
      "taskDurationMs": 404.566,
      "heapDeltaBytes": 3016288,
      "heapUsedBytes": 65075860,
      "domNodes": 78,
      "jsHeapTotalBytes": 5242880,
      "scriptDurationMs": 12.073,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 583.0829999999878,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 7.3919999999999995,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 366.05199999999996,
      "heapDeltaBytes": 10928608,
      "heapUsedBytes": 73247940,
      "domNodes": 18,
      "jsHeapTotalBytes": 5242880,
      "scriptDurationMs": 57.66,
      "eventListeners": 0,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 586.4039999999022,
      "styleRecalcs": 11,
      "styleRecalcDurationMs": 7.955,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 378.799,
      "heapDeltaBytes": 10833544,
      "heapUsedBytes": 73239392,
      "domNodes": 18,
      "jsHeapTotalBytes": 5505024,
      "scriptDurationMs": 61.163,
      "eventListeners": 2,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2055.3170000000023,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 8.944,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 696.7570000000001,
      "heapDeltaBytes": -13765064,
      "heapUsedBytes": 61927412,
      "domNodes": -281,
      "jsHeapTotalBytes": 2879488,
      "scriptDurationMs": 16.385000000000005,
      "eventListeners": -181,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2039.0879999999925,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 7.408000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 659.009,
      "heapDeltaBytes": -13038252,
      "heapUsedBytes": 63737004,
      "domNodes": -281,
      "jsHeapTotalBytes": 3403776,
      "scriptDurationMs": 17.862999999999996,
      "eventListeners": -181,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2347.46100000001,
      "styleRecalcs": 70,
      "styleRecalcDurationMs": 15.867000000000003,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1286.1250000000002,
      "heapDeltaBytes": -5359692,
      "heapUsedBytes": 72244624,
      "domNodes": -273,
      "jsHeapTotalBytes": 483328,
      "scriptDurationMs": 338.34299999999996,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2454.4329999999945,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 13.701,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1315.115,
      "heapDeltaBytes": -5041540,
      "heapUsedBytes": 72698008,
      "domNodes": -275,
      "jsHeapTotalBytes": -1875968,
      "scriptDurationMs": 365.94599999999997,
      "eventListeners": -149,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3314.130999999975,
      "styleRecalcs": 65,
      "styleRecalcDurationMs": 15.403,
      "layouts": 60,
      "layoutDurationMs": 7.739999999999999,
      "taskDurationMs": 1475.249,
      "heapDeltaBytes": -4023316,
      "heapUsedBytes": 73673204,
      "domNodes": -286,
      "jsHeapTotalBytes": 4976640,
      "scriptDurationMs": 400.241,
      "eventListeners": -185,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3264.33099999997,
      "styleRecalcs": 65,
      "styleRecalcDurationMs": 15.404000000000002,
      "layouts": 60,
      "layoutDurationMs": 7.960999999999999,
      "taskDurationMs": 1451.52,
      "heapDeltaBytes": -7174588,
      "heapUsedBytes": 71073732,
      "domNodes": -287,
      "jsHeapTotalBytes": 4976640,
      "scriptDurationMs": 402.74199999999996,
      "eventListeners": -181,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "legacy-node-drag",
      "durationMs": 2479.575000000011,
      "styleRecalcs": 46,
      "styleRecalcDurationMs": 10.639999999999997,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1470.8239999999998,
      "heapDeltaBytes": -14937088,
      "heapUsedBytes": 69987100,
      "domNodes": 12,
      "jsHeapTotalBytes": 8163328,
      "scriptDurationMs": 470.639,
      "eventListeners": 186,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "legacy-node-drag",
      "durationMs": 2556.1609999999746,
      "styleRecalcs": 45,
      "styleRecalcDurationMs": 10.293000000000003,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1577.9750000000001,
      "heapDeltaBytes": -20541804,
      "heapUsedBytes": 64294308,
      "domNodes": -247,
      "jsHeapTotalBytes": 8122368,
      "scriptDurationMs": 490.721,
      "eventListeners": 31,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66999999999998,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "minimap-idle",
      "durationMs": 2042.8009999999972,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 8.300000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 700.2120000000001,
      "heapDeltaBytes": -12676564,
      "heapUsedBytes": 70298712,
      "domNodes": -280,
      "jsHeapTotalBytes": 4190208,
      "scriptDurationMs": 16.732,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "minimap-idle",
      "durationMs": 2045.2420000000302,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 8.302,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 697.9530000000001,
      "heapDeltaBytes": -8699988,
      "heapUsedBytes": 74301820,
      "domNodes": -271,
      "jsHeapTotalBytes": 4976640,
      "scriptDurationMs": 18.132,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 596.2600000000293,
      "styleRecalcs": 46,
      "styleRecalcDurationMs": 10.912,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 400.96,
      "heapDeltaBytes": 11709476,
      "heapUsedBytes": 74437944,
      "domNodes": 18,
      "jsHeapTotalBytes": 5242880,
      "scriptDurationMs": 117.137,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.670000000000012,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 589.6389999999201,
      "styleRecalcs": 46,
      "styleRecalcDurationMs": 10,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 399.938,
      "heapDeltaBytes": 11476160,
      "heapUsedBytes": 74041420,
      "domNodes": 18,
      "jsHeapTotalBytes": 5505024,
      "scriptDurationMs": 116.15,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2017.9469999999924,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 7.821999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 547.134,
      "heapDeltaBytes": 1332172,
      "heapUsedBytes": 63674472,
      "domNodes": -283,
      "jsHeapTotalBytes": 4448256,
      "scriptDurationMs": 8.850999999999999,
      "eventListeners": -181,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.699999999999818
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2055.024000000003,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 8.394,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 562.42,
      "heapDeltaBytes": 3793216,
      "heapUsedBytes": 66476440,
      "domNodes": -282,
      "jsHeapTotalBytes": 4448256,
      "scriptDurationMs": 9.715000000000002,
      "eventListeners": -181,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1717.6860000000147,
      "styleRecalcs": 75,
      "styleRecalcDurationMs": 34.894999999999996,
      "layouts": 16,
      "layoutDurationMs": 3.927999999999999,
      "taskDurationMs": 821.712,
      "heapDeltaBytes": -5919416,
      "heapUsedBytes": 56192632,
      "domNodes": -280,
      "jsHeapTotalBytes": 5496832,
      "scriptDurationMs": 86.699,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1707.1559999999408,
      "styleRecalcs": 75,
      "styleRecalcDurationMs": 37.606,
      "layouts": 16,
      "layoutDurationMs": 4.784999999999999,
      "taskDurationMs": 847.2109999999999,
      "heapDeltaBytes": 16995180,
      "heapUsedBytes": 79697816,
      "domNodes": -280,
      "jsHeapTotalBytes": 4186112,
      "scriptDurationMs": 95.249,
      "eventListeners": -151,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "subgraph-transition-enter",
      "durationMs": 1402.1549999999934,
      "styleRecalcs": 19,
      "styleRecalcDurationMs": 32.074000000000005,
      "layouts": 14,
      "layoutDurationMs": 14.977,
      "taskDurationMs": 994.363,
      "heapDeltaBytes": -6078124,
      "heapUsedBytes": 87591172,
      "domNodes": 13673,
      "jsHeapTotalBytes": 12582912,
      "scriptDurationMs": 18.334999999999997,
      "eventListeners": 2373,
      "totalBlockingTimeMs": 143,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8515.938000000006,
      "styleRecalcs": 250,
      "styleRecalcDurationMs": 39.313,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 4439.2300000000005,
      "heapDeltaBytes": -2225492,
      "heapUsedBytes": 74627404,
      "domNodes": -274,
      "jsHeapTotalBytes": 483328,
      "scriptDurationMs": 1072.394,
      "eventListeners": -135,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8729.765000000043,
      "styleRecalcs": 251,
      "styleRecalcDurationMs": 44.782,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 4561.591,
      "heapDeltaBytes": -6414516,
      "heapUsedBytes": 70332368,
      "domNodes": -279,
      "jsHeapTotalBytes": -827392,
      "scriptDurationMs": 1078.13,
      "eventListeners": -133,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 18174.65100000004,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 17153.891000000003,
      "heapDeltaBytes": -34132488,
      "heapUsedBytes": 177993004,
      "domNodes": -8312,
      "jsHeapTotalBytes": -17608704,
      "scriptDurationMs": 122.44400000000002,
      "eventListeners": -16391,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.779999999999927,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 18270.99499999997,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 17371.301,
      "heapDeltaBytes": -49069680,
      "heapUsedBytes": 180948752,
      "domNodes": -8312,
      "jsHeapTotalBytes": -16588800,
      "scriptDurationMs": 121.43500000000002,
      "eventListeners": -16389,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 18.333333333333332,
      "p95FrameDurationMs": 16.80000000000291
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 21301.301999999963,
      "styleRecalcs": 165,
      "styleRecalcDurationMs": 20.24499999999996,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 20810.484,
      "heapDeltaBytes": -40208196,
      "heapUsedBytes": 186653772,
      "domNodes": -8312,
      "jsHeapTotalBytes": -17776640,
      "scriptDurationMs": 423.05800000000005,
      "eventListeners": -16385,
      "totalBlockingTimeMs": 189,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 21466.365999999995,
      "styleRecalcs": 171,
      "styleRecalcDurationMs": 20.538,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 20894.197,
      "heapDeltaBytes": -44971448,
      "heapUsedBytes": 173118976,
      "domNodes": -8312,
      "jsHeapTotalBytes": -23678976,
      "scriptDurationMs": 479.555,
      "eventListeners": -16387,
      "totalBlockingTimeMs": 112,
      "frameDurationMs": 17.780000000000047,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "workflow-execution",
      "durationMs": 115.33399999996163,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 15.205,
      "layouts": 2,
      "layoutDurationMs": 1.272,
      "taskDurationMs": 83.74300000000001,
      "heapDeltaBytes": 3107076,
      "heapUsedBytes": 65551300,
      "domNodes": 129,
      "jsHeapTotalBytes": 262144,
      "scriptDurationMs": 7.353999999999999,
      "eventListeners": 25,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "workflow-execution",
      "durationMs": 113.5469999999259,
      "styleRecalcs": 7,
      "styleRecalcDurationMs": 14.134,
      "layouts": 3,
      "layoutDurationMs": 0.6119999999999999,
      "taskDurationMs": 76.472,
      "heapDeltaBytes": 3106912,
      "heapUsedBytes": 64554876,
      "domNodes": 110,
      "jsHeapTotalBytes": 0,
      "scriptDurationMs": 5.966000000000001,
      "eventListeners": 49,
      "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

@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
🟡 Medium 2
🟢 Low 2

Panel: 8/8 reviewers contributed findings.

Comment thread src/lib/litegraph/src/LGraphNode.ts Outdated
Comment thread src/lib/litegraph/src/LGraphNode.ts Outdated
Comment thread src/lib/litegraph/src/LGraphNode.ts Outdated
Comment thread src/lib/litegraph/src/LGraphNode.ts Outdated
@codecov

codecov Bot commented Aug 23, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 83.33333% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/lib/litegraph/src/LGraphNode.ts 83.33% 0 Missing and 1 partial ⚠️
@@                             Coverage Diff                             @@
##           matt/fe-1817-serialize-placeholder-size   #15719      +/-   ##
===========================================================================
- Coverage                                    81.90%   81.89%   -0.02%     
===========================================================================
  Files                                         1888     1888              
  Lines                                       107284   107287       +3     
  Branches                                     29617    30087     +470     
===========================================================================
- Hits                                         87869    87858      -11     
- Misses                                       19077    19093      +16     
+ Partials                                       338      336       -2     
Flag Coverage Δ
e2e 67.41% <83.33%> (-0.03%) ⬇️
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.40% <83.33%> (+0.03%) ⬆️

... and 10 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.

Review follow-up on the placeholder decoration mirror.

shape and boxcolor are serialized by the normal path and are user-mutable on
a placeholder (useNodeCustomization.applyShape and LGraphCanvas
.onMenuNodeShapes act on any selected LGraphNode), so leaving them to replay
from the file dropped those edits on save - the same data loss this branch
sets out to fix. They now join title, color and bgcolor in the live mirror.

The five mirrored keys are omitted from the replayed serialization before the
live values are spread back over it, which replaces the per-key delete chain
and keeps "cleared decorations do not resurrect the recorded value" true for
every key by construction rather than one branch at a time.

flags is required by zComfyNode and zFlags is a non-nullable object, so the
previous absence round-trip emitted a node the workflow schema rejects, which
surfaces as a validation alert and skips tryFixLinks repair on the next open.
An empty object faithfully represents "no flags" and is schema-valid, so the
key is now always emitted; the v0.4 fixture snapshot gains "flags": {}
accordingly. Guarding this.flags with optional chaining also keeps the branch
consistent with the defensive reads elsewhere in the class.
@mattmillerai

Copy link
Copy Markdown
Contributor Author

🤖 The reviews loop filed Linear follow-up ticket(s) for review thread(s) deferred as out of scope for this PR:

  • BE-8849 — Refresh or bypass the stale last_serialization snapshot so live placeholder edits survive node replacement and collapse — filed as agent-spike (premise unverified)

The following carry agent-spike instead of agent-ok because their reachability claim was not backed by evidence (BE-5378) — the claim is investigated before any code is written, and "the premise does not hold" is a valid, successful outcome:

  • Refresh or bypass the stale last_serialization snapshot so live placeholder edits survive node replacement and collapse — no reachability block in the proposal

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