Skip to content

Conversation

@christian-byrne
Copy link
Contributor

@christian-byrne christian-byrne commented Jan 28, 2026

Summary

Fixes image/video previews getting stuck in loading state when widgets are added dynamically to a node.

Problem

When dynamic widgets are added to a node (e.g., by extensions), Vue reactivity triggers the watch on imageUrls prop even when the URL content is identical—the array has a new reference but the same values. This caused:

  1. startDelayedLoader() to reset loading state to pending
  2. If the cached image doesn't trigger @load before the 250ms timeout, the loader shows and stays stuck

Solution

Compare URL arrays by content, not reference. Only reset loading state when URLs actually change:

  • Check array length and element-by-element equality
  • Return early if URLs are identical (just a new array reference)
  • Remove deep: true since we compare manually

Screenshots

image Screenshot from 2026-01-28 15-24-18 Screenshot from 2026-01-28 15-24-05

┆Issue is synchronized with this Notion page by Unito

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 28, 2026

📝 Walkthrough

Walkthrough

This PR adds URL change detection guards to image and video preview component watchers to prevent unnecessary state resets when array references change without content changes. It removes deep watching and adds test coverage for the new behavior, while also refactoring media type detection in LGraphNode.

Changes

Cohort / File(s) Summary
Preview Component Watch Guards
src/renderer/extensions/vueNodes/VideoPreview.vue, src/renderer/extensions/vueNodes/components/ImagePreview.vue
Enhanced watchers with URL content comparison logic to avoid state resets on identical array content. Removed deep watching while retaining immediate flag. Both components now skip state reset when array length or individual URL elements remain unchanged.
ImagePreview Test Coverage
src/renderer/extensions/vueNodes/components/ImagePreview.test.ts
Added three new tests for URL change detection: validates loading state is not reset for identical URL content with different array references, correctly resets on actual URL changes, and handles transitions from empty to non-empty URL arrays.
Node Media Type Detection
src/renderer/extensions/vueNodes/components/LGraphNode.vue
Introduced computed hasVideoInput property to detect VIDEO-type inputs. Updated nodeMedia logic to use this computed property for cleaner media type classification between video and image.

Possibly related PRs

Suggested reviewers

  • AustinMroz
  • DrJKL
  • jtydhr88
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch dynamic-widget-rerender-fix

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@github-actions
Copy link

github-actions bot commented Jan 28, 2026

🎨 Storybook Build Status

Build completed successfully!

⏰ Completed at: 01/30/2026, 07:04:52 AM UTC

🔗 Links


🎉 Your Storybook is ready for review!

@github-actions
Copy link

github-actions bot commented Jan 28, 2026

🎭 Playwright Tests: ✅ Passed

Results: 507 passed, 0 failed, 0 flaky, 8 skipped (Total: 515)

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

@github-actions
Copy link

github-actions bot commented Jan 28, 2026

Bundle Size Report

Summary

  • Raw size: 22.1 MB baseline 22.1 MB — 🔴 +57 B
  • Gzip: 4.61 MB baseline 4.61 MB — 🟢 -11 B
  • Brotli: 3.42 MB baseline 3.42 MB — 🟢 -173 B
  • Bundles: 173 current • 173 baseline • 81 added / 81 removed

Category Glance
Graph Workspace 🔴 +262 B (974 kB) · Other 🟢 -198 B (7.1 MB) · Panels & Settings 🟢 -8 B (471 kB) · Data & Services 🔴 +1 B (2.71 MB) · Vendor & Third-Party ⚪ 0 B (10.7 MB) · Views & Navigation ⚪ 0 B (80.7 kB) · + 5 more

Per-category breakdown
App Entry Points — 26 kB (baseline 26 kB) • ⚪ 0 B

Main entry bundles and manifests

File Before After Δ Raw Δ Gzip Δ Brotli
assets/index-0o8eSuaX.js (removed) 26 kB 🟢 -26 kB 🟢 -7.5 kB 🟢 -6.61 kB
assets/index-pNNaUKhU.js (new) 26 kB 🔴 +26 kB 🔴 +7.51 kB 🔴 +6.63 kB

Status: 1 added / 1 removed

Graph Workspace — 974 kB (baseline 974 kB) • 🔴 +262 B

Graph editor runtime, canvas, workflow orchestration

File Before After Δ Raw Δ Gzip Δ Brotli
assets/GraphView-Cup03xZ8.js (new) 974 kB 🔴 +974 kB 🔴 +197 kB 🔴 +149 kB
assets/GraphView-BbQqs2Ly.js (removed) 974 kB 🟢 -974 kB 🟢 -197 kB 🟢 -149 kB

Status: 1 added / 1 removed

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

Top-level views, pages, and routed surfaces

File Before After Δ Raw Δ Gzip Δ Brotli
assets/CloudSurveyView-A5hVrC81.js (new) 17.1 kB 🔴 +17.1 kB 🔴 +3.6 kB 🔴 +3.05 kB
assets/CloudSurveyView-B8LkFGIm.js (removed) 17.1 kB 🟢 -17.1 kB 🟢 -3.61 kB 🟢 -3.05 kB
assets/CloudLoginView-C08UEOos.js (new) 11.8 kB 🔴 +11.8 kB 🔴 +3.09 kB 🔴 +2.71 kB
assets/CloudLoginView-C9R0_Ugu.js (removed) 11.8 kB 🟢 -11.8 kB 🟢 -3.09 kB 🟢 -2.71 kB
assets/UserCheckView-6hRsAnNZ.js (new) 10.5 kB 🔴 +10.5 kB 🔴 +2.45 kB 🔴 +2.13 kB
assets/UserCheckView-CLwAHgYQ.js (removed) 10.5 kB 🟢 -10.5 kB 🟢 -2.44 kB 🟢 -2.13 kB
assets/CloudLayoutView-BB2Oum5f.js (removed) 8.54 kB 🟢 -8.54 kB 🟢 -2.24 kB 🟢 -1.96 kB
assets/CloudLayoutView-BMQwcas1.js (new) 8.54 kB 🔴 +8.54 kB 🔴 +2.24 kB 🔴 +1.96 kB
assets/CloudSignupView-B3qZgE2h.js (removed) 8.18 kB 🟢 -8.18 kB 🟢 -2.32 kB 🟢 -2.02 kB
assets/CloudSignupView-BshizAoh.js (new) 8.18 kB 🔴 +8.18 kB 🔴 +2.32 kB 🔴 +2.02 kB
assets/CloudForgotPasswordView-B1msQ3bg.js (removed) 6.26 kB 🟢 -6.26 kB 🟢 -1.93 kB 🟢 -1.69 kB
assets/CloudForgotPasswordView-D1_GnZod.js (new) 6.26 kB 🔴 +6.26 kB 🔴 +1.93 kB 🔴 +1.69 kB
assets/UserSelectView-Bp8F8VYx.js (new) 5.28 kB 🔴 +5.28 kB 🔴 +1.76 kB 🔴 +1.57 kB
assets/UserSelectView-D0PLhTti.js (removed) 5.28 kB 🟢 -5.28 kB 🟢 -1.76 kB 🟢 -1.57 kB
assets/CloudSubscriptionRedirectView-DPadqUKi.js (removed) 5.27 kB 🟢 -5.27 kB 🟢 -1.73 kB 🟢 -1.54 kB
assets/CloudSubscriptionRedirectView-mhuRMWE3.js (new) 5.27 kB 🔴 +5.27 kB 🔴 +1.73 kB 🔴 +1.54 kB
assets/CloudAuthTimeoutView-BevAbWnL.js (removed) 5.24 kB 🟢 -5.24 kB 🟢 -1.7 kB 🟢 -1.48 kB
assets/CloudAuthTimeoutView-JuWsVkaO.js (new) 5.24 kB 🔴 +5.24 kB 🔴 +1.7 kB 🔴 +1.49 kB
assets/CloudSorryContactSupportView-cm9oKn4s.js 1.97 kB 1.97 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/layout-CUzumK-h.js 500 B 500 B ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 9 added / 9 removed

Panels & Settings — 471 kB (baseline 471 kB) • 🟢 -8 B

Configuration panels, inspectors, and settings screens

File Before After Δ Raw Δ Gzip Δ Brotli
assets/WorkspacePanel-22RIfqld.js (removed) 29.8 kB 🟢 -29.8 kB 🟢 -5.89 kB 🟢 -5.14 kB
assets/WorkspacePanel-jStFDNBt.js (new) 29.8 kB 🔴 +29.8 kB 🔴 +5.89 kB 🔴 +5.14 kB
assets/LegacyCreditsPanel-DSHi1D4z.js (removed) 23.8 kB 🟢 -23.8 kB 🟢 -5.94 kB 🟢 -5.22 kB
assets/LegacyCreditsPanel-fCSRYZE1.js (new) 23.8 kB 🔴 +23.8 kB 🔴 +5.94 kB 🔴 +5.23 kB
assets/SubscriptionPanel-DfYa0RWD.js (new) 21 kB 🔴 +21 kB 🔴 +5.04 kB 🔴 +4.43 kB
assets/SubscriptionPanel-DpViEyFm.js (removed) 21 kB 🟢 -21 kB 🟢 -5.04 kB 🟢 -4.44 kB
assets/KeybindingPanel-Cqh2zhF5.js (removed) 14.3 kB 🟢 -14.3 kB 🟢 -3.76 kB 🟢 -3.34 kB
assets/KeybindingPanel-CYFTn97g.js (new) 14.3 kB 🔴 +14.3 kB 🔴 +3.77 kB 🔴 +3.34 kB
assets/AboutPanel-CDXO6G8j.js (new) 10.8 kB 🔴 +10.8 kB 🔴 +2.68 kB 🔴 +2.43 kB
assets/AboutPanel-nAPeX13O.js (removed) 10.8 kB 🟢 -10.8 kB 🟢 -2.68 kB 🟢 -2.43 kB
assets/ExtensionPanel-7cZHyZm9.js (new) 10.2 kB 🔴 +10.2 kB 🔴 +2.71 kB 🔴 +2.4 kB
assets/ExtensionPanel-BN17h4C0.js (removed) 10.2 kB 🟢 -10.2 kB 🟢 -2.71 kB 🟢 -2.4 kB
assets/ServerConfigPanel-BjWZK3xJ.js (new) 7.23 kB 🔴 +7.23 kB 🔴 +2.17 kB 🔴 +1.94 kB
assets/ServerConfigPanel-Drtk3Cj9.js (removed) 7.23 kB 🟢 -7.23 kB 🟢 -2.17 kB 🟢 -1.94 kB
assets/UserPanel-BH0BNckB.js (new) 6.58 kB 🔴 +6.58 kB 🔴 +1.91 kB 🔴 +1.67 kB
assets/UserPanel-CX_ZP5HA.js (removed) 6.58 kB 🟢 -6.58 kB 🟢 -1.91 kB 🟢 -1.68 kB
assets/refreshRemoteConfig-BvAnKw7S.js (removed) 1.31 kB 🟢 -1.31 kB 🟢 -571 B 🟢 -496 B
assets/refreshRemoteConfig-CGC9ABps.js (new) 1.31 kB 🔴 +1.31 kB 🔴 +570 B 🔴 +503 B
assets/config-CQmXnrXP.js (removed) 1.16 kB 🟢 -1.16 kB 🟢 -609 B 🟢 -539 B
assets/config-LSjF2vmc.js (new) 1.15 kB 🔴 +1.15 kB 🔴 +604 B 🔴 +534 B
assets/cloudRemoteConfig-BpfcOE1q.js (removed) 1.11 kB 🟢 -1.11 kB 🟢 -509 B 🟢 -439 B
assets/cloudRemoteConfig-vtItT28I.js (new) 1.11 kB 🔴 +1.11 kB 🔴 +508 B 🔴 +439 B
assets/refreshRemoteConfig-Bckc2EMm.js (removed) 169 B 🟢 -169 B 🟢 -108 B 🟢 -102 B
assets/refreshRemoteConfig-DkVQqShu.js (new) 169 B 🔴 +169 B 🔴 +108 B 🔴 +101 B
assets/remoteConfig-B0mlVvm7.js 788 B 788 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-2UNjEj6k.js 32.9 kB 32.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-B2OMGvh7.js 31.2 kB 31.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-BcujOfpn.js 29.6 kB 29.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-BI09_t23.js 29.4 kB 29.4 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-BKamuseh.js 25.8 kB 25.8 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-BlTun9tZ.js 26.4 kB 26.4 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-CZ62uO3e.js 30.2 kB 30.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-DaK-NByz.js 35.2 kB 35.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-DaS3cSXp.js 39.4 kB 39.4 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-DWbMuaAa.js 32 kB 32 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/settings-S7pA60Hj.js 30.4 kB 30.4 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 12 added / 12 removed

User & Accounts — 3.94 kB (baseline 3.94 kB) • ⚪ 0 B

Authentication, profile, and account management bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/auth-CBmfDyH9.js (new) 3.54 kB 🔴 +3.54 kB 🔴 +1.24 kB 🔴 +1.07 kB
assets/auth-D3luOwL2.js (removed) 3.54 kB 🟢 -3.54 kB 🟢 -1.24 kB 🟢 -1.07 kB
assets/firebaseAuthStore-CejFsuno.js (new) 217 B 🔴 +217 B 🔴 +138 B 🔴 +121 B
assets/firebaseAuthStore-vCBMphFP.js (removed) 217 B 🟢 -217 B 🟢 -138 B 🟢 -121 B
assets/auth-BPMO3atV.js (removed) 178 B 🟢 -178 B 🟢 -142 B 🟢 -143 B
assets/auth-C5B9ZVGK.js (new) 178 B 🔴 +178 B 🔴 +142 B 🔴 +132 B

Status: 3 added / 3 removed

Editors & Dialogs — 2.89 kB (baseline 2.89 kB) • ⚪ 0 B

Modals, dialogs, drawers, and in-app editors

File Before After Δ Raw Δ Gzip Δ Brotli
assets/useSubscriptionDialog-G-t287Mu.js (new) 2.71 kB 🔴 +2.71 kB 🔴 +1.29 kB 🔴 +1.14 kB
assets/useSubscriptionDialog-nhvd3Qi8.js (removed) 2.71 kB 🟢 -2.71 kB 🟢 -1.28 kB 🟢 -1.15 kB
assets/useSubscriptionDialog-1nPIsiTW.js (removed) 179 B 🟢 -179 B 🟢 -110 B 🟢 -101 B
assets/useSubscriptionDialog-Bwr2VtEu.js (new) 179 B 🔴 +179 B 🔴 +110 B 🔴 +102 B

Status: 2 added / 2 removed

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

Reusable component library chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/ComfyQueueButton-B7AzI4p_.js (removed) 9.52 kB 🟢 -9.52 kB 🟢 -2.69 kB 🟢 -2.42 kB
assets/ComfyQueueButton-BMMG_ErT.js (new) 9.52 kB 🔴 +9.52 kB 🔴 +2.68 kB 🔴 +2.41 kB
assets/SubscribeButton-CGA_Qq0B.js (new) 4.63 kB 🔴 +4.63 kB 🔴 +1.57 kB 🔴 +1.39 kB
assets/SubscribeButton-CgElFdpH.js (removed) 4.63 kB 🟢 -4.63 kB 🟢 -1.57 kB 🟢 -1.39 kB
assets/cloudFeedbackTopbarButton-BFQoPxBi.js (removed) 1.24 kB 🟢 -1.24 kB 🟢 -675 B 🟢 -573 B
assets/cloudFeedbackTopbarButton-DXqB_qf_.js (new) 1.24 kB 🔴 +1.24 kB 🔴 +675 B 🔴 +573 B
assets/ComfyQueueButton-3QdV-nUE.js (new) 181 B 🔴 +181 B 🔴 +118 B 🔴 +120 B
assets/ComfyQueueButton-DhH2bjjk.js (removed) 181 B 🟢 -181 B 🟢 -118 B 🟢 -124 B
assets/Button-DbRyW27H.js 3.82 kB 3.82 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/CloudBadge-DdpkQvvD.js 1.85 kB 1.85 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/TopbarBadge-BFw4kSAY.js 8.36 kB 8.36 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/UserAvatar-D80lITos.js 1.73 kB 1.73 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetButton-CVau1vM3.js 2.41 kB 2.41 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 4 added / 4 removed

Data & Services — 2.71 MB (baseline 2.71 MB) • 🔴 +1 B

Stores, services, APIs, and repositories

File Before After Δ Raw Δ Gzip Δ Brotli
assets/dialogService-C0jZxvrW.js (removed) 2.01 MB 🟢 -2.01 MB 🟢 -425 kB 🟢 -324 kB
assets/dialogService-Cr0KR5Ae.js (new) 2.01 MB 🔴 +2.01 MB 🔴 +425 kB 🔴 +324 kB
assets/api-CnGotM8e.js (new) 675 kB 🔴 +675 kB 🔴 +149 kB 🔴 +119 kB
assets/api-0_xJMVsr.js (removed) 675 kB 🟢 -675 kB 🟢 -149 kB 🟢 -119 kB
assets/releaseStore-Cpg7QRvX.js (new) 8.91 kB 🔴 +8.91 kB 🔴 +2.4 kB 🔴 +2.12 kB
assets/releaseStore-Msq5KIWQ.js (removed) 8.91 kB 🟢 -8.91 kB 🟢 -2.4 kB 🟢 -2.12 kB
assets/keybindingService-Db47K4LT.js (new) 6.74 kB 🔴 +6.74 kB 🔴 +1.76 kB 🔴 +1.53 kB
assets/keybindingService-DyZNHzol.js (removed) 6.74 kB 🟢 -6.74 kB 🟢 -1.75 kB 🟢 -1.53 kB
assets/bootstrapStore-CT9MfHnC.js (removed) 2.69 kB 🟢 -2.69 kB 🟢 -1.03 kB 🟢 -967 B
assets/bootstrapStore-teDuukNJ.js (new) 2.69 kB 🔴 +2.69 kB 🔴 +1.02 kB 🔴 +967 B
assets/userStore-Bbs7yE1D.js (removed) 2.16 kB 🟢 -2.16 kB 🟢 -811 B 🟢 -724 B
assets/userStore-funG3U2d.js (new) 2.16 kB 🔴 +2.16 kB 🔴 +809 B 🔴 +722 B
assets/audioService-C4ihUw-I.js (removed) 2.03 kB 🟢 -2.03 kB 🟢 -929 B 🟢 -808 B
assets/audioService-D5Ou7oKf.js (new) 2.03 kB 🔴 +2.03 kB 🔴 +929 B 🔴 +813 B
assets/releaseStore-CjcnHj8p.js (new) 140 B 🔴 +140 B 🔴 +106 B 🔴 +108 B
assets/releaseStore-RM6-Y5p0.js (removed) 140 B 🟢 -140 B 🟢 -106 B 🟢 -110 B
assets/serverConfigStore-DOoqLe5c.js 2.64 kB 2.64 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 8 added / 8 removed

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

Helpers, composables, and utility bundles

File Before After Δ Raw Δ Gzip Δ Brotli
assets/useErrorHandling-BJ4-7oLQ.js (removed) 5.21 kB 🟢 -5.21 kB 🟢 -1.53 kB 🟢 -1.34 kB
assets/useErrorHandling-Dbg_6JQ9.js (new) 5.21 kB 🔴 +5.21 kB 🔴 +1.53 kB 🔴 +1.34 kB
assets/useWorkspaceUI-D5brUGrh.js (new) 3.42 kB 🔴 +3.42 kB 🔴 +975 B 🔴 +838 B
assets/useWorkspaceUI-u-o6kaJ8.js (removed) 3.42 kB 🟢 -3.42 kB 🟢 -974 B 🟢 -843 B
assets/useSubscriptionActions-bSXPbtBx.js (new) 2.22 kB 🔴 +2.22 kB 🔴 +866 B 🔴 +762 B
assets/useSubscriptionActions-C_-bV1aq.js (removed) 2.22 kB 🟢 -2.22 kB 🟢 -867 B 🟢 -763 B
assets/subscriptionCheckoutUtil-CDQ8THBd.js (removed) 2.03 kB 🟢 -2.03 kB 🟢 -871 B 🟢 -771 B
assets/subscriptionCheckoutUtil-ClODtj-V.js (new) 2.03 kB 🔴 +2.03 kB 🔴 +868 B 🔴 +759 B
assets/useSubscriptionCredits-Chj6K3LE.js (removed) 1.39 kB 🟢 -1.39 kB 🟢 -600 B 🟢 -529 B
assets/useSubscriptionCredits-DTsEwo0S.js (new) 1.39 kB 🔴 +1.39 kB 🔴 +598 B 🔴 +531 B
assets/audioUtils-CJEpTYmi.js (removed) 970 B 🟢 -970 B 🟢 -547 B 🟢 -459 B
assets/audioUtils-Coo1OTId.js (new) 970 B 🔴 +970 B 🔴 +546 B 🔴 +460 B
assets/useCurrentUser-_XKMqbeH.js (removed) 145 B 🟢 -145 B 🟢 -114 B 🟢 -104 B
assets/useCurrentUser-D0gGuUQ6.js (new) 145 B 🔴 +145 B 🔴 +114 B 🔴 +106 B
assets/_plugin-vue_export-helper-DuK_Fly3.js 467 B 467 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/colorUtil-DfMUHmsF.js 7.2 kB 7.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/markdownRendererUtil-DM9z_tTX.js 1.78 kB 1.78 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/tailwindUtil-BWBAZ7f9.js 488 B 488 B ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 7 added / 7 removed

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

External libraries and shared vendor chunks

File Before After Δ Raw Δ Gzip Δ Brotli
assets/vendor-chart-DHGfk3hn.js 408 kB 408 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-other-B3TsI6ya.js 4.1 MB 4.1 MB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-primevue-4Jj8eU28.js 3.04 MB 3.04 MB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-reka-ui-aCG649nF.js 263 kB 263 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-three-CERwhPwK.js 1.83 MB 1.83 MB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-tiptap-BxrEVL6s.js 650 kB 650 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-vue-Dwii0E-t.js 13.6 kB 13.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/vendor-xterm-IX6P8SWv.js 398 kB 398 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
Other — 7.1 MB (baseline 7.1 MB) • 🟢 -198 B

Bundles that do not match a named category

File Before After Δ Raw Δ Gzip Δ Brotli
assets/core-Bpml2FxC.js (removed) 180 kB 🟢 -180 kB 🟢 -43.3 kB 🟢 -36.2 kB
assets/core-DkdXAl3o.js (new) 180 kB 🔴 +180 kB 🔴 +43.3 kB 🔴 +36.2 kB
assets/WidgetSelect-BuaFBno9.js (new) 52.2 kB 🔴 +52.2 kB 🔴 +11.5 kB 🔴 +10 kB
assets/WidgetSelect-CAtMnQpb.js (removed) 52.2 kB 🟢 -52.2 kB 🟢 -11.5 kB 🟢 -10 kB
assets/Load3DControls-CG_T8fHV.js (new) 35.9 kB 🔴 +35.9 kB 🔴 +5.87 kB 🔴 +5.08 kB
assets/Load3DControls-visAYFcK.js (removed) 35.9 kB 🟢 -35.9 kB 🟢 -5.87 kB 🟢 -5.09 kB
assets/SubscriptionRequiredDialogContent-NC2Wm-qi.js (removed) 28.7 kB 🟢 -28.7 kB 🟢 -6.79 kB 🟢 -5.92 kB
assets/SubscriptionRequiredDialogContent-y0UzAo8P.js (new) 28.7 kB 🔴 +28.7 kB 🔴 +6.79 kB 🔴 +5.92 kB
assets/CurrentUserPopoverWorkspace-CiHD0e4i.js (new) 22.2 kB 🔴 +22.2 kB 🔴 +4.99 kB 🔴 +4.42 kB
assets/CurrentUserPopoverWorkspace-IK21Y1TX.js (removed) 22.2 kB 🟢 -22.2 kB 🟢 -4.99 kB 🟢 -4.42 kB
assets/Load3D-BxOd_BA7.js (new) 19.2 kB 🔴 +19.2 kB 🔴 +4.38 kB 🔴 +3.85 kB
assets/Load3D-DM6Rqpu-.js (removed) 19.2 kB 🟢 -19.2 kB 🟢 -4.38 kB 🟢 -3.84 kB
assets/WidgetInputNumber-BdJ4-C5f.js (new) 18.3 kB 🔴 +18.3 kB 🔴 +4.53 kB 🔴 +4.03 kB
assets/WidgetInputNumber-C1zzD6EW.js (removed) 18.3 kB 🟢 -18.3 kB 🟢 -4.53 kB 🟢 -4.03 kB
assets/WidgetRecordAudio-CNAWyAke.js (removed) 18.3 kB 🟢 -18.3 kB 🟢 -4.97 kB 🟢 -4.44 kB
assets/WidgetRecordAudio-fWP03CGs.js (new) 18.3 kB 🔴 +18.3 kB 🔴 +4.97 kB 🔴 +4.44 kB
assets/SubscriptionPanelContentWorkspace-D_0MeVoR.js (removed) 18.2 kB 🟢 -18.2 kB 🟢 -4.47 kB 🟢 -3.9 kB
assets/SubscriptionPanelContentWorkspace-D-50cKcg.js (new) 18.2 kB 🔴 +18.2 kB 🔴 +4.47 kB 🔴 +3.9 kB
assets/WidgetImageCrop-B8vN0f5F.js (removed) 17.1 kB 🟢 -17.1 kB 🟢 -4.14 kB 🟢 -3.63 kB
assets/WidgetImageCrop-BcQjHBVD.js (new) 17.1 kB 🔴 +17.1 kB 🔴 +4.14 kB 🔴 +3.63 kB
assets/PanelTemplate-2JgpsoeK.js (removed) 16.2 kB 🟢 -16.2 kB 🟢 -5.45 kB 🟢 -4.8 kB
assets/PanelTemplate-J-u2S9rS.js (new) 16.2 kB 🔴 +16.2 kB 🔴 +5.45 kB 🔴 +4.79 kB
assets/AudioPreviewPlayer-Brfeorqe.js (removed) 10.8 kB 🟢 -10.8 kB 🟢 -2.97 kB 🟢 -2.65 kB
assets/AudioPreviewPlayer-CaoF30mZ.js (new) 10.8 kB 🔴 +10.8 kB 🔴 +2.97 kB 🔴 +2.65 kB
assets/InviteMemberDialogContent-Cns2g9Rm.js (removed) 8.36 kB 🟢 -8.36 kB 🟢 -2.5 kB 🟢 -2.17 kB
assets/InviteMemberDialogContent-DCTyAXEx.js (new) 8.36 kB 🔴 +8.36 kB 🔴 +2.51 kB 🔴 +2.17 kB
assets/WidgetWithControl-B7o7fRkM.js (new) 8.07 kB 🔴 +8.07 kB 🔴 +2.68 kB 🔴 +2.41 kB
assets/WidgetWithControl-BhNk_cqm.js (removed) 8.07 kB 🟢 -8.07 kB 🟢 -2.68 kB 🟢 -2.41 kB
assets/CreateWorkspaceDialogContent-Bg3B71vi.js (removed) 5.93 kB 🟢 -5.93 kB 🟢 -1.92 kB 🟢 -1.68 kB
assets/CreateWorkspaceDialogContent-Dtl7fO2d.js (new) 5.93 kB 🔴 +5.93 kB 🔴 +1.92 kB 🔴 +1.68 kB
assets/EditWorkspaceDialogContent-BUzR5x5W.js (removed) 5.7 kB 🟢 -5.7 kB 🟢 -1.88 kB 🟢 -1.65 kB
assets/EditWorkspaceDialogContent-DE7_GqqA.js (new) 5.7 kB 🔴 +5.7 kB 🔴 +1.88 kB 🔴 +1.64 kB
assets/ValueControlPopover-BhK_bF9-.js (new) 5.17 kB 🔴 +5.17 kB 🔴 +1.69 kB 🔴 +1.5 kB
assets/ValueControlPopover-DdL0wMpV.js (removed) 5.17 kB 🟢 -5.17 kB 🟢 -1.69 kB 🟢 -1.5 kB
assets/DeleteWorkspaceDialogContent-CSiWWbCR.js (new) 4.59 kB 🔴 +4.59 kB 🔴 +1.56 kB 🔴 +1.35 kB
assets/DeleteWorkspaceDialogContent-Do8LHd_w.js (removed) 4.59 kB 🟢 -4.59 kB 🟢 -1.56 kB 🟢 -1.35 kB
assets/LeaveWorkspaceDialogContent-C5ayjfdf.js (removed) 4.41 kB 🟢 -4.41 kB 🟢 -1.5 kB 🟢 -1.31 kB
assets/LeaveWorkspaceDialogContent-D2RkngUw.js (new) 4.41 kB 🔴 +4.41 kB 🔴 +1.5 kB 🔴 +1.3 kB
assets/RemoveMemberDialogContent-De2r4_LU.js (new) 4.38 kB 🔴 +4.38 kB 🔴 +1.45 kB 🔴 +1.27 kB
assets/RemoveMemberDialogContent-jR-phpTt.js (removed) 4.38 kB 🟢 -4.38 kB 🟢 -1.45 kB 🟢 -1.27 kB
assets/RevokeInviteDialogContent-B_NRigYz.js (removed) 4.29 kB 🟢 -4.29 kB 🟢 -1.47 kB 🟢 -1.29 kB
assets/RevokeInviteDialogContent-CvEgHSzj.js (new) 4.29 kB 🔴 +4.29 kB 🔴 +1.47 kB 🔴 +1.29 kB
assets/GlobalToast-Bfq1l41B.js (removed) 3.05 kB 🟢 -3.05 kB 🟢 -1.1 kB 🟢 -941 B
assets/GlobalToast-BI2nkyMq.js (new) 3.05 kB 🔴 +3.05 kB 🔴 +1.1 kB 🔴 +947 B
assets/SubscribeToRun-BNX1ZG14.js (removed) 2.96 kB 🟢 -2.96 kB 🟢 -1.15 kB 🟢 -1.01 kB
assets/SubscribeToRun-DlX6XVsS.js (new) 2.96 kB 🔴 +2.96 kB 🔴 +1.15 kB 🔴 +1.01 kB
assets/cloudSessionCookie-DDfVmgr7.js (new) 2.94 kB 🔴 +2.94 kB 🔴 +928 B 🔴 +800 B
assets/cloudSessionCookie-DwHfnsk8.js (removed) 2.94 kB 🟢 -2.94 kB 🟢 -930 B 🟢 -800 B
assets/BaseViewTemplate-B2Ree9mF.js (removed) 2.42 kB 🟢 -2.42 kB 🟢 -1.04 kB 🟢 -941 B
assets/BaseViewTemplate-DBylaOKK.js (new) 2.42 kB 🔴 +2.42 kB 🔴 +1.04 kB 🔴 +941 B
assets/CloudRunButtonWrapper-6pi3Dhki.js (new) 1.79 kB 🔴 +1.79 kB 🔴 +644 B 🔴 +561 B
assets/CloudRunButtonWrapper-CPvudAWU.js (removed) 1.79 kB 🟢 -1.79 kB 🟢 -646 B 🟢 -597 B
assets/cloudBadges-CAqWxFKY.js (new) 1.08 kB 🔴 +1.08 kB 🔴 +538 B 🔴 +476 B
assets/cloudBadges-CLORO5-B.js (removed) 1.08 kB 🟢 -1.08 kB 🟢 -537 B 🟢 -498 B
assets/graphHasMissingNodes-BaIjF0fM.js (removed) 1.06 kB 🟢 -1.06 kB 🟢 -461 B 🟢 -419 B
assets/graphHasMissingNodes-Dz3PfNji.js (new) 1.06 kB 🔴 +1.06 kB 🔴 +461 B 🔴 +411 B
assets/cloudSubscription-CdjFYj--.js (removed) 976 B 🟢 -976 B 🟢 -464 B 🟢 -404 B
assets/cloudSubscription-CiysEVfR.js (new) 976 B 🔴 +976 B 🔴 +463 B 🔴 +403 B
assets/nightlyBadges-Bjnws1lH.js (new) 595 B 🔴 +595 B 🔴 +355 B 🔴 +308 B
assets/nightlyBadges-wXTaIV9p.js (removed) 595 B 🟢 -595 B 🟢 -356 B 🟢 -309 B
assets/SubscriptionPanelContentWorkspace-CFHGZeNa.js (removed) 266 B 🟢 -266 B 🟢 -136 B 🟢 -125 B
assets/SubscriptionPanelContentWorkspace-DxHeOXkd.js (new) 266 B 🔴 +266 B 🔴 +136 B 🔴 +117 B
assets/WidgetInputNumber-CCYjC_Wt.js (new) 186 B 🔴 +186 B 🔴 +119 B 🔴 +110 B
assets/WidgetInputNumber-DxdSVo7u.js (removed) 186 B 🟢 -186 B 🟢 -119 B 🟢 -110 B
assets/WidgetLegacy-CfV43XPn.js (new) 164 B 🔴 +164 B 🔴 +125 B 🔴 +117 B
assets/WidgetLegacy-DyaDuMR_.js (removed) 164 B 🟢 -164 B 🟢 -125 B 🟢 -113 B
assets/Load3D-CBfHqDES.js (new) 131 B 🔴 +131 B 🔴 +107 B 🔴 +116 B
assets/Load3D-CgfyUqU3.js (removed) 131 B 🟢 -131 B 🟢 -107 B 🟢 -113 B
assets/auto-DWs2ctGL.js 1.73 kB 1.73 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-BEw5ErI4.js 18.5 kB 18.5 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-BGeHkplA.js 17.9 kB 17.9 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-BV0l36Iz.js 17.2 kB 17.2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-C_Y3D6Cn.js 17.8 kB 17.8 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-C6piRza5.js 19.3 kB 19.3 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-Cf8Zq1td.js 18.8 kB 18.8 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-CiziP3Xs.js 18 kB 18 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-D1595tOr.js 19.3 kB 19.3 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-DXauvccL.js 20.6 kB 20.6 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-P5QCEfZc.js 18 kB 18 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/commands-WbYP_D61.js 17 kB 17 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/i18n-DKbrI9yU.js 500 kB 500 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/i18n-Do-i5KgH.js 188 B 188 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/LazyImage-ooHoQZNd.js 14.1 kB 14.1 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-BHtk4Fg_.js 174 kB 174 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-BMSlgLcp.js 155 kB 155 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-BQCWi9e4.js 112 kB 112 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-CJicmTR7.js 113 kB 113 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-CNOkBy-u.js 126 kB 126 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-CySb1R5_.js 151 kB 151 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-D0g10ZKf.js 131 kB 131 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-DMUPIFMF.js 133 kB 133 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-DpsGU4si.js 126 kB 126 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-Dz6IPJXM.js 144 kB 144 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/main-lrEzMywH.js 128 kB 128 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/Media3DTop-DUmUhXD6.js 2.38 kB 2.38 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/MediaAudioTop-CD66_Mw_.js 2 kB 2 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/MediaImageTop-D5feRGXX.js 2.34 kB 2.34 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/MediaVideoTop-De3MzVmp.js 2.82 kB 2.82 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/mixpanel.module-DLR992B1.js 143 B 143 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-A7pvB7zM.js 370 kB 370 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-BIVjUijC.js 345 kB 345 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-Chkn0HaI.js 343 kB 343 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-CK_6GHao.js 452 kB 452 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-CToVAwnT.js 373 kB 373 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-DDabdWgx.js 417 kB 417 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-DgvJyE3d.js 386 kB 386 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-DjxaeFt_.js 416 kB 416 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-DslnWEGg.js 377 kB 377 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-EPAM3kwk.js 373 kB 373 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/nodeDefs-NrulhNyH.js 366 kB 366 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/OBJLoader2WorkerModule-DTMpvldF.js 109 kB 109 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/previousFullPath-CmezY7As.js 838 B 838 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/rolldown-runtime-cVp-94Rc.js 1.96 kB 1.96 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/Slider-C87scEAV.js 4.21 kB 4.21 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/widget-BJiJuR5i.js 518 B 518 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetBoundingBox-CUtab2CB.js 4.71 kB 4.71 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetBoundingBox-D79nBMxa.js 186 B 186 B ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetChart-CiXfBVBH.js 2.79 kB 2.79 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetColorPicker-BEfQQjV6.js 3.71 kB 3.71 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetGalleria-jm_gz6R2.js 4.57 kB 4.57 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetImageCompare-DnZ84Utk.js 3.79 kB 3.79 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetInputText-WCHoBOIV.js 2.58 kB 2.58 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetLayoutField-BHrU_4qY.js 2.7 kB 2.7 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetMarkdown-3U-WuCE_.js 3.49 kB 3.49 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/widgetPropFilter-ERx8czR8.js 1.31 kB 1.31 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetTextarea-BXRA46js.js 3.87 kB 3.87 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/WidgetToggleSwitch-DYS14Ar3.js 3.26 kB 3.26 kB ⚪ 0 B ⚪ 0 B ⚪ 0 B
assets/widgetTypes-KPj-zM0O.js 573 B 573 B ⚪ 0 B ⚪ 0 B ⚪ 0 B

Status: 34 added / 34 removed

When widgets are added dynamically to a node, the watch on imageUrls/
imageUrls would trigger even when URL content is identical (just a new
array reference). This caused the preview to unnecessarily reset to
loading state.

Fix: Compare URL arrays by content, not reference. Only reset loading
state when URLs actually change.

- Add urlsChanged check comparing array length and element equality
- Remove deep: true since we compare manually
- Add unit tests for URL change detection behavior
- Add E2E test for dynamic widget scenario

Co-authored-by: Amp <amp@ampcode.com>
Amp-Thread-ID: https://ampcode.com/threads/T-019c03a2-2acb-74cc-94ef-993894df9b22
@christian-byrne christian-byrne force-pushed the dynamic-widget-rerender-fix branch from 391d159 to 115f788 Compare January 28, 2026 08:37
@christian-byrne christian-byrne marked this pull request as ready for review January 28, 2026 08:43
@christian-byrne christian-byrne requested a review from a team as a code owner January 28, 2026 08:43
@dosubot dosubot bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Jan 28, 2026
imageError.value = false
if (newUrls.length > 0) startDelayedLoader()
},
{ deep: true, immediate: true }
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Note for reviewers: deep is redundant on string arrays -- adding, removing, reassigning all trigger reactivity. Proof: Vue Playground Link

@christian-byrne christian-byrne added needs-backport Fix/change that needs to be cherry-picked to the current feature freeze branch core/1.38 Backport PRs for core 1.38 cloud/1.38 Backport PRs for cloud 1.38 labels Jan 28, 2026
Copy link
Collaborator

@AustinMroz AustinMroz left a comment

Choose a reason for hiding this comment

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

Change to watches look correct, but when manually testing, it seems that Videos are still reloading anytime the dynamicCombo is changed because the rand parameter in the url is changed.

Will keep investigating.

@AustinMroz
Copy link
Collaborator

AustinMroz commented Jan 29, 2026

In LGraphNode.vue nodeMedia is recomputing because the hasVideoInput iterates over node inputs (which have changed).

If we move hasVideoInput into a separate computed, then nodeMedia will only refire when the actual outputs have changed.

@christian-byrne
Copy link
Contributor Author

Change to watches look correct, but when manually testing, it seems that Videos are still reloading anytime the dynamicCombo is changed because the rand parameter in the url is changed.

This was just to fix the previews breaking entirely when dynamic widget changes. The rand and aggresive cache-busting on each /view call (on local) I feel is a separate thing. But curious: do you think we should remove that at least for videos? I can see people deleting image outputs then creating new ones with the same name, but videos might be less likely (and the perf hit is worse). WDYT?

@AustinMroz
Copy link
Collaborator

This was just to fix the previews breaking entirely when dynamic widget changes.

I'm pretty sure that's the part I'm having issues with. I'm running this branch with a single added console.log(urlsChanged) to the watch along with Comfy-Org/ComfyUI#11940. Every time I update the codec option, the video reloads and I get a log that the url has changed.

do you think we should remove that

I've got a slight leaning towards "not handing video differently than images". The ideal case is removing the cache busting entirely and trusting cache-control headers. That way we minimize the code changes between local and cloud.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@src/renderer/extensions/vueNodes/components/ImagePreview.test.ts`:
- Around line 343-368: Replace the real 300ms sleep with fake timers: call
vi.useFakeTimers() before triggering the prop change, then after await
wrapper.setProps({...}) and await nextTick(), advance the timers with await
vi.advanceTimersByTimeAsync(250) (or 300 to match existing expectation), await
nextTick(), and finally call vi.useRealTimers() to restore. Update the test
around wrapper.setProps/await nextTick() (the test function in
ImagePreview.test.ts that uses wrapper.setProps and img.trigger('load')) to
remove the new Promise(setTimeout...) and use
vi.useFakeTimers()/vi.advanceTimersByTimeAsync() instead so it matches the
adjacent test pattern.

Comment on lines +343 to +368
it('should reset loading state when imageUrls prop changes to different URLs', async () => {
const urls = ['/api/view?filename=test.png&type=output']
const wrapper = mountImagePreview({ imageUrls: urls })

// Simulate image load completing
const img = wrapper.find('img')
await img.trigger('load')
await nextTick()

// Verify loader is hidden
expect(wrapper.find('[aria-busy="true"]').exists()).toBe(false)

// Change to different URL
await wrapper.setProps({
imageUrls: ['/api/view?filename=different.png&type=output']
})
await nextTick()

// After 250ms timeout, loading state should be reset (aria-busy="true")
// We can check the internal state via the Skeleton appearing
// or wait for the timeout
await new Promise((resolve) => setTimeout(resolve, 300))
await nextTick()

expect(wrapper.find('[aria-busy="true"]').exists()).toBe(true)
})
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

cat -n src/renderer/extensions/vueNodes/components/ImagePreview.test.ts | head -400 | tail -100

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 4177


🏁 Script executed:

# Search for other uses of vi.useFakeTimers in the test file
rg -n "useFakeTimers|advanceTimersByTime|setTimeout" src/renderer/extensions/vueNodes/components/ImagePreview.test.ts -A 2 -B 2

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 708


🏁 Script executed:

# Check the full context of the test section around lines 343-368
sed -n '310,380p' src/renderer/extensions/vueNodes/components/ImagePreview.test.ts | cat -n

Repository: Comfy-Org/ComfyUI_frontend

Length of output: 3069


Use fake timers to match the adjacent test and avoid real 300ms waits.

The previous test in this suite (lines 313–341) already demonstrates the proper pattern using vi.useFakeTimers() and vi.advanceTimersByTimeAsync(). Apply the same approach here for consistency and to keep the test suite fast and deterministic.

♻️ Suggested update
   it('should reset loading state when imageUrls prop changes to different URLs', async () => {
+    vi.useFakeTimers()
+    try {
       const urls = ['/api/view?filename=test.png&type=output']
       const wrapper = mountImagePreview({ imageUrls: urls })

       // Simulate image load completing
       const img = wrapper.find('img')
       await img.trigger('load')
       await nextTick()

       // Verify loader is hidden
       expect(wrapper.find('[aria-busy="true"]').exists()).toBe(false)

       // Change to different URL
       await wrapper.setProps({
         imageUrls: ['/api/view?filename=different.png&type=output']
       })
       await nextTick()

       // After 250ms timeout, loading state should be reset (aria-busy="true")
-      await new Promise((resolve) => setTimeout(resolve, 300))
+      await vi.advanceTimersByTimeAsync(300)
       await nextTick()

       expect(wrapper.find('[aria-busy="true"]').exists()).toBe(true)
+    } finally {
+      vi.useRealTimers()
+    }
   })
📝 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
it('should reset loading state when imageUrls prop changes to different URLs', async () => {
const urls = ['/api/view?filename=test.png&type=output']
const wrapper = mountImagePreview({ imageUrls: urls })
// Simulate image load completing
const img = wrapper.find('img')
await img.trigger('load')
await nextTick()
// Verify loader is hidden
expect(wrapper.find('[aria-busy="true"]').exists()).toBe(false)
// Change to different URL
await wrapper.setProps({
imageUrls: ['/api/view?filename=different.png&type=output']
})
await nextTick()
// After 250ms timeout, loading state should be reset (aria-busy="true")
// We can check the internal state via the Skeleton appearing
// or wait for the timeout
await new Promise((resolve) => setTimeout(resolve, 300))
await nextTick()
expect(wrapper.find('[aria-busy="true"]').exists()).toBe(true)
})
it('should reset loading state when imageUrls prop changes to different URLs', async () => {
vi.useFakeTimers()
try {
const urls = ['/api/view?filename=test.png&type=output']
const wrapper = mountImagePreview({ imageUrls: urls })
// Simulate image load completing
const img = wrapper.find('img')
await img.trigger('load')
await nextTick()
// Verify loader is hidden
expect(wrapper.find('[aria-busy="true"]').exists()).toBe(false)
// Change to different URL
await wrapper.setProps({
imageUrls: ['/api/view?filename=different.png&type=output']
})
await nextTick()
// After 250ms timeout, loading state should be reset (aria-busy="true")
// We can check the internal state via the Skeleton appearing
// or wait for the timeout
await vi.advanceTimersByTimeAsync(300)
await nextTick()
expect(wrapper.find('[aria-busy="true"]').exists()).toBe(true)
} finally {
vi.useRealTimers()
}
})
🤖 Prompt for AI Agents
In `@src/renderer/extensions/vueNodes/components/ImagePreview.test.ts` around
lines 343 - 368, Replace the real 300ms sleep with fake timers: call
vi.useFakeTimers() before triggering the prop change, then after await
wrapper.setProps({...}) and await nextTick(), advance the timers with await
vi.advanceTimersByTimeAsync(250) (or 300 to match existing expectation), await
nextTick(), and finally call vi.useRealTimers() to restore. Update the test
around wrapper.setProps/await nextTick() (the test function in
ImagePreview.test.ts that uses wrapper.setProps and img.trigger('load')) to
remove the new Promise(setTimeout...) and use
vi.useFakeTimers()/vi.advanceTimersByTimeAsync() instead so it matches the
adjacent test pattern.

@christian-byrne
Copy link
Contributor Author

If we move hasVideoInput into a separate computed, then nodeMedia will only refire when the actual outputs have changed.

Done in 9013914

@christian-byrne
Copy link
Contributor Author

Tested and works better now (doesn't re-render when widgets are added).

@AustinMroz AustinMroz merged commit ee600a8 into main Jan 30, 2026
27 checks passed
@AustinMroz AustinMroz deleted the dynamic-widget-rerender-fix branch January 30, 2026 16:27
github-actions bot pushed a commit that referenced this pull request Jan 30, 2026
)

## Summary

Fixes image/video previews getting stuck in loading state when widgets
are added dynamically to a node.

## Problem

When dynamic widgets are added to a node (e.g., by extensions), Vue
reactivity triggers the watch on `imageUrls` prop even when the URL
content is identical—the array has a new reference but the same values.
This caused:
1. `startDelayedLoader()` to reset loading state to pending
2. If the cached image doesn't trigger `@load` before the 250ms timeout,
the loader shows and stays stuck

## Solution

Compare URL arrays by content, not reference. Only reset loading state
when URLs actually change:
- Check array length and element-by-element equality
- Return early if URLs are identical (just a new array reference)
- Remove `deep: true` since we compare manually

## Screenshots

<img width="749" height="647" alt="image"
src="https://github.com/user-attachments/assets/3a1ff656-59ed-467a-a121-b70b91423a50"
/>


<img width="749" height="647" alt="Screenshot from 2026-01-28 15-24-18"
src="https://github.com/user-attachments/assets/28265dad-1d79-47c8-9fd4-5a82b94e72cd"
/>

<img width="749" height="647" alt="Screenshot from 2026-01-28 15-24-05"
src="https://github.com/user-attachments/assets/c7af93b7-c898-405f-860b-0f82abe5af6d"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8366-fix-prevent-image-video-preview-reset-on-dynamic-widget-addition-2f66d73d3650819483b2d5cbfb78187f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
github-actions bot pushed a commit that referenced this pull request Jan 30, 2026
)

## Summary

Fixes image/video previews getting stuck in loading state when widgets
are added dynamically to a node.

## Problem

When dynamic widgets are added to a node (e.g., by extensions), Vue
reactivity triggers the watch on `imageUrls` prop even when the URL
content is identical—the array has a new reference but the same values.
This caused:
1. `startDelayedLoader()` to reset loading state to pending
2. If the cached image doesn't trigger `@load` before the 250ms timeout,
the loader shows and stays stuck

## Solution

Compare URL arrays by content, not reference. Only reset loading state
when URLs actually change:
- Check array length and element-by-element equality
- Return early if URLs are identical (just a new array reference)
- Remove `deep: true` since we compare manually

## Screenshots

<img width="749" height="647" alt="image"
src="https://github.com/user-attachments/assets/3a1ff656-59ed-467a-a121-b70b91423a50"
/>


<img width="749" height="647" alt="Screenshot from 2026-01-28 15-24-18"
src="https://github.com/user-attachments/assets/28265dad-1d79-47c8-9fd4-5a82b94e72cd"
/>

<img width="749" height="647" alt="Screenshot from 2026-01-28 15-24-05"
src="https://github.com/user-attachments/assets/c7af93b7-c898-405f-860b-0f82abe5af6d"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8366-fix-prevent-image-video-preview-reset-on-dynamic-widget-addition-2f66d73d3650819483b2d5cbfb78187f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
@comfy-pr-bot
Copy link
Member

@christian-byrne Successfully backported to #8492

@comfy-pr-bot
Copy link
Member

@christian-byrne Successfully backported to #8493

@github-actions github-actions bot removed the needs-backport Fix/change that needs to be cherry-picked to the current feature freeze branch label Jan 30, 2026
AustinMroz pushed a commit that referenced this pull request Jan 30, 2026
…c widget addition (#8492)

Backport of #8366 to `core/1.38`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8492-backport-core-1-38-fix-prevent-image-video-preview-reset-on-dynamic-widget-addition-2f86d73d36508123acfdfac61554da7e)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
AustinMroz pushed a commit that referenced this pull request Jan 30, 2026
…ic widget addition (#8493)

Backport of #8366 to `cloud/1.38`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8493-backport-cloud-1-38-fix-prevent-image-video-preview-reset-on-dynamic-widget-addition-2f86d73d36508192b27fc55a29d6d02e)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
christian-byrne added a commit that referenced this pull request Jan 31, 2026
## Summary
Fix for COM-14110: Preview image does not display new outputs in
vue-nodes.

## Problem
The merge logic in `setOutputsByLocatorId` updated `app.nodeOutputs` but
returned early without updating the reactive `nodeOutputs.value` ref.
This caused Vue components to never receive merged output updates
because only the non-reactive `app.nodeOutputs` was being updated.

## Solution
Added `nodeOutputs.value[nodeLocatorId] = existingOutput` after the
merge loop, before the return statement.

## Testing
- Added 2 unit tests covering the merge behavior
- All 4076 existing unit tests pass
- Typechecks pass
- Lint passes

## Notes
- Related open PRs touching same files: #8143, #8366 - potential minor
conflicts possible

Fixes COM-14110

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8479-fix-update-reactive-ref-after-merge-in-imagePreviewStore-2f86d73d365081f1a145fa5a9782515f)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
github-actions bot pushed a commit that referenced this pull request Jan 31, 2026
## Summary
Fix for COM-14110: Preview image does not display new outputs in
vue-nodes.

## Problem
The merge logic in `setOutputsByLocatorId` updated `app.nodeOutputs` but
returned early without updating the reactive `nodeOutputs.value` ref.
This caused Vue components to never receive merged output updates
because only the non-reactive `app.nodeOutputs` was being updated.

## Solution
Added `nodeOutputs.value[nodeLocatorId] = existingOutput` after the
merge loop, before the return statement.

## Testing
- Added 2 unit tests covering the merge behavior
- All 4076 existing unit tests pass
- Typechecks pass
- Lint passes

## Notes
- Related open PRs touching same files: #8143, #8366 - potential minor
conflicts possible

Fixes COM-14110

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8479-fix-update-reactive-ref-after-merge-in-imagePreviewStore-2f86d73d365081f1a145fa5a9782515f)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
github-actions bot pushed a commit that referenced this pull request Jan 31, 2026
## Summary
Fix for COM-14110: Preview image does not display new outputs in
vue-nodes.

## Problem
The merge logic in `setOutputsByLocatorId` updated `app.nodeOutputs` but
returned early without updating the reactive `nodeOutputs.value` ref.
This caused Vue components to never receive merged output updates
because only the non-reactive `app.nodeOutputs` was being updated.

## Solution
Added `nodeOutputs.value[nodeLocatorId] = existingOutput` after the
merge loop, before the return statement.

## Testing
- Added 2 unit tests covering the merge behavior
- All 4076 existing unit tests pass
- Typechecks pass
- Lint passes

## Notes
- Related open PRs touching same files: #8143, #8366 - potential minor
conflicts possible

Fixes COM-14110

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8479-fix-update-reactive-ref-after-merge-in-imagePreviewStore-2f86d73d365081f1a145fa5a9782515f)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
DrJKL pushed a commit that referenced this pull request Jan 31, 2026
)

## Summary

Fixes image/video previews getting stuck in loading state when widgets
are added dynamically to a node.

## Problem

When dynamic widgets are added to a node (e.g., by extensions), Vue
reactivity triggers the watch on `imageUrls` prop even when the URL
content is identical—the array has a new reference but the same values.
This caused:
1. `startDelayedLoader()` to reset loading state to pending
2. If the cached image doesn't trigger `@load` before the 250ms timeout,
the loader shows and stays stuck

## Solution

Compare URL arrays by content, not reference. Only reset loading state
when URLs actually change:
- Check array length and element-by-element equality
- Return early if URLs are identical (just a new array reference)
- Remove `deep: true` since we compare manually

## Screenshots

<img width="749" height="647" alt="image"
src="https://github.com/user-attachments/assets/3a1ff656-59ed-467a-a121-b70b91423a50"
/>


<img width="749" height="647" alt="Screenshot from 2026-01-28 15-24-18"
src="https://github.com/user-attachments/assets/28265dad-1d79-47c8-9fd4-5a82b94e72cd"
/>

<img width="749" height="647" alt="Screenshot from 2026-01-28 15-24-05"
src="https://github.com/user-attachments/assets/c7af93b7-c898-405f-860b-0f82abe5af6d"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8366-fix-prevent-image-video-preview-reset-on-dynamic-widget-addition-2f66d73d3650819483b2d5cbfb78187f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
DrJKL pushed a commit that referenced this pull request Jan 31, 2026
## Summary
Fix for COM-14110: Preview image does not display new outputs in
vue-nodes.

## Problem
The merge logic in `setOutputsByLocatorId` updated `app.nodeOutputs` but
returned early without updating the reactive `nodeOutputs.value` ref.
This caused Vue components to never receive merged output updates
because only the non-reactive `app.nodeOutputs` was being updated.

## Solution
Added `nodeOutputs.value[nodeLocatorId] = existingOutput` after the
merge loop, before the return statement.

## Testing
- Added 2 unit tests covering the merge behavior
- All 4076 existing unit tests pass
- Typechecks pass
- Lint passes

## Notes
- Related open PRs touching same files: #8143, #8366 - potential minor
conflicts possible

Fixes COM-14110

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8479-fix-update-reactive-ref-after-merge-in-imagePreviewStore-2f86d73d365081f1a145fa5a9782515f)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

cloud/1.38 Backport PRs for cloud 1.38 core/1.38 Backport PRs for core 1.38 size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants