Skip to content

Conversation

@AustinMroz
Copy link
Collaborator

@AustinMroz AustinMroz commented Nov 12, 2025

Adds support for "dynamic combo" widgets where selecting a value on a combo widget can cause other widgets or inputs to be created.

dynamic-widgets_00001

Includes a fairly large refactoring in litegraphService to remove #private methods and cleanup some duplication in constructors for subgraphNodes.

┆Issue is synchronized with this Notion page by Unito

@github-actions
Copy link

github-actions bot commented Nov 12, 2025

🎨 Storybook Build Status

Build completed successfully!

⏰ Completed at: 11/20/2025, 03:59:18 AM UTC

🔗 Links


🎉 Your Storybook is ready for review!

@github-actions
Copy link

github-actions bot commented Nov 12, 2025

🎭 Playwright Test Results

⚠️ Tests passed with flaky tests

⏰ Completed at: 11/20/2025, 05:18:07 AM UTC

📈 Summary

  • Total Tests: 497
  • Passed: 486 ✅
  • Failed: 0
  • Flaky: 2 ⚠️
  • Skipped: 9 ⏭️

📊 Test Reports by Browser

  • chromium: View Report • ✅ 477 / ❌ 0 / ⚠️ 2 / ⏭️ 9
  • chromium-2x: View Report • ✅ 2 / ❌ 0 / ⚠️ 0 / ⏭️ 0
  • chromium-0.5x: View Report • ✅ 1 / ❌ 0 / ⚠️ 0 / ⏭️ 0
  • mobile-chrome: View Report • ✅ 6 / ❌ 0 / ⚠️ 0 / ⏭️ 0

🎉 Click on the links above to view detailed test results for each browser configuration.

@github-actions
Copy link

github-actions bot commented Nov 12, 2025

Bundle Size Report

Summary

  • Raw size: 13.7 MB baseline 13.7 MB — 🟢 -2.26 kB
  • Gzip: 2.75 MB baseline 2.75 MB — 🔴 +706 B
  • Brotli: 2.16 MB baseline 2.16 MB — 🔴 +735 B
  • Bundles: 92 current • 92 baseline • 38 added / 38 removed

Category Glance
App Entry Points 🟢 -2.26 kB (3.12 MB) · Vendor & Third-Party ⚪ 0 B (5.32 MB) · Other ⚪ 0 B (3.87 MB) · Graph Workspace ⚪ 0 B (925 kB) · Panels & Settings ⚪ 0 B (306 kB) · UI Components ⚪ 0 B (141 kB) · + 3 more

Per-category breakdown
App Entry Points — 3.12 MB (baseline 3.12 MB) • 🟢 -2.26 kB

Main entry bundles and manifests

File Before After Δ Raw Δ Gzip Δ Brotli
assets/index-6CGwPO2U.js (removed) 2.76 MB 🟢 -2.76 MB 🟢 -576 kB 🟢 -437 kB
assets/index-De4VJu73.js (new) 2.76 MB 🔴 +2.76 MB 🔴 +577 kB 🔴 +438 kB
assets/index-CcB-UBV1.js (removed) 364 kB 🟢 -364 kB 🟢 -75.2 kB 🟢 -61.3 kB
assets/index-DPLSkMJ3.js (new) 364 kB 🔴 +364 kB 🔴 +75.2 kB 🔴 +61.3 kB
assets/index-BvPsQv-I.js (removed) 345 B 🟢 -345 B 🟢 -247 B 🟢 -234 B
assets/index-DxYYWFJi.js (new) 345 B 🔴 +345 B 🔴 +245 B 🔴 +231 B

Status: 3 added / 3 removed

Graph Workspace — 925 kB (baseline 925 kB) • ⚪ 0 B

Graph editor runtime, canvas, workflow orchestration

File Before After Δ Raw Δ Gzip Δ Brotli
assets/GraphView-BAWzmnjH.js (new) 925 kB 🔴 +925 kB 🔴 +179 kB 🔴 +138 kB
assets/GraphView-DqMO-2g-.js (removed) 925 kB 🟢 -925 kB 🟢 -179 kB 🟢 -137 kB

Status: 1 added / 1 removed

Views & Navigation — 7.97 kB (baseline 7.97 kB) • ⚪ 0 B

Top-level views, pages, and routed surfaces

File Before After Δ Raw Δ Gzip Δ Brotli
assets/UserSelectView-D-Qe06CC.js (removed) 7.97 kB 🟢 -7.97 kB 🟢 -2.43 kB 🟢 -2.13 kB
assets/UserSelectView-DE1RVhAL.js (new) 7.97 kB 🔴 +7.97 kB 🔴 +2.43 kB 🔴 +2.13 kB

Status: 1 added / 1 removed

Panels & Settings — 306 kB (baseline 306 kB) • ⚪ 0 B

Configuration panels, inspectors, and settings screens

File Before After Δ Raw Δ Gzip Δ Brotli
assets/CreditsPanel-BgN_5U-0.js (new) 22.9 kB 🔴 +22.9 kB 🔴 +5.46 kB 🔴 +4.79 kB
assets/CreditsPanel-C2JUHrSs.js (removed) 22.9 kB 🟢 -22.9 kB 🟢 -5.46 kB 🟢 -4.78 kB
assets/KeybindingPanel-C9Rj6-en.js (new) 15.1 kB 🔴 +15.1 kB 🔴 +3.73 kB 🔴 +3.29 kB
assets/KeybindingPanel-CRhBS8Kw.js (removed) 15.1 kB 🟢 -15.1 kB 🟢 -3.73 kB 🟢 -3.3 kB
assets/ExtensionPanel-lSODbfJo.js (removed) 11.9 kB 🟢 -11.9 kB 🟢 -2.79 kB 🟢 -2.45 kB
assets/ExtensionPanel-RAc76MQ2.js (new) 11.9 kB 🔴 +11.9 kB 🔴 +2.8 kB 🔴 +2.45 kB
assets/AboutPanel-BEGi46bb.js (new) 10.1 kB 🔴 +10.1 kB 🔴 +2.62 kB 🔴 +2.31 kB
assets/AboutPanel-C3ZXtkOF.js (removed) 10.1 kB 🟢 -10.1 kB 🟢 -2.62 kB 🟢 -2.31 kB
assets/ServerConfigPanel-B-sotrcT.js (new) 8.02 kB 🔴 +8.02 kB 🔴 +2.12 kB 🔴 +1.87 kB
assets/ServerConfigPanel-oBmgWc6K.js (removed) 8.02 kB 🟢 -8.02 kB 🟢 -2.12 kB 🟢 -1.88 kB
assets/UserPanel-BhLkQTEB.js (removed) 7.74 kB 🟢 -7.74 kB 🟢 -2.03 kB 🟢 -1.77 kB
assets/UserPanel-EBx0vNrq.js (new) 7.74 kB 🔴 +7.74 kB 🔴 +2.03 kB 🔴 +1.77 kB
assets/settings-BXTtSH4O.js 33.3 kB 33.3 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-C9Pzn-NG.js 25.2 kB 25.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-CCy2fA_h.js 27.3 kB 27.3 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-CQpqEFfl.js 26.6 kB 26.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-DHcnxypw.js 21.7 kB 21.7 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-DhFTK9fY.js 25.1 kB 25.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-DlT4t_ui.js 25.9 kB 25.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-DRgSrIdD.js 24.2 kB 24.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-tjkeqiZq.js 21.1 kB 21.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 6 added / 6 removed

UI Components — 141 kB (baseline 141 kB) • ⚪ 0 B

Reusable component library chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/Load3D.vue_vue_type_script_setup_true_lang-84PKL2Xh.js (removed) 53.9 kB 🟢 -53.9 kB 🟢 -8.43 kB 🟢 -7.24 kB
assets/Load3D.vue_vue_type_script_setup_true_lang-BJolcaKm.js (new) 53.9 kB 🔴 +53.9 kB 🔴 +8.43 kB 🔴 +7.23 kB
assets/WidgetSelect.vue_vue_type_script_setup_true_lang-C83TS2kj.js (new) 48.1 kB 🔴 +48.1 kB 🔴 +10.3 kB 🔴 +8.94 kB
assets/WidgetSelect.vue_vue_type_script_setup_true_lang-SeoM_ddm.js (removed) 48.1 kB 🟢 -48.1 kB 🟢 -10.3 kB 🟢 -8.94 kB
assets/WidgetInputNumber.vue_vue_type_script_setup_true_lang-Besg-jAQ.js (new) 12.7 kB 🔴 +12.7 kB 🔴 +3.3 kB 🔴 +2.92 kB
assets/WidgetInputNumber.vue_vue_type_script_setup_true_lang-CUYVmdFY.js (removed) 12.7 kB 🟢 -12.7 kB 🟢 -3.31 kB 🟢 -2.92 kB
assets/ComfyQueueButton-C1epNoFX.js (removed) 9.27 kB 🟢 -9.27 kB 🟢 -2.51 kB 🟢 -2.21 kB
assets/ComfyQueueButton-sbrZNqxM.js (new) 9.27 kB 🔴 +9.27 kB 🔴 +2.51 kB 🔴 +2.21 kB
assets/WidgetLayoutField.vue_vue_type_script_setup_true_lang-OHYXm4SX.js (removed) 2.14 kB 🟢 -2.14 kB 🟢 -796 B 🟢 -687 B
assets/WidgetLayoutField.vue_vue_type_script_setup_true_lang-RBOPFahF.js (new) 2.14 kB 🔴 +2.14 kB 🔴 +794 B 🔴 +691 B
assets/MediaTitle.vue_vue_type_script_setup_true_lang-CepBBy9G.js (new) 848 B 🔴 +848 B 🔴 +475 B 🔴 +418 B
assets/MediaTitle.vue_vue_type_script_setup_true_lang-COSIAlke.js (removed) 848 B 🟢 -848 B 🟢 -477 B 🟢 -416 B
assets/LazyImage.vue_vue_type_script_setup_true_lang-Wi-CcgaU.js 10.7 kB 10.7 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/UserAvatar.vue_vue_type_script_setup_true_lang-D2s8tnS2.js 1.26 kB 1.26 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetButton-ByrPd5jr.js 1.62 kB 1.62 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 6 added / 6 removed

Data & Services — 12.5 kB (baseline 12.5 kB) • ⚪ 0 B

Stores, services, APIs, and repositories

File Before After Δ Raw Δ Gzip Δ Brotli
assets/keybindingService-DqU_6qqj.js (new) 7.51 kB 🔴 +7.51 kB 🔴 +1.84 kB 🔴 +1.58 kB
assets/keybindingService-YFFBeCvf.js (removed) 7.51 kB 🟢 -7.51 kB 🟢 -1.84 kB 🟢 -1.58 kB
assets/audioService--SNYcc6I.js (new) 2.2 kB 🔴 +2.2 kB 🔴 +962 B 🔴 +821 B
assets/audioService-BusurfD6.js (removed) 2.2 kB 🟢 -2.2 kB 🟢 -961 B 🟢 -819 B
assets/serverConfigStore-BoHtzifw.js 2.79 kB 2.79 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 2 added / 2 removed

Utilities & Hooks — 2.94 kB (baseline 2.94 kB) • ⚪ 0 B

Helpers, composables, and utility bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/audioUtils-Dh0N6sbc.js (removed) 1.41 kB 🟢 -1.41 kB 🟢 -649 B 🟢 -542 B
assets/audioUtils-DXeyv3-S.js (new) 1.41 kB 🔴 +1.41 kB 🔴 +652 B 🔴 +552 B
assets/mathUtil-CTARWQ-l.js 1.07 kB 1.07 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeFilterUtil-CXKCRJ-m.js 460 B 460 B ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 1 added / 1 removed

Vendor & Third-Party — 5.32 MB (baseline 5.32 MB) • ⚪ 0 B

External libraries and shared vendor chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/vendor-other-z0ajCJVX.js 3.22 MB 3.22 MB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-primevue-PESgPnbc.js 517 B 517 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-three-aR6ntw5X.js 1.37 MB 1.37 MB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-tiptap-D2zb6Fg1.js 232 kB 232 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-vue-aBQ_uOio.js 92.6 kB 92.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-xterm-BZLod3g9.js 407 kB 407 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
Other — 3.87 MB (baseline 3.87 MB) • ⚪ 0 B

Bundles that do not match a named category

File Before After Δ Raw Δ Gzip Δ Brotli
assets/WidgetRecordAudio-DREZKKZW.js (removed) 21.9 kB 🟢 -21.9 kB 🟢 -5.52 kB 🟢 -4.88 kB
assets/WidgetRecordAudio-T9e3NuoJ.js (new) 21.9 kB 🔴 +21.9 kB 🔴 +5.52 kB 🔴 +4.87 kB
assets/AudioPreviewPlayer-BFczpxZJ.js (new) 14.9 kB 🔴 +14.9 kB 🔴 +3.69 kB 🔴 +3.3 kB
assets/AudioPreviewPlayer-DbwNROiX.js (removed) 14.9 kB 🟢 -14.9 kB 🟢 -3.69 kB 🟢 -3.3 kB
assets/WidgetGalleria-B84_-fO6.js (new) 5.56 kB 🔴 +5.56 kB 🔴 +1.74 kB 🔴 +1.54 kB
assets/WidgetGalleria-Bo4zjspw.js (removed) 5.56 kB 🟢 -5.56 kB 🟢 -1.74 kB 🟢 -1.54 kB
assets/WidgetColorPicker-BYplQVfp.js (new) 4.87 kB 🔴 +4.87 kB 🔴 +1.69 kB 🔴 +1.48 kB
assets/WidgetColorPicker-C0d2lVVK.js (removed) 4.87 kB 🟢 -4.87 kB 🟢 -1.69 kB 🟢 -1.48 kB
assets/WidgetMarkdown-C76kmDAg.js (new) 4.6 kB 🔴 +4.6 kB 🔴 +1.6 kB 🔴 +1.4 kB
assets/WidgetMarkdown-DiE46aMl.js (removed) 4.6 kB 🟢 -4.6 kB 🟢 -1.6 kB 🟢 -1.4 kB
assets/WidgetAudioUI-CbPXfJD9.js (removed) 4.33 kB 🟢 -4.33 kB 🟢 -1.44 kB 🟢 -1.29 kB
assets/WidgetAudioUI-CuVeylKv.js (new) 4.33 kB 🔴 +4.33 kB 🔴 +1.44 kB 🔴 +1.29 kB
assets/WidgetTextarea-BeKnf8yl.js (new) 3.6 kB 🔴 +3.6 kB 🔴 +1.26 kB 🔴 +1.11 kB
assets/WidgetTextarea-BSvr9HrW.js (removed) 3.6 kB 🟢 -3.6 kB 🟢 -1.26 kB 🟢 -1.13 kB
assets/WidgetInputText-BlV89lUJ.js (removed) 3.45 kB 🟢 -3.45 kB 🟢 -1.24 kB 🟢 -1.09 kB
assets/WidgetInputText-Bm1fKfyE.js (new) 3.45 kB 🔴 +3.45 kB 🔴 +1.24 kB 🔴 +1.09 kB
assets/WidgetToggleSwitch-mCWHO8RA.js (removed) 3.23 kB 🟢 -3.23 kB 🟢 -1.14 kB 🟢 -1.01 kB
assets/WidgetToggleSwitch-ONxcWk_b.js (new) 3.23 kB 🔴 +3.23 kB 🔴 +1.14 kB 🔴 +1.02 kB
assets/MediaImageBottom-B8mCJuI5.js (removed) 3.05 kB 🟢 -3.05 kB 🟢 -1.05 kB 🟢 -917 B
assets/MediaImageBottom-DgaBkHTL.js (new) 3.05 kB 🔴 +3.05 kB 🔴 +1.05 kB 🔴 +910 B
assets/MediaAudioBottom-BN0-B0hr.js (removed) 3 kB 🟢 -3 kB 🟢 -1.05 kB 🟢 -925 B
assets/MediaAudioBottom-C-EImJ8w.js (new) 3 kB 🔴 +3 kB 🔴 +1.05 kB 🔴 +926 B
assets/Media3DTop-ByTpSJOt.js (new) 3 kB 🔴 +3 kB 🔴 +1.08 kB 🔴 +909 B
assets/Media3DTop-C_IAjL5F.js (removed) 3 kB 🟢 -3 kB 🟢 -1.08 kB 🟢 -913 B
assets/MediaVideoBottom-NR1e8WF_.js (removed) 3 kB 🟢 -3 kB 🟢 -1.05 kB 🟢 -929 B
assets/MediaVideoBottom-OC9ExIo_.js (new) 3 kB 🔴 +3 kB 🔴 +1.05 kB 🔴 +917 B
assets/Media3DBottom-CmJKM9fk.js (new) 2.98 kB 🔴 +2.98 kB 🔴 +1.04 kB 🔴 +910 B
assets/Media3DBottom-DPF24MsZ.js (removed) 2.98 kB 🟢 -2.98 kB 🟢 -1.04 kB 🟢 -911 B
assets/WidgetSelect-BocWefC6.js (new) 2.17 kB 🔴 +2.17 kB 🔴 +675 B 🔴 +580 B
assets/WidgetSelect-D10YLtkh.js (removed) 2.17 kB 🟢 -2.17 kB 🟢 -674 B 🟢 -577 B
assets/WidgetInputNumber-DT8YEJ66.js (new) 2.12 kB 🔴 +2.12 kB 🔴 +664 B 🔴 +557 B
assets/WidgetInputNumber-M5yOd37k.js (removed) 2.12 kB 🟢 -2.12 kB 🟢 -665 B 🟢 -559 B
assets/Load3D-BWY2D0FA.js (removed) 1.94 kB 🟢 -1.94 kB 🟢 -601 B 🟢 -539 B
assets/Load3D-CD2h6dKN.js (new) 1.94 kB 🔴 +1.94 kB 🔴 +596 B 🔴 +533 B
assets/WidgetLegacy-BMWbJ48z.js (removed) 1.88 kB 🟢 -1.88 kB 🟢 -563 B 🟢 -507 B
assets/WidgetLegacy-BnEsgECt.js (new) 1.88 kB 🔴 +1.88 kB 🔴 +561 B 🔴 +504 B
assets/commands-_s-RvhJR.js 13.6 kB 13.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-BuUILW6P.js 13 kB 13 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-BV4R6fLx.js 14.9 kB 14.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-CLwPdnT6.js 14.2 kB 14.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-CWMchBmd.js 15.9 kB 15.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-DazTQhtc.js 12.9 kB 12.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-DmWrOe93.js 13.7 kB 13.7 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-DwiH7Kr6.js 13.8 kB 13.8 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-mS3LCNPn.js 14.5 kB 14.5 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-5lOBdqcC.js 84.5 kB 84.5 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-BOCuaVpE.js 73.4 kB 73.4 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-ClrEFGUz.js 72.4 kB 72.4 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-CyNU0iQX.js 99.3 kB 99.3 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-D7gwLxft.js 114 kB 114 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-DC8o4BCt.js 86.8 kB 86.8 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-DKiesCV4.js 94.3 kB 94.3 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-Hq2q-OtB.js 83.6 kB 83.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-USAlAlnj.js 82 kB 82 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/MediaAudioTop-RTI8pWy9.js 1.42 kB 1.42 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/MediaImageTop-Cxl4dc80.js 1.68 kB 1.68 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/MediaVideoTop-BB0lT7C5.js 2.7 kB 2.7 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-_Px5dSNW.js 306 kB 306 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-7z21KPoS.js 285 kB 285 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-BWKZzBPK.js 346 kB 346 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-CGbgH4Yl.js 320 kB 320 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-CjjjdWkV.js 313 kB 313 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-CVrNtxvj.js 288 kB 288 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-DLRSA0IK.js 309 kB 309 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-DQV2gnwA.js 372 kB 372 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-ofqLG5vz.js 310 kB 310 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetChart-rDmYEWg5.js 2.39 kB 2.39 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetImageCompare-Ds3K3ULR.js 2.15 kB 2.15 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/widgetPropFilter-BIbGSUAt.js 1.28 kB 1.28 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 18 added / 18 removed

@AustinMroz AustinMroz marked this pull request as ready for review November 19, 2025 00:08
@dosubot dosubot bot added the size:XL This PR changes 500-999 lines, ignoring generated files. label Nov 19, 2025
doesn't correctly apply on initial load
Still includes some tempoary implementations and seems to have an issue
with flawed logic for removing inputs
When a node is freshly added to a workflow, there's not a configure
event and the widgets absed on the initial state were not applied
Callback is usually, but not always triggered when the value changes.

This had two downsides:
- The dynamic Combo code would usually apply twice on value change
- If there was any code that also added callbacks to the widget, it
  would be applied multiple times.

Both of these are fixed by not setting the widget change code as a
callback.
litegraphService had a very large amount of duplicated code that was
private methods. Private methods are awful and prevent reuse.

These methods have all been moved to anonymous functions with no
duplication. The primary purpose of this is to expose a single
addNodeInput function for use in dynamic widget code.

The battle to end duck violence is long and hard fought.
The functionality is dumb and wrong, but required to keep expected
sizing behaviour for nodes like Clip Text Encode
@DrJKL DrJKL force-pushed the austin/dynamic-widgets branch from a395bd1 to 756e391 Compare November 19, 2025 01:28
@DrJKL DrJKL self-assigned this Nov 19, 2025
widget.value = info.widgets_values[i]
const widgetsWithValue = this.widgets
.values()
.filter((w) => w.serialize !== false)
Copy link
Contributor

Choose a reason for hiding this comment

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

So if serialize is undefined we want to treat it as true?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep, but we need exact equals here. We can't use !w.serialize

DrJKL
DrJKL previously approved these changes Nov 19, 2025
DrJKL
DrJKL previously approved these changes Nov 19, 2025
Copy link
Contributor

@christian-byrne christian-byrne left a comment

Choose a reason for hiding this comment

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

If I were to try to refactor the dynamicWidgets.ts file with the goal of making it as expressive as possible, it would look something like this:

import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration'
import type { ComboInputSpec, InputSpec } from '@/schemas/nodeDefSchema'
import { zDynamicComboInputSpec } from '@/schemas/nodeDefSchema'
import { useLitegraphService } from '@/services/litegraphService'
import { app } from '@/scripts/app'
import type { ComfyApp } from '@/scripts/app'

/**
 * Validates and normalizes the dynamic combo input spec.
 */
function parseDynamicComboInputSpec(untypedInputData: InputSpec) {
  const parseResult = zDynamicComboInputSpec.safeParse(untypedInputData)
  if (!parseResult.success) throw new Error('invalid DynamicCombo spec')
  return parseResult.data
}

/**
 * Builds:
 *  - optionKeys: the list of keys shown in the base COMBO widget
 *  - optionInputsByKey: key → structured input spec for that option
 *
 * Key point: unlike a normal combo, each option maps to an object of input specs.
 */
function buildDynamicOptionIndex(dynamicComboSpec: unknown) {
  const [, dynamicOptions] = dynamicComboSpec as {
    1: { options: Array<{ key: string; inputs: unknown }> }
  }

  const optionInputsByKey = Object.fromEntries(
    dynamicOptions.options.map((option) => [option.key, option.inputs])
  )

  const optionKeys = dynamicOptions.options.map((option) => option.key)

  return { optionInputsByKey, optionKeys }
}

/**
 * Removes all inputs and widgets previously added for dynamic options.
 */
function clearDynamicInputsAndWidgets(
  node: LGraphNode,
  dynamicNames: string[]
) {
  for (const name of dynamicNames) {
    const inputIndex = node.inputs.findIndex((input) => input.name === name)
    if (inputIndex !== -1) node.removeInput(inputIndex)

    if (!node.widgets) continue
    const widgetIndex = node.widgets.findIndex((widget) => widget.name === name)
    if (widgetIndex === -1) continue

    node.widgets[widgetIndex].value = undefined
    node.widgets.splice(widgetIndex, 1)
  }

  return [] as string[]
}

/**
 * Adds inputs and widgets for the selected option and keeps them adjacent
 * to the base combo widget, preserving input order as much as possible.
 */
function addDynamicInputsAndWidgets(
  node: LGraphNode,
  baseWidget: any,
  newSpec: any,
  addNodeInput: (node: LGraphNode, spec: InputSpec) => void,
  currentDynamicNames: string[]
) {
  if (!node.widgets) throw new Error('Not Reachable')

  const insertionPoint = node.widgets.findIndex((w) => w === baseWidget) + 1
  const startingWidgetCount = node.widgets.length

  const inputInsertionPoint =
    node.inputs.findIndex((i) => i.name === baseWidget.name) + 1
  const startingInputCount = node.inputs.length

  if (insertionPoint === 0) {
    throw new Error("Dynamic widget doesn't exist on node")
  }

  const inputTypes: [Record<string, InputSpec> | undefined, boolean][] = [
    [newSpec.required, false],
    [newSpec.optional, true]
  ]

  for (const [inputType, isOptional] of inputTypes) {
    for (const name in inputType ?? {}) {
      addNodeInput(
        node,
        transformInputSpecV1ToV2(inputType![name], {
          name,
          isOptional
        })
      )
      currentDynamicNames.push(name)
    }
  }

  const addedWidgets = node.widgets.splice(startingWidgetCount)
  node.widgets.splice(insertionPoint, 0, ...addedWidgets)

  if (inputInsertionPoint === 0) {
    if (addedWidgets.length === 0 && node.inputs.length !== startingInputCount) {
      // input is inputOnly, but lacks an insertion point
      throw new Error('Failed to find input socket for ' + baseWidget.name)
    }
    return currentDynamicNames
  }

  const addedInputs = node
    .spliceInputs(startingInputCount)
    .map((addedInput) => {
      const existingIndex = node.inputs.findIndex(
        (existingInput) => addedInput.name === existingInput.name
      )

      return existingIndex === -1
        ? addedInput
        : node.spliceInputs(existingIndex, 1)[0]
    })

  // assume existing inputs are in correct order
  node.spliceInputs(inputInsertionPoint, 0, ...addedInputs)
  node.size[1] = node.computeSize([...node.size])[1]

  return currentDynamicNames
}

/**
 * Wraps a combo widget so that updating its value runs the provided updater.
 */
function attachDynamicUpdateToWidget(
  widget: any,
  updateWidgets: (value?: string) => void
) {
  let widgetValue = widget.value

  Object.defineProperty(widget, 'value', {
    get() {
      return widgetValue
    },
    set(value) {
      widgetValue = value
      updateWidgets(value)
    }
  })

  // trigger once with initial value
  updateWidgets(widgetValue)
}

function dynamicComboWidget(
  node: LGraphNode,
  inputName: string,
  untypedInputData: InputSpec,
  appArg: ComfyApp,
  widgetName?: string
) {
  const { addNodeInput } = useLitegraphService()

  const dynamicComboSpec = parseDynamicComboInputSpec(untypedInputData)
  const { optionInputsByKey, optionKeys } =
    buildDynamicOptionIndex(dynamicComboSpec)

  // The base COMBO only needs the list of option keys.
  const comboSpecForBaseWidget: ComboInputSpec = [optionKeys, {}]

  const { widget, minWidth, minHeight } = app.widgets['COMBO'](
    node,
    inputName,
    comboSpecForBaseWidget,
    appArg,
    widgetName
  )

  let currentDynamicNames: string[] = []

  const updateWidgets = (value?: string) => {
    if (!node.widgets) throw new Error('Not Reachable')

    const selectedSpec = value ? optionInputsByKey[value] : undefined

    currentDynamicNames = clearDynamicInputsAndWidgets(
      node,
      currentDynamicNames
    )

    if (!selectedSpec) return

    currentDynamicNames = addDynamicInputsAndWidgets(
      node,
      widget,
      selectedSpec,
      addNodeInput,
      currentDynamicNames
    )
  }

  attachDynamicUpdateToWidget(widget, updateWidgets)

  return { widget, minWidth, minHeight }
}

export const dynamicWidgets = { COMFY_DYNAMICCOMBO_V3: dynamicComboWidget }

Comment on lines +101 to +109
Object.defineProperty(widget, 'value', {
get() {
return widgetValue
},
set(value) {
widgetValue = value
updateWidgets(value)
}
})
Copy link
Contributor

Choose a reason for hiding this comment

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

Using the callback property doesn't work?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yep. Callback also isn't triggered after loading widgets_values, which means dynamic widgets wouldn't get created when loading a saved workflow.

widgetName
)
let currentDynamicNames: string[] = []
const updateWidgets = (value?: string) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

The semantics of updateWidget(value?: string): void and const newSpec = value ? options[value] : undefined are somewhat confusing. So this function is called when a widget is updated with a new value, but also updates other widgets. And the new spec is derived from an options object which is addressed by the value?

Maybe what the options and value objects represent could be clearer somehow .

Comment on lines +20 to +22
const options = Object.fromEntries(
inputData[1].options.map(({ key, inputs }) => [key, inputs])
)
Copy link
Contributor

Choose a reason for hiding this comment

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

I wonder if we could communicate more about the data structure and semantics of the system via variable names, helper functions, code flow, etc.

@christian-byrne
Copy link
Contributor

This code looks good, is there a comfyanonymous/ComfyUI branch I can test with?

@AustinMroz
Copy link
Collaborator Author

Core branch is v3-dynamic-combo.
A PR hasn't been created for it yet.

@christian-byrne christian-byrne added the claude-review Add to trigger a PR code review from Claude Code label Nov 19, 2025
@github-actions github-actions bot removed the claude-review Add to trigger a PR code review from Claude Code label Nov 19, 2025
@christian-byrne
Copy link
Contributor

The test seems to be failing.


⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯

 FAIL  tests-ui/tests/litegraph/core/LGraphNode.test.ts > LGraphNode > widget serialization > should only configure widgets with serialize flag not set to false
 FAIL  tests-ui/tests/litegraph/core/LGraphNode.test.ts > LGraphNode > widget serialization > should only configure widgets with serialize flag not set to false
 FAIL  tests-ui/tests/litegraph/core/LGraphNode.test.ts > LGraphNode > widget serialization > should only configure widgets with serialize flag not set to false
AssertionError: expected 2 to be 100 // Object.is equality

- Expected
+ Received

- 100
+ 2

 ❯ tests-ui/tests/litegraph/core/LGraphNode.test.ts:562:38
    560| 
    561|       expect(node.widgets![0].value).toBe(1)
    562|       expect(node.widgets![1].value).toBe(100)
       |                                      ^
    563|     })
    564|   })

⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/6]⎯

 FAIL  tests-ui/tests/litegraph/core/LGraphNode.widgetOrder.test.ts > LGraphNode widget ordering > configure with widgets_values > should skip widgets with serialize: false
 FAIL  tests-ui/tests/litegraph/core/LGraphNode.widgetOrder.test.ts > LGraphNode widget ordering > configure with widgets_values > should skip widgets with serialize: false
 FAIL  tests-ui/tests/litegraph/core/LGraphNode.widgetOrder.test.ts > LGraphNode widget ordering > configure with widgets_values > should skip widgets with serialize: false

@AustinMroz
Copy link
Collaborator Author

Got bamboozled by the green checkmark on the last code commit and assumed flake. Should be fixed now.

@christian-byrne christian-byrne merged commit bc553f1 into main Nov 20, 2025
41 of 42 checks passed
@christian-byrne christian-byrne deleted the austin/dynamic-widgets branch November 20, 2025 23:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL This PR changes 500-999 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants