Skip to content

refactor: host-scoped promoted widget rows (no source traversal)#13434

Merged
DrJKL merged 13 commits into
feature/ecs-migrationfrom
drjkl/host-scoped-promoted-widgets
Jul 17, 2026
Merged

refactor: host-scoped promoted widget rows (no source traversal)#13434
DrJKL merged 13 commits into
feature/ecs-migrationfrom
drjkl/host-scoped-promoted-widgets

Conversation

@DrJKL

@DrJKL DrJKL commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Make right-side-panel promoted subgraph-input widget rows host-scoped, removing runtime graph traversal, per ADR0009.

Changes

  • What: SectionWidgets.vue and WidgetActions.vue no longer walk live links back to the interior source to identify promoted widgets. A promoted widget is host-scoped (widget.widgetId = graphId:hostNodeId:inputName, also carried on the host input slot), so reuse that:
    • isLinkedinputForWidget(node, widget)?.widgetId != null
    • isWidgetShownOnParents → match parent inputs by widget.widgetId
    • handleHideInput → drop the unreachable source-resolution branch (with the String(node.id)===String(parent.id) swap), keep demoteWidget
    • remove the now-unused widgetPromotedSource export
    • favorites keep their (nodeLocatorId, widgetName) key; added a TODO(ADR0009) to move to a pure WidgetId key (deferred — needs a persisted-format migration)
  • Breaking: none. resolvePromotedWidgetSource in clearWidgetErrors is left intact (ADR0009 permits source lookup for diagnostics).

Review Focus

Behavior equivalence of the three replaced predicates. The removed handleHideInput branch was unreachable: reaching it requires !isLinked, but whenever the old source resolved, isLinked was already true. The only divergences are broken/unresolvable interior links, which are strictly safer under the new logic. Net -55 lines, no new APIs.

Validated: pnpm typecheck, pnpm lint, pnpm knip clean; 127 subgraph/parameter unit tests pass.

Addendum: widget-value type consolidation

f48153d fixes null handling and starts collapsing the redundant value types. Current state and the path to one canonical type:

The problem. Five spellings of the same union, each able to skew independently:

Type Was Now
WidgetValue (simplifiedWidget.ts) string|number|boolean|object|undefined|null|void|File[] canonical: string|number|boolean|object|undefined|null
TWidgetValue (litegraph widgets.ts) IWidget['value'] (rejected null) = WidgetValue alias
WidgetState['value'] (widgetState.ts) unknown defaults to WidgetValue
NodeProperty (LGraphNode.ts) no null (runtime stored null anyway) |null added
IBaseWidget.callback value param unknown WidgetValue

The old unknown in the store forced isWidgetValue(x) ? x : undefined laundering at 6 call sites; 4 are now deleted. The guard survives only where it does real work: untrusted workflow JSON (proxyWidgetMigration.pickHostValue) and the serialize path (SubgraphNode.serialize).

Remaining steps (follow-up PRs):

  1. Codemod TWidgetValue -> WidgetValue (~20 imports incl. serialisation.ts, litegraph-augmentation.d.ts, panel callbacks), delete the alias. Zero behavior change.
  2. NodeProperty = Exclude<WidgetValue, undefined> - derives the one legitimate variant (properties use undefined as "absent") instead of a parallel hand-written union that can drift.
  3. Move WidgetValue to a leaf module (litegraph currently imports it from app-layer @/types/simplifiedWidget - inverted layering).
  4. Rename the shadowing generic migrateWidgetsValues<TWidgetValue> in litegraphUtil.ts to <T>.
  5. (Riskier, last) Narrow the bare object members - IChartWidget.value: object, ICustomWidget.value: string\|object - to Record<string, unknown>. This is the only change that makes the union meaningfully narrower than "anything non-callable"; needs an ecosystem deprecation window since custom is the extension escape hatch.

Invariant to keep: null and undefined are distinct on purpose. undefined = hole/absent in widgets_values arrays and property-presence checks; null = a real stored value. Regression tests in promotionUtils.test.ts and LGraphNode.test.ts pin this.

@dosubot dosubot Bot added the size:M This PR changes 30-99 lines, ignoring generated files. label Jul 3, 2026
@coderabbitai

coderabbitai Bot commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

Important

Pre-merge checks failed

Please resolve all errors before merging. Addressing warnings is optional.

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
End-To-End Regression Coverage For Fixes ❓ Inconclusive Commit subjects/title are missing, so I can’t verify the required bug-fix signal from PR metadata alone. Provide the PR title and commit subjects, then re-evaluate whether a browser_tests regression is required.
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Adr Compliance For Entity/Litegraph Changes ✅ Passed Diff touches graph-widget code but adds no ADR 0003/0008 patterns: no new imperative entity mutations, no new entity responsibilities, and no extension-facing callback/serialize/_version changes.
Title check ✅ Passed The title is concise and accurately summarizes the main refactor to host-scoped promoted widget rows without source traversal.
Description check ✅ Passed The description covers Summary, Changes, Breaking, and Review Focus, matching the template and explaining the main design decisions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch drjkl/host-scoped-promoted-widgets

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

@github-actions

github-actions Bot commented Jul 3, 2026

Copy link
Copy Markdown

🎨 Storybook: ✅ Built — View Storybook

Details

⏰ Completed at: 07/17/2026, 09:58:20 PM UTC

Links

🎭 Playwright: ✅ 1691 passed, 0 failed · 2 flaky

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

📦 Bundle Size

⏳ Size data collection in progress…

⚡ Performance Report

canvas-idle: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 60.3 MB heap
canvas-mouse-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 52.9 MB heap
canvas-zoom-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 58.0 MB heap
dom-widget-clipping: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 46.8 MB heap
large-graph-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 60.3 MB heap
large-graph-pan: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 54.8 MB heap
large-graph-zoom: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 68.8 MB heap
minimap-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 63.7 MB heap
subgraph-dom-widget-clipping: · 60.0 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 0ms TBT · 45.3 MB heap
subgraph-idle: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 59.0 MB heap
subgraph-mouse-sweep: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 50.7 MB heap
subgraph-transition-enter: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 153ms TBT · 98.8 MB heap
viewport-pan-sweep: · 60.0 avg FPS · 59.9 P5 FPS ✅ (target: ≥52) · 0ms TBT · 68.4 MB heap
vue-large-graph-idle: · 56.3 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 165.8 MB heap
vue-large-graph-pan: · 56.2 avg FPS · 59.5 P5 FPS ✅ (target: ≥52) · 56ms TBT · 162.5 MB heap
workflow-execution: · 60.0 avg FPS · 59.7 P5 FPS ✅ (target: ≥52) · 0ms TBT · 55.6 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 11ms
canvas-idle: layout count 0
canvas-idle: style recalc count 9
canvas-idle: task duration 503ms
canvas-idle: script duration 28ms
canvas-idle: TBT 0ms
canvas-idle: heap used 60.3 MB
canvas-idle: DOM nodes -283
canvas-idle: event listeners -198
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 44ms
canvas-mouse-sweep: layout count 12
canvas-mouse-sweep: style recalc count 78
canvas-mouse-sweep: task duration 844ms
canvas-mouse-sweep: script duration 126ms
canvas-mouse-sweep: TBT 0ms
canvas-mouse-sweep: heap used 52.9 MB
canvas-mouse-sweep: DOM nodes -90
canvas-mouse-sweep: event listeners -98
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 31
canvas-zoom-sweep: task duration 376ms
canvas-zoom-sweep: script duration 31ms
canvas-zoom-sweep: TBT 0ms
canvas-zoom-sweep: heap used 58.0 MB
canvas-zoom-sweep: DOM nodes -80
canvas-zoom-sweep: event listeners -83
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 9ms
dom-widget-clipping: layout count 0
dom-widget-clipping: style recalc count 11
dom-widget-clipping: task duration 387ms
dom-widget-clipping: script duration 59ms
dom-widget-clipping: TBT 0ms
dom-widget-clipping: heap used 46.8 MB
dom-widget-clipping: DOM nodes -271
dom-widget-clipping: event listeners -203
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 9ms
large-graph-idle: layout count 0
large-graph-idle: style recalc count 10
large-graph-idle: task duration 706ms
large-graph-idle: script duration 239ms
large-graph-idle: TBT 0ms
large-graph-idle: heap used 60.3 MB
large-graph-idle: DOM nodes 19
large-graph-idle: event listeners 5
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 19ms
large-graph-pan: layout count 0
large-graph-pan: style recalc count 69
large-graph-pan: task duration 1337ms
large-graph-pan: script duration 610ms
large-graph-pan: TBT 0ms
large-graph-pan: heap used 54.8 MB
large-graph-pan: DOM nodes -272
large-graph-pan: event listeners -197
large-graph-zoom: avg frame time 17ms
large-graph-zoom: p95 frame time 17ms
large-graph-zoom: layout duration 9ms
large-graph-zoom: style recalc duration 19ms
large-graph-zoom: layout count 60
large-graph-zoom: style recalc count 64
large-graph-zoom: task duration 1625ms
large-graph-zoom: script duration 784ms
large-graph-zoom: TBT 0ms
large-graph-zoom: heap used 68.8 MB
large-graph-zoom: DOM nodes -135
large-graph-zoom: event listeners 8
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 704ms
minimap-idle: script duration 239ms
minimap-idle: TBT 0ms
minimap-idle: heap used 63.7 MB
minimap-idle: DOM nodes 16
minimap-idle: event listeners 5
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 14ms
subgraph-dom-widget-clipping: layout count 0
subgraph-dom-widget-clipping: style recalc count 47
subgraph-dom-widget-clipping: task duration 429ms
subgraph-dom-widget-clipping: script duration 129ms
subgraph-dom-widget-clipping: TBT 0ms
subgraph-dom-widget-clipping: heap used 45.3 MB
subgraph-dom-widget-clipping: DOM nodes -271
subgraph-dom-widget-clipping: event listeners -197
subgraph-idle: avg frame time 17ms
subgraph-idle: p95 frame time 17ms
subgraph-idle: layout duration 0ms
subgraph-idle: style recalc duration 10ms
subgraph-idle: layout count 0
subgraph-idle: style recalc count 10
subgraph-idle: task duration 425ms
subgraph-idle: script duration 20ms
subgraph-idle: TBT 0ms
subgraph-idle: heap used 59.0 MB
subgraph-idle: DOM nodes -127
subgraph-idle: event listeners -98
subgraph-mouse-sweep: avg frame time 17ms
subgraph-mouse-sweep: p95 frame time 17ms
subgraph-mouse-sweep: layout duration 5ms
subgraph-mouse-sweep: style recalc duration 50ms
subgraph-mouse-sweep: layout count 16
subgraph-mouse-sweep: style recalc count 77
subgraph-mouse-sweep: task duration 772ms
subgraph-mouse-sweep: script duration 101ms
subgraph-mouse-sweep: TBT 0ms
subgraph-mouse-sweep: heap used 50.7 MB
subgraph-mouse-sweep: DOM nodes -232
subgraph-mouse-sweep: event listeners -199
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 33ms
subgraph-transition-enter: layout count 15
subgraph-transition-enter: style recalc count 19
subgraph-transition-enter: task duration 777ms
subgraph-transition-enter: script duration 26ms
subgraph-transition-enter: TBT 153ms
subgraph-transition-enter: heap used 98.8 MB
subgraph-transition-enter: DOM nodes 13673
subgraph-transition-enter: event listeners 2533
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 64ms
viewport-pan-sweep: layout count 0
viewport-pan-sweep: style recalc count 250
viewport-pan-sweep: task duration 4690ms
viewport-pan-sweep: script duration 1965ms
viewport-pan-sweep: TBT 0ms
viewport-pan-sweep: heap used 68.4 MB
viewport-pan-sweep: DOM nodes -126
viewport-pan-sweep: event listeners -81
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 14118ms
vue-large-graph-idle: script duration 957ms
vue-large-graph-idle: TBT 0ms
vue-large-graph-idle: heap used 165.8 MB
vue-large-graph-idle: DOM nodes -3300
vue-large-graph-idle: event listeners -16377
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 25ms
vue-large-graph-pan: layout count 0
vue-large-graph-pan: style recalc count 122
vue-large-graph-pan: task duration 17935ms
vue-large-graph-pan: script duration 1256ms
vue-large-graph-pan: TBT 56ms
vue-large-graph-pan: heap used 162.5 MB
vue-large-graph-pan: DOM nodes -3311
vue-large-graph-pan: event listeners -16371
workflow-execution: avg frame time 17ms
workflow-execution: p95 frame time 17ms
workflow-execution: layout duration 1ms
workflow-execution: style recalc duration 25ms
workflow-execution: layout count 5
workflow-execution: style recalc count 17
workflow-execution: task duration 172ms
workflow-execution: script duration 21ms
workflow-execution: TBT 0ms
workflow-execution: heap used 55.6 MB
workflow-execution: DOM nodes -5
workflow-execution: event listeners -32
Raw data
{
  "timestamp": "2026-07-17T22:07:51.487Z",
  "gitSha": "b2d08da4ec0b4d242f215b15aab7a034ab1eafd1",
  "branch": "drjkl/host-scoped-promoted-widgets",
  "measurements": [
    {
      "name": "canvas-idle",
      "durationMs": 2174.519000000004,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 9.602,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 541.516,
      "heapDeltaBytes": -6469508,
      "heapUsedBytes": 63284260,
      "domNodes": -287,
      "jsHeapTotalBytes": 18501632,
      "scriptDurationMs": 29.788999999999998,
      "eventListeners": -197,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-idle",
      "durationMs": 2066.6510000000358,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 12.264999999999999,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 463.6,
      "heapDeltaBytes": -6664024,
      "heapUsedBytes": 63217964,
      "domNodes": -278,
      "jsHeapTotalBytes": 19025920,
      "scriptDurationMs": 25.833,
      "eventListeners": -199,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1851.3169999999946,
      "styleRecalcs": 77,
      "styleRecalcDurationMs": 43.616,
      "layouts": 12,
      "layoutDurationMs": 3.908,
      "taskDurationMs": 830.38,
      "heapDeltaBytes": -5856156,
      "heapUsedBytes": 53462008,
      "domNodes": 59,
      "jsHeapTotalBytes": 26214400,
      "scriptDurationMs": 129.94,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-mouse-sweep",
      "durationMs": 1889.2380000000344,
      "styleRecalcs": 78,
      "styleRecalcDurationMs": 44.118,
      "layouts": 12,
      "layoutDurationMs": 3.8319999999999994,
      "taskDurationMs": 857.723,
      "heapDeltaBytes": -12195132,
      "heapUsedBytes": 57545304,
      "domNodes": -238,
      "jsHeapTotalBytes": 20336640,
      "scriptDurationMs": 122.56800000000001,
      "eventListeners": -199,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1766.194999999982,
      "styleRecalcs": 29,
      "styleRecalcDurationMs": 17.741,
      "layouts": 6,
      "layoutDurationMs": 0.8170000000000003,
      "taskDurationMs": 373.31399999999996,
      "heapDeltaBytes": 6937748,
      "heapUsedBytes": 59552632,
      "domNodes": -237,
      "jsHeapTotalBytes": 4321280,
      "scriptDurationMs": 28.387,
      "eventListeners": -184,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "canvas-zoom-sweep",
      "durationMs": 1724.1409999999178,
      "styleRecalcs": 32,
      "styleRecalcDurationMs": 19.115,
      "layouts": 6,
      "layoutDurationMs": 0.7240000000000001,
      "taskDurationMs": 379.41600000000005,
      "heapDeltaBytes": 2874920,
      "heapUsedBytes": 61994296,
      "domNodes": 78,
      "jsHeapTotalBytes": 25690112,
      "scriptDurationMs": 32.909,
      "eventListeners": 19,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 581.8840000000023,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 7.886000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 371.5590000000001,
      "heapDeltaBytes": -1294984,
      "heapUsedBytes": 50866768,
      "domNodes": -269,
      "jsHeapTotalBytes": -135168,
      "scriptDurationMs": 57.588,
      "eventListeners": -203,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "dom-widget-clipping",
      "durationMs": 604.5910000000276,
      "styleRecalcs": 12,
      "styleRecalcDurationMs": 9.429000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 402.468,
      "heapDeltaBytes": -22544756,
      "heapUsedBytes": 47365464,
      "domNodes": -272,
      "jsHeapTotalBytes": 5656576,
      "scriptDurationMs": 60.551,
      "eventListeners": -203,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2023.8709999999855,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 9.006000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 706.343,
      "heapDeltaBytes": -12990664,
      "heapUsedBytes": 62570448,
      "domNodes": 20,
      "jsHeapTotalBytes": 10629120,
      "scriptDurationMs": 236.31499999999997,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-idle",
      "durationMs": 2001.037999999994,
      "styleRecalcs": 9,
      "styleRecalcDurationMs": 8.863,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 706.313,
      "heapDeltaBytes": 8722488,
      "heapUsedBytes": 63935692,
      "domNodes": 18,
      "jsHeapTotalBytes": 4980736,
      "scriptDurationMs": 241.43300000000002,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2154.1599999999903,
      "styleRecalcs": 69,
      "styleRecalcDurationMs": 18.894000000000002,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1263.395,
      "heapDeltaBytes": -9318364,
      "heapUsedBytes": 55835988,
      "domNodes": -271,
      "jsHeapTotalBytes": -303104,
      "scriptDurationMs": 558.072,
      "eventListeners": -197,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-pan",
      "durationMs": 2156.7079999999805,
      "styleRecalcs": 68,
      "styleRecalcDurationMs": 18.469,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 1409.61,
      "heapDeltaBytes": -13203792,
      "heapUsedBytes": 59004276,
      "domNodes": -273,
      "jsHeapTotalBytes": 516096,
      "scriptDurationMs": 661.2800000000001,
      "eventListeners": -197,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3362.948000000017,
      "styleRecalcs": 63,
      "styleRecalcDurationMs": 17.979000000000003,
      "layouts": 60,
      "layoutDurationMs": 8.525,
      "taskDurationMs": 1628.0910000000001,
      "heapDeltaBytes": 28989424,
      "heapUsedBytes": 83628520,
      "domNodes": -281,
      "jsHeapTotalBytes": 262144,
      "scriptDurationMs": 791.1890000000001,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "large-graph-zoom",
      "durationMs": 3300.922000000014,
      "styleRecalcs": 65,
      "styleRecalcDurationMs": 20.77,
      "layouts": 60,
      "layoutDurationMs": 9.005,
      "taskDurationMs": 1621.052,
      "heapDeltaBytes": 1788588,
      "heapUsedBytes": 60694204,
      "domNodes": 12,
      "jsHeapTotalBytes": 8650752,
      "scriptDurationMs": 777.809,
      "eventListeners": 8,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "minimap-idle",
      "durationMs": 2010.0139999999556,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 8.129000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 697.9459999999999,
      "heapDeltaBytes": 8511852,
      "heapUsedBytes": 65802460,
      "domNodes": 16,
      "jsHeapTotalBytes": 4456448,
      "scriptDurationMs": 244.482,
      "eventListeners": 6,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "minimap-idle",
      "durationMs": 1995.025999999939,
      "styleRecalcs": 8,
      "styleRecalcDurationMs": 7.404000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 709.838,
      "heapDeltaBytes": 8213600,
      "heapUsedBytes": 67710360,
      "domNodes": 16,
      "jsHeapTotalBytes": 5505024,
      "scriptDurationMs": 233.833,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 612.542000000019,
      "styleRecalcs": 47,
      "styleRecalcDurationMs": 12.777000000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 420.45399999999995,
      "heapDeltaBytes": -22363384,
      "heapUsedBytes": 47257060,
      "domNodes": -270,
      "jsHeapTotalBytes": 6180864,
      "scriptDurationMs": 126.476,
      "eventListeners": -197,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.799999999999727
    },
    {
      "name": "subgraph-dom-widget-clipping",
      "durationMs": 608.6229999999659,
      "styleRecalcs": 47,
      "styleRecalcDurationMs": 14.765,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 437.832,
      "heapDeltaBytes": -22135492,
      "heapUsedBytes": 47719688,
      "domNodes": -272,
      "jsHeapTotalBytes": 5394432,
      "scriptDurationMs": 131.073,
      "eventListeners": -197,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2004.872000000006,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 9.415,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 394.026,
      "heapDeltaBytes": -61628,
      "heapUsedBytes": 61473292,
      "domNodes": 20,
      "jsHeapTotalBytes": 27000832,
      "scriptDurationMs": 19.517,
      "eventListeners": 4,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-idle",
      "durationMs": 2040.584000000024,
      "styleRecalcs": 10,
      "styleRecalcDurationMs": 10.634999999999998,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 456.805,
      "heapDeltaBytes": -7578356,
      "heapUsedBytes": 62202760,
      "domNodes": -273,
      "jsHeapTotalBytes": 20598784,
      "scriptDurationMs": 20.840999999999998,
      "eventListeners": -199,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1756.9569999999999,
      "styleRecalcs": 78,
      "styleRecalcDurationMs": 54.163999999999994,
      "layouts": 16,
      "layoutDurationMs": 4.5840000000000005,
      "taskDurationMs": 773.25,
      "heapDeltaBytes": -16867724,
      "heapUsedBytes": 53081236,
      "domNodes": -232,
      "jsHeapTotalBytes": 19550208,
      "scriptDurationMs": 102.419,
      "eventListeners": -199,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333335,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "subgraph-mouse-sweep",
      "durationMs": 1780.4179999999974,
      "styleRecalcs": 76,
      "styleRecalcDurationMs": 45.017,
      "layouts": 16,
      "layoutDurationMs": 4.8389999999999995,
      "taskDurationMs": 771.291,
      "heapDeltaBytes": -16748904,
      "heapUsedBytes": 53161992,
      "domNodes": -231,
      "jsHeapTotalBytes": 20336640,
      "scriptDurationMs": 99.798,
      "eventListeners": -199,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.800000000000182
    },
    {
      "name": "subgraph-transition-enter",
      "durationMs": 949.1709999999784,
      "styleRecalcs": 19,
      "styleRecalcDurationMs": 33.347,
      "layouts": 15,
      "layoutDurationMs": 14.944000000000003,
      "taskDurationMs": 776.7580000000003,
      "heapDeltaBytes": 27123596,
      "heapUsedBytes": 103598332,
      "domNodes": 13673,
      "jsHeapTotalBytes": 15204352,
      "scriptDurationMs": 26.289999999999996,
      "eventListeners": 2533,
      "totalBlockingTimeMs": 153,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8279.696999999998,
      "styleRecalcs": 250,
      "styleRecalcDurationMs": 64.95200000000001,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 4789.584000000001,
      "heapDeltaBytes": 9069276,
      "heapUsedBytes": 72514636,
      "domNodes": -270,
      "jsHeapTotalBytes": 6717440,
      "scriptDurationMs": 2040.628,
      "eventListeners": -181,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.66333333333332,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "viewport-pan-sweep",
      "durationMs": 8221.389999999928,
      "styleRecalcs": 250,
      "styleRecalcDurationMs": 62.48,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 4591.109,
      "heapDeltaBytes": 15196740,
      "heapUsedBytes": 70982428,
      "domNodes": 18,
      "jsHeapTotalBytes": 11710464,
      "scriptDurationMs": 1890.243,
      "eventListeners": 20,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 14101.883000000043,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 14079.673,
      "heapDeltaBytes": -10750616,
      "heapUsedBytes": 183878652,
      "domNodes": -3300,
      "jsHeapTotalBytes": 13602816,
      "scriptDurationMs": 937.2199999999999,
      "eventListeners": -16378,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.776666666666642,
      "p95FrameDurationMs": 16.700000000000728
    },
    {
      "name": "vue-large-graph-idle",
      "durationMs": 14174.10300000006,
      "styleRecalcs": 0,
      "styleRecalcDurationMs": 0,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 14157.243999999999,
      "heapDeltaBytes": -39889548,
      "heapUsedBytes": 163733320,
      "domNodes": -3300,
      "jsHeapTotalBytes": -2125824,
      "scriptDurationMs": 977.1739999999999,
      "eventListeners": -16376,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 17.776666666666763,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 18007.60399999996,
      "styleRecalcs": 124,
      "styleRecalcDurationMs": 25.231999999999978,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 17985.703999999998,
      "heapDeltaBytes": -34981592,
      "heapUsedBytes": 164278708,
      "domNodes": -3311,
      "jsHeapTotalBytes": 15962112,
      "scriptDurationMs": 1247.819,
      "eventListeners": -16373,
      "totalBlockingTimeMs": 45,
      "frameDurationMs": 17.223333333333358,
      "p95FrameDurationMs": 16.80000000000291
    },
    {
      "name": "vue-large-graph-pan",
      "durationMs": 17902.113999999983,
      "styleRecalcs": 120,
      "styleRecalcDurationMs": 25.283,
      "layouts": 0,
      "layoutDurationMs": 0,
      "taskDurationMs": 17883.846999999998,
      "heapDeltaBytes": -22920120,
      "heapUsedBytes": 176422496,
      "domNodes": -3311,
      "jsHeapTotalBytes": 19808256,
      "scriptDurationMs": 1263.9410000000003,
      "eventListeners": -16369,
      "totalBlockingTimeMs": 67,
      "frameDurationMs": 18.333333333333332,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "workflow-execution",
      "durationMs": 483.5260000000403,
      "styleRecalcs": 16,
      "styleRecalcDurationMs": 26.448999999999998,
      "layouts": 4,
      "layoutDurationMs": 1.354,
      "taskDurationMs": 225.852,
      "heapDeltaBytes": -22034468,
      "heapUsedBytes": 49025716,
      "domNodes": -167,
      "jsHeapTotalBytes": 4345856,
      "scriptDurationMs": 22.325999999999997,
      "eventListeners": -134,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.663333333333338,
      "p95FrameDurationMs": 16.799999999999272
    },
    {
      "name": "workflow-execution",
      "durationMs": 458.705000000009,
      "styleRecalcs": 17,
      "styleRecalcDurationMs": 23.453,
      "layouts": 5,
      "layoutDurationMs": 1.467,
      "taskDurationMs": 118.892,
      "heapDeltaBytes": 4993172,
      "heapUsedBytes": 67530912,
      "domNodes": 157,
      "jsHeapTotalBytes": 4980736,
      "scriptDurationMs": 18.953,
      "eventListeners": 71,
      "totalBlockingTimeMs": 0,
      "frameDurationMs": 16.666666666666668,
      "p95FrameDurationMs": 16.699999999999818
    }
  ]
}

@DrJKL
DrJKL requested review from LittleSound and jaeone94 July 3, 2026 19:46
@DrJKL
DrJKL marked this pull request as draft July 3, 2026 19:47
@codecov

codecov Bot commented Jul 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 75.75758% with 8 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
...Nodes/widgets/composables/useProgressTextWidget.ts 0.00% 6 Missing ⚠️
...omponents/rightSidePanel/parameters/WidgetItem.vue 66.66% 1 Missing ⚠️
src/utils/widgetUtil.ts 0.00% 1 Missing ⚠️
@@                   Coverage Diff                    @@
##             feature/ecs-migration   #13434   +/-   ##
========================================================
  Coverage                         ?   78.71%           
========================================================
  Files                            ?     1653           
  Lines                            ?   110837           
  Branches                         ?    40451           
========================================================
  Hits                             ?    87247           
  Misses                           ?    22742           
  Partials                         ?      848           
Flag Coverage Δ
unit 65.83% <75.75%> (?)

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

Files with missing lines Coverage Δ
...nents/rightSidePanel/parameters/SectionWidgets.vue 74.39% <ø> (ø)
...ts/rightSidePanel/parameters/TabSubgraphInputs.vue 69.04% <ø> (ø)
...onents/rightSidePanel/parameters/WidgetActions.vue 82.60% <100.00%> (ø)
...ponents/rightSidePanel/subgraph/SubgraphEditor.vue 85.01% <100.00%> (ø)
src/composables/useUpstreamValue.ts 93.33% <ø> (ø)
src/core/graph/subgraph/promotedInputWidget.ts 97.91% <100.00%> (ø)
src/core/graph/subgraph/promotionUtils.ts 93.05% <100.00%> (ø)
src/extensions/core/load3d.ts 70.69% <100.00%> (ø)
src/extensions/core/widgetInputs.ts 71.81% <ø> (ø)
src/extensions/core/widgetValuePropagation.ts 87.09% <ø> (ø)
... and 13 more
🚀 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.

@DrJKL
DrJKL force-pushed the drjkl/safe-widget-cleanup branch from 5c69430 to 31dd441 Compare July 3, 2026 20:47
@DrJKL
DrJKL force-pushed the drjkl/host-scoped-promoted-widgets branch from 7f920c1 to eef74bd Compare July 3, 2026 20:50

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/lib/litegraph/src/LGraphNode.ts (1)

1064-1087: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Avoid returning before widget sync. setProperty(name, null) can normalize to undefined and hit the early return when the stored property is also undefined, which skips the matching widget’s w.value = value update. Move the widget sync outside the bailout, or make the bailout depend on the persisted write only.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/litegraph/src/LGraphNode.ts` around lines 1064 - 1087, The early
return in setProperty is skipping widget synchronization when a property
normalizes to the same persisted value, so the matching widget never gets its
value updated. Adjust LGraphNode.setProperty so the bailout only applies to the
persisted property write, and ensure the widget sync loop still runs for the
matching widget even when nextValue equals the stored property. Use the
setProperty method and its w.options.property match to locate the fix.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/components/rightSidePanel/parameters/WidgetActions.test.ts`:
- Around line 211-259: The current WidgetActions test coverage misses the
nested-subgraph edge case around the isLinked logic in WidgetActions.vue. Update
the test suite to add a case that exercises a real distinct node and host
scenario where node !== host and node.isSubgraphNode() returns true, so the
“Show input” visibility behavior is locked in for the advanced-input/nested
SubgraphNode path. Use the existing WidgetActions test helpers and the
isLinked-related behavior in WidgetActions.vue to place the new assertion near
the current “already linked” coverage.

In `@src/components/rightSidePanel/parameters/WidgetActions.vue`:
- Around line 35-41: The current isLinked computed in WidgetActions.vue hides
“Show input” based only on node.isSubgraphNode() and inputForWidget(node,
widget), which can incorrectly treat nested subgraph widgets as already
promoted. Update the logic to check the host context explicitly, using node ===
host or a host-based lookup before deciding canShowInput, so only widgets
actually promoted on the current host suppress the action.

In `@src/components/rightSidePanel/subgraph/SubgraphEditor.vue`:
- Around line 250-252: The `isRowLinked` helper in `SubgraphEditor.vue` is doing
a redundant `widgetId` null check for promoted rows. Since `buildPromotedRows`
only creates promoted rows when `promotedInputWidget(input)` succeeds and that
already guarantees `input.widgetId` is set, simplify `isRowLinked(row:
ActiveRow)` to rely on `row.kind === 'promoted'` alone and remove the extra
`row.input.widgetId != null` condition.

---

Outside diff comments:
In `@src/lib/litegraph/src/LGraphNode.ts`:
- Around line 1064-1087: The early return in setProperty is skipping widget
synchronization when a property normalizes to the same persisted value, so the
matching widget never gets its value updated. Adjust LGraphNode.setProperty so
the bailout only applies to the persisted property write, and ensure the widget
sync loop still runs for the matching widget even when nextValue equals the
stored property. Use the setProperty method and its w.options.property match to
locate the fix.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3d9e62f0-791a-4a6e-aaf7-42e623399514

📥 Commits

Reviewing files that changed from the base of the PR and between 31dd441 and 72a1d83.

📒 Files selected for processing (32)
  • src/components/rightSidePanel/parameters/SectionWidgets.vue
  • src/components/rightSidePanel/parameters/TabSubgraphInputs.test.ts
  • src/components/rightSidePanel/parameters/TabSubgraphInputs.vue
  • src/components/rightSidePanel/parameters/WidgetActions.test.ts
  • src/components/rightSidePanel/parameters/WidgetActions.vue
  • src/components/rightSidePanel/parameters/WidgetItem.vue
  • src/components/rightSidePanel/subgraph/SubgraphEditor.vue
  • src/core/graph/subgraph/promotedInputWidget.ts
  • src/core/graph/subgraph/promotionUtils.test.ts
  • src/core/graph/subgraph/promotionUtils.ts
  • src/extensions/core/load3d.ts
  • src/extensions/core/widgetInputs.ts
  • src/extensions/core/widgetValuePropagation.ts
  • src/lib/litegraph/src/LGraphNode.ts
  • src/lib/litegraph/src/types/widgets.ts
  • src/locales/ar/main.json
  • src/locales/en/main.json
  • src/locales/es/main.json
  • src/locales/fa/main.json
  • src/locales/fr/main.json
  • src/locales/he/main.json
  • src/locales/ja/main.json
  • src/locales/ko/main.json
  • src/locales/pt-BR/main.json
  • src/locales/ru/main.json
  • src/locales/tr/main.json
  • src/locales/zh-TW/main.json
  • src/locales/zh/main.json
  • src/renderer/extensions/vueNodes/composables/useProcessedWidgets.test.ts
  • src/renderer/extensions/vueNodes/composables/useProcessedWidgets.ts
  • src/stores/workspace/favoritedWidgetsStore.ts
  • src/utils/widgetUtil.ts
💤 Files with no reviewable changes (14)
  • src/locales/zh/main.json
  • src/locales/ja/main.json
  • src/locales/zh-TW/main.json
  • src/locales/fr/main.json
  • src/locales/es/main.json
  • src/locales/ko/main.json
  • src/locales/he/main.json
  • src/locales/fa/main.json
  • src/locales/pt-BR/main.json
  • src/locales/ru/main.json
  • src/locales/tr/main.json
  • src/locales/ar/main.json
  • src/core/graph/subgraph/promotedInputWidget.ts
  • src/locales/en/main.json

Comment thread src/components/rightSidePanel/parameters/WidgetActions.test.ts
Comment thread src/components/rightSidePanel/parameters/WidgetActions.vue
Comment thread src/components/rightSidePanel/subgraph/SubgraphEditor.vue
@DrJKL
DrJKL force-pushed the drjkl/host-scoped-promoted-widgets branch from 72a1d83 to 38ec586 Compare July 6, 2026 18:55
@DrJKL
DrJKL changed the base branch from drjkl/safe-widget-cleanup to drjkl/and-then-there-were-none July 6, 2026 18:55
@DrJKL DrJKL added refactor preview-cpu Creates a preview ephemeral environment for this PR (CPU only) labels Jul 6, 2026
@DrJKL
DrJKL force-pushed the drjkl/and-then-there-were-none branch from c221781 to a71df2f Compare July 6, 2026 20:58
@DrJKL
DrJKL force-pushed the drjkl/host-scoped-promoted-widgets branch from 38ec586 to b713409 Compare July 6, 2026 20:59
@DrJKL
DrJKL marked this pull request as ready for review July 7, 2026 06:40
@DrJKL
DrJKL requested review from a team and jtydhr88 as code owners July 7, 2026 06:40
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:M This PR changes 30-99 lines, ignoring generated files. labels Jul 7, 2026

@jaeone94 jaeone94 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The host-scoping core looks good to me — the three predicate replacements read as equivalent (or strictly safer on the dangling-input edge), and I agree the removed handleHideInput branch was unreachable. Two inline comments below, plus one PR-level note:

Suggestion: The null-widget-value work in the last commit (b71340970) — widening IBaseWidget's default TValue to WidgetValue, the setProperty normalization, and the follow-ups in useProcessedWidgets/widgetInputs/widgetValuePropagation/load3d — doesn't share any files with the host-scoping commits, and those all precede it in the stack, so they appear to build and behave without it. Since IBaseWidget's default value type and setProperty are extension-facing surfaces, they'd probably benefit from their own description and review. Given this is already a stack, would you consider splitting that commit into one more layer? That would also let the setProperty discussion below proceed without holding up the host-scoping work here.

Comment thread src/lib/litegraph/src/LGraphNode.ts Outdated
if (value === this.properties[name]) return
// Node properties are persisted and cannot hold null/void, so normalize
// before comparing and storing.
const nextValue = value ?? undefined

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Issue: If I'm reading the value ?? undefined normalization right, it changes the observable behavior of setProperty(name, null) in two cases: (1) when the property is unset (or currently undefined), nextValue === properties[name] early-returns, so what used to store null and fire onPropertyChanged is now a silent no-op; (2) when the property holds a value, undefined gets stored, the key disappears from the serialized workflow (previously persisted as "name": null), and onPropertyChanged receives undefined where it used to get null. Since setProperty/onPropertyChanged are surfaces custom nodes use, if this change is intentional it'd be good to have a test and a short migration note; if not, reverting this hunk seems safer. One more thing worth confirming: within the same call, the widget-sync loop still assigns the pre-normalization value, so the property and the widget receive different values for a null argument — is that intended?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed 👍✅ Reverted the setProperty normalization hunk ↩️ and added regression coverage for property storage 💾, callbacks 📣, widget synchronization 🔄, and serialization 📦 in 326d4b8945 🧪🚀✨

).not.toBeInTheDocument()
})

it('toggles the favorite for the host node itself', async () => {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggestion: Unless I'm misreading the fixture, "toggles the favorite for the host node itself" renders without a host. In a host-less configuration the pre-change logic (favoriteNode falling back to node when no parents were given) produces the same outcome, so I don't think this test can catch a regression in the favorite-target decision this PR changes. Passing { host } — so the test exercises the configuration its title describes — and asserting toggleFavorite still receives the row's node would pin against someone reintroducing host-directed favorites later (and the title could be adjusted to match).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed 👍✅ The test now supplies a distinct host 🏠 and verifies that the row’s node remains the favorite target ⭐🎯 in d1366ebe66 🧪🚀

const canShowInput = computed(() => host != null && !isLinked.value)
const isFavorited = computed(() =>
favoritedWidgetsStore.isFavorited(favoriteNode.value, widget.name)
favoritedWidgetsStore.isFavorited(node, widget.name)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue: handleHideInput and the hide/show toggle are removed entirely, but the "Hide input" action was reachable for non-linked physically-promoted widgets. Git history shows bb96e3c (#10648) explicitly restored this path (canToggleVisibility = hasParents && !isLinked). After this PR, a user inside a subgraph can no longer demote a promoted widget from the Parameters tab three-dot menu.

Is the intent to remove this action completely, or should the demote path (demoteWidget(node, widget, [host])) be kept and gated on a separate condition?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Remove until we have better handling. The previous path had special cases for "promoted" vs "linked to Subgraph input". Now that we have Subgraph Link Only Promotion, hiding becomes a difficult UX/product issue.

sourceWidgetName: source.widgetName
})
const linkedInput = row.input._subgraphSlot
if (linkedInput) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue: when row.input._subgraphSlot is absent (possible if the input was created before _subgraphSlot hydration), the demotion silently no-ops while refreshPromotedWidgetRendering() still fires, giving a false success signal. The old path via demotePromotedInput had the same silent skip, but the Partial<ISubgraphInput> widening makes this gap wider. Consider adding an else { console.warn(...) } branch, or asserting _subgraphSlot is non-null in dev mode.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is not a new runtime gap introduced here 🔍✅ The previous demotePromotedInput path also returned without mutation when _subgraphSlot was absent ↩️, and the widened static type does not expand the runtime inputs 🧠📐 I’m leaving the existing behavior unchanged 👍🧘✨

})
const linkedInput = row.input._subgraphSlot
if (linkedInput) {
if (row.input.link != null) linkedInput.disconnect()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question: when row.input.link != null, the code calls linkedInput.disconnect() but does not call subgraphNode.subgraph.removeInput(linkedInput). Did the old demotePromotedInput also leave the slot alive in this case, or did it always remove the slot? If the slot should be removed, this branch may leave a dangling empty subgraph input.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

IIRC, it left an empty slot. This was the looooong thread about demotion semantics with all of the axioms and lemmas.

!!source &&
isLinkedPromotion(activeNode.value, String(row.node.id), source.widgetName)
)
return row.kind === 'promoted' && row.input.widgetId != null

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question: the old isRowLinked had if (row.node.id === UNASSIGNED_NODE_ID) return true for synthetic promoted rows not yet assigned a real ID. The new check row.input.widgetId != null returns false for those rows if widgetId is not yet populated -- which would enable the demote button prematurely. Is widgetId guaranteed to be set for all rows that previously matched the UNASSIGNED_NODE_ID sentinel?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

buildPromotedRows only creates a promoted row after promotedInputWidget(input) succeeds 🏗️✅, which guarantees widgetId 🆔🔒 The unassigned source-node sentinel does not create a promoted row without that input identity 🚫🧙 The redundant check was removed in d1366ebe66 ✂️🧹🚀

type: 'TestNode',
rootGraph: { id: 'graph-test' },
isSubgraphNode: () => true,
getSlotFromWidget: () => ({ widgetId: 'graph-test:1:test_widget' })

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: (non-blocking) the mock returns { widgetId: '...' } unconditionally regardless of which widget is queried. A name-matching bug in getSlotFromWidget would still pass this test. Consider discriminating the mock on widget.name (e.g. (w) => w.name === 'test_widget' ? { widgetId: '...' } : undefined) so it only matches the correct widget.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed 👍✅ The mock now returns a slot only for the expected widget 🎯🧩 in d1366ebe66 🧪🚀✨

expect(onResetToDefault).toHaveBeenCalledWith('option1')
})

it('promotes the widget into the host when "Show input" is clicked', async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: (non-blocking) there is no integration-level test that uses a real SubgraphNode + promoteValueWidgetViaSubgraphInput, then renders WidgetActions and asserts "Show input" is absent for a linked promotion and present for an unlinked one. The deleted isLinkedPromotion tests partially covered this invariant. One test at this level would guard the new inputForWidget(node, widget)?.widgetId != null check against future regressions.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I’m deferring this ⏸️📝 The suggested test exercises the previous promotion-chain model 🔗🪦, while this PR intentionally makes widget behavior node-local 📍 and independent 🧩✨ The focused tests cover the intended boundary 🎯🧪✅

/** In-memory array of favorited widget IDs, ordered for display */
const favoritedIds = ref<string[]>([])

// TODO(ADR0009): key favorites by the host-scoped WidgetId instead of

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: (non-blocking) the TODO mentions a deferred migration but does not note the data-loss risk: existing favorited promoted widgets saved under the host's nodeLocatorId will silently disappear without a compat shim that reads the old key format on first load. Worth adding: // Risk: without a compat reader, existing persisted favorites for promoted widgets will be silently dropped on upgrade.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

The existing TODO already states that adopting the new persisted key requires a one-time migration 📝🔄💾 Since the migration is deferred ⏳ and no persisted format changes in this PR 🔒📦, I’m avoiding an additional comment that repeats the same requirement 🦜🚫🧹✨

})
const linkedInput = row.input._subgraphSlot
if (linkedInput) {
if (row.input.link != null) linkedInput.disconnect()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

nitpick (non-blocking): the link != null ? disconnect() : removeInput() split and the variable name linkedInput (for what is actually a SubgraphInput slot, not a link) are non-obvious without context. A brief inline comment would help future readers understand why a live-linked slot is only disconnected while an unlinked slot is fully removed. Renaming linkedInput to subgraphSlot would also match the field name _subgraphSlot.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Renamed linkedInput to subgraphSlot in d1366ebe66 🏷️✅🚀 The clearer name makes the relationship explicit 🔍✨, so I did not add a comment paraphrasing the disconnect/remove branches 📝🚫🦜🧹

if (live) {
const normalized = normalizeWidgetValue(newValue)
live.widget.value = normalized ?? undefined
live.widget.value = normalized

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

question: live.widget.value = normalized now allows null to reach IBaseWidget.value, where before it was coerced to undefined via ?? undefined. Custom nodes that serialize widgets_values via LGraphNode.serialize() will now emit JSON null for this slot. Is that intentional? If so, it may be worth noting in the PR description or release notes since it is a subtle behavioral change for the extension ecosystem.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

LGraphNode.serialize() already normalizes both undefined and null widget values to JSON null 🔄➡️🈳, so this does not change the serialized widgets_values representation 📦🔒 No release note is needed for the serialization behavior described here 📝🚫✅✨

@christian-byrne christian-byrne removed their assignment Jul 14, 2026
Base automatically changed from drjkl/and-then-there-was-no-store to feature/ecs-migration July 16, 2026 22:51
@dosubot dosubot Bot added size:XL This PR changes 500-999 lines, ignoring generated files. and removed size:L This PR changes 100-499 lines, ignoring generated files. labels Jul 16, 2026
DrJKL and others added 11 commits July 16, 2026 15:55
SectionWidgets and WidgetActions identified promoted subgraph-input widgets
by walking live links back to the interior source. Per ADR0009 a promoted
widget is host-scoped (widget.widgetId = graphId:hostNodeId:inputName, also
carried on the host input slot), so reuse that instead:

- isLinked: inputForWidget(node, widget)?.widgetId != null
- isWidgetShownOnParents: match parent inputs by widget.widgetId
- handleHideInput: drop unreachable source-resolution branch, keep demoteWidget
- remove now-unused widgetPromotedSource export

Favorites keep their current (nodeLocatorId, widgetName) key; added a
TODO(ADR0009) to move to a pure WidgetId key (deferred: needs a persisted
format migration).

Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
isShownOnParents never changed observable behavior: for promoted rows
parents[0] === node (so favoriteNode was unchanged) and the toggle was
suppressed by isLinked; for interior rows it was always false. Its
Hide-toggle branch and parent-redirect were unreachable.

- drop isShownOnParents prop from WidgetItem and WidgetActions
- favorites key directly off node (was favoriteNode)
- WidgetActions toggle becomes a one-way Show Input action; remove the
  dead handleHideInput + demoteWidget usage and the hideInput locale key
- remove SectionWidgets.isWidgetShownOnParents

Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
- demoteRow now removes the linked input via the host input slot we
  already have, instead of re-resolving the source and calling
  demotePromotedInput
- un-export demotePromotedInput (only internal callers remain)
- drop dead parents param from renameWidget/promptRenameWidget

Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
isLinkedPromotion has no production callers outside promotionUtils.
Drop its dedicated direct-unit describe block and rewrite the demoteWidget
tests that used it as an assertion oracle to check observable interior
link state instead.

Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
- WidgetActions: show-input promotion, isLinked gating, favorite toggle
  by host node
- useProcessedWidgets: live update handler forwards null (not undefined)
  to the widget callback

Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
The promoted-widget side panel only ever has one host SubgraphNode, so
collapse the parents: SubgraphNode[] prop to host?: SubgraphNode across
SectionWidgets/WidgetItem/WidgetActions and drop the [node] wrapper in
TabSubgraphInputs.

Amp-Thread-ID: https://ampcode.com/threads/T-019f2964-7a96-7579-8883-caeb3b2b07da
Co-authored-by: Amp <amp@ampcode.com>
Default IBaseWidget TValue to the canonical WidgetValue so a widget value
may be null. The Vue update handler now assigns the same normalized value
to the store, live widget .value, and callback instead of coercing .value
to undefined. Coerce null/void to undefined only at the persisted
NodeProperty boundary.

Amp-Thread-ID: https://ampcode.com/threads/T-019f2a75-8d64-7605-980d-7aca51a5f19c
Co-authored-by: Amp <amp@ampcode.com>
@DrJKL
DrJKL force-pushed the drjkl/host-scoped-promoted-widgets branch from 8105355 to 1343173 Compare July 16, 2026 23:14
@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. and removed size:XL This PR changes 500-999 lines, ignoring generated files. labels Jul 16, 2026
@DrJKL DrJKL assigned christian-byrne and unassigned DrJKL Jul 16, 2026
@DrJKL
DrJKL requested a review from jaeone94 July 16, 2026 23:23
@DrJKL
DrJKL requested a review from christian-byrne July 17, 2026 00:47

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/lib/litegraph/src/types/widgets.ts`:
- Line 415: Update isWidgetValue to accept null consistently with the
WidgetValue default type, and ensure applySubgraphInputOrder preserves null
values through reordering rather than converting them to undefined or skipping
restoration. Add a regression test covering null widget values being reordered
and restored to the correct inputs.

In `@src/lib/litegraph/src/widgets/BaseWidget.ts`:
- Around line 430-435: Update the property synchronization condition in the
widget value-change logic to remove the v != null guard, while retaining the
options.property check and node.properties presence check. Ensure
node.setProperty is called for null values so this.value and node.properties
remain synchronized.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 3e142bfa-e47c-49b1-a0d1-d8556a8896b8

📥 Commits

Reviewing files that changed from the base of the PR and between 72a1d83 and d3c4a5a.

📒 Files selected for processing (29)
  • src/components/rightSidePanel/parameters/SectionWidgets.vue
  • src/components/rightSidePanel/parameters/TabSubgraphInputs.test.ts
  • src/components/rightSidePanel/parameters/TabSubgraphInputs.vue
  • src/components/rightSidePanel/parameters/WidgetActions.test.ts
  • src/components/rightSidePanel/parameters/WidgetActions.vue
  • src/components/rightSidePanel/parameters/WidgetItem.vue
  • src/components/rightSidePanel/subgraph/SubgraphEditor.vue
  • src/core/graph/subgraph/promotedInputWidget.ts
  • src/core/graph/subgraph/promotionUtils.test.ts
  • src/core/graph/subgraph/promotionUtils.ts
  • src/extensions/core/load3d.ts
  • src/extensions/core/widgetInputs.ts
  • src/extensions/core/widgetValuePropagation.ts
  • src/lib/litegraph/src/LGraphNode.test.ts
  • src/lib/litegraph/src/types/widgets.ts
  • src/lib/litegraph/src/widgets/BaseWidget.ts
  • src/locales/ar/main.json
  • src/locales/en/main.json
  • src/locales/es/main.json
  • src/locales/fa/main.json
  • src/locales/fr/main.json
  • src/locales/he/main.json
  • src/locales/ja/main.json
  • src/locales/ko/main.json
  • src/locales/pt-BR/main.json
  • src/locales/ru/main.json
  • src/locales/tr/main.json
  • src/locales/zh-TW/main.json
  • src/locales/zh/main.json
💤 Files with no reviewable changes (13)
  • src/core/graph/subgraph/promotedInputWidget.ts
  • src/locales/zh-TW/main.json
  • src/locales/ko/main.json
  • src/locales/fr/main.json
  • src/locales/pt-BR/main.json
  • src/locales/ja/main.json
  • src/locales/zh/main.json
  • src/locales/ar/main.json
  • src/locales/es/main.json
  • src/locales/fa/main.json
  • src/locales/he/main.json
  • src/locales/tr/main.json
  • src/locales/ru/main.json

*/
export interface IBaseWidget<
TValue = boolean | number | string | object | undefined,
TValue = WidgetValue,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Keep isWidgetValue aligned with the new default type.

WidgetValue includes null, but isWidgetValue still rejects it. applySubgraphInputOrder consequently converts null values to undefined and skips restoring them after reordering, which can lose promoted widget values or associate values with the wrong input. Update the predicate and add a null-reordering regression test.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/litegraph/src/types/widgets.ts` at line 415, Update isWidgetValue to
accept null consistently with the WidgetValue default type, and ensure
applySubgraphInputOrder preserves null values through reordering rather than
converting them to undefined or skipping restoration. Add a regression test
covering null widget values being reordered and restored to the correct inputs.

Comment on lines 430 to 435
if (
this.options?.property &&
v != null &&
node.properties[this.options.property] !== undefined
) {
node.setProperty(this.options.property, v)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Do not skip property synchronization for null.

When a property-backed widget changes from a non-null value to null, this.value is updated but node.setProperty(...) is skipped. The widget and node.properties then diverge, and serialization retains the stale property value. Remove the v != null guard while retaining the existing property-presence check.

Proposed fix
     if (
       this.options?.property &&
-      v != null &&
       node.properties[this.options.property] !== undefined
     ) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (
this.options?.property &&
v != null &&
node.properties[this.options.property] !== undefined
) {
node.setProperty(this.options.property, v)
if (
this.options?.property &&
node.properties[this.options.property] !== undefined
) {
node.setProperty(this.options.property, v)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/litegraph/src/widgets/BaseWidget.ts` around lines 430 - 435, Update
the property synchronization condition in the widget value-change logic to
remove the v != null guard, while retaining the options.property check and
node.properties presence check. Ensure node.setProperty is called for null
values so this.value and node.properties remain synchronized.

isWidgetValue rejected null while the canonical WidgetValue allows it,
so promoted-input reorder silently dropped null values, and
BaseWidget.setValue skipped setProperty for null, letting widget value
and bound node property skew.

TWidgetValue, WidgetValue, and WidgetState.value had collapsed into the
same union spelled three ways (plus unknown in the store). Alias
TWidgetValue to WidgetValue, default WidgetState to WidgetValue, drop
the dead File[]/void members, and delete the four guards that only
laundered unknown - isWidgetValue remains at the untrusted-JSON and
serialize boundaries.
@DrJKL
DrJKL merged commit db3f66e into feature/ecs-migration Jul 17, 2026
48 checks passed
@DrJKL
DrJKL deleted the drjkl/host-scoped-promoted-widgets branch July 17, 2026 22:52
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 17, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

preview-cpu Creates a preview ephemeral environment for this PR (CPU only) refactor size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants