comfyui-frontend-package 1.38.5__py3-none-any.whl → 1.38.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. comfyui_frontend_package/static/assets/{AboutPanel-CHse5rOA.js → AboutPanel-lkjGFasi.js} +2 -2
  2. comfyui_frontend_package/static/assets/{AboutPanel-CHse5rOA.js.map → AboutPanel-lkjGFasi.js.map} +1 -1
  3. comfyui_frontend_package/static/assets/{AudioPreviewPlayer-CAa8V66L.js → AudioPreviewPlayer-BxCSKPl9.js} +2 -2
  4. comfyui_frontend_package/static/assets/{AudioPreviewPlayer-CAa8V66L.js.map → AudioPreviewPlayer-BxCSKPl9.js.map} +1 -1
  5. comfyui_frontend_package/static/assets/AudioPreviewPlayer-CkxKvcVf.js +1 -0
  6. comfyui_frontend_package/static/assets/{BaseViewTemplate-DA6zfigT.js → BaseViewTemplate-CjODF2hh.js} +2 -2
  7. comfyui_frontend_package/static/assets/{BaseViewTemplate-DA6zfigT.js.map → BaseViewTemplate-CjODF2hh.js.map} +1 -1
  8. comfyui_frontend_package/static/assets/{CloudAuthTimeoutView-9OPBS1hE.js → CloudAuthTimeoutView-D-QkjPNh.js} +2 -2
  9. comfyui_frontend_package/static/assets/{CloudAuthTimeoutView-9OPBS1hE.js.map → CloudAuthTimeoutView-D-QkjPNh.js.map} +1 -1
  10. comfyui_frontend_package/static/assets/{CloudBadge-BnLiAHDN.js → CloudBadge-B4nmLus2.js} +2 -2
  11. comfyui_frontend_package/static/assets/{CloudBadge-BnLiAHDN.js.map → CloudBadge-B4nmLus2.js.map} +1 -1
  12. comfyui_frontend_package/static/assets/{CloudForgotPasswordView-BqDR_C7K.js → CloudForgotPasswordView-DOEV9hGr.js} +2 -2
  13. comfyui_frontend_package/static/assets/{CloudForgotPasswordView-BqDR_C7K.js.map → CloudForgotPasswordView-DOEV9hGr.js.map} +1 -1
  14. comfyui_frontend_package/static/assets/{CloudLayoutView-vTrrVUOY.js → CloudLayoutView-ShKH6rRV.js} +2 -2
  15. comfyui_frontend_package/static/assets/{CloudLayoutView-vTrrVUOY.js.map → CloudLayoutView-ShKH6rRV.js.map} +1 -1
  16. comfyui_frontend_package/static/assets/{CloudLoginView-T17euJly.js → CloudLoginView-C3Te42U9.js} +2 -2
  17. comfyui_frontend_package/static/assets/{CloudLoginView-T17euJly.js.map → CloudLoginView-C3Te42U9.js.map} +1 -1
  18. comfyui_frontend_package/static/assets/CloudRunButtonWrapper-Cub7EB34.js +3 -0
  19. comfyui_frontend_package/static/assets/{CloudRunButtonWrapper-hQc4BNkX.js.map → CloudRunButtonWrapper-Cub7EB34.js.map} +1 -1
  20. comfyui_frontend_package/static/assets/{CloudSignupView-vEDby5k3.js → CloudSignupView-X2oiL3ZR.js} +2 -2
  21. comfyui_frontend_package/static/assets/{CloudSignupView-vEDby5k3.js.map → CloudSignupView-X2oiL3ZR.js.map} +1 -1
  22. comfyui_frontend_package/static/assets/{CloudSubscriptionRedirectView-DPyO745g.js → CloudSubscriptionRedirectView-UjNv8emo.js} +2 -2
  23. comfyui_frontend_package/static/assets/{CloudSubscriptionRedirectView-DPyO745g.js.map → CloudSubscriptionRedirectView-UjNv8emo.js.map} +1 -1
  24. comfyui_frontend_package/static/assets/{CloudSurveyView-CBtTd9Ru.js → CloudSurveyView-IaiucCTP.js} +2 -2
  25. comfyui_frontend_package/static/assets/{CloudSurveyView-CBtTd9Ru.js.map → CloudSurveyView-IaiucCTP.js.map} +1 -1
  26. comfyui_frontend_package/static/assets/ComfyQueueButton-BppnHbrl.js +1 -0
  27. comfyui_frontend_package/static/assets/{ComfyQueueButton-MZrp7wYJ.js → ComfyQueueButton-HjSIKZKO.js} +2 -2
  28. comfyui_frontend_package/static/assets/{ComfyQueueButton-MZrp7wYJ.js.map → ComfyQueueButton-HjSIKZKO.js.map} +1 -1
  29. comfyui_frontend_package/static/assets/{ExtensionPanel-CrWVGUtg.js → ExtensionPanel-Bzb9QtKj.js} +2 -2
  30. comfyui_frontend_package/static/assets/{ExtensionPanel-CrWVGUtg.js.map → ExtensionPanel-Bzb9QtKj.js.map} +1 -1
  31. comfyui_frontend_package/static/assets/{GlobalToast-BiCmpIvO.js → GlobalToast-BSCvu6Hw.js} +2 -2
  32. comfyui_frontend_package/static/assets/{GlobalToast-BiCmpIvO.js.map → GlobalToast-BSCvu6Hw.js.map} +1 -1
  33. comfyui_frontend_package/static/assets/{GraphView-BCkpNGgz.js → GraphView-gYVCtm1V.js} +5 -5
  34. comfyui_frontend_package/static/assets/GraphView-gYVCtm1V.js.map +1 -0
  35. comfyui_frontend_package/static/assets/{KeybindingPanel-CAXL5TlV.js → KeybindingPanel-DF-bG4iO.js} +2 -2
  36. comfyui_frontend_package/static/assets/{KeybindingPanel-CAXL5TlV.js.map → KeybindingPanel-DF-bG4iO.js.map} +1 -1
  37. comfyui_frontend_package/static/assets/{LegacyCreditsPanel-6vR8koQy.js → LegacyCreditsPanel-D-CboO8k.js} +2 -2
  38. comfyui_frontend_package/static/assets/{LegacyCreditsPanel-6vR8koQy.js.map → LegacyCreditsPanel-D-CboO8k.js.map} +1 -1
  39. comfyui_frontend_package/static/assets/Load3D-c9UwgGoI.js +1 -0
  40. comfyui_frontend_package/static/assets/{Load3D-ei1BUF9O.js → Load3D-vYr8M3jJ.js} +2 -2
  41. comfyui_frontend_package/static/assets/{Load3D-ei1BUF9O.js.map → Load3D-vYr8M3jJ.js.map} +1 -1
  42. comfyui_frontend_package/static/assets/{PanelTemplate-BjN5XNg2.js → PanelTemplate-BJda9e5J.js} +2 -2
  43. comfyui_frontend_package/static/assets/{PanelTemplate-BjN5XNg2.js.map → PanelTemplate-BJda9e5J.js.map} +1 -1
  44. comfyui_frontend_package/static/assets/{ServerConfigPanel-CxovH9Qk.js → ServerConfigPanel-VsC6xlZJ.js} +2 -2
  45. comfyui_frontend_package/static/assets/{ServerConfigPanel-CxovH9Qk.js.map → ServerConfigPanel-VsC6xlZJ.js.map} +1 -1
  46. comfyui_frontend_package/static/assets/{SubscribeButton-CTOQRkfg.js → SubscribeButton-DZBycfCA.js} +2 -2
  47. comfyui_frontend_package/static/assets/{SubscribeButton-CTOQRkfg.js.map → SubscribeButton-DZBycfCA.js.map} +1 -1
  48. comfyui_frontend_package/static/assets/{SubscribeToRun-DnXzV8y0.js → SubscribeToRun-4YolxBOL.js} +2 -2
  49. comfyui_frontend_package/static/assets/{SubscribeToRun-DnXzV8y0.js.map → SubscribeToRun-4YolxBOL.js.map} +1 -1
  50. comfyui_frontend_package/static/assets/{SubscriptionPanel-D9uv7z8f.js → SubscriptionPanel-B6txX4Vm.js} +2 -2
  51. comfyui_frontend_package/static/assets/{SubscriptionPanel-D9uv7z8f.js.map → SubscriptionPanel-B6txX4Vm.js.map} +1 -1
  52. comfyui_frontend_package/static/assets/{SubscriptionRequiredDialogContent--VmT16oc.js → SubscriptionRequiredDialogContent-COEF2VQ_.js} +2 -2
  53. comfyui_frontend_package/static/assets/{SubscriptionRequiredDialogContent--VmT16oc.js.map → SubscriptionRequiredDialogContent-COEF2VQ_.js.map} +1 -1
  54. comfyui_frontend_package/static/assets/{UserCheckView-spD3LyMu.js → UserCheckView-x-fkcYzc.js} +2 -2
  55. comfyui_frontend_package/static/assets/{UserCheckView-spD3LyMu.js.map → UserCheckView-x-fkcYzc.js.map} +1 -1
  56. comfyui_frontend_package/static/assets/{UserPanel-Su6NtJ5q.js → UserPanel-7N9QknQj.js} +2 -2
  57. comfyui_frontend_package/static/assets/{UserPanel-Su6NtJ5q.js.map → UserPanel-7N9QknQj.js.map} +1 -1
  58. comfyui_frontend_package/static/assets/{UserSelectView-C5LBOPcv.js → UserSelectView-BYjOkfSa.js} +2 -2
  59. comfyui_frontend_package/static/assets/{UserSelectView-C5LBOPcv.js.map → UserSelectView-BYjOkfSa.js.map} +1 -1
  60. comfyui_frontend_package/static/assets/{ValueControlPopover-BdlDzT8l.js → ValueControlPopover-BPAa35QG.js} +2 -2
  61. comfyui_frontend_package/static/assets/{ValueControlPopover-BdlDzT8l.js.map → ValueControlPopover-BPAa35QG.js.map} +1 -1
  62. comfyui_frontend_package/static/assets/{WidgetAudioUI-BDZxDx_r.js → WidgetAudioUI-Dw-r3Ews.js} +2 -2
  63. comfyui_frontend_package/static/assets/{WidgetAudioUI-BDZxDx_r.js.map → WidgetAudioUI-Dw-r3Ews.js.map} +1 -1
  64. comfyui_frontend_package/static/assets/{WidgetImageCrop-CYRW7t2Q.js → WidgetImageCrop-kERy9g5I.js} +2 -2
  65. comfyui_frontend_package/static/assets/{WidgetImageCrop-CYRW7t2Q.js.map → WidgetImageCrop-kERy9g5I.js.map} +1 -1
  66. comfyui_frontend_package/static/assets/{WidgetInputNumber-BOKO36G3.js → WidgetInputNumber-BaClCNAC.js} +1 -1
  67. comfyui_frontend_package/static/assets/WidgetInputNumber-DU_D0Fzy.js +3 -0
  68. comfyui_frontend_package/static/assets/WidgetInputNumber-DU_D0Fzy.js.map +1 -0
  69. comfyui_frontend_package/static/assets/{WidgetLegacy-Bslv9wZZ.js → WidgetLegacy-B4nipUM9.js} +1 -1
  70. comfyui_frontend_package/static/assets/{WidgetRecordAudio-Bzy8PIzN.js → WidgetRecordAudio-Nk8dH238.js} +2 -2
  71. comfyui_frontend_package/static/assets/{WidgetRecordAudio-Bzy8PIzN.js.map → WidgetRecordAudio-Nk8dH238.js.map} +1 -1
  72. comfyui_frontend_package/static/assets/WidgetSelect-DzZPpO_-.js +1 -0
  73. comfyui_frontend_package/static/assets/{WidgetSelect-zgrFVzHH.js → WidgetSelect-nSQrk_hd.js} +2 -2
  74. comfyui_frontend_package/static/assets/{WidgetSelect-zgrFVzHH.js.map → WidgetSelect-nSQrk_hd.js.map} +1 -1
  75. comfyui_frontend_package/static/assets/{WidgetWithControl-Dh2FWOiA.js → WidgetWithControl-Da6zUB5e.js} +3 -3
  76. comfyui_frontend_package/static/assets/{WidgetWithControl-Dh2FWOiA.js.map → WidgetWithControl-Da6zUB5e.js.map} +1 -1
  77. comfyui_frontend_package/static/assets/{api-CUAc7rDA.js → api-Dwq2LQIW.js} +4 -4
  78. comfyui_frontend_package/static/assets/api-Dwq2LQIW.js.map +1 -0
  79. comfyui_frontend_package/static/assets/{audioService-DvndbCi2.js → audioService-DvVaKhuU.js} +2 -2
  80. comfyui_frontend_package/static/assets/{audioService-DvndbCi2.js.map → audioService-DvVaKhuU.js.map} +1 -1
  81. comfyui_frontend_package/static/assets/{audioUtils-DpjpcKbH.js → audioUtils-DD4rUYVZ.js} +2 -2
  82. comfyui_frontend_package/static/assets/{audioUtils-DpjpcKbH.js.map → audioUtils-DD4rUYVZ.js.map} +1 -1
  83. comfyui_frontend_package/static/assets/{auth-B8ZZ0KKQ.js → auth-B9axG-yZ.js} +2 -2
  84. comfyui_frontend_package/static/assets/{auth-B8ZZ0KKQ.js.map → auth-B9axG-yZ.js.map} +1 -1
  85. comfyui_frontend_package/static/assets/auth-D74DTev8.js +1 -0
  86. comfyui_frontend_package/static/assets/{cloudBadges-C1a7fBky.js → cloudBadges-D5mGJbRy.js} +2 -2
  87. comfyui_frontend_package/static/assets/{cloudBadges-C1a7fBky.js.map → cloudBadges-D5mGJbRy.js.map} +1 -1
  88. comfyui_frontend_package/static/assets/{cloudFeedbackTopbarButton-DR0T8sWG.js → cloudFeedbackTopbarButton-RZUssOmb.js} +2 -2
  89. comfyui_frontend_package/static/assets/{cloudFeedbackTopbarButton-DR0T8sWG.js.map → cloudFeedbackTopbarButton-RZUssOmb.js.map} +1 -1
  90. comfyui_frontend_package/static/assets/{cloudRemoteConfig-DhMjC5TB.js → cloudRemoteConfig-F9J0iGyF.js} +2 -2
  91. comfyui_frontend_package/static/assets/{cloudRemoteConfig-DhMjC5TB.js.map → cloudRemoteConfig-F9J0iGyF.js.map} +1 -1
  92. comfyui_frontend_package/static/assets/{cloudSessionCookie-Duxk6ux1.js → cloudSessionCookie-MEORlqtg.js} +2 -2
  93. comfyui_frontend_package/static/assets/{cloudSessionCookie-Duxk6ux1.js.map → cloudSessionCookie-MEORlqtg.js.map} +1 -1
  94. comfyui_frontend_package/static/assets/{cloudSubscription-B8l6B9Nx.js → cloudSubscription-CuWNXKVx.js} +2 -2
  95. comfyui_frontend_package/static/assets/{cloudSubscription-B8l6B9Nx.js.map → cloudSubscription-CuWNXKVx.js.map} +1 -1
  96. comfyui_frontend_package/static/assets/{core-DBfeqMDR.js → core-IYu8XAIx.js} +4 -4
  97. comfyui_frontend_package/static/assets/{core-DBfeqMDR.js.map → core-IYu8XAIx.js.map} +1 -1
  98. comfyui_frontend_package/static/assets/{dialogService-BZ1FmjZL.js → dialogService-YG0RH337.js} +9 -9
  99. comfyui_frontend_package/static/assets/{dialogService-BZ1FmjZL.js.map → dialogService-YG0RH337.js.map} +1 -1
  100. comfyui_frontend_package/static/assets/firebaseAuthStore-DnNaPbuZ.js +1 -0
  101. comfyui_frontend_package/static/assets/{graphHasMissingNodes-C79Wi51S.js → graphHasMissingNodes-BhD1N6zI.js} +2 -2
  102. comfyui_frontend_package/static/assets/{graphHasMissingNodes-C79Wi51S.js.map → graphHasMissingNodes-BhD1N6zI.js.map} +1 -1
  103. comfyui_frontend_package/static/assets/{index-CGxJFSof.js → index-BMy3twho.js} +3 -3
  104. comfyui_frontend_package/static/assets/{index-CGxJFSof.js.map → index-BMy3twho.js.map} +1 -1
  105. comfyui_frontend_package/static/assets/{index-DNpOhRra.css → index-KMO9qFHH.css} +1 -1
  106. comfyui_frontend_package/static/assets/{keybindingService-B88NjeAU.js → keybindingService-CBLPjYHI.js} +2 -2
  107. comfyui_frontend_package/static/assets/{keybindingService-B88NjeAU.js.map → keybindingService-CBLPjYHI.js.map} +1 -1
  108. comfyui_frontend_package/static/assets/{releaseStore-x0vHjxrw.js → releaseStore-DDOxzkVb.js} +2 -2
  109. comfyui_frontend_package/static/assets/{releaseStore-x0vHjxrw.js.map → releaseStore-DDOxzkVb.js.map} +1 -1
  110. comfyui_frontend_package/static/assets/releaseStore-iVkqunL8.js +1 -0
  111. comfyui_frontend_package/static/assets/{subscriptionCheckoutUtil-B_OvUP2T.js → subscriptionCheckoutUtil-DswSOreM.js} +2 -2
  112. comfyui_frontend_package/static/assets/{subscriptionCheckoutUtil-B_OvUP2T.js.map → subscriptionCheckoutUtil-DswSOreM.js.map} +1 -1
  113. comfyui_frontend_package/static/assets/{useCurrentUser-BJcn2Vgo.js → useCurrentUser-NdaCJzIK.js} +1 -1
  114. comfyui_frontend_package/static/assets/{useErrorHandling-CI8_F4yx.js → useErrorHandling-Cfa5N_7c.js} +2 -2
  115. comfyui_frontend_package/static/assets/{useErrorHandling-CI8_F4yx.js.map → useErrorHandling-Cfa5N_7c.js.map} +1 -1
  116. comfyui_frontend_package/static/assets/{useSubscriptionDialog-Chxkdny5.js → useSubscriptionDialog-792qfEJ2.js} +3 -3
  117. comfyui_frontend_package/static/assets/{useSubscriptionDialog-Chxkdny5.js.map → useSubscriptionDialog-792qfEJ2.js.map} +1 -1
  118. comfyui_frontend_package/static/assets/useSubscriptionDialog-B-eGeK3j.js +1 -0
  119. comfyui_frontend_package/static/assets/{userStore-BkgQPjq6.js → userStore-BAS9m9W6.js} +2 -2
  120. comfyui_frontend_package/static/assets/{userStore-BkgQPjq6.js.map → userStore-BAS9m9W6.js.map} +1 -1
  121. comfyui_frontend_package/static/assets/vendor-three-BFcUNSs9.js.map +1 -1
  122. comfyui_frontend_package/static/index.html +1 -1
  123. {comfyui_frontend_package-1.38.5.dist-info → comfyui_frontend_package-1.38.6.dist-info}/METADATA +1 -1
  124. {comfyui_frontend_package-1.38.5.dist-info → comfyui_frontend_package-1.38.6.dist-info}/RECORD +126 -126
  125. comfyui_frontend_package/static/assets/AudioPreviewPlayer-BoEdyGI_.js +0 -1
  126. comfyui_frontend_package/static/assets/CloudRunButtonWrapper-hQc4BNkX.js +0 -3
  127. comfyui_frontend_package/static/assets/ComfyQueueButton-BbQnRThI.js +0 -1
  128. comfyui_frontend_package/static/assets/GraphView-BCkpNGgz.js.map +0 -1
  129. comfyui_frontend_package/static/assets/Load3D-DHBmC_AU.js +0 -1
  130. comfyui_frontend_package/static/assets/WidgetInputNumber-DGKypM5j.js +0 -3
  131. comfyui_frontend_package/static/assets/WidgetInputNumber-DGKypM5j.js.map +0 -1
  132. comfyui_frontend_package/static/assets/WidgetSelect-DsJGH12l.js +0 -1
  133. comfyui_frontend_package/static/assets/api-CUAc7rDA.js.map +0 -1
  134. comfyui_frontend_package/static/assets/auth-D3RiiqZ8.js +0 -1
  135. comfyui_frontend_package/static/assets/firebaseAuthStore-CZgxeMyf.js +0 -1
  136. comfyui_frontend_package/static/assets/releaseStore-CubqSv5t.js +0 -1
  137. comfyui_frontend_package/static/assets/useSubscriptionDialog-BzMzio2H.js +0 -1
  138. {comfyui_frontend_package-1.38.5.dist-info → comfyui_frontend_package-1.38.6.dist-info}/WHEEL +0 -0
  139. {comfyui_frontend_package-1.38.5.dist-info → comfyui_frontend_package-1.38.6.dist-info}/top_level.txt +0 -0
@@ -1 +0,0 @@
1
- {"version":3,"mappings":"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","names":[],"ignoreList":[],"sources":["../../src/base/common/async.ts","../../src/components/MenuHamburger.vue","../../src/components/dialog/UnloadWindowConfirmDialog.vue","../../src/components/LiteGraphCanvasSplitterOverlay.vue","../../src/composables/useTooltipConfig.ts","../../src/components/actionbar/ComfyRunButton/index.ts","../../src/components/actionbar/ComfyActionbar.vue","../../src/composables/useWorkflowActionsMenu.ts","../../src/components/breadcrumb/SubgraphBreadcrumbItem.vue","../../src/composables/element/useOverflowObserver.ts","../../src/components/breadcrumb/SubgraphBreadcrumb.vue","../../src/components/queue/QueueOverlayActive.vue","../../src/components/queue/CompletionSummaryBanner.vue","../../src/components/queue/QueueOverlayEmpty.vue","../../src/components/queue/QueueOverlayHeader.vue","../../src/components/queue/job/JobContextMenu.vue","../../src/components/queue/job/JobFiltersBar.vue","../../src/components/queue/job/useJobErrorReporting.ts","../../src/components/queue/job/useQueueEstimates.ts","../../src/components/queue/job/JobDetailsPopover.vue","../../src/components/queue/job/QueueAssetPreview.vue","../../src/components/queue/job/QueueJobItem.vue","../../src/components/queue/job/JobGroupsList.vue","../../src/components/queue/QueueOverlayExpanded.vue","../../src/components/queue/dialogs/QueueClearHistoryDialog.vue","../../src/composables/queue/useCompletionSummary.ts","../../src/composables/queue/useResultGallery.ts","../../src/components/queue/QueueProgressOverlay.vue","../../src/stores/actionBarButtonStore.ts","../../src/components/topbar/ActionBarButtons.vue","../../src/components/topbar/CurrentUserPopover.vue","../../src/components/topbar/CurrentUserButton.vue","../../src/components/topbar/LoginButton.vue","../../src/components/TopMenuSection.vue","../../src/components/common/ExtensionSlot.vue","../../src/components/bottomPanel/BottomPanel.vue","../../src/composables/element/useAbsolutePosition.ts","../../src/composables/element/useDomClipping.ts","../../src/components/graph/widgets/DomWidget.vue","../../src/components/graph/DomWidgets.vue","../../src/composables/useZoomControls.ts","../../src/renderer/extensions/minimap/composables/useMinimapGraph.ts","../../src/renderer/extensions/minimap/composables/useMinimapInteraction.ts","../../src/renderer/extensions/minimap/composables/useMinimapRenderer.ts","../../src/renderer/extensions/minimap/composables/useMinimapSettings.ts","../../src/renderer/extensions/minimap/composables/useMinimapViewport.ts","../../src/renderer/extensions/minimap/composables/useMinimap.ts","../../src/components/graph/CanvasModeSelector.vue","../../src/components/graph/modals/ZoomControlsModal.vue","../../src/components/graph/GraphCanvasMenu.vue","../../src/components/graph/NodeTooltip.vue","../../src/components/graph/selectionToolbox/BypassButton.vue","../../src/components/graph/selectionToolbox/ColorPickerButton.vue","../../src/components/graph/selectionToolbox/ConfigureSubgraph.vue","../../src/composables/graph/useSelectionState.ts","../../src/components/graph/selectionToolbox/ConvertToSubgraphButton.vue","../../src/components/graph/selectionToolbox/DeleteButton.vue","../../src/components/graph/selectionToolbox/ExecuteButton.vue","../../src/components/graph/selectionToolbox/ExtensionCommandButton.vue","../../src/components/graph/selectionToolbox/InfoButton.vue","../../src/components/graph/selectionToolbox/Load3DViewerButton.vue","../../src/components/graph/selectionToolbox/MaskEditorButton.vue","../../src/composables/useRefreshableSelection.ts","../../src/components/graph/selectionToolbox/RefreshSelectionButton.vue","../../src/components/graph/selectionToolbox/SaveToSubgraphLibrary.vue","../../src/composables/canvas/useSelectionToolboxPosition.ts","../../src/composables/graph/contextMenuConverter.ts","../../src/composables/graph/useCanvasRefresh.ts","../../src/composables/graph/useNodeCustomization.ts","../../src/composables/graph/useGroupMenuOptions.ts","../../src/composables/graph/useImageMenuOptions.ts","../../src/composables/graph/useSelectedNodeActions.ts","../../src/composables/graph/useNodeMenuOptions.ts","../../src/composables/graph/useFrameNodes.ts","../../src/composables/graph/useNodeArrangement.ts","../../src/composables/graph/useSelectionOperations.ts","../../src/composables/graph/useSelectionMenuOptions.ts","../../src/composables/graph/useMoreOptionsMenu.ts","../../src/components/graph/selectionToolbox/ColorPickerMenu.vue","../../src/components/graph/NodeContextMenu.vue","../../src/components/graph/selectionToolbox/FrameNodes.vue","../../src/components/graph/selectionToolbox/NodeOptionsButton.vue","../../src/components/graph/selectionToolbox/VerticalDivider.vue","../../src/components/graph/SelectionToolbox.vue","../../src/components/graph/TitleEditor.vue","../../src/composables/graph/useGraphHierarchy.ts","../../src/components/rightSidePanel/info/TabInfo.vue","../../src/components/rightSidePanel/shared.ts","../../src/components/rightSidePanel/layout/TransitionCollapse.vue","../../src/components/rightSidePanel/layout/PropertiesAccordionItem.vue","../../src/types/simplifiedWidget.ts","../../src/composables/graph/useGraphNodeManager.ts","../../src/components/rightSidePanel/parameters/WidgetActions.vue","../../src/components/rightSidePanel/parameters/WidgetItem.vue","../../src/components/rightSidePanel/parameters/SectionWidgets.vue","../../src/components/rightSidePanel/parameters/TabGlobalParameters.vue","../../src/components/rightSidePanel/parameters/TabNodes.vue","../../src/components/rightSidePanel/parameters/TabNormalInputs.vue","../../src/components/rightSidePanel/parameters/TabSubgraphInputs.vue","../../src/components/rightSidePanel/settings/FieldSwitch.vue","../../src/components/rightSidePanel/settings/TabGlobalSettings.vue","../../src/components/rightSidePanel/settings/SetNodeColor.vue","../../src/renderer/extensions/vueNodes/widgets/components/form/FormSelectButton.vue","../../src/components/rightSidePanel/settings/SetNodeState.vue","../../src/components/rightSidePanel/settings/SetPinned.vue","../../src/components/rightSidePanel/settings/NodeSettings.vue","../../src/components/rightSidePanel/settings/TabSettings.vue","../../src/components/rightSidePanel/subgraph/SubgraphNodeWidget.vue","../../src/components/rightSidePanel/subgraph/SubgraphEditor.vue","../../src/components/rightSidePanel/RightSidePanel.vue","../../src/stores/workspace/searchBoxStore.ts","../../src/components/searchbox/NodeSearchItem.vue","../../src/components/searchbox/NodeSearchBox.vue","../../src/components/searchbox/NodeSearchBoxPopover.vue","../../src/stores/helpCenterStore.ts","../../src/composables/useHelpCenter.ts","../../src/platform/updates/components/ReleaseNotificationToast.vue","../../src/platform/updates/components/WhatsNewPopup.vue","../../src/components/icons/PuzzleIcon.vue","../../src/components/helpcenter/HelpCenterMenuContent.vue","../../src/components/helpcenter/HelpCenterPopups.vue","../../src/components/icons/ComfyLogo.vue","../../src/components/card/CardBottom.vue","../../src/components/card/CardContainer.vue","../../src/components/card/CardTop.vue","../../src/components/chip/SquareChip.vue","../../src/components/templates/thumbnails/BaseThumbnail.vue","../../src/components/templates/thumbnails/CompareSliderThumbnail.vue","../../src/components/templates/thumbnails/DefaultThumbnail.vue","../../src/components/templates/thumbnails/HoverDissolveThumbnail.vue","../../src/composables/useLazyPagination.ts","../../src/stores/templateRankingStore.ts","../../src/composables/useTemplateFiltering.ts","../../src/platform/workflow/templates/composables/useTemplateWorkflows.ts","../../src/utils/gridUtil.ts","../../src/components/custom/widget/WorkflowTemplateSelectorDialog.vue","../../src/composables/useWorkflowTemplateSelectorDialog.ts","../../src/utils/mouseDownUtil.ts","../../src/components/sidebar/ComfyMenuButton.vue","../../src/components/sidebar/ModeToggle.vue","../../src/components/sidebar/SidebarIcon.vue","../../src/components/sidebar/SidebarBottomPanelToggleButton.vue","../../src/components/sidebar/SidebarSettingsButton.vue","../../src/components/sidebar/SidebarShortcutsToggleButton.vue","../../src/components/sidebar/SidebarHelpCenterIcon.vue","../../src/components/sidebar/SidebarLogoutIcon.vue","../../src/components/sidebar/SidebarTemplatesButton.vue","../../src/components/sidebar/SideToolbar.vue","../../src/stores/topbarBadgeStore.ts","../../src/components/topbar/TopbarBadges.vue","../../src/components/common/OverlayIcon.vue","../../src/components/topbar/TopMenuHelpButton.vue","../../src/components/topbar/WorkflowTabPopover.vue","../../src/components/topbar/WorkflowTab.vue","../../src/components/topbar/WorkflowOverflowMenu.vue","../../src/components/topbar/WorkflowTabs.vue","../../src/renderer/core/layout/sync/useLayoutSync.ts","../../src/composables/graph/useVueNodeLifecycle.ts","../../src/composables/node/useNodePricing.ts","../../src/composables/node/usePriceBadge.ts","../../src/composables/node/useWatchWidget.ts","../../src/composables/node/useNodeBadge.ts","../../src/composables/useCanvasDrop.ts","../../src/composables/useContextMenuTranslation.ts","../../src/composables/useCopy.ts","../../src/composables/useGlobalLitegraph.ts","../../src/platform/settings/composables/useLitegraphSettings.ts","../../src/platform/settings/constants/coreSettings.ts","../../src/platform/workflow/persistence/composables/useWorkflowAutoSave.ts","../../src/platform/workflow/templates/composables/useTemplateUrlLoader.ts","../../src/platform/workflow/persistence/composables/useWorkflowPersistence.ts","../../src/renderer/core/layout/transform/useTransformSettling.ts","../../src/renderer/core/layout/transform/useTransformState.ts","../../src/renderer/core/layout/transform/TransformPane.vue","../../src/renderer/extensions/minimap/MiniMap.vue","../../src/renderer/extensions/vueNodes/utils/selectionUtils.ts","../../src/renderer/extensions/vueNodes/composables/useNodeEventHandlers.ts","../../src/renderer/extensions/vueNodes/composables/useNodeSnap.ts","../../src/renderer/extensions/vueNodes/composables/useShiftKeySync.ts","../../src/renderer/extensions/vueNodes/layout/useNodeDrag.ts","../../src/renderer/extensions/vueNodes/composables/useNodePointerInteractions.ts","../../src/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking.ts","../../src/renderer/extensions/vueNodes/execution/useNodeExecutionState.ts","../../src/renderer/extensions/vueNodes/layout/useNodeLayout.ts","../../src/renderer/extensions/vueNodes/preview/useNodePreviewState.ts","../../src/renderer/extensions/vueNodes/interactions/resize/useNodeResize.ts","../../src/renderer/extensions/vueNodes/components/LivePreview.vue","../../src/renderer/extensions/vueNodes/VideoPreview.vue","../../src/renderer/extensions/vueNodes/components/ImagePreview.vue","../../src/renderer/extensions/vueNodes/components/NodeContent.vue","../../src/renderer/extensions/vueNodes/components/LGraphNode.vue","../../src/services/newUserService.ts","../../src/components/graph/SelectionRectangle.vue","../../src/components/graph/GraphCanvas.vue","../../src/components/toast/RerouteMigrationToast.vue","../../src/composables/useBrowserTabTitle.ts","../../src/components/widget/panel/RightSidePanel.vue","../../src/components/widget/SampleModelSelector.vue","../../src/composables/useModelSelectorDialog.ts","../../src/platform/assets/utils/createModelNodeFromAsset.ts","../../src/platform/support/config.ts","../../src/composables/useCoreCommands.ts","../../src/composables/useProgressFavicon.ts","../../src/types/serverArgs.ts","../../src/constants/serverConfig.ts","../../src/components/honeyToast/HoneyToast.vue","../../src/components/common/StatusBadge.vue","../../src/components/toast/ProgressToastItem.vue","../../src/platform/assets/components/ModelImportProgressDialog.vue","../../src/platform/updates/common/versionCompatibilityStore.ts","../../src/platform/updates/common/useFrontendVersionMismatchWarning.ts","../../src/services/autoQueueService.ts","../../src/components/ui/TypeformPopoverButton.vue","../../src/renderer/extensions/linearMode/DropZone.vue","../../src/renderer/extensions/linearMode/LinearControls.vue","../../src/components/ui/ZoomPane.vue","../../src/renderer/extensions/linearMode/ImagePreview.vue","../../src/renderer/extensions/linearMode/Preview3d.vue","../../src/renderer/extensions/linearMode/VideoPreview.vue","../../src/renderer/extensions/linearMode/mediaTypes.ts","../../src/renderer/extensions/linearMode/LinearPreview.vue","../../src/renderer/extensions/linearMode/OutputHistory.vue","../../src/views/LinearView.vue","../../src/workbench/extensions/manager/components/ManagerProgressToast.vue","../../src/views/GraphView.vue"],"sourcesContent":["/**\n * Cross-browser async utilities for scheduling tasks during browser idle time\n * with proper fallbacks for browsers that don't support requestIdleCallback.\n *\n * Implementation based on:\n * https://github.com/microsoft/vscode/blob/main/src/vs/base/common/async.ts\n */\n\ninterface IdleDeadline {\n didTimeout: boolean\n timeRemaining(): number\n}\n\ninterface IDisposable {\n dispose(): void\n}\n\n/**\n * Internal implementation function that handles the actual scheduling logic.\n * Uses feature detection to determine whether to use native requestIdleCallback\n * or fall back to setTimeout-based implementation.\n */\nlet _runWhenIdle: (\n targetWindow: any,\n callback: (idle: IdleDeadline) => void,\n timeout?: number\n) => IDisposable\n\n/**\n * Execute the callback during the next browser idle period.\n * Falls back to setTimeout-based scheduling in browsers without native support.\n */\nexport let runWhenGlobalIdle: (\n callback: (idle: IdleDeadline) => void,\n timeout?: number\n ) => IDisposable\n\n // Self-invoking function to set up the idle callback implementation\n;(function () {\n const safeGlobal: any = globalThis\n\n if (\n typeof safeGlobal.requestIdleCallback !== 'function' ||\n typeof safeGlobal.cancelIdleCallback !== 'function'\n ) {\n // Fallback implementation for browsers without native support (e.g., Safari)\n _runWhenIdle = (_targetWindow, runner, _timeout?) => {\n setTimeout(() => {\n if (disposed) {\n return\n }\n\n // Simulate IdleDeadline - give 15ms window (one frame at ~64fps)\n const end = Date.now() + 15\n const deadline: IdleDeadline = {\n didTimeout: true,\n timeRemaining() {\n return Math.max(0, end - Date.now())\n }\n }\n\n runner(Object.freeze(deadline))\n })\n\n let disposed = false\n return {\n dispose() {\n if (disposed) {\n return\n }\n disposed = true\n }\n }\n }\n } else {\n // Native requestIdleCallback implementation\n _runWhenIdle = (targetWindow: typeof safeGlobal, runner, timeout?) => {\n const handle: number = targetWindow.requestIdleCallback(\n runner,\n typeof timeout === 'number' ? { timeout } : undefined\n )\n\n let disposed = false\n return {\n dispose() {\n if (disposed) {\n return\n }\n disposed = true\n targetWindow.cancelIdleCallback(handle)\n }\n }\n }\n }\n\n runWhenGlobalIdle = (runner, timeout) =>\n _runWhenIdle(globalThis, runner, timeout)\n})()\n","<template>\n <div\n v-show=\"workspaceState.focusMode\"\n class=\"fixed z-9999 flex flex-row no-drag top-0 right-0\"\n >\n <Button\n v-tooltip=\"{ value: $t('menu.showMenu'), showDelay: 300 }\"\n variant=\"muted-textonly\"\n size=\"lg\"\n :aria-label=\"$t('menu.showMenu')\"\n aria-live=\"assertive\"\n @click=\"exitFocusMode\"\n @contextmenu=\"showNativeSystemMenu\"\n >\n <i class=\"pi pi-bars\" />\n </Button>\n <div class=\"window-actions-spacer\" />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { watchEffect } from 'vue'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { app } from '@/scripts/app'\nimport { useWorkspaceStore } from '@/stores/workspaceStore'\nimport { showNativeSystemMenu } from '@/utils/envUtil'\n\nconst workspaceState = useWorkspaceStore()\nconst settingStore = useSettingStore()\nconst exitFocusMode = () => {\n workspaceState.focusMode = false\n}\n\nwatchEffect(() => {\n if (settingStore.get('Comfy.UseNewMenu') !== 'Disabled') {\n return\n }\n if (workspaceState.focusMode) {\n app.ui.menuContainer.style.display = 'none'\n } else {\n app.ui.menuContainer.style.display = 'block'\n }\n})\n</script>\n","<template>\n <div>\n <!--\n UnloadWindowConfirmDialog: This component does not render\n anything visible. It is used to confirm the user wants to\n close the window, and if they do, it will call the\n beforeunload event.\n -->\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { onBeforeUnmount, onMounted } from 'vue'\n\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\n\nconst settingStore = useSettingStore()\nconst workflowStore = useWorkflowStore()\n\nconst handleBeforeUnload = (event: BeforeUnloadEvent) => {\n if (\n settingStore.get('Comfy.Window.UnloadConfirmation') &&\n workflowStore.modifiedWorkflows.length > 0\n ) {\n event.preventDefault()\n return true\n }\n return undefined\n}\n\nonMounted(() => {\n window.addEventListener('beforeunload', handleBeforeUnload)\n})\n\nonBeforeUnmount(() => {\n window.removeEventListener('beforeunload', handleBeforeUnload)\n})\n</script>\n","<template>\n <div\n class=\"w-full h-full absolute top-0 left-0 z-999 pointer-events-none flex flex-col\"\n >\n <slot name=\"workflow-tabs\" />\n\n <div\n class=\"pointer-events-none flex flex-1 overflow-hidden\"\n :class=\"{\n 'flex-row': sidebarLocation === 'left',\n 'flex-row-reverse': sidebarLocation === 'right'\n }\"\n >\n <div class=\"side-toolbar-container\">\n <slot name=\"side-toolbar\" />\n </div>\n\n <Splitter\n :key=\"splitterRefreshKey\"\n class=\"bg-transparent pointer-events-none border-none flex-1 overflow-hidden\"\n :state-key=\"sidebarStateKey\"\n state-storage=\"local\"\n @resizestart=\"onResizestart\"\n >\n <!-- First panel: sidebar when left, properties when right -->\n <SplitterPanel\n v-if=\"\n !focusMode && (sidebarLocation === 'left' || rightSidePanelVisible)\n \"\n :class=\"\n sidebarLocation === 'left'\n ? cn(\n 'side-bar-panel bg-comfy-menu-bg pointer-events-auto',\n sidebarPanelVisible && 'min-w-78'\n )\n : 'bg-comfy-menu-bg pointer-events-auto'\n \"\n :min-size=\"sidebarLocation === 'left' ? 10 : 15\"\n :size=\"20\"\n :style=\"firstPanelStyle\"\n :role=\"sidebarLocation === 'left' ? 'complementary' : undefined\"\n :aria-label=\"\n sidebarLocation === 'left' ? t('sideToolbar.sidebar') : undefined\n \"\n >\n <slot\n v-if=\"sidebarLocation === 'left' && sidebarPanelVisible\"\n name=\"side-bar-panel\"\n />\n <slot\n v-else-if=\"sidebarLocation === 'right'\"\n name=\"right-side-panel\"\n />\n </SplitterPanel>\n\n <!-- Main panel (always present) -->\n <SplitterPanel :size=\"80\" class=\"flex flex-col\">\n <slot name=\"topmenu\" :sidebar-panel-visible />\n\n <Splitter\n class=\"bg-transparent pointer-events-none border-none splitter-overlay-bottom mr-1 mb-1 ml-1 flex-1\"\n layout=\"vertical\"\n :pt:gutter=\"\n cn(\n 'rounded-tl-lg rounded-tr-lg ',\n !(bottomPanelVisible && !focusMode) && 'hidden'\n )\n \"\n state-key=\"bottom-panel-splitter\"\n state-storage=\"local\"\n @resizestart=\"onResizestart\"\n >\n <SplitterPanel class=\"graph-canvas-panel relative\">\n <slot name=\"graph-canvas-panel\" />\n </SplitterPanel>\n <SplitterPanel\n v-show=\"bottomPanelVisible && !focusMode\"\n class=\"bottom-panel border border-(--p-panel-border-color) max-w-full overflow-x-auto bg-comfy-menu-bg pointer-events-auto rounded-lg\"\n >\n <slot name=\"bottom-panel\" />\n </SplitterPanel>\n </Splitter>\n </SplitterPanel>\n\n <!-- Last panel: properties when left, sidebar when right -->\n <SplitterPanel\n v-if=\"\n !focusMode && (sidebarLocation === 'right' || rightSidePanelVisible)\n \"\n :class=\"\n sidebarLocation === 'right'\n ? cn(\n 'side-bar-panel bg-comfy-menu-bg pointer-events-auto',\n sidebarPanelVisible && 'min-w-78'\n )\n : 'bg-comfy-menu-bg pointer-events-auto'\n \"\n :min-size=\"sidebarLocation === 'right' ? 10 : 15\"\n :size=\"20\"\n :style=\"lastPanelStyle\"\n :role=\"sidebarLocation === 'right' ? 'complementary' : undefined\"\n :aria-label=\"\n sidebarLocation === 'right' ? t('sideToolbar.sidebar') : undefined\n \"\n >\n <slot v-if=\"sidebarLocation === 'left'\" name=\"right-side-panel\" />\n <slot\n v-else-if=\"sidebarLocation === 'right' && sidebarPanelVisible\"\n name=\"side-bar-panel\"\n />\n </SplitterPanel>\n </Splitter>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { cn } from '@comfyorg/tailwind-utils'\nimport { storeToRefs } from 'pinia'\nimport Splitter from 'primevue/splitter'\nimport type { SplitterResizeStartEvent } from 'primevue/splitter'\nimport SplitterPanel from 'primevue/splitterpanel'\nimport { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'\nimport { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'\nimport { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'\nimport { useWorkspaceStore } from '@/stores/workspaceStore'\n\nconst workspaceStore = useWorkspaceStore()\nconst settingStore = useSettingStore()\nconst rightSidePanelStore = useRightSidePanelStore()\nconst sidebarTabStore = useSidebarTabStore()\nconst { t } = useI18n()\nconst sidebarLocation = computed<'left' | 'right'>(() =>\n settingStore.get('Comfy.Sidebar.Location')\n)\n\nconst unifiedWidth = computed(() =>\n settingStore.get('Comfy.Sidebar.UnifiedWidth')\n)\n\nconst { focusMode } = storeToRefs(workspaceStore)\n\nconst { activeSidebarTabId, activeSidebarTab } = storeToRefs(sidebarTabStore)\nconst { bottomPanelVisible } = storeToRefs(useBottomPanelStore())\nconst { isOpen: rightSidePanelVisible } = storeToRefs(rightSidePanelStore)\n\nconst sidebarPanelVisible = computed(() => activeSidebarTab.value !== null)\n\nconst sidebarStateKey = computed(() => {\n return unifiedWidth.value\n ? 'unified-sidebar'\n : // When no tab is active, use a default key to maintain state\n (activeSidebarTabId.value ?? 'default-sidebar')\n})\n\n/**\n * Avoid triggering default behaviors during drag-and-drop, such as text selection.\n */\nfunction onResizestart({ originalEvent: event }: SplitterResizeStartEvent) {\n event.preventDefault()\n}\n\n/*\n * Force refresh the splitter when right panel visibility or sidebar location changes\n * to recalculate the width and panel order\n */\nconst splitterRefreshKey = computed(() => {\n return `main-splitter${rightSidePanelVisible.value ? '-with-right-panel' : ''}-${sidebarLocation.value}`\n})\n\nconst firstPanelStyle = computed(() => {\n if (sidebarLocation.value === 'left') {\n return { display: sidebarPanelVisible.value ? 'flex' : 'none' }\n }\n return undefined\n})\n\nconst lastPanelStyle = computed(() => {\n if (sidebarLocation.value === 'right') {\n return { display: sidebarPanelVisible.value ? 'flex' : 'none' }\n }\n return undefined\n})\n</script>\n\n<style scoped>\n:deep(.p-splitter-gutter) {\n pointer-events: auto;\n}\n\n:deep(.p-splitter-gutter:hover),\n:deep(.p-splitter-gutter[data-p-gutter-resizing='true']) {\n transition: background-color 0.2s ease 300ms;\n background-color: var(--p-primary-color);\n}\n\n/* Hide sidebar gutter when sidebar is not visible */\n:deep(.side-bar-panel[style*='display: none'] + .p-splitter-gutter),\n:deep(.p-splitter-gutter + .side-bar-panel[style*='display: none']) {\n display: none;\n}\n\n.splitter-overlay-bottom :deep(.p-splitter-gutter) {\n transform: translateY(5px);\n}\n</style>\n","/**\n * Build a tooltip configuration object compatible with v-tooltip.\n * Consumers pass the translated text value.\n */\nexport const buildTooltipConfig = (value: string) => ({\n value,\n showDelay: 300,\n hideDelay: 0,\n pt: {\n text: {\n class:\n 'border-node-component-tooltip-border bg-node-component-tooltip-surface text-node-component-tooltip border rounded-md px-2 py-1 text-xs leading-none shadow-none'\n },\n arrow: {\n class: 'border-t-node-component-tooltip-border'\n }\n }\n})\n","import { defineAsyncComponent } from 'vue'\n\nimport { isCloud } from '@/platform/distribution/types'\n\nexport default isCloud && window.__CONFIG__?.subscription_required\n ? defineAsyncComponent(() => import('./CloudRunButtonWrapper.vue'))\n : defineAsyncComponent(() => import('./ComfyQueueButton.vue'))\n","<template>\n <div class=\"flex h-full items-center\" :class=\"cn(!isDocked && '-ml-2')\">\n <div\n v-if=\"isDragging && !isDocked\"\n :class=\"actionbarClass\"\n @mouseenter=\"onMouseEnterDropZone\"\n @mouseleave=\"onMouseLeaveDropZone\"\n >\n {{ t('actionbar.dockToTop') }}\n </div>\n\n <Panel\n class=\"pointer-events-auto\"\n :style=\"style\"\n :class=\"panelClass\"\n :pt=\"{\n header: { class: 'hidden' },\n content: { class: isDocked ? 'p-0' : 'p-1' }\n }\"\n >\n <div ref=\"panelRef\" class=\"flex items-center select-none gap-2\">\n <span\n ref=\"dragHandleRef\"\n :class=\"\n cn(\n 'drag-handle cursor-grab w-3 h-max',\n isDragging && 'cursor-grabbing'\n )\n \"\n />\n <Suspense @resolve=\"comfyRunButtonResolved\">\n <ComfyRunButton />\n </Suspense>\n <Button\n v-tooltip.bottom=\"cancelJobTooltipConfig\"\n variant=\"destructive\"\n size=\"icon\"\n :disabled=\"isExecutionIdle\"\n :aria-label=\"t('menu.interrupt')\"\n @click=\"cancelCurrentJob\"\n >\n <i class=\"icon-[lucide--x] size-4\" />\n </Button>\n </div>\n </Panel>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport {\n useDraggable,\n useEventListener,\n useLocalStorage,\n watchDebounced\n} from '@vueuse/core'\nimport { clamp } from 'es-toolkit/compat'\nimport { storeToRefs } from 'pinia'\nimport Panel from 'primevue/panel'\nimport { computed, nextTick, ref, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { buildTooltipConfig } from '@/composables/useTooltipConfig'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { useExecutionStore } from '@/stores/executionStore'\nimport { cn } from '@/utils/tailwindUtil'\n\nimport ComfyRunButton from './ComfyRunButton'\n\nconst settingsStore = useSettingStore()\nconst commandStore = useCommandStore()\nconst { t } = useI18n()\nconst { isIdle: isExecutionIdle } = storeToRefs(useExecutionStore())\n\nconst position = computed(() => settingsStore.get('Comfy.UseNewMenu'))\nconst visible = computed(() => position.value !== 'Disabled')\n\nconst panelRef = ref<HTMLElement | null>(null)\nconst dragHandleRef = ref<HTMLElement | null>(null)\nconst isDocked = useLocalStorage('Comfy.MenuPosition.Docked', true)\nconst storedPosition = useLocalStorage('Comfy.MenuPosition.Floating', {\n x: 0,\n y: 0\n})\nconst { x, y, style, isDragging } = useDraggable(panelRef, {\n initialValue: { x: 0, y: 0 },\n handle: dragHandleRef,\n containerElement: document.body\n})\n\n// Update storedPosition when x or y changes\nwatchDebounced(\n [x, y],\n ([newX, newY]) => {\n storedPosition.value = { x: newX, y: newY }\n },\n { debounce: 300 }\n)\n\n// Set initial position to bottom center\nconst setInitialPosition = () => {\n if (panelRef.value) {\n const screenWidth = window.innerWidth\n const screenHeight = window.innerHeight\n const menuWidth = panelRef.value.offsetWidth\n const menuHeight = panelRef.value.offsetHeight\n\n if (menuWidth === 0 || menuHeight === 0) {\n return\n }\n\n // Check if stored position exists and is within bounds\n if (storedPosition.value.x !== 0 || storedPosition.value.y !== 0) {\n // Ensure stored position is within screen bounds\n x.value = clamp(storedPosition.value.x, 0, screenWidth - menuWidth)\n y.value = clamp(storedPosition.value.y, 0, screenHeight - menuHeight)\n captureLastDragState()\n return\n }\n\n // If no stored position or current position, set to bottom center\n if (x.value === 0 && y.value === 0) {\n x.value = clamp((screenWidth - menuWidth) / 2, 0, screenWidth - menuWidth)\n y.value = clamp(\n screenHeight - menuHeight - 10,\n 0,\n screenHeight - menuHeight\n )\n captureLastDragState()\n }\n }\n}\n\n//The ComfyRunButton is a dynamic import. Which means it will not be loaded onMount in this component.\n//So we must use suspense resolve to ensure that is has loaded and updated the DOM before calling setInitialPosition()\nasync function comfyRunButtonResolved() {\n await nextTick()\n setInitialPosition()\n}\n\nwatch(visible, async (newVisible) => {\n if (newVisible) {\n await nextTick(setInitialPosition)\n }\n})\n\n/**\n * Track run button handle drag start using mousedown on the drag handle.\n */\nuseEventListener(dragHandleRef, 'mousedown', () => {\n useTelemetry()?.trackUiButtonClicked({\n button_id: 'actionbar_run_handle_drag_start'\n })\n})\n\nconst lastDragState = ref({\n x: x.value,\n y: y.value,\n windowWidth: window.innerWidth,\n windowHeight: window.innerHeight\n})\nconst captureLastDragState = () => {\n lastDragState.value = {\n x: x.value,\n y: y.value,\n windowWidth: window.innerWidth,\n windowHeight: window.innerHeight\n }\n}\nwatch(\n isDragging,\n (newIsDragging) => {\n if (!newIsDragging) {\n // Stop dragging\n captureLastDragState()\n }\n },\n { immediate: true }\n)\n\nconst adjustMenuPosition = () => {\n if (panelRef.value) {\n const screenWidth = window.innerWidth\n const screenHeight = window.innerHeight\n const menuWidth = panelRef.value.offsetWidth\n const menuHeight = panelRef.value.offsetHeight\n\n // Calculate distances to all edges\n const distanceLeft = lastDragState.value.x\n const distanceRight =\n lastDragState.value.windowWidth - (lastDragState.value.x + menuWidth)\n const distanceTop = lastDragState.value.y\n const distanceBottom =\n lastDragState.value.windowHeight - (lastDragState.value.y + menuHeight)\n\n // Find the smallest distance to determine which edge to anchor to\n const distances = [\n { edge: 'left', distance: distanceLeft },\n { edge: 'right', distance: distanceRight },\n { edge: 'top', distance: distanceTop },\n { edge: 'bottom', distance: distanceBottom }\n ]\n const closestEdge = distances.reduce((min, curr) =>\n curr.distance < min.distance ? curr : min\n )\n\n // Calculate vertical position as a percentage of screen height\n const verticalRatio =\n lastDragState.value.y / lastDragState.value.windowHeight\n const horizontalRatio =\n lastDragState.value.x / lastDragState.value.windowWidth\n\n // Apply positioning based on closest edge\n if (closestEdge.edge === 'left') {\n x.value = closestEdge.distance // Maintain exact distance from left\n y.value = verticalRatio * screenHeight\n } else if (closestEdge.edge === 'right') {\n x.value = screenWidth - menuWidth - closestEdge.distance // Maintain exact distance from right\n y.value = verticalRatio * screenHeight\n } else if (closestEdge.edge === 'top') {\n x.value = horizontalRatio * screenWidth\n y.value = closestEdge.distance // Maintain exact distance from top\n } else {\n // bottom\n x.value = horizontalRatio * screenWidth\n y.value = screenHeight - menuHeight - closestEdge.distance // Maintain exact distance from bottom\n }\n\n // Ensure the menu stays within the screen bounds\n x.value = clamp(x.value, 0, screenWidth - menuWidth)\n y.value = clamp(y.value, 0, screenHeight - menuHeight)\n }\n}\n\nuseEventListener(window, 'resize', adjustMenuPosition)\n\n// Drop zone state\nconst isMouseOverDropZone = ref(false)\n\n// Mouse event handlers for self-contained drop zone\nconst onMouseEnterDropZone = () => {\n if (isDragging.value) {\n isMouseOverDropZone.value = true\n }\n}\n\nconst onMouseLeaveDropZone = () => {\n if (isDragging.value) {\n isMouseOverDropZone.value = false\n }\n}\n\n// Handle drag state changes\nwatch(isDragging, (dragging) => {\n if (dragging) {\n // Starting to drag - undock if docked\n if (isDocked.value) {\n isDocked.value = false\n }\n } else {\n // Stopped dragging - dock if mouse is over drop zone\n if (isMouseOverDropZone.value) {\n isDocked.value = true\n }\n // Reset drop zone state\n isMouseOverDropZone.value = false\n }\n})\n\nconst cancelJobTooltipConfig = computed(() =>\n buildTooltipConfig(t('menu.interrupt'))\n)\n\nconst cancelCurrentJob = async () => {\n if (isExecutionIdle.value) return\n await commandStore.execute('Comfy.Interrupt')\n}\n\nconst actionbarClass = computed(() =>\n cn(\n 'w-[200px] border-dashed border-blue-500 opacity-80',\n 'm-1.5 flex items-center justify-center self-stretch',\n 'rounded-md before:w-50 before:-ml-50 before:h-full',\n 'pointer-events-auto',\n isMouseOverDropZone.value &&\n 'border-[3px] opacity-100 scale-105 shadow-[0_0_20px] shadow-blue-500'\n )\n)\nconst panelClass = computed(() =>\n cn(\n 'actionbar pointer-events-auto z-1300',\n isDragging.value && 'select-none pointer-events-none',\n isDocked.value\n ? 'p-0 static border-none bg-transparent'\n : 'fixed shadow-interface'\n )\n)\n</script>\n","import type { MenuItem } from 'primevue/menuitem'\nimport type { ComputedRef, Ref } from 'vue'\nimport { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useWorkflowService } from '@/platform/workflow/core/services/workflowService'\nimport type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'\nimport {\n useWorkflowBookmarkStore,\n useWorkflowStore\n} from '@/platform/workflow/management/stores/workflowStore'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { useSubgraphStore } from '@/stores/subgraphStore'\n\ninterface WorkflowActionsMenuOptions {\n /** Whether this is the root workflow level. Defaults to true. */\n isRoot?: boolean\n /** Whether to include the delete workflow action. Defaults to true. */\n includeDelete?: boolean\n /** Override the workflow to operate on. If not provided, uses activeWorkflow. */\n workflow?: Ref<ComfyWorkflow | null> | ComputedRef<ComfyWorkflow | null>\n}\n\nexport function useWorkflowActionsMenu(\n startRename: () => void,\n options: WorkflowActionsMenuOptions = {}\n) {\n const { isRoot = true, includeDelete = true, workflow } = options\n const { t } = useI18n()\n const workflowStore = useWorkflowStore()\n const workflowService = useWorkflowService()\n const bookmarkStore = useWorkflowBookmarkStore()\n const commandStore = useCommandStore()\n const subgraphStore = useSubgraphStore()\n\n const targetWorkflow = computed(\n () => workflow?.value ?? workflowStore.activeWorkflow\n )\n\n /** Switch to the target workflow tab if it's not already active */\n const ensureWorkflowActive = async (wf: ComfyWorkflow | null) => {\n if (!wf || wf === workflowStore.activeWorkflow) return\n await workflowService.openWorkflow(wf)\n }\n\n const menuItems = computed<MenuItem[]>(() => {\n const workflow = targetWorkflow.value\n const isBlueprint = workflow\n ? subgraphStore.isSubgraphBlueprint(workflow)\n : false\n\n const items: MenuItem[] = []\n\n const addItem = (\n label: string,\n icon: string,\n command: () => void,\n visible = true,\n disabled = false,\n separator = false\n ) => {\n if (!visible) return\n if (separator) items.push({ separator: true })\n items.push({ label, icon, command, disabled })\n }\n\n addItem(\n t('g.rename'),\n 'pi pi-pencil',\n async () => {\n await ensureWorkflowActive(targetWorkflow.value)\n startRename()\n },\n true,\n isRoot && !workflow?.isPersisted\n )\n\n addItem(\n t('breadcrumbsMenu.duplicate'),\n 'pi pi-copy',\n async () => {\n if (workflow) {\n await workflowService.duplicateWorkflow(workflow)\n }\n },\n isRoot && !isBlueprint\n )\n\n addItem(\n t('menuLabels.Save'),\n 'pi pi-save',\n async () => {\n await ensureWorkflowActive(workflow)\n await commandStore.execute('Comfy.SaveWorkflow')\n },\n isRoot,\n false,\n true\n )\n\n addItem(\n t('menuLabels.Save As'),\n 'pi pi-save',\n async () => {\n await ensureWorkflowActive(workflow)\n await commandStore.execute('Comfy.SaveWorkflowAs')\n },\n isRoot\n )\n\n addItem(\n bookmarkStore.isBookmarked(workflow?.path ?? '')\n ? t('tabMenu.removeFromBookmarks')\n : t('tabMenu.addToBookmarks'),\n 'pi pi-bookmark' +\n (bookmarkStore.isBookmarked(workflow?.path ?? '') ? '-fill' : ''),\n async () => {\n if (workflow?.path) {\n await bookmarkStore.toggleBookmarked(workflow.path)\n }\n },\n isRoot,\n workflow?.isTemporary ?? false\n )\n\n addItem(\n t('menuLabels.Export'),\n 'pi pi-download',\n async () => {\n await ensureWorkflowActive(workflow)\n await commandStore.execute('Comfy.ExportWorkflow')\n },\n isRoot\n )\n\n addItem(\n t('menuLabels.Export (API)'),\n 'pi pi-download',\n async () => {\n await ensureWorkflowActive(workflow)\n await commandStore.execute('Comfy.ExportWorkflowAPI')\n },\n isRoot\n )\n\n addItem(\n t('breadcrumbsMenu.clearWorkflow'),\n 'pi pi-trash',\n async () => {\n await ensureWorkflowActive(workflow)\n await commandStore.execute('Comfy.ClearWorkflow')\n },\n true,\n false,\n true\n )\n\n addItem(\n t('subgraphStore.publish'),\n 'pi pi-upload',\n async () => {\n if (workflow) {\n await workflowService.saveWorkflowAs(workflow)\n }\n },\n isRoot && isBlueprint,\n false,\n true\n )\n\n addItem(\n isBlueprint\n ? t('breadcrumbsMenu.deleteBlueprint')\n : t('breadcrumbsMenu.deleteWorkflow'),\n 'pi pi-times',\n async () => {\n if (workflow) {\n await workflowService.deleteWorkflow(workflow)\n }\n },\n isRoot && includeDelete,\n false,\n true\n )\n\n return items\n })\n\n return {\n menuItems\n }\n}\n","<template>\n <a\n ref=\"wrapperRef\"\n v-tooltip.bottom=\"{\n value: tooltipText,\n showDelay: 512\n }\"\n draggable=\"false\"\n href=\"#\"\n class=\"p-breadcrumb-item-link h-8 cursor-pointer px-2\"\n :class=\"{\n 'flex items-center gap-1': isActive,\n 'p-breadcrumb-item-link-menu-visible': menu?.overlayVisible,\n 'p-breadcrumb-item-link-icon-visible': isActive,\n 'active-breadcrumb-item': isActive\n }\"\n @click=\"handleClick\"\n >\n <i\n v-if=\"hasMissingNodes && isRoot\"\n class=\"icon-[lucide--triangle-alert] text-warning-background\"\n />\n <span class=\"p-breadcrumb-item-label px-2\">{{ item.label }}</span>\n <Tag v-if=\"item.isBlueprint\" value=\"Blueprint\" severity=\"primary\" />\n <i v-if=\"isActive\" class=\"pi pi-angle-down text-[10px]\"></i>\n </a>\n <Menu\n v-if=\"isActive || isRoot\"\n ref=\"menu\"\n :model=\"menuItems\"\n :popup=\"true\"\n :pt=\"{\n root: {\n style: 'background-color: var(--comfy-menu-bg)'\n },\n itemLink: {\n class: 'py-2'\n }\n }\"\n />\n <InputText\n v-if=\"isEditing\"\n ref=\"itemInputRef\"\n v-model=\"itemLabel\"\n class=\"fixed z-10000 px-2 py-2 text-[.8rem]\"\n @blur=\"inputBlur(false)\"\n @click.stop\n @keydown.enter=\"inputBlur(true)\"\n @keydown.esc=\"inputBlur(false)\"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport InputText from 'primevue/inputtext'\nimport type { MenuState } from 'primevue/menu'\nimport Menu from 'primevue/menu'\nimport type { MenuItem } from 'primevue/menuitem'\nimport Tag from 'primevue/tag'\nimport { computed, nextTick, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useWorkflowActionsMenu } from '@/composables/useWorkflowActionsMenu'\nimport { useWorkflowService } from '@/platform/workflow/core/services/workflowService'\nimport {\n ComfyWorkflow,\n useWorkflowStore\n} from '@/platform/workflow/management/stores/workflowStore'\nimport { app } from '@/scripts/app'\nimport { useDialogService } from '@/services/dialogService'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { useNodeDefStore } from '@/stores/nodeDefStore'\nimport { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'\nimport { appendJsonExt } from '@/utils/formatUtil'\nimport { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'\n\ninterface Props {\n item: MenuItem\n isActive?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n isActive: false\n})\n\nconst nodeDefStore = useNodeDefStore()\nconst hasMissingNodes = computed(() =>\n graphHasMissingNodes(app.rootGraph, nodeDefStore.nodeDefsByName)\n)\n\nconst { t } = useI18n()\nconst menu = ref<InstanceType<typeof Menu> & MenuState>()\nconst dialogService = useDialogService()\nconst workflowStore = useWorkflowStore()\nconst workflowService = useWorkflowService()\nconst isEditing = ref(false)\nconst itemLabel = ref<string>()\nconst itemInputRef = ref<{ $el?: HTMLInputElement }>()\nconst wrapperRef = ref<HTMLAnchorElement>()\n\nconst rename = async (\n newName: string | null | undefined,\n initialName: string\n) => {\n if (newName && newName !== initialName) {\n // Synchronize the node titles with the new name\n props.item.updateTitle?.(newName)\n\n if (workflowStore.activeSubgraph) {\n workflowStore.activeSubgraph.name = newName\n } else if (workflowStore.activeWorkflow) {\n try {\n await workflowService.renameWorkflow(\n workflowStore.activeWorkflow,\n ComfyWorkflow.basePath + appendJsonExt(newName)\n )\n } catch (error) {\n console.error(error)\n dialogService.showErrorDialog(error)\n return\n }\n }\n\n // Force the navigation stack to recompute the labels\n // TODO: investigate if there is a better way to do this\n const navigationStore = useSubgraphNavigationStore()\n navigationStore.restoreState(navigationStore.exportState())\n }\n}\n\nconst isRoot = props.item.key === 'root'\n\nconst tooltipText = computed(() => {\n if (hasMissingNodes.value && isRoot) {\n return t('breadcrumbsMenu.missingNodesWarning')\n }\n return props.item.label\n})\n\nconst startRename = async () => {\n // Check if element is hidden (collapsed breadcrumb)\n // When collapsed, root item is hidden via CSS display:none, so use rename command\n if (isRoot && wrapperRef.value?.offsetParent === null) {\n await useCommandStore().execute('Comfy.RenameWorkflow')\n return\n }\n\n isEditing.value = true\n itemLabel.value = props.item.label as string\n void nextTick(() => {\n if (itemInputRef.value?.$el) {\n itemInputRef.value.$el.focus()\n itemInputRef.value.$el.select()\n if (wrapperRef.value) {\n itemInputRef.value.$el.style.width = `${Math.max(200, wrapperRef.value.offsetWidth)}px`\n }\n }\n })\n}\n\nconst { menuItems } = useWorkflowActionsMenu(startRename, { isRoot })\n\nconst handleClick = (event: MouseEvent) => {\n if (isEditing.value) {\n return\n }\n\n if (event.detail === 1) {\n if (props.isActive) {\n menu.value?.toggle(event)\n } else {\n props.item.command?.({ item: props.item, originalEvent: event })\n }\n } else if (props.isActive && event.detail === 2) {\n menu.value?.hide()\n event.stopPropagation()\n event.preventDefault()\n startRename()\n }\n}\n\nconst inputBlur = async (doRename: boolean) => {\n if (doRename) {\n await rename(itemLabel.value, props.item.label as string)\n }\n\n isEditing.value = false\n}\n\nconst toggleMenu = (event: MouseEvent) => {\n menu.value?.toggle(event)\n}\n\ndefineExpose({\n toggleMenu\n})\n</script>\n\n<style scoped>\n@reference '../../assets/css/style.css';\n\n.p-breadcrumb-item-link,\n.p-breadcrumb-item-icon {\n @apply select-none;\n}\n\n.p-breadcrumb-item-link {\n @apply overflow-hidden;\n}\n\n.p-breadcrumb-item-label {\n @apply whitespace-nowrap text-ellipsis overflow-hidden;\n}\n\n.active-breadcrumb-item {\n color: var(--text-primary);\n}\n</style>\n","import { useMutationObserver, useResizeObserver } from '@vueuse/core'\nimport { debounce } from 'es-toolkit/compat'\nimport { readonly, ref } from 'vue'\n\n/**\n * Observes an element for overflow changes and optionally debounces the check\n * @param element - The element to observe\n * @param options - The options for the observer\n * @param options.debounceTime - The time to debounce the check in milliseconds\n * @param options.useMutationObserver - Whether to use a mutation observer to check for overflow\n * @param options.useResizeObserver - Whether to use a resize observer to check for overflow\n * @returns An object containing the isOverflowing state and the checkOverflow function to manually trigger\n */\nexport const useOverflowObserver = (\n element: HTMLElement,\n options?: {\n debounceTime?: number\n useMutationObserver?: boolean\n useResizeObserver?: boolean\n onCheck?: (isOverflowing: boolean) => void\n }\n) => {\n options = {\n debounceTime: 25,\n useMutationObserver: true,\n useResizeObserver: true,\n ...options\n }\n\n const isOverflowing = ref(false)\n const disposeFns: (() => void)[] = []\n const disposed = ref(false)\n\n const checkOverflowFn = () => {\n isOverflowing.value = element.scrollWidth > element.clientWidth\n options.onCheck?.(isOverflowing.value)\n }\n\n const checkOverflow = options.debounceTime\n ? debounce(checkOverflowFn, options.debounceTime)\n : checkOverflowFn\n\n if (options.useMutationObserver) {\n disposeFns.push(\n useMutationObserver(element, checkOverflow, {\n subtree: true,\n childList: true\n }).stop\n )\n }\n if (options.useResizeObserver) {\n disposeFns.push(useResizeObserver(element, checkOverflow).stop)\n }\n\n return {\n isOverflowing: readonly(isOverflowing),\n disposed: readonly(disposed),\n checkOverflow,\n dispose: () => {\n disposed.value = true\n disposeFns.forEach((fn) => fn())\n }\n }\n}\n","<template>\n <div\n class=\"subgraph-breadcrumb flex w-auto drop-shadow-[var(--interface-panel-drop-shadow)]\"\n :class=\"{\n 'subgraph-breadcrumb-collapse': collapseTabs,\n 'subgraph-breadcrumb-overflow': overflowingTabs\n }\"\n :style=\"{\n '--p-breadcrumb-gap': `0px`,\n '--p-breadcrumb-item-margin': `${ITEM_GAP / 2}px`,\n '--p-breadcrumb-item-min-width': `${MIN_WIDTH}px`,\n '--p-breadcrumb-item-padding': `${ITEM_PADDING}px`,\n '--p-breadcrumb-icon-width': `${ICON_WIDTH}px`\n }\"\n >\n <Button\n class=\"context-menu-button pointer-events-auto h-8 w-8 shrink-0 border border-transparent bg-transparent p-0 transition-all hover:rounded-lg hover:border-interface-stroke hover:bg-comfy-menu-bg\"\n icon=\"pi pi-bars\"\n text\n severity=\"secondary\"\n size=\"small\"\n @click=\"handleMenuClick\"\n />\n <Button\n v-if=\"isInSubgraph\"\n class=\"back-button pointer-events-auto h-8 w-8 shrink-0 border border-transparent bg-transparent p-0 transition-all hover:rounded-lg hover:border-interface-stroke hover:bg-comfy-menu-bg\"\n text\n severity=\"secondary\"\n size=\"small\"\n @click=\"handleBackClick\"\n >\n <i class=\"icon-[lucide--undo-2]\" />\n </Button>\n <Breadcrumb\n ref=\"breadcrumbRef\"\n class=\"w-fit rounded-lg p-0\"\n :class=\"{ hidden: !isInSubgraph }\"\n :model=\"items\"\n :pt=\"{ item: { class: 'pointer-events-auto' } }\"\n :aria-label=\"$t('g.graphNavigation')\"\n >\n <template #item=\"{ item }\">\n <SubgraphBreadcrumbItem\n :ref=\"(el) => setItemRef(item, el)\"\n :item=\"item\"\n :is-active=\"item.key === activeItemKey\"\n />\n </template>\n <template #separator\n ><span style=\"transform: scale(1.5)\"> / </span></template\n >\n </Breadcrumb>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport Breadcrumb from 'primevue/breadcrumb'\nimport Button from 'primevue/button'\nimport type { MenuItem } from 'primevue/menuitem'\nimport { computed, onUpdated, ref, watch } from 'vue'\n\nimport SubgraphBreadcrumbItem from '@/components/breadcrumb/SubgraphBreadcrumbItem.vue'\nimport { useOverflowObserver } from '@/composables/element/useOverflowObserver'\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'\nimport { useSubgraphStore } from '@/stores/subgraphStore'\nimport { forEachSubgraphNode } from '@/utils/graphTraversalUtil'\n\nconst MIN_WIDTH = 28\nconst ITEM_GAP = 8\nconst ITEM_PADDING = 8\nconst ICON_WIDTH = 20\n\nconst workflowStore = useWorkflowStore()\nconst navigationStore = useSubgraphNavigationStore()\nconst breadcrumbRef = ref<InstanceType<typeof Breadcrumb>>()\nconst rootItemRef = ref<InstanceType<typeof SubgraphBreadcrumbItem>>()\nconst setItemRef = (item: MenuItem, el: unknown) => {\n if (item.key === 'root') {\n rootItemRef.value = el as InstanceType<typeof SubgraphBreadcrumbItem>\n }\n}\nconst workflowName = computed(() => workflowStore.activeWorkflow?.filename)\nconst isBlueprint = computed(() =>\n useSubgraphStore().isSubgraphBlueprint(workflowStore.activeWorkflow)\n)\nconst collapseTabs = ref(false)\nconst overflowingTabs = ref(false)\n\nconst isInSubgraph = computed(() => navigationStore.navigationStack.length > 0)\n\nconst home = computed(() => ({\n label: workflowName.value,\n icon: 'pi pi-home',\n key: 'root',\n isBlueprint: isBlueprint.value,\n command: () => {\n useTelemetry()?.trackUiButtonClicked({\n button_id: 'breadcrumb_subgraph_root_selected'\n })\n const canvas = useCanvasStore().getCanvas()\n if (!canvas.graph) throw new TypeError('Canvas has no graph')\n\n canvas.setGraph(canvas.graph.rootGraph)\n }\n}))\n\nconst items = computed(() => {\n const items = navigationStore.navigationStack.map<MenuItem>((subgraph) => ({\n label: subgraph.name,\n key: `subgraph-${subgraph.id}`,\n command: () => {\n useTelemetry()?.trackUiButtonClicked({\n button_id: 'breadcrumb_subgraph_item_selected'\n })\n const canvas = useCanvasStore().getCanvas()\n if (!canvas.graph) throw new TypeError('Canvas has no graph')\n\n canvas.setGraph(subgraph)\n },\n updateTitle: (title: string) => {\n const rootGraph = useCanvasStore().getCanvas().graph?.rootGraph\n if (!rootGraph) return\n\n forEachSubgraphNode(rootGraph, subgraph.id, (node) => {\n node.title = title\n })\n }\n }))\n\n return [home.value, ...items]\n})\n\nconst activeItemKey = computed(() => items.value.at(-1)?.key)\n\nconst handleMenuClick = (event: MouseEvent) => {\n useTelemetry()?.trackUiButtonClicked({\n button_id: 'breadcrumb_subgraph_menu_selected'\n })\n rootItemRef.value?.toggleMenu(event)\n}\n\nconst handleBackClick = () => {\n void useCommandStore().execute('Comfy.Graph.ExitSubgraph')\n}\n\nconst breadcrumbElement = computed(() => {\n if (!breadcrumbRef.value) return null\n\n const el = (breadcrumbRef.value as unknown as { $el: HTMLElement }).$el\n const list = el?.querySelector('.p-breadcrumb-list') as HTMLElement\n return list\n})\n\n// Check for overflow on breadcrumb items and collapse/expand the breadcrumb to fit\nlet overflowObserver: ReturnType<typeof useOverflowObserver> | undefined\nwatch(breadcrumbElement, (el) => {\n overflowObserver?.dispose()\n overflowObserver = undefined\n\n if (!el) return\n\n overflowObserver = useOverflowObserver(el, {\n onCheck: (isOverflowing) => {\n overflowingTabs.value = isOverflowing\n\n if (collapseTabs.value) {\n // Items are currently hidden, check if we can show them\n if (!isOverflowing) {\n const items = [\n ...el.querySelectorAll('.p-breadcrumb-item')\n ] as HTMLElement[]\n\n if (items.length < 3) return\n\n const itemsWithIcon = items.filter((item) =>\n item.querySelector('.p-breadcrumb-item-link-icon-visible')\n ).length\n const separators = el.querySelectorAll(\n '.p-breadcrumb-separator'\n ) as NodeListOf<HTMLElement>\n const separator = separators[separators.length - 1] as HTMLElement\n const separatorWidth = separator.offsetWidth\n\n // items + separators + gaps + icons\n const itemsWidth =\n (MIN_WIDTH + ITEM_PADDING + ITEM_PADDING) * items.length +\n itemsWithIcon * ICON_WIDTH\n const separatorsWidth = (items.length - 1) * separatorWidth\n const gapsWidth = (items.length - 1) * (ITEM_GAP * 2)\n const totalWidth = itemsWidth + separatorsWidth + gapsWidth\n const containerWidth = el.clientWidth\n\n if (totalWidth <= containerWidth) {\n collapseTabs.value = false\n }\n }\n } else if (isOverflowing) {\n collapseTabs.value = true\n }\n }\n })\n})\n\n// If e.g. the workflow name changes, we need to check the overflow again\nonUpdated(() => {\n if (!overflowObserver?.disposed.value) {\n overflowObserver?.checkOverflow()\n }\n})\n</script>\n\n<style scoped>\n@reference '../../assets/css/style.css';\n\n.subgraph-breadcrumb:not(:empty) {\n flex: auto;\n flex-shrink: 10000;\n min-width: 120px;\n}\n\n.subgraph-breadcrumb,\n:deep(.p-breadcrumb) {\n @apply overflow-hidden;\n}\n\n:deep(.p-breadcrumb) {\n width: 100%;\n background-color: transparent;\n}\n\n:deep(.p-breadcrumb-item) {\n @apply flex items-center overflow-hidden h-8;\n min-width: calc(var(--p-breadcrumb-item-min-width) + 1rem);\n border: 1px solid transparent;\n background-color: transparent;\n transition: all 0.2s;\n /* Collapse middle items first */\n flex-shrink: 10000;\n}\n\n:deep(.p-breadcrumb-separator) {\n border: 1px solid transparent;\n background-color: transparent;\n display: flex;\n padding: 0 var(--p-breadcrumb-item-margin);\n}\n\n:deep(.p-breadcrumb-item-link) {\n padding: 0\n calc(var(--p-breadcrumb-item-margin) + var(--p-breadcrumb-item-padding));\n}\n\n:deep(.p-breadcrumb-item:hover) {\n @apply rounded-lg;\n border-color: var(--interface-stroke);\n background-color: var(--comfy-menu-bg);\n}\n\n:deep(.p-breadcrumb-item:has(.p-breadcrumb-item-link-icon-visible)) {\n min-width: calc(var(--p-breadcrumb-item-min-width) + 1rem + 20px);\n}\n\n:deep(.p-breadcrumb-item:first-child) {\n /* Then collapse the root workflow */\n flex-shrink: 5000;\n\n .p-breadcrumb-item-link {\n padding-left: var(--p-breadcrumb-item-padding);\n }\n}\n\n:deep(.p-breadcrumb-item:last-child) {\n /* Then collapse the active item */\n flex-shrink: 1;\n}\n\n:deep(.p-breadcrumb-item-link-menu-visible) {\n background-color: color-mix(\n in srgb,\n var(--fg-color) 10%,\n var(--comfy-menu-bg)\n ) !important;\n color: var(--fg-color);\n}\n</style>\n\n<style>\n@reference '../../assets/css/style.css';\n\n.subgraph-breadcrumb-collapse .p-breadcrumb-list {\n .p-breadcrumb-item,\n .p-breadcrumb-separator {\n @apply hidden;\n }\n\n .p-breadcrumb-item:nth-last-child(3),\n .p-breadcrumb-separator:nth-last-child(2),\n .p-breadcrumb-item:nth-last-child(1) {\n @apply flex;\n }\n}\n</style>\n","<template>\n <div class=\"flex flex-col gap-3 p-2\">\n <div class=\"flex flex-col gap-1\">\n <div\n class=\"relative h-2 w-full overflow-hidden rounded-full border border-interface-stroke bg-interface-panel-surface\"\n >\n <div\n class=\"absolute inset-0 h-full rounded-full transition-[width]\"\n :style=\"totalProgressStyle\"\n />\n <div\n class=\"absolute inset-0 h-full rounded-full transition-[width]\"\n :style=\"currentNodeProgressStyle\"\n />\n </div>\n <div class=\"flex items-start justify-end gap-4 text-[12px] leading-none\">\n <div class=\"flex items-center gap-1 text-text-primary opacity-90\">\n <i18n-t keypath=\"sideToolbar.queueProgressOverlay.total\">\n <template #percent>\n <span class=\"font-bold\">{{ totalPercentFormatted }}</span>\n </template>\n </i18n-t>\n </div>\n <div class=\"flex items-center gap-1 text-text-secondary\">\n <span>{{ t('sideToolbar.queueProgressOverlay.currentNode') }}</span>\n <span class=\"inline-block max-w-[10rem] truncate\">{{\n currentNodeName\n }}</span>\n <span class=\"flex items-center gap-1\">\n <span>{{ currentNodePercentFormatted }}</span>\n </span>\n </div>\n </div>\n </div>\n\n <div :class=\"bottomRowClass\">\n <div class=\"flex items-center gap-4 text-[12px] text-text-primary\">\n <div class=\"flex items-center gap-2\">\n <span class=\"opacity-90\">\n <span class=\"font-bold\">{{ runningCount }}</span>\n <span class=\"ml-1\">{{\n t('sideToolbar.queueProgressOverlay.running')\n }}</span>\n </span>\n <Button\n v-if=\"runningCount > 0\"\n v-tooltip.top=\"cancelJobTooltip\"\n variant=\"destructive\"\n size=\"icon\"\n :aria-label=\"t('sideToolbar.queueProgressOverlay.interruptAll')\"\n @click=\"$emit('interruptAll')\"\n >\n <i\n class=\"icon-[lucide--x] block size-4 leading-none text-text-primary\"\n />\n </Button>\n </div>\n\n <div class=\"flex items-center gap-2\">\n <span class=\"opacity-90\">\n <span class=\"font-bold\">{{ queuedCount }}</span>\n <span class=\"ml-1\">{{\n t('sideToolbar.queueProgressOverlay.queuedSuffix')\n }}</span>\n </span>\n <Button\n v-if=\"queuedCount > 0\"\n v-tooltip.top=\"clearQueueTooltip\"\n variant=\"destructive\"\n size=\"icon\"\n :aria-label=\"t('sideToolbar.queueProgressOverlay.clearQueued')\"\n @click=\"$emit('clearQueued')\"\n >\n <i\n class=\"icon-[lucide--list-x] block size-4 leading-none text-text-primary\"\n />\n </Button>\n </div>\n </div>\n\n <Button\n class=\"min-w-30 flex-1 px-2 py-0\"\n variant=\"secondary\"\n size=\"md\"\n @click=\"$emit('viewAllJobs')\"\n >\n {{ t('sideToolbar.queueProgressOverlay.viewAllJobs') }}\n </Button>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { buildTooltipConfig } from '@/composables/useTooltipConfig'\n\ndefineProps<{\n totalProgressStyle: Record<string, string>\n currentNodeProgressStyle: Record<string, string>\n totalPercentFormatted: string\n currentNodePercentFormatted: string\n currentNodeName: string\n runningCount: number\n queuedCount: number\n bottomRowClass: string\n}>()\n\ndefineEmits<{\n (e: 'interruptAll'): void\n (e: 'clearQueued'): void\n (e: 'viewAllJobs'): void\n}>()\n\nconst { t } = useI18n()\nconst cancelJobTooltip = computed(() =>\n buildTooltipConfig(t('sideToolbar.queueProgressOverlay.cancelJobTooltip'))\n)\nconst clearQueueTooltip = computed(() =>\n buildTooltipConfig(t('sideToolbar.queueProgressOverlay.clearQueueTooltip'))\n)\n</script>\n","<template>\n <Button\n variant=\"secondary\"\n size=\"lg\"\n class=\"group w-full justify-between gap-3 p-1 text-left font-normal hover:cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background\"\n :aria-label=\"props.ariaLabel\"\n @click=\"emit('click', $event)\"\n >\n <span class=\"inline-flex items-center gap-2\">\n <span v-if=\"props.mode === 'allFailed'\" class=\"inline-flex items-center\">\n <i\n class=\"ml-1 icon-[lucide--circle-alert] block size-4 leading-none text-destructive-background\"\n />\n </span>\n\n <span class=\"inline-flex items-center gap-2\">\n <span\n v-if=\"props.mode !== 'allFailed'\"\n class=\"relative inline-flex h-6 items-center\"\n >\n <span\n v-for=\"(url, idx) in props.thumbnailUrls\"\n :key=\"url + idx\"\n class=\"inline-block h-6 w-6 overflow-hidden rounded-[6px] border-0 bg-secondary-background\"\n :style=\"{ marginLeft: idx === 0 ? '0' : '-12px' }\"\n >\n <img\n :src=\"url\"\n :alt=\"$t('sideToolbar.queueProgressOverlay.preview')\"\n class=\"h-full w-full object-cover\"\n />\n </span>\n </span>\n\n <span class=\"text-[14px] font-normal text-text-primary\">\n <template v-if=\"props.mode === 'allSuccess'\">\n <i18n-t\n keypath=\"sideToolbar.queueProgressOverlay.jobsCompleted\"\n :plural=\"props.completedCount\"\n >\n <template #count>\n <span class=\"font-bold\">{{ props.completedCount }}</span>\n </template>\n </i18n-t>\n </template>\n <template v-else-if=\"props.mode === 'mixed'\">\n <i18n-t\n keypath=\"sideToolbar.queueProgressOverlay.jobsCompleted\"\n :plural=\"props.completedCount\"\n >\n <template #count>\n <span class=\"font-bold\">{{ props.completedCount }}</span>\n </template>\n </i18n-t>\n <span>, </span>\n <i18n-t\n keypath=\"sideToolbar.queueProgressOverlay.jobsFailed\"\n :plural=\"props.failedCount\"\n >\n <template #count>\n <span class=\"font-bold\">{{ props.failedCount }}</span>\n </template>\n </i18n-t>\n </template>\n <template v-else>\n <i18n-t\n keypath=\"sideToolbar.queueProgressOverlay.jobsFailed\"\n :plural=\"props.failedCount\"\n >\n <template #count>\n <span class=\"font-bold\">{{ props.failedCount }}</span>\n </template>\n </i18n-t>\n </template>\n </span>\n </span>\n </span>\n\n <span\n class=\"flex items-center justify-center rounded p-1 text-text-secondary transition-colors duration-200 ease-in-out\"\n >\n <i class=\"icon-[lucide--chevron-down] block size-4 leading-none\" />\n </span>\n </Button>\n</template>\n\n<script setup lang=\"ts\">\nimport Button from '@/components/ui/button/Button.vue'\nimport type {\n CompletionSummary,\n CompletionSummaryMode\n} from '@/composables/queue/useCompletionSummary'\n\ntype Props = {\n mode: CompletionSummaryMode\n completedCount: CompletionSummary['completedCount']\n failedCount: CompletionSummary['failedCount']\n thumbnailUrls?: CompletionSummary['thumbnailUrls']\n ariaLabel?: string\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n thumbnailUrls: () => []\n})\n\nconst emit = defineEmits<{\n (e: 'click', event: MouseEvent): void\n}>()\n</script>\n","<template>\n <div class=\"pointer-events-auto\">\n <CompletionSummaryBanner\n :mode=\"summary.mode\"\n :completed-count=\"summary.completedCount\"\n :failed-count=\"summary.failedCount\"\n :thumbnail-urls=\"summary.thumbnailUrls\"\n :aria-label=\"t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')\"\n @click=\"$emit('summaryClick')\"\n />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useI18n } from 'vue-i18n'\n\nimport CompletionSummaryBanner from '@/components/queue/CompletionSummaryBanner.vue'\nimport type { CompletionSummary } from '@/composables/queue/useCompletionSummary'\n\ndefineProps<{ summary: CompletionSummary }>()\n\ndefineEmits<{\n (e: 'summaryClick'): void\n}>()\n\nconst { t } = useI18n()\n</script>\n","<template>\n <div\n class=\"flex h-12 items-center justify-between gap-2 border-b border-interface-stroke px-2\"\n >\n <div class=\"px-2 text-[14px] font-normal text-text-primary\">\n <span>{{ headerTitle }}</span>\n <span\n v-if=\"showConcurrentIndicator\"\n class=\"ml-4 inline-flex items-center gap-1 text-blue-100\"\n >\n <span class=\"inline-block size-2 rounded-full bg-blue-100\" />\n <span>\n <span class=\"font-bold\">{{ concurrentWorkflowCount }}</span>\n <span class=\"ml-1\">{{\n t('sideToolbar.queueProgressOverlay.running')\n }}</span>\n </span>\n </span>\n </div>\n <div v-if=\"!isCloud\" class=\"flex items-center gap-1\">\n <Button\n v-tooltip.top=\"moreTooltipConfig\"\n variant=\"textonly\"\n size=\"icon\"\n :aria-label=\"t('sideToolbar.queueProgressOverlay.moreOptions')\"\n @click=\"onMoreClick\"\n >\n <i\n class=\"icon-[lucide--more-horizontal] block size-4 leading-none text-text-secondary\"\n />\n </Button>\n <Popover\n ref=\"morePopoverRef\"\n :dismissable=\"true\"\n :close-on-escape=\"true\"\n unstyled\n :pt=\"{\n root: { class: 'absolute z-50' },\n content: {\n class: [\n 'bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg font-inter'\n ]\n }\n }\"\n >\n <div\n class=\"flex flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3 font-inter\"\n >\n <Button\n class=\"w-full justify-start\"\n variant=\"textonly\"\n size=\"sm\"\n :aria-label=\"t('sideToolbar.queueProgressOverlay.clearHistory')\"\n @click=\"onClearHistoryFromMenu\"\n >\n <i class=\"icon-[lucide--file-x-2] size-4 text-muted\" />\n <span>{{\n t('sideToolbar.queueProgressOverlay.clearHistory')\n }}</span>\n </Button>\n </div>\n </Popover>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport Popover from 'primevue/popover'\nimport type { PopoverMethods } from 'primevue/popover'\nimport { computed, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { buildTooltipConfig } from '@/composables/useTooltipConfig'\nimport { isCloud } from '@/platform/distribution/types'\n\ndefineProps<{\n headerTitle: string\n showConcurrentIndicator: boolean\n concurrentWorkflowCount: number\n}>()\n\nconst emit = defineEmits<{\n (e: 'clearHistory'): void\n}>()\n\nconst { t } = useI18n()\n\nconst morePopoverRef = ref<PopoverMethods | null>(null)\nconst moreTooltipConfig = computed(() => buildTooltipConfig(t('g.more')))\n\nconst onMoreClick = (event: MouseEvent) => {\n morePopoverRef.value?.toggle(event)\n}\nconst onClearHistoryFromMenu = () => {\n morePopoverRef.value?.hide()\n emit('clearHistory')\n}\n</script>\n","<template>\n <Popover\n ref=\"jobItemPopoverRef\"\n :dismissable=\"true\"\n :close-on-escape=\"true\"\n unstyled\n :pt=\"{\n root: { class: 'absolute z-50' },\n content: {\n class: [\n 'bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg font-inter'\n ]\n }\n }\"\n >\n <div\n class=\"flex min-w-[14rem] flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3 font-inter\"\n >\n <template v-for=\"entry in entries\" :key=\"entry.key\">\n <div v-if=\"entry.kind === 'divider'\" class=\"px-2 py-1\">\n <div class=\"h-px bg-interface-stroke\" />\n </div>\n <Button\n v-else\n class=\"w-full justify-start bg-transparent\"\n variant=\"textonly\"\n size=\"sm\"\n :aria-label=\"entry.label\"\n @click=\"onEntry(entry)\"\n >\n <i\n v-if=\"entry.icon\"\n :class=\"[\n entry.icon,\n 'block size-4 shrink-0 leading-none text-text-secondary'\n ]\"\n />\n <span>{{ entry.label }}</span>\n </Button>\n </template>\n </div>\n </Popover>\n</template>\n\n<script setup lang=\"ts\">\nimport Popover from 'primevue/popover'\nimport { ref } from 'vue'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport type { MenuEntry } from '@/composables/queue/useJobMenu'\n\ndefineProps<{ entries: MenuEntry[] }>()\n\nconst emit = defineEmits<{\n (e: 'action', entry: MenuEntry): void\n}>()\n\nconst jobItemPopoverRef = ref<InstanceType<typeof Popover> | null>(null)\n\nfunction open(event: Event) {\n if (jobItemPopoverRef.value) {\n jobItemPopoverRef.value.toggle(event)\n }\n}\n\nfunction hide() {\n jobItemPopoverRef.value?.hide()\n}\n\nfunction onEntry(entry: MenuEntry) {\n emit('action', entry)\n}\n\ndefineExpose({ open, hide })\n</script>\n","<template>\n <div class=\"flex items-center justify-between gap-2 px-3\">\n <div class=\"min-w-0 flex-1 overflow-x-auto\">\n <div class=\"inline-flex items-center gap-1 whitespace-nowrap\">\n <Button\n v-for=\"tab in visibleJobTabs\"\n :key=\"tab\"\n :variant=\"selectedJobTab === tab ? 'secondary' : 'muted-textonly'\"\n size=\"sm\"\n class=\"px-3\"\n @click=\"$emit('update:selectedJobTab', tab)\"\n >\n {{ tabLabel(tab) }}\n </Button>\n </div>\n </div>\n <div class=\"ml-2 flex shrink-0 items-center gap-2\">\n <Button\n v-if=\"showWorkflowFilter\"\n v-tooltip.top=\"filterTooltipConfig\"\n variant=\"secondary\"\n size=\"icon\"\n :aria-label=\"t('sideToolbar.queueProgressOverlay.filterJobs')\"\n @click=\"onFilterClick\"\n >\n <i class=\"icon-[lucide--list-filter] size-4\" />\n <span\n v-if=\"selectedWorkflowFilter !== 'all'\"\n class=\"pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground\"\n />\n </Button>\n <Popover\n v-if=\"showWorkflowFilter\"\n ref=\"filterPopoverRef\"\n :dismissable=\"true\"\n :close-on-escape=\"true\"\n unstyled\n :pt=\"{\n root: { class: 'absolute z-50' },\n content: {\n class: [\n 'bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg font-inter'\n ]\n }\n }\"\n >\n <div\n class=\"flex min-w-[12rem] flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3\"\n >\n <Button\n class=\"w-full justify-between\"\n variant=\"textonly\"\n size=\"sm\"\n @click=\"selectWorkflowFilter('all')\"\n >\n <span>{{\n t('sideToolbar.queueProgressOverlay.filterAllWorkflows')\n }}</span>\n <i\n v-if=\"selectedWorkflowFilter === 'all'\"\n class=\"icon-[lucide--check] size-4\"\n />\n </Button>\n <div class=\"mx-2 mt-1 h-px\" />\n <Button\n class=\"w-full justify-between\"\n variant=\"textonly\"\n @click=\"selectWorkflowFilter('current')\"\n >\n <span>{{\n t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')\n }}</span>\n <i\n v-if=\"selectedWorkflowFilter === 'current'\"\n class=\"icon-[lucide--check] block size-4 leading-none text-text-secondary\"\n />\n </Button>\n </div>\n </Popover>\n <Button\n v-tooltip.top=\"sortTooltipConfig\"\n variant=\"secondary\"\n size=\"icon\"\n :aria-label=\"t('sideToolbar.queueProgressOverlay.sortJobs')\"\n @click=\"onSortClick\"\n >\n <i class=\"icon-[lucide--arrow-up-down] size-4\" />\n <span\n v-if=\"selectedSortMode !== 'mostRecent'\"\n class=\"pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground\"\n />\n </Button>\n <Popover\n ref=\"sortPopoverRef\"\n :dismissable=\"true\"\n :close-on-escape=\"true\"\n unstyled\n :pt=\"{\n root: { class: 'absolute z-50' },\n content: {\n class: [\n 'bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg font-inter'\n ]\n }\n }\"\n >\n <div\n class=\"flex min-w-[12rem] flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3\"\n >\n <template v-for=\"(mode, index) in jobSortModes\" :key=\"mode\">\n <Button\n class=\"w-full justify-between\"\n variant=\"textonly\"\n size=\"sm\"\n @click=\"selectSortMode(mode)\"\n >\n <span>{{ sortLabel(mode) }}</span>\n <i\n v-if=\"selectedSortMode === mode\"\n class=\"icon-[lucide--check] size-4 text-text-secondary\"\n />\n </Button>\n <div\n v-if=\"index < jobSortModes.length - 1\"\n class=\"mx-2 mt-1 h-px\"\n />\n </template>\n </div>\n </Popover>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport Popover from 'primevue/popover'\nimport { computed, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { jobSortModes, jobTabs } from '@/composables/queue/useJobList'\nimport type { JobSortMode, JobTab } from '@/composables/queue/useJobList'\nimport { buildTooltipConfig } from '@/composables/useTooltipConfig'\nimport { isCloud } from '@/platform/distribution/types'\n\nconst props = defineProps<{\n selectedJobTab: JobTab\n selectedWorkflowFilter: 'all' | 'current'\n selectedSortMode: JobSortMode\n hasFailedJobs: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'update:selectedJobTab', value: JobTab): void\n (e: 'update:selectedWorkflowFilter', value: 'all' | 'current'): void\n (e: 'update:selectedSortMode', value: JobSortMode): void\n}>()\n\nconst { t } = useI18n()\nconst filterPopoverRef = ref<InstanceType<typeof Popover> | null>(null)\nconst sortPopoverRef = ref<InstanceType<typeof Popover> | null>(null)\n\nconst filterTooltipConfig = computed(() =>\n buildTooltipConfig(t('sideToolbar.queueProgressOverlay.filterBy'))\n)\nconst sortTooltipConfig = computed(() =>\n buildTooltipConfig(t('sideToolbar.queueProgressOverlay.sortBy'))\n)\n\n// This can be removed when cloud implements /jobs and we switch to it.\nconst showWorkflowFilter = !isCloud\n\nconst visibleJobTabs = computed(() =>\n props.hasFailedJobs ? jobTabs : jobTabs.filter((tab) => tab !== 'Failed')\n)\n\nconst onFilterClick = (event: Event) => {\n if (filterPopoverRef.value) {\n filterPopoverRef.value.toggle(event)\n }\n}\nconst selectWorkflowFilter = (value: 'all' | 'current') => {\n ;(filterPopoverRef.value as any)?.hide?.()\n emit('update:selectedWorkflowFilter', value)\n}\n\nconst onSortClick = (event: Event) => {\n if (sortPopoverRef.value) {\n sortPopoverRef.value.toggle(event)\n }\n}\n\nconst selectSortMode = (value: JobSortMode) => {\n ;(sortPopoverRef.value as any)?.hide?.()\n emit('update:selectedSortMode', value)\n}\n\nconst tabLabel = (tab: JobTab) => {\n if (tab === 'All') return t('g.all')\n if (tab === 'Completed') return t('g.completed')\n return t('g.failed')\n}\n\nconst sortLabel = (mode: JobSortMode) => {\n if (mode === 'mostRecent') {\n return t('queue.jobList.sortMostRecent')\n }\n if (mode === 'totalGenerationTime') {\n return t('queue.jobList.sortTotalGenerationTime')\n }\n return ''\n}\n</script>\n","import { computed } from 'vue'\nimport type { ComputedRef } from 'vue'\n\nimport type { ExecutionErrorDialogInput } from '@/services/dialogService'\nimport type { TaskItemImpl } from '@/stores/queueStore'\n\ntype CopyHandler = (value: string) => void | Promise<void>\n\nexport type JobErrorDialogService = {\n showExecutionErrorDialog: (executionError: ExecutionErrorDialogInput) => void\n showErrorDialog: (\n error: Error,\n options?: {\n reportType?: string\n [key: string]: unknown\n }\n ) => void\n}\n\ntype UseJobErrorReportingOptions = {\n taskForJob: ComputedRef<TaskItemImpl | null>\n copyToClipboard: CopyHandler\n dialog: JobErrorDialogService\n}\n\nexport const useJobErrorReporting = ({\n taskForJob,\n copyToClipboard,\n dialog\n}: UseJobErrorReportingOptions) => {\n const errorMessageValue = computed(() => taskForJob.value?.errorMessage ?? '')\n\n const copyErrorMessage = () => {\n if (errorMessageValue.value) {\n void copyToClipboard(errorMessageValue.value)\n }\n }\n\n const reportJobError = () => {\n const executionError = taskForJob.value?.executionError\n if (executionError) {\n dialog.showExecutionErrorDialog(executionError)\n return\n }\n\n if (errorMessageValue.value) {\n dialog.showErrorDialog(new Error(errorMessageValue.value), {\n reportType: 'queueJobError'\n })\n }\n }\n\n return {\n errorMessageValue,\n copyErrorMessage,\n reportJobError\n }\n}\n","import { computed } from 'vue'\nimport type { ComputedRef, Ref } from 'vue'\n\nimport type { useExecutionStore } from '@/stores/executionStore'\nimport type { TaskItemImpl, useQueueStore } from '@/stores/queueStore'\nimport type { JobState } from '@/types/queue'\n\ntype QueueStore = ReturnType<typeof useQueueStore>\ntype ExecutionStore = ReturnType<typeof useExecutionStore>\n\nexport type UseQueueEstimatesOptions = {\n queueStore: QueueStore\n executionStore: ExecutionStore\n taskForJob: ComputedRef<TaskItemImpl | null>\n jobState: ComputedRef<JobState | null>\n firstSeenTs: ComputedRef<number | undefined>\n jobsAhead: ComputedRef<number | null>\n nowTs: Ref<number>\n}\n\ntype EstimateRange = [number, number]\n\nexport const formatElapsedTime = (ms: number): string => {\n const totalSec = Math.max(0, Math.floor(ms / 1000))\n const minutes = Math.floor(totalSec / 60)\n const seconds = totalSec % 60\n return `${minutes}m ${seconds}s`\n}\n\nconst pickRecentDurations = (queueStore: QueueStore) =>\n queueStore.historyTasks\n .map((task: TaskItemImpl) => Number(task.executionTimeInSeconds))\n .filter(\n (value: number | undefined) =>\n typeof value === 'number' && !Number.isNaN(value)\n ) as number[]\n\nexport const useQueueEstimates = ({\n queueStore,\n executionStore,\n taskForJob,\n jobState,\n firstSeenTs,\n jobsAhead,\n nowTs\n}: UseQueueEstimatesOptions) => {\n const runningWorkflowCount = computed(\n () => executionStore.runningWorkflowCount\n )\n\n const showParallelQueuedStats = computed(\n () =>\n jobState.value === 'pending' &&\n !!firstSeenTs.value &&\n (runningWorkflowCount.value ?? 0) > 1\n )\n\n const recentDurations = computed<number[]>(() =>\n pickRecentDurations(queueStore).slice(-20)\n )\n\n const runningRemainingRangeSeconds = computed<EstimateRange | null>(() => {\n const durations = recentDurations.value\n if (!durations.length) return null\n const sorted = durations.slice().sort((a, b) => a - b)\n const avg = sorted.reduce((sum, value) => sum + value, 0) / sorted.length\n const p75 =\n sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.75))]\n const running = queueStore.runningTasks as TaskItemImpl[]\n const now = nowTs.value\n const remaining = running\n .map((task) => task.executionStartTimestamp)\n .filter((timestamp): timestamp is number => typeof timestamp === 'number')\n .map((startTs) => {\n const elapsed = Math.max(0, Math.floor((now - startTs) / 1000))\n return {\n lo: Math.max(0, Math.round(avg - elapsed)),\n hi: Math.max(0, Math.round(p75 - elapsed))\n }\n })\n if (!remaining.length) return null\n const minLo = remaining.reduce(\n (min, range) => Math.min(min, range.lo),\n Infinity\n )\n const minHi = remaining.reduce(\n (min, range) => Math.min(min, range.hi),\n Infinity\n )\n return [minLo, minHi]\n })\n\n const estimateRangeSeconds = computed<EstimateRange | null>(() => {\n const durations = recentDurations.value\n if (!durations.length) return null\n const ahead = jobsAhead.value\n if (ahead == null) return null\n const sorted = durations.slice().sort((a, b) => a - b)\n const avg = sorted.reduce((sum, value) => sum + value, 0) / sorted.length\n const p75 =\n sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.75))]\n if (ahead <= 0) {\n return runningRemainingRangeSeconds.value ?? [0, 0]\n }\n const runningCount = Math.max(1, runningWorkflowCount.value || 1)\n const batches = Math.ceil(ahead / runningCount)\n return [Math.round(avg * batches), Math.round(p75 * batches)]\n })\n\n const estimateRemainingRangeSeconds = computed<EstimateRange | null>(() => {\n const durations = recentDurations.value\n if (!durations.length) return null\n const sorted = durations.slice().sort((a, b) => a - b)\n const avg = sorted.reduce((sum, value) => sum + value, 0) / sorted.length\n const p75 =\n sorted[Math.min(sorted.length - 1, Math.floor(sorted.length * 0.75))]\n const task = taskForJob.value as TaskItemImpl & {\n executionStartTimestamp?: number\n }\n const execStart =\n jobState.value === 'running' ? task?.executionStartTimestamp : undefined\n const baseTs = execStart ?? firstSeenTs.value\n const elapsed = baseTs\n ? Math.max(0, Math.floor((nowTs.value - baseTs) / 1000))\n : 0\n const lo = Math.max(0, Math.round(avg - elapsed))\n const hi = Math.max(0, Math.round(p75 - elapsed))\n return [lo, hi]\n })\n\n const timeElapsedValue = computed(() => {\n const task = taskForJob.value as TaskItemImpl & {\n executionStartTimestamp?: number\n }\n const execStart =\n jobState.value === 'running' ? task?.executionStartTimestamp : undefined\n const baseTs = execStart ?? firstSeenTs.value\n if (!baseTs) return ''\n return formatElapsedTime(nowTs.value - baseTs)\n })\n\n return {\n runningWorkflowCount,\n showParallelQueuedStats,\n estimateRangeSeconds,\n estimateRemainingRangeSeconds,\n timeElapsedValue\n }\n}\n","<template>\n <div\n class=\"w-[300px] min-w-[260px] rounded-lg border border-interface-stroke bg-interface-panel-surface shadow-md\"\n >\n <div class=\"flex items-center border-b border-interface-stroke p-4\">\n <span\n class=\"text-[0.875rem] leading-normal font-normal text-text-primary\"\n >{{ t('queue.jobDetails.header') }}</span\n >\n </div>\n <div class=\"flex flex-col gap-6 px-4 pt-4 pb-4\">\n <div class=\"grid grid-cols-2 items-center gap-x-2 gap-y-2\">\n <template v-for=\"row in baseRows\" :key=\"row.label\">\n <div\n class=\"flex items-center text-[0.75rem] leading-normal font-normal text-text-primary\"\n >\n {{ row.label }}\n </div>\n <div\n class=\"flex min-w-0 items-center text-[0.75rem] leading-normal font-normal text-text-secondary\"\n >\n <span class=\"block min-w-0 truncate\">{{ row.value }}</span>\n <Button\n v-if=\"row.canCopy\"\n size=\"icon\"\n variant=\"muted-textonly\"\n :aria-label=\"copyAriaLabel\"\n @click.stop=\"copyJobId\"\n >\n <i class=\"icon-[lucide--copy] size-4\" />\n </Button>\n </div>\n </template>\n </div>\n\n <div\n v-if=\"extraRows.length\"\n class=\"grid grid-cols-2 items-center gap-x-2 gap-y-2\"\n >\n <template v-for=\"row in extraRows\" :key=\"row.label\">\n <div\n class=\"flex items-center text-[0.75rem] leading-normal font-normal text-text-primary\"\n >\n {{ row.label }}\n </div>\n <div\n class=\"flex min-w-0 items-center text-[0.75rem] leading-normal font-normal text-text-secondary\"\n >\n <span class=\"block min-w-0 truncate\">{{ row.value }}</span>\n </div>\n </template>\n </div>\n\n <div v-if=\"jobState === 'failed'\" class=\"grid grid-cols-2 gap-x-2\">\n <div\n class=\"flex items-center text-[0.75rem] leading-normal font-normal text-text-primary\"\n >\n {{ t('queue.jobDetails.errorMessage') }}\n </div>\n <div class=\"flex items-center justify-between gap-4\">\n <Button\n class=\"justify-start px-0\"\n variant=\"muted-textonly\"\n size=\"sm\"\n icon-position=\"right\"\n @click.stop=\"copyErrorMessage\"\n >\n <span>{{ copyAriaLabel }}</span>\n <i class=\"icon-[lucide--copy] block size-3.5 leading-none\" />\n </Button>\n <Button\n class=\"justify-start px-0\"\n variant=\"muted-textonly\"\n size=\"sm\"\n icon-position=\"right\"\n @click.stop=\"reportJobError\"\n >\n <span>{{ t('queue.jobDetails.report') }}</span>\n <i\n class=\"icon-[lucide--message-circle-warning] block size-3.5 leading-none\"\n />\n </Button>\n </div>\n <div\n class=\"col-span-2 mt-2 rounded bg-interface-panel-hover-surface px-4 py-2 text-[0.75rem] leading-normal text-text-secondary\"\n >\n {{ errorMessageValue }}\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onMounted, onUnmounted, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { useCopyToClipboard } from '@/composables/useCopyToClipboard'\nimport { isCloud } from '@/platform/distribution/types'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { useDialogService } from '@/services/dialogService'\nimport { useExecutionStore } from '@/stores/executionStore'\nimport { useQueueStore } from '@/stores/queueStore'\nimport type { TaskItemImpl } from '@/stores/queueStore'\nimport { formatClockTime } from '@/utils/dateTimeUtil'\nimport { jobStateFromTask } from '@/utils/queueUtil'\n\nimport { useJobErrorReporting } from './useJobErrorReporting'\nimport { formatElapsedTime, useQueueEstimates } from './useQueueEstimates'\n\nconst props = defineProps<{\n jobId: string\n workflowId?: string\n}>()\n\nconst copyAriaLabel = computed(() => t('g.copy'))\n\nconst workflowStore = useWorkflowStore()\nconst queueStore = useQueueStore()\nconst executionStore = useExecutionStore()\nconst dialog = useDialogService()\nconst { locale, t } = useI18n()\n\nconst workflowValue = computed(() => {\n const wid = props.workflowId\n if (!wid) return ''\n const activeId = workflowStore.activeWorkflow?.activeState?.id\n if (activeId && activeId === wid) {\n return workflowStore.activeWorkflow?.filename ?? wid\n }\n return wid\n})\nconst jobIdValue = computed(() => props.jobId)\n\nconst { copyToClipboard } = useCopyToClipboard()\nconst copyJobId = () => void copyToClipboard(jobIdValue.value)\n\nconst taskForJob = computed(() => {\n const pid = props.jobId\n const findIn = (arr: TaskItemImpl[]) =>\n arr.find((t) => String(t.promptId ?? '') === String(pid))\n return (\n findIn(queueStore.pendingTasks) ||\n findIn(queueStore.runningTasks) ||\n findIn(queueStore.historyTasks) ||\n null\n )\n})\n\nconst jobState = computed(() => {\n const task = taskForJob.value\n if (!task) return null\n const isInitializing = executionStore.isPromptInitializing(\n String(task?.promptId)\n )\n return jobStateFromTask(task, isInitializing)\n})\n\nconst firstSeenTs = computed<number | undefined>(() => {\n const task = taskForJob.value\n return task?.createTime\n})\n\nconst queuedAtValue = computed(() =>\n firstSeenTs.value !== undefined\n ? formatClockTime(firstSeenTs.value, locale.value)\n : ''\n)\n\nconst currentQueueIndex = computed<number | null>(() => {\n const task = taskForJob.value\n return task ? Number(task.queueIndex) : null\n})\n\nconst jobsAhead = computed<number | null>(() => {\n const idx = currentQueueIndex.value\n if (idx == null) return null\n const ahead = queueStore.pendingTasks.filter(\n (t: TaskItemImpl) => Number(t.queueIndex) < idx\n )\n return ahead.length\n})\n\nconst queuePositionValue = computed(() => {\n if (jobsAhead.value == null) return ''\n const n = jobsAhead.value\n return t('queue.jobDetails.queuePositionValue', { count: n }, n)\n})\n\nconst nowTs = ref<number>(Date.now())\nlet timer: number | null = null\nonMounted(() => {\n timer = window.setInterval(() => {\n nowTs.value = Date.now()\n }, 1000)\n})\nonUnmounted(() => {\n if (timer != null) {\n clearInterval(timer)\n timer = null\n }\n})\n\nconst {\n showParallelQueuedStats,\n estimateRangeSeconds,\n estimateRemainingRangeSeconds,\n timeElapsedValue\n} = useQueueEstimates({\n queueStore,\n executionStore,\n taskForJob,\n jobState,\n firstSeenTs,\n jobsAhead,\n nowTs\n})\n\nconst formatEta = (lo: number, hi: number): string => {\n if (hi <= 60) {\n const hiS = Math.max(1, Math.round(hi))\n const loS = Math.max(1, Math.min(hiS, Math.round(lo)))\n if (loS === hiS)\n return t('queue.jobDetails.eta.seconds', { count: hiS }, hiS)\n return t('queue.jobDetails.eta.secondsRange', { lo: loS, hi: hiS })\n }\n if (lo >= 60 && hi < 90) {\n return t('queue.jobDetails.eta.minutes', { count: 1 }, 1)\n }\n const loM = Math.max(1, Math.floor(lo / 60))\n const hiM = Math.max(loM, Math.ceil(hi / 60))\n if (loM === hiM) {\n return t('queue.jobDetails.eta.minutes', { count: loM }, loM)\n }\n return t('queue.jobDetails.eta.minutesRange', { lo: loM, hi: hiM })\n}\n\nconst estimatedStartInValue = computed(() => {\n const range = estimateRangeSeconds.value\n if (!range) return ''\n const [lo, hi] = range\n return formatEta(lo, hi)\n})\n\nconst estimatedFinishInValue = computed(() => {\n const range = estimateRemainingRangeSeconds.value\n if (!range) return ''\n const [lo, hi] = range\n return formatEta(lo, hi)\n})\n\ntype DetailRow = { label: string; value: string; canCopy?: boolean }\n\nconst baseRows = computed<DetailRow[]>(() => [\n { label: t('queue.jobDetails.workflow'), value: workflowValue.value },\n { label: t('queue.jobDetails.jobId'), value: jobIdValue.value, canCopy: true }\n])\n\nconst extraRows = computed<DetailRow[]>(() => {\n if (jobState.value === 'pending') {\n if (!firstSeenTs.value) return []\n const rows: DetailRow[] = [\n { label: t('queue.jobDetails.queuedAt'), value: queuedAtValue.value }\n ]\n if (showParallelQueuedStats.value) {\n rows.push(\n {\n label: t('queue.jobDetails.queuePosition'),\n value: queuePositionValue.value\n },\n {\n label: t('queue.jobDetails.timeElapsed'),\n value: timeElapsedValue.value\n },\n {\n label: t('queue.jobDetails.estimatedStartIn'),\n value: estimatedStartInValue.value\n }\n )\n }\n return rows\n }\n if (jobState.value === 'running') {\n if (!firstSeenTs.value) return []\n return [\n { label: t('queue.jobDetails.queuedAt'), value: queuedAtValue.value },\n {\n label: t('queue.jobDetails.timeElapsed'),\n value: timeElapsedValue.value\n },\n {\n label: t('queue.jobDetails.estimatedFinishIn'),\n value: estimatedFinishInValue.value\n }\n ]\n }\n if (jobState.value === 'completed') {\n const task = taskForJob.value as any\n const endTs: number | undefined = task?.executionEndTimestamp\n const execMs: number | undefined = task?.executionTime\n const generatedOnValue = endTs ? formatClockTime(endTs, locale.value) : ''\n const totalGenTimeValue =\n execMs !== undefined ? formatElapsedTime(execMs) : ''\n const computeHoursValue =\n execMs !== undefined ? (execMs / 3600000).toFixed(3) + ' hours' : ''\n\n const rows: DetailRow[] = [\n { label: t('queue.jobDetails.generatedOn'), value: generatedOnValue },\n {\n label: t('queue.jobDetails.totalGenerationTime'),\n value: totalGenTimeValue\n }\n ]\n if (isCloud) {\n rows.push({\n label: t('queue.jobDetails.computeHoursUsed'),\n value: computeHoursValue\n })\n }\n return rows\n }\n if (jobState.value === 'failed') {\n const task = taskForJob.value as any\n const execMs: number | undefined = task?.executionTime\n const failedAfterValue =\n execMs !== undefined ? formatElapsedTime(execMs) : ''\n const computeHoursValue =\n execMs !== undefined ? (execMs / 3600000).toFixed(3) + ' hours' : ''\n const rows: DetailRow[] = [\n { label: t('queue.jobDetails.queuedAt'), value: queuedAtValue.value },\n { label: t('queue.jobDetails.failedAfter'), value: failedAfterValue }\n ]\n if (isCloud) {\n rows.push({\n label: t('queue.jobDetails.computeHoursUsed'),\n value: computeHoursValue\n })\n }\n return rows\n }\n return []\n})\n\nconst { errorMessageValue, copyErrorMessage, reportJobError } =\n useJobErrorReporting({\n taskForJob,\n copyToClipboard,\n dialog\n })\n</script>\n","<template>\n <div class=\"w-[300px] min-w-[260px] rounded-lg shadow-md\">\n <div class=\"p-3\">\n <div class=\"relative aspect-square w-full overflow-hidden rounded-lg\">\n <img\n ref=\"imgRef\"\n :src=\"imageUrl\"\n :alt=\"name\"\n class=\"h-full w-full cursor-pointer object-contain\"\n @click=\"$emit('image-click')\"\n @load=\"onImgLoad\"\n />\n <div\n v-if=\"timeLabel\"\n class=\"absolute bottom-2 left-2 rounded px-2 py-0.5 text-xs text-text-primary\"\n :style=\"{\n background: 'rgba(217, 217, 217, 0.40)',\n backdropFilter: 'blur(2px)'\n }\"\n >\n {{ timeLabel }}\n </div>\n </div>\n <div class=\"mt-2 text-center\">\n <div\n class=\"truncate text-[0.875rem] leading-normal font-semibold text-text-primary\"\n :title=\"name\"\n >\n {{ name }}\n </div>\n <div\n v-if=\"width && height\"\n class=\"mt-1 text-[0.75rem] leading-normal text-text-secondary\"\n >\n {{ width }}x{{ height }}\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\n\ndefineOptions({ inheritAttrs: false })\n\ndefineProps<{\n imageUrl: string\n name: string\n timeLabel?: string\n}>()\n\ndefineEmits(['image-click'])\n\nconst imgRef = ref<HTMLImageElement | null>(null)\nconst width = ref<number | null>(null)\nconst height = ref<number | null>(null)\n\nconst onImgLoad = () => {\n const el = imgRef.value\n if (!el) return\n width.value = el.naturalWidth || null\n height.value = el.naturalHeight || null\n}\n</script>\n","<template>\n <div\n ref=\"rowRef\"\n class=\"relative\"\n @mouseenter=\"onRowEnter\"\n @mouseleave=\"onRowLeave\"\n @contextmenu.stop.prevent=\"onContextMenu\"\n >\n <Teleport to=\"body\">\n <div\n v-if=\"!isPreviewVisible && showDetails && popoverPosition\"\n class=\"fixed z-50\"\n :style=\"{\n top: `${popoverPosition.top}px`,\n right: `${popoverPosition.right}px`\n }\"\n @mouseenter=\"onPopoverEnter\"\n @mouseleave=\"onPopoverLeave\"\n >\n <JobDetailsPopover\n :job-id=\"props.jobId\"\n :workflow-id=\"props.workflowId\"\n />\n </div>\n </Teleport>\n <Teleport to=\"body\">\n <div\n v-if=\"isPreviewVisible && canShowPreview && popoverPosition\"\n class=\"fixed z-50\"\n :style=\"{\n top: `${popoverPosition.top}px`,\n right: `${popoverPosition.right}px`\n }\"\n @mouseenter=\"onPreviewEnter\"\n @mouseleave=\"onPreviewLeave\"\n >\n <QueueAssetPreview\n :image-url=\"iconImageUrl!\"\n :name=\"props.title\"\n :time-label=\"rightText || undefined\"\n @image-click=\"emit('view')\"\n />\n </div>\n </Teleport>\n <div\n class=\"relative flex items-center justify-between gap-2 overflow-hidden rounded-lg border border-secondary-background bg-secondary-background p-1 text-[12px] text-text-primary transition-colors duration-150 ease-in-out hover:border-secondary-background-hover hover:bg-secondary-background-hover\"\n @mouseenter=\"isHovered = true\"\n @mouseleave=\"isHovered = false\"\n >\n <div\n v-if=\"\n props.state === 'running' &&\n hasAnyProgressPercent(\n props.progressTotalPercent,\n props.progressCurrentPercent\n )\n \"\n :class=\"progressBarContainerClass\"\n >\n <div\n v-if=\"hasProgressPercent(props.progressTotalPercent)\"\n :class=\"progressBarPrimaryClass\"\n :style=\"progressPercentStyle(props.progressTotalPercent)\"\n />\n <div\n v-if=\"hasProgressPercent(props.progressCurrentPercent)\"\n :class=\"progressBarSecondaryClass\"\n :style=\"progressPercentStyle(props.progressCurrentPercent)\"\n />\n </div>\n\n <div class=\"relative z-1 flex items-center gap-1\">\n <div class=\"relative inline-flex items-center justify-center\">\n <div\n class=\"absolute left-1/2 top-1/2 size-10 -translate-x-1/2 -translate-y-1/2\"\n @mouseenter.stop=\"onIconEnter\"\n @mouseleave.stop=\"onIconLeave\"\n />\n <div\n class=\"inline-flex h-6 w-6 items-center justify-center overflow-hidden rounded-[6px]\"\n >\n <img\n v-if=\"iconImageUrl\"\n :src=\"iconImageUrl\"\n class=\"h-full w-full object-cover\"\n />\n <i\n v-else\n :class=\"cn(iconClass, 'size-4', shouldSpin && 'animate-spin')\"\n />\n </div>\n </div>\n </div>\n\n <div class=\"relative z-1 min-w-0 flex-1\">\n <div class=\"truncate opacity-90\" :title=\"props.title\">\n <slot name=\"primary\">{{ props.title }}</slot>\n </div>\n </div>\n\n <!--\n TODO: Refactor action buttons to use a declarative config system.\n\n Instead of hardcoding button visibility logic in the template, define an array of\n action button configs with properties like:\n - icon, label, action, tooltip\n - visibleStates: JobState[] (which job states show this button)\n - alwaysVisible: boolean (show without hover)\n - destructive: boolean (use destructive styling)\n\n Then render buttons in two groups:\n 1. Always-visible buttons (outside Transition)\n 2. Hover-only buttons (inside Transition)\n\n This would eliminate the current duplication where the cancel button exists\n both outside (for running) and inside (for pending) the Transition.\n -->\n <div class=\"relative z-1 flex items-center gap-2 text-text-secondary\">\n <Transition\n mode=\"out-in\"\n enter-active-class=\"transition-opacity transition-transform duration-150 ease-out\"\n leave-active-class=\"transition-opacity transition-transform duration-150 ease-in\"\n enter-from-class=\"opacity-0 translate-y-0.5\"\n enter-to-class=\"opacity-100 translate-y-0\"\n leave-from-class=\"opacity-100 translate-y-0\"\n leave-to-class=\"opacity-0 translate-y-0.5\"\n >\n <div\n v-if=\"isHovered\"\n key=\"actions\"\n class=\"inline-flex items-center gap-2 pr-1\"\n >\n <Button\n v-if=\"props.state === 'failed' && computedShowClear\"\n v-tooltip.top=\"deleteTooltipConfig\"\n variant=\"destructive\"\n size=\"icon\"\n :aria-label=\"t('g.delete')\"\n @click.stop=\"onDeleteClick\"\n >\n <i class=\"icon-[lucide--trash-2] size-4\" />\n </Button>\n <Button\n v-else-if=\"\n props.state !== 'completed' &&\n props.state !== 'running' &&\n computedShowClear\n \"\n v-tooltip.top=\"cancelTooltipConfig\"\n variant=\"destructive\"\n size=\"icon\"\n :aria-label=\"t('g.cancel')\"\n @click.stop=\"onCancelClick\"\n >\n <i class=\"icon-[lucide--x] size-4\" />\n </Button>\n <Button\n v-else-if=\"props.state === 'completed'\"\n variant=\"textonly\"\n size=\"sm\"\n @click.stop=\"emit('view')\"\n >{{ t('menuLabels.View') }}</Button\n >\n <Button\n v-if=\"props.showMenu !== undefined ? props.showMenu : true\"\n v-tooltip.top=\"moreTooltipConfig\"\n variant=\"textonly\"\n size=\"icon-sm\"\n :aria-label=\"t('g.more')\"\n @click.stop=\"emit('menu', $event)\"\n >\n <i class=\"icon-[lucide--more-horizontal] size-4\" />\n </Button>\n </div>\n <div\n v-else-if=\"props.state !== 'running'\"\n key=\"secondary\"\n class=\"pr-2\"\n >\n <slot name=\"secondary\">{{ props.rightText }}</slot>\n </div>\n </Transition>\n <!-- Running job cancel button - always visible -->\n <Button\n v-if=\"props.state === 'running' && computedShowClear\"\n v-tooltip.top=\"cancelTooltipConfig\"\n variant=\"destructive\"\n size=\"icon\"\n :aria-label=\"t('g.cancel')\"\n @click.stop=\"onCancelClick\"\n >\n <i class=\"icon-[lucide--x] size-4\" />\n </Button>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, ref, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport JobDetailsPopover from '@/components/queue/job/JobDetailsPopover.vue'\nimport QueueAssetPreview from '@/components/queue/job/QueueAssetPreview.vue'\nimport Button from '@/components/ui/button/Button.vue'\nimport { useProgressBarBackground } from '@/composables/useProgressBarBackground'\nimport { buildTooltipConfig } from '@/composables/useTooltipConfig'\nimport type { JobState } from '@/types/queue'\nimport { iconForJobState } from '@/utils/queueDisplay'\nimport { cn } from '@/utils/tailwindUtil'\n\nconst props = withDefaults(\n defineProps<{\n jobId: string\n workflowId?: string\n state: JobState\n title: string\n rightText?: string\n iconName?: string\n iconImageUrl?: string\n showClear?: boolean\n showMenu?: boolean\n progressTotalPercent?: number\n progressCurrentPercent?: number\n activeDetailsId?: string | null\n }>(),\n {\n workflowId: undefined,\n rightText: '',\n iconName: undefined,\n iconImageUrl: undefined,\n showClear: undefined,\n showMenu: undefined,\n progressTotalPercent: undefined,\n progressCurrentPercent: undefined,\n runningNodeName: undefined,\n activeDetailsId: null\n }\n)\n\nconst emit = defineEmits<{\n (e: 'cancel'): void\n (e: 'delete'): void\n (e: 'menu', event: MouseEvent): void\n (e: 'view'): void\n (e: 'details-enter', jobId: string): void\n (e: 'details-leave', jobId: string): void\n}>()\n\nconst { t } = useI18n()\nconst {\n progressBarContainerClass,\n progressBarPrimaryClass,\n progressBarSecondaryClass,\n hasProgressPercent,\n hasAnyProgressPercent,\n progressPercentStyle\n} = useProgressBarBackground()\n\nconst cancelTooltipConfig = computed(() => buildTooltipConfig(t('g.cancel')))\nconst deleteTooltipConfig = computed(() => buildTooltipConfig(t('g.delete')))\nconst moreTooltipConfig = computed(() => buildTooltipConfig(t('g.more')))\n\nconst rowRef = ref<HTMLDivElement | null>(null)\nconst showDetails = computed(() => props.activeDetailsId === props.jobId)\n\nconst onRowEnter = () => {\n if (!isPreviewVisible.value) emit('details-enter', props.jobId)\n}\nconst onRowLeave = () => emit('details-leave', props.jobId)\nconst onPopoverEnter = () => emit('details-enter', props.jobId)\nconst onPopoverLeave = () => emit('details-leave', props.jobId)\n\nconst isPreviewVisible = ref(false)\nconst previewHideTimer = ref<number | null>(null)\nconst previewShowTimer = ref<number | null>(null)\nconst clearPreviewHideTimer = () => {\n if (previewHideTimer.value !== null) {\n clearTimeout(previewHideTimer.value)\n previewHideTimer.value = null\n }\n}\nconst clearPreviewShowTimer = () => {\n if (previewShowTimer.value !== null) {\n clearTimeout(previewShowTimer.value)\n previewShowTimer.value = null\n }\n}\nconst canShowPreview = computed(\n () => props.state === 'completed' && !!props.iconImageUrl\n)\nconst scheduleShowPreview = () => {\n if (!canShowPreview.value) return\n clearPreviewHideTimer()\n clearPreviewShowTimer()\n previewShowTimer.value = window.setTimeout(() => {\n isPreviewVisible.value = true\n previewShowTimer.value = null\n }, 200)\n}\nconst scheduleHidePreview = () => {\n clearPreviewHideTimer()\n clearPreviewShowTimer()\n previewHideTimer.value = window.setTimeout(() => {\n isPreviewVisible.value = false\n previewHideTimer.value = null\n }, 150)\n}\nconst onIconEnter = () => scheduleShowPreview()\nconst onIconLeave = () => scheduleHidePreview()\nconst onPreviewEnter = () => scheduleShowPreview()\nconst onPreviewLeave = () => scheduleHidePreview()\n\nconst popoverPosition = ref<{ top: number; right: number } | null>(null)\n\nconst updatePopoverPosition = () => {\n const el = rowRef.value\n if (!el) return\n const rect = el.getBoundingClientRect()\n const gap = 8\n popoverPosition.value = {\n top: rect.top,\n right: window.innerWidth - rect.left + gap\n }\n}\n\nconst isAnyPopoverVisible = computed(\n () => showDetails.value || (isPreviewVisible.value && canShowPreview.value)\n)\n\nwatch(\n isAnyPopoverVisible,\n (visible) => {\n if (visible) {\n nextTick(updatePopoverPosition)\n } else {\n popoverPosition.value = null\n }\n },\n { immediate: false }\n)\n\nconst isHovered = ref(false)\n\nconst iconClass = computed(() => {\n if (props.iconName) return props.iconName\n return iconForJobState(props.state)\n})\n\nconst shouldSpin = computed(\n () =>\n props.state === 'pending' &&\n iconClass.value === iconForJobState('pending') &&\n !props.iconImageUrl\n)\n\nconst computedShowClear = computed(() => {\n if (props.showClear !== undefined) return props.showClear\n return props.state !== 'completed'\n})\n\nconst emitDetailsLeave = () => emit('details-leave', props.jobId)\n\nconst onCancelClick = () => {\n emitDetailsLeave()\n emit('cancel')\n}\n\nconst onDeleteClick = () => {\n emitDetailsLeave()\n emit('delete')\n}\n\nconst onContextMenu = (event: MouseEvent) => {\n const shouldShowMenu = props.showMenu !== undefined ? props.showMenu : true\n if (shouldShowMenu) emit('menu', event)\n}\n</script>\n","<template>\n <div class=\"flex flex-col gap-4 px-3 pb-4\">\n <div\n v-for=\"group in displayedJobGroups\"\n :key=\"group.key\"\n class=\"flex flex-col gap-2\"\n >\n <div class=\"text-[12px] leading-none text-text-secondary\">\n {{ group.label }}\n </div>\n <QueueJobItem\n v-for=\"ji in group.items\"\n :key=\"ji.id\"\n :job-id=\"ji.id\"\n :workflow-id=\"ji.taskRef?.workflowId\"\n :state=\"ji.state\"\n :title=\"ji.title\"\n :right-text=\"ji.meta\"\n :icon-name=\"ji.iconName\"\n :icon-image-url=\"ji.iconImageUrl\"\n :show-clear=\"ji.showClear\"\n :show-menu=\"true\"\n :progress-total-percent=\"ji.progressTotalPercent\"\n :progress-current-percent=\"ji.progressCurrentPercent\"\n :running-node-name=\"ji.runningNodeName\"\n :active-details-id=\"activeDetailsId\"\n @cancel=\"emitCancelItem(ji)\"\n @delete=\"emitDeleteItem(ji)\"\n @menu=\"(ev) => $emit('menu', ji, ev)\"\n @view=\"$emit('viewItem', ji)\"\n @details-enter=\"onDetailsEnter\"\n @details-leave=\"onDetailsLeave\"\n />\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { onBeforeUnmount, ref, watch } from 'vue'\n\nimport QueueJobItem from '@/components/queue/job/QueueJobItem.vue'\nimport type { JobGroup, JobListItem } from '@/composables/queue/useJobList'\n\nconst props = defineProps<{ displayedJobGroups: JobGroup[] }>()\n\nconst emit = defineEmits<{\n (e: 'cancelItem', item: JobListItem): void\n (e: 'deleteItem', item: JobListItem): void\n (e: 'menu', item: JobListItem, ev: MouseEvent): void\n (e: 'viewItem', item: JobListItem): void\n}>()\n\nconst emitCancelItem = (item: JobListItem) => {\n emit('cancelItem', item)\n}\n\nconst emitDeleteItem = (item: JobListItem) => {\n emit('deleteItem', item)\n}\n\nconst activeDetailsId = ref<string | null>(null)\nconst hideTimer = ref<number | null>(null)\nconst showTimer = ref<number | null>(null)\nconst clearHideTimer = () => {\n if (hideTimer.value !== null) {\n clearTimeout(hideTimer.value)\n hideTimer.value = null\n }\n}\nconst clearShowTimer = () => {\n if (showTimer.value !== null) {\n clearTimeout(showTimer.value)\n showTimer.value = null\n }\n}\nconst onDetailsEnter = (jobId: string) => {\n clearHideTimer()\n clearShowTimer()\n showTimer.value = window.setTimeout(() => {\n activeDetailsId.value = jobId\n showTimer.value = null\n }, 200)\n}\nconst onDetailsLeave = (jobId: string) => {\n clearHideTimer()\n clearShowTimer()\n hideTimer.value = window.setTimeout(() => {\n if (activeDetailsId.value === jobId) activeDetailsId.value = null\n hideTimer.value = null\n }, 150)\n}\n\nconst resetActiveDetails = () => {\n clearHideTimer()\n clearShowTimer()\n activeDetailsId.value = null\n}\n\nwatch(\n () => props.displayedJobGroups,\n (groups) => {\n const activeId = activeDetailsId.value\n if (!activeId) return\n\n const hasActiveJob = groups.some((group) =>\n group.items.some((item) => item.id === activeId)\n )\n\n if (!hasActiveJob) resetActiveDetails()\n }\n)\n\nonBeforeUnmount(resetActiveDetails)\n</script>\n","<template>\n <div class=\"flex w-full flex-col gap-4\">\n <QueueOverlayHeader\n :header-title=\"headerTitle\"\n :show-concurrent-indicator=\"showConcurrentIndicator\"\n :concurrent-workflow-count=\"concurrentWorkflowCount\"\n @clear-history=\"$emit('clearHistory')\"\n />\n\n <div class=\"flex items-center justify-between px-3\">\n <Button\n class=\"grow gap-1 justify-center\"\n variant=\"secondary\"\n size=\"sm\"\n @click=\"$emit('showAssets')\"\n >\n <i class=\"icon-[comfy--image-ai-edit] size-4\" />\n <span>{{ t('sideToolbar.queueProgressOverlay.showAssets') }}</span>\n </Button>\n <div class=\"ml-4 inline-flex items-center\">\n <div\n class=\"inline-flex h-6 items-center text-[12px] leading-none text-text-primary opacity-90\"\n >\n <span class=\"font-bold\">{{ queuedCount }}</span>\n <span class=\"ml-1\">{{\n t('sideToolbar.queueProgressOverlay.queuedSuffix')\n }}</span>\n </div>\n <Button\n v-if=\"queuedCount > 0\"\n class=\"ml-2\"\n variant=\"destructive\"\n size=\"icon\"\n :aria-label=\"t('sideToolbar.queueProgressOverlay.clearQueued')\"\n @click=\"$emit('clearQueued')\"\n >\n <i class=\"icon-[lucide--list-x] size-4\" />\n </Button>\n </div>\n </div>\n\n <JobFiltersBar\n :selected-job-tab=\"selectedJobTab\"\n :selected-workflow-filter=\"selectedWorkflowFilter\"\n :selected-sort-mode=\"selectedSortMode\"\n :has-failed-jobs=\"hasFailedJobs\"\n @update:selected-job-tab=\"$emit('update:selectedJobTab', $event)\"\n @update:selected-workflow-filter=\"\n $emit('update:selectedWorkflowFilter', $event)\n \"\n @update:selected-sort-mode=\"$emit('update:selectedSortMode', $event)\"\n />\n\n <div class=\"flex-1 min-h-0 overflow-y-auto\">\n <JobGroupsList\n :displayed-job-groups=\"displayedJobGroups\"\n @cancel-item=\"onCancelItemEvent\"\n @delete-item=\"onDeleteItemEvent\"\n @view-item=\"$emit('viewItem', $event)\"\n @menu=\"onMenuItem\"\n />\n </div>\n\n <JobContextMenu\n ref=\"jobContextMenuRef\"\n :entries=\"jobMenuEntries\"\n @action=\"onJobMenuAction\"\n />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport type {\n JobGroup,\n JobListItem,\n JobSortMode,\n JobTab\n} from '@/composables/queue/useJobList'\nimport type { MenuEntry } from '@/composables/queue/useJobMenu'\nimport { useJobMenu } from '@/composables/queue/useJobMenu'\n\nimport QueueOverlayHeader from './QueueOverlayHeader.vue'\nimport JobContextMenu from './job/JobContextMenu.vue'\nimport JobFiltersBar from './job/JobFiltersBar.vue'\nimport JobGroupsList from './job/JobGroupsList.vue'\n\ndefineProps<{\n headerTitle: string\n showConcurrentIndicator: boolean\n concurrentWorkflowCount: number\n queuedCount: number\n selectedJobTab: JobTab\n selectedWorkflowFilter: 'all' | 'current'\n selectedSortMode: JobSortMode\n displayedJobGroups: JobGroup[]\n hasFailedJobs: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'showAssets'): void\n (e: 'clearHistory'): void\n (e: 'clearQueued'): void\n (e: 'update:selectedJobTab', value: JobTab): void\n (e: 'update:selectedWorkflowFilter', value: 'all' | 'current'): void\n (e: 'update:selectedSortMode', value: JobSortMode): void\n (e: 'cancelItem', item: JobListItem): void\n (e: 'deleteItem', item: JobListItem): void\n (e: 'viewItem', item: JobListItem): void\n}>()\n\nconst { t } = useI18n()\n\nconst currentMenuItem = ref<JobListItem | null>(null)\nconst jobContextMenuRef = ref<InstanceType<typeof JobContextMenu> | null>(null)\n\nconst { jobMenuEntries } = useJobMenu(\n () => currentMenuItem.value,\n (item) => emit('viewItem', item)\n)\n\nconst onCancelItemEvent = (item: JobListItem) => {\n emit('cancelItem', item)\n}\n\nconst onDeleteItemEvent = (item: JobListItem) => {\n emit('deleteItem', item)\n}\n\nconst onMenuItem = (item: JobListItem, event: Event) => {\n currentMenuItem.value = item\n jobContextMenuRef.value?.open(event)\n}\n\nconst onJobMenuAction = async (entry: MenuEntry) => {\n if (entry.kind === 'divider') return\n if (entry.onClick) await entry.onClick()\n jobContextMenuRef.value?.hide()\n}\n</script>\n","<template>\n <section\n class=\"w-[360px] rounded-2xl border border-interface-stroke bg-interface-panel-surface text-text-primary shadow-interface font-inter\"\n >\n <header\n class=\"flex items-center justify-between border-b border-interface-stroke px-4 py-4\"\n >\n <p class=\"m-0 text-[14px] font-normal leading-none\">\n {{ t('sideToolbar.queueProgressOverlay.clearHistoryDialogTitle') }}\n </p>\n <Button\n size=\"icon\"\n variant=\"muted-textonly\"\n :aria-label=\"t('g.close')\"\n @click=\"onCancel\"\n >\n <i class=\"icon-[lucide--x] block size-4 leading-none\" />\n </Button>\n </header>\n\n <div class=\"flex flex-col gap-4 px-4 py-4 text-[14px] text-text-secondary\">\n <p class=\"m-0\">\n {{\n t('sideToolbar.queueProgressOverlay.clearHistoryDialogDescription')\n }}\n </p>\n <p class=\"m-0\">\n {{ t('sideToolbar.queueProgressOverlay.clearHistoryDialogAssetsNote') }}\n </p>\n </div>\n\n <footer class=\"flex items-center justify-end px-4 py-4\">\n <div class=\"flex items-center gap-4 leading-none\">\n <Button variant=\"muted-textonly\" size=\"lg\" @click=\"onCancel\">\n {{ t('g.cancel') }}\n </Button>\n <Button\n variant=\"secondary\"\n size=\"lg\"\n :disabled=\"isClearing\"\n @click=\"onConfirm\"\n >{{ t('g.clear') }}</Button\n >\n </div>\n </footer>\n </section>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { useErrorHandling } from '@/composables/useErrorHandling'\nimport { useDialogStore } from '@/stores/dialogStore'\nimport { useQueueStore } from '@/stores/queueStore'\n\nconst dialogStore = useDialogStore()\nconst queueStore = useQueueStore()\nconst { t } = useI18n()\nconst { wrapWithErrorHandlingAsync } = useErrorHandling()\n\nconst isClearing = ref(false)\n\nconst clearHistory = wrapWithErrorHandlingAsync(\n async () => {\n await queueStore.clear(['history'])\n dialogStore.closeDialog()\n },\n undefined,\n () => {\n isClearing.value = false\n }\n)\n\nconst onConfirm = async () => {\n if (isClearing.value) return\n isClearing.value = true\n await clearHistory()\n}\n\nconst onCancel = () => {\n dialogStore.closeDialog()\n}\n</script>\n","import { computed, ref, watch } from 'vue'\n\nimport { useExecutionStore } from '@/stores/executionStore'\nimport { useQueueStore } from '@/stores/queueStore'\nimport { jobStateFromTask } from '@/utils/queueUtil'\n\nexport type CompletionSummaryMode = 'allSuccess' | 'mixed' | 'allFailed'\n\nexport type CompletionSummary = {\n mode: CompletionSummaryMode\n completedCount: number\n failedCount: number\n thumbnailUrls: string[]\n}\n\n/**\n * Tracks queue activity transitions and exposes a short-lived summary of the\n * most recent generation batch.\n */\nexport const useCompletionSummary = () => {\n const queueStore = useQueueStore()\n const executionStore = useExecutionStore()\n\n const isActive = computed(\n () => queueStore.runningTasks.length > 0 || !executionStore.isIdle\n )\n\n const lastActiveStartTs = ref<number | null>(null)\n const _summary = ref<CompletionSummary | null>(null)\n const dismissTimer = ref<number | null>(null)\n\n const clearDismissTimer = () => {\n if (dismissTimer.value !== null) {\n clearTimeout(dismissTimer.value)\n dismissTimer.value = null\n }\n }\n\n const startDismissTimer = () => {\n clearDismissTimer()\n dismissTimer.value = window.setTimeout(() => {\n _summary.value = null\n dismissTimer.value = null\n }, 6000)\n }\n\n const clearSummary = () => {\n _summary.value = null\n clearDismissTimer()\n }\n\n watch(\n isActive,\n (active, prev) => {\n if (!prev && active) {\n lastActiveStartTs.value = Date.now()\n }\n if (prev && !active) {\n const start = lastActiveStartTs.value ?? 0\n const finished = queueStore.historyTasks.filter((t) => {\n const ts = t.executionEndTimestamp\n return typeof ts === 'number' && ts >= start\n })\n\n if (!finished.length) {\n _summary.value = null\n clearDismissTimer()\n return\n }\n\n let completedCount = 0\n let failedCount = 0\n const imagePreviews: string[] = []\n\n for (const task of finished) {\n const state = jobStateFromTask(task, false)\n if (state === 'completed') {\n completedCount++\n const preview = task.previewOutput\n if (preview?.isImage) {\n imagePreviews.push(preview.urlWithTimestamp)\n }\n } else if (state === 'failed') {\n failedCount++\n }\n }\n\n if (completedCount === 0 && failedCount === 0) {\n _summary.value = null\n clearDismissTimer()\n return\n }\n\n let mode: CompletionSummaryMode = 'mixed'\n if (failedCount === 0) mode = 'allSuccess'\n else if (completedCount === 0) mode = 'allFailed'\n\n _summary.value = {\n mode,\n completedCount,\n failedCount,\n thumbnailUrls: imagePreviews.slice(0, 3)\n }\n startDismissTimer()\n }\n },\n { immediate: true }\n )\n\n const summary = computed(() => _summary.value)\n\n return {\n summary,\n clearSummary\n }\n}\n","import { ref, shallowRef } from 'vue'\n\nimport type { JobListItem } from '@/composables/queue/useJobList'\nimport { findActiveIndex, getOutputsForTask } from '@/services/jobOutputCache'\nimport type { ResultItemImpl, TaskItemImpl } from '@/stores/queueStore'\n\n/**\n * Manages result gallery state and activation for queue items.\n */\nexport function useResultGallery(getFilteredTasks: () => TaskItemImpl[]) {\n const galleryActiveIndex = ref(-1)\n const galleryItems = shallowRef<ResultItemImpl[]>([])\n\n async function onViewItem(item: JobListItem) {\n const tasks = getFilteredTasks()\n if (!tasks.length) return\n\n const targetTask = item.taskRef\n const targetOutputs = targetTask\n ? await getOutputsForTask(targetTask)\n : null\n\n // Request was superseded by a newer one\n if (targetOutputs === null && targetTask) return\n\n // Use target's outputs if available, otherwise fall back to all previews\n const items = targetOutputs?.length\n ? targetOutputs\n : tasks\n .map((t) => t.previewOutput)\n .filter((o): o is ResultItemImpl => !!o)\n\n if (!items.length) return\n\n galleryItems.value = items\n galleryActiveIndex.value = findActiveIndex(\n items,\n item.taskRef?.previewOutput?.url\n )\n }\n\n return {\n galleryActiveIndex,\n galleryItems,\n onViewItem\n }\n}\n","<template>\n <div\n v-show=\"isVisible\"\n :class=\"['flex', 'justify-end', 'w-full', 'pointer-events-none']\"\n >\n <div\n class=\"pointer-events-auto flex w-[350px] min-w-[310px] max-h-[60vh] flex-col overflow-hidden rounded-lg border font-inter transition-colors duration-200 ease-in-out\"\n :class=\"containerClass\"\n @mouseenter=\"isHovered = true\"\n @mouseleave=\"isHovered = false\"\n >\n <!-- Expanded state -->\n <QueueOverlayExpanded\n v-if=\"isExpanded\"\n v-model:selected-job-tab=\"selectedJobTab\"\n v-model:selected-workflow-filter=\"selectedWorkflowFilter\"\n v-model:selected-sort-mode=\"selectedSortMode\"\n class=\"flex-1 min-h-0\"\n :header-title=\"headerTitle\"\n :show-concurrent-indicator=\"showConcurrentIndicator\"\n :concurrent-workflow-count=\"concurrentWorkflowCount\"\n :queued-count=\"queuedCount\"\n :displayed-job-groups=\"displayedJobGroups\"\n :has-failed-jobs=\"hasFailedJobs\"\n @show-assets=\"openAssetsSidebar\"\n @clear-history=\"onClearHistoryFromMenu\"\n @clear-queued=\"cancelQueuedWorkflows\"\n @cancel-item=\"onCancelItem\"\n @delete-item=\"onDeleteItem\"\n @view-item=\"inspectJobAsset\"\n />\n\n <QueueOverlayActive\n v-else-if=\"hasActiveJob\"\n :total-progress-style=\"totalProgressStyle\"\n :current-node-progress-style=\"currentNodeProgressStyle\"\n :total-percent-formatted=\"totalPercentFormatted\"\n :current-node-percent-formatted=\"currentNodePercentFormatted\"\n :current-node-name=\"currentNodeName\"\n :running-count=\"runningCount\"\n :queued-count=\"queuedCount\"\n :bottom-row-class=\"bottomRowClass\"\n @interrupt-all=\"interruptAll\"\n @clear-queued=\"cancelQueuedWorkflows\"\n @view-all-jobs=\"viewAllJobs\"\n />\n\n <QueueOverlayEmpty\n v-else-if=\"completionSummary\"\n :summary=\"completionSummary\"\n @summary-click=\"onSummaryClick\"\n />\n </div>\n </div>\n\n <ResultGallery\n v-model:active-index=\"galleryActiveIndex\"\n :all-gallery-items=\"galleryItems\"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, nextTick, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport QueueOverlayActive from '@/components/queue/QueueOverlayActive.vue'\nimport QueueOverlayEmpty from '@/components/queue/QueueOverlayEmpty.vue'\nimport QueueOverlayExpanded from '@/components/queue/QueueOverlayExpanded.vue'\nimport QueueClearHistoryDialog from '@/components/queue/dialogs/QueueClearHistoryDialog.vue'\nimport ResultGallery from '@/components/sidebar/tabs/queue/ResultGallery.vue'\nimport { useCompletionSummary } from '@/composables/queue/useCompletionSummary'\nimport { useJobList } from '@/composables/queue/useJobList'\nimport type { JobListItem } from '@/composables/queue/useJobList'\nimport { useQueueProgress } from '@/composables/queue/useQueueProgress'\nimport { useResultGallery } from '@/composables/queue/useResultGallery'\nimport { useErrorHandling } from '@/composables/useErrorHandling'\nimport { useAssetSelectionStore } from '@/platform/assets/composables/useAssetSelectionStore'\nimport { isCloud } from '@/platform/distribution/types'\nimport { api } from '@/scripts/api'\nimport { useAssetsStore } from '@/stores/assetsStore'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { useDialogStore } from '@/stores/dialogStore'\nimport { useExecutionStore } from '@/stores/executionStore'\nimport { useQueueStore } from '@/stores/queueStore'\nimport { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'\n\ntype OverlayState = 'hidden' | 'empty' | 'active' | 'expanded'\n\nconst props = withDefaults(\n defineProps<{\n expanded?: boolean\n menuHovered?: boolean\n }>(),\n {\n menuHovered: false\n }\n)\n\nconst emit = defineEmits<{\n (e: 'update:expanded', value: boolean): void\n}>()\n\nconst { t } = useI18n()\nconst queueStore = useQueueStore()\nconst commandStore = useCommandStore()\nconst executionStore = useExecutionStore()\nconst sidebarTabStore = useSidebarTabStore()\nconst dialogStore = useDialogStore()\nconst assetsStore = useAssetsStore()\nconst assetSelectionStore = useAssetSelectionStore()\nconst { wrapWithErrorHandlingAsync } = useErrorHandling()\n\nconst {\n totalPercentFormatted,\n currentNodePercentFormatted,\n totalProgressStyle,\n currentNodeProgressStyle\n} = useQueueProgress()\nconst isHovered = ref(false)\nconst isOverlayHovered = computed(() => isHovered.value || props.menuHovered)\nconst internalExpanded = ref(false)\nconst isExpanded = computed({\n get: () =>\n props.expanded === undefined ? internalExpanded.value : props.expanded,\n set: (value) => {\n if (props.expanded === undefined) {\n internalExpanded.value = value\n }\n emit('update:expanded', value)\n }\n})\n\nconst { summary: completionSummary, clearSummary } = useCompletionSummary()\nconst hasCompletionSummary = computed(() => completionSummary.value !== null)\n\nconst runningCount = computed(() => queueStore.runningTasks.length)\nconst queuedCount = computed(() => queueStore.pendingTasks.length)\nconst isExecuting = computed(() => !executionStore.isIdle)\nconst hasActiveJob = computed(() => runningCount.value > 0 || isExecuting.value)\nconst activeJobsCount = computed(() => runningCount.value + queuedCount.value)\n\nconst overlayState = computed<OverlayState>(() => {\n if (isExpanded.value) return 'expanded'\n if (hasActiveJob.value) return 'active'\n if (hasCompletionSummary.value) return 'empty'\n return 'hidden'\n})\n\nconst showBackground = computed(\n () =>\n overlayState.value === 'expanded' ||\n overlayState.value === 'empty' ||\n (overlayState.value === 'active' && isOverlayHovered.value)\n)\n\nconst isVisible = computed(() => overlayState.value !== 'hidden')\n\nconst containerClass = computed(() =>\n showBackground.value\n ? 'border-interface-stroke bg-interface-panel-surface shadow-interface'\n : 'border-transparent bg-transparent shadow-none'\n)\n\nconst bottomRowClass = computed(\n () =>\n `flex items-center justify-end gap-4 transition-opacity duration-200 ease-in-out ${\n overlayState.value === 'active' && isOverlayHovered.value\n ? 'opacity-100 pointer-events-auto'\n : 'opacity-0 pointer-events-none'\n }`\n)\nconst headerTitle = computed(() =>\n hasActiveJob.value\n ? `${activeJobsCount.value} ${t('sideToolbar.queueProgressOverlay.activeJobsSuffix')}`\n : t('sideToolbar.queueProgressOverlay.jobQueue')\n)\n\nconst concurrentWorkflowCount = computed(\n () => executionStore.runningWorkflowCount\n)\nconst showConcurrentIndicator = computed(\n () => concurrentWorkflowCount.value > 1\n)\n\nconst {\n selectedJobTab,\n selectedWorkflowFilter,\n selectedSortMode,\n hasFailedJobs,\n filteredTasks,\n groupedJobItems,\n currentNodeName\n} = useJobList()\n\nconst displayedJobGroups = computed(() => groupedJobItems.value)\n\nconst onCancelItem = wrapWithErrorHandlingAsync(async (item: JobListItem) => {\n const promptId = item.taskRef?.promptId\n if (!promptId) return\n\n if (item.state === 'running' || item.state === 'initialization') {\n // Running/initializing jobs: interrupt execution\n await api.interrupt(promptId)\n await queueStore.update()\n } else if (item.state === 'pending') {\n // Pending jobs: remove from queue\n await api.deleteItem('queue', promptId)\n await queueStore.update()\n }\n})\n\nconst onDeleteItem = wrapWithErrorHandlingAsync(async (item: JobListItem) => {\n if (!item.taskRef) return\n await queueStore.delete(item.taskRef)\n})\n\nconst {\n galleryActiveIndex,\n galleryItems,\n onViewItem: openResultGallery\n} = useResultGallery(() => filteredTasks.value)\n\nconst setExpanded = (expanded: boolean) => {\n isExpanded.value = expanded\n}\n\nconst openExpandedFromEmpty = () => {\n setExpanded(true)\n}\n\nconst viewAllJobs = () => {\n setExpanded(true)\n}\n\nconst onSummaryClick = () => {\n openExpandedFromEmpty()\n clearSummary()\n}\n\nconst openAssetsSidebar = () => {\n sidebarTabStore.activeSidebarTabId = 'assets'\n}\n\nconst focusAssetInSidebar = async (item: JobListItem) => {\n const task = item.taskRef\n const promptId = task?.promptId\n const preview = task?.previewOutput\n if (!promptId || !preview) return\n\n const assetId = String(promptId)\n openAssetsSidebar()\n await nextTick()\n await assetsStore.updateHistory()\n const asset = assetsStore.historyAssets.find(\n (existingAsset) => existingAsset.id === assetId\n )\n if (!asset) {\n throw new Error('Asset not found in media assets panel')\n }\n assetSelectionStore.setSelection([assetId])\n}\n\nconst inspectJobAsset = wrapWithErrorHandlingAsync(\n async (item: JobListItem) => {\n await openResultGallery(item)\n await focusAssetInSidebar(item)\n }\n)\n\nconst cancelQueuedWorkflows = wrapWithErrorHandlingAsync(async () => {\n await commandStore.execute('Comfy.ClearPendingTasks')\n})\n\nconst interruptAll = wrapWithErrorHandlingAsync(async () => {\n const tasks = queueStore.runningTasks\n const promptIds = tasks\n .map((task) => task.promptId)\n .filter((id): id is string => typeof id === 'string' && id.length > 0)\n\n if (!promptIds.length) return\n\n // Cloud backend supports cancelling specific jobs via /queue delete,\n // while /interrupt always targets the \"first\" job. Use the targeted API\n // on cloud to ensure we cancel the workflow the user clicked.\n if (isCloud) {\n await Promise.all(promptIds.map((id) => api.deleteItem('queue', id)))\n return\n }\n\n await Promise.all(promptIds.map((id) => api.interrupt(id)))\n})\n\nconst showClearHistoryDialog = () => {\n dialogStore.showDialog({\n key: 'queue-clear-history',\n component: QueueClearHistoryDialog,\n dialogComponentProps: {\n headless: true,\n closable: false,\n closeOnEscape: true,\n dismissableMask: true,\n pt: {\n root: {\n class: 'max-w-[360px] w-auto bg-transparent border-none shadow-none'\n },\n content: {\n class: '!p-0 bg-transparent'\n }\n }\n }\n })\n}\n\nconst onClearHistoryFromMenu = () => {\n showClearHistoryDialog()\n}\n</script>\n","import { defineStore } from 'pinia'\nimport { computed } from 'vue'\n\nimport type { ActionBarButton } from '@/types/comfy'\n\nimport { useExtensionStore } from './extensionStore'\n\nexport const useActionBarButtonStore = defineStore('actionBarButton', () => {\n const extensionStore = useExtensionStore()\n\n const buttons = computed<ActionBarButton[]>(() =>\n extensionStore.extensions.flatMap((e) => e.actionBarButtons ?? [])\n )\n\n return {\n buttons\n }\n})\n","<template>\n <div class=\"flex h-full shrink-0 items-center gap-1 empty:hidden\">\n <Button\n v-for=\"(button, index) in actionBarButtonStore.buttons\"\n :key=\"index\"\n v-tooltip.bottom=\"button.tooltip\"\n :aria-label=\"button.tooltip || button.label\"\n :class=\"button.class\"\n variant=\"muted-textonly\"\n size=\"sm\"\n class=\"h-7 rounded-full\"\n @click=\"button.onClick\"\n >\n <i :class=\"button.icon\" />\n <span v-if=\"!isMobile && button.label\">{{ button.label }}</span>\n </Button>\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { breakpointsTailwind, useBreakpoints } from '@vueuse/core'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { useActionBarButtonStore } from '@/stores/actionBarButtonStore'\n\nconst actionBarButtonStore = useActionBarButtonStore()\n\nconst breakpoints = useBreakpoints(breakpointsTailwind)\nconst isMobile = breakpoints.smaller('sm')\n</script>\n","<!-- A popover that shows current user information and actions -->\n<template>\n <div\n class=\"current-user-popover w-80 -m-3 p-2 rounded-lg border border-border-default bg-base-background shadow-[1px_1px_8px_0_rgba(0,0,0,0.4)]\"\n >\n <!-- User Info Section -->\n <div class=\"flex flex-col items-center px-0 py-3 mb-4\">\n <UserAvatar\n class=\"mb-1\"\n :photo-url=\"userPhotoUrl\"\n :pt:icon:class=\"{\n 'text-2xl!': !userPhotoUrl\n }\"\n size=\"large\"\n />\n\n <!-- User Details -->\n <h3 class=\"my-0 mb-1 truncate text-base font-bold text-base-foreground\">\n {{ userDisplayName || $t('g.user') }}\n </h3>\n <p v-if=\"userEmail\" class=\"my-0 truncate text-sm text-muted\">\n {{ userEmail }}\n </p>\n <span\n v-if=\"subscriptionTierName\"\n class=\"my-0 text-xs text-foreground bg-secondary-background-hover rounded-full uppercase px-2 py-0.5 font-bold mt-2\"\n >\n {{ subscriptionTierName }}\n </span>\n </div>\n\n <!-- Credits Section -->\n <div v-if=\"isActiveSubscription\" class=\"flex items-center gap-2 px-4 py-2\">\n <i class=\"icon-[lucide--component] text-amber-400 text-sm\" />\n <Skeleton\n v-if=\"authStore.isFetchingBalance\"\n width=\"4rem\"\n height=\"1.25rem\"\n class=\"w-full\"\n />\n <span v-else class=\"text-base font-semibold text-base-foreground\">{{\n formattedBalance\n }}</span>\n <i\n v-tooltip=\"{ value: $t('credits.unified.tooltip'), showDelay: 300 }\"\n class=\"icon-[lucide--circle-help] cursor-help text-base text-muted-foreground mr-auto\"\n />\n <Button\n variant=\"secondary\"\n size=\"sm\"\n class=\"text-base-foreground\"\n data-testid=\"add-credits-button\"\n @click=\"handleTopUp\"\n >\n {{ $t('subscription.addCredits') }}\n </Button>\n </div>\n\n <div v-else class=\"flex justify-center px-4\">\n <SubscribeButton\n :fluid=\"false\"\n :label=\"$t('subscription.subscribeToComfyCloud')\"\n size=\"sm\"\n variant=\"gradient\"\n @subscribed=\"handleSubscribed\"\n />\n </div>\n\n <Divider class=\"my-2 mx-0\" />\n\n <div\n v-if=\"isActiveSubscription\"\n class=\"flex items-center gap-2 px-4 py-2 cursor-pointer hover:bg-secondary-background-hover\"\n data-testid=\"partner-nodes-menu-item\"\n @click=\"handleOpenPartnerNodesInfo\"\n >\n <i class=\"icon-[lucide--tag] text-muted-foreground text-sm\" />\n <span class=\"text-sm text-base-foreground flex-1\">{{\n $t('subscription.partnerNodesCredits')\n }}</span>\n </div>\n\n <div\n class=\"flex items-center gap-2 px-4 py-2 cursor-pointer hover:bg-secondary-background-hover\"\n data-testid=\"plans-pricing-menu-item\"\n @click=\"handleOpenPlansAndPricing\"\n >\n <i class=\"icon-[lucide--receipt-text] text-muted-foreground text-sm\" />\n <span class=\"text-sm text-base-foreground flex-1\">{{\n $t('subscription.plansAndPricing')\n }}</span>\n <span\n v-if=\"canUpgrade\"\n class=\"text-xs font-bold text-base-background bg-base-foreground px-1.5 py-0.5 rounded-full\"\n >\n {{ $t('subscription.upgrade') }}\n </span>\n </div>\n\n <div\n v-if=\"isActiveSubscription\"\n class=\"flex items-center gap-2 px-4 py-2 cursor-pointer hover:bg-secondary-background-hover\"\n data-testid=\"manage-plan-menu-item\"\n @click=\"handleOpenPlanAndCreditsSettings\"\n >\n <i class=\"icon-[lucide--file-text] text-muted-foreground text-sm\" />\n <span class=\"text-sm text-base-foreground flex-1\">{{\n $t('subscription.managePlan')\n }}</span>\n </div>\n\n <div\n class=\"flex items-center gap-2 px-4 py-2 cursor-pointer hover:bg-secondary-background-hover\"\n data-testid=\"user-settings-menu-item\"\n @click=\"handleOpenUserSettings\"\n >\n <i class=\"icon-[lucide--settings-2] text-muted-foreground text-sm\" />\n <span class=\"text-sm text-base-foreground flex-1\">{{\n $t('userSettings.accountSettings')\n }}</span>\n </div>\n\n <Divider class=\"my-2 mx-0\" />\n\n <div\n class=\"flex items-center gap-2 px-4 py-2 cursor-pointer hover:bg-secondary-background-hover\"\n data-testid=\"logout-menu-item\"\n @click=\"handleLogout\"\n >\n <i class=\"icon-[lucide--log-out] text-muted-foreground text-sm\" />\n <span class=\"text-sm text-base-foreground flex-1\">{{\n $t('auth.signOut.signOut')\n }}</span>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport Divider from 'primevue/divider'\nimport Skeleton from 'primevue/skeleton'\nimport { computed, onMounted } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { formatCreditsFromCents } from '@/base/credits/comfyCredits'\nimport UserAvatar from '@/components/common/UserAvatar.vue'\nimport Button from '@/components/ui/button/Button.vue'\nimport { useCurrentUser } from '@/composables/auth/useCurrentUser'\nimport { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'\nimport { useExternalLink } from '@/composables/useExternalLink'\nimport SubscribeButton from '@/platform/cloud/subscription/components/SubscribeButton.vue'\nimport { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'\nimport { useSubscriptionDialog } from '@/platform/cloud/subscription/composables/useSubscriptionDialog'\nimport { isCloud } from '@/platform/distribution/types'\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useDialogService } from '@/services/dialogService'\nimport { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'\n\nconst emit = defineEmits<{\n close: []\n}>()\n\nconst { buildDocsUrl, docsPaths } = useExternalLink()\n\nconst { userDisplayName, userEmail, userPhotoUrl, handleSignOut } =\n useCurrentUser()\nconst authActions = useFirebaseAuthActions()\nconst authStore = useFirebaseAuthStore()\nconst dialogService = useDialogService()\nconst {\n isActiveSubscription,\n subscriptionTierName,\n subscriptionTier,\n fetchStatus\n} = useSubscription()\nconst subscriptionDialog = useSubscriptionDialog()\nconst { locale } = useI18n()\n\nconst formattedBalance = computed(() => {\n const cents =\n authStore.balance?.effective_balance_micros ??\n authStore.balance?.amount_micros ??\n 0\n return formatCreditsFromCents({\n cents,\n locale: locale.value,\n numberOptions: {\n minimumFractionDigits: 0,\n maximumFractionDigits: 2\n }\n })\n})\n\nconst canUpgrade = computed(() => {\n const tier = subscriptionTier.value\n return (\n tier === 'FOUNDERS_EDITION' || tier === 'STANDARD' || tier === 'CREATOR'\n )\n})\n\nconst handleOpenUserSettings = () => {\n dialogService.showSettingsDialog('user')\n emit('close')\n}\n\nconst handleOpenPlansAndPricing = () => {\n subscriptionDialog.show()\n emit('close')\n}\n\nconst handleOpenPlanAndCreditsSettings = () => {\n if (isCloud) {\n dialogService.showSettingsDialog('subscription')\n } else {\n dialogService.showSettingsDialog('credits')\n }\n\n emit('close')\n}\n\nconst handleTopUp = () => {\n // Track purchase credits entry from avatar popover\n useTelemetry()?.trackAddApiCreditButtonClicked()\n dialogService.showTopUpCreditsDialog()\n emit('close')\n}\n\nconst handleOpenPartnerNodesInfo = () => {\n window.open(\n buildDocsUrl(docsPaths.partnerNodesPricing, { includeLocale: true }),\n '_blank'\n )\n emit('close')\n}\n\nconst handleLogout = async () => {\n await handleSignOut()\n emit('close')\n}\n\nconst handleSubscribed = async () => {\n await fetchStatus()\n}\n\nonMounted(() => {\n void authActions.fetchBalance()\n})\n</script>\n","<!-- A button that shows current authenticated user's avatar -->\n<template>\n <div>\n <Button\n v-if=\"isLoggedIn\"\n class=\"p-1 hover:bg-transparent\"\n variant=\"muted-textonly\"\n :aria-label=\"$t('g.currentUser')\"\n @click=\"popover?.toggle($event)\"\n >\n <div\n :class=\"\n cn(\n 'flex items-center gap-1 rounded-full hover:bg-interface-button-hover-surface justify-center',\n compact && 'size-full aspect-square'\n )\n \"\n >\n <UserAvatar :photo-url=\"photoURL\" :class=\"compact && 'size-full'\" />\n\n <i v-if=\"showArrow\" class=\"icon-[lucide--chevron-down] size-3 px-1\" />\n </div>\n </Button>\n\n <Popover\n ref=\"popover\"\n :show-arrow=\"false\"\n :pt=\"{\n root: {\n class: 'rounded-lg'\n }\n }\"\n >\n <CurrentUserPopover @close=\"closePopover\" />\n </Popover>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport Popover from 'primevue/popover'\nimport { computed, ref } from 'vue'\n\nimport UserAvatar from '@/components/common/UserAvatar.vue'\nimport Button from '@/components/ui/button/Button.vue'\nimport { useCurrentUser } from '@/composables/auth/useCurrentUser'\nimport { cn } from '@/utils/tailwindUtil'\n\nimport CurrentUserPopover from './CurrentUserPopover.vue'\n\nconst { showArrow = true, compact = false } = defineProps<{\n showArrow?: boolean\n compact?: boolean\n}>()\n\nconst { isLoggedIn, userPhotoUrl } = useCurrentUser()\n\nconst popover = ref<InstanceType<typeof Popover> | null>(null)\nconst photoURL = computed<string | undefined>(\n () => userPhotoUrl.value ?? undefined\n)\n\nconst closePopover = () => {\n popover.value?.hide()\n}\n</script>\n","<template>\n <Button\n v-if=\"!isLoggedIn\"\n variant=\"textonly\"\n size=\"icon\"\n :class=\"cn('group rounded-full text-base-foreground p-0', className)\"\n :aria-label=\"t('g.login')\"\n @click=\"handleSignIn()\"\n @mouseenter=\"showPopover\"\n @mouseleave=\"hidePopover\"\n >\n <span\n class=\"flex size-full items-center justify-center rounded-full bg-secondary-background transition-colors group-hover:bg-transparent\"\n >\n <i class=\"icon-[lucide--user] size-4\" />\n </span>\n </Button>\n <Popover\n ref=\"popoverRef\"\n class=\"p-2\"\n @mouseout=\"hidePopover\"\n @mouseover=\"cancelHidePopover\"\n >\n <div>\n <div class=\"mb-1\">{{ t('auth.loginButton.tooltipHelp') }}</div>\n <a\n :href=\"apiNodesOverviewUrl\"\n target=\"_blank\"\n class=\"text-neutral-500 hover:text-primary\"\n >{{ t('auth.loginButton.tooltipLearnMore') }}</a\n >\n </div>\n </Popover>\n</template>\n\n<script setup lang=\"ts\">\nimport Popover from 'primevue/popover'\nimport type { HTMLAttributes } from 'vue'\nimport { ref } from 'vue'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { useCurrentUser } from '@/composables/auth/useCurrentUser'\nimport { useExternalLink } from '@/composables/useExternalLink'\nimport { t } from '@/i18n'\nimport { cn } from '@/utils/tailwindUtil'\n\nconst { class: className } = defineProps<{\n class?: HTMLAttributes['class']\n}>()\n\nconst { isLoggedIn, handleSignIn } = useCurrentUser()\nconst { buildDocsUrl } = useExternalLink()\nconst apiNodesOverviewUrl = buildDocsUrl(\n '/tutorials/api-nodes/overview#api-nodes',\n {\n includeLocale: true\n }\n)\nconst popoverRef = ref<InstanceType<typeof Popover> | null>(null)\nlet hideTimeout: ReturnType<typeof setTimeout> | null = null\nlet showTimeout: ReturnType<typeof setTimeout> | null = null\n\nconst showPopover = (event: Event) => {\n // Clear any existing timeouts\n if (hideTimeout) {\n clearTimeout(hideTimeout)\n hideTimeout = null\n }\n if (showTimeout) {\n clearTimeout(showTimeout)\n showTimeout = null\n }\n\n showTimeout = setTimeout(() => {\n if (popoverRef.value) {\n popoverRef.value.show(event, event.target as HTMLElement)\n }\n }, 200)\n}\n\nconst cancelHidePopover = () => {\n if (hideTimeout) {\n clearTimeout(hideTimeout)\n hideTimeout = null\n }\n}\n\nconst hidePopover = () => {\n // Clear show timeout if mouse leaves before popover appears\n if (showTimeout) {\n clearTimeout(showTimeout)\n showTimeout = null\n }\n\n hideTimeout = setTimeout(() => {\n if (popoverRef.value) {\n popoverRef.value.hide()\n }\n }, 150) // Minimal delay to allow moving to popover\n}\n</script>\n","<template>\n <div\n v-if=\"!workspaceStore.focusMode\"\n class=\"ml-1 flex gap-x-0.5 pt-1\"\n @mouseenter=\"isTopMenuHovered = true\"\n @mouseleave=\"isTopMenuHovered = false\"\n >\n <div class=\"min-w-0 flex-1\">\n <SubgraphBreadcrumb />\n </div>\n\n <div class=\"mx-1 flex flex-col items-end gap-1\">\n <div class=\"flex items-center gap-2\">\n <div\n v-if=\"managerState.shouldShowManagerButtons.value\"\n class=\"pointer-events-auto flex h-12 shrink-0 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface\"\n >\n <Button\n v-tooltip.bottom=\"customNodesManagerTooltipConfig\"\n variant=\"secondary\"\n size=\"icon\"\n :aria-label=\"t('menu.customNodesManager')\"\n class=\"relative\"\n @click=\"openCustomNodeManager\"\n >\n <i class=\"icon-[lucide--puzzle] size-4\" />\n <span\n v-if=\"shouldShowRedDot\"\n class=\"absolute top-0.5 right-1 size-2 rounded-full bg-red-500\"\n />\n </Button>\n </div>\n\n <div\n class=\"actionbar-container pointer-events-auto flex gap-2 h-12 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface\"\n >\n <ActionBarButtons />\n <!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->\n <div\n ref=\"legacyCommandsContainerRef\"\n class=\"[&:not(:has(*>*:not(:empty)))]:hidden\"\n ></div>\n <ComfyActionbar />\n <Button\n v-tooltip.bottom=\"queueHistoryTooltipConfig\"\n type=\"destructive\"\n size=\"icon\"\n :aria-pressed=\"isQueueOverlayExpanded\"\n :aria-label=\"\n t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')\n \"\n @click=\"toggleQueueOverlay\"\n >\n <i class=\"icon-[lucide--history] size-4\" />\n <span\n v-if=\"queuedCount > 0\"\n class=\"absolute -top-1 -right-1 min-w-[16px] rounded-full bg-primary-background py-0.25 text-[10px] font-medium leading-[14px] text-base-foreground\"\n >\n {{ queuedCount }}\n </span>\n </Button>\n <CurrentUserButton\n v-if=\"isLoggedIn && !isIntegratedTabBar\"\n class=\"shrink-0\"\n />\n <LoginButton v-else-if=\"isDesktop && !isIntegratedTabBar\" />\n <Button\n v-if=\"!isRightSidePanelOpen\"\n v-tooltip.bottom=\"rightSidePanelTooltipConfig\"\n type=\"secondary\"\n size=\"icon\"\n :aria-label=\"t('rightSidePanel.togglePanel')\"\n @click=\"rightSidePanelStore.togglePanel\"\n >\n <i class=\"icon-[lucide--panel-right] size-4\" />\n </Button>\n </div>\n </div>\n <QueueProgressOverlay\n v-model:expanded=\"isQueueOverlayExpanded\"\n :menu-hovered=\"isTopMenuHovered\"\n />\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { storeToRefs } from 'pinia'\nimport { computed, onMounted, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'\nimport SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'\nimport QueueProgressOverlay from '@/components/queue/QueueProgressOverlay.vue'\nimport ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'\nimport CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'\nimport LoginButton from '@/components/topbar/LoginButton.vue'\nimport Button from '@/components/ui/button/Button.vue'\nimport { useCurrentUser } from '@/composables/auth/useCurrentUser'\nimport { useErrorHandling } from '@/composables/useErrorHandling'\nimport { buildTooltipConfig } from '@/composables/useTooltipConfig'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useReleaseStore } from '@/platform/updates/common/releaseStore'\nimport { app } from '@/scripts/app'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { useQueueStore, useQueueUIStore } from '@/stores/queueStore'\nimport { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'\nimport { useWorkspaceStore } from '@/stores/workspaceStore'\nimport { isElectron } from '@/utils/envUtil'\nimport { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'\nimport { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'\nimport { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'\n\nconst settingStore = useSettingStore()\nconst workspaceStore = useWorkspaceStore()\nconst rightSidePanelStore = useRightSidePanelStore()\nconst managerState = useManagerState()\nconst { isLoggedIn } = useCurrentUser()\nconst isDesktop = isElectron()\nconst { t } = useI18n()\nconst { toastErrorHandler } = useErrorHandling()\nconst commandStore = useCommandStore()\nconst queueStore = useQueueStore()\nconst queueUIStore = useQueueUIStore()\nconst { isOverlayExpanded: isQueueOverlayExpanded } = storeToRefs(queueUIStore)\nconst releaseStore = useReleaseStore()\nconst { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)\nconst { shouldShowRedDot: shouldShowConflictRedDot } =\n useConflictAcknowledgment()\nconst isTopMenuHovered = ref(false)\nconst queuedCount = computed(() => queueStore.pendingTasks.length)\nconst isIntegratedTabBar = computed(\n () => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'\n)\nconst queueHistoryTooltipConfig = computed(() =>\n buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))\n)\nconst customNodesManagerTooltipConfig = computed(() =>\n buildTooltipConfig(t('menu.customNodesManager'))\n)\n\n// Use either release red dot or conflict red dot\nconst shouldShowRedDot = computed((): boolean => {\n const releaseRedDot = showReleaseRedDot.value\n return releaseRedDot || shouldShowConflictRedDot.value\n})\n\n// Right side panel toggle\nconst { isOpen: isRightSidePanelOpen } = storeToRefs(rightSidePanelStore)\nconst rightSidePanelTooltipConfig = computed(() =>\n buildTooltipConfig(t('rightSidePanel.togglePanel'))\n)\n\n// Maintain support for legacy topbar elements attached by custom scripts\nconst legacyCommandsContainerRef = ref<HTMLElement>()\nonMounted(() => {\n if (legacyCommandsContainerRef.value) {\n app.menu.element.style.width = 'fit-content'\n legacyCommandsContainerRef.value.appendChild(app.menu.element)\n }\n})\n\nconst toggleQueueOverlay = () => {\n commandStore.execute('Comfy.Queue.ToggleOverlay')\n}\n\nconst openCustomNodeManager = async () => {\n try {\n await managerState.openManager({\n initialTab: ManagerTab.All,\n showToastOnLegacyError: false\n })\n } catch (error) {\n try {\n toastErrorHandler(error)\n } catch (toastError) {\n console.error(error)\n console.error(toastError)\n }\n }\n}\n</script>\n","<template>\n <component :is=\"extension.component\" v-if=\"extension.type === 'vue'\" />\n <div\n v-else\n :ref=\"\n (el) => {\n if (el)\n mountCustomExtension(\n props.extension as CustomExtension,\n el as HTMLElement\n )\n }\n \"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport { onBeforeUnmount } from 'vue'\n\nimport type { CustomExtension, VueExtension } from '@/types/extensionTypes'\n\nconst props = defineProps<{\n extension: VueExtension | CustomExtension\n}>()\n\nconst mountCustomExtension = (extension: CustomExtension, el: HTMLElement) => {\n extension.render(el)\n}\n\nonBeforeUnmount(() => {\n if (props.extension.type === 'custom' && props.extension.destroy) {\n props.extension.destroy()\n }\n})\n</script>\n","<template>\n <div class=\"flex h-full flex-col\">\n <Tabs\n :key=\"$i18n.locale\"\n v-model:value=\"bottomPanelStore.activeBottomPanelTabId\"\n style=\"--p-tabs-tablist-background: var(--comfy-menu-bg)\"\n >\n <TabList\n pt:tab-list=\"border-none h-full flex items-center py-2 border-b-1 border-solid\"\n class=\"bg-transparent\"\n >\n <div class=\"flex w-full justify-between\">\n <div class=\"tabs-container font-inter\">\n <Tab\n v-for=\"tab in bottomPanelStore.bottomPanelTabs\"\n :key=\"tab.id\"\n :value=\"tab.id\"\n class=\"m-1 mx-2 border-none font-inter\"\n :class=\"{\n 'tab-list-single-item':\n bottomPanelStore.bottomPanelTabs.length === 1\n }\"\n :pt:root=\"\n (x: TabPassThroughMethodOptions) => ({\n class: {\n 'p-3 rounded-lg': true,\n 'pointer-events-none':\n bottomPanelStore.bottomPanelTabs.length === 1\n },\n style: {\n color: 'var(--fg-color)',\n backgroundColor:\n !x.context.active ||\n bottomPanelStore.bottomPanelTabs.length === 1\n ? ''\n : 'var(--bg-color)'\n }\n })\n \"\n >\n <span class=\"font-normal\">\n {{ getTabDisplayTitle(tab) }}\n </span>\n </Tab>\n </div>\n <div class=\"flex items-center gap-2\">\n <Button\n v-if=\"isShortcutsTabActive\"\n variant=\"muted-textonly\"\n size=\"sm\"\n @click=\"openKeybindingSettings\"\n >\n <i class=\"pi pi-cog\" />\n {{ $t('shortcuts.manageShortcuts') }}\n </Button>\n <Button\n class=\"justify-self-end\"\n variant=\"muted-textonly\"\n size=\"sm\"\n :aria-label=\"t('g.close')\"\n @click=\"closeBottomPanel\"\n >\n <i class=\"pi pi-times\" />\n </Button>\n </div>\n </div>\n </TabList>\n </Tabs>\n <!-- h-0 to force the div to grow -->\n <div class=\"h-0 grow\">\n <ExtensionSlot\n v-if=\"\n bottomPanelStore.bottomPanelVisible &&\n bottomPanelStore.activeBottomPanelTab\n \"\n :extension=\"bottomPanelStore.activeBottomPanelTab\"\n />\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport Tab from 'primevue/tab'\nimport type { TabPassThroughMethodOptions } from 'primevue/tab'\nimport TabList from 'primevue/tablist'\nimport Tabs from 'primevue/tabs'\nimport { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport ExtensionSlot from '@/components/common/ExtensionSlot.vue'\nimport Button from '@/components/ui/button/Button.vue'\nimport { useDialogService } from '@/services/dialogService'\nimport { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'\nimport type { BottomPanelExtension } from '@/types/extensionTypes'\n\nconst bottomPanelStore = useBottomPanelStore()\nconst dialogService = useDialogService()\nconst { t } = useI18n()\n\nconst isShortcutsTabActive = computed(() => {\n const activeTabId = bottomPanelStore.activeBottomPanelTabId\n return (\n activeTabId === 'shortcuts-essentials' ||\n activeTabId === 'shortcuts-view-controls'\n )\n})\n\nconst shouldCapitalizeTab = (tabId: string): boolean => {\n return tabId !== 'shortcuts-essentials' && tabId !== 'shortcuts-view-controls'\n}\n\nconst getTabDisplayTitle = (tab: BottomPanelExtension): string => {\n const title = tab.titleKey ? t(tab.titleKey) : tab.title || ''\n return shouldCapitalizeTab(tab.id) ? title.toUpperCase() : title\n}\n\nconst openKeybindingSettings = async () => {\n dialogService.showSettingsDialog('keybinding')\n}\n\nconst closeBottomPanel = () => {\n bottomPanelStore.activePanel = null\n}\n</script>\n\n<style scoped>\n:deep(.p-tablist-active-bar) {\n display: none;\n}\n</style>\n","import type { CSSProperties } from 'vue'\nimport { ref, watch } from 'vue'\n\nimport { useCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'\nimport type { Size, Vector2 } from '@/lib/litegraph/src/litegraph'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\n\nexport interface PositionConfig {\n /* The position of the element on litegraph canvas */\n pos: Vector2\n /* The size of the element on litegraph canvas */\n size: Size\n /* The scale factor of the canvas */\n scale?: number\n}\n\nexport function useAbsolutePosition(options: { useTransform?: boolean } = {}) {\n const { useTransform = false } = options\n\n const canvasStore = useCanvasStore()\n const lgCanvas = canvasStore.getCanvas()\n const { canvasPosToClientPos, update: updateCanvasPosition } =\n useCanvasPositionConversion(lgCanvas.canvas, lgCanvas)\n\n const settingStore = useSettingStore()\n watch(\n [\n () => settingStore.get('Comfy.Sidebar.Location'),\n () => settingStore.get('Comfy.Sidebar.Size'),\n () => settingStore.get('Comfy.UseNewMenu')\n ],\n () => updateCanvasPosition(),\n { flush: 'post' }\n )\n\n /**\n * @note Do NOT convert style to a computed value, as it will cause lag when\n * updating the style on different animation frames. Vue's computed value is\n * evaluated asynchronously.\n */\n const style = ref<CSSProperties>({})\n\n /**\n * Compute the style of the element based on the position and size.\n *\n * @param position\n */\n const computeStyle = (position: PositionConfig): CSSProperties => {\n const { pos, size, scale = lgCanvas.ds.scale } = position\n const [left, top] = canvasPosToClientPos(pos)\n const [width, height] = size\n\n return useTransform\n ? {\n position: 'fixed',\n transformOrigin: '0 0',\n transform: `scale(${scale})`,\n left: `${left}px`,\n top: `${top}px`,\n width: `${width}px`,\n height: `${height}px`\n }\n : {\n position: 'fixed',\n left: `${left}px`,\n top: `${top}px`,\n width: `${width * scale}px`,\n height: `${height * scale}px`\n }\n }\n\n /**\n * Update the position of the element on the litegraph canvas.\n *\n * @param config\n */\n const updatePosition = (config: PositionConfig) => {\n style.value = computeStyle(config)\n }\n\n return {\n style,\n updatePosition\n }\n}\n","import type { CSSProperties } from 'vue'\nimport { ref } from 'vue'\n\ninterface Rect {\n x: number\n y: number\n width: number\n height: number\n}\n\n/**\n * Finds the intersection between two rectangles\n */\nfunction intersect(a: Rect, b: Rect): [number, number, number, number] | null {\n const x1 = Math.max(a.x, b.x)\n const y1 = Math.max(a.y, b.y)\n const x2 = Math.min(a.x + a.width, b.x + b.width)\n const y2 = Math.min(a.y + a.height, b.y + b.height)\n\n if (x1 >= x2 || y1 >= y2) {\n return null\n }\n\n return [x1, y1, x2 - x1, y2 - y1]\n}\n\ninterface ClippingOptions {\n margin?: number\n}\n\nexport const useDomClipping = (options: ClippingOptions = {}) => {\n const style = ref<CSSProperties>({})\n const { margin = 4 } = options\n\n /**\n * Calculates a clip path for an element based on its intersection with a selected area\n */\n const calculateClipPath = (\n elementRect: DOMRect,\n canvasRect: DOMRect,\n isSelected: boolean,\n selectedArea?: {\n x: number\n y: number\n width: number\n height: number\n scale: number\n offset: [number, number]\n }\n ): string => {\n if (!isSelected && selectedArea) {\n const { scale, offset } = selectedArea\n\n // Get intersection in browser space\n const intersection = intersect(\n {\n x: elementRect.left - canvasRect.left,\n y: elementRect.top - canvasRect.top,\n width: elementRect.width,\n height: elementRect.height\n },\n {\n x: (selectedArea.x + offset[0] - margin) * scale,\n y: (selectedArea.y + offset[1] - margin) * scale,\n width: (selectedArea.width + 2 * margin) * scale,\n height: (selectedArea.height + 2 * margin) * scale\n }\n )\n\n if (!intersection) {\n return ''\n }\n\n // Convert intersection to canvas scale (element has scale transform)\n const clipX =\n (intersection[0] - elementRect.left + canvasRect.left) / scale + 'px'\n const clipY =\n (intersection[1] - elementRect.top + canvasRect.top) / scale + 'px'\n const clipWidth = intersection[2] / scale + 'px'\n const clipHeight = intersection[3] / scale + 'px'\n\n return `polygon(0% 0%, 0% 100%, ${clipX} 100%, ${clipX} ${clipY}, calc(${clipX} + ${clipWidth}) ${clipY}, calc(${clipX} + ${clipWidth}) calc(${clipY} + ${clipHeight}), ${clipX} calc(${clipY} + ${clipHeight}), ${clipX} 100%, 100% 100%, 100% 0%)`\n }\n\n return ''\n }\n\n /**\n * Updates the clip-path style based on element and selection information\n */\n const updateClipPath = (\n element: HTMLElement,\n canvasElement: HTMLCanvasElement,\n isSelected: boolean,\n selectedArea?: {\n x: number\n y: number\n width: number\n height: number\n scale: number\n offset: [number, number]\n }\n ) => {\n const elementRect = element.getBoundingClientRect()\n const canvasRect = canvasElement.getBoundingClientRect()\n\n const clipPath = calculateClipPath(\n elementRect,\n canvasRect,\n isSelected,\n selectedArea\n )\n\n style.value = {\n clipPath: clipPath || 'none',\n willChange: 'clip-path'\n }\n }\n\n return {\n style,\n updateClipPath\n }\n}\n","<template>\n <div\n v-show=\"widgetState.visible\"\n ref=\"widgetElement\"\n class=\"dom-widget\"\n :title=\"tooltip\"\n :style=\"style\"\n >\n <component\n :is=\"widget.component\"\n v-if=\"isComponentWidget(widget)\"\n :model-value=\"widget.value\"\n :widget=\"widget\"\n v-bind=\"widget.props\"\n @update:model-value=\"emit('update:widgetValue', $event)\"\n />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useElementBounding, useEventListener } from '@vueuse/core'\nimport type { CSSProperties } from 'vue'\nimport { computed, nextTick, onMounted, ref, watch } from 'vue'\n\nimport { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'\nimport { useDomClipping } from '@/composables/element/useDomClipping'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { isComponentWidget, isDOMWidget } from '@/scripts/domWidget'\nimport type { DomWidgetState } from '@/stores/domWidgetStore'\n\nconst { widgetState } = defineProps<{\n widgetState: DomWidgetState\n}>()\nconst widget = widgetState.widget\n\nconst emit = defineEmits<{\n 'update:widgetValue': [value: string | object]\n}>()\n\nconst widgetElement = ref<HTMLElement | undefined>()\n\n/**\n * @note Do NOT convert style to a computed value, as it will cause lag when\n * updating the style on different animation frames. Vue's computed value is\n * evaluated asynchronously.\n */\nconst style = ref<CSSProperties>({})\nconst { style: positionStyle, updatePosition } = useAbsolutePosition({\n useTransform: true\n})\nconst { style: clippingStyle, updateClipPath } = useDomClipping()\n\nconst canvasStore = useCanvasStore()\nconst settingStore = useSettingStore()\nconst enableDomClipping = computed(() =>\n settingStore.get('Comfy.DOMClippingEnabled')\n)\n\nconst updateDomClipping = () => {\n const lgCanvas = canvasStore.canvas\n if (!lgCanvas || !widgetElement.value) return\n\n const selectedNode = Object.values(lgCanvas.selected_nodes ?? {})[0]\n if (!selectedNode) {\n // Clear clipping when no node is selected\n updateClipPath(widgetElement.value, lgCanvas.canvas, false, undefined)\n return\n }\n\n const isSelected = selectedNode === widgetState.widget.node\n const renderArea = selectedNode?.renderArea\n const offset = lgCanvas.ds.offset\n const scale = lgCanvas.ds.scale\n const selectedAreaConfig = renderArea\n ? {\n x: renderArea[0],\n y: renderArea[1],\n width: renderArea[2],\n height: renderArea[3],\n scale,\n offset: [offset[0], offset[1]] as [number, number]\n }\n : undefined\n\n updateClipPath(\n widgetElement.value,\n lgCanvas.canvas,\n isSelected,\n selectedAreaConfig\n )\n}\n\n/**\n * @note mapping between canvas position and client position depends on the\n * canvas element's position, so we need to watch the canvas element's position\n * and update the position of the widget accordingly.\n */\nconst { left, top } = useElementBounding(canvasStore.getCanvas().canvas)\nwatch(\n [() => widgetState, left, top],\n ([widgetState, _, __]) => {\n updatePosition(widgetState)\n if (enableDomClipping.value) {\n updateDomClipping()\n }\n\n style.value = {\n ...positionStyle.value,\n ...(enableDomClipping.value ? clippingStyle.value : {}),\n zIndex: widgetState.zIndex,\n pointerEvents:\n widgetState.readonly || widget.computedDisabled ? 'none' : 'auto',\n opacity: widget.computedDisabled ? 0.5 : 1\n }\n },\n { deep: true }\n)\n\nwatch(\n () => widgetState.visible,\n (newVisible, oldVisible) => {\n if (!newVisible && oldVisible) {\n widget.options.onHide?.(widget)\n }\n }\n)\nuseEventListener(document, 'mousedown', (event) => {\n if (!isDOMWidget(widget) || !widgetState.visible || !widget.element.blur) {\n return\n }\n if (!widget.element.contains(event.target as HTMLElement)) {\n widget.element.blur()\n }\n})\n\nonMounted(() => {\n if (!isDOMWidget(widget)) {\n return\n }\n useEventListener(\n widget.element,\n widget.options.selectOn ?? ['focus', 'click'],\n () => {\n const lgCanvas = canvasStore.canvas\n lgCanvas?.selectNode(widgetState.widget.node)\n lgCanvas?.bringToFront(widgetState.widget.node)\n }\n )\n})\n\nconst inputSpec = widget.node.constructor.nodeData\nconst tooltip = inputSpec?.inputs?.[widget.name]?.tooltip\n\n// Mount DOM element when widget is or becomes visible\nconst mountElementIfVisible = () => {\n if (!(widgetState.visible && isDOMWidget(widget) && widgetElement.value)) {\n return\n }\n // Only append if not already a child\n if (widgetElement.value.contains(widget.element)) {\n return\n }\n widgetElement.value.appendChild(widget.element)\n}\n\n// Check on mount - but only after next tick to ensure visibility is calculated\nonMounted(() => {\n nextTick(() => {\n mountElementIfVisible()\n }).catch((error) => {\n console.error('Error mounting DOM widget element:', error)\n })\n})\n\n// And watch for visibility changes\nwatch(\n () => widgetState.visible,\n () => {\n mountElementIfVisible()\n }\n)\n</script>\n\n<style scoped>\n@reference '../../../assets/css/style.css';\n\n.dom-widget > * {\n @apply h-full w-full;\n}\n</style>\n","<template>\n <!-- Create a new stacking context for widgets to avoid z-index issues -->\n <div class=\"isolate\">\n <DomWidget\n v-for=\"widgetState in widgetStates\"\n :key=\"widgetState.widget.id\"\n :widget-state=\"widgetState\"\n @update:widget-value=\"widgetState.widget.value = $event\"\n />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { whenever } from '@vueuse/core'\nimport { computed } from 'vue'\n\nimport DomWidget from '@/components/graph/widgets/DomWidget.vue'\nimport { useChainCallback } from '@/composables/functional/useChainCallback'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useDomWidgetStore } from '@/stores/domWidgetStore'\n\nconst domWidgetStore = useDomWidgetStore()\n\nconst widgetStates = computed(() => [...domWidgetStore.widgetStates.values()])\n\nconst updateWidgets = () => {\n const lgCanvas = canvasStore.canvas\n if (!lgCanvas) return\n\n const lowQuality = lgCanvas.low_quality\n const currentGraph = lgCanvas.graph\n\n for (const widgetState of widgetStates.value) {\n const widget = widgetState.widget\n\n // Early exit for non-visible widgets\n if (!widget.isVisible() || !widgetState.active) {\n widgetState.visible = false\n continue\n }\n\n // Check if the widget's node is in the current graph\n const node = widget.node\n const isInCorrectGraph = currentGraph?.nodes.includes(node)\n\n widgetState.visible =\n !!isInCorrectGraph &&\n lgCanvas.isNodeVisible(node) &&\n !(widget.options.hideOnZoom && lowQuality)\n\n if (widgetState.visible && node) {\n const margin = widget.margin\n widgetState.pos = [node.pos[0] + margin, node.pos[1] + margin + widget.y]\n widgetState.size = [\n (widget.width ?? node.width) - margin * 2,\n (widget.computedHeight ?? 50) - margin * 2\n ]\n // TODO: optimize this logic as it's O(n), where n is the number of nodes\n widgetState.zIndex = lgCanvas.graph?.nodes.indexOf(node) ?? -1\n widgetState.readonly = lgCanvas.read_only\n }\n }\n}\n\nconst canvasStore = useCanvasStore()\nwhenever(\n () => canvasStore.canvas,\n (canvas) =>\n (canvas.onDrawForeground = useChainCallback(\n canvas.onDrawForeground,\n updateWidgets\n )),\n { immediate: true }\n)\n</script>\n","import { computed, ref } from 'vue'\n\nexport function useZoomControls() {\n const isModalVisible = ref(false)\n\n const showModal = () => {\n isModalVisible.value = true\n }\n\n const hideModal = () => {\n isModalVisible.value = false\n }\n\n const toggleModal = () => {\n isModalVisible.value = !isModalVisible.value\n }\n\n const hasActivePopup = computed(() => isModalVisible.value)\n\n return {\n isModalVisible,\n showModal,\n hideModal,\n toggleModal,\n hasActivePopup\n }\n}\n","import { useThrottleFn } from '@vueuse/core'\nimport { ref, watch } from 'vue'\nimport type { Ref } from 'vue'\n\nimport type {\n LGraph,\n LGraphNode,\n LGraphTriggerEvent\n} from '@/lib/litegraph/src/litegraph'\nimport type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema'\nimport { layoutStore } from '@/renderer/core/layout/store/layoutStore'\nimport { api } from '@/scripts/api'\n\nimport { MinimapDataSourceFactory } from '../data/MinimapDataSourceFactory'\nimport type { UpdateFlags } from '../types'\n\ninterface GraphCallbacks {\n onNodeAdded?: (node: LGraphNode) => void\n onNodeRemoved?: (node: LGraphNode) => void\n onConnectionChange?: (node: LGraphNode) => void\n onTrigger?: (event: LGraphTriggerEvent) => void\n}\n\nexport function useMinimapGraph(\n graph: Ref<LGraph | null>,\n onGraphChanged: () => void\n) {\n const nodeStatesCache = new Map<NodeId, string>()\n const linksCache = ref<string>('')\n const lastNodeCount = ref(0)\n const updateFlags = ref<UpdateFlags>({\n bounds: false,\n nodes: false,\n connections: false,\n viewport: false\n })\n\n // Track LayoutStore version for change detection\n const layoutStoreVersion = layoutStore.getVersion()\n\n // Map to store original callbacks per graph ID\n const originalCallbacksMap = new Map<string, GraphCallbacks>()\n\n const handleGraphChangedThrottled = useThrottleFn(() => {\n onGraphChanged()\n }, 500)\n\n const setupEventListeners = () => {\n const g = graph.value\n if (!g) return\n\n // Check if we've already wrapped this graph's callbacks\n if (originalCallbacksMap.has(g.id)) {\n return\n }\n\n // Store the original callbacks for this graph\n const originalCallbacks: GraphCallbacks = {\n onNodeAdded: g.onNodeAdded,\n onNodeRemoved: g.onNodeRemoved,\n onConnectionChange: g.onConnectionChange,\n onTrigger: g.onTrigger\n }\n originalCallbacksMap.set(g.id, originalCallbacks)\n\n g.onNodeAdded = function (node: LGraphNode) {\n originalCallbacks.onNodeAdded?.call(this, node)\n void handleGraphChangedThrottled()\n }\n\n g.onNodeRemoved = function (node: LGraphNode) {\n originalCallbacks.onNodeRemoved?.call(this, node)\n nodeStatesCache.delete(node.id)\n void handleGraphChangedThrottled()\n }\n\n g.onConnectionChange = function (node: LGraphNode) {\n originalCallbacks.onConnectionChange?.call(this, node)\n void handleGraphChangedThrottled()\n }\n\n g.onTrigger = function (event: LGraphTriggerEvent) {\n originalCallbacks.onTrigger?.call(this, event)\n\n // Listen for visual property changes that affect minimap rendering\n if (\n event.type === 'node:property:changed' &&\n (event.property === 'mode' ||\n event.property === 'bgcolor' ||\n event.property === 'color')\n ) {\n // Invalidate cache for this node to force redraw\n nodeStatesCache.delete(String(event.nodeId))\n void handleGraphChangedThrottled()\n }\n }\n }\n\n const cleanupEventListeners = (oldGraph?: LGraph) => {\n const g = oldGraph || graph.value\n if (!g) return\n\n const originalCallbacks = originalCallbacksMap.get(g.id)\n if (!originalCallbacks) {\n console.error(\n 'Attempted to cleanup event listeners for graph that was never set up'\n )\n return\n }\n\n g.onNodeAdded = originalCallbacks.onNodeAdded\n g.onNodeRemoved = originalCallbacks.onNodeRemoved\n g.onConnectionChange = originalCallbacks.onConnectionChange\n g.onTrigger = originalCallbacks.onTrigger\n\n originalCallbacksMap.delete(g.id)\n }\n\n const checkForChangesInternal = () => {\n const g = graph.value\n if (!g) return false\n\n let structureChanged = false\n let positionChanged = false\n let connectionChanged = false\n\n // Use unified data source for change detection\n const dataSource = MinimapDataSourceFactory.create(g)\n\n // Check for node count changes\n const currentNodeCount = dataSource.getNodeCount()\n if (currentNodeCount !== lastNodeCount.value) {\n structureChanged = true\n lastNodeCount.value = currentNodeCount\n }\n\n // Check for node position/size changes\n const nodes = dataSource.getNodes()\n for (const node of nodes) {\n const nodeId = node.id\n const currentState = `${node.x},${node.y},${node.width},${node.height}`\n\n if (nodeStatesCache.get(nodeId) !== currentState) {\n positionChanged = true\n nodeStatesCache.set(nodeId, currentState)\n }\n }\n\n // Clean up removed nodes from cache\n const currentNodeIds = new Set(nodes.map((n) => n.id))\n for (const [nodeId] of nodeStatesCache) {\n if (!currentNodeIds.has(nodeId)) {\n nodeStatesCache.delete(nodeId)\n structureChanged = true\n }\n }\n\n // TODO: update when Layoutstore tracks links\n const currentLinks = JSON.stringify(g.links || {})\n if (currentLinks !== linksCache.value) {\n connectionChanged = true\n linksCache.value = currentLinks\n }\n\n if (structureChanged || positionChanged) {\n updateFlags.value.bounds = true\n updateFlags.value.nodes = true\n }\n\n if (connectionChanged) {\n updateFlags.value.connections = true\n }\n\n return structureChanged || positionChanged || connectionChanged\n }\n\n const init = () => {\n setupEventListeners()\n api.addEventListener('graphChanged', handleGraphChangedThrottled)\n\n watch(layoutStoreVersion, () => {\n void handleGraphChangedThrottled()\n })\n }\n\n const destroy = () => {\n cleanupEventListeners()\n api.removeEventListener('graphChanged', handleGraphChangedThrottled)\n nodeStatesCache.clear()\n }\n\n const clearCache = () => {\n nodeStatesCache.clear()\n linksCache.value = ''\n lastNodeCount.value = 0\n }\n\n return {\n updateFlags,\n setupEventListeners,\n cleanupEventListeners,\n checkForChanges: checkForChangesInternal,\n init,\n destroy,\n clearCache\n }\n}\n","import { ref } from 'vue'\nimport type { Ref, ShallowRef } from 'vue'\n\nimport type { MinimapCanvas } from '../types'\n\nexport function useMinimapInteraction(\n containerRef: Readonly<ShallowRef<HTMLDivElement | null>>,\n bounds: Ref<{ minX: number; minY: number; width: number; height: number }>,\n scale: Ref<number>,\n width: number,\n height: number,\n centerViewOn: (worldX: number, worldY: number) => void,\n canvas: Ref<MinimapCanvas | null>\n) {\n const isDragging = ref(false)\n const containerRect = ref({\n left: 0,\n top: 0,\n width: width,\n height: height\n })\n\n const updateContainerRect = () => {\n if (!containerRef.value) return\n\n const rect = containerRef.value.getBoundingClientRect()\n containerRect.value = {\n left: rect.left,\n top: rect.top,\n width: rect.width,\n height: rect.height\n }\n }\n\n const handlePointerDown = (e: PointerEvent) => {\n isDragging.value = true\n updateContainerRect()\n const target = e.currentTarget\n if (target instanceof HTMLElement) {\n target.setPointerCapture(e.pointerId)\n }\n handlePointerMove(e)\n }\n\n const handlePointerMove = (e: PointerEvent) => {\n if (!isDragging.value || !canvas.value) return\n\n const x = e.clientX - containerRect.value.left\n const y = e.clientY - containerRect.value.top\n\n const offsetX = (width - bounds.value.width * scale.value) / 2\n const offsetY = (height - bounds.value.height * scale.value) / 2\n\n const worldX = (x - offsetX) / scale.value + bounds.value.minX\n const worldY = (y - offsetY) / scale.value + bounds.value.minY\n\n centerViewOn(worldX, worldY)\n }\n\n const releasePointer = (e?: PointerEvent) => {\n isDragging.value = false\n if (!e) return\n\n const target = e.currentTarget\n if (\n target instanceof HTMLElement &&\n target.hasPointerCapture(e.pointerId)\n ) {\n target.releasePointerCapture(e.pointerId)\n }\n }\n\n const handlePointerUp = releasePointer\n\n const handlePointerCancel = releasePointer\n\n const handleWheel = (e: WheelEvent) => {\n e.preventDefault()\n\n const c = canvas.value\n if (!c) return\n\n if (\n containerRect.value.left === 0 &&\n containerRect.value.top === 0 &&\n containerRef.value\n ) {\n updateContainerRect()\n }\n\n const ds = c.ds\n const delta = e.deltaY > 0 ? 0.9 : 1.1\n\n const newScale = ds.scale * delta\n\n const MIN_SCALE = 0.1\n const MAX_SCALE = 10\n\n if (newScale < MIN_SCALE || newScale > MAX_SCALE) return\n\n const x = e.clientX - containerRect.value.left\n const y = e.clientY - containerRect.value.top\n\n const offsetX = (width - bounds.value.width * scale.value) / 2\n const offsetY = (height - bounds.value.height * scale.value) / 2\n\n const worldX = (x - offsetX) / scale.value + bounds.value.minX\n const worldY = (y - offsetY) / scale.value + bounds.value.minY\n\n ds.scale = newScale\n\n centerViewOn(worldX, worldY)\n }\n\n return {\n isDragging,\n containerRect,\n updateContainerRect,\n handlePointerDown,\n handlePointerMove,\n handlePointerUp,\n handlePointerCancel,\n handleWheel\n }\n}\n","import { ref } from 'vue'\nimport type { Ref, ShallowRef } from 'vue'\n\nimport type { LGraph } from '@/lib/litegraph/src/litegraph'\n\nimport { renderMinimapToCanvas } from '../minimapCanvasRenderer'\nimport type { UpdateFlags } from '../types'\n\nexport function useMinimapRenderer(\n canvasRef: Readonly<ShallowRef<HTMLCanvasElement | null>>,\n graph: Ref<LGraph | null>,\n bounds: Ref<{ minX: number; minY: number; width: number; height: number }>,\n scale: Ref<number>,\n updateFlags: Ref<UpdateFlags>,\n settings: {\n nodeColors: Ref<boolean>\n showLinks: Ref<boolean>\n showGroups: Ref<boolean>\n renderBypass: Ref<boolean>\n renderError: Ref<boolean>\n },\n width: number,\n height: number\n) {\n const needsFullRedraw = ref(true)\n const needsBoundsUpdate = ref(true)\n\n const renderMinimap = () => {\n const g = graph.value\n if (!canvasRef.value || !g) return\n\n const ctx = canvasRef.value.getContext('2d')\n if (!ctx) return\n\n // Fast path for 0 nodes - just show background\n if (!g._nodes || g._nodes.length === 0) {\n ctx.clearRect(0, 0, width, height)\n return\n }\n\n const needsRedraw =\n needsFullRedraw.value ||\n updateFlags.value.nodes ||\n updateFlags.value.connections\n\n if (needsRedraw) {\n renderMinimapToCanvas(canvasRef.value, g, {\n bounds: bounds.value,\n scale: scale.value,\n settings: {\n nodeColors: settings.nodeColors.value,\n showLinks: settings.showLinks.value,\n showGroups: settings.showGroups.value,\n renderBypass: settings.renderBypass.value,\n renderError: settings.renderError.value\n },\n width,\n height\n })\n\n needsFullRedraw.value = false\n updateFlags.value.nodes = false\n updateFlags.value.connections = false\n }\n }\n\n const updateMinimap = (\n updateBounds: () => void,\n updateViewport: () => void\n ) => {\n if (needsBoundsUpdate.value || updateFlags.value.bounds) {\n updateBounds()\n needsBoundsUpdate.value = false\n updateFlags.value.bounds = false\n needsFullRedraw.value = true\n // When bounds change, we need to update the viewport position\n updateFlags.value.viewport = true\n }\n\n if (\n needsFullRedraw.value ||\n updateFlags.value.nodes ||\n updateFlags.value.connections\n ) {\n renderMinimap()\n }\n\n // Update viewport if needed (e.g., after bounds change)\n if (updateFlags.value.viewport) {\n updateViewport()\n updateFlags.value.viewport = false\n }\n }\n\n const forceFullRedraw = () => {\n needsFullRedraw.value = true\n updateFlags.value.bounds = true\n updateFlags.value.nodes = true\n updateFlags.value.connections = true\n updateFlags.value.viewport = true\n }\n\n return {\n needsFullRedraw,\n needsBoundsUpdate,\n renderMinimap,\n updateMinimap,\n forceFullRedraw\n }\n}\n","import { computed } from 'vue'\n\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'\n\n/**\n * Composable for minimap configuration options that are set by the user in the\n * settings. Provides reactive computed properties for the settings.\n */\nexport function useMinimapSettings() {\n const settingStore = useSettingStore()\n const colorPaletteStore = useColorPaletteStore()\n\n const nodeColors = computed(() =>\n settingStore.get('Comfy.Minimap.NodeColors')\n )\n const showLinks = computed(() => settingStore.get('Comfy.Minimap.ShowLinks'))\n const showGroups = computed(() =>\n settingStore.get('Comfy.Minimap.ShowGroups')\n )\n const renderBypass = computed(() =>\n settingStore.get('Comfy.Minimap.RenderBypassState')\n )\n const renderError = computed(() =>\n settingStore.get('Comfy.Minimap.RenderErrorState')\n )\n\n const width = 253\n const height = 200\n\n // Theme-aware colors\n const isLightTheme = computed(\n () => colorPaletteStore.completedActivePalette.light_theme\n )\n\n const containerStyles = computed(() => ({\n width: `${width}px`,\n height: `${height}px`,\n border: '1px solid var(--interface-stroke)',\n borderRadius: '8px'\n }))\n\n const panelStyles = computed(() => ({\n width: `210px`,\n height: `${height}px`,\n border: '1px solid var(--interface-stroke)',\n borderRadius: '8px'\n }))\n\n return {\n nodeColors,\n showLinks,\n showGroups,\n renderBypass,\n renderError,\n containerStyles,\n panelStyles,\n isLightTheme\n }\n}\n","import { useRafFn } from '@vueuse/core'\nimport { computed, ref } from 'vue'\nimport type { Ref } from 'vue'\n\nimport type { LGraph } from '@/lib/litegraph/src/litegraph'\nimport {\n calculateMinimapScale,\n enforceMinimumBounds\n} from '@/renderer/core/spatial/boundsCalculator'\nimport { MinimapDataSourceFactory } from '@/renderer/extensions/minimap/data/MinimapDataSourceFactory'\n\nimport type { MinimapBounds, MinimapCanvas, ViewportTransform } from '../types'\n\nexport function useMinimapViewport(\n canvas: Ref<MinimapCanvas | null>,\n graph: Ref<LGraph | null>,\n width: number,\n height: number\n) {\n const bounds = ref<MinimapBounds>({\n minX: 0,\n minY: 0,\n maxX: 0,\n maxY: 0,\n width: 0,\n height: 0\n })\n\n const scale = ref(1)\n const viewportTransform = ref<ViewportTransform>({\n x: 0,\n y: 0,\n width: 0,\n height: 0\n })\n\n const canvasDimensions = ref({\n width: 0,\n height: 0\n })\n\n const updateCanvasDimensions = () => {\n const c = canvas.value\n if (!c) return\n\n const canvasEl = c.canvas\n const dpr = window.devicePixelRatio || 1\n\n canvasDimensions.value = {\n width: canvasEl.clientWidth || canvasEl.width / dpr,\n height: canvasEl.clientHeight || canvasEl.height / dpr\n }\n }\n\n const calculateGraphBounds = (): MinimapBounds => {\n // Use unified data source\n const dataSource = MinimapDataSourceFactory.create(graph.value)\n\n if (!dataSource.hasData()) {\n return { minX: 0, minY: 0, maxX: 100, maxY: 100, width: 100, height: 100 }\n }\n\n const sourceBounds = dataSource.getBounds()\n return enforceMinimumBounds(sourceBounds)\n }\n\n const calculateScale = () => {\n return calculateMinimapScale(bounds.value, width, height)\n }\n\n const updateViewport = () => {\n const c = canvas.value\n if (!c) return\n\n if (\n canvasDimensions.value.width === 0 ||\n canvasDimensions.value.height === 0\n ) {\n updateCanvasDimensions()\n }\n\n const ds = c.ds\n\n const viewportWidth = canvasDimensions.value.width / ds.scale\n const viewportHeight = canvasDimensions.value.height / ds.scale\n\n const worldX = -ds.offset[0]\n const worldY = -ds.offset[1]\n\n const centerOffsetX = (width - bounds.value.width * scale.value) / 2\n const centerOffsetY = (height - bounds.value.height * scale.value) / 2\n\n viewportTransform.value = {\n x: (worldX - bounds.value.minX) * scale.value + centerOffsetX,\n y: (worldY - bounds.value.minY) * scale.value + centerOffsetY,\n width: viewportWidth * scale.value,\n height: viewportHeight * scale.value\n }\n }\n\n const updateBounds = () => {\n bounds.value = calculateGraphBounds()\n scale.value = calculateScale()\n }\n\n const centerViewOn = (worldX: number, worldY: number) => {\n const c = canvas.value\n if (!c) return\n\n if (\n canvasDimensions.value.width === 0 ||\n canvasDimensions.value.height === 0\n ) {\n updateCanvasDimensions()\n }\n\n const ds = c.ds\n\n const viewportWidth = canvasDimensions.value.width / ds.scale\n const viewportHeight = canvasDimensions.value.height / ds.scale\n\n ds.offset[0] = -(worldX - viewportWidth / 2)\n ds.offset[1] = -(worldY - viewportHeight / 2)\n\n c.setDirty(true, true)\n }\n const { resume: startViewportSync, pause: stopViewportSync } =\n useRafFn(updateViewport)\n\n return {\n bounds: computed(() => bounds.value),\n scale: computed(() => scale.value),\n viewportTransform: computed(() => viewportTransform.value),\n canvasDimensions: computed(() => canvasDimensions.value),\n updateCanvasDimensions,\n updateViewport,\n updateBounds,\n centerViewOn,\n startViewportSync,\n stopViewportSync\n }\n}\n","import { useRafFn } from '@vueuse/core'\nimport { computed, nextTick, ref, shallowRef, watch } from 'vue'\nimport type { ShallowRef } from 'vue'\n\nimport type { LGraph } from '@/lib/litegraph/src/litegraph'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\n\nimport type { MinimapCanvas, MinimapSettingsKey } from '../types'\nimport { useMinimapGraph } from './useMinimapGraph'\nimport { useMinimapInteraction } from './useMinimapInteraction'\nimport { useMinimapRenderer } from './useMinimapRenderer'\nimport { useMinimapSettings } from './useMinimapSettings'\nimport { useMinimapViewport } from './useMinimapViewport'\n\nexport function useMinimap({\n canvasRefMaybe,\n containerRefMaybe\n}: {\n canvasRefMaybe?: Readonly<ShallowRef<HTMLCanvasElement | null>>\n containerRefMaybe?: Readonly<ShallowRef<HTMLDivElement | null>>\n} = {}) {\n const canvasStore = useCanvasStore()\n const workflowStore = useWorkflowStore()\n const settingStore = useSettingStore()\n\n const minimapRef = ref<HTMLElement | null>(null)\n const canvasRef = canvasRefMaybe ?? shallowRef(null)\n const containerRef = containerRefMaybe ?? shallowRef(null)\n\n const visible = ref(true)\n const initialized = ref(false)\n\n const width = 250\n const height = 200\n\n const canvas = computed(() => canvasStore.canvas as MinimapCanvas | null)\n const graph = computed(() => {\n // If we're in a subgraph, use that; otherwise use the canvas graph\n const activeSubgraph = workflowStore.activeSubgraph\n return (activeSubgraph || canvas.value?.graph) as LGraph | null\n })\n\n // Settings\n const settings = useMinimapSettings()\n const {\n nodeColors,\n showLinks,\n showGroups,\n renderBypass,\n renderError,\n containerStyles,\n panelStyles\n } = settings\n\n const updateOption = async (key: MinimapSettingsKey, value: boolean) => {\n await settingStore.set(key, value)\n renderer.forceFullRedraw()\n renderer.updateMinimap(viewport.updateBounds, viewport.updateViewport)\n }\n\n // Viewport management\n const viewport = useMinimapViewport(canvas, graph, width, height)\n\n // Interaction handling\n const interaction = useMinimapInteraction(\n containerRef,\n viewport.bounds,\n viewport.scale,\n width,\n height,\n viewport.centerViewOn,\n canvas\n )\n\n // Graph event management\n const graphManager = useMinimapGraph(graph, () => {\n renderer.forceFullRedraw()\n renderer.updateMinimap(viewport.updateBounds, viewport.updateViewport)\n })\n\n // Rendering\n const renderer = useMinimapRenderer(\n canvasRef,\n graph,\n viewport.bounds,\n viewport.scale,\n graphManager.updateFlags,\n settings,\n width,\n height\n )\n\n // RAF loop for continuous updates\n const { pause: pauseChangeDetection, resume: resumeChangeDetection } =\n useRafFn(\n async () => {\n if (visible.value) {\n const hasChanges = await graphManager.checkForChanges()\n if (hasChanges) {\n renderer.updateMinimap(\n viewport.updateBounds,\n viewport.updateViewport\n )\n }\n }\n },\n { immediate: false }\n )\n\n const init = async () => {\n if (initialized.value) return\n\n visible.value = settingStore.get('Comfy.Minimap.Visible')\n\n if (canvas.value && graph.value) {\n graphManager.init()\n\n if (containerRef.value) {\n interaction.updateContainerRect()\n }\n viewport.updateCanvasDimensions()\n\n window.addEventListener('resize', interaction.updateContainerRect)\n window.addEventListener('scroll', interaction.updateContainerRect)\n window.addEventListener('resize', viewport.updateCanvasDimensions)\n\n renderer.forceFullRedraw()\n renderer.updateMinimap(viewport.updateBounds, viewport.updateViewport)\n viewport.updateViewport()\n\n if (visible.value) {\n resumeChangeDetection()\n viewport.startViewportSync()\n }\n initialized.value = true\n }\n }\n\n const destroy = () => {\n pauseChangeDetection()\n viewport.stopViewportSync()\n graphManager.destroy()\n\n window.removeEventListener('resize', interaction.updateContainerRect)\n window.removeEventListener('scroll', interaction.updateContainerRect)\n window.removeEventListener('resize', viewport.updateCanvasDimensions)\n\n initialized.value = false\n }\n\n watch(\n canvas,\n async (newCanvas, oldCanvas) => {\n if (oldCanvas) {\n graphManager.cleanupEventListeners()\n pauseChangeDetection()\n viewport.stopViewportSync()\n graphManager.destroy()\n window.removeEventListener('resize', interaction.updateContainerRect)\n window.removeEventListener('scroll', interaction.updateContainerRect)\n window.removeEventListener('resize', viewport.updateCanvasDimensions)\n }\n if (newCanvas && !initialized.value) {\n await init()\n }\n },\n { immediate: true, flush: 'post' }\n )\n\n // Watch for graph changes (e.g., when navigating to/from subgraphs)\n watch(graph, (newGraph, oldGraph) => {\n if (newGraph && newGraph !== oldGraph) {\n graphManager.cleanupEventListeners(oldGraph || undefined)\n graphManager.setupEventListeners()\n renderer.forceFullRedraw()\n renderer.updateMinimap(viewport.updateBounds, viewport.updateViewport)\n }\n })\n\n watch(visible, async (isVisible) => {\n if (isVisible) {\n if (containerRef.value) {\n interaction.updateContainerRect()\n }\n viewport.updateCanvasDimensions()\n\n renderer.forceFullRedraw()\n\n await nextTick()\n await nextTick()\n\n renderer.updateMinimap(viewport.updateBounds, viewport.updateViewport)\n viewport.updateViewport()\n resumeChangeDetection()\n viewport.startViewportSync()\n } else {\n pauseChangeDetection()\n viewport.stopViewportSync()\n }\n })\n\n const toggle = async () => {\n visible.value = !visible.value\n await settingStore.set('Comfy.Minimap.Visible', visible.value)\n }\n\n const setMinimapRef = (ref: HTMLElement | null) => {\n minimapRef.value = ref\n }\n\n // Dynamic viewport styles based on actual viewport transform\n const viewportStyles = computed(() => {\n const transform = viewport.viewportTransform.value\n return {\n transform: `translate(${transform.x}px, ${transform.y}px)`,\n width: `${transform.width}px`,\n height: `${transform.height}px`,\n border: `2px solid ${settings.isLightTheme.value ? '#E0E0E0' : '#FFF'}`,\n backgroundColor: `rgba(255, 255, 255, 0.2)`,\n willChange: 'transform',\n backfaceVisibility: 'hidden' as const,\n perspective: '1000px',\n pointerEvents: 'none' as const\n }\n })\n\n return {\n visible: computed(() => visible.value),\n initialized: computed(() => initialized.value),\n\n containerStyles,\n viewportStyles,\n panelStyles,\n width,\n height,\n\n nodeColors,\n showLinks,\n showGroups,\n renderBypass,\n renderError,\n\n init,\n destroy,\n toggle,\n renderMinimap: renderer.renderMinimap,\n handlePointerDown: interaction.handlePointerDown,\n handlePointerMove: interaction.handlePointerMove,\n handlePointerUp: interaction.handlePointerUp,\n handlePointerCancel: interaction.handlePointerCancel,\n handleWheel: interaction.handleWheel,\n setMinimapRef,\n updateOption\n }\n}\n","<template>\n <Button\n ref=\"buttonRef\"\n variant=\"secondary\"\n class=\"group h-8 rounded-none! bg-comfy-menu-bg p-0 transition-none! hover:rounded-lg! hover:bg-interface-button-hover-surface!\"\n :style=\"buttonStyles\"\n @click=\"toggle\"\n >\n <div class=\"flex items-center gap-1 pr-0.5\">\n <div\n class=\"rounded-lg bg-interface-panel-selected-surface p-2 group-hover:bg-interface-button-hover-surface\"\n >\n <i :class=\"currentModeIcon\" class=\"block h-4 w-4\" />\n </div>\n <i class=\"icon-[lucide--chevron-down] block h-4 w-4 pr-1.5\" />\n </div>\n </Button>\n\n <Popover\n ref=\"popover\"\n :auto-z-index=\"true\"\n :base-z-index=\"1000\"\n :dismissable=\"true\"\n :close-on-escape=\"true\"\n unstyled\n :pt=\"popoverPt\"\n >\n <div class=\"flex flex-col gap-1\">\n <div\n class=\"flex cursor-pointer items-center justify-between px-3 py-2 text-sm hover:bg-node-component-surface-hovered\"\n @click=\"setMode('select')\"\n >\n <div class=\"flex items-center gap-2\">\n <i class=\"icon-[lucide--mouse-pointer-2] h-4 w-4\" />\n <span>{{ $t('graphCanvasMenu.select') }}</span>\n </div>\n <span class=\"text-[9px] text-text-primary\">{{\n unlockCommandText\n }}</span>\n </div>\n\n <div\n class=\"flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm hover:bg-node-component-surface-hovered\"\n @click=\"setMode('hand')\"\n >\n <div class=\"flex items-center gap-2\">\n <i class=\"icon-[lucide--hand] h-4 w-4\" />\n <span>{{ $t('graphCanvasMenu.hand') }}</span>\n </div>\n <span class=\"text-[9px] text-text-primary\">{{ lockCommandText }}</span>\n </div>\n </div>\n </Popover>\n</template>\n\n<script setup lang=\"ts\">\nimport Popover from 'primevue/popover'\nimport { computed, ref } from 'vue'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useCommandStore } from '@/stores/commandStore'\n\ninterface Props {\n buttonStyles?: Record<string, string>\n}\n\ndefineProps<Props>()\nconst buttonRef = ref<InstanceType<typeof Button>>()\nconst popover = ref<InstanceType<typeof Popover>>()\nconst commandStore = useCommandStore()\nconst canvasStore = useCanvasStore()\n\nconst isCanvasReadOnly = computed(() => canvasStore.canvas?.read_only ?? false)\n\nconst currentModeIcon = computed(() =>\n isCanvasReadOnly.value\n ? 'icon-[lucide--hand]'\n : 'icon-[lucide--mouse-pointer-2]'\n)\n\nconst unlockCommandText = computed(() =>\n commandStore\n .formatKeySequence(commandStore.getCommand('Comfy.Canvas.Unlock'))\n .toUpperCase()\n)\n\nconst lockCommandText = computed(() =>\n commandStore\n .formatKeySequence(commandStore.getCommand('Comfy.Canvas.Lock'))\n .toUpperCase()\n)\n\nconst toggle = (event: Event) => {\n const el = (buttonRef.value as any)?.$el || buttonRef.value\n popover.value?.toggle(event, el)\n}\n\nconst setMode = (mode: 'select' | 'hand') => {\n if (mode === 'select' && isCanvasReadOnly.value) {\n void commandStore.execute('Comfy.Canvas.Unlock')\n } else if (mode === 'hand' && !isCanvasReadOnly.value) {\n void commandStore.execute('Comfy.Canvas.Lock')\n }\n popover.value?.hide()\n}\n\nconst popoverPt = computed(() => ({\n root: {\n class: 'absolute z-50 -translate-y-2'\n },\n content: {\n class: [\n 'mb-2 text-text-primary',\n 'shadow-lg border border-interface-stroke',\n 'bg-nav-background',\n 'rounded-lg',\n 'p-2 px-3',\n 'min-w-39',\n 'select-none'\n ]\n }\n}))\n</script>\n","<template>\n <div\n v-if=\"visible\"\n class=\"absolute right-0 bottom-[62px] z-1300 flex w-[250px] justify-center border-0! bg-inherit!\"\n >\n <div\n class=\"w-4/5 rounded-lg border border-interface-stroke bg-interface-panel-surface p-2 text-text-primary shadow-lg select-none\"\n :style=\"filteredMinimapStyles\"\n @click.stop\n >\n <div class=\"flex flex-col gap-1\">\n <div\n class=\"flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm hover:bg-node-component-surface-hovered\"\n @mousedown=\"startRepeat('Comfy.Canvas.ZoomIn')\"\n @mouseup=\"stopRepeat\"\n @mouseleave=\"stopRepeat\"\n >\n <span class=\"font-medium\">{{ $t('graphCanvasMenu.zoomIn') }}</span>\n <span class=\"text-[9px] text-text-primary\">{{\n zoomInCommandText\n }}</span>\n </div>\n\n <div\n class=\"flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm hover:bg-node-component-surface-hovered\"\n @mousedown=\"startRepeat('Comfy.Canvas.ZoomOut')\"\n @mouseup=\"stopRepeat\"\n @mouseleave=\"stopRepeat\"\n >\n <span class=\"font-medium\">{{ $t('graphCanvasMenu.zoomOut') }}</span>\n <span class=\"text-[9px] text-text-primary\">{{\n zoomOutCommandText\n }}</span>\n </div>\n\n <div\n class=\"flex cursor-pointer items-center justify-between rounded px-3 py-2 text-sm hover:bg-node-component-surface-hovered\"\n @click=\"executeCommand('Comfy.Canvas.FitView')\"\n >\n <span class=\"font-medium\">{{ $t('zoomControls.zoomToFit') }}</span>\n <span class=\"text-[9px] text-text-primary\">{{\n zoomToFitCommandText\n }}</span>\n </div>\n\n <div\n ref=\"zoomInputContainer\"\n class=\"zoomInputContainer flex items-center gap-1 rounded bg-input-surface p-2\"\n >\n <InputNumber\n :default-value=\"canvasStore.appScalePercentage\"\n :min=\"1\"\n :max=\"1000\"\n :show-buttons=\"false\"\n :use-grouping=\"false\"\n :unstyled=\"true\"\n input-class=\"bg-transparent border-none outline-hidden text-sm shadow-none my-0 w-full\"\n fluid\n @input=\"applyZoom\"\n @keyup.enter=\"applyZoom\"\n />\n <span class=\"flex-shrink-0 text-sm text-text-primary\">%</span>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport type { InputNumberInputEvent } from 'primevue'\nimport { InputNumber } from 'primevue'\nimport { computed, nextTick, ref, watch } from 'vue'\n\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'\nimport { useCommandStore } from '@/stores/commandStore'\n\nconst minimap = useMinimap()\nconst commandStore = useCommandStore()\nconst canvasStore = useCanvasStore()\nconst { formatKeySequence } = useCommandStore()\n\ninterface Props {\n visible: boolean\n}\n\nconst props = defineProps<Props>()\n\nconst interval = ref<number | null>(null)\n\nconst applyZoom = (val: InputNumberInputEvent) => {\n const inputValue = val.value as number\n if (isNaN(inputValue) || inputValue < 1 || inputValue > 1000) {\n return\n }\n canvasStore.setAppZoomFromPercentage(inputValue)\n}\n\nconst executeCommand = (command: string) => {\n void commandStore.execute(command)\n}\n\nconst startRepeat = (command: string) => {\n if (interval.value) return\n const cmd = () => commandStore.execute(command)\n void cmd()\n interval.value = window.setInterval(cmd, 100)\n}\n\nconst stopRepeat = () => {\n if (interval.value) {\n clearInterval(interval.value)\n interval.value = null\n }\n}\nconst filteredMinimapStyles = computed(() => {\n return {\n ...minimap.containerStyles.value,\n height: undefined,\n width: undefined\n }\n})\nconst zoomInCommandText = computed(() =>\n formatKeySequence(commandStore.getCommand('Comfy.Canvas.ZoomIn'))\n)\nconst zoomOutCommandText = computed(() =>\n formatKeySequence(commandStore.getCommand('Comfy.Canvas.ZoomOut'))\n)\nconst zoomToFitCommandText = computed(() =>\n formatKeySequence(commandStore.getCommand('Comfy.Canvas.FitView'))\n)\nconst zoomInputContainer = ref<HTMLDivElement | null>(null)\n\nwatch(\n () => props.visible,\n async (newVal) => {\n if (newVal) {\n await nextTick()\n const input = zoomInputContainer.value?.querySelector(\n 'input'\n ) as HTMLInputElement\n input?.focus()\n }\n }\n)\n</script>\n<style>\n.zoomInputContainer:focus-within {\n border: 1px solid var(--color-white);\n}\n</style>\n","<template>\n <div>\n <ZoomControlsModal :visible=\"isModalVisible\" @close=\"hideModal\" />\n\n <!-- Backdrop -->\n <div\n v-if=\"hasActivePopup\"\n class=\"fixed inset-0 z-1200\"\n @click=\"hideModal\"\n ></div>\n\n <ButtonGroup\n class=\"absolute right-0 bottom-0 z-1200 flex-row gap-1 border-[1px] border-interface-stroke bg-comfy-menu-bg p-2\"\n :style=\"{\n ...stringifiedMinimapStyles.buttonGroupStyles\n }\"\n @wheel=\"canvasInteractions.handleWheel\"\n >\n <CanvasModeSelector\n :button-styles=\"stringifiedMinimapStyles.buttonStyles\"\n />\n\n <div class=\"h-[27px] w-[1px] self-center bg-node-divider\" />\n\n <Button\n v-tooltip.top=\"fitViewTooltip\"\n variant=\"secondary\"\n :aria-label=\"fitViewTooltip\"\n :style=\"stringifiedMinimapStyles.buttonStyles\"\n class=\"h-8 w-8 bg-comfy-menu-bg p-0 hover:bg-interface-button-hover-surface!\"\n @click=\"() => commandStore.execute('Comfy.Canvas.FitView')\"\n >\n <i class=\"icon-[lucide--focus] h-4 w-4\" />\n </Button>\n\n <Button\n v-tooltip.top=\"t('zoomControls.label')\"\n variant=\"secondary\"\n :class=\"zoomButtonClass\"\n :aria-label=\"t('zoomControls.label')\"\n data-testid=\"zoom-controls-button\"\n :style=\"stringifiedMinimapStyles.buttonStyles\"\n @click=\"toggleModal\"\n >\n <span class=\"inline-flex items-center gap-1 px-2 text-xs\">\n <span>{{ canvasStore.appScalePercentage }}%</span>\n <i class=\"icon-[lucide--chevron-down] h-4 w-4\" />\n </span>\n </Button>\n\n <div class=\"h-[27px] w-[1px] self-center bg-node-divider\" />\n\n <Button\n v-tooltip.top=\"minimapTooltip\"\n variant=\"secondary\"\n :aria-label=\"minimapTooltip\"\n data-testid=\"toggle-minimap-button\"\n :style=\"stringifiedMinimapStyles.buttonStyles\"\n :class=\"minimapButtonClass\"\n @click=\"onMinimapToggleClick\"\n >\n <i class=\"icon-[lucide--map] h-4 w-4\" />\n </Button>\n\n <Button\n v-tooltip.top=\"{\n value: linkVisibilityTooltip,\n pt: {\n root: {\n style: 'z-index: 2; transform: translateY(-20px);'\n }\n }\n }\"\n variant=\"secondary\"\n :class=\"linkVisibleClass\"\n :aria-label=\"linkVisibilityAriaLabel\"\n data-testid=\"toggle-link-visibility-button\"\n :style=\"stringifiedMinimapStyles.buttonStyles\"\n @click=\"onLinkVisibilityToggleClick\"\n >\n <i class=\"icon-[lucide--route-off] h-4 w-4\" />\n </Button>\n </ButtonGroup>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport ButtonGroup from 'primevue/buttongroup'\nimport { computed, onBeforeUnmount, onMounted } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { useZoomControls } from '@/composables/useZoomControls'\nimport { LiteGraph } from '@/lib/litegraph/src/litegraph'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'\nimport { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'\nimport { useCommandStore } from '@/stores/commandStore'\n\nimport CanvasModeSelector from './CanvasModeSelector.vue'\nimport ZoomControlsModal from './modals/ZoomControlsModal.vue'\n\nconst { t } = useI18n()\nconst commandStore = useCommandStore()\nconst { formatKeySequence } = useCommandStore()\nconst canvasStore = useCanvasStore()\nconst settingStore = useSettingStore()\nconst canvasInteractions = useCanvasInteractions()\nconst minimap = useMinimap()\n\nconst { isModalVisible, toggleModal, hideModal, hasActivePopup } =\n useZoomControls()\n\nconst stringifiedMinimapStyles = computed(() => {\n const buttonGroupKeys = ['borderRadius']\n const buttonKeys = ['borderRadius']\n const additionalButtonStyles = {\n border: 'none'\n }\n\n const containerStyles = minimap.containerStyles.value\n\n const buttonStyles = {\n ...Object.fromEntries(\n Object.entries(containerStyles).filter(([key]) =>\n buttonKeys.includes(key)\n )\n ),\n ...additionalButtonStyles\n }\n const buttonGroupStyles = Object.entries(containerStyles)\n .filter(([key]) => buttonGroupKeys.includes(key))\n .reduce((acc, [key, value]) => ({ ...acc, [key]: value }), {})\n\n return { buttonStyles, buttonGroupStyles }\n})\n\n// Computed properties for reactive states\nconst linkHidden = computed(\n () => settingStore.get('Comfy.LinkRenderMode') === LiteGraph.HIDDEN_LINK\n)\n\n// Computed properties for command text\nconst fitViewCommandText = computed(() =>\n formatKeySequence(\n commandStore.getCommand('Comfy.Canvas.FitView')\n ).toUpperCase()\n)\nconst minimapCommandText = computed(() =>\n formatKeySequence(\n commandStore.getCommand('Comfy.Canvas.ToggleMinimap')\n ).toUpperCase()\n)\n\n// Computed properties for button classes and states\nconst zoomButtonClass = computed(() => [\n 'bg-comfy-menu-bg',\n isModalVisible.value ? 'not-active:bg-interface-panel-selected-surface!' : '',\n 'hover:bg-interface-button-hover-surface!',\n 'p-0',\n 'h-8',\n 'w-15'\n])\n\nconst minimapButtonClass = computed(() => ({\n 'bg-comfy-menu-bg': true,\n 'hover:bg-interface-button-hover-surface!': true,\n 'not-active:bg-interface-panel-selected-surface!': settingStore.get(\n 'Comfy.Minimap.Visible'\n ),\n 'p-0': true,\n 'w-8': true,\n 'h-8': true\n}))\n\n// Computed properties for tooltip and aria-label texts\nconst fitViewTooltip = computed(() => {\n const label = t('graphCanvasMenu.fitView')\n const shortcut = fitViewCommandText.value\n return shortcut ? `${label} (${shortcut})` : label\n})\nconst minimapTooltip = computed(() => {\n const label = settingStore.get('Comfy.Minimap.Visible')\n ? t('zoomControls.hideMinimap')\n : t('zoomControls.showMinimap')\n const shortcut = minimapCommandText.value\n return shortcut ? `${label} (${shortcut})` : label\n})\nconst linkVisibilityTooltip = computed(() =>\n linkHidden.value\n ? t('graphCanvasMenu.showLinks')\n : t('graphCanvasMenu.hideLinks')\n)\nconst linkVisibilityAriaLabel = computed(() =>\n linkHidden.value\n ? t('graphCanvasMenu.showLinks')\n : t('graphCanvasMenu.hideLinks')\n)\nconst linkVisibleClass = computed(() => [\n 'bg-comfy-menu-bg',\n linkHidden.value ? 'not-active:bg-interface-panel-selected-surface!' : '',\n 'hover:bg-interface-button-hover-surface!',\n 'p-0',\n 'w-8',\n 'h-8'\n])\n\nonMounted(() => {\n canvasStore.initScaleSync()\n})\n\n/**\n * Track minimap toggle button click and execute the command.\n */\nconst onMinimapToggleClick = () => {\n useTelemetry()?.trackUiButtonClicked({\n button_id: 'graph_menu_minimap_toggle_clicked'\n })\n void commandStore.execute('Comfy.Canvas.ToggleMinimap')\n}\n\n/**\n * Track hide/show links button click and execute the command.\n */\nconst onLinkVisibilityToggleClick = () => {\n useTelemetry()?.trackUiButtonClicked({\n button_id: 'graph_menu_hide_links_toggle_clicked'\n })\n void commandStore.execute('Comfy.Canvas.ToggleLinkVisibility')\n}\n\nonBeforeUnmount(() => {\n canvasStore.cleanupScaleSync()\n})\n</script>\n","<template>\n <div\n v-if=\"tooltipText\"\n ref=\"tooltipRef\"\n class=\"node-tooltip\"\n :style=\"{ left, top }\"\n >\n {{ tooltipText }}\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useEventListener } from '@vueuse/core'\nimport { nextTick, ref } from 'vue'\n\nimport { st } from '@/i18n'\nimport {\n LiteGraph,\n isOverNodeInput,\n isOverNodeOutput\n} from '@/lib/litegraph/src/litegraph'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { app as comfyApp } from '@/scripts/app'\nimport { isDOMWidget } from '@/scripts/domWidget'\nimport { useNodeDefStore } from '@/stores/nodeDefStore'\nimport { normalizeI18nKey } from '@/utils/formatUtil'\n\nlet idleTimeout: number\nconst nodeDefStore = useNodeDefStore()\nconst settingStore = useSettingStore()\nconst tooltipRef = ref<HTMLDivElement | undefined>()\nconst tooltipText = ref('')\nconst left = ref<string>()\nconst top = ref<string>()\n\nfunction hideTooltip() {\n return (tooltipText.value = '')\n}\n\nasync function showTooltip(tooltip: string | null | undefined) {\n if (!tooltip) return\n\n left.value = comfyApp.canvas.mouse[0] + 'px'\n top.value = comfyApp.canvas.mouse[1] + 'px'\n tooltipText.value = tooltip\n\n await nextTick()\n\n const rect = tooltipRef.value?.getBoundingClientRect()\n if (!rect) return\n\n if (rect.right > window.innerWidth) {\n left.value = comfyApp.canvas.mouse[0] - rect.width + 'px'\n }\n\n if (rect.top < 0) {\n top.value = comfyApp.canvas.mouse[1] + rect.height + 'px'\n }\n}\n\nfunction onIdle() {\n const { canvas } = comfyApp\n const node = canvas?.node_over\n if (!node) return\n\n const ctor = node.constructor as { title_mode?: 0 | 1 | 2 | 3 }\n const nodeDef = nodeDefStore.nodeDefsByName[node.type ?? '']\n\n if (\n ctor.title_mode !== LiteGraph.NO_TITLE &&\n canvas.graph_mouse[1] < node.pos[1] // If we are over a node, but not within the node then we are on its title\n ) {\n return showTooltip(nodeDef?.description)\n }\n\n if (node.flags?.collapsed) return\n\n const inputSlot = isOverNodeInput(\n node,\n canvas.graph_mouse[0],\n canvas.graph_mouse[1],\n [0, 0]\n )\n if (inputSlot !== -1) {\n const inputName = node.inputs[inputSlot].name\n const translatedTooltip = st(\n `nodeDefs.${normalizeI18nKey(node.type ?? '')}.inputs.${normalizeI18nKey(inputName)}.tooltip`,\n nodeDef?.inputs[inputName]?.tooltip ?? ''\n )\n return showTooltip(translatedTooltip)\n }\n\n const outputSlot = isOverNodeOutput(\n node,\n canvas.graph_mouse[0],\n canvas.graph_mouse[1],\n [0, 0]\n )\n if (outputSlot !== -1) {\n const translatedTooltip = st(\n `nodeDefs.${normalizeI18nKey(node.type ?? '')}.outputs.${outputSlot}.tooltip`,\n nodeDef?.outputs[outputSlot]?.tooltip ?? ''\n )\n return showTooltip(translatedTooltip)\n }\n\n const widget = comfyApp.canvas.getWidgetAtCursor()\n // Dont show for DOM widgets, these use native browser tooltips as we dont get proper mouse events on these\n if (widget && !isDOMWidget(widget)) {\n const translatedTooltip = st(\n `nodeDefs.${normalizeI18nKey(node.type ?? '')}.inputs.${normalizeI18nKey(widget.name)}.tooltip`,\n nodeDef?.inputs[widget.name]?.tooltip ?? ''\n )\n // Widget tooltip can be set dynamically, current translation collection does not support this.\n return showTooltip(widget.tooltip ?? translatedTooltip)\n }\n}\n\nconst onMouseMove = (e: MouseEvent) => {\n hideTooltip()\n clearTimeout(idleTimeout)\n\n if ((e.target as Node).nodeName !== 'CANVAS') return\n idleTimeout = window.setTimeout(\n onIdle,\n settingStore.get('LiteGraph.Node.TooltipDelay')\n )\n}\n\nuseEventListener(window, 'mousemove', onMouseMove)\nuseEventListener(window, 'click', hideTooltip)\n</script>\n\n<style lang=\"css\" scoped>\n.node-tooltip {\n pointer-events: none;\n background: var(--comfy-input-bg);\n border-radius: 5px;\n box-shadow: 0 0 5px rgb(0 0 0 / 0.4);\n color: var(--input-text);\n left: 0;\n max-width: 30vw;\n padding: 4px 8px;\n position: absolute;\n top: 0;\n transform: translate(5px, calc(-100% - 5px));\n white-space: pre-wrap;\n z-index: 99999;\n}\n</style>\n","<template>\n <Button\n v-tooltip.top=\"{\n value: $t('commands.Comfy_Canvas_ToggleSelectedNodes_Bypass.label'),\n showDelay: 1000\n }\"\n variant=\"muted-textonly\"\n :aria-label=\"$t('commands.Comfy_Canvas_ToggleSelectedNodes_Bypass.label')\"\n data-testid=\"bypass-button\"\n class=\"hover:bg-secondary-background\"\n @click=\"toggleBypass\"\n >\n <i class=\"icon-[lucide--redo-dot] size-4\" />\n </Button>\n</template>\n\n<script setup lang=\"ts\">\nimport Button from '@/components/ui/button/Button.vue'\nimport { useCommandStore } from '@/stores/commandStore'\n\nconst commandStore = useCommandStore()\n\nconst toggleBypass = async () => {\n await commandStore.execute('Comfy.Canvas.ToggleSelectedNodes.Bypass')\n}\n</script>\n","<template>\n <div class=\"relative\">\n <Button\n v-tooltip.top=\"{\n value: localizedCurrentColorName ?? t('color.noColor'),\n showDelay: 1000\n }\"\n data-testid=\"color-picker-button\"\n variant=\"muted-textonly\"\n :aria-label=\"t('g.color')\"\n @click=\"() => (showColorPicker = !showColorPicker)\"\n >\n <div class=\"flex items-center gap-1 px-0\">\n <i class=\"pi pi-circle-fill\" :style=\"{ color: currentColor ?? '' }\" />\n <i class=\"icon-[lucide--chevron-down]\" />\n </div>\n </Button>\n <div\n v-if=\"showColorPicker\"\n class=\"color-picker-container absolute -top-10 left-1/2\"\n >\n <SelectButton\n :model-value=\"selectedColorOption\"\n :options=\"colorOptions\"\n option-label=\"name\"\n data-key=\"value\"\n @update:model-value=\"applyColor\"\n >\n <template #option=\"{ option }\">\n <i\n v-tooltip.top=\"option.localizedName\"\n class=\"pi pi-circle-fill\"\n :style=\"{\n color: isLightTheme ? option.value.light : option.value.dark\n }\"\n :data-testid=\"option.name\"\n />\n </template>\n </SelectButton>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport SelectButton from 'primevue/selectbutton'\nimport type { Raw } from 'vue'\nimport { computed, ref, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport type {\n ColorOption as CanvasColorOption,\n Positionable\n} from '@/lib/litegraph/src/litegraph'\nimport {\n LGraphCanvas,\n LiteGraph,\n isColorable\n} from '@/lib/litegraph/src/litegraph'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'\nimport { adjustColor } from '@/utils/colorUtil'\nimport { getItemsColorOption } from '@/utils/litegraphUtil'\n\nconst { t } = useI18n()\nconst canvasStore = useCanvasStore()\nconst colorPaletteStore = useColorPaletteStore()\nconst workflowStore = useWorkflowStore()\nconst isLightTheme = computed(\n () => colorPaletteStore.completedActivePalette.light_theme\n)\nconst toLightThemeColor = (color: string) =>\n adjustColor(color, { lightness: 0.5 })\n\nconst showColorPicker = ref(false)\n\ntype ColorOption = {\n name: string\n localizedName: string\n value: {\n dark: string\n light: string\n }\n}\n\nconst NO_COLOR_OPTION: ColorOption = {\n name: 'noColor',\n localizedName: t('color.noColor'),\n value: {\n dark: LiteGraph.NODE_DEFAULT_BGCOLOR,\n light: toLightThemeColor(LiteGraph.NODE_DEFAULT_BGCOLOR)\n }\n}\nconst colorOptions: ColorOption[] = [\n NO_COLOR_OPTION,\n ...Object.entries(LGraphCanvas.node_colors).map(([name, color]) => ({\n name,\n localizedName: t(`color.${name}`),\n value: {\n dark: color.bgcolor,\n light: toLightThemeColor(color.bgcolor)\n }\n }))\n]\n\nconst selectedColorOption = ref<ColorOption | null>(null)\nconst applyColor = (colorOption: ColorOption | null) => {\n const colorName = colorOption?.name ?? NO_COLOR_OPTION.name\n const canvasColorOption =\n colorName === NO_COLOR_OPTION.name\n ? null\n : LGraphCanvas.node_colors[colorName]\n\n for (const item of canvasStore.selectedItems) {\n if (isColorable(item)) {\n item.setColorOption(canvasColorOption)\n }\n }\n\n canvasStore.canvas?.setDirty(true, true)\n currentColorOption.value = canvasColorOption\n showColorPicker.value = false\n workflowStore.activeWorkflow?.changeTracker.checkState()\n}\n\nconst currentColorOption = ref<CanvasColorOption | null>(null)\nconst currentColor = computed(() =>\n currentColorOption.value\n ? isLightTheme.value\n ? toLightThemeColor(currentColorOption.value?.bgcolor)\n : currentColorOption.value?.bgcolor\n : null\n)\n\nconst localizedCurrentColorName = computed(() => {\n if (!currentColorOption.value?.bgcolor) return null\n const colorOption = colorOptions.find(\n (option) =>\n option.value.dark === currentColorOption.value?.bgcolor ||\n option.value.light === currentColorOption.value?.bgcolor\n )\n return colorOption?.localizedName ?? NO_COLOR_OPTION.localizedName\n})\nconst updateColorSelectionFromNode = (\n newSelectedItems: Raw<Positionable[]>\n) => {\n showColorPicker.value = false\n selectedColorOption.value = null\n currentColorOption.value = getItemsColorOption(newSelectedItems)\n}\nwatch(\n () => canvasStore.selectedItems,\n (newSelectedItems) => {\n updateColorSelectionFromNode(newSelectedItems)\n },\n { immediate: true }\n)\n</script>\n\n<style scoped>\n@reference '../../../assets/css/style.css';\n\n.color-picker-container {\n transform: translateX(-50%);\n}\n\n:deep(.p-togglebutton) {\n @apply py-2 px-1;\n}\n</style>\n","<template>\n <Button\n v-tooltip.top=\"{\n value: $t('commands.Comfy_Graph_EditSubgraphWidgets.label'),\n showDelay: 1000\n }\"\n variant=\"muted-textonly\"\n :aria-label=\"$t('commands.Comfy_Graph_EditSubgraphWidgets.label')\"\n @click=\"handleClick\"\n >\n <i class=\"icon-[lucide--settings-2]\" />\n </Button>\n</template>\n<script setup lang=\"ts\">\nimport Button from '@/components/ui/button/Button.vue'\nimport { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'\n\nconst rightSidePanelStore = useRightSidePanelStore()\n\nconst handleClick = () => {\n rightSidePanelStore.openPanel('subgraph')\n}\n</script>\n","import { storeToRefs } from 'pinia'\nimport { computed } from 'vue'\n\nimport { useNodeLibrarySidebarTab } from '@/composables/sidebarTabs/useNodeLibrarySidebarTab'\nimport type { LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport { LGraphEventMode, SubgraphNode } from '@/lib/litegraph/src/litegraph'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useNodeDefStore } from '@/stores/nodeDefStore'\nimport { useNodeHelpStore } from '@/stores/workspace/nodeHelpStore'\nimport { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'\nimport { isImageNode, isLGraphNode, isLoad3dNode } from '@/utils/litegraphUtil'\nimport { filterOutputNodes } from '@/utils/nodeFilterUtil'\n\nexport interface NodeSelectionState {\n collapsed: boolean\n pinned: boolean\n bypassed: boolean\n}\n\n/**\n * Centralized computed selection state + shared helper actions to avoid duplication\n * between selection toolbox, context menus, and other UI affordances.\n */\nexport function useSelectionState() {\n const canvasStore = useCanvasStore()\n const nodeDefStore = useNodeDefStore()\n const sidebarTabStore = useSidebarTabStore()\n const nodeHelpStore = useNodeHelpStore()\n const { id: nodeLibraryTabId } = useNodeLibrarySidebarTab()\n\n const { selectedItems } = storeToRefs(canvasStore)\n\n const selectedNodes = computed(() => {\n return selectedItems.value.filter((i: unknown) =>\n isLGraphNode(i)\n ) as LGraphNode[]\n })\n\n const nodeDef = computed(() => {\n if (selectedNodes.value.length !== 1) return null\n return nodeDefStore.fromLGraphNode(selectedNodes.value[0])\n })\n\n const hasAnySelection = computed(() => selectedItems.value.length > 0)\n const hasSingleSelection = computed(() => selectedItems.value.length === 1)\n const hasMultipleSelection = computed(() => selectedItems.value.length > 1)\n\n const isSingleNode = computed(\n () => hasSingleSelection.value && isLGraphNode(selectedItems.value[0])\n )\n const isSingleSubgraph = computed(\n () =>\n isSingleNode.value &&\n (selectedItems.value[0] as LGraphNode)?.isSubgraphNode?.()\n )\n const isSingleImageNode = computed(\n () =>\n isSingleNode.value && isImageNode(selectedItems.value[0] as LGraphNode)\n )\n\n const hasSubgraphs = computed(() =>\n selectedItems.value.some((i: unknown) => i instanceof SubgraphNode)\n )\n\n const hasAny3DNodeSelected = computed(() => {\n const enable3DViewer = useSettingStore().get('Comfy.Load3D.3DViewerEnable')\n return (\n selectedNodes.value.length === 1 &&\n selectedNodes.value.some(isLoad3dNode) &&\n enable3DViewer\n )\n })\n\n const hasImageNode = computed(() => isSingleImageNode.value)\n const hasOutputNodesSelected = computed(\n () => filterOutputNodes(selectedNodes.value).length > 0\n )\n\n // Helper function to compute selection flags (reused by both computed and function)\n const computeSelectionStatesFromNodes = (\n nodes: LGraphNode[]\n ): NodeSelectionState => {\n if (!nodes.length)\n return { collapsed: false, pinned: false, bypassed: false }\n return {\n collapsed: nodes.some((n) => n.flags?.collapsed),\n pinned: nodes.some((n) => n.pinned),\n bypassed: nodes.some((n) => n.mode === LGraphEventMode.BYPASS)\n }\n }\n\n const selectedNodesStates = computed<NodeSelectionState>(() =>\n computeSelectionStatesFromNodes(selectedNodes.value)\n )\n\n // On-demand computation (non-reactive) so callers can fetch fresh flags\n const computeSelectionFlags = (): NodeSelectionState =>\n computeSelectionStatesFromNodes(selectedNodes.value)\n\n /** Toggle node help sidebar/panel for the single selected node (if any). */\n const showNodeHelp = () => {\n const def = nodeDef.value\n if (!def) return\n\n const isSidebarActive =\n sidebarTabStore.activeSidebarTabId === nodeLibraryTabId\n const currentHelpNode = nodeHelpStore.currentHelpNode\n const isSameNodeHelpOpen =\n isSidebarActive &&\n nodeHelpStore.isHelpOpen &&\n currentHelpNode?.nodePath === def.nodePath\n\n if (isSameNodeHelpOpen) {\n nodeHelpStore.closeHelp()\n sidebarTabStore.toggleSidebarTab(nodeLibraryTabId)\n return\n }\n\n if (!isSidebarActive) sidebarTabStore.toggleSidebarTab(nodeLibraryTabId)\n nodeHelpStore.openHelp(def)\n }\n\n return {\n selectedItems,\n selectedNodes,\n nodeDef,\n showNodeHelp,\n hasAny3DNodeSelected,\n hasAnySelection,\n hasSingleSelection,\n hasMultipleSelection,\n isSingleNode,\n isSingleSubgraph,\n isSingleImageNode,\n hasSubgraphs,\n hasImageNode,\n hasOutputNodesSelected,\n selectedNodesStates,\n computeSelectionFlags\n }\n}\n","<template>\n <Button\n v-if=\"isUnpackVisible\"\n v-tooltip.top=\"{\n value: $t('commands.Comfy_Graph_UnpackSubgraph.label'),\n showDelay: 1000\n }\"\n variant=\"muted-textonly\"\n :aria-label=\"$t('commands.Comfy_Graph_UnpackSubgraph.label')\"\n data-testid=\"convert-to-subgraph-button\"\n @click=\"() => commandStore.execute('Comfy.Graph.UnpackSubgraph')\"\n >\n <i class=\"icon-[lucide--expand] size-4\" />\n </Button>\n <Button\n v-else-if=\"isConvertVisible\"\n v-tooltip.top=\"{\n value: $t('commands.Comfy_Graph_ConvertToSubgraph.label'),\n showDelay: 1000\n }\"\n variant=\"muted-textonly\"\n size=\"icon\"\n :aria-label=\"$t('commands.Comfy_Graph_ConvertToSubgraph.label')\"\n data-testid=\"convert-to-subgraph-button\"\n @click=\"() => commandStore.execute('Comfy.Graph.ConvertToSubgraph')\"\n >\n <i class=\"icon-[lucide--shrink] size-4\" />\n </Button>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { useSelectionState } from '@/composables/graph/useSelectionState'\nimport { useCommandStore } from '@/stores/commandStore'\n\nconst commandStore = useCommandStore()\nconst { isSingleSubgraph, hasAnySelection } = useSelectionState()\n\nconst isUnpackVisible = isSingleSubgraph\nconst isConvertVisible = computed(\n () => hasAnySelection.value && !isSingleSubgraph.value\n)\n</script>\n","<template>\n <Button\n v-show=\"isDeletable\"\n v-tooltip.top=\"{\n value: $t('commands.Comfy_Canvas_DeleteSelectedItems.label'),\n showDelay: 1000\n }\"\n variant=\"muted-textonly\"\n :aria-label=\"$t('commands.Comfy_Canvas_DeleteSelectedItems.label')\"\n data-testid=\"delete-button\"\n @click=\"() => commandStore.execute('Comfy.Canvas.DeleteSelectedItems')\"\n >\n <i class=\"icon-[lucide--trash-2]\" />\n </Button>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { useSelectionState } from '@/composables/graph/useSelectionState'\nimport type { Positionable } from '@/lib/litegraph/src/interfaces'\nimport { useCommandStore } from '@/stores/commandStore'\n\nconst commandStore = useCommandStore()\nconst { selectedItems } = useSelectionState()\n\nconst isDeletable = computed(() =>\n selectedItems.value.some((x: Positionable) => x.removable !== false)\n)\n</script>\n","<template>\n <Button\n v-tooltip.top=\"{\n value: t('selectionToolbox.executeButton.tooltip'),\n showDelay: 1000\n }\"\n variant=\"primary\"\n :aria-label=\"t('selectionToolbox.executeButton.tooltip')\"\n @mouseenter=\"() => handleMouseEnter()\"\n @mouseleave=\"() => handleMouseLeave()\"\n @click=\"handleClick\"\n >\n <i class=\"icon-[lucide--play]\" />\n </Button>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { useSelectionState } from '@/composables/graph/useSelectionState'\nimport type { LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { isLGraphNode } from '@/utils/litegraphUtil'\nimport { isOutputNode } from '@/utils/nodeFilterUtil'\n\nconst { t } = useI18n()\nconst commandStore = useCommandStore()\nconst canvasStore = useCanvasStore()\nconst { selectedNodes } = useSelectionState()\n\nconst canvas = canvasStore.getCanvas()\nconst buttonHovered = ref(false)\nconst selectedOutputNodes = computed(() =>\n selectedNodes.value.filter(isLGraphNode).filter(isOutputNode)\n)\n\nfunction outputNodeStokeStyle(this: LGraphNode) {\n if (\n this.selected &&\n this.constructor.nodeData?.output_node &&\n buttonHovered.value\n ) {\n return { color: 'orange', lineWidth: 2, padding: 10 }\n }\n}\n\nconst handleMouseEnter = () => {\n buttonHovered.value = true\n for (const node of selectedOutputNodes.value) {\n node.strokeStyles['outputNode'] = outputNodeStokeStyle\n }\n canvas.setDirty(true)\n}\n\nconst handleMouseLeave = () => {\n buttonHovered.value = false\n canvas.setDirty(true)\n}\n\nconst handleClick = async () => {\n await commandStore.execute('Comfy.QueueSelectedOutputNodes')\n}\n</script>\n","<template>\n <Button\n v-tooltip.top=\"{\n value:\n st(`commands.${normalizeI18nKey(command.id)}.label`, '') || undefined,\n showDelay: 1000\n }\"\n variant=\"muted-textonly\"\n :aria-label=\"st(`commands.${normalizeI18nKey(command.id)}.label`, '')\"\n @click=\"() => commandStore.execute(command.id)\"\n >\n <i\n :class=\"[\n typeof command.icon === 'function' ? command.icon() : command.icon\n ]\"\n />\n </Button>\n</template>\n\n<script setup lang=\"ts\">\nimport Button from '@/components/ui/button/Button.vue'\nimport { st } from '@/i18n'\nimport type { ComfyCommand } from '@/stores/commandStore'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { normalizeI18nKey } from '@/utils/formatUtil'\n\ndefineProps<{\n command: ComfyCommand\n}>()\n\nconst commandStore = useCommandStore()\n</script>\n","<template>\n <Button\n v-tooltip.top=\"{\n value: $t('g.info'),\n showDelay: 1000\n }\"\n data-testid=\"info-button\"\n variant=\"muted-textonly\"\n :aria-label=\"$t('g.info')\"\n @click=\"onInfoClick\"\n >\n <i class=\"icon-[lucide--info]\" />\n </Button>\n</template>\n\n<script setup lang=\"ts\">\nimport Button from '@/components/ui/button/Button.vue'\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'\n\nconst rightSidePanelStore = useRightSidePanelStore()\n\n/**\n * Track node info button click and toggle node help.\n */\nconst onInfoClick = () => {\n useTelemetry()?.trackUiButtonClicked({\n button_id: 'selection_toolbox_node_info_opened'\n })\n rightSidePanelStore.openPanel('info')\n}\n</script>\n","<template>\n <Button\n v-tooltip.top=\"{\n value: $t('commands.Comfy_3DViewer_Open3DViewer.label'),\n showDelay: 1000\n }\"\n variant=\"muted-textonly\"\n :aria-label=\"$t('commands.Comfy_3DViewer_Open3DViewer.label')\"\n @click=\"open3DViewer\"\n >\n <i class=\"icon-[lucide--pencil]\" />\n </Button>\n</template>\n\n<script setup lang=\"ts\">\nimport Button from '@/components/ui/button/Button.vue'\nimport { useCommandStore } from '@/stores/commandStore'\n\nconst commandStore = useCommandStore()\n\nconst open3DViewer = () => {\n void commandStore.execute('Comfy.3DViewer.Open3DViewer')\n}\n</script>\n","<template>\n <Button\n v-show=\"isSingleImageNode\"\n v-tooltip.top=\"{\n value: $t('commands.Comfy_MaskEditor_OpenMaskEditor.label'),\n showDelay: 1000\n }\"\n variant=\"muted-textonly\"\n :aria-label=\"$t('commands.Comfy_MaskEditor_OpenMaskEditor.label')\"\n @click=\"openMaskEditor\"\n >\n <i class=\"icon-[comfy--mask]\" />\n </Button>\n</template>\n\n<script setup lang=\"ts\">\nimport Button from '@/components/ui/button/Button.vue'\nimport { useSelectionState } from '@/composables/graph/useSelectionState'\nimport { useCommandStore } from '@/stores/commandStore'\n\nconst commandStore = useCommandStore()\nconst { isSingleImageNode } = useSelectionState()\n\nconst openMaskEditor = () => {\n void commandStore.execute('Comfy.MaskEditor.OpenMaskEditor')\n}\n</script>\n","import { computed, ref, watchEffect } from 'vue'\n\nimport type { LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { isLGraphNode } from '@/utils/litegraphUtil'\n\ninterface RefreshableItem {\n refresh: () => Promise<void> | void\n}\n\nconst isRefreshableWidget = (widget: unknown): widget is RefreshableItem =>\n widget != null &&\n typeof widget === 'object' &&\n 'refresh' in widget &&\n typeof widget.refresh === 'function'\n\n/**\n * Tracks selected nodes and their refreshable widgets\n */\nexport const useRefreshableSelection = () => {\n const graphStore = useCanvasStore()\n const selectedNodes = ref<LGraphNode[]>([])\n\n watchEffect(() => {\n selectedNodes.value = graphStore.selectedItems.filter(isLGraphNode)\n })\n\n const refreshableWidgets = computed<RefreshableItem[]>(() =>\n selectedNodes.value.flatMap((node) => {\n if (!node.widgets) return []\n const items: RefreshableItem[] = []\n for (const widget of node.widgets) {\n if (isRefreshableWidget(widget)) {\n items.push(widget)\n }\n }\n return items\n })\n )\n\n const isRefreshable = computed(() => refreshableWidgets.value.length > 0)\n\n async function refreshSelected() {\n if (!isRefreshable.value) return\n\n await Promise.all(refreshableWidgets.value.map((item) => item.refresh()))\n }\n\n return {\n isRefreshable,\n refreshSelected\n }\n}\n","<template>\n <Button\n v-show=\"isRefreshable\"\n v-tooltip.top=\"t('g.refreshNode')\"\n variant=\"muted-textonly\"\n :aria-label=\"t('g.refreshNode')\"\n data-testid=\"refresh-button\"\n @click=\"refreshSelected\"\n >\n <i class=\"icon-[lucide--refresh-cw]\" />\n </Button>\n</template>\n\n<script setup lang=\"ts\">\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { useRefreshableSelection } from '@/composables/useRefreshableSelection'\n\nconst { t } = useI18n()\nconst { isRefreshable, refreshSelected } = useRefreshableSelection()\n</script>\n","<template>\n <Button\n v-show=\"isVisible\"\n v-tooltip.top=\"{\n value: $t('commands.Comfy_PublishSubgraph.label'),\n showDelay: 1000\n }\"\n variant=\"muted-textonly\"\n :aria-label=\"$t('commands.Comfy_PublishSubgraph.label')\"\n @click=\"() => commandStore.execute('Comfy.PublishSubgraph')\"\n >\n <i class=\"icon-[lucide--book-open]\" />\n </Button>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { SubgraphNode } from '@/lib/litegraph/src/litegraph'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useCommandStore } from '@/stores/commandStore'\n\nconst commandStore = useCommandStore()\nconst canvasStore = useCanvasStore()\n\nconst isVisible = computed(() => {\n return (\n canvasStore.selectedItems?.length === 1 &&\n canvasStore.selectedItems[0] instanceof SubgraphNode\n )\n})\n</script>\n","import { useElementBounding, useRafFn } from '@vueuse/core'\nimport { computed, onUnmounted, ref, watch, watchEffect } from 'vue'\nimport type { Ref } from 'vue'\n\nimport { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'\nimport { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'\nimport type { ReadOnlyRect } from '@/lib/litegraph/src/interfaces'\nimport {\n LGraphGroup,\n LGraphNode,\n LiteGraph\n} from '@/lib/litegraph/src/litegraph'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { layoutStore } from '@/renderer/core/layout/store/layoutStore'\nimport { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'\nimport { computeUnionBounds } from '@/utils/mathUtil'\n\n/**\n * Manages the position of the selection toolbox independently.\n * Uses CSS custom properties for performant transform updates.\n */\n\n// Shared signals for auxiliary UI (e.g., MoreOptions) to coordinate hide/restore\nconst moreOptionsOpen = ref(false)\nconst forceCloseMoreOptionsSignal = ref(0)\nconst restoreMoreOptionsSignal = ref(0)\nconst moreOptionsRestorePending = ref(false)\nlet moreOptionsWasOpenBeforeDrag = false\nlet moreOptionsSelectionSignature: string | null = null\n\nfunction buildSelectionSignature(\n store: ReturnType<typeof useCanvasStore>\n): string | null {\n const c = store.canvas\n if (!c) return null\n const items = Array.from(c.selectedItems)\n if (items.length !== 1) return null\n const item = items[0]\n if (isLGraphNode(item)) return `N:${item.id}`\n if (isLGraphGroup(item)) return `G:${item.id}`\n return null\n}\n\nfunction currentSelectionMatchesSignature(\n store: ReturnType<typeof useCanvasStore>\n) {\n if (!moreOptionsSelectionSignature) return false\n return buildSelectionSignature(store) === moreOptionsSelectionSignature\n}\n\nexport function useSelectionToolboxPosition(\n toolboxRef: Ref<HTMLElement | undefined>\n) {\n const canvasStore = useCanvasStore()\n const lgCanvas = canvasStore.getCanvas()\n const { getSelectableItems } = useSelectedLiteGraphItems()\n const { shouldRenderVueNodes } = useVueFeatureFlags()\n\n // World position of selection center\n const worldPosition = ref({ x: 0, y: 0 })\n\n const visible = ref(false)\n\n // Use VueUse to reactively track canvas bounding rect\n const { left: canvasLeft, top: canvasTop } = useElementBounding(\n lgCanvas.canvas\n )\n\n // Unified dragging state - combines both LiteGraph and Vue node dragging\n const isDragging = computed((): boolean => {\n const litegraphDragging = canvasStore.canvas?.state?.draggingItems ?? false\n const vueNodeDragging =\n shouldRenderVueNodes.value && layoutStore.isDraggingVueNodes.value\n return litegraphDragging || vueNodeDragging\n })\n\n /**\n * Update position based on selection\n */\n const updateSelectionBounds = () => {\n const selectableItems = getSelectableItems()\n\n if (!selectableItems.size) {\n visible.value = false\n return\n }\n\n // Don't show toolbox while dragging\n if (isDragging.value) {\n visible.value = false\n return\n }\n\n visible.value = true\n\n // Get bounds for all selected items\n const allBounds: ReadOnlyRect[] = []\n for (const item of selectableItems) {\n // Skip items without valid IDs\n if (item.id == null) continue\n\n if (shouldRenderVueNodes.value && typeof item.id === 'string') {\n // Use layout store for Vue nodes (only works with string IDs)\n const layout = layoutStore.getNodeLayoutRef(item.id).value\n if (layout) {\n allBounds.push([\n layout.bounds.x,\n layout.bounds.y,\n layout.bounds.width,\n layout.bounds.height\n ])\n }\n } else {\n // Fallback to LiteGraph bounds for regular nodes or non-string IDs\n if (item instanceof LGraphNode || item instanceof LGraphGroup) {\n allBounds.push([\n item.pos[0],\n item.pos[1] - LiteGraph.NODE_TITLE_HEIGHT,\n item.size[0],\n item.size[1] + LiteGraph.NODE_TITLE_HEIGHT\n ])\n }\n }\n }\n\n // Compute union bounds\n const unionBounds = computeUnionBounds(allBounds)\n if (!unionBounds) return\n\n worldPosition.value = {\n x: unionBounds.x + unionBounds.width / 2,\n // createBounds() applied a default padding of 10px\n // so adjust Y to maintain visual consistency\n y: unionBounds.y - 10\n }\n\n updateTransform()\n }\n\n const updateTransform = () => {\n if (!visible.value) return\n\n const { scale, offset } = lgCanvas.ds\n\n const screenX =\n (worldPosition.value.x + offset[0]) * scale + canvasLeft.value\n const screenY =\n (worldPosition.value.y + offset[1]) * scale + canvasTop.value\n\n // Update CSS custom properties directly for best performance\n if (toolboxRef.value) {\n toolboxRef.value.style.setProperty('--tb-x', `${screenX}px`)\n toolboxRef.value.style.setProperty('--tb-y', `${screenY}px`)\n }\n }\n\n // Sync with canvas transform\n const { resume: startSync, pause: stopSync } = useRafFn(updateTransform)\n\n watchEffect(() => {\n if (visible.value) {\n startSync()\n } else {\n stopSync()\n }\n })\n\n // Watch for selection changes\n watch(\n () => canvasStore.getCanvas().state.selectionChanged,\n (changed) => {\n if (changed) {\n if (moreOptionsRestorePending.value || moreOptionsSelectionSignature) {\n moreOptionsRestorePending.value = false\n moreOptionsWasOpenBeforeDrag = false\n if (!moreOptionsOpen.value) {\n moreOptionsSelectionSignature = null\n } else {\n moreOptionsSelectionSignature = buildSelectionSignature(canvasStore)\n }\n }\n updateSelectionBounds()\n canvasStore.getCanvas().state.selectionChanged = false\n }\n },\n { immediate: true }\n )\n watch(\n () => moreOptionsOpen.value,\n (v) => {\n if (v) {\n moreOptionsSelectionSignature = buildSelectionSignature(canvasStore)\n } else if (!canvasStore.canvas?.state?.draggingItems) {\n moreOptionsSelectionSignature = null\n if (moreOptionsRestorePending.value)\n moreOptionsRestorePending.value = false\n }\n }\n )\n\n const handleDragStateChange = (dragging: boolean) => {\n if (dragging) {\n handleDragStart()\n return\n }\n\n handleDragEnd()\n }\n\n const handleDragStart = () => {\n visible.value = false\n\n // Early return if more options wasn't open\n if (!moreOptionsOpen.value) {\n moreOptionsRestorePending.value = false\n moreOptionsWasOpenBeforeDrag = false\n return\n }\n\n // Handle more options cleanup\n const currentSig = buildSelectionSignature(canvasStore)\n const selectionChanged = currentSig !== moreOptionsSelectionSignature\n\n if (selectionChanged) {\n moreOptionsSelectionSignature = null\n }\n moreOptionsOpen.value = false\n moreOptionsWasOpenBeforeDrag = true\n moreOptionsRestorePending.value = !!moreOptionsSelectionSignature\n\n if (moreOptionsRestorePending.value) {\n forceCloseMoreOptionsSignal.value++\n return\n }\n\n moreOptionsWasOpenBeforeDrag = false\n }\n\n const handleDragEnd = () => {\n requestAnimationFrame(() => {\n updateSelectionBounds()\n\n const selectionMatches = currentSelectionMatchesSignature(canvasStore)\n const shouldRestore =\n moreOptionsWasOpenBeforeDrag &&\n visible.value &&\n moreOptionsRestorePending.value &&\n selectionMatches\n\n // Single point of assignment for each ref\n moreOptionsRestorePending.value =\n shouldRestore && moreOptionsRestorePending.value\n moreOptionsWasOpenBeforeDrag = false\n\n if (shouldRestore) {\n restoreMoreOptionsSignal.value++\n }\n })\n }\n\n watch(isDragging, handleDragStateChange)\n\n onUnmounted(() => {\n resetMoreOptionsState()\n })\n\n return {\n visible\n }\n}\n\n// External cleanup utility to be called when SelectionToolbox component unmounts\nfunction resetMoreOptionsState() {\n moreOptionsOpen.value = false\n moreOptionsRestorePending.value = false\n moreOptionsWasOpenBeforeDrag = false\n moreOptionsSelectionSignature = null\n}\n","import { default as DOMPurify } from 'dompurify'\n\nimport { LiteGraph } from '@/lib/litegraph/src/litegraph'\nimport type {\n IContextMenuValue,\n LGraphNode,\n IContextMenuOptions,\n ContextMenu\n} from '@/lib/litegraph/src/litegraph'\n\nimport type { MenuOption, SubMenuOption } from './useMoreOptionsMenu'\nimport type { ContextMenuDivElement } from '@/lib/litegraph/src/interfaces'\n\n/**\n * Hard blacklist - items that should NEVER be included\n */\nconst HARD_BLACKLIST = new Set([\n 'Properties', // Never include Properties submenu\n 'Colors', // Use singular \"Color\" instead\n 'Shapes', // Use singular \"Shape\" instead\n 'Title',\n 'Mode',\n 'Properties Panel',\n 'Copy (Clipspace)'\n])\n\n/**\n * Core menu items - items that should appear in the main menu, not under Extensions\n * Includes both LiteGraph base menu items and ComfyUI built-in functionality\n */\nconst CORE_MENU_ITEMS = new Set([\n // Basic operations\n 'Rename',\n 'Copy',\n 'Duplicate',\n 'Clone',\n // Node state operations\n 'Run Branch',\n 'Pin',\n 'Unpin',\n 'Bypass',\n 'Remove Bypass',\n 'Mute',\n // Structure operations\n 'Convert to Subgraph',\n 'Frame selection',\n 'Minimize Node',\n 'Expand',\n 'Collapse',\n // Info and adjustments\n 'Node Info',\n 'Resize',\n 'Title',\n 'Properties Panel',\n 'Adjust Size',\n // Visual\n 'Color',\n 'Colors',\n 'Shape',\n 'Shapes',\n 'Mode',\n // Built-in node operations (node-specific)\n 'Open Image',\n 'Copy Image',\n 'Save Image',\n 'Open in Mask Editor',\n 'Edit Subgraph Widgets',\n 'Unpack Subgraph',\n 'Copy (Clipspace)',\n 'Paste (Clipspace)',\n // Selection and alignment\n 'Align Selected To',\n 'Distribute Nodes',\n // Deletion\n 'Delete',\n 'Remove',\n // LiteGraph base items\n 'Show Advanced',\n 'Hide Advanced'\n])\n\n/**\n * Normalize menu item label for duplicate detection\n * Handles variations like Colors/Color, Shapes/Shape, Pin/Unpin, Remove/Delete\n */\nfunction normalizeLabel(label: string): string {\n return label\n .toLowerCase()\n .replace(/^un/, '') // Remove 'un' prefix (Unpin -> Pin)\n .trim()\n}\n\n/**\n * Check if a similar menu item already exists in the results\n * Returns true if an item with the same normalized label exists\n */\nfunction isDuplicateItem(label: string, existingItems: MenuOption[]): boolean {\n const normalizedLabel = normalizeLabel(label)\n\n // Map of equivalent items\n const equivalents: Record<string, string[]> = {\n color: ['color', 'colors'],\n shape: ['shape', 'shapes'],\n pin: ['pin', 'unpin'],\n delete: ['remove', 'delete'],\n duplicate: ['clone', 'duplicate']\n }\n\n return existingItems.some((item) => {\n if (!item.label) return false\n\n const existingNormalized = normalizeLabel(item.label)\n\n // Check direct match\n if (existingNormalized === normalizedLabel) return true\n\n // Check if they're in the same equivalence group\n for (const values of Object.values(equivalents)) {\n if (\n values.includes(normalizedLabel) &&\n values.includes(existingNormalized)\n ) {\n return true\n }\n }\n\n return false\n })\n}\n\n/**\n * Check if a menu item is a core menu item (not an extension)\n * Core items include LiteGraph base items and ComfyUI built-in functionality\n */\nfunction isCoreMenuItem(label: string): boolean {\n return CORE_MENU_ITEMS.has(label)\n}\n\n/**\n * Filter out duplicate menu items based on label\n * Gives precedence to Vue hardcoded options over LiteGraph options\n */\nfunction removeDuplicateMenuOptions(options: MenuOption[]): MenuOption[] {\n // Group items by label\n const itemsByLabel = new Map<string, MenuOption[]>()\n const itemsWithoutLabel: MenuOption[] = []\n\n for (const opt of options) {\n // Always keep dividers and category items\n if (opt.type === 'divider' || opt.type === 'category') {\n itemsWithoutLabel.push(opt)\n continue\n }\n\n // Items without labels are kept as-is\n if (!opt.label) {\n itemsWithoutLabel.push(opt)\n continue\n }\n\n // Group by label\n if (!itemsByLabel.has(opt.label)) {\n itemsByLabel.set(opt.label, [])\n }\n itemsByLabel.get(opt.label)!.push(opt)\n }\n\n // Select best item for each label (prefer vue over litegraph)\n const result: MenuOption[] = []\n const seenLabels = new Set<string>()\n\n for (const opt of options) {\n // Add non-labeled items in original order\n if (opt.type === 'divider' || opt.type === 'category' || !opt.label) {\n if (itemsWithoutLabel.includes(opt)) {\n result.push(opt)\n const idx = itemsWithoutLabel.indexOf(opt)\n itemsWithoutLabel.splice(idx, 1)\n }\n continue\n }\n\n // Skip if we already processed this label\n if (seenLabels.has(opt.label)) {\n continue\n }\n seenLabels.add(opt.label)\n\n // Get all items with this label\n const duplicates = itemsByLabel.get(opt.label)!\n\n // If only one item, add it\n if (duplicates.length === 1) {\n result.push(duplicates[0])\n continue\n }\n\n // Multiple items: prefer vue source over litegraph\n const vueItem = duplicates.find((item) => item.source === 'vue')\n if (vueItem) {\n result.push(vueItem)\n } else {\n // No vue item, just take the first one\n result.push(duplicates[0])\n }\n }\n\n return result\n}\n\n/**\n * Order groups for menu items - defines the display order of sections\n */\nconst MENU_ORDER: string[] = [\n // Section 1: Basic operations\n 'Rename',\n 'Copy',\n 'Duplicate',\n // Section 2: Node actions\n 'Run Branch',\n 'Pin',\n 'Unpin',\n 'Bypass',\n 'Remove Bypass',\n 'Mute',\n // Section 3: Structure operations\n 'Convert to Subgraph',\n 'Frame selection',\n 'Minimize Node',\n 'Expand',\n 'Collapse',\n 'Resize',\n 'Clone',\n // Section 4: Node properties\n 'Node Info',\n 'Color',\n // Section 5: Node-specific operations\n 'Open in Mask Editor',\n 'Open Image',\n 'Copy Image',\n 'Save Image',\n 'Copy (Clipspace)',\n 'Paste (Clipspace)',\n // Fallback for other core items\n 'Convert to Group Node (Deprecated)'\n]\n\n/**\n * Get the order index for a menu item (lower = earlier in menu)\n */\nfunction getMenuItemOrder(label: string): number {\n const index = MENU_ORDER.indexOf(label)\n return index === -1 ? 999 : index\n}\n\n/**\n * Build structured menu with core items first, then extensions under a labeled section\n * Ensures Delete always appears at the bottom\n */\nexport function buildStructuredMenu(options: MenuOption[]): MenuOption[] {\n // First, remove duplicates (giving precedence to Vue hardcoded options)\n const deduplicated = removeDuplicateMenuOptions(options)\n const coreItemsMap = new Map<string, MenuOption>()\n const extensionItems: MenuOption[] = []\n let deleteItem: MenuOption | undefined\n\n // Separate items into core and extension categories\n for (const option of deduplicated) {\n // Skip dividers for now - we'll add them between sections later\n if (option.type === 'divider') {\n continue\n }\n\n // Skip category labels (they'll be added separately)\n if (option.type === 'category') {\n continue\n }\n\n // Check if this is the Delete/Remove item - save it for the end\n const isDeleteItem = option.label === 'Delete' || option.label === 'Remove'\n if (isDeleteItem && !option.hasSubmenu) {\n deleteItem = option\n continue\n }\n\n // Categorize based on label\n if (option.label && isCoreMenuItem(option.label)) {\n coreItemsMap.set(option.label, option)\n } else {\n extensionItems.push(option)\n }\n }\n // Build ordered core items based on MENU_ORDER\n const orderedCoreItems: MenuOption[] = []\n const coreLabels = Array.from(coreItemsMap.keys())\n coreLabels.sort((a, b) => getMenuItemOrder(a) - getMenuItemOrder(b))\n\n // Section boundaries based on MENU_ORDER indices\n // Section 1: 0-2 (Rename, Copy, Duplicate)\n // Section 2: 3-8 (Run Branch, Pin, Unpin, Bypass, Remove Bypass, Mute)\n // Section 3: 9-15 (Convert to Subgraph, Frame selection, Minimize Node, Expand, Collapse, Resize, Clone)\n // Section 4: 16-17 (Node Info, Color)\n // Section 5: 18+ (Image operations and fallback items)\n const getSectionNumber = (index: number): number => {\n if (index <= 2) return 1\n if (index <= 8) return 2\n if (index <= 15) return 3\n if (index <= 17) return 4\n return 5\n }\n\n let lastSection = 0\n for (const label of coreLabels) {\n const item = coreItemsMap.get(label)!\n const itemIndex = getMenuItemOrder(label)\n const currentSection = getSectionNumber(itemIndex)\n\n // Add divider when moving to a new section\n if (lastSection > 0 && currentSection !== lastSection) {\n orderedCoreItems.push({ type: 'divider' })\n }\n\n orderedCoreItems.push(item)\n lastSection = currentSection\n }\n\n // Build the final menu structure\n const result: MenuOption[] = []\n\n // Add ordered core items with their dividers\n result.push(...orderedCoreItems)\n\n // Add extensions section if there are extension items\n if (extensionItems.length > 0) {\n // Add divider before Extensions section\n result.push({ type: 'divider' })\n\n // Add non-clickable Extensions label\n result.push({\n label: 'Extensions',\n type: 'category',\n disabled: true\n })\n\n // Add extension items\n result.push(...extensionItems)\n }\n\n // Add Delete at the bottom if it exists\n if (deleteItem) {\n result.push({ type: 'divider' })\n result.push(deleteItem)\n }\n\n return result\n}\n\n/**\n * Convert LiteGraph IContextMenuValue items to Vue MenuOption format\n * Used to bridge LiteGraph context menus into Vue node menus\n * @param items - The LiteGraph menu items to convert\n * @param node - The node context (optional)\n * @param applyStructuring - Whether to apply menu structuring (core/extensions separation). Defaults to true.\n */\nexport function convertContextMenuToOptions(\n items: (IContextMenuValue | null)[],\n node?: LGraphNode,\n applyStructuring: boolean = true\n): MenuOption[] {\n const result: MenuOption[] = []\n\n for (const item of items) {\n // Null items are separators in LiteGraph\n if (item === null) {\n result.push({ type: 'divider' })\n continue\n }\n\n // Skip items without content (shouldn't happen, but be safe)\n if (!item.content) {\n continue\n }\n\n // Skip hard blacklisted items\n if (HARD_BLACKLIST.has(item.content)) {\n continue\n }\n\n // Skip if a similar item already exists in results\n if (isDuplicateItem(item.content, result)) {\n continue\n }\n\n const option: MenuOption = {\n label: item.content,\n source: 'litegraph'\n }\n\n // Pass through disabled state\n if (item.disabled) {\n option.disabled = true\n }\n\n // Handle submenus\n if (item.has_submenu) {\n // Static submenu with pre-defined options\n if (item.submenu?.options) {\n option.hasSubmenu = true\n option.submenu = convertSubmenuToOptions(item.submenu.options)\n }\n // Dynamic submenu - callback creates it on-demand\n else if (item.callback && !item.disabled) {\n option.hasSubmenu = true\n // Intercept the callback to capture dynamic submenu items\n const capturedSubmenu = captureDynamicSubmenu(item, node)\n if (capturedSubmenu) {\n option.submenu = capturedSubmenu\n } else {\n console.warn(\n '[ContextMenuConverter] Failed to capture submenu for:',\n item.content\n )\n }\n }\n }\n // Handle callback (only if not disabled and not a submenu)\n else if (item.callback && !item.disabled) {\n // Wrap the callback to match the () => void signature\n option.action = () => {\n try {\n void item.callback?.call(\n item as unknown as ContextMenuDivElement,\n item.value,\n {},\n undefined,\n undefined,\n item\n )\n } catch (error) {\n console.error('Error executing context menu callback:', error)\n }\n }\n }\n\n result.push(option)\n }\n\n // Apply structured menu with core items and extensions section (if requested)\n if (applyStructuring) {\n return buildStructuredMenu(result)\n }\n\n return result\n}\n\n/**\n * Capture submenu items from a dynamic submenu callback\n * Intercepts ContextMenu constructor to extract items without creating HTML menu\n */\nfunction captureDynamicSubmenu(\n item: IContextMenuValue,\n node?: LGraphNode\n): SubMenuOption[] | undefined {\n let capturedItems: readonly (IContextMenuValue | string | null)[] | undefined\n let capturedOptions: IContextMenuOptions | undefined\n\n // Store original ContextMenu constructor\n const OriginalContextMenu = LiteGraph.ContextMenu\n\n try {\n // Mock ContextMenu constructor to capture submenu items and options\n LiteGraph.ContextMenu = function (\n items: readonly (IContextMenuValue | string | null)[],\n options?: IContextMenuOptions\n ) {\n // Capture both items and options\n capturedItems = items\n capturedOptions = options\n // Return a minimal mock object to prevent errors\n return {\n close: () => {},\n root: document.createElement('div')\n } as unknown as ContextMenu\n } as unknown as typeof ContextMenu\n\n // Execute the callback to trigger submenu creation\n try {\n // Create a mock MouseEvent for the callback\n const mockEvent = new MouseEvent('click', {\n bubbles: true,\n cancelable: true,\n clientX: 0,\n clientY: 0\n })\n\n // Create a mock parent menu\n const mockMenu = {\n close: () => {},\n root: document.createElement('div')\n } as unknown as ContextMenu\n\n // Call the callback which should trigger ContextMenu constructor\n // Callback signature varies, but typically: (value, options, event, menu, node)\n void item.callback?.call(\n item as unknown as ContextMenuDivElement,\n item.value,\n {},\n mockEvent,\n mockMenu,\n node // Pass the node context for callbacks that need it\n )\n } catch (error) {\n console.warn(\n '[ContextMenuConverter] Error executing callback for:',\n item.content,\n error\n )\n }\n } finally {\n // Always restore original constructor\n LiteGraph.ContextMenu = OriginalContextMenu\n }\n\n // Convert captured items to Vue submenu format\n if (capturedItems) {\n const converted = convertSubmenuToOptions(capturedItems, capturedOptions)\n return converted\n }\n\n console.warn('[ContextMenuConverter] No items captured for:', item.content)\n return undefined\n}\n\n/**\n * Convert LiteGraph submenu items to Vue SubMenuOption format\n */\nfunction convertSubmenuToOptions(\n items: readonly (IContextMenuValue | string | null)[],\n options?: IContextMenuOptions\n): SubMenuOption[] {\n const result: SubMenuOption[] = []\n\n for (const item of items) {\n // Skip null separators\n if (item === null) {\n continue\n }\n\n // Handle string items (simple labels like in Mode/Shapes menus)\n if (typeof item === 'string') {\n const subOption: SubMenuOption = {\n label: item,\n action: () => {\n try {\n // Call the options callback with the string value\n if (options?.callback) {\n void options.callback.call(\n null,\n item,\n options,\n undefined,\n undefined,\n options.extra\n )\n }\n } catch (error) {\n console.error('Error executing string item callback:', error)\n }\n }\n }\n result.push(subOption)\n continue\n }\n\n // Handle object items\n if (!item.content) {\n continue\n }\n\n // Extract text content from HTML if present\n const content = stripHtmlTags(item.content)\n\n const subOption: SubMenuOption = {\n label: content,\n action: () => {\n try {\n void item.callback?.call(\n item as unknown as ContextMenuDivElement,\n item.value,\n {},\n undefined,\n undefined,\n item\n )\n } catch (error) {\n console.error('Error executing submenu callback:', error)\n }\n }\n }\n\n // Pass through disabled state\n if (item.disabled) {\n subOption.disabled = true\n }\n\n result.push(subOption)\n }\n return result\n}\n\n/**\n * Strip HTML tags from content string safely\n * LiteGraph menu items often include HTML for styling\n */\nfunction stripHtmlTags(html: string): string {\n // Use DOMPurify to sanitize and strip all HTML tags\n const sanitized = DOMPurify.sanitize(html, { ALLOWED_TAGS: [] })\n const result = sanitized.trim()\n return result || html.replace(/<[^>]*>/g, '').trim() || html\n}\n","// call nextTick on all changeTracker\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\n\n/**\n * Composable for refreshing nodes in the graph\n * */\nexport function useCanvasRefresh() {\n const canvasStore = useCanvasStore()\n const workflowStore = useWorkflowStore()\n const refreshCanvas = () => {\n canvasStore.canvas?.emitBeforeChange()\n canvasStore.canvas?.setDirty(true, true)\n canvasStore.canvas?.graph?.afterChange()\n canvasStore.canvas?.emitAfterChange()\n workflowStore.activeWorkflow?.changeTracker?.checkState()\n }\n\n return {\n refreshCanvas\n }\n}\n","import { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport {\n LGraphCanvas,\n LGraphNode,\n LiteGraph,\n RenderShape,\n isColorable\n} from '@/lib/litegraph/src/litegraph'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'\nimport { adjustColor } from '@/utils/colorUtil'\n\nimport { useCanvasRefresh } from './useCanvasRefresh'\n\ninterface ColorOption {\n name: string\n localizedName: string\n value: {\n dark: string\n light: string\n }\n}\n\ninterface ShapeOption {\n name: string\n localizedName: string\n value: RenderShape\n}\n\n/**\n * Composable for handling node color and shape customization\n */\nexport function useNodeCustomization() {\n const { t } = useI18n()\n const canvasStore = useCanvasStore()\n const colorPaletteStore = useColorPaletteStore()\n const canvasRefresh = useCanvasRefresh()\n const isLightTheme = computed(\n () => colorPaletteStore.completedActivePalette.light_theme\n )\n\n const toLightThemeColor = (color: string) =>\n adjustColor(color, { lightness: 0.5 })\n\n // Color options\n const NO_COLOR_OPTION: ColorOption = {\n name: 'noColor',\n localizedName: t('color.noColor'),\n value: {\n dark: LiteGraph.NODE_DEFAULT_BGCOLOR,\n light: toLightThemeColor(LiteGraph.NODE_DEFAULT_BGCOLOR)\n }\n }\n\n const colorOptions: ColorOption[] = [\n NO_COLOR_OPTION,\n ...Object.entries(LGraphCanvas.node_colors).map(([name, color]) => ({\n name,\n localizedName: t(`color.${name}`),\n value: {\n dark: color.bgcolor,\n light: toLightThemeColor(color.bgcolor)\n }\n }))\n ]\n\n // Shape options\n const shapeOptions: ShapeOption[] = [\n {\n name: 'default',\n localizedName: t('shape.default'),\n value: RenderShape.ROUND\n },\n {\n name: 'box',\n localizedName: t('shape.box'),\n value: RenderShape.BOX\n },\n {\n name: 'card',\n localizedName: t('shape.CARD'),\n value: RenderShape.CARD\n }\n ]\n\n const applyColor = (colorOption: ColorOption | null) => {\n const colorName = colorOption?.name ?? NO_COLOR_OPTION.name\n const canvasColorOption =\n colorName === NO_COLOR_OPTION.name\n ? null\n : LGraphCanvas.node_colors[colorName]\n\n for (const item of canvasStore.selectedItems) {\n if (isColorable(item)) {\n item.setColorOption(canvasColorOption)\n }\n }\n\n canvasRefresh.refreshCanvas()\n }\n\n const applyShape = (shapeOption: ShapeOption) => {\n const selectedNodes = Array.from(canvasStore.selectedItems).filter(\n (item): item is LGraphNode => item instanceof LGraphNode\n )\n\n if (selectedNodes.length === 0) {\n return\n }\n\n selectedNodes.forEach((node) => {\n node.shape = shapeOption.value\n })\n\n canvasRefresh.refreshCanvas()\n }\n\n const getCurrentColor = (): ColorOption | null => {\n const selectedItems = Array.from(canvasStore.selectedItems)\n if (selectedItems.length === 0) return null\n\n // Get color from first colorable item\n const firstColorableItem = selectedItems.find((item) => isColorable(item))\n if (!firstColorableItem || !isColorable(firstColorableItem)) return null\n\n // Get the current color option from the colorable item\n const currentColorOption = firstColorableItem.getColorOption()\n const currentBgColor = currentColorOption?.bgcolor ?? null\n\n // Find matching color option\n return (\n colorOptions.find(\n (option) =>\n option.value.dark === currentBgColor ||\n option.value.light === currentBgColor\n ) ?? NO_COLOR_OPTION\n )\n }\n\n const getCurrentShape = (): ShapeOption | null => {\n const selectedNodes = Array.from(canvasStore.selectedItems).filter(\n (item): item is LGraphNode => item instanceof LGraphNode\n )\n\n if (selectedNodes.length === 0) return null\n\n const firstNode = selectedNodes[0]\n const currentShape = firstNode.shape ?? RenderShape.ROUND\n\n return (\n shapeOptions.find((option) => option.value === currentShape) ??\n shapeOptions[0]\n )\n }\n\n return {\n colorOptions,\n shapeOptions,\n applyColor,\n applyShape,\n getCurrentColor,\n getCurrentShape,\n isLightTheme\n }\n}\n","import { useI18n } from 'vue-i18n'\n\nimport { LGraphEventMode } from '@/lib/litegraph/src/litegraph'\nimport type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\n\nimport { useCanvasRefresh } from './useCanvasRefresh'\nimport type { MenuOption } from './useMoreOptionsMenu'\nimport { useNodeCustomization } from './useNodeCustomization'\n\n/**\n * Composable for group-related menu operations\n */\nexport function useGroupMenuOptions() {\n const { t } = useI18n()\n const canvasStore = useCanvasStore()\n const workflowStore = useWorkflowStore()\n const settingStore = useSettingStore()\n const canvasRefresh = useCanvasRefresh()\n const { shapeOptions, colorOptions, isLightTheme } = useNodeCustomization()\n\n const getFitGroupToNodesOption = (groupContext: LGraphGroup): MenuOption => ({\n label: 'Fit Group To Nodes',\n icon: 'icon-[lucide--move-diagonal-2]',\n action: () => {\n try {\n groupContext.recomputeInsideNodes()\n } catch (e) {\n console.warn('Failed to recompute group nodes:', e)\n return\n }\n\n const padding = settingStore.get('Comfy.GroupSelectedNodes.Padding')\n groupContext.resizeTo(groupContext.children, padding)\n groupContext.graph?.change()\n canvasStore.canvas?.setDirty(true, true)\n workflowStore.activeWorkflow?.changeTracker?.checkState()\n }\n })\n\n const getGroupShapeOptions = (\n groupContext: LGraphGroup,\n bump: () => void\n ): MenuOption => ({\n label: t('contextMenu.Shape'),\n icon: 'icon-[lucide--box]',\n hasSubmenu: true,\n submenu: shapeOptions.map((shape) => ({\n label: shape.localizedName,\n action: () => {\n const nodes = (groupContext.nodes || []) as LGraphNode[]\n nodes.forEach((node) => (node.shape = shape.value))\n canvasRefresh.refreshCanvas()\n bump()\n }\n }))\n })\n\n const getGroupColorOptions = (\n groupContext: LGraphGroup,\n bump: () => void\n ): MenuOption => ({\n label: t('contextMenu.Color'),\n icon: 'icon-[lucide--palette]',\n hasSubmenu: true,\n submenu: colorOptions.map((colorOption) => ({\n label: colorOption.localizedName,\n color: isLightTheme.value\n ? colorOption.value.light\n : colorOption.value.dark,\n action: () => {\n groupContext.color = isLightTheme.value\n ? colorOption.value.light\n : colorOption.value.dark\n canvasRefresh.refreshCanvas()\n bump()\n }\n }))\n })\n\n const getGroupModeOptions = (\n groupContext: LGraphGroup,\n bump: () => void\n ): MenuOption[] => {\n const options: MenuOption[] = []\n\n try {\n groupContext.recomputeInsideNodes()\n } catch (e) {\n console.warn('Failed to recompute group nodes for mode options:', e)\n return options\n }\n\n const groupNodes = (groupContext.nodes || []) as LGraphNode[]\n if (!groupNodes.length) return options\n\n // Check if all nodes have the same mode\n let allSame = true\n for (let i = 1; i < groupNodes.length; i++) {\n if (groupNodes[i].mode !== groupNodes[0].mode) {\n allSame = false\n break\n }\n }\n\n const createModeAction = (label: string, mode: LGraphEventMode) => ({\n label: t(`selectionToolbox.${label}`),\n icon:\n mode === LGraphEventMode.BYPASS\n ? 'icon-[lucide--ban]'\n : mode === LGraphEventMode.NEVER\n ? 'icon-[lucide--zap-off]'\n : 'icon-[lucide--play]',\n action: () => {\n groupNodes.forEach((n) => {\n n.mode = mode\n })\n canvasStore.canvas?.setDirty(true, true)\n groupContext.graph?.change()\n workflowStore.activeWorkflow?.changeTracker?.checkState()\n bump()\n }\n })\n\n if (allSame) {\n const current = groupNodes[0].mode\n switch (current) {\n case LGraphEventMode.ALWAYS:\n options.push(\n createModeAction('Set Group Nodes to Never', LGraphEventMode.NEVER)\n )\n options.push(\n createModeAction('Bypass Group Nodes', LGraphEventMode.BYPASS)\n )\n break\n case LGraphEventMode.NEVER:\n options.push(\n createModeAction(\n 'Set Group Nodes to Always',\n LGraphEventMode.ALWAYS\n )\n )\n options.push(\n createModeAction('Bypass Group Nodes', LGraphEventMode.BYPASS)\n )\n break\n case LGraphEventMode.BYPASS:\n options.push(\n createModeAction(\n 'Set Group Nodes to Always',\n LGraphEventMode.ALWAYS\n )\n )\n options.push(\n createModeAction('Set Group Nodes to Never', LGraphEventMode.NEVER)\n )\n break\n default:\n options.push(\n createModeAction(\n 'Set Group Nodes to Always',\n LGraphEventMode.ALWAYS\n )\n )\n options.push(\n createModeAction('Set Group Nodes to Never', LGraphEventMode.NEVER)\n )\n options.push(\n createModeAction('Bypass Group Nodes', LGraphEventMode.BYPASS)\n )\n break\n }\n } else {\n options.push(\n createModeAction('Set Group Nodes to Always', LGraphEventMode.ALWAYS)\n )\n options.push(\n createModeAction('Set Group Nodes to Never', LGraphEventMode.NEVER)\n )\n options.push(\n createModeAction('Bypass Group Nodes', LGraphEventMode.BYPASS)\n )\n }\n\n return options\n }\n\n return {\n getFitGroupToNodesOption,\n getGroupShapeOptions,\n getGroupColorOptions,\n getGroupModeOptions\n }\n}\n","import { useI18n } from 'vue-i18n'\n\nimport { downloadFile } from '@/base/common/downloadUtil'\nimport type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'\nimport { useCommandStore } from '@/stores/commandStore'\n\nimport type { MenuOption } from './useMoreOptionsMenu'\n\n/**\n * Composable for image-related menu operations\n */\nexport function useImageMenuOptions() {\n const { t } = useI18n()\n\n const openMaskEditor = () => {\n const commandStore = useCommandStore()\n void commandStore.execute('Comfy.MaskEditor.OpenMaskEditor')\n }\n\n const openImage = (node: LGraphNode) => {\n if (!node?.imgs?.length) return\n const img = node.imgs[node.imageIndex ?? 0]\n if (!img) return\n const url = new URL(img.src)\n url.searchParams.delete('preview')\n window.open(url.toString(), '_blank')\n }\n\n const copyImage = async (node: LGraphNode) => {\n if (!node?.imgs?.length) return\n const img = node.imgs[node.imageIndex ?? 0]\n if (!img) return\n\n const canvas = document.createElement('canvas')\n const ctx = canvas.getContext('2d')\n if (!ctx) return\n\n canvas.width = img.naturalWidth\n canvas.height = img.naturalHeight\n ctx.drawImage(img, 0, 0)\n\n try {\n const blob = await new Promise<Blob | null>((resolve) => {\n canvas.toBlob(resolve, 'image/png')\n })\n\n if (!blob) {\n console.warn('Failed to create image blob')\n return\n }\n\n // Check if clipboard API is available\n if (!navigator.clipboard?.write) {\n console.warn('Clipboard API not available')\n return\n }\n\n await navigator.clipboard.write([\n new ClipboardItem({ 'image/png': blob })\n ])\n } catch (error) {\n console.error('Failed to copy image to clipboard:', error)\n }\n }\n\n const saveImage = (node: LGraphNode) => {\n if (!node?.imgs?.length) return\n const img = node.imgs[node.imageIndex ?? 0]\n if (!img) return\n\n try {\n const url = new URL(img.src)\n url.searchParams.delete('preview')\n downloadFile(url.toString())\n } catch (error) {\n console.error('Failed to save image:', error)\n }\n }\n\n const getImageMenuOptions = (node: LGraphNode): MenuOption[] => {\n if (!node?.imgs?.length) return []\n\n return [\n {\n label: t('contextMenu.Open in Mask Editor'),\n action: () => openMaskEditor()\n },\n {\n label: t('contextMenu.Open Image'),\n icon: 'icon-[lucide--external-link]',\n action: () => openImage(node)\n },\n {\n label: t('contextMenu.Copy Image'),\n icon: 'icon-[lucide--copy]',\n action: () => copyImage(node)\n },\n {\n label: t('contextMenu.Save Image'),\n icon: 'icon-[lucide--download]',\n action: () => saveImage(node)\n }\n ]\n }\n\n return {\n getImageMenuOptions\n }\n}\n","import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'\nimport { LGraphEventMode } from '@/lib/litegraph/src/litegraph'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { app } from '@/scripts/app'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { filterOutputNodes } from '@/utils/nodeFilterUtil'\n\n/**\n * Composable for handling node information and utility operations\n */\nexport function useSelectedNodeActions() {\n const { getSelectedNodes, toggleSelectedNodesMode } =\n useSelectedLiteGraphItems()\n const commandStore = useCommandStore()\n const workflowStore = useWorkflowStore()\n\n const adjustNodeSize = () => {\n const selectedNodes = getSelectedNodes()\n\n selectedNodes.forEach((node) => {\n const optimalSize = node.computeSize()\n node.setSize([optimalSize[0], optimalSize[1]])\n })\n\n app.canvas.setDirty(true, true)\n workflowStore.activeWorkflow?.changeTracker?.checkState()\n }\n\n const toggleNodeCollapse = () => {\n const selectedNodes = getSelectedNodes()\n selectedNodes.forEach((node) => {\n node.collapse()\n })\n\n app.canvas.setDirty(true, true)\n workflowStore.activeWorkflow?.changeTracker?.checkState()\n }\n\n const toggleNodePin = () => {\n const selectedNodes = getSelectedNodes()\n selectedNodes.forEach((node) => {\n node.pin(!node.pinned)\n })\n\n app.canvas.setDirty(true, true)\n workflowStore.activeWorkflow?.changeTracker?.checkState()\n }\n\n const toggleNodeBypass = () => {\n toggleSelectedNodesMode(LGraphEventMode.BYPASS)\n app.canvas.setDirty(true, true)\n }\n\n const runBranch = async () => {\n const selectedNodes = getSelectedNodes()\n const selectedOutputNodes = filterOutputNodes(selectedNodes)\n if (selectedOutputNodes.length === 0) return\n await commandStore.execute('Comfy.QueueSelectedOutputNodes')\n }\n\n return {\n adjustNodeSize,\n toggleNodeCollapse,\n toggleNodePin,\n toggleNodeBypass,\n runBranch\n }\n}\n","import { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport type { MenuOption } from './useMoreOptionsMenu'\nimport { useNodeCustomization } from './useNodeCustomization'\nimport { useSelectedNodeActions } from './useSelectedNodeActions'\nimport type { NodeSelectionState } from './useSelectionState'\n\n/**\n * Composable for node-related menu operations\n */\nexport function useNodeMenuOptions() {\n const { t } = useI18n()\n const { shapeOptions, applyShape, applyColor, colorOptions, isLightTheme } =\n useNodeCustomization()\n const {\n adjustNodeSize,\n toggleNodeCollapse,\n toggleNodePin,\n toggleNodeBypass,\n runBranch\n } = useSelectedNodeActions()\n\n const shapeSubmenu = computed(() =>\n shapeOptions.map((shape) => ({\n label: shape.localizedName,\n action: () => applyShape(shape)\n }))\n )\n\n const colorSubmenu = computed(() => {\n return colorOptions.map((colorOption) => ({\n label: colorOption.localizedName,\n color: isLightTheme.value\n ? colorOption.value.light\n : colorOption.value.dark,\n action: () =>\n applyColor(colorOption.name === 'noColor' ? null : colorOption)\n }))\n })\n\n const getAdjustSizeOption = (): MenuOption => ({\n label: t('contextMenu.Adjust Size'),\n icon: 'icon-[lucide--move-diagonal-2]',\n action: adjustNodeSize\n })\n\n const getNodeVisualOptions = (\n states: NodeSelectionState,\n bump: () => void\n ): MenuOption[] => [\n {\n label: states.collapsed\n ? t('contextMenu.Expand Node')\n : t('contextMenu.Minimize Node'),\n icon: states.collapsed\n ? 'icon-[lucide--maximize-2]'\n : 'icon-[lucide--minimize-2]',\n action: () => {\n toggleNodeCollapse()\n bump()\n }\n },\n {\n label: t('contextMenu.Shape'),\n icon: 'icon-[lucide--box]',\n hasSubmenu: true,\n submenu: shapeSubmenu.value,\n action: () => {}\n },\n {\n label: t('contextMenu.Color'),\n icon: 'icon-[lucide--palette]',\n hasSubmenu: true,\n submenu: colorSubmenu.value,\n isColorPicker: true,\n action: () => {}\n }\n ]\n\n const getPinOption = (\n states: NodeSelectionState,\n bump: () => void\n ): MenuOption => ({\n label: states.pinned ? t('contextMenu.Unpin') : t('contextMenu.Pin'),\n icon: states.pinned ? 'icon-[lucide--pin-off]' : 'icon-[lucide--pin]',\n action: () => {\n toggleNodePin()\n bump()\n }\n })\n\n const getBypassOption = (\n states: NodeSelectionState,\n bump: () => void\n ): MenuOption => ({\n label: states.bypassed\n ? t('contextMenu.Remove Bypass')\n : t('contextMenu.Bypass'),\n icon: 'icon-[lucide--redo-dot]',\n shortcut: 'Ctrl+B',\n action: () => {\n toggleNodeBypass()\n bump()\n }\n })\n\n const getRunBranchOption = (): MenuOption => ({\n label: t('contextMenu.Run Branch'),\n icon: 'icon-[lucide--play]',\n action: runBranch\n })\n\n const getNodeInfoOption = (showNodeHelp: () => void): MenuOption => ({\n label: t('contextMenu.Node Info'),\n icon: 'icon-[lucide--info]',\n action: showNodeHelp\n })\n\n return {\n getNodeInfoOption,\n getAdjustSizeOption,\n getNodeVisualOptions,\n getPinOption,\n getBypassOption,\n getRunBranchOption,\n colorSubmenu\n }\n}\n","import { computed } from 'vue'\n\nimport { useSelectionState } from '@/composables/graph/useSelectionState'\nimport { LGraphGroup } from '@/lib/litegraph/src/litegraph'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useTitleEditorStore } from '@/renderer/core/canvas/canvasStore'\nimport { app } from '@/scripts/app'\n\n/**\n * Composable encapsulating logic for framing currently selected nodes into a group.\n */\nexport function useFrameNodes() {\n const settingStore = useSettingStore()\n const titleEditorStore = useTitleEditorStore()\n const { hasMultipleSelection } = useSelectionState()\n\n const canFrame = computed(() => hasMultipleSelection.value)\n\n const frameNodes = () => {\n const { canvas } = app\n if (!canvas.selectedItems?.size) return\n const group = new LGraphGroup()\n const padding = settingStore.get('Comfy.GroupSelectedNodes.Padding')\n group.resizeTo(canvas.selectedItems, padding)\n canvas.graph?.add(group)\n titleEditorStore.titleEditorTarget = group\n }\n\n return { frameNodes, canFrame }\n}\n","import { useI18n } from 'vue-i18n'\n\nimport type { Direction } from '@/lib/litegraph/src/interfaces'\nimport { alignNodes, distributeNodes } from '@/lib/litegraph/src/utils/arrange'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { isLGraphNode } from '@/utils/litegraphUtil'\n\nimport { useCanvasRefresh } from './useCanvasRefresh'\n\ninterface AlignOption {\n name: string\n localizedName: string\n value: Direction\n icon: string\n}\n\ninterface DistributeOption {\n name: string\n localizedName: string\n value: boolean // true for horizontal, false for vertical\n icon: string\n}\n\n/**\n * Composable for handling node alignment and distribution\n */\nexport function useNodeArrangement() {\n const { t } = useI18n()\n const canvasStore = useCanvasStore()\n const canvasRefresh = useCanvasRefresh()\n const alignOptions: AlignOption[] = [\n {\n name: 'top',\n localizedName: t('contextMenu.Top'),\n value: 'top',\n icon: 'icon-[lucide--align-start-vertical]'\n },\n {\n name: 'bottom',\n localizedName: t('contextMenu.Bottom'),\n value: 'bottom',\n icon: 'icon-[lucide--align-end-vertical]'\n },\n {\n name: 'left',\n localizedName: t('contextMenu.Left'),\n value: 'left',\n icon: 'icon-[lucide--align-start-horizontal]'\n },\n {\n name: 'right',\n localizedName: t('contextMenu.Right'),\n value: 'right',\n icon: 'icon-[lucide--align-end-horizontal]'\n }\n ]\n\n const distributeOptions: DistributeOption[] = [\n {\n name: 'horizontal',\n localizedName: t('contextMenu.Horizontal'),\n value: true,\n icon: 'icon-[lucide--align-center-horizontal]'\n },\n {\n name: 'vertical',\n localizedName: t('contextMenu.Vertical'),\n value: false,\n icon: 'icon-[lucide--align-center-vertical]'\n }\n ]\n\n const applyAlign = (alignOption: AlignOption) => {\n const selectedNodes = Array.from(canvasStore.selectedItems).filter((item) =>\n isLGraphNode(item)\n )\n\n if (selectedNodes.length === 0) {\n return\n }\n\n const newPositions = alignNodes(selectedNodes, alignOption.value)\n canvasStore.canvas?.repositionNodesVueMode(newPositions)\n\n canvasRefresh.refreshCanvas()\n }\n\n const applyDistribute = (distributeOption: DistributeOption) => {\n const selectedNodes = Array.from(canvasStore.selectedItems).filter((item) =>\n isLGraphNode(item)\n )\n\n if (selectedNodes.length < 2) {\n return\n }\n\n const newPositions = distributeNodes(selectedNodes, distributeOption.value)\n canvasStore.canvas?.repositionNodesVueMode(newPositions)\n canvasRefresh.refreshCanvas()\n }\n\n return {\n alignOptions,\n distributeOptions,\n applyAlign,\n applyDistribute\n }\n}\n","// import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems' // Unused for now\nimport { t } from '@/i18n'\nimport { LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport { useToastStore } from '@/platform/updates/common/toastStore'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport {\n useCanvasStore,\n useTitleEditorStore\n} from '@/renderer/core/canvas/canvasStore'\nimport { app } from '@/scripts/app'\nimport { useDialogService } from '@/services/dialogService'\n\n/**\n * Composable for handling basic selection operations like copy, paste, duplicate, delete, rename\n */\nexport function useSelectionOperations() {\n // const { getSelectedNodes } = useSelectedLiteGraphItems() // Unused for now\n const canvasStore = useCanvasStore()\n const toastStore = useToastStore()\n const dialogService = useDialogService()\n const titleEditorStore = useTitleEditorStore()\n const workflowStore = useWorkflowStore()\n\n const copySelection = () => {\n const canvas = app.canvas\n if (!canvas.selectedItems || canvas.selectedItems.size === 0) {\n toastStore.add({\n severity: 'warn',\n summary: t('g.nothingToCopy'),\n detail: t('g.selectItemsToCopy'),\n life: 3000\n })\n return\n }\n\n canvas.copyToClipboard()\n toastStore.add({\n severity: 'success',\n summary: t('g.copied'),\n detail: t('g.itemsCopiedToClipboard'),\n life: 2000\n })\n }\n\n const pasteSelection = () => {\n const canvas = app.canvas\n canvas.pasteFromClipboard({ connectInputs: false })\n\n // Trigger change tracking\n workflowStore.activeWorkflow?.changeTracker?.checkState()\n }\n\n const duplicateSelection = () => {\n const canvas = app.canvas\n if (!canvas.selectedItems || canvas.selectedItems.size === 0) {\n toastStore.add({\n severity: 'warn',\n summary: t('g.nothingToDuplicate'),\n detail: t('g.selectItemsToDuplicate'),\n life: 3000\n })\n return\n }\n\n // Copy current selection\n canvas.copyToClipboard()\n\n // Clear selection to avoid confusion\n canvas.selectedItems.clear()\n canvasStore.updateSelectedItems()\n\n // Paste to create duplicates\n canvas.pasteFromClipboard({ connectInputs: false })\n\n // Trigger change tracking\n workflowStore.activeWorkflow?.changeTracker?.checkState()\n }\n\n const deleteSelection = () => {\n const canvas = app.canvas\n if (!canvas.selectedItems || canvas.selectedItems.size === 0) {\n toastStore.add({\n severity: 'warn',\n summary: t('g.nothingToDelete'),\n detail: t('g.selectItemsToDelete'),\n life: 3000\n })\n return\n }\n\n canvas.deleteSelected()\n canvas.setDirty(true, true)\n\n // Trigger change tracking\n workflowStore.activeWorkflow?.changeTracker?.checkState()\n }\n\n const renameSelection = async () => {\n const selectedItems = Array.from(canvasStore.selectedItems)\n\n // Handle single node selection\n if (selectedItems.length === 1) {\n const item = selectedItems[0]\n\n // For nodes, use the title editor\n if (item instanceof LGraphNode) {\n titleEditorStore.titleEditorTarget = item\n return\n }\n\n // For other items like groups, use prompt dialog\n const currentTitle = 'title' in item ? (item.title as string) : ''\n const newTitle = await dialogService.prompt({\n title: t('g.rename'),\n message: t('g.enterNewName'),\n defaultValue: currentTitle\n })\n\n if (newTitle && newTitle !== currentTitle) {\n if ('title' in item) {\n // Type-safe assignment for items with title property\n const titledItem = item as { title: string }\n titledItem.title = newTitle\n app.canvas.setDirty(true, true)\n workflowStore.activeWorkflow?.changeTracker?.checkState()\n }\n }\n return\n }\n\n // Handle multiple selections - batch rename\n if (selectedItems.length > 1) {\n const baseTitle = await dialogService.prompt({\n title: t('g.batchRename'),\n message: t('g.enterBaseName'),\n defaultValue: 'Item'\n })\n\n if (baseTitle) {\n selectedItems.forEach((item, index) => {\n if ('title' in item) {\n // Type-safe assignment for items with title property\n const titledItem = item as { title: string }\n titledItem.title = `${baseTitle} ${index + 1}`\n }\n })\n app.canvas.setDirty(true, true)\n workflowStore.activeWorkflow?.changeTracker?.checkState()\n }\n return\n }\n\n toastStore.add({\n severity: 'warn',\n summary: t('g.nothingToRename'),\n detail: t('g.selectItemsToRename'),\n life: 3000\n })\n }\n\n return {\n copySelection,\n pasteSelection,\n duplicateSelection,\n deleteSelection,\n renameSelection\n }\n}\n","import { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useCommandStore } from '@/stores/commandStore'\n\nimport { useFrameNodes } from './useFrameNodes'\nimport { BadgeVariant } from './useMoreOptionsMenu'\nimport type { MenuOption } from './useMoreOptionsMenu'\nimport { useNodeArrangement } from './useNodeArrangement'\nimport { useSelectionOperations } from './useSelectionOperations'\nimport { useSubgraphOperations } from './useSubgraphOperations'\n\n/**\n * Composable for selection-related menu operations\n */\nexport function useSelectionMenuOptions() {\n const { t } = useI18n()\n const {\n copySelection,\n duplicateSelection,\n deleteSelection,\n renameSelection\n } = useSelectionOperations()\n\n const { alignOptions, distributeOptions, applyAlign, applyDistribute } =\n useNodeArrangement()\n\n const { convertToSubgraph, unpackSubgraph, addSubgraphToLibrary } =\n useSubgraphOperations()\n\n const { frameNodes } = useFrameNodes()\n\n const alignSubmenu = computed(() =>\n alignOptions.map((align) => ({\n label: align.localizedName,\n icon: align.icon,\n action: () => applyAlign(align)\n }))\n )\n\n const distributeSubmenu = computed(() =>\n distributeOptions.map((distribute) => ({\n label: distribute.localizedName,\n icon: distribute.icon,\n action: () => applyDistribute(distribute)\n }))\n )\n\n const getBasicSelectionOptions = (): MenuOption[] => [\n {\n label: t('contextMenu.Rename'),\n action: renameSelection\n },\n {\n label: t('contextMenu.Copy'),\n shortcut: 'Ctrl+C',\n action: copySelection\n },\n {\n label: t('contextMenu.Duplicate'),\n shortcut: 'Ctrl+D',\n action: duplicateSelection\n }\n ]\n\n const getSubgraphOptions = ({\n hasSubgraphs,\n hasMultipleSelection\n }: {\n hasSubgraphs: boolean\n hasMultipleSelection: boolean\n }): MenuOption[] => {\n const convertOption: MenuOption = {\n label: t('contextMenu.Convert to Subgraph'),\n icon: 'icon-[lucide--shrink]',\n action: convertToSubgraph,\n badge: BadgeVariant.NEW\n }\n\n const options: MenuOption[] = []\n const showConvertOption = !hasSubgraphs || hasMultipleSelection\n\n if (showConvertOption) {\n options.push(convertOption)\n }\n\n if (hasSubgraphs) {\n options.push(\n {\n label: t('contextMenu.Add Subgraph to Library'),\n icon: 'icon-[lucide--folder-plus]',\n action: addSubgraphToLibrary\n },\n {\n label: t('contextMenu.Unpack Subgraph'),\n icon: 'icon-[lucide--expand]',\n action: unpackSubgraph\n }\n )\n }\n\n return options\n }\n\n const getMultipleNodesOptions = (): MenuOption[] => {\n const convertToGroupNodes = () => {\n const commandStore = useCommandStore()\n void commandStore.execute(\n 'Comfy.GroupNode.ConvertSelectedNodesToGroupNode'\n )\n }\n\n return [\n {\n label: t('contextMenu.Convert to Group Node'),\n icon: 'icon-[lucide--group]',\n action: convertToGroupNodes,\n badge: BadgeVariant.DEPRECATED\n },\n {\n label: t('g.frameNodes'),\n icon: 'icon-[lucide--frame]',\n action: frameNodes\n }\n ]\n }\n\n const getAlignmentOptions = (): MenuOption[] => [\n {\n label: t('contextMenu.Align Selected To'),\n icon: 'icon-[lucide--align-start-horizontal]',\n hasSubmenu: true,\n submenu: alignSubmenu.value,\n action: () => {}\n },\n {\n label: t('contextMenu.Distribute Nodes'),\n icon: 'icon-[lucide--align-center-horizontal]',\n hasSubmenu: true,\n submenu: distributeSubmenu.value,\n action: () => {}\n }\n ]\n\n const getDeleteOption = (): MenuOption => ({\n label: t('contextMenu.Delete'),\n icon: 'icon-[lucide--trash-2]',\n shortcut: 'Delete',\n action: deleteSelection\n })\n\n return {\n getBasicSelectionOptions,\n getSubgraphOptions,\n getMultipleNodesOptions,\n getDeleteOption,\n getAlignmentOptions,\n alignSubmenu,\n distributeSubmenu\n }\n}\n","import { computed, ref } from 'vue'\nimport type { Ref } from 'vue'\n\nimport type { LGraphGroup } from '@/lib/litegraph/src/litegraph'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { isLGraphGroup } from '@/utils/litegraphUtil'\n\nimport {\n buildStructuredMenu,\n convertContextMenuToOptions\n} from './contextMenuConverter'\nimport { useGroupMenuOptions } from './useGroupMenuOptions'\nimport { useImageMenuOptions } from './useImageMenuOptions'\nimport { useNodeMenuOptions } from './useNodeMenuOptions'\nimport { useSelectionMenuOptions } from './useSelectionMenuOptions'\nimport { useSelectionState } from './useSelectionState'\n\nexport interface MenuOption {\n label?: string\n icon?: string\n shortcut?: string\n hasSubmenu?: boolean\n type?: 'divider' | 'category'\n action?: () => void\n submenu?: SubMenuOption[]\n badge?: BadgeVariant\n disabled?: boolean\n source?: 'litegraph' | 'vue'\n isColorPicker?: boolean\n}\n\nexport interface SubMenuOption {\n label: string\n icon?: string\n action: () => void\n color?: string\n disabled?: boolean\n}\n\nexport enum BadgeVariant {\n NEW = 'new',\n DEPRECATED = 'deprecated'\n}\n\n// Global singleton for NodeOptions component reference\nlet nodeOptionsInstance: null | NodeOptionsInstance = null\n\n/**\n * Toggle the node options popover\n * @param event - The trigger event\n */\nexport function toggleNodeOptions(event: Event) {\n if (nodeOptionsInstance?.toggle) {\n nodeOptionsInstance.toggle(event)\n }\n}\n\n/**\n * Show the node options popover (always shows, doesn't toggle)\n * Use this for contextmenu events where we always want to show at the new position\n * @param event - The trigger event (must be MouseEvent for position)\n */\nexport function showNodeOptions(event: MouseEvent) {\n if (nodeOptionsInstance?.show) {\n nodeOptionsInstance.show(event)\n }\n}\n\n/**\n * Hide the node options popover\n */\ninterface NodeOptionsInstance {\n toggle: (event: Event) => void\n show: (event: MouseEvent) => void\n hide: () => void\n isOpen: Ref<boolean>\n}\n\n/**\n * Register the NodeOptions component instance\n * @param instance - The NodeOptions component instance\n */\nexport function registerNodeOptionsInstance(\n instance: null | NodeOptionsInstance\n) {\n nodeOptionsInstance = instance\n}\n\n/**\n * Mark menu options as coming from Vue hardcoded menu\n */\nfunction markAsVueOptions(options: MenuOption[]): MenuOption[] {\n return options.map((opt) => {\n // Don't mark dividers or category labels\n if (opt.type === 'divider' || opt.type === 'category') {\n return opt\n }\n return { ...opt, source: 'vue' }\n })\n}\n\n/**\n * Composable for managing the More Options menu configuration\n * Refactored to use smaller, focused composables for better maintainability\n */\nexport function useMoreOptionsMenu() {\n const {\n selectedItems,\n selectedNodes,\n nodeDef,\n showNodeHelp,\n hasSubgraphs: hasSubgraphsComputed,\n hasImageNode,\n hasOutputNodesSelected,\n hasMultipleSelection,\n computeSelectionFlags\n } = useSelectionState()\n\n const canvasStore = useCanvasStore()\n\n const { getImageMenuOptions } = useImageMenuOptions()\n const {\n getNodeInfoOption,\n getNodeVisualOptions,\n getPinOption,\n getBypassOption,\n getRunBranchOption\n } = useNodeMenuOptions()\n const {\n getFitGroupToNodesOption,\n getGroupColorOptions,\n getGroupModeOptions\n } = useGroupMenuOptions()\n const {\n getBasicSelectionOptions,\n getSubgraphOptions,\n getMultipleNodesOptions\n } = useSelectionMenuOptions()\n\n const hasSubgraphs = hasSubgraphsComputed\n const hasMultipleNodes = hasMultipleSelection\n\n // Internal version to force menu rebuild after state mutations\n const optionsVersion = ref(0)\n const bump = () => {\n optionsVersion.value++\n }\n\n const menuOptions = computed((): MenuOption[] => {\n // Reference selection flags to ensure re-computation when they change\n\n optionsVersion.value\n const states = computeSelectionFlags()\n\n // Detect single group selection context (and no nodes explicitly selected)\n const selectedGroups = selectedItems.value.filter(\n isLGraphGroup\n ) as LGraphGroup[]\n const groupContext: LGraphGroup | null =\n selectedGroups.length === 1 && selectedNodes.value.length === 0\n ? selectedGroups[0]\n : null\n const hasSubgraphsSelected = hasSubgraphs.value\n\n // For single node selection, also get LiteGraph menu items to merge\n const litegraphOptions: MenuOption[] = []\n if (\n selectedNodes.value.length === 1 &&\n !groupContext &&\n canvasStore.canvas\n ) {\n try {\n const node = selectedNodes.value[0]\n const rawItems = canvasStore.canvas.getNodeMenuOptions(node)\n // Don't apply structuring yet - we'll do it after merging with Vue options\n litegraphOptions.push(\n ...convertContextMenuToOptions(rawItems, node, false)\n )\n } catch (error) {\n console.error('Error getting LiteGraph menu items:', error)\n }\n }\n\n const options: MenuOption[] = []\n\n // Section 1: Basic selection operations (Rename, Copy, Duplicate)\n const basicOps = getBasicSelectionOptions()\n options.push(...basicOps)\n options.push({ type: 'divider' })\n\n // Section 2: Node actions (Run Branch, Pin, Bypass, Mute)\n if (hasOutputNodesSelected.value) {\n const runBranch = getRunBranchOption()\n options.push(runBranch)\n }\n if (!groupContext) {\n const pin = getPinOption(states, bump)\n const bypass = getBypassOption(states, bump)\n options.push(pin)\n options.push(bypass)\n }\n if (groupContext) {\n const groupModes = getGroupModeOptions(groupContext, bump)\n options.push(...groupModes)\n }\n options.push({ type: 'divider' })\n\n // Section 3: Structure operations (Convert to Subgraph, Frame selection, Minimize Node)\n options.push(\n ...getSubgraphOptions({\n hasSubgraphs: hasSubgraphsSelected,\n hasMultipleSelection: hasMultipleNodes.value\n })\n )\n if (hasMultipleNodes.value) {\n options.push(...getMultipleNodesOptions())\n }\n if (groupContext) {\n options.push(getFitGroupToNodesOption(groupContext))\n } else {\n // Node context: Expand/Minimize\n const visualOptions = getNodeVisualOptions(states, bump)\n if (visualOptions.length > 0) {\n options.push(visualOptions[0]) // Expand/Minimize (index 0)\n }\n }\n options.push({ type: 'divider' })\n\n // Section 4: Node properties (Node Info, Shape, Color)\n if (nodeDef.value) {\n options.push(getNodeInfoOption(showNodeHelp))\n }\n if (groupContext) {\n options.push(getGroupColorOptions(groupContext, bump))\n } else {\n // Add shape and color options\n const visualOptions = getNodeVisualOptions(states, bump)\n if (visualOptions.length > 1) {\n options.push(visualOptions[1]) // Shape (index 1)\n }\n if (visualOptions.length > 2) {\n options.push(visualOptions[2]) // Color (index 2)\n }\n }\n options.push({ type: 'divider' })\n\n // Section 5: Image operations (if image node)\n if (hasImageNode.value && selectedNodes.value.length > 0) {\n options.push(...getImageMenuOptions(selectedNodes.value[0]))\n options.push({ type: 'divider' })\n }\n // Section 6 & 7: Extensions and Delete are handled by buildStructuredMenu\n\n // Mark all Vue options with source\n const markedVueOptions = markAsVueOptions(options)\n\n if (litegraphOptions.length > 0) {\n // Merge: LiteGraph options first, then Vue options (Vue will win in dedup)\n const merged = [...litegraphOptions, ...markedVueOptions]\n return buildStructuredMenu(merged)\n }\n // For other cases, structure the Vue options\n const result = buildStructuredMenu(markedVueOptions)\n return result\n })\n\n // Computed property to get only menu items with submenus\n const menuOptionsWithSubmenu = computed(() =>\n menuOptions.value.filter((option) => option.hasSubmenu && option.submenu)\n )\n\n return {\n menuOptions,\n menuOptionsWithSubmenu,\n bump,\n hasSubgraphs,\n registerNodeOptionsInstance\n }\n}\n","<template>\n <Popover\n ref=\"popoverRef\"\n :auto-z-index=\"true\"\n :base-z-index=\"1100\"\n :dismissable=\"true\"\n :close-on-escape=\"true\"\n unstyled\n :pt=\"{\n root: {\n class: 'absolute z-60'\n },\n content: {\n class: [\n 'text-base-foreground rounded-lg',\n 'shadow-lg border border-base-background',\n 'bg-interface-panel-surface'\n ]\n }\n }\"\n >\n <div\n :class=\"\n isColorSubmenu\n ? 'flex flex-col gap-1 p-2'\n : 'flex flex-col p-2 min-w-40'\n \"\n >\n <div\n v-for=\"subOption in option.submenu\"\n :key=\"subOption.label\"\n :class=\"\n cn(\n 'hover:bg-secondary-background-hover rounded cursor-pointer',\n isColorSubmenu\n ? 'w-7 h-7 flex items-center justify-center'\n : 'flex items-center gap-2 px-3 py-1.5 text-sm',\n subOption.disabled\n ? 'cursor-not-allowed pointer-events-none text-node-icon-disabled'\n : 'hover:bg-secondary-background-hover'\n )\n \"\n :title=\"subOption.label\"\n @click=\"handleSubmenuClick(subOption)\"\n >\n <div\n v-if=\"subOption.color\"\n class=\"size-5 rounded-full border border-border-default\"\n :style=\"{ backgroundColor: subOption.color }\"\n />\n <template v-else-if=\"!subOption.color\">\n <i\n v-if=\"isShapeSelected(subOption)\"\n class=\"icon-[lucide--check] size-4 flex-shrink-0\"\n />\n <div v-else class=\"w-4 flex-shrink-0\" />\n <span>{{ subOption.label }}</span>\n </template>\n </div>\n </div>\n </Popover>\n</template>\n\n<script setup lang=\"ts\">\nimport { cn } from '@comfyorg/tailwind-utils'\nimport Popover from 'primevue/popover'\nimport { computed, ref } from 'vue'\n\nimport type {\n MenuOption,\n SubMenuOption\n} from '@/composables/graph/useMoreOptionsMenu'\nimport { useNodeCustomization } from '@/composables/graph/useNodeCustomization'\n\ninterface Props {\n option: MenuOption\n}\n\ninterface Emits {\n (e: 'submenu-click', subOption: SubMenuOption): void\n}\n\nconst props = defineProps<Props>()\nconst emit = defineEmits<Emits>()\n\nconst { getCurrentShape } = useNodeCustomization()\n\nconst popoverRef = ref<InstanceType<typeof Popover>>()\n\nconst toggle = (event: Event, target?: HTMLElement) => {\n popoverRef.value?.toggle(event, target)\n}\ndefineExpose({\n toggle\n})\n\nconst handleSubmenuClick = (subOption: SubMenuOption) => {\n if (subOption.disabled) {\n return\n }\n emit('submenu-click', subOption)\n popoverRef.value?.hide()\n}\n\nconst isShapeSelected = (subOption: SubMenuOption): boolean => {\n if (subOption.color) return false\n\n const currentShape = getCurrentShape()\n if (!currentShape) return false\n\n return currentShape.localizedName === subOption.label\n}\n\nconst isColorSubmenu = computed(() => {\n return (\n props.option.submenu &&\n props.option.submenu.length > 0 &&\n props.option.submenu.every((item) => item.color && !item.icon)\n )\n})\n</script>\n","<template>\n <ContextMenu\n ref=\"contextMenu\"\n :model=\"menuItems\"\n class=\"max-h-[80vh] md:max-h-none overflow-y-auto md:overflow-y-visible\"\n @show=\"onMenuShow\"\n @hide=\"onMenuHide\"\n >\n <template #item=\"{ item, props, hasSubmenu }\">\n <a\n v-bind=\"props.action\"\n class=\"flex items-center gap-2 px-3 py-1.5\"\n @click=\"item.isColorSubmenu ? showColorPopover($event) : undefined\"\n >\n <i v-if=\"item.icon\" :class=\"[item.icon, 'size-4']\" />\n <span class=\"flex-1\">{{ item.label }}</span>\n <span\n v-if=\"item.shortcut\"\n class=\"flex h-3.5 min-w-3.5 items-center justify-center rounded bg-interface-menu-keybind-surface-default px-1 py-0 text-xs\"\n >\n {{ item.shortcut }}\n </span>\n <i\n v-if=\"hasSubmenu || item.isColorSubmenu\"\n class=\"icon-[lucide--chevron-right] size-4 opacity-60\"\n />\n </a>\n </template>\n </ContextMenu>\n\n <!-- Color picker menu (custom with color circles) -->\n <ColorPickerMenu\n v-if=\"colorOption\"\n ref=\"colorPickerMenu\"\n key=\"color-picker-menu\"\n :option=\"colorOption\"\n @submenu-click=\"handleColorSelect\"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport { useElementBounding, useEventListener, useRafFn } from '@vueuse/core'\nimport ContextMenu from 'primevue/contextmenu'\nimport type { MenuItem } from 'primevue/menuitem'\nimport { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'\n\nimport {\n registerNodeOptionsInstance,\n useMoreOptionsMenu\n} from '@/composables/graph/useMoreOptionsMenu'\nimport type {\n MenuOption,\n SubMenuOption\n} from '@/composables/graph/useMoreOptionsMenu'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\n\nimport ColorPickerMenu from './selectionToolbox/ColorPickerMenu.vue'\n\ninterface ExtendedMenuItem extends MenuItem {\n isColorSubmenu?: boolean\n shortcut?: string\n originalOption?: MenuOption\n}\n\nconst contextMenu = ref<InstanceType<typeof ContextMenu>>()\nconst colorPickerMenu = ref<InstanceType<typeof ColorPickerMenu>>()\nconst isOpen = ref(false)\n\nconst { menuOptions, bump } = useMoreOptionsMenu()\nconst canvasStore = useCanvasStore()\n\n// World position (canvas coordinates) where menu was opened\nconst worldPosition = ref({ x: 0, y: 0 })\n\n// Get canvas bounding rect reactively\nconst lgCanvas = canvasStore.getCanvas()\nconst { left: canvasLeft, top: canvasTop } = useElementBounding(lgCanvas.canvas)\n\n// Track last canvas transform to detect actual changes\nlet lastScale = 0\nlet lastOffsetX = 0\nlet lastOffsetY = 0\n\n// Update menu position based on canvas transform\nconst updateMenuPosition = () => {\n if (!isOpen.value) return\n\n const menuInstance = contextMenu.value as unknown as {\n container?: HTMLElement\n }\n const menuEl = menuInstance?.container\n if (!menuEl) return\n\n const { scale, offset } = lgCanvas.ds\n\n // Only update if canvas transform actually changed\n if (\n scale === lastScale &&\n offset[0] === lastOffsetX &&\n offset[1] === lastOffsetY\n ) {\n return\n }\n\n lastScale = scale\n lastOffsetX = offset[0]\n lastOffsetY = offset[1]\n\n // Convert world position to screen position\n const screenX = (worldPosition.value.x + offset[0]) * scale + canvasLeft.value\n const screenY = (worldPosition.value.y + offset[1]) * scale + canvasTop.value\n\n // Update menu position\n menuEl.style.left = `${screenX}px`\n menuEl.style.top = `${screenY}px`\n}\n\n// Sync with canvas transform using requestAnimationFrame\nconst { resume: startSync, pause: stopSync } = useRafFn(updateMenuPosition, {\n immediate: false\n})\n\n// Start/stop syncing based on menu visibility\nwatchEffect(() => {\n if (isOpen.value) {\n startSync()\n } else {\n stopSync()\n }\n})\n\n// Close on touch outside to handle mobile devices where click might be swallowed\nuseEventListener(\n window,\n 'touchstart',\n (event: TouchEvent) => {\n if (!isOpen.value || !contextMenu.value) return\n\n const target = event.target as Node\n const contextMenuInstance = contextMenu.value as unknown as {\n container?: HTMLElement\n $el?: HTMLElement\n }\n const menuEl = contextMenuInstance.container || contextMenuInstance.$el\n\n if (menuEl && !menuEl.contains(target)) {\n hide()\n }\n },\n { passive: true }\n)\n\n// Find color picker option\nconst colorOption = computed(() =>\n menuOptions.value.find((opt) => opt.isColorPicker)\n)\n\n// Check if option is the color picker\nfunction isColorOption(option: MenuOption): boolean {\n return Boolean(option.isColorPicker)\n}\n\n// Convert MenuOption to PrimeVue MenuItem\nfunction convertToMenuItem(option: MenuOption): ExtendedMenuItem {\n if (option.type === 'divider') return { separator: true }\n\n const isColor = isColorOption(option)\n\n const item: ExtendedMenuItem = {\n label: option.label,\n icon: option.icon,\n disabled: option.disabled,\n shortcut: option.shortcut,\n isColorSubmenu: isColor,\n originalOption: option\n }\n\n // Native submenus for non-color options\n if (option.hasSubmenu && option.submenu && !isColor) {\n item.items = option.submenu.map((sub) => ({\n label: sub.label,\n icon: sub.icon,\n disabled: sub.disabled,\n command: () => {\n sub.action()\n hide()\n }\n }))\n }\n\n // Regular action items\n if (!option.hasSubmenu && option.action) {\n item.command = () => {\n option.action?.()\n hide()\n }\n }\n\n return item\n}\n\n// Build menu items\nconst menuItems = computed<ExtendedMenuItem[]>(() =>\n menuOptions.value.map(convertToMenuItem)\n)\n\n// Show context menu\nfunction show(event: MouseEvent) {\n bump()\n\n // Convert screen position to world coordinates\n // Screen position relative to canvas = event position - canvas offset\n const screenX = event.clientX - canvasLeft.value\n const screenY = event.clientY - canvasTop.value\n\n // Convert to world coordinates using canvas transform\n const { scale, offset } = lgCanvas.ds\n worldPosition.value = {\n x: screenX / scale - offset[0],\n y: screenY / scale - offset[1]\n }\n\n // Initialize last* values to current transform to prevent updateMenuPosition\n // from overwriting PrimeVue's flip-adjusted position on the first RAF tick\n lastScale = scale\n lastOffsetX = offset[0]\n lastOffsetY = offset[1]\n\n isOpen.value = true\n contextMenu.value?.show(event)\n}\n\n// Hide context menu\nfunction hide() {\n contextMenu.value?.hide()\n}\n\nfunction toggle(event: Event) {\n if (isOpen.value) {\n hide()\n } else {\n show(event as MouseEvent)\n }\n}\n\ndefineExpose({ toggle, hide, isOpen, show })\n\nfunction showColorPopover(event: MouseEvent) {\n event.stopPropagation()\n event.preventDefault()\n const target = Array.from((event.currentTarget as HTMLElement).children).find(\n (el) => el.classList.contains('icon-[lucide--chevron-right]')\n ) as HTMLElement\n colorPickerMenu.value?.toggle(event, target)\n}\n\n// Handle color selection\nfunction handleColorSelect(subOption: SubMenuOption) {\n subOption.action()\n hide()\n}\n\nfunction onMenuShow() {\n isOpen.value = true\n}\n\nfunction onMenuHide() {\n isOpen.value = false\n}\n\nonMounted(() => {\n registerNodeOptionsInstance({ toggle, show, hide, isOpen })\n})\n\nonUnmounted(() => {\n registerNodeOptionsInstance(null)\n})\n</script>\n","<template>\n <Button\n v-tooltip.top=\"{\n value: $t('g.frameNodes'),\n showDelay: 1000\n }\"\n variant=\"muted-textonly\"\n :aria-label=\"$t('g.frameNodes')\"\n @click=\"frameNodes\"\n >\n <i class=\"icon-[lucide--frame]\" />\n </Button>\n</template>\n\n<script setup lang=\"ts\">\nimport Button from '@/components/ui/button/Button.vue'\nimport { useFrameNodes } from '@/composables/graph/useFrameNodes'\n\nconst { frameNodes } = useFrameNodes()\n</script>\n","<template>\n <Button\n v-tooltip.top=\"{\n value: $t('g.moreOptions'),\n showDelay: 1000\n }\"\n data-testid=\"more-options-button\"\n variant=\"muted-textonly\"\n :aria-label=\"$t('g.moreOptions')\"\n @click=\"handleClick\"\n >\n <i class=\"icon-[lucide--more-vertical]\" />\n </Button>\n</template>\n\n<script setup lang=\"ts\">\nimport Button from '@/components/ui/button/Button.vue'\nimport { toggleNodeOptions } from '@/composables/graph/useMoreOptionsMenu'\n\nconst handleClick = (event: Event) => {\n toggleNodeOptions(event)\n}\n</script>\n","<template>\n <div class=\"h-6 w-px self-center bg-border-default\" />\n</template>\n","<template>\n <div\n ref=\"toolboxRef\"\n style=\"transform: translate(var(--tb-x), var(--tb-y))\"\n class=\"pointer-events-none fixed top-0 left-0 z-40\"\n >\n <Transition name=\"slide-up\">\n <Panel\n v-if=\"visible\"\n class=\"selection-toolbox pointer-events-auto rounded-lg border border-interface-stroke bg-interface-panel-surface\"\n :pt=\"{\n header: 'hidden',\n content: 'p-1 h-10 flex flex-row gap-1'\n }\"\n @wheel=\"canvasInteractions.forwardEventToCanvas\"\n >\n <DeleteButton v-if=\"showDelete\" />\n <VerticalDivider v-if=\"showInfoButton && showAnyPrimaryActions\" />\n <InfoButton v-if=\"showInfoButton\" />\n\n <ColorPickerButton v-if=\"showColorPicker\" />\n <FrameNodes v-if=\"showFrameNodes\" />\n <ConvertToSubgraphButton v-if=\"showConvertToSubgraph\" />\n <ConfigureSubgraph v-if=\"showSubgraphButtons\" />\n <PublishSubgraphButton v-if=\"showSubgraphButtons\" />\n <MaskEditorButton v-if=\"showMaskEditor\" />\n <VerticalDivider\n v-if=\"showAnyPrimaryActions && showAnyControlActions\"\n />\n\n <BypassButton v-if=\"showBypass\" />\n <RefreshSelectionButton v-if=\"showRefresh\" />\n <Load3DViewerButton v-if=\"showLoad3DViewer\" />\n\n <ExtensionCommandButton\n v-for=\"command in extensionToolboxCommands\"\n :key=\"command.id\"\n :command=\"command\"\n />\n <ExecuteButton v-if=\"showExecute\" />\n <NodeOptionsButton />\n </Panel>\n </Transition>\n </div>\n <NodeContextMenu />\n</template>\n\n<script setup lang=\"ts\">\nimport Panel from 'primevue/panel'\nimport { computed, ref } from 'vue'\n\nimport BypassButton from '@/components/graph/selectionToolbox/BypassButton.vue'\nimport ColorPickerButton from '@/components/graph/selectionToolbox/ColorPickerButton.vue'\nimport ConfigureSubgraph from '@/components/graph/selectionToolbox/ConfigureSubgraph.vue'\nimport ConvertToSubgraphButton from '@/components/graph/selectionToolbox/ConvertToSubgraphButton.vue'\nimport DeleteButton from '@/components/graph/selectionToolbox/DeleteButton.vue'\nimport ExecuteButton from '@/components/graph/selectionToolbox/ExecuteButton.vue'\nimport ExtensionCommandButton from '@/components/graph/selectionToolbox/ExtensionCommandButton.vue'\nimport InfoButton from '@/components/graph/selectionToolbox/InfoButton.vue'\nimport Load3DViewerButton from '@/components/graph/selectionToolbox/Load3DViewerButton.vue'\nimport MaskEditorButton from '@/components/graph/selectionToolbox/MaskEditorButton.vue'\nimport RefreshSelectionButton from '@/components/graph/selectionToolbox/RefreshSelectionButton.vue'\nimport PublishSubgraphButton from '@/components/graph/selectionToolbox/SaveToSubgraphLibrary.vue'\nimport { useSelectionToolboxPosition } from '@/composables/canvas/useSelectionToolboxPosition'\nimport { useSelectionState } from '@/composables/graph/useSelectionState'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'\nimport { useExtensionService } from '@/services/extensionService'\nimport { useCommandStore } from '@/stores/commandStore'\nimport type { ComfyCommandImpl } from '@/stores/commandStore'\n\nimport NodeContextMenu from './NodeContextMenu.vue'\nimport FrameNodes from './selectionToolbox/FrameNodes.vue'\nimport NodeOptionsButton from './selectionToolbox/NodeOptionsButton.vue'\nimport VerticalDivider from './selectionToolbox/VerticalDivider.vue'\n\nconst commandStore = useCommandStore()\nconst canvasStore = useCanvasStore()\nconst extensionService = useExtensionService()\nconst canvasInteractions = useCanvasInteractions()\n\nconst toolboxRef = ref<HTMLElement | undefined>()\nconst { visible } = useSelectionToolboxPosition(toolboxRef)\n\nconst extensionToolboxCommands = computed<ComfyCommandImpl[]>(() => {\n const commandIds = new Set<string>(\n canvasStore.selectedItems\n .map(\n (item) =>\n extensionService\n .invokeExtensions('getSelectionToolboxCommands', item)\n .flat() as string[]\n )\n .flat()\n )\n return Array.from(commandIds)\n .map((commandId) => commandStore.getCommand(commandId))\n .filter((command): command is ComfyCommandImpl => command !== undefined)\n})\n\nconst {\n hasAnySelection,\n hasMultipleSelection,\n isSingleNode,\n isSingleSubgraph,\n isSingleImageNode,\n hasAny3DNodeSelected,\n hasOutputNodesSelected,\n nodeDef\n} = useSelectionState()\nconst showInfoButton = computed(() => !!nodeDef.value)\n\nconst showColorPicker = computed(() => hasAnySelection.value)\nconst showConvertToSubgraph = computed(() => hasAnySelection.value)\nconst showFrameNodes = computed(() => hasMultipleSelection.value)\nconst showSubgraphButtons = computed(() => isSingleSubgraph.value)\n\nconst showBypass = computed(\n () =>\n isSingleNode.value || isSingleSubgraph.value || hasMultipleSelection.value\n)\nconst showLoad3DViewer = computed(() => hasAny3DNodeSelected.value)\nconst showMaskEditor = computed(() => isSingleImageNode.value)\n\nconst showDelete = computed(() => hasAnySelection.value)\nconst showRefresh = computed(() => hasAnySelection.value)\nconst showExecute = computed(() => hasOutputNodesSelected.value)\n\nconst showAnyPrimaryActions = computed(\n () =>\n showColorPicker.value ||\n showConvertToSubgraph.value ||\n showFrameNodes.value ||\n showSubgraphButtons.value\n)\n\nconst showAnyControlActions = computed(() => showBypass.value)\n</script>\n\n<style scoped>\n.selection-toolbox {\n transform: translateX(-50%) translateY(-120%);\n}\n\n@keyframes slideUp {\n 0% {\n transform: translateX(-50%) translateY(-100%);\n opacity: 0;\n }\n 50% {\n transform: translateX(-50%) translateY(-125%);\n opacity: 0.5;\n }\n 100% {\n transform: translateX(-50%) translateY(-120%);\n opacity: 1;\n }\n}\n\n.slide-up-enter-active {\n animation: slideUp 125ms ease-out;\n}\n\n.slide-up-leave-active {\n animation: slideUp 25ms ease-out reverse;\n}\n</style>\n","<template>\n <div\n v-if=\"showInput\"\n class=\"group-title-editor node-title-editor\"\n :style=\"inputStyle\"\n >\n <EditableText\n :is-editing=\"showInput\"\n :model-value=\"editedTitle\"\n @edit=\"onEdit\"\n />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useEventListener } from '@vueuse/core'\nimport { computed, ref, watch } from 'vue'\nimport type { CSSProperties } from 'vue'\n\nimport EditableText from '@/components/common/EditableText.vue'\nimport { useAbsolutePosition } from '@/composables/element/useAbsolutePosition'\nimport {\n LGraphGroup,\n LGraphNode,\n LiteGraph\n} from '@/lib/litegraph/src/litegraph'\nimport type { LiteGraphCanvasEvent } from '@/lib/litegraph/src/litegraph'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport {\n useCanvasStore,\n useTitleEditorStore\n} from '@/renderer/core/canvas/canvasStore'\nimport { app } from '@/scripts/app'\n\nconst settingStore = useSettingStore()\n\nconst showInput = ref(false)\nconst editedTitle = ref('')\nconst { style: inputPositionStyle, updatePosition } = useAbsolutePosition()\nconst inputFontStyle = ref<CSSProperties>({})\nconst inputStyle = computed<CSSProperties>(() => ({\n ...inputPositionStyle.value,\n ...inputFontStyle.value\n}))\n\nconst titleEditorStore = useTitleEditorStore()\nconst canvasStore = useCanvasStore()\nconst previousCanvasDraggable = ref(true)\n\nconst onEdit = (newValue: string) => {\n if (titleEditorStore.titleEditorTarget && newValue?.trim()) {\n const trimmedTitle = newValue.trim()\n titleEditorStore.titleEditorTarget.title = trimmedTitle\n\n // If this is a subgraph node, sync the runtime subgraph name for breadcrumb reactivity\n const target = titleEditorStore.titleEditorTarget\n if (target instanceof LGraphNode && target.isSubgraphNode?.()) {\n target.subgraph.name = trimmedTitle\n }\n\n app.canvas.setDirty(true, true)\n }\n showInput.value = false\n titleEditorStore.titleEditorTarget = null\n canvasStore.canvas!.allow_dragcanvas = previousCanvasDraggable.value\n}\n\nwatch(\n () => titleEditorStore.titleEditorTarget,\n (target) => {\n if (target === null) {\n return\n }\n editedTitle.value = target.title\n showInput.value = true\n const canvas = canvasStore.canvas!\n previousCanvasDraggable.value = canvas.allow_dragcanvas\n canvas.allow_dragcanvas = false\n const scale = canvas.ds.scale\n\n if (target instanceof LGraphGroup) {\n const group = target\n updatePosition({\n pos: group.pos,\n size: [group.size[0], group.titleHeight]\n })\n inputFontStyle.value = { fontSize: `${group.font_size * scale}px` }\n } else if (target instanceof LGraphNode) {\n const node = target\n const [x, y] = node.getBounding()\n updatePosition({\n pos: [x, y],\n size: [node.width, LiteGraph.NODE_TITLE_HEIGHT]\n })\n inputFontStyle.value = { fontSize: `${12 * scale}px` }\n }\n }\n)\n\nconst canvasEventHandler = (event: LiteGraphCanvasEvent) => {\n if (event.detail.subType === 'group-double-click') {\n if (!settingStore.get('Comfy.Group.DoubleClickTitleToEdit')) {\n return\n }\n\n const group: LGraphGroup = event.detail.group\n const [_, y] = group.pos\n\n const e = event.detail.originalEvent\n const relativeY = e.canvasY - y\n // Only allow editing if the click is on the title bar\n if (relativeY <= group.titleHeight) {\n titleEditorStore.titleEditorTarget = group\n }\n } else if (event.detail.subType === 'node-double-click') {\n if (!settingStore.get('Comfy.Node.DoubleClickTitleToEdit')) {\n return\n }\n\n const node: LGraphNode = event.detail.node\n const [_, y] = node.pos\n\n const e = event.detail.originalEvent\n const relativeY = e.canvasY - y\n // Only allow editing if the click is on the title bar\n if (relativeY <= 0) {\n titleEditorStore.titleEditorTarget = node\n }\n }\n}\n\nuseEventListener(document, 'litegraph:canvas', canvasEventHandler)\n</script>\n\n<style scoped>\n.group-title-editor.node-title-editor {\n z-index: 9999;\n padding: 0.25rem;\n}\n\n:deep(.editable-text) {\n width: 100%;\n height: 100%;\n}\n\n:deep(.editable-text input) {\n width: 100%;\n height: 100%;\n /* Override the default font size */\n font-size: inherit;\n}\n</style>\n","import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport { containsCentre, containsRect } from '@/lib/litegraph/src/measure'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\n\n/**\n * Composable for working with graph hierarchy, specifically group containment.\n */\nexport function useGraphHierarchy() {\n const canvasStore = useCanvasStore()\n\n /**\n * Finds the smallest group that contains the center of a node's bounding box.\n * When multiple groups contain the node, returns the one with the smallest area.\n *\n * TODO: This traverses the entire graph and could be very slow; needs optimization.\n * Consider spatial indexing or caching for large graphs.\n *\n * @param node - The node to find the parent group for\n * @returns The parent group if found, otherwise null\n */\n function findParentGroup(node: LGraphNode): LGraphGroup | null {\n const graphGroups = (canvasStore.canvas?.graph?.groups ??\n []) as LGraphGroup[]\n\n let parent: LGraphGroup | null = null\n\n for (const group of graphGroups) {\n const groupRect = group.boundingRect\n if (!containsCentre(groupRect, node.boundingRect)) continue\n\n if (!parent) {\n parent = group\n continue\n }\n\n const parentRect = parent.boundingRect\n const candidateInsideParent = containsRect(parentRect, groupRect)\n const parentInsideCandidate = containsRect(groupRect, parentRect)\n\n if (candidateInsideParent && !parentInsideCandidate) {\n parent = group\n continue\n }\n\n const candidateArea = groupRect[2] * groupRect[3]\n const parentArea = parentRect[2] * parentRect[3]\n\n if (candidateArea < parentArea) parent = group\n }\n\n return parent\n }\n\n return {\n findParentGroup\n }\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport NodeHelpContent from '@/components/node/NodeHelpContent.vue'\nimport type { LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport { useNodeDefStore } from '@/stores/nodeDefStore'\n\nconst { nodes } = defineProps<{\n nodes: LGraphNode[]\n}>()\nconst node = computed(() => nodes[0])\n\nconst nodeDefStore = useNodeDefStore()\n\nconst nodeInfo = computed(() => {\n return nodeDefStore.fromLGraphNode(node.value)\n})\n</script>\n\n<template>\n <div v-if=\"nodeInfo\" class=\"p-3\">\n <NodeHelpContent :node=\"nodeInfo\" />\n </div>\n</template>\n","import type { InjectionKey, MaybeRefOrGetter } from 'vue'\nimport { computed, toValue } from 'vue'\n\nimport { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'\nimport type { Positionable } from '@/lib/litegraph/src/interfaces'\nimport type { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'\nimport type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'\nimport type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'\nimport type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'\nimport { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'\n\nexport const GetNodeParentGroupKey: InjectionKey<\n (node: LGraphNode) => LGraphGroup | null\n> = Symbol('getNodeParentGroup')\n\nexport type NodeWidgetsList = Array<{ node: LGraphNode; widget: IBaseWidget }>\nexport type NodeWidgetsListList = Array<{\n node: LGraphNode\n widgets: NodeWidgetsList\n}>\n\n/**\n * Searches widgets in a list and returns search results.\n * Filters by name, localized label, type, and user-input value.\n * Performs basic tokenization of the query string.\n */\nexport function searchWidgets<T extends { widget: IBaseWidget }[]>(\n list: T,\n query: string\n): T {\n if (query.trim() === '') {\n return list\n }\n const words = query.trim().toLowerCase().split(' ')\n return list.filter(({ widget }) => {\n const label = widget.label?.toLowerCase()\n const name = widget.name.toLowerCase()\n const type = widget.type.toLowerCase()\n const value = widget.value?.toString().toLowerCase()\n return words.every(\n (word) =>\n name.includes(word) ||\n label?.includes(word) ||\n type?.includes(word) ||\n value?.includes(word)\n )\n }) as T\n}\n\n/**\n * Searches widgets and nodes in a list and returns search results.\n * First checks if the node title matches the query (if so, keeps entire node).\n * Otherwise, filters widgets using searchWidgets.\n * Performs basic tokenization of the query string.\n */\nexport function searchWidgetsAndNodes(\n list: NodeWidgetsListList,\n query: string\n): NodeWidgetsListList {\n if (query.trim() === '') {\n return list\n }\n const words = query.trim().toLowerCase().split(' ')\n return list\n .map((item) => {\n const { node } = item\n const title = node.getTitle().toLowerCase()\n if (words.every((word) => title.includes(word))) {\n return { ...item, keep: true }\n }\n return {\n ...item,\n keep: false,\n widgets: searchWidgets(item.widgets, query)\n }\n })\n .filter((item) => item.keep || item.widgets.length > 0)\n}\n\ntype MixedSelectionItem = LGraphGroup | LGraphNode\ntype FlatAndCategorizeSelectedItemsResult = {\n all: MixedSelectionItem[]\n nodes: LGraphNode[]\n groups: LGraphGroup[]\n others: Positionable[]\n nodeToParentGroup: Map<LGraphNode, LGraphGroup>\n}\n\ntype FlatItemsContext = {\n nodeToParentGroup: Map<LGraphNode, LGraphGroup>\n depth: number\n parentGroup?: LGraphGroup\n}\n\n/**\n * The selected items may contain \"Group\" nodes, which can include child nodes.\n * This function flattens such structures and categorizes items into:\n * - all: all categorizable nodes (does not include nodes in \"others\")\n * - nodes: node items\n * - groups: group items\n * - others: items not currently supported\n * - nodeToParentGroup: a map from each node to its direct parent group (if any)\n * @param items The selected items to flatten and categorize\n * @returns An object containing arrays: all, nodes, groups, others, and nodeToParentGroup map\n */\nexport function flatAndCategorizeSelectedItems(\n items: Positionable[]\n): FlatAndCategorizeSelectedItemsResult {\n const ctx: FlatItemsContext = {\n nodeToParentGroup: new Map<LGraphNode, LGraphGroup>(),\n depth: 0\n }\n const { all, nodes, groups, others } = flatItems(items, ctx)\n return {\n all: repeatItems(all),\n nodes: repeatItems(nodes),\n groups: repeatItems(groups),\n others: repeatItems(others),\n nodeToParentGroup: ctx.nodeToParentGroup\n }\n}\n\nexport function useFlatAndCategorizeSelectedItems(\n items: MaybeRefOrGetter<Positionable[]>\n) {\n const result = computed(() => flatAndCategorizeSelectedItems(toValue(items)))\n\n return {\n flattedItems: computed(() => result.value.all),\n selectedNodes: computed(() => result.value.nodes),\n selectedGroups: computed(() => result.value.groups),\n selectedOthers: computed(() => result.value.others),\n nodeToParentGroup: computed(() => result.value.nodeToParentGroup)\n }\n}\n\nfunction flatItems(\n items: Positionable[],\n ctx: FlatItemsContext\n): Omit<FlatAndCategorizeSelectedItemsResult, 'nodeToParentGroup'> {\n const result: MixedSelectionItem[] = []\n const nodes: LGraphNode[] = []\n const groups: LGraphGroup[] = []\n const others: Positionable[] = []\n\n if (ctx.depth > 1000) {\n return {\n all: [],\n nodes: [],\n groups: [],\n others: []\n }\n }\n\n for (let i = 0; i < items.length; i++) {\n const item = items[i] as Positionable\n\n if (isLGraphGroup(item)) {\n result.push(item)\n groups.push(item)\n\n const children = Array.from(item.children)\n const childCtx: FlatItemsContext = {\n nodeToParentGroup: ctx.nodeToParentGroup,\n depth: ctx.depth + 1,\n parentGroup: item\n }\n const {\n all: childAll,\n nodes: childNodes,\n groups: childGroups,\n others: childOthers\n } = flatItems(children, childCtx)\n result.push(...childAll)\n nodes.push(...childNodes)\n groups.push(...childGroups)\n others.push(...childOthers)\n } else if (isLGraphNode(item)) {\n result.push(item)\n nodes.push(item)\n if (ctx.parentGroup) {\n ctx.nodeToParentGroup.set(item, ctx.parentGroup)\n }\n } else {\n // Other types of items are not supported yet\n // Do not add to all\n others.push(item)\n }\n }\n return {\n all: result,\n nodes,\n groups,\n others\n }\n}\n\nfunction repeatItems<T>(items: T[]): T[] {\n const itemSet = new Set<T>()\n const result: T[] = []\n for (const item of items) {\n if (itemSet.has(item)) continue\n itemSet.add(item)\n result.push(item)\n }\n return result\n}\n\n/**\n * Renames a widget and its corresponding input.\n * Handles both regular widgets and proxy widgets in subgraphs.\n *\n * @param widget The widget to rename\n * @param node The node containing the widget\n * @param newLabel The new label for the widget (empty string or undefined to clear)\n * @param parents Optional array of parent SubgraphNodes (for proxy widgets)\n * @returns true if the rename was successful, false otherwise\n */\nexport function renameWidget(\n widget: IBaseWidget,\n node: LGraphNode,\n newLabel: string,\n parents?: SubgraphNode[]\n): boolean {\n // For proxy widgets in subgraphs, we need to rename the original interior widget\n if (isProxyWidget(widget) && parents?.length) {\n const subgraph = parents[0].subgraph\n if (!subgraph) {\n console.error('Could not find subgraph for proxy widget')\n return false\n }\n const interiorNode = subgraph.getNodeById(parseInt(widget._overlay.nodeId))\n\n if (!interiorNode) {\n console.error('Could not find interior node for proxy widget')\n return false\n }\n\n const originalWidget = interiorNode.widgets?.find(\n (w) => w.name === widget._overlay.widgetName\n )\n\n if (!originalWidget) {\n console.error('Could not find original widget for proxy widget')\n return false\n }\n\n // Rename the original widget\n originalWidget.label = newLabel || undefined\n\n // Also rename the corresponding input on the interior node\n const interiorInput = interiorNode.inputs?.find(\n (inp) => inp.widget?.name === widget._overlay.widgetName\n )\n if (interiorInput) {\n interiorInput.label = newLabel || undefined\n }\n }\n\n // Always rename the widget on the current node (either regular widget or proxy widget)\n const input = node.inputs?.find((inp) => inp.widget?.name === widget.name)\n\n // Intentionally mutate the widget object here as it's a reference\n // to the actual widget in the graph\n widget.label = newLabel || undefined\n if (input) {\n input.label = newLabel || undefined\n }\n\n return true\n}\n","<script setup lang=\"ts\">\nimport { computed, onMounted, ref } from 'vue'\n\n// From: https://stackoverflow.com/a/71426342/22392721\ninterface Props {\n duration?: number\n easingEnter?: string\n easingLeave?: string\n opacityClosed?: number\n opacityOpened?: number\n disable?: boolean\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n duration: 150,\n easingEnter: 'ease-in-out',\n easingLeave: 'ease-in-out',\n opacityClosed: 0,\n opacityOpened: 1\n})\n\nconst closed = '0px'\n\nconst isMounted = ref(false)\nonMounted(() => (isMounted.value = true))\n\nconst duration = computed(() =>\n isMounted.value && !props.disable ? props.duration : 0\n)\n\ninterface initialStyle {\n height: string\n width: string\n position: string\n visibility: string\n overflow: string\n paddingTop: string\n paddingBottom: string\n borderTopWidth: string\n borderBottomWidth: string\n marginTop: string\n marginBottom: string\n}\n\nfunction getElementStyle(element: HTMLElement) {\n return {\n height: element.style.height,\n width: element.style.width,\n position: element.style.position,\n visibility: element.style.visibility,\n overflow: element.style.overflow,\n paddingTop: element.style.paddingTop,\n paddingBottom: element.style.paddingBottom,\n borderTopWidth: element.style.borderTopWidth,\n borderBottomWidth: element.style.borderBottomWidth,\n marginTop: element.style.marginTop,\n marginBottom: element.style.marginBottom\n }\n}\n\nfunction prepareElement(element: HTMLElement, initialStyle: initialStyle) {\n const { width } = getComputedStyle(element)\n element.style.width = width\n element.style.position = 'absolute'\n element.style.visibility = 'hidden'\n element.style.height = ''\n const { height } = getComputedStyle(element)\n element.style.width = initialStyle.width\n element.style.position = initialStyle.position\n element.style.visibility = initialStyle.visibility\n element.style.height = closed\n element.style.overflow = 'hidden'\n return initialStyle.height && initialStyle.height !== closed\n ? initialStyle.height\n : height\n}\n\nfunction animateTransition(\n element: HTMLElement,\n initialStyle: initialStyle,\n done: () => void,\n keyframes: Keyframe[] | PropertyIndexedKeyframes | null,\n options?: number | KeyframeAnimationOptions\n) {\n const animation = element.animate(keyframes, options)\n // Set height to 'auto' to restore it after animation\n element.style.height = initialStyle.height\n animation.onfinish = () => {\n element.style.overflow = initialStyle.overflow\n done()\n }\n}\n\nfunction getEnterKeyframes(height: string, initialStyle: initialStyle) {\n return [\n {\n height: closed,\n opacity: props.opacityClosed,\n paddingTop: closed,\n paddingBottom: closed,\n borderTopWidth: closed,\n borderBottomWidth: closed,\n marginTop: closed,\n marginBottom: closed\n },\n {\n height,\n opacity: props.opacityOpened,\n paddingTop: initialStyle.paddingTop,\n paddingBottom: initialStyle.paddingBottom,\n borderTopWidth: initialStyle.borderTopWidth,\n borderBottomWidth: initialStyle.borderBottomWidth,\n marginTop: initialStyle.marginTop,\n marginBottom: initialStyle.marginBottom\n }\n ]\n}\n\nfunction enterTransition(element: Element, done: () => void) {\n const HTMLElement = element as HTMLElement\n const initialStyle = getElementStyle(HTMLElement)\n const height = prepareElement(HTMLElement, initialStyle)\n const keyframes = getEnterKeyframes(height, initialStyle)\n const options = { duration: duration.value, easing: props.easingEnter }\n animateTransition(HTMLElement, initialStyle, done, keyframes, options)\n}\n\nfunction leaveTransition(element: Element, done: () => void) {\n const HTMLElement = element as HTMLElement\n const initialStyle = getElementStyle(HTMLElement)\n const { height } = getComputedStyle(HTMLElement)\n HTMLElement.style.height = height\n HTMLElement.style.overflow = 'hidden'\n const keyframes = getEnterKeyframes(height, initialStyle).reverse()\n const options = { duration: duration.value, easing: props.easingLeave }\n animateTransition(HTMLElement, initialStyle, done, keyframes, options)\n}\n</script>\n\n<template>\n <Transition :css=\"false\" @enter=\"enterTransition\" @leave=\"leaveTransition\">\n <slot />\n </Transition>\n</template>\n","<script lang=\"ts\" setup>\nimport { computed } from 'vue'\n\nimport { cn } from '@/utils/tailwindUtil'\n\nimport TransitionCollapse from './TransitionCollapse.vue'\n\nconst props = defineProps<{\n disabled?: boolean\n label?: string\n enableEmptyState?: boolean\n tooltip?: string\n}>()\n\nconst isCollapse = defineModel<boolean>('collapse', { default: false })\n\nconst isExpanded = computed(() => !isCollapse.value && !props.disabled)\n\nconst tooltipConfig = computed(() => {\n if (!props.tooltip) return undefined\n return { value: props.tooltip, showDelay: 1000 }\n})\n</script>\n\n<template>\n <div class=\"flex flex-col bg-comfy-menu-bg\">\n <div\n class=\"sticky top-0 z-10 flex items-center justify-between backdrop-blur-xl bg-inherit\"\n >\n <button\n v-tooltip=\"tooltipConfig\"\n type=\"button\"\n :class=\"\n cn(\n 'group min-h-12 bg-transparent border-0 outline-0 ring-0 w-full text-left flex items-center justify-between pl-4 pr-3',\n !disabled && 'cursor-pointer'\n )\n \"\n :disabled=\"disabled\"\n @click=\"isCollapse = !isCollapse\"\n >\n <span class=\"text-sm font-semibold line-clamp-2 flex-1\">\n <slot name=\"label\">\n {{ label }}\n </slot>\n </span>\n\n <i\n :class=\"\n cn(\n 'text-muted-foreground group-hover:text-base-foreground group-has-[.subbutton:hover]:text-muted-foreground group-focus:text-base-foreground icon-[lucide--chevron-up] size-4 transition-all',\n isCollapse && '-rotate-180',\n disabled && 'opacity-0'\n )\n \"\n />\n </button>\n </div>\n <TransitionCollapse>\n <div v-if=\"isExpanded\" class=\"pb-4\">\n <slot />\n </div>\n <slot v-else-if=\"enableEmptyState && disabled\" name=\"empty\">\n <div>\n {{ $t('g.empty') }}\n </div>\n </slot>\n </TransitionCollapse>\n </div>\n</template>\n","/**\n * Simplified widget interface for Vue-based node rendering\n * Removes all DOM manipulation and positioning concerns\n */\nimport type { InputSpec as InputSpecV2 } from '@/schemas/nodeDef/nodeDefSchemaV2'\n\n/** Valid types for widget values */\nexport type WidgetValue =\n | string\n | number\n | boolean\n | object\n | undefined\n | null\n | void\n | File[]\n\nconst CONTROL_OPTIONS = [\n 'fixed',\n 'increment',\n 'decrement',\n 'randomize'\n] as const\nexport type ControlOptions = (typeof CONTROL_OPTIONS)[number]\n\nfunction isControlOption(val: WidgetValue): val is ControlOptions {\n return CONTROL_OPTIONS.includes(val as ControlOptions)\n}\n\nexport function normalizeControlOption(val: WidgetValue): ControlOptions {\n if (isControlOption(val)) return val\n return 'randomize'\n}\n\nexport type SafeControlWidget = {\n value: ControlOptions\n update: (value: WidgetValue) => void\n}\n\nexport interface SimplifiedWidget<\n T extends WidgetValue = WidgetValue,\n O = Record<string, any>\n> {\n /** Display name of the widget */\n name: string\n\n /** Widget type identifier (e.g., 'STRING', 'INT', 'COMBO') */\n type: string\n\n /** Current value of the widget */\n value: T\n\n borderStyle?: string\n\n /** Callback fired when value changes */\n callback?: (value: T) => void\n\n /** Optional method to compute widget size requirements */\n computeSize?: () => { minHeight: number; maxHeight?: number }\n\n /** Localized display label (falls back to name if not provided) */\n label?: string\n\n /** Widget options including filtered PrimeVue props */\n options?: O\n\n /** Override for use with subgraph promoted asset widgets*/\n nodeType?: string\n\n /** Optional serialization method for custom value handling */\n serializeValue?: () => any\n\n /** Optional input specification backing this widget */\n spec?: InputSpecV2\n\n controlWidget?: SafeControlWidget\n}\n\nexport interface SimplifiedControlWidget<\n T extends WidgetValue = WidgetValue,\n O = Record<string, any>\n> extends SimplifiedWidget<T, O> {\n controlWidget: SafeControlWidget\n}\n","/**\n * Vue node lifecycle management for LiteGraph integration\n * Provides event-driven reactivity with performance optimizations\n */\nimport { reactiveComputed } from '@vueuse/core'\nimport { customRef, reactive, shallowReactive } from 'vue'\n\nimport { useChainCallback } from '@/composables/functional/useChainCallback'\nimport { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'\nimport type {\n INodeInputSlot,\n INodeOutputSlot\n} from '@/lib/litegraph/src/interfaces'\nimport type {\n IBaseWidget,\n IWidgetOptions\n} from '@/lib/litegraph/src/types/widgets'\nimport { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'\nimport { LayoutSource } from '@/renderer/core/layout/types'\nimport type { NodeId } from '@/renderer/core/layout/types'\nimport type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'\nimport { isDOMWidget } from '@/scripts/domWidget'\nimport { useNodeDefStore } from '@/stores/nodeDefStore'\nimport type { WidgetValue, SafeControlWidget } from '@/types/simplifiedWidget'\nimport { normalizeControlOption } from '@/types/simplifiedWidget'\n\nimport type {\n LGraph,\n LGraphBadge,\n LGraphNode,\n LGraphTriggerAction,\n LGraphTriggerEvent,\n LGraphTriggerParam\n} from '@/lib/litegraph/src/litegraph'\nimport type { TitleMode } from '@/lib/litegraph/src/types/globalEnums'\nimport { NodeSlotType } from '@/lib/litegraph/src/types/globalEnums'\nimport { app } from '@/scripts/app'\n\nexport interface WidgetSlotMetadata {\n index: number\n linked: boolean\n}\n\nexport interface SafeWidgetData {\n name: string\n type: string\n value: WidgetValue\n borderStyle?: string\n callback?: ((value: unknown) => void) | undefined\n controlWidget?: SafeControlWidget\n hasLayoutSize?: boolean\n isDOMWidget?: boolean\n label?: string\n nodeType?: string\n options?: IWidgetOptions<unknown>\n spec?: InputSpec\n slotMetadata?: WidgetSlotMetadata\n}\n\nexport interface VueNodeData {\n executing: boolean\n id: NodeId\n mode: number\n selected: boolean\n title: string\n type: string\n apiNode?: boolean\n badges?: (LGraphBadge | (() => LGraphBadge))[]\n bgcolor?: string\n color?: string\n flags?: {\n collapsed?: boolean\n pinned?: boolean\n }\n hasErrors?: boolean\n inputs?: INodeInputSlot[]\n outputs?: INodeOutputSlot[]\n resizable?: boolean\n shape?: number\n subgraphId?: string | null\n titleMode?: TitleMode\n widgets?: SafeWidgetData[]\n}\n\nexport interface GraphNodeManager {\n // Reactive state - safe data extracted from LiteGraph nodes\n vueNodeData: ReadonlyMap<string, VueNodeData>\n\n // Access to original LiteGraph nodes (non-reactive)\n getNode(id: string): LGraphNode | undefined\n\n // Lifecycle methods\n cleanup(): void\n}\n\nfunction widgetWithVueTrack(\n widget: IBaseWidget\n): asserts widget is IBaseWidget & { vueTrack: () => void } {\n if (widget.vueTrack) return\n\n customRef((track, trigger) => {\n widget.callback = useChainCallback(widget.callback, trigger)\n widget.vueTrack = track\n return { get() {}, set() {} }\n })\n}\nfunction useReactiveWidgetValue(widget: IBaseWidget) {\n widgetWithVueTrack(widget)\n widget.vueTrack()\n return widget.value\n}\n\nfunction getControlWidget(widget: IBaseWidget): SafeControlWidget | undefined {\n const cagWidget = widget.linkedWidgets?.find(\n (w) => w.name == 'control_after_generate'\n )\n if (!cagWidget) return\n return {\n value: normalizeControlOption(cagWidget.value),\n update: (value) => (cagWidget.value = normalizeControlOption(value))\n }\n}\n\nfunction getNodeType(node: LGraphNode, widget: IBaseWidget) {\n if (!node.isSubgraphNode() || !isProxyWidget(widget)) return undefined\n const subNode = node.subgraph.getNodeById(widget._overlay.nodeId)\n return subNode?.type\n}\n\n/**\n * Shared widget enhancements used by both safeWidgetMapper and Right Side Panel\n */\ninterface SharedWidgetEnhancements {\n /** Reactive widget value that updates when the widget changes */\n value: WidgetValue\n /** Control widget for seed randomization/increment/decrement */\n controlWidget?: SafeControlWidget\n /** Input specification from node definition */\n spec?: InputSpec\n /** Node type (for subgraph promoted widgets) */\n nodeType?: string\n /** Border style for promoted/advanced widgets */\n borderStyle?: string\n /** Widget label */\n label?: string\n /** Widget options */\n options?: Record<string, any>\n}\n\n/**\n * Extracts common widget enhancements shared across different rendering contexts.\n * This function centralizes the logic for extracting metadata and reactive values\n * from widgets, ensuring consistency between Nodes 2.0 and Right Side Panel.\n */\nexport function getSharedWidgetEnhancements(\n node: LGraphNode,\n widget: IBaseWidget\n): SharedWidgetEnhancements {\n const nodeDefStore = useNodeDefStore()\n\n return {\n value: useReactiveWidgetValue(widget),\n controlWidget: getControlWidget(widget),\n spec: nodeDefStore.getInputSpecForWidget(node, widget.name),\n nodeType: getNodeType(node, widget),\n borderStyle: widget.promoted\n ? 'ring ring-component-node-widget-promoted'\n : widget.advanced\n ? 'ring ring-component-node-widget-advanced'\n : undefined,\n label: widget.label,\n options: widget.options\n }\n}\n\n/**\n * Validates that a value is a valid WidgetValue type\n */\nconst normalizeWidgetValue = (value: unknown): WidgetValue => {\n if (value === null || value === undefined || value === void 0) {\n return undefined\n }\n if (\n typeof value === 'string' ||\n typeof value === 'number' ||\n typeof value === 'boolean'\n ) {\n return value\n }\n if (typeof value === 'object') {\n // Check if it's a File array\n if (\n Array.isArray(value) &&\n value.length > 0 &&\n value.every((item): item is File => item instanceof File)\n ) {\n return value\n }\n // Otherwise it's a generic object\n return value\n }\n // If none of the above, return undefined\n console.warn(`Invalid widget value type: ${typeof value}`, value)\n return undefined\n}\n\nfunction safeWidgetMapper(\n node: LGraphNode,\n slotMetadata: Map<string, WidgetSlotMetadata>\n): (widget: IBaseWidget) => SafeWidgetData {\n return function (widget) {\n try {\n // Get shared enhancements used by both Nodes 2.0 and Right Side Panel\n const sharedEnhancements = getSharedWidgetEnhancements(node, widget)\n const slotInfo = slotMetadata.get(widget.name)\n\n // Wrapper callback specific to Nodes 2.0 rendering\n const callback = (v: unknown) => {\n const value = normalizeWidgetValue(v)\n widget.value = value ?? undefined\n // Match litegraph callback signature: (value, canvas, node, pos, event)\n // Some extensions (e.g., Impact Pack) expect node as the 3rd parameter\n widget.callback?.(value, app.canvas, node)\n // Trigger redraw for all legacy widgets on this node (e.g., mask preview)\n // This ensures widgets that depend on other widget values get updated\n node.widgets?.forEach((w) => w.triggerDraw?.())\n }\n\n return {\n name: widget.name,\n type: widget.type,\n ...sharedEnhancements,\n callback,\n hasLayoutSize: typeof widget.computeLayoutSize === 'function',\n isDOMWidget: isDOMWidget(widget),\n slotMetadata: slotInfo\n }\n } catch (error) {\n return {\n name: widget.name || 'unknown',\n type: widget.type || 'text',\n value: undefined\n }\n }\n }\n}\n\n// Extract safe data from LiteGraph node for Vue consumption\nexport function extractVueNodeData(node: LGraphNode): VueNodeData {\n // Determine subgraph ID - null for root graph, string for subgraphs\n const subgraphId =\n node.graph && 'id' in node.graph && node.graph !== node.graph.rootGraph\n ? String(node.graph.id)\n : null\n // Extract safe widget data\n const slotMetadata = new Map<string, WidgetSlotMetadata>()\n\n const reactiveWidgets = shallowReactive<IBaseWidget[]>(node.widgets ?? [])\n Object.defineProperty(node, 'widgets', {\n get() {\n return reactiveWidgets\n },\n set(v) {\n reactiveWidgets.splice(0, reactiveWidgets.length, ...v)\n }\n })\n const reactiveInputs = shallowReactive<INodeInputSlot[]>(node.inputs ?? [])\n Object.defineProperty(node, 'inputs', {\n get() {\n return reactiveInputs\n },\n set(v) {\n reactiveInputs.splice(0, reactiveInputs.length, ...v)\n }\n })\n\n const safeWidgets = reactiveComputed<SafeWidgetData[]>(() => {\n node.inputs?.forEach((input, index) => {\n if (!input?.widget?.name) return\n slotMetadata.set(input.widget.name, {\n index,\n linked: input.link != null\n })\n })\n return node.widgets?.map(safeWidgetMapper(node, slotMetadata)) ?? []\n })\n\n const nodeType =\n node.type ||\n node.constructor?.comfyClass ||\n node.constructor?.title ||\n node.constructor?.name ||\n 'Unknown'\n\n const apiNode = node.constructor?.nodeData?.api_node ?? false\n const badges = node.badges\n\n return {\n id: String(node.id),\n title: typeof node.title === 'string' ? node.title : '',\n type: nodeType,\n mode: node.mode || 0,\n titleMode: node.title_mode,\n selected: node.selected || false,\n executing: false, // Will be updated separately based on execution state\n subgraphId,\n apiNode,\n badges,\n hasErrors: !!node.has_errors,\n widgets: safeWidgets,\n inputs: reactiveInputs,\n outputs: node.outputs ? [...node.outputs] : undefined,\n flags: node.flags ? { ...node.flags } : undefined,\n color: node.color || undefined,\n bgcolor: node.bgcolor || undefined,\n resizable: node.resizable,\n shape: node.shape\n }\n}\n\nexport function useGraphNodeManager(graph: LGraph): GraphNodeManager {\n // Get layout mutations composable\n const { createNode, deleteNode, setSource } = useLayoutMutations()\n // Safe reactive data extracted from LiteGraph nodes\n const vueNodeData = reactive(new Map<string, VueNodeData>())\n\n // Non-reactive storage for original LiteGraph nodes\n const nodeRefs = new Map<string, LGraphNode>()\n\n const refreshNodeSlots = (nodeId: string) => {\n const nodeRef = nodeRefs.get(nodeId)\n const currentData = vueNodeData.get(nodeId)\n\n if (!nodeRef || !currentData) return\n\n // Only extract slot-related data instead of full node re-extraction\n const slotMetadata = new Map<string, WidgetSlotMetadata>()\n\n nodeRef.inputs?.forEach((input, index) => {\n if (!input?.widget?.name) return\n slotMetadata.set(input.widget.name, {\n index,\n linked: input.link != null\n })\n })\n\n // Update only widgets with new slot metadata, keeping other widget data intact\n for (const widget of currentData.widgets ?? []) {\n const slotInfo = slotMetadata.get(widget.name)\n if (slotInfo) widget.slotMetadata = slotInfo\n }\n }\n\n // Get access to original LiteGraph node (non-reactive)\n const getNode = (id: string): LGraphNode | undefined => {\n return nodeRefs.get(id)\n }\n\n const syncWithGraph = () => {\n if (!graph?._nodes) return\n\n const currentNodes = new Set(graph._nodes.map((n) => String(n.id)))\n\n // Remove deleted nodes\n for (const id of Array.from(vueNodeData.keys())) {\n if (!currentNodes.has(id)) {\n nodeRefs.delete(id)\n vueNodeData.delete(id)\n }\n }\n\n // Add/update existing nodes\n graph._nodes.forEach((node) => {\n const id = String(node.id)\n\n // Store non-reactive reference\n nodeRefs.set(id, node)\n\n // Extract and store safe data for Vue\n vueNodeData.set(id, extractVueNodeData(node))\n })\n }\n\n /**\n * Handles node addition to the graph - sets up Vue state and spatial indexing\n * Defers position extraction until after potential configure() calls\n */\n const handleNodeAdded = (\n node: LGraphNode,\n originalCallback?: (node: LGraphNode) => void\n ) => {\n const id = String(node.id)\n\n // Store non-reactive reference to original node\n nodeRefs.set(id, node)\n\n // Extract initial data for Vue (may be incomplete during graph configure)\n vueNodeData.set(id, extractVueNodeData(node))\n\n const initializeVueNodeLayout = () => {\n // Extract actual positions after configure() has potentially updated them\n const nodePosition = { x: node.pos[0], y: node.pos[1] }\n const nodeSize = { width: node.size[0], height: node.size[1] }\n\n // Add node to layout store with final positions\n setSource(LayoutSource.Canvas)\n void createNode(id, {\n position: nodePosition,\n size: nodeSize,\n zIndex: node.order || 0,\n visible: true\n })\n }\n\n // Check if we're in the middle of configuring the graph (workflow loading)\n if (window.app?.configuringGraph) {\n // During workflow loading - defer layout initialization until configure completes\n // Chain our callback with any existing onAfterGraphConfigured callback\n node.onAfterGraphConfigured = useChainCallback(\n node.onAfterGraphConfigured,\n () => {\n // Re-extract data now that configure() has populated title/slots/widgets/etc.\n vueNodeData.set(id, extractVueNodeData(node))\n initializeVueNodeLayout()\n }\n )\n } else {\n // Not during workflow loading - initialize layout immediately\n // This handles individual node additions during normal operation\n initializeVueNodeLayout()\n }\n\n // Call original callback if provided\n if (originalCallback) {\n void originalCallback(node)\n }\n }\n\n /**\n * Handles node removal from the graph - cleans up all references\n */\n const handleNodeRemoved = (\n node: LGraphNode,\n originalCallback?: (node: LGraphNode) => void\n ) => {\n const id = String(node.id)\n\n // Remove node from layout store\n setSource(LayoutSource.Canvas)\n void deleteNode(id)\n\n // Clean up all tracking references\n nodeRefs.delete(id)\n vueNodeData.delete(id)\n\n // Call original callback if provided\n if (originalCallback) {\n originalCallback(node)\n }\n }\n\n /**\n * Creates cleanup function for event listeners and state\n */\n const createCleanupFunction = (\n originalOnNodeAdded: ((node: LGraphNode) => void) | undefined,\n originalOnNodeRemoved: ((node: LGraphNode) => void) | undefined,\n originalOnTrigger: ((event: LGraphTriggerEvent) => void) | undefined\n ) => {\n return () => {\n // Restore original callbacks\n graph.onNodeAdded = originalOnNodeAdded || undefined\n graph.onNodeRemoved = originalOnNodeRemoved || undefined\n graph.onTrigger = originalOnTrigger || undefined\n\n // Clear all state maps\n nodeRefs.clear()\n vueNodeData.clear()\n }\n }\n\n /**\n * Sets up event listeners - now simplified with extracted handlers\n */\n const setupEventListeners = (): (() => void) => {\n // Store original callbacks\n const originalOnNodeAdded = graph.onNodeAdded\n const originalOnNodeRemoved = graph.onNodeRemoved\n const originalOnTrigger = graph.onTrigger\n\n // Set up graph event handlers\n graph.onNodeAdded = (node: LGraphNode) => {\n handleNodeAdded(node, originalOnNodeAdded)\n }\n\n graph.onNodeRemoved = (node: LGraphNode) => {\n handleNodeRemoved(node, originalOnNodeRemoved)\n }\n\n const triggerHandlers: {\n [K in LGraphTriggerAction]: (event: LGraphTriggerParam<K>) => void\n } = {\n 'node:property:changed': (propertyEvent) => {\n const nodeId = String(propertyEvent.nodeId)\n const currentData = vueNodeData.get(nodeId)\n\n if (currentData) {\n switch (propertyEvent.property) {\n case 'title':\n vueNodeData.set(nodeId, {\n ...currentData,\n title: String(propertyEvent.newValue)\n })\n break\n case 'flags.collapsed':\n vueNodeData.set(nodeId, {\n ...currentData,\n flags: {\n ...currentData.flags,\n collapsed: Boolean(propertyEvent.newValue)\n }\n })\n break\n case 'flags.pinned':\n vueNodeData.set(nodeId, {\n ...currentData,\n flags: {\n ...currentData.flags,\n pinned: Boolean(propertyEvent.newValue)\n }\n })\n break\n case 'mode':\n vueNodeData.set(nodeId, {\n ...currentData,\n mode:\n typeof propertyEvent.newValue === 'number'\n ? propertyEvent.newValue\n : 0\n })\n break\n case 'color':\n vueNodeData.set(nodeId, {\n ...currentData,\n color:\n typeof propertyEvent.newValue === 'string'\n ? propertyEvent.newValue\n : undefined\n })\n break\n case 'bgcolor':\n vueNodeData.set(nodeId, {\n ...currentData,\n bgcolor:\n typeof propertyEvent.newValue === 'string'\n ? propertyEvent.newValue\n : undefined\n })\n break\n case 'shape':\n vueNodeData.set(nodeId, {\n ...currentData,\n shape:\n typeof propertyEvent.newValue === 'number'\n ? propertyEvent.newValue\n : undefined\n })\n }\n }\n },\n 'node:slot-errors:changed': (slotErrorsEvent) => {\n refreshNodeSlots(String(slotErrorsEvent.nodeId))\n },\n 'node:slot-links:changed': (slotLinksEvent) => {\n if (slotLinksEvent.slotType === NodeSlotType.INPUT) {\n refreshNodeSlots(String(slotLinksEvent.nodeId))\n }\n }\n }\n\n graph.onTrigger = (event: LGraphTriggerEvent) => {\n switch (event.type) {\n case 'node:property:changed':\n triggerHandlers['node:property:changed'](event)\n break\n case 'node:slot-errors:changed':\n triggerHandlers['node:slot-errors:changed'](event)\n break\n case 'node:slot-links:changed':\n triggerHandlers['node:slot-links:changed'](event)\n break\n }\n\n // Chain to original handler\n originalOnTrigger?.(event)\n }\n\n // Initialize state\n syncWithGraph()\n\n // Return cleanup function\n return createCleanupFunction(\n originalOnNodeAdded || undefined,\n originalOnNodeRemoved || undefined,\n originalOnTrigger || undefined\n )\n }\n\n // Set up event listeners immediately\n const cleanup = setupEventListeners()\n\n // Process any existing nodes after event listeners are set up\n if (graph._nodes && graph._nodes.length > 0) {\n graph._nodes.forEach((node: LGraphNode) => {\n if (graph.onNodeAdded) {\n graph.onNodeAdded(node)\n }\n })\n }\n\n return {\n vueNodeData,\n getNode,\n cleanup\n }\n}\n","<script setup lang=\"ts\">\nimport { cn } from '@comfyorg/tailwind-utils'\nimport { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport MoreButton from '@/components/button/MoreButton.vue'\nimport { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'\nimport {\n demoteWidget,\n promoteWidget\n} from '@/core/graph/subgraph/proxyWidgetUtils'\nimport type { LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'\nimport type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useDialogService } from '@/services/dialogService'\nimport { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'\n\nconst {\n widget,\n node,\n parents = [],\n isShownOnParents = false\n} = defineProps<{\n widget: IBaseWidget\n node: LGraphNode\n parents?: SubgraphNode[]\n isShownOnParents?: boolean\n}>()\n\nconst label = defineModel<string>('label', { required: true })\n\nconst canvasStore = useCanvasStore()\nconst favoritedWidgetsStore = useFavoritedWidgetsStore()\nconst dialogService = useDialogService()\nconst { t } = useI18n()\n\nconst hasParents = computed(() => parents?.length > 0)\nconst favoriteNode = computed(() =>\n isShownOnParents && hasParents.value ? parents[0] : node\n)\nconst isFavorited = computed(() =>\n favoritedWidgetsStore.isFavorited(favoriteNode.value, widget.name)\n)\n\nasync function handleRename() {\n const newLabel = await dialogService.prompt({\n title: t('g.rename'),\n message: t('g.enterNewName') + ':',\n defaultValue: widget.label,\n placeholder: widget.name\n })\n\n if (newLabel === null) return\n label.value = newLabel\n}\n\nfunction handleHideInput() {\n if (!parents?.length) return\n\n // For proxy widgets (already promoted), we need to find the original interior node and widget\n if (isProxyWidget(widget)) {\n const subgraph = parents[0].subgraph\n const interiorNode = subgraph.getNodeById(parseInt(widget._overlay.nodeId))\n\n if (!interiorNode) {\n console.error('Could not find interior node for proxy widget')\n return\n }\n\n const originalWidget = interiorNode.widgets?.find(\n (w) => w.name === widget._overlay.widgetName\n )\n\n if (!originalWidget) {\n console.error('Could not find original widget for proxy widget')\n return\n }\n\n demoteWidget(interiorNode, originalWidget, parents)\n } else {\n // For regular widgets (not yet promoted), use them directly\n demoteWidget(node, widget, parents)\n }\n\n canvasStore.canvas?.setDirty(true, true)\n}\n\nfunction handleShowInput() {\n if (!parents?.length) return\n\n promoteWidget(node, widget, parents)\n canvasStore.canvas?.setDirty(true, true)\n}\n\nfunction handleToggleFavorite() {\n favoritedWidgetsStore.toggleFavorite(favoriteNode.value, widget.name)\n}\n\nconst buttonClasses = cn([\n 'border-none bg-transparent',\n 'w-full flex items-center gap-2 rounded px-3 py-2 text-sm',\n 'cursor-pointer transition-all hover:bg-secondary-background-hover active:scale-95'\n])\n</script>\n\n<template>\n <MoreButton\n is-vertical\n class=\"text-muted-foreground bg-transparent hover:text-base-foreground hover:bg-secondary-background-hover active:scale-95 transition-all\"\n >\n <template #default=\"{ close }\">\n <button\n :class=\"buttonClasses\"\n @click=\"\n () => {\n handleRename()\n close()\n }\n \"\n >\n <i class=\"icon-[lucide--edit] size-4\" />\n <span>{{ t('g.rename') }}</span>\n </button>\n\n <button\n v-if=\"hasParents\"\n :class=\"buttonClasses\"\n @click=\"\n () => {\n if (isShownOnParents) handleHideInput()\n else handleShowInput()\n close()\n }\n \"\n >\n <template v-if=\"isShownOnParents\">\n <i class=\"icon-[lucide--eye-off] size-4\" />\n <span>{{ t('rightSidePanel.hideInput') }}</span>\n </template>\n <template v-else>\n <i class=\"icon-[lucide--eye] size-4\" />\n <span>{{ t('rightSidePanel.showInput') }}</span>\n </template>\n </button>\n\n <button\n :class=\"buttonClasses\"\n @click=\"\n () => {\n handleToggleFavorite()\n close()\n }\n \"\n >\n <template v-if=\"isFavorited\">\n <i class=\"icon-[lucide--star]\" />\n <span>{{ t('rightSidePanel.removeFavorite') }}</span>\n </template>\n <template v-else>\n <i class=\"icon-[lucide--star]\" />\n <span>{{ t('rightSidePanel.addFavorite') }}</span>\n </template>\n </button>\n </template>\n </MoreButton>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, customRef, ref } from 'vue'\n\nimport EditableText from '@/components/common/EditableText.vue'\nimport { getSharedWidgetEnhancements } from '@/composables/graph/useGraphNodeManager'\nimport { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'\nimport type { LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'\nimport type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport WidgetLegacy from '@/renderer/extensions/vueNodes/widgets/components/WidgetLegacy.vue'\nimport {\n getComponent,\n shouldExpand\n} from '@/renderer/extensions/vueNodes/widgets/registry/widgetRegistry'\nimport { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'\nimport { getNodeByExecutionId } from '@/utils/graphTraversalUtil'\nimport { cn } from '@/utils/tailwindUtil'\n\nimport { renameWidget } from '../shared'\nimport WidgetActions from './WidgetActions.vue'\n\nconst {\n widget,\n node,\n isDraggable = false,\n hiddenFavoriteIndicator = false,\n showNodeName = false,\n parents = [],\n isShownOnParents = false\n} = defineProps<{\n widget: IBaseWidget\n node: LGraphNode\n isDraggable?: boolean\n hiddenFavoriteIndicator?: boolean\n showNodeName?: boolean\n parents?: SubgraphNode[]\n isShownOnParents?: boolean\n}>()\n\nconst canvasStore = useCanvasStore()\nconst favoritedWidgetsStore = useFavoritedWidgetsStore()\nconst isEditing = ref(false)\n\nconst widgetComponent = computed(() => {\n const component = getComponent(widget.type, widget.name)\n return component || WidgetLegacy\n})\n\nconst enhancedWidget = computed(() => {\n // Get shared enhancements (reactive value, controlWidget, spec, nodeType, etc.)\n const enhancements = getSharedWidgetEnhancements(node, widget)\n return { ...widget, ...enhancements }\n})\n\nconst sourceNodeName = computed((): string | null => {\n let sourceNode: LGraphNode | null = node\n if (isProxyWidget(widget)) {\n const { graph, nodeId } = widget._overlay\n sourceNode = getNodeByExecutionId(graph, nodeId)\n }\n return sourceNode ? sourceNode.title || sourceNode.type : null\n})\n\nconst hasParents = computed(() => parents?.length > 0)\nconst favoriteNode = computed(() =>\n isShownOnParents && hasParents.value ? parents[0] : node\n)\n\nconst widgetValue = computed({\n get: () => {\n widget.vueTrack?.()\n return widget.value\n },\n set: (newValue: string | number | boolean | object) => {\n // eslint-disable-next-line vue/no-mutating-props\n widget.value = newValue\n widget.callback?.(newValue)\n canvasStore.canvas?.setDirty(true, true)\n }\n})\n\nconst displayLabel = customRef((track, trigger) => {\n return {\n get() {\n track()\n return widget.label || widget.name\n },\n set(newValue: string) {\n isEditing.value = false\n\n const trimmedLabel = newValue.trim()\n\n const success = renameWidget(widget, node, trimmedLabel, parents)\n\n if (success) {\n canvasStore.canvas?.setDirty(true)\n trigger()\n }\n }\n }\n})\n</script>\n\n<template>\n <div\n :class=\"\n cn(\n 'widget-item col-span-full grid grid-cols-subgrid rounded-lg group',\n isDraggable &&\n 'draggable-item !will-change-auto drag-handle cursor-grab bg-comfy-menu-bg [&.is-draggable]:cursor-grabbing outline-comfy-menu-bg [&.is-draggable]:outline-4 [&.is-draggable]:outline-offset-0 [&.is-draggable]:opacity-70'\n )\n \"\n >\n <!-- widget header -->\n <div\n :class=\"\n cn(\n 'min-h-8 flex items-center justify-between gap-1 mb-1.5 min-w-0',\n isDraggable && 'pointer-events-none'\n )\n \"\n >\n <EditableText\n v-if=\"widget.name\"\n :model-value=\"displayLabel\"\n :is-editing=\"isEditing\"\n :input-attrs=\"{ placeholder: widget.name }\"\n class=\"text-sm leading-8 p-0 m-0 truncate pointer-events-auto cursor-text\"\n @edit=\"displayLabel = $event\"\n @cancel=\"isEditing = false\"\n @click=\"isEditing = true\"\n />\n\n <span\n v-if=\"(showNodeName || hasParents) && sourceNodeName\"\n class=\"text-xs text-muted-foreground flex-1 p-0 my-0 mx-1 truncate text-right min-w-10\"\n >\n {{ sourceNodeName }}\n </span>\n <div class=\"flex items-center gap-1 shrink-0 pointer-events-auto\">\n <WidgetActions\n v-model:label=\"displayLabel\"\n :widget=\"widget\"\n :node=\"node\"\n :parents=\"parents\"\n :is-shown-on-parents=\"isShownOnParents\"\n />\n </div>\n </div>\n <!-- favorite indicator -->\n <div\n v-if=\"\n !hiddenFavoriteIndicator &&\n favoritedWidgetsStore.isFavorited(favoriteNode, widget.name)\n \"\n class=\"relative z-2 pointer-events-none\"\n >\n <i\n class=\"absolute -right-1 -top-1 pi pi-star-fill text-xs text-muted-foreground pointer-events-none\"\n />\n </div>\n <!-- widget content -->\n <component\n :is=\"widgetComponent\"\n v-model=\"widgetValue\"\n :widget=\"enhancedWidget\"\n :node-id=\"String(node.id)\"\n :node-type=\"node.type\"\n :class=\"cn('col-span-1', shouldExpand(widget.type) && 'min-h-36')\"\n />\n <!-- Drag handle -->\n <div\n :class=\"\n cn(\n 'pointer-events-none mt-1.5 mx-auto max-w-40 w-1/2 h-1 rounded-lg bg-transparent transition-colors duration-150',\n 'group-hover:bg-interface-stroke group-[.is-draggable]:bg-component-node-widget-background-highlighted',\n !isDraggable && 'opacity-0'\n )\n \"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, inject, provide, ref, shallowRef, watchEffect } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'\nimport { parseProxyWidgets } from '@/core/schemas/proxyWidget'\nimport type {\n LGraphGroup,\n LGraphNode,\n SubgraphNode\n} from '@/lib/litegraph/src/litegraph'\nimport type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\n\nimport PropertiesAccordionItem from '../layout/PropertiesAccordionItem.vue'\nimport { GetNodeParentGroupKey } from '../shared'\nimport WidgetItem from './WidgetItem.vue'\n\nconst {\n label,\n node,\n widgets: widgetsProp,\n showLocateButton = false,\n isDraggable = false,\n hiddenFavoriteIndicator = false,\n showNodeName = false,\n parents = [],\n enableEmptyState = false,\n tooltip\n} = defineProps<{\n label?: string\n parents?: SubgraphNode[]\n node?: LGraphNode\n widgets: { widget: IBaseWidget; node: LGraphNode }[]\n showLocateButton?: boolean\n isDraggable?: boolean\n hiddenFavoriteIndicator?: boolean\n showNodeName?: boolean\n /**\n * Whether to show the empty state slot when there are no widgets.\n */\n enableEmptyState?: boolean\n tooltip?: string\n}>()\n\nconst collapse = defineModel<boolean>('collapse', { default: false })\n\nconst widgetsContainer = ref<HTMLElement>()\nconst rootElement = ref<HTMLElement>()\n\nconst widgets = shallowRef(widgetsProp)\nwatchEffect(() => (widgets.value = widgetsProp))\n\nprovide('hideLayoutField', true)\n\nconst canvasStore = useCanvasStore()\nconst { t } = useI18n()\n\nconst getNodeParentGroup = inject(GetNodeParentGroupKey, null)\n\nfunction isWidgetShownOnParents(\n widgetNode: LGraphNode,\n widget: IBaseWidget\n): boolean {\n if (!parents.length) return false\n const proxyWidgets = parseProxyWidgets(parents[0].properties.proxyWidgets)\n\n // For proxy widgets (already promoted), check using overlay information\n if (isProxyWidget(widget)) {\n return proxyWidgets.some(\n ([nodeId, widgetName]) =>\n widget._overlay.nodeId == nodeId &&\n widget._overlay.widgetName === widgetName\n )\n }\n\n // For regular widgets (not yet promoted), check using node ID and widget name\n return proxyWidgets.some(\n ([nodeId, widgetName]) =>\n widgetNode.id == nodeId && widget.name === widgetName\n )\n}\n\nconst isEmpty = computed(() => widgets.value.length === 0)\n\nconst displayLabel = computed(\n () => label ?? (node ? node.title : t('rightSidePanel.inputs'))\n)\n\nconst targetNode = computed<LGraphNode | null>(() => {\n if (node) return node\n if (isEmpty.value) return null\n\n const firstNodeId = widgets.value[0].node.id\n const allSameNode = widgets.value.every(({ node }) => node.id === firstNodeId)\n\n return allSameNode ? widgets.value[0].node : null\n})\n\nconst parentGroup = computed<LGraphGroup | null>(() => {\n if (!targetNode.value || !getNodeParentGroup) return null\n return getNodeParentGroup(targetNode.value)\n})\n\nconst canShowLocateButton = computed(\n () => showLocateButton && targetNode.value !== null\n)\n\nfunction handleLocateNode() {\n if (!targetNode.value || !canvasStore.canvas) return\n\n const graphNode = canvasStore.canvas.graph?.getNodeById(targetNode.value.id)\n if (graphNode) {\n canvasStore.canvas.animateToBounds(graphNode.boundingRect)\n }\n}\n\ndefineExpose({\n widgetsContainer,\n rootElement\n})\n</script>\n\n<template>\n <div ref=\"rootElement\">\n <PropertiesAccordionItem\n v-model:collapse=\"collapse\"\n :enable-empty-state\n :disabled=\"isEmpty\"\n :tooltip\n >\n <template #label>\n <div class=\"flex items-center gap-2 flex-1 min-w-0\">\n <span class=\"flex-1 flex items-center gap-2 min-w-0\">\n <span class=\"truncate\">\n <slot name=\"label\">\n {{ displayLabel }}\n </slot>\n </span>\n <span\n v-if=\"parentGroup\"\n class=\"text-xs text-muted-foreground truncate flex-1 text-right min-w-11\"\n :title=\"parentGroup.title\"\n >\n {{ parentGroup.title }}\n </span>\n </span>\n <Button\n v-if=\"canShowLocateButton\"\n variant=\"textonly\"\n size=\"icon-sm\"\n class=\"subbutton shrink-0 mr-3 size-8 cursor-pointer text-muted-foreground hover:text-base-foreground\"\n :title=\"t('rightSidePanel.locateNode')\"\n :aria-label=\"t('rightSidePanel.locateNode')\"\n @click.stop=\"handleLocateNode\"\n >\n <i class=\"icon-[lucide--locate] size-4\" />\n </Button>\n </div>\n </template>\n\n <template #empty><slot name=\"empty\" /></template>\n\n <div\n ref=\"widgetsContainer\"\n class=\"space-y-2 rounded-lg px-4 pt-1 relative\"\n >\n <TransitionGroup name=\"list-scale\">\n <WidgetItem\n v-for=\"{ widget, node } in widgets\"\n :key=\"`${node.id}-${widget.name}-${widget.type}`\"\n :widget=\"widget\"\n :node=\"node\"\n :is-draggable=\"isDraggable\"\n :hidden-favorite-indicator=\"hiddenFavoriteIndicator\"\n :show-node-name=\"showNodeName\"\n :parents=\"parents\"\n :is-shown-on-parents=\"isWidgetShownOnParents(node, widget)\"\n />\n </TransitionGroup>\n </div>\n </PropertiesAccordionItem>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useMounted, watchDebounced } from '@vueuse/core'\nimport { storeToRefs } from 'pinia'\nimport {\n computed,\n nextTick,\n onBeforeUnmount,\n onMounted,\n ref,\n shallowRef\n} from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'\nimport { DraggableList } from '@/scripts/ui/draggableList'\nimport { useFavoritedWidgetsStore } from '@/stores/workspace/favoritedWidgetsStore'\nimport type { ValidFavoritedWidget } from '@/stores/workspace/favoritedWidgetsStore'\nimport { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'\n\nimport { searchWidgets } from '../shared'\nimport SectionWidgets from './SectionWidgets.vue'\n\nconst favoritedWidgetsStore = useFavoritedWidgetsStore()\nconst rightSidePanelStore = useRightSidePanelStore()\nconst { searchQuery } = storeToRefs(rightSidePanelStore)\nconst { t } = useI18n()\n\nconst draggableList = ref<DraggableList | undefined>(undefined)\nconst sectionWidgetsRef = ref<{ widgetsContainer: HTMLElement }>()\nconst isSearching = ref(false)\n\nconst favoritedWidgets = computed(\n () => favoritedWidgetsStore.validFavoritedWidgets\n)\n\nconst label = computed(() =>\n favoritedWidgets.value.length === 0\n ? t('rightSidePanel.favoritesNone')\n : t('rightSidePanel.favorites')\n)\n\nconst searchedFavoritedWidgets = shallowRef<ValidFavoritedWidget[]>(\n favoritedWidgets.value\n)\n\nasync function searcher(query: string) {\n isSearching.value = query.trim().length > 0\n searchedFavoritedWidgets.value = searchWidgets(favoritedWidgets.value, query)\n}\n\nconst isMounted = useMounted()\n\nfunction setDraggableState() {\n if (!isMounted.value) return\n draggableList.value?.dispose()\n const container = sectionWidgetsRef.value?.widgetsContainer\n if (isSearching.value || !container?.children?.length) return\n\n draggableList.value = new DraggableList(container, '.draggable-item')\n\n draggableList.value.applyNewItemsOrder = function () {\n const reorderedItems: HTMLElement[] = []\n\n let oldPosition = -1\n this.getAllItems().forEach((item, index) => {\n if (item === this.draggableItem) {\n oldPosition = index\n return\n }\n if (!this.isItemToggled(item)) {\n reorderedItems[index] = item\n return\n }\n const newIndex = this.isItemAbove(item) ? index + 1 : index - 1\n reorderedItems[newIndex] = item\n })\n\n for (let index = 0; index < this.getAllItems().length; index++) {\n const item = reorderedItems[index]\n if (typeof item === 'undefined') {\n reorderedItems[index] = this.draggableItem as HTMLElement\n }\n }\n\n const newPosition = reorderedItems.indexOf(\n this.draggableItem as HTMLElement\n )\n const widgets = [...searchedFavoritedWidgets.value]\n const [widget] = widgets.splice(oldPosition, 1)\n widgets.splice(newPosition, 0, widget)\n searchedFavoritedWidgets.value = widgets\n favoritedWidgetsStore.reorderFavorites(widgets)\n }\n}\n\nwatchDebounced(\n searchedFavoritedWidgets,\n () => {\n setDraggableState()\n },\n { debounce: 100 }\n)\n\nonMounted(() => {\n setDraggableState()\n})\n\nonBeforeUnmount(() => {\n draggableList.value?.dispose()\n})\n</script>\n\n<template>\n <div class=\"px-4 pt-1 pb-4 flex gap-2 border-b border-interface-stroke\">\n <FormSearchInput\n v-model=\"searchQuery\"\n :searcher\n :update-key=\"favoritedWidgets\"\n />\n </div>\n <SectionWidgets\n ref=\"sectionWidgetsRef\"\n :label\n :widgets=\"searchedFavoritedWidgets\"\n :is-draggable=\"!isSearching\"\n hidden-favorite-indicator\n show-node-name\n enable-empty-state\n class=\"border-b border-interface-stroke\"\n @update:collapse=\"nextTick(setDraggableState)\"\n >\n <template #empty>\n <div class=\"text-sm text-muted-foreground px-4 text-center py-10\">\n {{\n isSearching\n ? t('rightSidePanel.noneSearchDesc')\n : t('rightSidePanel.favoritesNoneDesc')\n }}\n </div>\n </template>\n </SectionWidgets>\n</template>\n","<script setup lang=\"ts\">\nimport { storeToRefs } from 'pinia'\nimport { computed, ref, shallowRef } from 'vue'\n\nimport type { LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'\nimport { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'\n\nimport { searchWidgetsAndNodes } from '../shared'\nimport type { NodeWidgetsListList } from '../shared'\nimport SectionWidgets from './SectionWidgets.vue'\n\nconst canvasStore = useCanvasStore()\nconst workflowStore = useWorkflowStore()\n\nconst nodes = computed((): LGraphNode[] => {\n // Depend on activeWorkflow to trigger recomputation when workflow changes\n void workflowStore.activeWorkflow?.path\n return (canvasStore.canvas?.graph?.nodes ?? []) as LGraphNode[]\n})\n\nconst rightSidePanelStore = useRightSidePanelStore()\nconst { searchQuery } = storeToRefs(rightSidePanelStore)\n\nconst widgetsSectionDataList = computed((): NodeWidgetsListList => {\n return nodes.value.map((node) => {\n const { widgets = [] } = node\n const shownWidgets = widgets\n .filter((w) => !(w.options?.canvasOnly || w.options?.hidden))\n .map((widget) => ({ node, widget }))\n return {\n widgets: shownWidgets,\n node\n }\n })\n})\n\nconst searchedWidgetsSectionDataList = shallowRef<NodeWidgetsListList>(\n widgetsSectionDataList.value\n)\nconst isSearching = ref(false)\nasync function searcher(query: string) {\n const list = widgetsSectionDataList.value\n const target = searchedWidgetsSectionDataList\n isSearching.value = query.trim() !== ''\n target.value = searchWidgetsAndNodes(list, query)\n}\n</script>\n\n<template>\n <div class=\"px-4 pt-1 pb-4 flex gap-2 border-b border-interface-stroke\">\n <FormSearchInput\n v-model=\"searchQuery\"\n :searcher\n :update-key=\"widgetsSectionDataList\"\n />\n </div>\n <TransitionGroup tag=\"div\" name=\"list-scale\" class=\"relative\">\n <div\n v-if=\"isSearching && searchedWidgetsSectionDataList.length === 0\"\n class=\"text-sm text-muted-foreground px-4 text-center pt-5 pb-15\"\n >\n {{ $t('rightSidePanel.noneSearchDesc') }}\n </div>\n <SectionWidgets\n v-for=\"{ node, widgets } in searchedWidgetsSectionDataList\"\n :key=\"node.id\"\n :node\n :widgets\n :collapse=\"!isSearching\"\n :tooltip=\"\n isSearching || widgets.length\n ? ''\n : $t('rightSidePanel.inputsNoneTooltip')\n \"\n show-locate-button\n class=\"border-b border-interface-stroke\"\n />\n </TransitionGroup>\n</template>\n","<script setup lang=\"ts\">\nimport { storeToRefs } from 'pinia'\nimport { computed, ref, shallowRef } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport type { LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'\nimport { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'\n\nimport { searchWidgetsAndNodes } from '../shared'\nimport type { NodeWidgetsListList } from '../shared'\nimport SectionWidgets from './SectionWidgets.vue'\n\nconst { nodes, mustShowNodeTitle } = defineProps<{\n mustShowNodeTitle?: boolean\n nodes: LGraphNode[]\n}>()\n\nconst { t } = useI18n()\n\nconst rightSidePanelStore = useRightSidePanelStore()\nconst { searchQuery } = storeToRefs(rightSidePanelStore)\n\nconst widgetsSectionDataList = computed((): NodeWidgetsListList => {\n return nodes.map((node) => {\n const { widgets = [] } = node\n const shownWidgets = widgets\n .filter((w) => !(w.options?.canvasOnly || w.options?.hidden))\n .map((widget) => ({ node, widget }))\n\n return { widgets: shownWidgets, node }\n })\n})\n\nconst isMultipleNodesSelected = computed(\n () => widgetsSectionDataList.value.length > 1\n)\n\nconst searchedWidgetsSectionDataList = shallowRef<NodeWidgetsListList>(\n widgetsSectionDataList.value\n)\nconst isSearching = ref(false)\n\nasync function searcher(query: string) {\n const list = widgetsSectionDataList.value\n const target = searchedWidgetsSectionDataList\n isSearching.value = query.trim() !== ''\n target.value = searchWidgetsAndNodes(list, query)\n}\n\nconst label = computed(() => {\n const sections = widgetsSectionDataList.value\n return !mustShowNodeTitle && sections.length === 1\n ? sections[0].widgets.length !== 0\n ? t('rightSidePanel.inputs')\n : t('rightSidePanel.inputsNone')\n : undefined // SectionWidgets display node titles by default\n})\n</script>\n\n<template>\n <div class=\"px-4 pt-1 pb-4 flex gap-2 border-b border-interface-stroke\">\n <FormSearchInput\n v-model=\"searchQuery\"\n :searcher\n :update-key=\"widgetsSectionDataList\"\n />\n </div>\n <TransitionGroup tag=\"div\" name=\"list-scale\" class=\"relative\">\n <div\n v-if=\"searchedWidgetsSectionDataList.length === 0\"\n class=\"text-sm text-muted-foreground px-4 py-10 text-center\"\n >\n {{\n isSearching\n ? t('rightSidePanel.noneSearchDesc')\n : t('rightSidePanel.nodesNoneDesc')\n }}\n </div>\n <SectionWidgets\n v-for=\"{ widgets, node } in searchedWidgetsSectionDataList\"\n :key=\"node.id\"\n :node\n :label\n :widgets\n :collapse=\"isMultipleNodesSelected && !isSearching\"\n :show-locate-button=\"isMultipleNodesSelected\"\n :tooltip=\"\n isSearching || widgets.length\n ? ''\n : t('rightSidePanel.inputsNoneTooltip')\n \"\n class=\"border-b border-interface-stroke\"\n />\n </TransitionGroup>\n</template>\n","<script setup lang=\"ts\">\nimport { useMounted, watchDebounced } from '@vueuse/core'\nimport { storeToRefs } from 'pinia'\nimport {\n computed,\n customRef,\n nextTick,\n onBeforeUnmount,\n onMounted,\n ref,\n shallowRef,\n triggerRef,\n useTemplateRef,\n watch\n} from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'\nimport { parseProxyWidgets } from '@/core/schemas/proxyWidget'\nimport type { ProxyWidgetsProperty } from '@/core/schemas/proxyWidget'\nimport type { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'\nimport { DraggableList } from '@/scripts/ui/draggableList'\nimport { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'\n\nimport { searchWidgets } from '../shared'\nimport type { NodeWidgetsList } from '../shared'\nimport SectionWidgets from './SectionWidgets.vue'\n\nconst { node } = defineProps<{\n node: SubgraphNode\n}>()\n\nconst { t } = useI18n()\nconst canvasStore = useCanvasStore()\nconst rightSidePanelStore = useRightSidePanelStore()\nconst { focusedSection, searchQuery } = storeToRefs(rightSidePanelStore)\n\nconst advancedInputsCollapsed = ref(true)\nconst draggableList = ref<DraggableList | undefined>(undefined)\nconst sectionWidgetsRef = useTemplateRef('sectionWidgetsRef')\nconst advancedInputsSectionRef = useTemplateRef('advancedInputsSectionRef')\n\n// Use customRef to track proxyWidgets changes\nconst proxyWidgets = customRef<ProxyWidgetsProperty>((track, trigger) => ({\n get() {\n track()\n return parseProxyWidgets(node.properties.proxyWidgets)\n },\n set(value?: ProxyWidgetsProperty) {\n trigger()\n if (!value) return\n // eslint-disable-next-line vue/no-mutating-props\n node.properties.proxyWidgets = value\n }\n}))\n\nwatch(\n focusedSection,\n async (section) => {\n if (section === 'advanced-inputs') {\n advancedInputsCollapsed.value = false\n rightSidePanelStore.clearFocusedSection()\n\n await nextTick()\n\n await new Promise((resolve) => setTimeout(resolve, 300))\n\n const sectionComponent = advancedInputsSectionRef.value\n const sectionElement = sectionComponent?.rootElement\n if (sectionElement) {\n sectionElement.scrollIntoView({ behavior: 'smooth', block: 'start' })\n }\n }\n },\n { immediate: true }\n)\n\nconst widgetsList = computed((): NodeWidgetsList => {\n const proxyWidgetsOrder = proxyWidgets.value\n const { widgets = [] } = node\n\n // Map proxyWidgets to actual proxy widgets in the correct order\n const result: NodeWidgetsList = []\n for (const [nodeId, widgetName] of proxyWidgetsOrder) {\n // Find the proxy widget that matches this nodeId and widgetName\n const widget = widgets.find((w) => {\n // Check if this is a proxy widget with _overlay\n if (isProxyWidget(w)) {\n return (\n String(w._overlay.nodeId) === nodeId &&\n w._overlay.widgetName === widgetName\n )\n }\n // For non-proxy widgets (like linked widgets), match by name\n return w.name === widgetName\n })\n if (widget) {\n result.push({ node, widget })\n }\n }\n return result\n})\n\nconst advancedInputsWidgets = computed((): NodeWidgetsList => {\n const interiorNodes = node.subgraph.nodes\n const proxyWidgetsValue = parseProxyWidgets(node.properties.proxyWidgets)\n\n // Get all widgets from interior nodes\n const allInteriorWidgets = interiorNodes.flatMap((interiorNode) => {\n const { widgets = [] } = interiorNode\n return widgets\n .filter((w) => !w.computedDisabled)\n .map((widget) => ({ node: interiorNode, widget }))\n })\n\n // Filter out widgets that are already promoted using tuple matching\n return allInteriorWidgets.filter(({ node: interiorNode, widget }) => {\n return !proxyWidgetsValue.some(\n ([nodeId, widgetName]) =>\n interiorNode.id == nodeId && widget.name === widgetName\n )\n })\n})\n\nconst parents = computed<SubgraphNode[]>(() => [node])\n\nconst searchedWidgetsList = shallowRef<NodeWidgetsList>(widgetsList.value)\nconst isSearching = ref(false)\n\nasync function searcher(query: string) {\n isSearching.value = query.trim() !== ''\n searchedWidgetsList.value = searchWidgets(widgetsList.value, query)\n}\n\nconst isMounted = useMounted()\n\nfunction setDraggableState() {\n if (!isMounted.value) return\n\n draggableList.value?.dispose()\n const container = sectionWidgetsRef.value?.widgetsContainer\n if (isSearching.value || !container?.children?.length) return\n\n draggableList.value = new DraggableList(container, '.draggable-item')\n\n draggableList.value.applyNewItemsOrder = function () {\n const reorderedItems: HTMLElement[] = []\n\n let oldPosition = -1\n this.getAllItems().forEach((item, index) => {\n if (item === this.draggableItem) {\n oldPosition = index\n return\n }\n if (!this.isItemToggled(item)) {\n reorderedItems[index] = item\n return\n }\n const newIndex = this.isItemAbove(item) ? index + 1 : index - 1\n reorderedItems[newIndex] = item\n })\n\n if (oldPosition === -1) {\n console.error('[TabSubgraphInputs] draggableItem not found in items')\n return\n }\n\n for (let index = 0; index < this.getAllItems().length; index++) {\n const item = reorderedItems[index]\n if (typeof item === 'undefined') {\n reorderedItems[index] = this.draggableItem as HTMLElement\n }\n }\n\n const newPosition = reorderedItems.indexOf(\n this.draggableItem as HTMLElement\n )\n\n // Update proxyWidgets order\n const pw = proxyWidgets.value\n const [w] = pw.splice(oldPosition, 1)\n pw.splice(newPosition, 0, w)\n proxyWidgets.value = pw\n canvasStore.canvas?.setDirty(true, true)\n triggerRef(proxyWidgets)\n }\n}\n\nwatchDebounced(searchedWidgetsList, () => setDraggableState(), {\n debounce: 100\n})\nonMounted(() => setDraggableState())\nonBeforeUnmount(() => draggableList.value?.dispose())\n\nconst label = computed(() => {\n return searchedWidgetsList.value.length !== 0\n ? t('rightSidePanel.inputs')\n : t('rightSidePanel.inputsNone')\n})\n</script>\n\n<template>\n <div class=\"px-4 pt-1 pb-4 flex gap-2 border-b border-interface-stroke\">\n <FormSearchInput\n v-model=\"searchQuery\"\n :searcher\n :update-key=\"widgetsList\"\n />\n </div>\n <SectionWidgets\n ref=\"sectionWidgetsRef\"\n :node\n :label\n :parents\n :widgets=\"searchedWidgetsList\"\n :is-draggable=\"!isSearching\"\n :enable-empty-state=\"isSearching\"\n :tooltip=\"\n isSearching || searchedWidgetsList.length\n ? ''\n : t('rightSidePanel.inputsNoneTooltip')\n \"\n class=\"border-b border-interface-stroke\"\n @update:collapse=\"nextTick(setDraggableState)\"\n >\n <template #empty>\n <div class=\"text-sm text-muted-foreground px-4 text-center pt-5 pb-15\">\n {{ t('rightSidePanel.noneSearchDesc') }}\n </div>\n </template>\n </SectionWidgets>\n <SectionWidgets\n v-if=\"advancedInputsWidgets.length > 0\"\n ref=\"advancedInputsSectionRef\"\n v-model:collapse=\"advancedInputsCollapsed\"\n :label=\"t('rightSidePanel.advancedInputs')\"\n :parents=\"parents\"\n :widgets=\"advancedInputsWidgets\"\n show-node-name\n class=\"border-b border-interface-stroke\"\n />\n</template>\n","<script setup lang=\"ts\">\nimport ToggleSwitch from 'primevue/toggleswitch'\n\nimport LayoutField from './LayoutField.vue'\n\ndefineProps<{\n label: string\n tooltip?: string\n}>()\n\nconst modelValue = defineModel<boolean>({ default: false })\n</script>\n\n<template>\n <LayoutField singleline :label :tooltip>\n <ToggleSwitch\n v-model=\"modelValue\"\n class=\"transition-transform active:scale-90\"\n />\n </LayoutField>\n</template>\n","<script setup lang=\"ts\">\nimport InputNumber from 'primevue/inputnumber'\nimport Select from 'primevue/select'\nimport { computed, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport Slider from '@/components/ui/slider/Slider.vue'\nimport { LiteGraph } from '@/lib/litegraph/src/litegraph'\nimport type { LinkRenderType } from '@/lib/litegraph/src/types/globalEnums'\nimport { LinkMarkerShape } from '@/lib/litegraph/src/types/globalEnums'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { WidgetInputBaseClass } from '@/renderer/extensions/vueNodes/widgets/components/layout'\nimport { useDialogService } from '@/services/dialogService'\nimport { cn } from '@/utils/tailwindUtil'\n\nimport PropertiesAccordionItem from '../layout/PropertiesAccordionItem.vue'\nimport FieldSwitch from './FieldSwitch.vue'\nimport LayoutField from './LayoutField.vue'\n\nconst { t } = useI18n()\nconst settingStore = useSettingStore()\nconst dialogService = useDialogService()\n\n// NODES settings\nconst showAdvancedParameters = ref(false) // Placeholder for future implementation\n\nconst showToolbox = computed({\n get: () => settingStore.get('Comfy.Canvas.SelectionToolbox'),\n set: (value) => settingStore.set('Comfy.Canvas.SelectionToolbox', value)\n})\n\nconst nodes2Enabled = computed({\n get: () => settingStore.get('Comfy.VueNodes.Enabled'),\n set: (value) => settingStore.set('Comfy.VueNodes.Enabled', value)\n})\n\n// CANVAS settings\nconst gridSpacing = computed({\n get: () => settingStore.get('Comfy.SnapToGrid.GridSize'),\n set: (value) => settingStore.set('Comfy.SnapToGrid.GridSize', value)\n})\n\nconst snapToGrid = computed({\n get: () => settingStore.get('pysssss.SnapToGrid'),\n set: (value) => settingStore.set('pysssss.SnapToGrid', value)\n})\n\n// CONNECTION LINKS settings\nconst linkShape = computed({\n get: () => settingStore.get('Comfy.Graph.LinkMarkers'),\n set: (value) => settingStore.set('Comfy.Graph.LinkMarkers', value)\n})\n\nconst linkShapeOptions = computed(() => [\n { value: LinkMarkerShape.None, label: t('g.none') },\n { value: LinkMarkerShape.Circle, label: t('shape.circle') },\n { value: LinkMarkerShape.Arrow, label: t('shape.arrow') }\n])\n\nlet theOldLinkRenderMode: LinkRenderType = LiteGraph.SPLINE_LINK\nconst showConnectedLinks = computed({\n get: () => settingStore.get('Comfy.LinkRenderMode') !== LiteGraph.HIDDEN_LINK,\n set: (value) => {\n let oldLinkRenderMode = settingStore.get('Comfy.LinkRenderMode')\n if (oldLinkRenderMode !== LiteGraph.HIDDEN_LINK) {\n theOldLinkRenderMode = oldLinkRenderMode\n }\n const newMode = value ? theOldLinkRenderMode : LiteGraph.HIDDEN_LINK\n settingStore.set('Comfy.LinkRenderMode', newMode)\n }\n})\n\nconst GRID_SIZE_MIN = 1\nconst GRID_SIZE_MAX = 100\nconst GRID_SIZE_STEP = 1\n\nfunction updateGridSpacingFromSlider(values?: number[]) {\n if (!values?.length) return\n gridSpacing.value = values[0]\n}\n\nfunction updateGridSpacingFromInput(value: number | null | undefined) {\n if (typeof value !== 'number') return\n\n const clampedValue = Math.min(GRID_SIZE_MAX, Math.max(GRID_SIZE_MIN, value))\n gridSpacing.value = Math.round(clampedValue / GRID_SIZE_STEP) * GRID_SIZE_STEP\n}\n\nfunction openFullSettings() {\n dialogService.showSettingsDialog()\n}\n</script>\n\n<template>\n <div class=\"flex flex-col border-t border-interface-stroke\">\n <!-- NODES Section -->\n <PropertiesAccordionItem class=\"border-b border-interface-stroke\">\n <template #label>\n {{ t('rightSidePanel.globalSettings.nodes') }}\n </template>\n <div class=\"space-y-4 px-4 py-3\">\n <FieldSwitch\n v-model=\"showAdvancedParameters\"\n :label=\"t('rightSidePanel.globalSettings.showAdvanced')\"\n :tooltip=\"t('rightSidePanel.globalSettings.showAdvancedTooltip')\"\n />\n <FieldSwitch\n v-model=\"showToolbox\"\n :label=\"t('rightSidePanel.globalSettings.showToolbox')\"\n />\n <FieldSwitch\n v-model=\"nodes2Enabled\"\n :label=\"t('rightSidePanel.globalSettings.nodes2')\"\n />\n </div>\n </PropertiesAccordionItem>\n\n <!-- CANVAS Section -->\n <PropertiesAccordionItem class=\"border-b border-interface-stroke\">\n <template #label>\n {{ t('rightSidePanel.globalSettings.canvas') }}\n </template>\n <div class=\"space-y-4 px-4 py-3\">\n <LayoutField :label=\"t('rightSidePanel.globalSettings.gridSpacing')\">\n <div\n :class=\"\n cn(WidgetInputBaseClass, 'flex items-center gap-2 pl-3 pr-2')\n \"\n >\n <Slider\n :model-value=\"[gridSpacing]\"\n class=\"flex-grow text-xs\"\n :min=\"GRID_SIZE_MIN\"\n :max=\"GRID_SIZE_MAX\"\n :step=\"GRID_SIZE_STEP\"\n @update:model-value=\"updateGridSpacingFromSlider\"\n />\n <InputNumber\n :model-value=\"gridSpacing\"\n class=\"w-16\"\n size=\"small\"\n pt:pc-input-text:root=\"min-w-[4ch] bg-transparent border-none text-center truncate\"\n :min=\"GRID_SIZE_MIN\"\n :max=\"GRID_SIZE_MAX\"\n :step=\"GRID_SIZE_STEP\"\n :allow-empty=\"false\"\n @update:model-value=\"updateGridSpacingFromInput\"\n />\n </div>\n </LayoutField>\n <FieldSwitch\n v-model=\"snapToGrid\"\n :label=\"t('rightSidePanel.globalSettings.snapNodesToGrid')\"\n />\n </div>\n </PropertiesAccordionItem>\n\n <!-- CONNECTION LINKS Section -->\n <PropertiesAccordionItem class=\"border-b border-interface-stroke\">\n <template #label>\n {{ t('rightSidePanel.globalSettings.connectionLinks') }}\n </template>\n <div class=\"space-y-4 px-4 py-3\">\n <LayoutField :label=\"t('rightSidePanel.globalSettings.linkShape')\">\n <Select\n v-model=\"linkShape\"\n :options=\"linkShapeOptions\"\n :aria-label=\"t('rightSidePanel.globalSettings.linkShape')\"\n :class=\"cn(WidgetInputBaseClass, 'w-full text-xs')\"\n size=\"small\"\n :pt=\"{\n option: 'text-xs',\n dropdown: 'w-8',\n label: cn('truncate min-w-[4ch]', $slots.default && 'mr-5'),\n overlay: 'w-fit min-w-full'\n }\"\n data-capture-wheel=\"true\"\n option-label=\"label\"\n option-value=\"value\"\n />\n </LayoutField>\n <FieldSwitch\n v-model=\"showConnectedLinks\"\n :label=\"t('rightSidePanel.globalSettings.showConnectedLinks')\"\n />\n </div>\n </PropertiesAccordionItem>\n\n <!-- View all settings button -->\n <div\n class=\"flex items-center justify-center p-4 border-b border-interface-stroke\"\n >\n <Button\n variant=\"muted-textonly\"\n size=\"sm\"\n class=\"gap-2 text-sm\"\n @click=\"openFullSettings\"\n >\n {{ t('rightSidePanel.globalSettings.viewAllSettings') }}\n <i class=\"icon-[lucide--settings] size-4\" />\n </Button>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'\nimport { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'\nimport type { ColorOption } from '@/lib/litegraph/src/litegraph'\nimport { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'\nimport { adjustColor } from '@/utils/colorUtil'\nimport { cn } from '@/utils/tailwindUtil'\n\nimport LayoutField from './LayoutField.vue'\n\n/**\n * Good design limits dependencies and simplifies the interface of the abstraction layer.\n * Here, we only care about the getColorOption and setColorOption methods,\n * and do not concern ourselves with other methods.\n */\ntype PickedNode = Pick<LGraphNode, 'getColorOption' | 'setColorOption'>\n\nconst { nodes } = defineProps<{ nodes: PickedNode[] }>()\nconst emit = defineEmits<{ (e: 'changed'): void }>()\n\nconst { t } = useI18n()\n\nconst colorPaletteStore = useColorPaletteStore()\n\ntype NodeColorOption = {\n name: string\n localizedName: () => string\n value: {\n dark: string\n light: string\n ringDark: string\n ringLight: string\n }\n}\n\nconst nodeColorEntries = Object.entries(LGraphCanvas.node_colors)\n\nfunction getColorValue(color: string): NodeColorOption['value'] {\n return {\n dark: adjustColor(color, { lightness: 0.3 }),\n light: adjustColor(color, { lightness: 0.4 }),\n ringDark: adjustColor(color, { lightness: 0.5 }),\n ringLight: adjustColor(color, { lightness: 0.1 })\n }\n}\n\nconst NO_COLOR_OPTION: NodeColorOption = {\n name: 'noColor',\n localizedName: () => t('color.noColor'),\n value: getColorValue(LiteGraph.NODE_DEFAULT_BGCOLOR)\n}\n\nconst colorOptions: NodeColorOption[] = [\n NO_COLOR_OPTION,\n ...nodeColorEntries.map(([name, color]) => ({\n name,\n localizedName: () => t(`color.${name}`),\n value: getColorValue(color.bgcolor)\n }))\n]\n\nconst isLightTheme = computed(\n () => colorPaletteStore.completedActivePalette.light_theme\n)\n\nconst nodeColor = computed<NodeColorOption['name'] | null>({\n get() {\n if (nodes.length === 0) return null\n const theColorOptions = nodes.map((item) => item.getColorOption())\n\n let colorOption: ColorOption | null | false = theColorOptions[0]\n if (!theColorOptions.every((option) => option === colorOption)) {\n colorOption = false\n }\n\n if (colorOption === false) return null\n if (colorOption == null || (!colorOption.bgcolor && !colorOption.color))\n return NO_COLOR_OPTION.name\n return (\n nodeColorEntries.find(\n ([_, color]) =>\n color.bgcolor === colorOption.bgcolor &&\n color.color === colorOption.color\n )?.[0] ?? null\n )\n },\n set(colorName) {\n if (colorName === null) return\n\n const canvasColorOption =\n colorName === NO_COLOR_OPTION.name\n ? null\n : LGraphCanvas.node_colors[colorName]\n\n for (const item of nodes) {\n item.setColorOption(canvasColorOption)\n }\n\n emit('changed')\n }\n})\n</script>\n\n<template>\n <LayoutField :label=\"t('rightSidePanel.color')\">\n <div\n class=\"bg-secondary-background border-none rounded-lg p-1 grid grid-cols-5 gap-1 justify-items-center\"\n >\n <button\n v-for=\"option of colorOptions\"\n :key=\"option.name\"\n :class=\"\n cn(\n 'size-8 rounded-lg bg-transparent border-0 outline-0 ring-0 text-left flex justify-center items-center cursor-pointer',\n option.name === nodeColor\n ? 'bg-interface-menu-component-surface-selected'\n : 'hover:bg-interface-menu-component-surface-selected'\n )\n \"\n @click=\"nodeColor = option.name\"\n >\n <div\n v-tooltip.top=\"option.localizedName()\"\n :class=\"cn('size-4 rounded-full ring-2 ring-gray-500/10')\"\n :style=\"{\n backgroundColor: isLightTheme\n ? option.value.light\n : option.value.dark,\n '--tw-ring-color':\n option.name === nodeColor\n ? isLightTheme\n ? option.value.ringLight\n : option.value.ringDark\n : undefined\n }\"\n :data-testid=\"option.name\"\n />\n </button>\n </div>\n </LayoutField>\n</template>\n","<template>\n <div\n :class=\"\n cn(\n WidgetInputBaseClass,\n 'p-1 inline-flex justify-center items-center gap-1'\n )\n \"\n >\n <button\n v-for=\"(option, index) in options\"\n :key=\"getOptionValue(option, index)\"\n :class=\"\n cn(\n 'flex-1 h-6 px-5 py-[5px] rounded flex justify-center items-center gap-1 transition-all duration-150 ease-in-out truncate min-w-[4ch]',\n 'bg-transparent border-none',\n 'text-center text-xs font-normal',\n {\n 'bg-interface-menu-component-surface-selected':\n isSelected(index) && !disabled,\n 'hover:bg-interface-menu-component-surface-selected/50':\n !isSelected(index) && !disabled,\n 'opacity-50 cursor-not-allowed': disabled,\n 'cursor-pointer': !disabled\n },\n isSelected(index) && !disabled\n ? 'text-text-primary'\n : 'text-text-secondary'\n )\n \"\n :disabled=\"disabled\"\n @click=\"handleSelect(index)\"\n >\n {{ getOptionLabel(option) }}\n </button>\n </div>\n</template>\n\n<script\n setup\n lang=\"ts\"\n generic=\"\n T extends string | number | { label: string; value: string | number }\n \"\n>\nimport { cn } from '@/utils/tailwindUtil'\n\nimport { WidgetInputBaseClass } from '../layout'\n\ntype ModelValue = T extends object ? T['value'] : T\n\ninterface Props {\n modelValue: ModelValue | null | undefined\n options: T[]\n optionLabel?: string // PrimeVue compatible prop\n optionValue?: string // PrimeVue compatible prop\n disabled?: boolean\n}\n\ninterface Emits {\n 'update:modelValue': [value: ModelValue]\n}\n\nconst props = withDefaults(defineProps<Props>(), {\n disabled: false,\n optionLabel: 'label',\n optionValue: 'value'\n})\n\nconst emit = defineEmits<Emits>()\n\n// handle both string/number arrays and object arrays with PrimeVue compatibility\nconst getOptionValue = (option: T, index: number): ModelValue => {\n if (typeof option !== 'object') {\n return option as ModelValue\n }\n\n const valueField = props.optionValue\n const value =\n (option as any)[valueField] ??\n option.value ??\n (option as any).name ??\n option.label ??\n index\n return value\n}\n\n// for display with PrimeVue compatibility\nconst getOptionLabel = (option: T): string => {\n if (typeof option === 'object' && option !== null) {\n const labelField = props.optionLabel\n return (\n (option as any)[labelField] ??\n option.label ??\n (option as any).name ??\n option.value ??\n String(option)\n )\n }\n return String(option)\n}\n\nconst isSelected = (index: number): boolean => {\n const optionValue = getOptionValue(props.options[index], index)\n return String(optionValue) === String(props.modelValue ?? '')\n}\n\nconst handleSelect = (index: number) => {\n if (props.disabled) return\n\n const optionValue = getOptionValue(props.options[index], index)\n emit('update:modelValue', optionValue)\n}\n</script>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'\nimport { LGraphEventMode } from '@/lib/litegraph/src/litegraph'\nimport FormSelectButton from '@/renderer/extensions/vueNodes/widgets/components/form/FormSelectButton.vue'\n\nimport LayoutField from './LayoutField.vue'\n\n/**\n * Good design limits dependencies and simplifies the interface of the abstraction layer.\n * Here, we only care about the mode method,\n * and do not concern ourselves with other methods.\n */\ntype PickedNode = Pick<LGraphNode, 'mode'>\n\nconst { nodes } = defineProps<{ nodes: PickedNode[] }>()\nconst emit = defineEmits<{ (e: 'changed'): void }>()\n\nconst { t } = useI18n()\n\nconst nodeState = computed({\n get() {\n let mode: LGraphNode['mode'] | null = null\n\n if (nodes.length === 0) return null\n\n // For multiple nodes, if all nodes have the same mode, return that mode, otherwise return null\n if (nodes.length > 1) {\n mode = nodes[0].mode\n if (!nodes.every((node) => node.mode === mode)) {\n mode = null\n }\n } else {\n mode = nodes[0].mode\n }\n\n return mode\n },\n set(value: LGraphNode['mode']) {\n nodes.forEach((node) => {\n node.mode = value\n })\n emit('changed')\n }\n})\n</script>\n\n<template>\n <LayoutField :label=\"t('rightSidePanel.nodeState')\">\n <FormSelectButton\n v-model=\"nodeState\"\n class=\"w-full\"\n :options=\"[\n {\n label: t('rightSidePanel.normal'),\n value: LGraphEventMode.ALWAYS\n },\n {\n label: t('rightSidePanel.bypass'),\n value: LGraphEventMode.BYPASS\n },\n {\n label: t('rightSidePanel.mute'),\n value: LGraphEventMode.NEVER\n }\n ]\"\n />\n </LayoutField>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'\n\nimport FieldSwitch from './FieldSwitch.vue'\n\ntype PickedNode = Pick<LGraphNode, 'pinned' | 'pin'>\n\n/**\n * Good design limits dependencies and simplifies the interface of the abstraction layer.\n * Here, we only care about the pinned and pin methods,\n * and do not concern ourselves with other methods.\n */\nconst { nodes } = defineProps<{ nodes: PickedNode[] }>()\nconst emit = defineEmits<{ (e: 'changed'): void }>()\n\nconst { t } = useI18n()\n\n// Pinned state\nconst isPinned = computed<boolean>({\n get() {\n return nodes.some((node) => node.pinned)\n },\n set(value) {\n nodes.forEach((node) => node.pin(value))\n emit('changed')\n }\n})\n</script>\n\n<template>\n <FieldSwitch v-model=\"isPinned\" :label=\"t('rightSidePanel.pinned')\" />\n</template>\n","<template>\n <div class=\"space-y-4 text-sm text-muted-foreground\">\n <SetNodeState\n v-if=\"isNodes(targetNodes)\"\n :nodes=\"targetNodes\"\n @changed=\"handleChanged\"\n />\n <SetNodeColor :nodes=\"targetNodes\" @changed=\"handleChanged\" />\n <SetPinned :nodes=\"targetNodes\" @changed=\"handleChanged\" />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { shallowRef, watchEffect } from 'vue'\n\nimport type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { isLGraphGroup } from '@/utils/litegraphUtil'\n\nimport SetNodeColor from './SetNodeColor.vue'\nimport SetNodeState from './SetNodeState.vue'\nimport SetPinned from './SetPinned.vue'\n\nconst props = defineProps<{\n /**\n * - If the item is a Group, Node State cannot be set\n * as Groups do not have a 'mode' property.\n *\n * - The nodes array can contain either all Nodes or all Groups,\n * but it must not be a mix of both.\n */\n nodes?: LGraphNode[] | LGraphGroup[]\n}>()\n\nconst targetNodes = shallowRef<LGraphNode[] | LGraphGroup[]>([])\nwatchEffect(() => {\n if (props.nodes) {\n targetNodes.value = props.nodes\n } else {\n targetNodes.value = []\n }\n})\n\nconst canvasStore = useCanvasStore()\n\nfunction isNodes(nodes: LGraphNode[] | LGraphGroup[]): nodes is LGraphNode[] {\n return !nodes.some((node) => isLGraphGroup(node))\n}\n\nfunction handleChanged() {\n /**\n * This is not a random comment—it's crucial.\n * Otherwise, the UI cannot update correctly.\n * There is a bug with triggerRef here, so we can't use triggerRef.\n * We'll work around it for now and later submit a Vue issue and pull request to fix it.\n */\n targetNodes.value = targetNodes.value.slice()\n\n canvasStore.canvas?.setDirty(true, true)\n}\n</script>\n","<template>\n <div v-if=\"isOnlyHasNodes\" class=\"p-4\">\n <NodeSettings :nodes=\"theNodes\" />\n </div>\n <div v-else class=\"border-t border-interface-stroke\">\n <PropertiesAccordionItem\n v-if=\"hasNodes\"\n class=\"border-b border-interface-stroke\"\n :label=\"$t('rightSidePanel.nodes')\"\n >\n <NodeSettings :nodes=\"theNodes\" class=\"p-4\" />\n </PropertiesAccordionItem>\n <PropertiesAccordionItem\n v-if=\"hasGroups\"\n class=\"border-b border-interface-stroke\"\n :label=\"$t('rightSidePanel.groups')\"\n >\n <NodeSettings :nodes=\"theGroups\" class=\"p-4\" />\n </PropertiesAccordionItem>\n </div>\n</template>\n\n<script setup lang=\"ts\">\n/**\n * If we only need to show settings for Nodes,\n * there's no need to wrap them in PropertiesAccordionItem,\n * making the UI cleaner.\n * But if there are multiple types of settings,\n * it's better to wrap them; otherwise,\n * the UI would look messy.\n */\nimport { computed } from 'vue'\nimport type { Raw } from 'vue'\n\nimport type { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'\nimport type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'\nimport type { Positionable } from '@/lib/litegraph/src/interfaces'\nimport { isLGraphGroup, isLGraphNode } from '@/utils/litegraphUtil'\n\nimport PropertiesAccordionItem from '../layout/PropertiesAccordionItem.vue'\nimport NodeSettings from './NodeSettings.vue'\n\nconst props = defineProps<{\n nodes: Raw<Positionable>[]\n}>()\n\nconst theNodes = computed<LGraphNode[]>(() =>\n props.nodes.filter((node) => isLGraphNode(node))\n)\n\nconst theGroups = computed<LGraphGroup[]>(() =>\n props.nodes.filter((node) => isLGraphGroup(node))\n)\n\nconst hasGroups = computed(() => theGroups.value.length > 0)\nconst hasNodes = computed(() => theNodes.value.length > 0)\nconst isOnlyHasNodes = computed(() => hasNodes.value && !hasGroups.value)\n</script>\n","<script setup lang=\"ts\">\nimport Button from '@/components/ui/button/Button.vue'\nimport { cn } from '@/utils/tailwindUtil'\nimport type { ClassValue } from '@/utils/tailwindUtil'\n\nconst props = defineProps<{\n nodeTitle: string\n widgetName: string\n isDraggable?: boolean\n isPhysical?: boolean\n class?: ClassValue\n}>()\ndefineEmits<{\n (e: 'toggleVisibility'): void\n}>()\n\nfunction getIcon() {\n return props.isPhysical\n ? 'icon-[lucide--link]'\n : props.isDraggable\n ? 'icon-[lucide--eye]'\n : 'icon-[lucide--eye-off]'\n}\n</script>\n\n<template>\n <div\n :class=\"\n cn(\n 'flex py-1 px-2 break-all rounded items-center gap-1',\n 'bg-node-component-surface',\n props.isDraggable &&\n 'draggable-item drag-handle cursor-grab [&.is-draggable]:cursor-grabbing hover:ring-1 ring-accent-background',\n props.class\n )\n \"\n >\n <div class=\"pointer-events-none flex-1\">\n <div class=\"text-xs text-text-secondary line-clamp-1\">\n {{ nodeTitle }}\n </div>\n <div class=\"text-sm line-clamp-1 leading-8\">{{ widgetName }}</div>\n </div>\n <Button\n variant=\"muted-textonly\"\n size=\"sm\"\n :disabled=\"isPhysical\"\n @click.stop=\"$emit('toggleVisibility')\"\n >\n <i :class=\"getIcon()\" />\n </Button>\n <div\n v-if=\"isDraggable\"\n class=\"size-4 pointer-events-none icon-[lucide--grip-vertical]\"\n />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { storeToRefs } from 'pinia'\nimport {\n computed,\n customRef,\n onBeforeUnmount,\n onMounted,\n ref,\n triggerRef,\n watch\n} from 'vue'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport {\n demoteWidget,\n isRecommendedWidget,\n matchesPropertyItem,\n matchesWidgetItem,\n promoteWidget,\n pruneDisconnected,\n widgetItemToProperty\n} from '@/core/graph/subgraph/proxyWidgetUtils'\nimport type { WidgetItem } from '@/core/graph/subgraph/proxyWidgetUtils'\nimport { parseProxyWidgets } from '@/core/schemas/proxyWidget'\nimport type { ProxyWidgetsProperty } from '@/core/schemas/proxyWidget'\nimport type { LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'\nimport type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport FormSearchInput from '@/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.vue'\nimport { DraggableList } from '@/scripts/ui/draggableList'\nimport { useLitegraphService } from '@/services/litegraphService'\nimport { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'\n\nimport SubgraphNodeWidget from './SubgraphNodeWidget.vue'\n\nconst canvasStore = useCanvasStore()\nconst rightSidePanelStore = useRightSidePanelStore()\nconst { searchQuery } = storeToRefs(rightSidePanelStore)\n\nconst draggableList = ref<DraggableList | undefined>(undefined)\nconst draggableItems = ref()\nconst proxyWidgets = customRef<ProxyWidgetsProperty>((track, trigger) => ({\n get() {\n track()\n const node = activeNode.value\n if (!node) return []\n return parseProxyWidgets(node.properties.proxyWidgets)\n },\n set(value?: ProxyWidgetsProperty) {\n trigger()\n const node = activeNode.value\n if (!value) return\n if (!node) {\n console.error('Attempted to toggle widgets with no node selected')\n return\n }\n node.properties.proxyWidgets = value\n }\n}))\n\nconst activeNode = computed(() => {\n const node = canvasStore.selectedItems[0]\n if (node instanceof SubgraphNode) return node\n return undefined\n})\n\nconst activeWidgets = computed<WidgetItem[]>({\n get() {\n if (!activeNode.value) return []\n const node = activeNode.value\n function mapWidgets([id, name]: [string, string]): WidgetItem[] {\n if (id === '-1') {\n const widget = node.widgets.find((w) => w.name === name)\n if (!widget) return []\n return [[{ id: -1, title: '(Linked)', type: '' }, widget]]\n }\n const wNode = node.subgraph._nodes_by_id[id]\n if (!wNode?.widgets) return []\n const widget = wNode.widgets.find((w) => w.name === name)\n if (!widget) return []\n return [[wNode, widget]]\n }\n return proxyWidgets.value.flatMap(mapWidgets)\n },\n set(value: WidgetItem[]) {\n const node = activeNode.value\n if (!node) {\n console.error('Attempted to toggle widgets with no node selected')\n return\n }\n proxyWidgets.value = value.map(widgetItemToProperty)\n }\n})\n\nconst interiorWidgets = computed<WidgetItem[]>(() => {\n const node = activeNode.value\n if (!node) return []\n const { updatePreviews } = useLitegraphService()\n const interiorNodes = node.subgraph.nodes\n for (const node of interiorNodes) {\n node.updateComputedDisabled()\n updatePreviews(node)\n }\n return interiorNodes\n .flatMap(nodeWidgets)\n .filter(([_, w]: WidgetItem) => !w.computedDisabled)\n})\n\nconst candidateWidgets = computed<WidgetItem[]>(() => {\n const node = activeNode.value\n if (!node) return []\n const widgets = proxyWidgets.value\n return interiorWidgets.value.filter(\n (widgetItem: WidgetItem) => !widgets.some(matchesPropertyItem(widgetItem))\n )\n})\nconst filteredCandidates = computed<WidgetItem[]>(() => {\n const query = searchQuery.value.toLowerCase()\n if (!query) return candidateWidgets.value\n return candidateWidgets.value.filter(\n ([n, w]: WidgetItem) =>\n n.title.toLowerCase().includes(query) ||\n w.name.toLowerCase().includes(query)\n )\n})\n\nconst recommendedWidgets = computed(() => {\n const node = activeNode.value\n if (!node) return []\n return filteredCandidates.value.filter(isRecommendedWidget)\n})\n\nconst filteredActive = computed<WidgetItem[]>(() => {\n const query = searchQuery.value.toLowerCase()\n if (!query) return activeWidgets.value\n return activeWidgets.value.filter(\n ([n, w]: WidgetItem) =>\n n.title.toLowerCase().includes(query) ||\n w.name.toLowerCase().includes(query)\n )\n})\n\nfunction toKey(item: WidgetItem) {\n return `${item[0].id}: ${item[1].name}`\n}\nfunction nodeWidgets(n: LGraphNode): WidgetItem[] {\n if (!n.widgets) return []\n return n.widgets.map((w: IBaseWidget) => [n, w])\n}\nfunction demote([node, widget]: WidgetItem) {\n const subgraphNode = activeNode.value\n if (!subgraphNode) return []\n demoteWidget(node, widget, [subgraphNode])\n triggerRef(proxyWidgets)\n}\nfunction promote([node, widget]: WidgetItem) {\n const subgraphNode = activeNode.value\n if (!subgraphNode) return []\n promoteWidget(node, widget, [subgraphNode])\n triggerRef(proxyWidgets)\n}\nfunction showAll() {\n const node = activeNode.value\n if (!node) return\n const widgets = proxyWidgets.value\n const toAdd: ProxyWidgetsProperty =\n filteredCandidates.value.map(widgetItemToProperty)\n widgets.push(...toAdd)\n proxyWidgets.value = widgets\n}\nfunction hideAll() {\n const node = activeNode.value\n if (!node) return\n proxyWidgets.value = proxyWidgets.value.filter(\n (propertyItem) =>\n !filteredActive.value.some(matchesWidgetItem(propertyItem)) ||\n propertyItem[0] === '-1'\n )\n}\nfunction showRecommended() {\n const node = activeNode.value\n if (!node) return\n const widgets = proxyWidgets.value\n const toAdd: ProxyWidgetsProperty =\n recommendedWidgets.value.map(widgetItemToProperty)\n widgets.push(...toAdd)\n proxyWidgets.value = widgets\n}\n\nfunction setDraggableState() {\n draggableList.value?.dispose()\n if (searchQuery.value || !draggableItems.value?.children?.length) return\n draggableList.value = new DraggableList(\n draggableItems.value,\n '.draggable-item'\n )\n draggableList.value.applyNewItemsOrder = function () {\n const reorderedItems = []\n\n let oldPosition = -1\n this.getAllItems().forEach((item, index) => {\n if (item === this.draggableItem) {\n oldPosition = index\n return\n }\n if (!this.isItemToggled(item)) {\n reorderedItems[index] = item\n return\n }\n const newIndex = this.isItemAbove(item) ? index + 1 : index - 1\n reorderedItems[newIndex] = item\n })\n\n for (let index = 0; index < this.getAllItems().length; index++) {\n const item = reorderedItems[index]\n if (typeof item === 'undefined') {\n reorderedItems[index] = this.draggableItem\n }\n }\n const newPosition = reorderedItems.indexOf(this.draggableItem)\n const aw = activeWidgets.value\n const [w] = aw.splice(oldPosition, 1)\n aw.splice(newPosition, 0, w)\n activeWidgets.value = aw\n }\n}\nwatch(filteredActive, () => {\n setDraggableState()\n})\n\nonMounted(() => {\n setDraggableState()\n if (activeNode.value) pruneDisconnected(activeNode.value)\n})\nonBeforeUnmount(() => {\n draggableList.value?.dispose()\n})\n</script>\n\n<template>\n <div v-if=\"activeNode\" class=\"subgraph-edit-section flex h-full flex-col\">\n <div class=\"px-4 pb-4 pt-1 flex gap-2 border-b border-interface-stroke\">\n <FormSearchInput v-model=\"searchQuery\" />\n </div>\n\n <div class=\"flex-1\">\n <div\n v-if=\"\n searchQuery &&\n filteredActive.length === 0 &&\n filteredCandidates.length === 0\n \"\n class=\"text-sm text-muted-foreground px-4 py-10 text-center\"\n >\n {{ $t('rightSidePanel.noneSearchDesc') }}\n </div>\n\n <div\n v-if=\"filteredActive.length\"\n class=\"flex flex-col border-b border-interface-stroke\"\n >\n <div\n class=\"sticky top-0 z-10 flex items-center justify-between backdrop-blur-xl min-h-12 px-4\"\n >\n <div class=\"text-sm font-semibold uppercase line-clamp-1\">\n {{ $t('subgraphStore.shown') }}\n </div>\n <a\n class=\"cursor-pointer text-right text-xs font-normal text-text-secondary hover:text-azure-600 whitespace-nowrap\"\n @click.stop=\"hideAll\"\n >\n {{ $t('subgraphStore.hideAll') }}</a\n >\n </div>\n <div ref=\"draggableItems\" class=\"pb-2 px-2 space-y-0.5 mt-0.5\">\n <SubgraphNodeWidget\n v-for=\"[node, widget] in filteredActive\"\n :key=\"toKey([node, widget])\"\n class=\"bg-comfy-menu-bg\"\n :node-title=\"node.title\"\n :widget-name=\"widget.name\"\n :is-shown=\"true\"\n :is-draggable=\"!searchQuery\"\n :is-physical=\"node.id === -1\"\n @toggle-visibility=\"demote([node, widget])\"\n />\n </div>\n </div>\n\n <div\n v-if=\"filteredCandidates.length\"\n class=\"flex flex-col border-b border-interface-stroke\"\n >\n <div\n class=\"sticky top-0 z-10 flex items-center justify-between backdrop-blur-xl min-h-12 px-4\"\n >\n <div class=\"text-sm font-semibold uppercase line-clamp-1\">\n {{ $t('subgraphStore.hidden') }}\n </div>\n <a\n class=\"cursor-pointer text-right text-xs font-normal text-text-secondary hover:text-azure-600 whitespace-nowrap\"\n @click.stop=\"showAll\"\n >\n {{ $t('subgraphStore.showAll') }}</a\n >\n </div>\n <div class=\"pb-2 px-2 space-y-0.5 mt-0.5\">\n <SubgraphNodeWidget\n v-for=\"[node, widget] in filteredCandidates\"\n :key=\"toKey([node, widget])\"\n class=\"bg-comfy-menu-bg\"\n :node-title=\"node.title\"\n :widget-name=\"widget.name\"\n @toggle-visibility=\"promote([node, widget])\"\n />\n </div>\n </div>\n\n <div\n v-if=\"recommendedWidgets.length\"\n class=\"flex justify-center border-b border-interface-stroke py-4\"\n >\n <Button\n size=\"sm\"\n class=\"rounded border-none px-3 py-0.5\"\n @click.stop=\"showRecommended\"\n >\n {{ $t('subgraphStore.showRecommended') }}\n </Button>\n </div>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { storeToRefs } from 'pinia'\nimport { computed, provide, ref, watchEffect } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport EditableText from '@/components/common/EditableText.vue'\nimport Tab from '@/components/tab/Tab.vue'\nimport TabList from '@/components/tab/TabList.vue'\nimport Button from '@/components/ui/button/Button.vue'\nimport { useGraphHierarchy } from '@/composables/graph/useGraphHierarchy'\nimport { SubgraphNode } from '@/lib/litegraph/src/litegraph'\nimport type { LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'\nimport type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore'\nimport { cn } from '@/utils/tailwindUtil'\n\nimport TabInfo from './info/TabInfo.vue'\nimport TabGlobalParameters from './parameters/TabGlobalParameters.vue'\nimport TabNodes from './parameters/TabNodes.vue'\nimport TabNormalInputs from './parameters/TabNormalInputs.vue'\nimport TabSubgraphInputs from './parameters/TabSubgraphInputs.vue'\nimport TabGlobalSettings from './settings/TabGlobalSettings.vue'\nimport TabSettings from './settings/TabSettings.vue'\nimport {\n GetNodeParentGroupKey,\n useFlatAndCategorizeSelectedItems\n} from './shared'\nimport SubgraphEditor from './subgraph/SubgraphEditor.vue'\n\nconst canvasStore = useCanvasStore()\nconst rightSidePanelStore = useRightSidePanelStore()\nconst settingStore = useSettingStore()\nconst { t } = useI18n()\nconst { findParentGroup } = useGraphHierarchy()\n\nconst { selectedItems: directlySelectedItems } = storeToRefs(canvasStore)\nconst { activeTab, isEditingSubgraph } = storeToRefs(rightSidePanelStore)\n\nconst sidebarLocation = computed<'left' | 'right'>(() =>\n settingStore.get('Comfy.Sidebar.Location')\n)\n\n// Panel is on the left when sidebar is on the right, and vice versa\nconst panelIcon = computed(() =>\n sidebarLocation.value === 'right'\n ? 'icon-[lucide--panel-left]'\n : 'icon-[lucide--panel-right]'\n)\n\nconst { flattedItems, selectedNodes, selectedGroups, nodeToParentGroup } =\n useFlatAndCategorizeSelectedItems(directlySelectedItems)\n\nconst shouldShowGroupNames = computed(() => {\n return !(\n directlySelectedItems.value.length === 1 &&\n (selectedGroups.value.length === 1 || selectedNodes.value.length === 1)\n )\n})\n\nprovide(GetNodeParentGroupKey, (node: LGraphNode) => {\n if (!shouldShowGroupNames.value) return null\n return nodeToParentGroup.value.get(node) ?? findParentGroup(node)\n})\n\nconst hasSelection = computed(() => flattedItems.value.length > 0)\n\nconst selectedSingleNode = computed(() => {\n return selectedNodes.value.length === 1 && flattedItems.value.length === 1\n ? selectedNodes.value[0]\n : null\n})\n\nconst isSingleSubgraphNode = computed(() => {\n return selectedSingleNode.value instanceof SubgraphNode\n})\n\nfunction closePanel() {\n rightSidePanelStore.closePanel()\n}\n\ntype RightSidePanelTabList = Array<{\n label: () => string\n value: RightSidePanelTab\n}>\n\nconst tabs = computed<RightSidePanelTabList>(() => {\n const list: RightSidePanelTabList = []\n\n list.push({\n label: () =>\n flattedItems.value.length > 1\n ? t('rightSidePanel.nodes')\n : t('rightSidePanel.parameters'),\n value: 'parameters'\n })\n\n if (!hasSelection.value) {\n list.push({\n label: () => t('rightSidePanel.nodes'),\n value: 'nodes'\n })\n }\n\n if (hasSelection.value) {\n if (selectedSingleNode.value && !isSingleSubgraphNode.value) {\n list.push({\n label: () => t('rightSidePanel.info'),\n value: 'info'\n })\n }\n }\n\n list.push({\n label: () =>\n hasSelection.value\n ? t('g.settings')\n : t('rightSidePanel.globalSettings.title'),\n value: 'settings'\n })\n\n return list\n})\n\n// Use global state for activeTab and ensure it's valid\nwatchEffect(() => {\n if (\n !tabs.value.some((tab) => tab.value === activeTab.value) &&\n !(activeTab.value === 'subgraph' && isSingleSubgraphNode.value)\n ) {\n rightSidePanelStore.openPanel(tabs.value[0].value)\n }\n})\n\nfunction resolveTitle() {\n const items = flattedItems.value\n const nodes = selectedNodes.value\n const groups = selectedGroups.value\n\n if (items.length === 0) {\n return t('rightSidePanel.workflowOverview')\n }\n if (directlySelectedItems.value.length === 1) {\n if (groups.length === 1) {\n return groups[0].title || t('rightSidePanel.fallbackGroupTitle')\n }\n if (nodes.length === 1) {\n return (\n nodes[0].title || nodes[0].type || t('rightSidePanel.fallbackNodeTitle')\n )\n }\n }\n return t('rightSidePanel.title', { count: items.length })\n}\n\nconst panelTitle = ref(resolveTitle())\nwatchEffect(() => (panelTitle.value = resolveTitle()))\n\nconst isEditing = ref(false)\n\nconst allowTitleEdit = computed(() => {\n return (\n directlySelectedItems.value.length === 1 &&\n (selectedGroups.value.length === 1 || selectedNodes.value.length === 1)\n )\n})\n\nfunction handleTitleEdit(newTitle: string) {\n isEditing.value = false\n\n const trimmedTitle = newTitle.trim()\n if (!trimmedTitle) return\n\n const node = selectedGroups.value[0] || selectedNodes.value[0]\n if (!node) return\n\n if (trimmedTitle === node.title) return\n\n node.title = trimmedTitle\n panelTitle.value = trimmedTitle\n canvasStore.canvas?.setDirty(true, true)\n}\n\nfunction handleTitleCancel() {\n isEditing.value = false\n}\n</script>\n\n<template>\n <div\n data-testid=\"properties-panel\"\n class=\"flex size-full flex-col bg-comfy-menu-bg\"\n >\n <!-- Panel Header -->\n <section class=\"pt-1\">\n <div class=\"flex items-center justify-between pl-4 pr-3\">\n <h3 class=\"my-3.5 text-sm font-semibold line-clamp-2 cursor-default\">\n <template v-if=\"allowTitleEdit\">\n <EditableText\n :model-value=\"panelTitle\"\n :is-editing=\"isEditing\"\n :input-attrs=\"{ 'data-testid': 'node-title-input' }\"\n class=\"cursor-text\"\n @edit=\"handleTitleEdit\"\n @cancel=\"handleTitleCancel\"\n @click=\"isEditing = true\"\n />\n <i\n v-if=\"!isEditing\"\n class=\"icon-[lucide--pencil] size-4 text-muted-foreground ml-2 content-center relative top-[2px] hover:text-base-foreground cursor-pointer shrink-0\"\n @click=\"isEditing = true\"\n />\n </template>\n <template v-else>\n {{ panelTitle }}\n </template>\n </h3>\n\n <div class=\"flex gap-2\">\n <Button\n v-if=\"isSingleSubgraphNode\"\n variant=\"secondary\"\n size=\"icon\"\n :class=\"cn(isEditingSubgraph && 'bg-secondary-background-selected')\"\n @click=\"\n rightSidePanelStore.openPanel(\n isEditingSubgraph ? 'parameters' : 'subgraph'\n )\n \"\n >\n <i class=\"icon-[lucide--settings-2]\" />\n </Button>\n <Button\n variant=\"secondary\"\n size=\"icon\"\n :aria-pressed=\"rightSidePanelStore.isOpen\"\n :aria-label=\"t('rightSidePanel.togglePanel')\"\n @click=\"closePanel\"\n >\n <i :class=\"cn(panelIcon, 'size-4')\" />\n </Button>\n </div>\n </div>\n <nav class=\"px-4 pb-2 pt-1 overflow-x-auto\">\n <TabList\n :model-value=\"activeTab\"\n @update:model-value=\"\n (newTab: RightSidePanelTab) => {\n rightSidePanelStore.openPanel(newTab)\n }\n \"\n >\n <Tab\n v-for=\"tab in tabs\"\n :key=\"tab.value\"\n class=\"text-sm py-1 px-2 font-inter transition-all active:scale-95\"\n :value=\"tab.value\"\n >\n {{ tab.label() }}\n </Tab>\n </TabList>\n </nav>\n </section>\n\n <!-- Panel Content -->\n <div class=\"scrollbar-thin flex-1 overflow-y-auto\">\n <template v-if=\"!hasSelection\">\n <TabGlobalParameters v-if=\"activeTab === 'parameters'\" />\n <TabNodes v-else-if=\"activeTab === 'nodes'\" />\n <TabGlobalSettings v-else-if=\"activeTab === 'settings'\" />\n </template>\n <SubgraphEditor\n v-else-if=\"isSingleSubgraphNode && isEditingSubgraph\"\n :node=\"selectedSingleNode\"\n />\n <template v-else>\n <TabSubgraphInputs\n v-if=\"activeTab === 'parameters' && isSingleSubgraphNode\"\n :node=\"selectedSingleNode as SubgraphNode\"\n />\n <TabNormalInputs\n v-else-if=\"activeTab === 'parameters'\"\n :nodes=\"selectedNodes\"\n :must-show-node-title=\"selectedGroups.length > 0\"\n />\n <TabInfo v-else-if=\"activeTab === 'info'\" :nodes=\"selectedNodes\" />\n <TabSettings\n v-else-if=\"activeTab === 'settings'\"\n :nodes=\"flattedItems\"\n />\n </template>\n </div>\n </div>\n</template>\n","import { useMouse } from '@vueuse/core'\nimport { defineStore } from 'pinia'\nimport { computed, ref, shallowRef } from 'vue'\n\nimport type NodeSearchBoxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'\nimport type { CanvasPointerEvent } from '@/lib/litegraph/src/litegraph'\nimport { useSettingStore } from '@/platform/settings/settingStore'\n\nexport const useSearchBoxStore = defineStore('searchBox', () => {\n const settingStore = useSettingStore()\n const { x, y } = useMouse()\n\n const newSearchBoxEnabled = computed(\n () => settingStore.get('Comfy.NodeSearchBoxImpl') === 'default'\n )\n\n const popoverRef = shallowRef<InstanceType<\n typeof NodeSearchBoxPopover\n > | null>(null)\n\n function setPopoverRef(\n popover: InstanceType<typeof NodeSearchBoxPopover> | null\n ) {\n popoverRef.value = popover\n }\n\n const visible = ref(false)\n function toggleVisible() {\n if (newSearchBoxEnabled.value) {\n visible.value = !visible.value\n return\n }\n if (!popoverRef.value) return\n popoverRef.value.showSearchBox(\n new MouseEvent('click', {\n clientX: x.value,\n clientY: y.value,\n // @ts-expect-error layerY is a nonstandard property\n layerY: y.value\n }) as unknown as CanvasPointerEvent\n )\n }\n\n return {\n newSearchBoxEnabled,\n setPopoverRef,\n toggleVisible,\n visible\n }\n})\n","<template>\n <div\n class=\"option-container flex w-full cursor-pointer items-center justify-between overflow-hidden px-2 py-0\"\n >\n <div class=\"option-display-name flex flex-col font-semibold\">\n <div>\n <span v-if=\"isBookmarked\">\n <i class=\"pi pi-bookmark-fill mr-1 text-sm\" />\n </span>\n <span v-html=\"highlightQuery(nodeDef.display_name, currentQuery)\" />\n <span>&nbsp;</span>\n <Tag v-if=\"showIdName\" severity=\"secondary\">\n <span v-html=\"highlightQuery(nodeDef.name, currentQuery)\" />\n </Tag>\n </div>\n <div\n v-if=\"showCategory\"\n class=\"option-category truncate text-sm font-light text-muted\"\n >\n {{ nodeDef.category.replaceAll('/', ' > ') }}\n </div>\n </div>\n <div class=\"option-badges\">\n <Tag\n v-if=\"nodeDef.experimental\"\n :value=\"$t('g.experimental')\"\n severity=\"primary\"\n />\n <Tag\n v-if=\"nodeDef.deprecated\"\n :value=\"$t('g.deprecated')\"\n severity=\"danger\"\n />\n <Tag\n v-if=\"showNodeFrequency && nodeFrequency > 0\"\n :value=\"formatNumberWithSuffix(nodeFrequency, { roundToInt: true })\"\n severity=\"secondary\"\n />\n <Chip\n v-if=\"nodeDef.nodeSource.type !== NodeSourceType.Unknown\"\n class=\"text-sm font-light\"\n >\n {{ nodeDef.nodeSource.displayText }}\n </Chip>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport Chip from 'primevue/chip'\nimport Tag from 'primevue/tag'\nimport { computed } from 'vue'\n\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'\nimport type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'\nimport { useNodeFrequencyStore } from '@/stores/nodeDefStore'\nimport { NodeSourceType } from '@/types/nodeSource'\nimport { formatNumberWithSuffix, highlightQuery } from '@/utils/formatUtil'\n\nconst settingStore = useSettingStore()\nconst showCategory = computed(() =>\n settingStore.get('Comfy.NodeSearchBoxImpl.ShowCategory')\n)\nconst showIdName = computed(() =>\n settingStore.get('Comfy.NodeSearchBoxImpl.ShowIdName')\n)\nconst showNodeFrequency = computed(() =>\n settingStore.get('Comfy.NodeSearchBoxImpl.ShowNodeFrequency')\n)\nconst nodeFrequencyStore = useNodeFrequencyStore()\nconst nodeFrequency = computed(() =>\n nodeFrequencyStore.getNodeFrequency(props.nodeDef)\n)\n\nconst nodeBookmarkStore = useNodeBookmarkStore()\nconst isBookmarked = computed(() =>\n nodeBookmarkStore.isBookmarked(props.nodeDef)\n)\n\nconst props = defineProps<{\n nodeDef: ComfyNodeDefImpl\n currentQuery: string\n}>()\n</script>\n\n<style scoped>\n:deep(.highlight) {\n background-color: var(--p-primary-color);\n color: var(--p-primary-contrast-color);\n font-weight: 700;\n border-radius: 0.25rem;\n padding: 0 0.125rem;\n margin: -0.125rem 0.125rem;\n}\n</style>\n","<template>\n <div\n class=\"comfy-vue-node-search-container flex w-full min-w-96 items-center justify-center\"\n >\n <div\n v-if=\"enableNodePreview && hoveredSuggestion\"\n class=\"comfy-vue-node-preview-container absolute top-[50px] left-[-375px] z-50 cursor-pointer\"\n @mousedown.stop=\"onAddNode(hoveredSuggestion!)\"\n >\n <NodePreview\n :key=\"hoveredSuggestion?.name || ''\"\n :node-def=\"hoveredSuggestion\"\n />\n </div>\n\n <Button\n variant=\"secondary\"\n :aria-label=\"$t('g.addNodeFilterCondition')\"\n class=\"filter-button z-10\"\n @click=\"nodeSearchFilterVisible = true\"\n >\n <i class=\"pi pi-filter\" />\n </Button>\n <Dialog\n v-model:visible=\"nodeSearchFilterVisible\"\n class=\"min-w-96\"\n dismissable-mask\n modal\n @hide=\"reFocusInput\"\n >\n <template #header>\n <h3>{{ $t('g.addNodeFilterCondition') }}</h3>\n </template>\n <div class=\"_dialog-body\">\n <NodeSearchFilter @add-filter=\"onAddFilter\" />\n </div>\n </Dialog>\n\n <AutoCompletePlus\n ref=\"autoCompletePlus\"\n :model-value=\"filters\"\n class=\"comfy-vue-node-search-box z-10 grow\"\n scroll-height=\"40vh\"\n :placeholder=\"placeholder\"\n :input-id=\"inputId\"\n append-to=\"self\"\n :suggestions=\"suggestions\"\n :delay=\"100\"\n :loading=\"!nodeFrequencyStore.isLoaded\"\n complete-on-focus\n auto-option-focus\n force-selection\n multiple\n option-label=\"display_name\"\n @complete=\"search($event.query)\"\n @option-select=\"onAddNode($event.value)\"\n @focused-option-changed=\"setHoverSuggestion($event)\"\n >\n <template #option=\"{ option }\">\n <NodeSearchItem :node-def=\"option\" :current-query=\"currentQuery\" />\n </template>\n <!-- FilterAndValue -->\n <template #chip=\"{ value }\">\n <SearchFilterChip\n v-if=\"value.filterDef && value.value\"\n :key=\"`${value.filterDef.id}-${value.value}`\"\n :text=\"value.value\"\n :badge=\"value.filterDef.invokeSequence.toUpperCase()\"\n :badge-class=\"value.filterDef.invokeSequence + '-badge'\"\n @remove=\"\n onRemoveFilter(\n $event,\n value as FuseFilterWithValue<ComfyNodeDefImpl, string>\n )\n \"\n />\n </template>\n </AutoCompletePlus>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { debounce } from 'es-toolkit/compat'\nimport Dialog from 'primevue/dialog'\nimport { computed, nextTick, onMounted, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport NodePreview from '@/components/node/NodePreview.vue'\nimport AutoCompletePlus from '@/components/primevueOverride/AutoCompletePlus.vue'\nimport NodeSearchFilter from '@/components/searchbox/NodeSearchFilter.vue'\nimport NodeSearchItem from '@/components/searchbox/NodeSearchItem.vue'\nimport Button from '@/components/ui/button/Button.vue'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useTelemetry } from '@/platform/telemetry'\nimport type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'\nimport { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'\nimport type { FuseFilterWithValue } from '@/utils/fuseUtil'\n\nimport SearchFilterChip from '../common/SearchFilterChip.vue'\n\nconst settingStore = useSettingStore()\nconst { t } = useI18n()\nconst telemetry = useTelemetry()\n\nconst enableNodePreview = computed(() =>\n settingStore.get('Comfy.NodeSearchBoxImpl.NodePreview')\n)\n\nconst { filters, searchLimit = 64 } = defineProps<{\n filters: FuseFilterWithValue<ComfyNodeDefImpl, string>[]\n searchLimit?: number\n}>()\n\nconst autoCompletePlus = ref()\nconst nodeSearchFilterVisible = ref(false)\nconst inputId = `comfy-vue-node-search-box-input-${Math.random()}`\nconst suggestions = ref<ComfyNodeDefImpl[]>([])\nconst hoveredSuggestion = ref<ComfyNodeDefImpl | null>(null)\nconst currentQuery = ref('')\nconst placeholder = computed(() => {\n return filters.length === 0 ? t('g.searchNodes') + '...' : ''\n})\n\nconst nodeDefStore = useNodeDefStore()\nconst nodeFrequencyStore = useNodeFrequencyStore()\n\n// Debounced search tracking (500ms as per implementation plan)\nconst debouncedTrackSearch = debounce((query: string) => {\n if (query.trim()) {\n telemetry?.trackNodeSearch({ query })\n }\n}, 500)\n\nconst search = (query: string) => {\n const queryIsEmpty = query === '' && filters.length === 0\n currentQuery.value = query\n suggestions.value = queryIsEmpty\n ? nodeFrequencyStore.topNodeDefs\n : [\n ...nodeDefStore.nodeSearchService.searchNode(query, filters, {\n limit: searchLimit\n })\n ]\n\n // Track search queries with debounce\n debouncedTrackSearch(query)\n}\n\nconst emit = defineEmits(['addFilter', 'removeFilter', 'addNode'])\n\n// Track node selection and emit addNode event\nconst onAddNode = (nodeDef: ComfyNodeDefImpl) => {\n telemetry?.trackNodeSearchResultSelected({\n node_type: nodeDef.name,\n last_query: currentQuery.value\n })\n emit('addNode', nodeDef)\n}\n\nlet inputElement: HTMLInputElement | null = null\nconst reFocusInput = async () => {\n inputElement ??= document.getElementById(inputId) as HTMLInputElement\n if (inputElement) {\n inputElement.blur()\n await nextTick(() => inputElement?.focus())\n }\n}\n\nonMounted(() => {\n inputElement ??= document.getElementById(inputId) as HTMLInputElement\n if (inputElement) inputElement.focus()\n autoCompletePlus.value.hide = () => search('')\n search('')\n autoCompletePlus.value.show()\n})\nconst onAddFilter = (\n filterAndValue: FuseFilterWithValue<ComfyNodeDefImpl, string>\n) => {\n nodeSearchFilterVisible.value = false\n emit('addFilter', filterAndValue)\n}\nconst onRemoveFilter = async (\n event: Event,\n filterAndValue: FuseFilterWithValue<ComfyNodeDefImpl, string>\n) => {\n event.stopPropagation()\n event.preventDefault()\n emit('removeFilter', filterAndValue)\n await reFocusInput()\n}\nconst setHoverSuggestion = (index: number) => {\n if (index === -1) {\n hoveredSuggestion.value = null\n return\n }\n const value = suggestions.value[index]\n hoveredSuggestion.value = value\n}\n</script>\n","<template>\n <div>\n <Dialog\n v-model:visible=\"visible\"\n modal\n :dismissable-mask=\"dismissable\"\n :pt=\"{\n root: {\n class: 'invisible-dialog-root',\n role: 'search'\n },\n mask: { class: 'node-search-box-dialog-mask' },\n transition: {\n enterFromClass: 'opacity-0 scale-75',\n // 100ms is the duration of the transition in the dialog component\n enterActiveClass: 'transition-all duration-100 ease-out',\n leaveActiveClass: 'transition-all duration-100 ease-in',\n leaveToClass: 'opacity-0 scale-75'\n }\n }\"\n @hide=\"clearFilters\"\n >\n <template #container>\n <NodeSearchBox\n :filters=\"nodeFilters\"\n @add-filter=\"addFilter\"\n @remove-filter=\"removeFilter\"\n @add-node=\"addNode\"\n />\n </template>\n </Dialog>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useEventListener } from '@vueuse/core'\nimport { storeToRefs } from 'pinia'\nimport Dialog from 'primevue/dialog'\nimport { computed, ref, toRaw, watch, watchEffect } from 'vue'\n\nimport type { Point } from '@/lib/litegraph/src/interfaces'\nimport type { LiteGraphCanvasEvent } from '@/lib/litegraph/src/litegraph'\nimport { LGraphNode, LiteGraph } from '@/lib/litegraph/src/litegraph'\nimport type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useLitegraphService } from '@/services/litegraphService'\nimport type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'\nimport { useNodeDefStore } from '@/stores/nodeDefStore'\nimport { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'\nimport { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'\nimport type { FuseFilterWithValue } from '@/utils/fuseUtil'\n\nimport NodeSearchBox from './NodeSearchBox.vue'\n\nlet triggerEvent: CanvasPointerEvent | null = null\nlet listenerController: AbortController | null = null\nlet disconnectOnReset = false\n\nconst settingStore = useSettingStore()\nconst searchBoxStore = useSearchBoxStore()\nconst litegraphService = useLitegraphService()\n\nconst { visible, newSearchBoxEnabled } = storeToRefs(searchBoxStore)\nconst dismissable = ref(true)\nfunction getNewNodeLocation(): Point {\n return triggerEvent\n ? [triggerEvent.canvasX, triggerEvent.canvasY]\n : litegraphService.getCanvasCenter()\n}\nconst nodeFilters = ref<FuseFilterWithValue<ComfyNodeDefImpl, string>[]>([])\nfunction addFilter(filter: FuseFilterWithValue<ComfyNodeDefImpl, string>) {\n nodeFilters.value.push(filter)\n}\nfunction removeFilter(filter: FuseFilterWithValue<ComfyNodeDefImpl, string>) {\n nodeFilters.value = nodeFilters.value.filter(\n (f) => toRaw(f) !== toRaw(filter)\n )\n}\nfunction clearFilters() {\n nodeFilters.value = []\n}\nfunction closeDialog() {\n visible.value = false\n}\nconst canvasStore = useCanvasStore()\n\nfunction addNode(nodeDef: ComfyNodeDefImpl) {\n const node = litegraphService.addNodeOnGraph(nodeDef, {\n pos: getNewNodeLocation()\n })\n\n if (disconnectOnReset && triggerEvent) {\n canvasStore.getCanvas().linkConnector.connectToNode(node, triggerEvent)\n } else if (!triggerEvent) {\n console.warn('The trigger event was undefined when addNode was called.')\n }\n\n disconnectOnReset = false\n\n // Notify changeTracker - new step should be added\n useWorkflowStore().activeWorkflow?.changeTracker?.checkState()\n window.requestAnimationFrame(closeDialog)\n}\n\nfunction showSearchBox(e: CanvasPointerEvent | null) {\n if (newSearchBoxEnabled.value) {\n if (e?.pointerType === 'touch') {\n setTimeout(() => {\n showNewSearchBox(e)\n }, 128)\n } else {\n showNewSearchBox(e)\n }\n } else {\n canvasStore.getCanvas().showSearchBox(e)\n }\n}\n\nfunction getFirstLink() {\n return canvasStore.getCanvas().linkConnector.renderLinks.at(0)\n}\n\nconst nodeDefStore = useNodeDefStore()\nfunction showNewSearchBox(e: CanvasPointerEvent | null) {\n const firstLink = getFirstLink()\n if (firstLink) {\n const filter =\n firstLink.toType === 'input'\n ? nodeDefStore.nodeSearchService.inputTypeFilter\n : nodeDefStore.nodeSearchService.outputTypeFilter\n\n const dataType = firstLink.fromSlot.type?.toString() ?? ''\n addFilter({\n filterDef: filter,\n value: dataType\n })\n }\n\n visible.value = true\n triggerEvent = e\n\n // Prevent the dialog from being dismissed immediately\n dismissable.value = false\n setTimeout(() => {\n dismissable.value = true\n }, 300)\n}\n\nfunction showContextMenu(e: CanvasPointerEvent) {\n const firstLink = getFirstLink()\n if (!firstLink) return\n\n const { node, fromSlot, toType } = firstLink\n const commonOptions = {\n e,\n allow_searchbox: true,\n showSearchBox: () => {\n cancelResetOnContextClose()\n showSearchBox(e)\n }\n }\n const afterRerouteId = firstLink.fromReroute?.id\n const connectionOptions =\n toType === 'input'\n ? { nodeFrom: node, slotFrom: fromSlot, afterRerouteId }\n : { nodeTo: node, slotTo: fromSlot, afterRerouteId }\n\n const canvas = canvasStore.getCanvas()\n const menu = canvas.showConnectionMenu({\n ...connectionOptions,\n ...commonOptions\n })\n\n if (!menu) {\n console.warn('No menu was returned from showConnectionMenu')\n return\n }\n\n triggerEvent = e\n listenerController = new AbortController()\n const { signal } = listenerController\n const options = { once: true, signal }\n\n // Connect the node after it is created via context menu\n useEventListener(\n canvas.canvas,\n 'connect-new-default-node',\n (createEvent) => {\n if (!(createEvent instanceof CustomEvent))\n throw new Error('Invalid event')\n\n const node: unknown = createEvent.detail?.node\n if (!(node instanceof LGraphNode)) throw new Error('Invalid node')\n\n disconnectOnReset = false\n createEvent.preventDefault()\n canvas.linkConnector.connectToNode(node, e)\n },\n options\n )\n\n // Reset when the context menu is closed\n const cancelResetOnContextClose = useEventListener(\n menu.controller.signal,\n 'abort',\n reset,\n options\n )\n}\n\n// Disable litegraph's default behavior of release link and search box.\nwatchEffect(() => {\n const { canvas } = canvasStore\n if (!canvas) return\n\n LiteGraph.release_link_on_empty_shows_menu = false\n canvas.allow_searchbox = false\n\n useEventListener(\n canvas.linkConnector.events,\n 'dropped-on-canvas',\n handleDroppedOnCanvas\n )\n})\n\nfunction canvasEventHandler(e: LiteGraphCanvasEvent) {\n if (e.detail.subType === 'empty-double-click') {\n showSearchBox(e.detail.originalEvent)\n } else if (e.detail.subType === 'group-double-click') {\n const group = e.detail.group\n const [_, y] = group.pos\n const relativeY = e.detail.originalEvent.canvasY - y\n // Show search box if the click is NOT on the title bar\n if (relativeY > group.titleHeight) {\n showSearchBox(e.detail.originalEvent)\n }\n }\n}\n\nconst linkReleaseAction = computed(() =>\n settingStore.get('Comfy.LinkRelease.Action')\n)\n\nconst linkReleaseActionShift = computed(() =>\n settingStore.get('Comfy.LinkRelease.ActionShift')\n)\n\n// Prevent normal LinkConnector reset (called by CanvasPointer.finally)\nfunction preventDefault(e: Event) {\n return e.preventDefault()\n}\nfunction cancelNextReset(e: CustomEvent<CanvasPointerEvent>) {\n e.preventDefault()\n\n const canvas = canvasStore.getCanvas()\n canvas.linkConnector.state.snapLinksPos = [e.detail.canvasX, e.detail.canvasY]\n useEventListener(canvas.linkConnector.events, 'reset', preventDefault, {\n once: true\n })\n}\n\nfunction handleDroppedOnCanvas(e: CustomEvent<CanvasPointerEvent>) {\n disconnectOnReset = true\n const action = e.detail.shiftKey\n ? linkReleaseActionShift.value\n : linkReleaseAction.value\n switch (action) {\n case LinkReleaseTriggerAction.SEARCH_BOX:\n cancelNextReset(e)\n showSearchBox(e.detail)\n break\n case LinkReleaseTriggerAction.CONTEXT_MENU:\n cancelNextReset(e)\n showContextMenu(e.detail)\n break\n case LinkReleaseTriggerAction.NO_ACTION:\n default:\n break\n }\n}\n\n// Resets litegraph state\nfunction reset() {\n listenerController?.abort()\n listenerController = null\n triggerEvent = null\n\n const canvas = canvasStore.getCanvas()\n canvas.linkConnector.events.removeEventListener('reset', preventDefault)\n if (disconnectOnReset) canvas.linkConnector.disconnectLinks()\n\n canvas.linkConnector.reset()\n canvas.setDirty(true, true)\n}\n\n// Reset connecting links when the search box is closed\nwatch(visible, () => {\n if (!visible.value) reset()\n})\n\nuseEventListener(document, 'litegraph:canvas', canvasEventHandler)\ndefineExpose({ showSearchBox })\n</script>\n\n<style>\n.invisible-dialog-root {\n width: 60%;\n min-width: 24rem;\n max-width: 48rem;\n border: 0 !important;\n background-color: transparent !important;\n margin-top: 25vh;\n margin-left: 400px;\n}\n@media all and (max-width: 768px) {\n .invisible-dialog-root {\n margin-left: 0;\n }\n}\n\n.node-search-box-dialog-mask {\n align-items: flex-start !important;\n}\n</style>\n","import { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\nexport type HelpCenterTriggerLocation = 'sidebar' | 'topbar'\n\nexport const useHelpCenterStore = defineStore('helpCenter', () => {\n const isVisible = ref(false)\n const triggerLocation = ref<HelpCenterTriggerLocation>('sidebar')\n\n const toggle = (location: HelpCenterTriggerLocation = 'sidebar') => {\n if (!isVisible.value) {\n triggerLocation.value = location\n }\n isVisible.value = !isVisible.value\n }\n\n const show = (location: HelpCenterTriggerLocation = 'sidebar') => {\n triggerLocation.value = location\n isVisible.value = true\n }\n\n const hide = () => {\n isVisible.value = false\n }\n\n return {\n isVisible,\n triggerLocation,\n toggle,\n show,\n hide\n }\n})\n","import { storeToRefs } from 'pinia'\nimport { computed, onMounted } from 'vue'\n\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useReleaseStore } from '@/platform/updates/common/releaseStore'\nimport { useDialogService } from '@/services/dialogService'\nimport { useHelpCenterStore } from '@/stores/helpCenterStore'\nimport type { HelpCenterTriggerLocation } from '@/stores/helpCenterStore'\nimport { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'\nimport { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'\n\nexport function useHelpCenter(\n triggerFrom: HelpCenterTriggerLocation = 'sidebar'\n) {\n const settingStore = useSettingStore()\n const releaseStore = useReleaseStore()\n const helpCenterStore = useHelpCenterStore()\n const { isVisible: isHelpCenterVisible, triggerLocation } =\n storeToRefs(helpCenterStore)\n const { shouldShowRedDot: showReleaseRedDot } = storeToRefs(releaseStore)\n\n const conflictDetection = useConflictDetection()\n const { showNodeConflictDialog } = useDialogService()\n\n // Use conflict acknowledgment state from composable - call only once\n const { shouldShowRedDot: shouldShowConflictRedDot, markConflictsAsSeen } =\n useConflictAcknowledgment()\n\n // Use either release red dot or conflict red dot\n const shouldShowRedDot = computed((): boolean => {\n const releaseRedDot = showReleaseRedDot.value\n return releaseRedDot || shouldShowConflictRedDot.value\n })\n\n const sidebarLocation = computed(() =>\n settingStore.get('Comfy.Sidebar.Location')\n )\n\n /**\n * Toggle Help Center and track UI button click.\n */\n const toggleHelpCenter = () => {\n useTelemetry()?.trackUiButtonClicked({\n button_id: `${triggerFrom}_help_center_toggled`\n })\n helpCenterStore.toggle(triggerFrom)\n }\n\n const closeHelpCenter = () => {\n helpCenterStore.hide()\n }\n\n /**\n * Handle What's New popup dismissal\n * Check if conflict modal should be shown after ComfyUI update\n */\n const handleWhatsNewDismissed = async () => {\n try {\n // Check if conflict modal should be shown after update\n const shouldShow =\n await conflictDetection.shouldShowConflictModalAfterUpdate()\n if (shouldShow) {\n showConflictModal()\n }\n } catch (error) {\n console.error('[HelpCenter] Error checking conflict modal:', error)\n }\n }\n\n /**\n * Show the node conflict dialog with current conflict data\n */\n const showConflictModal = () => {\n showNodeConflictDialog({\n showAfterWhatsNew: true,\n dialogComponentProps: {\n onClose: () => {\n markConflictsAsSeen()\n }\n }\n })\n }\n\n // Initialize release store on mount\n onMounted(async () => {\n // Initialize release store to fetch releases for toast and popup\n await releaseStore.initialize()\n })\n\n return {\n isHelpCenterVisible,\n triggerLocation,\n shouldShowRedDot,\n sidebarLocation,\n toggleHelpCenter,\n closeHelpCenter,\n handleWhatsNewDismissed\n }\n}\n","<template>\n <div v-if=\"shouldShow\" class=\"release-toast-popup\">\n <div\n class=\"w-96 max-h-96 bg-base-background border border-border-default rounded-lg shadow-[1px_1px_8px_0_rgba(0,0,0,0.4)] flex flex-col\"\n >\n <!-- Main content -->\n <div class=\"p-4 flex flex-col gap-4 flex-1 min-h-0\">\n <!-- Header section with icon and text -->\n <div class=\"flex items-center gap-4\">\n <div\n class=\"p-3 bg-primary-background-hover rounded-lg flex items-center justify-center shrink-0\"\n >\n <i class=\"icon-[lucide--rocket] w-4 h-4 text-white\" />\n </div>\n <div class=\"flex flex-col gap-1\">\n <div\n class=\"text-sm font-normal text-base-foreground leading-[1.429]\"\n >\n {{ $t('releaseToast.newVersionAvailable') }}\n </div>\n <div\n class=\"text-sm font-normal text-muted-foreground leading-[1.21]\"\n >\n {{ latestRelease?.version }}\n </div>\n </div>\n </div>\n\n <!-- Description section -->\n <div\n class=\"pl-14 text-sm font-normal text-muted-foreground leading-[1.21] overflow-y-auto flex-1 min-h-0\"\n v-html=\"formattedContent\"\n ></div>\n </div>\n\n <!-- Footer section -->\n <div class=\"flex justify-between items-center px-4 pb-4\">\n <a\n class=\"flex items-center gap-2 text-sm font-normal py-1 text-muted-foreground hover:text-base-foreground\"\n :href=\"changelogUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n @click=\"handleLearnMore\"\n >\n <i class=\"icon-[lucide--external-link] w-4 h-4\"></i>\n {{ $t('releaseToast.whatsNew') }}\n </a>\n <div class=\"flex items-center gap-4\">\n <button\n class=\"h-6 px-0 bg-transparent border-none text-sm font-normal text-muted-foreground hover:text-base-foreground cursor-pointer\"\n @click=\"handleSkip\"\n >\n {{ $t('releaseToast.skip') }}\n </button>\n <button\n class=\"h-10 px-4 bg-secondary-background hover:bg-secondary-background-hover rounded-lg border-none text-sm font-normal text-base-foreground cursor-pointer\"\n @click=\"handleUpdate\"\n >\n {{ $t('releaseToast.update') }}\n </button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { default as DOMPurify } from 'dompurify'\nimport { computed, onMounted, ref, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useErrorHandling } from '@/composables/useErrorHandling'\nimport { useExternalLink } from '@/composables/useExternalLink'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { isElectron } from '@/utils/envUtil'\nimport { formatVersionAnchor } from '@/utils/formatUtil'\nimport { renderMarkdownToHtml } from '@/utils/markdownRendererUtil'\n\nimport type { ReleaseNote } from '../common/releaseService'\nimport { useReleaseStore } from '../common/releaseStore'\n\nconst { buildDocsUrl } = useExternalLink()\nconst { toastErrorHandler } = useErrorHandling()\nconst releaseStore = useReleaseStore()\nconst { t } = useI18n()\n\n// Local state for dismissed status\nconst isDismissed = ref(false)\n\n// Get latest release from store\nconst latestRelease = computed<ReleaseNote | null>(() => {\n return releaseStore.recentRelease\n})\n\n// Show toast when new version available and not dismissed\nconst shouldShow = computed(\n () => releaseStore.shouldShowToast && !isDismissed.value\n)\n\n// Generate changelog URL with version anchor (language-aware)\nconst changelogUrl = computed(() => {\n const changelogBaseUrl = buildDocsUrl('/changelog', { includeLocale: true })\n if (latestRelease.value?.version) {\n const versionAnchor = formatVersionAnchor(latestRelease.value.version)\n return `${changelogBaseUrl}#${versionAnchor}`\n }\n return changelogBaseUrl\n})\n\nconst formattedContent = computed(() => {\n if (!latestRelease.value?.content) {\n return DOMPurify.sanitize(`<p>${t('releaseToast.description')}</p>`)\n }\n\n try {\n const markdown = latestRelease.value.content\n // Remove the h1 title line and images for toast mode\n const contentWithoutTitle = markdown.replace(/^# .+$/m, '')\n const contentWithoutImages = contentWithoutTitle.replace(\n /!\\[.*?\\]\\(.*?\\)/g,\n ''\n )\n\n // Check if there's meaningful content left after cleanup\n const trimmedContent = contentWithoutImages.trim()\n if (!trimmedContent || trimmedContent.replace(/\\s+/g, '') === '') {\n return DOMPurify.sanitize(`<p>${t('releaseToast.description')}</p>`)\n }\n\n // renderMarkdownToHtml already sanitizes with DOMPurify, so this is safe\n return renderMarkdownToHtml(contentWithoutImages)\n } catch (error) {\n console.error('Error parsing markdown:', error)\n // Fallback to plain text with line breaks - sanitize the HTML we create\n const fallbackContent = latestRelease.value.content.replace(/\\n/g, '<br>')\n return fallbackContent.trim()\n ? DOMPurify.sanitize(fallbackContent)\n : DOMPurify.sanitize(`<p>${t('releaseToast.description')}</p>`)\n }\n})\n\n// Auto-hide timer\nlet hideTimer: ReturnType<typeof setTimeout> | null = null\n\nconst startAutoHide = () => {\n if (hideTimer) clearTimeout(hideTimer)\n hideTimer = setTimeout(() => {\n dismissToast()\n }, 8000) // 8 second auto-hide\n}\n\nconst clearAutoHide = () => {\n if (hideTimer) {\n clearTimeout(hideTimer)\n hideTimer = null\n }\n}\n\nconst dismissToast = () => {\n isDismissed.value = true\n clearAutoHide()\n}\n\nconst handleSkip = () => {\n if (latestRelease.value) {\n void releaseStore.handleSkipRelease(latestRelease.value.version)\n }\n dismissToast()\n}\n\nconst handleLearnMore = () => {\n if (latestRelease.value) {\n void releaseStore.handleShowChangelog(latestRelease.value.version)\n }\n // Do not dismiss; anchor will navigate in new tab but keep toast? spec maybe wants dismiss? We'll dismiss.\n dismissToast()\n}\n\nconst handleUpdate = async () => {\n if (isElectron()) {\n try {\n await useCommandStore().execute('Comfy-Desktop.CheckForUpdates')\n dismissToast()\n } catch (error) {\n toastErrorHandler(error)\n }\n return\n }\n\n window.open(\n buildDocsUrl('/installation/update_comfyui', { includeLocale: true }),\n '_blank'\n )\n dismissToast()\n}\n\n// Start auto-hide when toast becomes visible\nwatch(shouldShow, (isVisible) => {\n if (isVisible) {\n startAutoHide()\n } else {\n clearAutoHide()\n }\n})\n\n// Initialize on mount\nonMounted(async () => {\n // Fetch releases if not already loaded\n if (!releaseStore.releases.length) {\n await releaseStore.fetchReleases()\n }\n})\n\n// Expose methods for testing\ndefineExpose({\n handleSkip,\n handleLearnMore,\n handleUpdate\n})\n</script>\n\n<style scoped>\n/* Toast popup - positioning handled by parent */\n.release-toast-popup {\n position: absolute;\n bottom: 1rem;\n z-index: 1000;\n pointer-events: auto;\n}\n\n/* Sidebar positioning classes applied by parent - matching help center */\n.release-toast-popup.sidebar-left,\n.release-toast-popup.sidebar-left.small-sidebar {\n left: 1rem;\n}\n\n.release-toast-popup.sidebar-right {\n right: 1rem;\n}\n</style>\n","<template>\n <div v-if=\"shouldShow\" class=\"whats-new-popup-container left-4\">\n <div class=\"whats-new-popup\" @click.stop>\n <!-- Close Button -->\n <Button\n class=\"close-button absolute top-2 right-2 z-10 w-8 h-8 p-2 rounded-lg opacity-50\"\n :aria-label=\"$t('g.close')\"\n size=\"icon-sm\"\n variant=\"muted-textonly\"\n @click=\"closePopup\"\n >\n <i class=\"icon-[lucide--x]\" />\n </Button>\n\n <!-- Modal Body -->\n <div class=\"modal-body flex flex-col gap-4 px-0 pt-0 pb-2 flex-1\">\n <!-- Release Content -->\n <div\n class=\"content-text max-h-96 overflow-y-auto\"\n v-html=\"formattedContent\"\n ></div>\n </div>\n\n <!-- Modal Footer -->\n <div\n class=\"modal-footer flex justify-between items-center gap-4 px-4 pb-4\"\n >\n <a\n class=\"learn-more-link flex items-center gap-2 text-sm font-normal py-1\"\n :href=\"changelogUrl\"\n target=\"_blank\"\n rel=\"noopener noreferrer\"\n @click=\"closePopup\"\n >\n <i class=\"icon-[lucide--external-link]\"></i>\n {{ $t('whatsNewPopup.learnMore') }}\n </a>\n <div class=\"footer-actions flex items-center gap-4\">\n <Button\n class=\"h-8\"\n size=\"sm\"\n variant=\"muted-textonly\"\n @click=\"closePopup\"\n >\n {{ $t('whatsNewPopup.later') }}\n </Button>\n </div>\n </div>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { default as DOMPurify } from 'dompurify'\nimport { computed, onMounted, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { useExternalLink } from '@/composables/useExternalLink'\nimport { formatVersionAnchor } from '@/utils/formatUtil'\nimport { renderMarkdownToHtml } from '@/utils/markdownRendererUtil'\n\nimport type { ReleaseNote } from '../common/releaseService'\nimport { useReleaseStore } from '../common/releaseStore'\n\nconst { buildDocsUrl } = useExternalLink()\nconst releaseStore = useReleaseStore()\nconst { t } = useI18n()\n\n// Define emits\nconst emit = defineEmits<{\n 'whats-new-dismissed': []\n}>()\n\n// Local state for dismissed status\nconst isDismissed = ref(false)\n\n// Get latest release from store\nconst latestRelease = computed<ReleaseNote | null>(() => {\n return releaseStore.recentRelease\n})\n\n// Show popup when on latest version and not dismissed\nconst shouldShow = computed(\n () => releaseStore.shouldShowPopup && !isDismissed.value\n)\n\n// Generate changelog URL with version anchor (language-aware)\nconst changelogUrl = computed(() => {\n const changelogBaseUrl = buildDocsUrl('/changelog', { includeLocale: true })\n if (latestRelease.value?.version) {\n const versionAnchor = formatVersionAnchor(latestRelease.value.version)\n return `${changelogBaseUrl}#${versionAnchor}`\n }\n return changelogBaseUrl\n})\n\nconst formattedContent = computed(() => {\n if (!latestRelease.value?.content) {\n return DOMPurify.sanitize(`<p>${t('whatsNewPopup.noReleaseNotes')}</p>`)\n }\n\n try {\n const markdown = latestRelease.value.content\n\n // Check if content is meaningful (not just whitespace)\n const trimmedContent = markdown.trim()\n if (!trimmedContent || trimmedContent.replace(/\\s+/g, '') === '') {\n return DOMPurify.sanitize(`<p>${t('whatsNewPopup.noReleaseNotes')}</p>`)\n }\n\n // Extract image and remaining content separately\n const imageMatch = markdown.match(/!\\[.*?\\]\\(.*?\\)/)\n const image = imageMatch ? imageMatch[0] : ''\n\n // Remove image from content but keep original title\n const contentWithoutImage = markdown.replace(/!\\[.*?\\]\\(.*?\\)/, '').trim()\n\n // Reorder: image first, then original content\n const reorderedContent = [image, contentWithoutImage]\n .filter(Boolean)\n .join('\\n\\n')\n\n // renderMarkdownToHtml already sanitizes with DOMPurify, so this is safe\n return renderMarkdownToHtml(reorderedContent)\n } catch (error) {\n console.error('Error parsing markdown:', error)\n // Fallback to plain text with line breaks - sanitize the HTML we create\n const fallbackContent = latestRelease.value.content.replace(/\\n/g, '<br>')\n return fallbackContent.trim()\n ? DOMPurify.sanitize(fallbackContent)\n : DOMPurify.sanitize(`<p>${t('whatsNewPopup.noReleaseNotes')}</p>`)\n }\n})\n\nconst show = () => {\n isDismissed.value = false\n}\n\nconst hide = () => {\n isDismissed.value = true\n emit('whats-new-dismissed')\n}\n\nconst closePopup = async () => {\n // Mark \"what's new\" seen when popup is closed\n if (latestRelease.value) {\n await releaseStore.handleWhatsNewSeen(latestRelease.value.version)\n }\n hide()\n}\n\n// Initialize on mount\nonMounted(async () => {\n // Fetch releases if not already loaded\n if (!releaseStore.releases.length) {\n await releaseStore.fetchReleases()\n }\n})\n\n// Expose methods for parent component and tests\ndefineExpose({\n show,\n hide,\n closePopup\n})\n</script>\n\n<style scoped>\n/* Popup container - positioning handled by parent */\n.whats-new-popup-container {\n --whats-new-popup-bottom: 1rem;\n\n position: absolute;\n bottom: var(--whats-new-popup-bottom);\n z-index: 1000;\n pointer-events: auto;\n}\n\n.whats-new-popup {\n background: var(--interface-menu-surface);\n border-radius: 8px;\n max-width: 400px;\n width: 400px;\n border: 1px solid var(--interface-menu-stroke);\n box-shadow: 1px 1px 8px 0 rgb(0 0 0 / 0.2);\n position: relative;\n display: flex;\n flex-direction: column;\n}\n\n/* Modal Body */\n.modal-body {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n padding: 0;\n flex: 1;\n}\n\n.modal-header {\n display: flex;\n flex-direction: column;\n gap: 8px;\n}\n\n.content-text {\n color: var(--text-primary);\n font-size: 14px;\n line-height: 1.5;\n word-wrap: break-word;\n padding: 0 1rem;\n}\n\n/* Style the markdown content */\n/* Title */\n.content-text :deep(*) {\n box-sizing: border-box;\n}\n\n.content-text :deep(h1) {\n color: var(--text-secondary);\n font-family: Inter, sans-serif;\n font-size: 14px;\n font-weight: 400;\n margin-top: 1rem;\n margin-bottom: 8px;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n}\n\n/* What's new title - targets h2 or strong text after h1 */\n.content-text :deep(h2),\n.content-text :deep(h1 + p strong) {\n color: var(--text-primary);\n font-family: Inter, sans-serif;\n font-size: 14px;\n font-weight: 600;\n margin: 0 0 8px;\n line-height: 1.429;\n}\n\n/* Regular paragraphs - short description */\n.content-text :deep(p) {\n color: var(--text-secondary);\n font-family: Inter, sans-serif;\n margin: 1rem 0;\n}\n\n/* List */\n.content-text :deep(ul),\n.content-text :deep(ol) {\n margin-bottom: 0;\n padding-left: 0;\n list-style: none;\n}\n\n.content-text :deep(ul:first-child),\n.content-text :deep(ol:first-child) {\n margin-top: 0;\n}\n\n.content-text :deep(ul:last-child),\n.content-text :deep(ol:last-child) {\n margin-bottom: 0;\n}\n\n.content-text :deep(li) {\n margin-bottom: 6px;\n position: relative;\n padding-left: 18px;\n color: var(--text-secondary);\n font-family: Inter, sans-serif;\n font-size: 14px;\n font-weight: 400;\n line-height: 1.2102;\n}\n\n.content-text :deep(li:last-child) {\n margin-bottom: 0;\n}\n\n.content-text :deep(li::before) {\n content: '';\n position: absolute;\n left: 4px;\n top: 7px;\n width: 6px;\n height: 6px;\n border: 2px solid var(--text-secondary);\n border-radius: 50%;\n background: transparent;\n}\n\n.content-text :deep(li strong) {\n color: var(--text-secondary);\n font-family: Inter, sans-serif;\n font-size: 14px;\n font-weight: 400;\n line-height: 1.2102;\n margin-right: 4px;\n}\n\n.content-text :deep(li p) {\n margin: 2px 0 0;\n display: inline;\n}\n\n/* Code styling */\n.content-text :deep(code) {\n background-color: var(--input-surface);\n border: 1px solid var(--interface-menu-stroke);\n border-radius: 4px;\n padding: 2px 6px;\n color: var(--text-primary);\n white-space: nowrap;\n}\n\n.content-text :deep(img) {\n width: 100%;\n height: 200px;\n margin: 0 0 16px;\n object-fit: cover;\n display: block;\n border-radius: 8px;\n}\n\n.content-text :deep(img:first-child) {\n margin: -1rem -1rem 16px;\n width: calc(100% + 2rem);\n border-top-left-radius: 8px;\n border-top-right-radius: 8px;\n border-bottom-left-radius: 0;\n border-bottom-right-radius: 0;\n}\n\n/* Add border to content when image is present */\n.content-text:has(img:first-child) {\n border-left: 1px solid var(--interface-menu-stroke);\n border-right: 1px solid var(--interface-menu-stroke);\n border-top: 1px solid var(--interface-menu-stroke);\n border-bottom-left-radius: 8px;\n border-bottom-right-radius: 8px;\n margin: -1px;\n margin-bottom: 0;\n}\n\n.content-text :deep(img + h1) {\n margin-top: 0;\n}\n\n/* Secondary headings */\n.content-text :deep(h3) {\n color: var(--text-primary);\n font-family: Inter, sans-serif;\n font-size: 16px;\n font-weight: 600;\n margin: 16px 0 8px;\n line-height: 1.4;\n}\n\n/* Modal Footer */\n.modal-footer {\n display: flex;\n justify-content: space-between;\n align-items: center;\n gap: 16px;\n padding: 16px;\n border-top: none;\n}\n\n.footer-actions {\n display: flex;\n align-items: center;\n gap: 16px;\n}\n\n.learn-more-link {\n display: flex;\n align-items: center;\n gap: 8px;\n color: var(--text-secondary);\n font-size: 14px;\n font-weight: 400;\n line-height: 1.2102;\n text-decoration: none;\n padding: 4px 0;\n}\n\n.learn-more-link:hover {\n color: var(--text-primary);\n}\n\n.learn-more-link i {\n width: 16px;\n height: 16px;\n}\n\n.action-secondary {\n height: 32px;\n padding: 4px 0;\n background: transparent;\n border: none;\n color: var(--text-secondary);\n font-size: 14px;\n font-weight: 400;\n line-height: 1.2102;\n cursor: pointer;\n border-radius: 4px;\n}\n\n.action-secondary:hover {\n color: var(--text-primary);\n}\n\n.action-primary {\n height: 40px;\n padding: 8px 16px;\n background: var(--interface-menu-component-surface-hovered);\n border-radius: 8px;\n border: none;\n color: var(--text-primary);\n font-size: 14px;\n font-weight: 400;\n line-height: 1.2102;\n cursor: pointer;\n}\n\n.action-primary:hover {\n background: var(--button-hover-surface);\n}\n</style>\n","<template>\n <svg\n xmlns=\"http://www.w3.org/2000/svg\"\n :width=\"size\"\n :height=\"size\"\n viewBox=\"0 0 16 16\"\n fill=\"none\"\n :class=\"iconClass\"\n >\n <g clip-path=\"url(#clip0_1099_16244)\">\n <path\n d=\"M4.99992 3.00016C4.99992 2.07969 5.74611 1.3335 6.66658 1.3335C7.58706 1.3335 8.33325 2.07969 8.33325 3.00016V4.00016H8.99992C9.9318 4.00016 10.3977 4.00016 10.7653 4.1524C11.2553 4.35539 11.6447 4.74474 11.8477 5.2348C11.9999 5.60234 11.9999 6.06828 11.9999 7.00016H12.9999C13.9204 7.00016 14.6666 7.74635 14.6666 8.66683C14.6666 9.5873 13.9204 10.3335 12.9999 10.3335H11.9999V11.4668C11.9999 12.5869 11.9999 13.147 11.7819 13.5748C11.5902 13.9511 11.2842 14.2571 10.9079 14.4488C10.4801 14.6668 9.92002 14.6668 8.79992 14.6668H8.33325V13.5002C8.33325 12.6717 7.66168 12.0002 6.83325 12.0002C6.00482 12.0002 5.33325 12.6717 5.33325 13.5002V14.6668H4.53325C3.41315 14.6668 2.85309 14.6668 2.42527 14.4488C2.04895 14.2571 1.74299 13.9511 1.55124 13.5748C1.33325 13.147 1.33325 12.5869 1.33325 11.4668V10.3335H2.33325C3.25373 10.3335 3.99992 9.5873 3.99992 8.66683C3.99992 7.74635 3.25373 7.00016 2.33325 7.00016H1.33325C1.33325 6.06828 1.33325 5.60234 1.48549 5.2348C1.68848 4.74474 2.07783 4.35539 2.56789 4.1524C2.93543 4.00016 3.40137 4.00016 4.33325 4.00016H4.99992V3.00016Z\"\n :stroke=\"color\"\n stroke-width=\"1.2\"\n stroke-linecap=\"round\"\n stroke-linejoin=\"round\"\n />\n </g>\n <defs>\n <clipPath id=\"clip0_1099_16244\">\n <rect width=\"16\" height=\"16\" fill=\"white\" />\n </clipPath>\n </defs>\n </svg>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\ninterface Props {\n size?: number | string\n color?: string\n class?: string\n}\nconst {\n size = 16,\n color = 'currentColor',\n class: className\n} = defineProps<Props>()\nconst iconClass = computed(() => className || '')\n</script>\n","<template>\n <div\n class=\"help-center-menu flex flex-col items-start gap-1\"\n role=\"menu\"\n :aria-label=\"$t('help.helpCenterMenu')\"\n >\n <!-- Main Menu Items -->\n <div class=\"w-full\">\n <nav class=\"flex w-full flex-col gap-2\" role=\"menubar\">\n <button\n v-for=\"menuItem in menuItems\"\n v-show=\"menuItem.visible !== false\"\n :key=\"menuItem.key\"\n type=\"button\"\n class=\"help-menu-item\"\n :class=\"{ 'more-item': menuItem.key === 'more' }\"\n role=\"menuitem\"\n @click=\"menuItem.action\"\n @mouseenter=\"onMenuItemHover(menuItem.key, $event)\"\n @mouseleave=\"onMenuItemLeave(menuItem.key)\"\n >\n <div class=\"help-menu-icon-container\">\n <div class=\"help-menu-icon\">\n <component\n :is=\"menuItem.icon\"\n v-if=\"typeof menuItem.icon === 'object'\"\n :size=\"16\"\n />\n <i v-else :class=\"menuItem.icon\" />\n </div>\n <div v-if=\"menuItem.showRedDot\" class=\"menu-red-dot\" />\n </div>\n <span class=\"menu-label\">{{ menuItem.label }}</span>\n <i\n v-if=\"menuItem.showExternalIcon\"\n class=\"icon-[lucide--external-link] text-primary w-4 h-4 ml-auto\"\n />\n <i\n v-if=\"menuItem.key === 'more'\"\n class=\"pi pi-chevron-right ml-auto\"\n />\n </button>\n </nav>\n <div\n class=\"flex h-4 flex-col items-center justify-between self-stretch p-2\"\n >\n <div class=\"w-full border-b border-interface-menu-stroke\" />\n </div>\n </div>\n\n <!-- More Submenu -->\n <Teleport to=\"body\">\n <div\n v-if=\"isSubmenuVisible\"\n ref=\"submenuRef\"\n class=\"more-submenu\"\n :style=\"submenuStyle\"\n @mouseenter=\"onSubmenuHover\"\n @mouseleave=\"onSubmenuLeave\"\n >\n <template\n v-for=\"submenuItem in moreMenuItem?.items\"\n :key=\"submenuItem.key\"\n >\n <div\n v-if=\"submenuItem.type === 'divider'\"\n v-show=\"submenuItem.visible !== false\"\n class=\"submenu-divider\"\n />\n <button\n v-else\n v-show=\"submenuItem.visible !== false\"\n type=\"button\"\n class=\"help-menu-item submenu-item\"\n role=\"menuitem\"\n @click=\"submenuItem.action\"\n >\n <span class=\"menu-label\">{{ submenuItem.label }}</span>\n </button>\n </template>\n </div>\n </Teleport>\n\n <!-- What's New Section -->\n <section\n v-if=\"showVersionUpdates\"\n class=\"w-full\"\n data-testid=\"whats-new-section\"\n >\n <h3\n class=\"section-description flex items-center gap-2.5 self-stretch px-8 pt-2 pb-2\"\n >\n {{ $t('helpCenter.whatsNew') }}\n </h3>\n\n <!-- Release Items -->\n <div\n v-if=\"hasReleases\"\n role=\"group\"\n :aria-label=\"$t('help.recentReleases')\"\n >\n <article\n v-for=\"release in releaseStore.recentReleases\"\n :key=\"release.id || release.version\"\n class=\"release-menu-item flex h-12 min-h-6 cursor-pointer items-center gap-2 self-stretch rounded p-2 transition-colors hover:bg-interface-menu-component-surface-hovered\"\n role=\"button\"\n tabindex=\"0\"\n @click=\"onReleaseClick(release)\"\n @keydown.enter=\"onReleaseClick(release)\"\n @keydown.space.prevent=\"onReleaseClick(release)\"\n >\n <i class=\"help-menu-icon icon-[lucide--package]\" aria-hidden=\"true\" />\n <div class=\"release-content\">\n <span class=\"release-title\">\n {{\n $t('g.releaseTitle', {\n package: 'Comfy',\n version: release.version\n })\n }}\n </span>\n <time class=\"release-date\" :datetime=\"release.published_at\">\n <span class=\"normal-state\">\n {{ formatReleaseDate(release.published_at) }}\n </span>\n <span class=\"hover-state\">\n {{ $t('helpCenter.clickToLearnMore') }}\n </span>\n </time>\n </div>\n </article>\n </div>\n\n <!-- Loading State -->\n <div\n v-else-if=\"releaseStore.isLoading\"\n class=\"help-menu-item\"\n role=\"status\"\n aria-live=\"polite\"\n >\n <i class=\"pi pi-spin pi-spinner help-menu-icon\" aria-hidden=\"true\" />\n <span>{{ $t('helpCenter.loadingReleases') }}</span>\n </div>\n\n <!-- No Releases State -->\n <div v-else class=\"help-menu-item\" role=\"status\">\n <i class=\"pi pi-info-circle help-menu-icon\" aria-hidden=\"true\" />\n <span>{{ $t('helpCenter.noRecentReleases') }}</span>\n </div>\n </section>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useToast } from 'primevue/usetoast'\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'\nimport type { CSSProperties, Component } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport PuzzleIcon from '@/components/icons/PuzzleIcon.vue'\nimport { useExternalLink } from '@/composables/useExternalLink'\nimport { isCloud } from '@/platform/distribution/types'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useTelemetry } from '@/platform/telemetry'\nimport type { ReleaseNote } from '@/platform/updates/common/releaseService'\nimport { useReleaseStore } from '@/platform/updates/common/releaseStore'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { electronAPI, isElectron } from '@/utils/envUtil'\nimport { formatVersionAnchor } from '@/utils/formatUtil'\nimport { useConflictAcknowledgment } from '@/workbench/extensions/manager/composables/useConflictAcknowledgment'\nimport { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'\nimport { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'\nimport { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'\n\n// Types\ninterface MenuItem {\n key: string\n icon?: string | Component\n label?: string\n action?: () => void\n visible?: boolean\n type?: 'item' | 'divider'\n items?: MenuItem[]\n showRedDot?: boolean\n showExternalIcon?: boolean\n}\n\n// Constants\nconst TIME_UNITS = {\n MINUTE: 60 * 1000,\n HOUR: 60 * 60 * 1000,\n DAY: 24 * 60 * 60 * 1000,\n WEEK: 7 * 24 * 60 * 60 * 1000,\n MONTH: 30 * 24 * 60 * 60 * 1000,\n YEAR: 365 * 24 * 60 * 60 * 1000\n} as const\n\nconst SUBMENU_CONFIG = {\n DELAY_MS: 100,\n OFFSET_PX: 8,\n Z_INDEX: 10001\n} as const\n\n// Composables\nconst { t } = useI18n()\nconst toast = useToast()\nconst { staticUrls, buildDocsUrl } = useExternalLink()\nconst releaseStore = useReleaseStore()\nconst commandStore = useCommandStore()\nconst settingStore = useSettingStore()\nconst telemetry = useTelemetry()\n\n// Track when help center was opened\nconst openedAt = ref(Date.now())\n\n// Emits\nconst emit = defineEmits<{\n close: []\n}>()\n\n// State\nconst isSubmenuVisible = ref(false)\nconst submenuRef = ref<HTMLElement | null>(null)\nconst submenuStyle = ref<CSSProperties>({})\nlet hoverTimeout: number | null = null\n\n// Computed\nconst hasReleases = computed(() => releaseStore.releases.length > 0)\nconst showVersionUpdates = computed(() =>\n settingStore.get('Comfy.Notification.ShowVersionUpdates')\n)\n\n// Use conflict acknowledgment state from composable\nconst { shouldShowRedDot: shouldShowManagerRedDot } =\n useConflictAcknowledgment()\nconst { isNewManagerUI } = useManagerState()\n\nconst moreItems = computed<MenuItem[]>(() => {\n const allMoreItems: MenuItem[] = [\n {\n key: 'desktop-guide',\n type: 'item',\n label: t('helpCenter.desktopUserGuide'),\n visible: isElectron(),\n action: () => {\n trackResourceClick('docs', true)\n openExternalLink(\n buildDocsUrl('/installation/desktop', {\n includeLocale: true,\n platform: true\n })\n )\n emit('close')\n }\n },\n {\n key: 'dev-tools',\n type: 'item',\n label: t('helpCenter.openDevTools'),\n visible: isElectron(),\n action: () => {\n openDevTools()\n emit('close')\n }\n },\n {\n key: 'divider-1',\n type: 'divider',\n visible: isElectron()\n },\n {\n key: 'reinstall',\n type: 'item',\n label: t('helpCenter.reinstall'),\n visible: isElectron(),\n action: () => {\n onReinstall()\n emit('close')\n }\n }\n ]\n\n // Filter for visible items only\n return allMoreItems.filter((item) => item.visible !== false)\n})\n\nconst hasVisibleMoreItems = computed(() => {\n return !!moreItems.value.length\n})\n\nconst moreMenuItem = computed(() =>\n menuItems.value.find((item) => item.key === 'more')\n)\n\nconst menuItems = computed<MenuItem[]>(() => {\n const items: MenuItem[] = [\n {\n key: 'feedback',\n type: 'item',\n icon: 'icon-[lucide--clipboard-pen]',\n label: t('helpCenter.feedback'),\n action: () => {\n trackResourceClick('help_feedback', false)\n void commandStore.execute('Comfy.ContactSupport')\n emit('close')\n }\n },\n {\n key: 'help',\n type: 'item',\n icon: 'icon-[lucide--message-circle-question]',\n label: t('helpCenter.help'),\n action: () => {\n trackResourceClick('help_feedback', false)\n void commandStore.execute('Comfy.ContactSupport')\n emit('close')\n }\n },\n {\n key: 'docs',\n type: 'item',\n icon: 'icon-[lucide--book-open]',\n label: t('helpCenter.docs'),\n showExternalIcon: true,\n action: () => {\n trackResourceClick('docs', true)\n const path = isCloud ? '/get_started/cloud' : '/'\n openExternalLink(buildDocsUrl(path, { includeLocale: true }))\n emit('close')\n }\n },\n {\n key: 'discord',\n type: 'item',\n icon: 'pi pi-discord',\n label: 'Discord',\n showExternalIcon: true,\n action: () => {\n trackResourceClick('discord', true)\n openExternalLink(staticUrls.discord)\n emit('close')\n }\n },\n {\n key: 'github',\n type: 'item',\n icon: 'icon-[lucide--github]',\n label: t('helpCenter.github'),\n showExternalIcon: true,\n action: () => {\n trackResourceClick('github', true)\n openExternalLink(staticUrls.github)\n emit('close')\n }\n }\n ]\n\n // Extension manager - only in non-cloud distributions\n if (!isCloud) {\n items.push({\n key: 'manager',\n type: 'item',\n icon: PuzzleIcon,\n label: t('helpCenter.managerExtension'),\n showRedDot: shouldShowManagerRedDot.value,\n action: async () => {\n trackResourceClick('manager', false)\n await useManagerState().openManager({\n initialTab: ManagerTab.All,\n showToastOnLegacyError: false\n })\n emit('close')\n }\n })\n }\n // Update ComfyUI - only for non-desktop, non-cloud with new manager UI\n if (!isElectron() && !isCloud && isNewManagerUI.value) {\n items.push({\n key: 'update-comfyui',\n type: 'item',\n icon: 'icon-[lucide--download]',\n label: t('helpCenter.updateComfyUI'),\n action: () => {\n onUpdateComfyUI()\n emit('close')\n }\n })\n }\n\n items.push({\n key: 'more',\n type: 'item',\n icon: '',\n label: t('helpCenter.more'),\n visible: hasVisibleMoreItems.value,\n action: () => {}, // No action for more item\n items: moreItems.value\n })\n\n return items\n})\n\n// Utility Functions\nconst trackResourceClick = (\n resourceType:\n | 'docs'\n | 'discord'\n | 'github'\n | 'help_feedback'\n | 'manager'\n | 'release_notes',\n isExternal: boolean\n): void => {\n telemetry?.trackHelpResourceClicked({\n resource_type: resourceType,\n is_external: isExternal,\n source: 'help_center'\n })\n}\n\nconst openExternalLink = (url: string): void => {\n window.open(url, '_blank', 'noopener,noreferrer')\n}\n\nconst clearHoverTimeout = (): void => {\n if (hoverTimeout) {\n clearTimeout(hoverTimeout)\n hoverTimeout = null\n }\n}\n\nconst calculateSubmenuPosition = (button: HTMLElement): CSSProperties => {\n const rect = button.getBoundingClientRect()\n const submenuWidth = 210 // Width defined in CSS\n\n // Get actual submenu height if available, otherwise estimate based on visible item count\n const visibleItemCount =\n moreMenuItem.value?.items?.filter((item) => item.visible !== false)\n .length || 0\n const estimatedHeight = visibleItemCount * 48 + 16 // ~48px per item + padding\n const submenuHeight = submenuRef.value?.offsetHeight || estimatedHeight\n\n // Get viewport dimensions\n const viewportWidth = window.innerWidth\n const viewportHeight = window.innerHeight\n\n // Calculate basic position (aligned with button)\n let top = rect.top\n let left = rect.right + SUBMENU_CONFIG.OFFSET_PX\n\n // Check if submenu would overflow viewport on the right\n if (left + submenuWidth > viewportWidth) {\n // Position submenu to the left of the button instead\n left = rect.left - submenuWidth - SUBMENU_CONFIG.OFFSET_PX\n }\n\n // Check if submenu would overflow viewport at the bottom\n if (top + submenuHeight > viewportHeight) {\n // Position submenu above the button, aligned to bottom\n top = Math.max(\n SUBMENU_CONFIG.OFFSET_PX, // Minimum distance from top of viewport\n rect.bottom - submenuHeight\n )\n }\n\n // Ensure submenu doesn't go above viewport\n if (top < SUBMENU_CONFIG.OFFSET_PX) {\n top = SUBMENU_CONFIG.OFFSET_PX\n }\n\n top -= 8\n\n return {\n position: 'fixed',\n top: `${top}px`,\n left: `${left}px`,\n zIndex: SUBMENU_CONFIG.Z_INDEX\n }\n}\n\nconst formatReleaseDate = (dateString?: string): string => {\n if (!dateString) return 'date'\n\n const date = new Date(dateString)\n const now = new Date()\n const diffTime = Math.abs(now.getTime() - date.getTime())\n\n const timeUnits = [\n { unit: TIME_UNITS.YEAR, key: 'yearsAgo' },\n { unit: TIME_UNITS.MONTH, key: 'monthsAgo' },\n { unit: TIME_UNITS.WEEK, key: 'weeksAgo' },\n { unit: TIME_UNITS.DAY, key: 'daysAgo' },\n { unit: TIME_UNITS.HOUR, key: 'hoursAgo' },\n { unit: TIME_UNITS.MINUTE, key: 'minutesAgo' }\n ]\n\n for (const { unit, key } of timeUnits) {\n const value = Math.floor(diffTime / unit)\n if (value > 0) {\n return t(`g.relativeTime.${key}`, { count: value })\n }\n }\n\n return t('g.relativeTime.now')\n}\n\n// Event Handlers\nconst onMenuItemHover = async (\n key: string,\n event: MouseEvent\n): Promise<void> => {\n if (key !== 'more' || !moreMenuItem.value?.items) return\n\n // Don't show submenu if all items are hidden\n const hasVisibleItems = moreMenuItem.value.items.some(\n (item) => item.visible !== false\n )\n if (!hasVisibleItems) return\n\n clearHoverTimeout()\n\n const moreButton = event.currentTarget as HTMLElement\n\n // Calculate initial position before showing submenu\n submenuStyle.value = calculateSubmenuPosition(moreButton)\n\n // Show submenu with correct position\n isSubmenuVisible.value = true\n\n // After submenu is rendered, refine position if needed\n await nextTick()\n if (submenuRef.value) {\n submenuStyle.value = calculateSubmenuPosition(moreButton)\n }\n}\n\nconst onMenuItemLeave = (key: string): void => {\n if (key !== 'more') return\n\n hoverTimeout = window.setTimeout(() => {\n isSubmenuVisible.value = false\n }, SUBMENU_CONFIG.DELAY_MS)\n}\n\nconst onSubmenuHover = (): void => {\n clearHoverTimeout()\n}\n\nconst onSubmenuLeave = (): void => {\n isSubmenuVisible.value = false\n}\n\nconst openDevTools = (): void => {\n if (isElectron()) {\n electronAPI().openDevTools()\n }\n}\n\nconst onReinstall = (): void => {\n if (isElectron()) {\n void electronAPI().reinstall()\n }\n}\n\nconst onUpdateComfyUI = async (): Promise<void> => {\n const { updateComfyUI, rebootComfyUI, error } = useComfyManagerService()\n\n toast.add({\n severity: 'info',\n summary: t('helpCenter.updateComfyUIStarted'),\n detail: t('helpCenter.updateComfyUIStartedDetail'),\n life: 3000\n })\n\n try {\n const result = await updateComfyUI({ is_stable: true })\n\n if (result === null || error.value) {\n toast.add({\n severity: 'error',\n summary: t('g.error'),\n detail: error.value || t('helpCenter.updateComfyUIFailed'),\n life: 5000\n })\n return\n }\n\n toast.add({\n severity: 'success',\n summary: t('helpCenter.updateComfyUISuccess'),\n detail: t('helpCenter.updateComfyUISuccessDetail'),\n life: 3000\n })\n\n await rebootComfyUI()\n } catch (err) {\n toast.add({\n severity: 'error',\n summary: t('g.error'),\n detail: err instanceof Error ? err.message : t('g.unknownError'),\n life: 5000\n })\n }\n}\n\nconst onReleaseClick = (release: ReleaseNote): void => {\n trackResourceClick('release_notes', true)\n void releaseStore.handleShowChangelog(release.version)\n const versionAnchor = formatVersionAnchor(release.version)\n const changelogUrl = `${buildDocsUrl('/changelog', { includeLocale: true })}#${versionAnchor}`\n openExternalLink(changelogUrl)\n emit('close')\n}\n\n// Lifecycle\nonMounted(async () => {\n telemetry?.trackHelpCenterOpened({ source: 'sidebar' })\n if (!hasReleases.value) {\n await releaseStore.fetchReleases()\n }\n})\n\nonBeforeUnmount(() => {\n const timeSpentSeconds = Math.round((Date.now() - openedAt.value) / 1000)\n telemetry?.trackHelpCenterClosed({ time_spent_seconds: timeSpentSeconds })\n})\n</script>\n\n<style scoped>\n.help-center-menu {\n width: 256px;\n max-height: 500px;\n overflow-y: auto;\n background: var(--interface-menu-surface);\n border-radius: 8px;\n box-shadow: 0 2px 12px 0 rgb(0 0 0 / 0.1);\n border: 1px solid var(--interface-menu-stroke);\n padding: 12px 8px;\n position: relative;\n}\n\n.help-menu-item {\n display: flex;\n align-items: center;\n width: 100%;\n height: 32px;\n min-height: 24px;\n padding: 8px;\n gap: 8px;\n background: transparent;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n transition: background-color 0.2s;\n font-size: 0.9rem;\n color: var(--text-primary);\n text-align: left;\n}\n\n.help-menu-item:hover {\n background-color: var(--interface-menu-component-surface-hovered);\n}\n\n.help-menu-item:focus,\n.help-menu-item:focus-visible {\n outline: none;\n box-shadow: none;\n}\n\n.help-menu-icon-container {\n position: relative;\n width: 16px;\n height: 16px;\n flex-shrink: 0;\n}\n\n.help-menu-icon {\n width: 16px;\n height: 16px;\n font-size: 16px;\n color: var(--text-primary);\n display: flex;\n justify-content: center;\n align-items: center;\n flex-shrink: 0;\n}\n\n.help-menu-icon svg {\n width: 16px;\n height: 16px;\n color: var(--text-primary);\n}\n\n.menu-red-dot {\n position: absolute;\n top: -2px;\n right: -2px;\n width: 8px;\n height: 8px;\n background: #ff3b30;\n border-radius: 50%;\n border: 1.5px solid var(--p-content-background);\n z-index: 1;\n}\n\n.menu-label {\n flex: 1;\n}\n\n.more-item {\n justify-content: space-between;\n}\n\n.section-description {\n color: var(--text-secondary);\n font-family: var(--font-inter);\n font-size: 12px;\n font-style: normal;\n font-weight: 700;\n line-height: normal;\n margin: 0;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n}\n\n.release-menu-item {\n position: relative;\n}\n\n.release-content {\n flex: 1;\n display: flex;\n flex-direction: column;\n gap: 4px;\n min-width: 0;\n}\n\n.release-title {\n font-size: 0.9rem;\n line-height: 1.2;\n font-weight: 500;\n color: var(--text-primary);\n}\n\n.release-date {\n height: 16px;\n color: var(--text-secondary);\n font-family: var(--font-inter);\n font-size: 12px;\n font-style: normal;\n font-weight: 400;\n line-height: normal;\n overflow: hidden;\n text-overflow: ellipsis;\n display: -webkit-box;\n -webkit-line-clamp: 1;\n -webkit-box-orient: vertical;\n}\n\n.release-date .hover-state {\n display: none;\n}\n\n.release-menu-item:hover .release-date .normal-state,\n.release-menu-item:focus-within .release-date .normal-state {\n display: none;\n}\n\n.release-menu-item:hover .release-date .hover-state,\n.release-menu-item:focus-within .release-date .hover-state {\n display: inline;\n}\n\n/* Submenu Styles */\n.more-submenu {\n width: 210px;\n padding: 12px 8px;\n background: var(--interface-menu-surface);\n border-radius: 8px;\n border: 1px solid var(--interface-menu-stroke);\n box-shadow: 0 2px 12px 0 rgb(0 0 0 / 0.1);\n overflow: hidden;\n transition: opacity 0.15s ease-out;\n}\n\n.submenu-item {\n padding: 8px;\n height: 32px;\n min-height: 24px;\n border-radius: 4px;\n color: var(--text-primary);\n font-size: 0.9rem;\n font-weight: inherit;\n line-height: inherit;\n}\n\n.submenu-item:hover {\n background-color: var(--interface-menu-component-surface-hovered);\n}\n\n.submenu-item:focus,\n.submenu-item:focus-visible {\n outline: none;\n box-shadow: none;\n}\n\n.submenu-divider {\n height: 1px;\n background: var(--interface-menu-stroke);\n margin: 4px 0;\n}\n\n/* Scrollbar Styling */\n.help-center-menu::-webkit-scrollbar {\n width: 6px;\n}\n\n.help-center-menu::-webkit-scrollbar-track {\n background: transparent;\n}\n\n.help-center-menu::-webkit-scrollbar-thumb {\n background: var(--interface-menu-stroke);\n border-radius: 3px;\n}\n\n.help-center-menu::-webkit-scrollbar-thumb:hover {\n background: var(--text-secondary);\n}\n\n/* Reduced Motion */\n@media (prefers-reduced-motion: reduce) {\n .help-menu-item {\n transition: none;\n }\n}\n</style>\n","<template>\n <!-- Help Center Popup positioned within canvas area -->\n <Teleport to=\"#graph-canvas-container\">\n <div\n v-if=\"isHelpCenterVisible\"\n class=\"help-center-popup\"\n :class=\"{\n 'sidebar-left':\n triggerLocation === 'sidebar' && sidebarLocation === 'left',\n 'sidebar-right':\n triggerLocation === 'sidebar' && sidebarLocation === 'right',\n 'topbar-right': triggerLocation === 'topbar',\n 'small-sidebar': isSmall\n }\"\n >\n <HelpCenterMenuContent @close=\"closeHelpCenter\" />\n </div>\n </Teleport>\n\n <!-- Release Notification Toast positioned within canvas area -->\n <Teleport to=\"#graph-canvas-container\">\n <ReleaseNotificationToast\n :class=\"{\n 'sidebar-left': sidebarLocation === 'left',\n 'sidebar-right': sidebarLocation === 'right',\n 'small-sidebar': isSmall\n }\"\n />\n </Teleport>\n\n <!-- WhatsNew Popup positioned within canvas area -->\n <Teleport to=\"#graph-canvas-container\">\n <WhatsNewPopup\n :class=\"{\n 'sidebar-left': sidebarLocation === 'left',\n 'sidebar-right': sidebarLocation === 'right',\n 'small-sidebar': isSmall\n }\"\n @whats-new-dismissed=\"handleWhatsNewDismissed\"\n />\n </Teleport>\n\n <!-- Backdrop to close popup when clicking outside -->\n <Teleport to=\"body\">\n <div\n v-if=\"isHelpCenterVisible\"\n class=\"help-center-backdrop\"\n @click=\"closeHelpCenter\"\n />\n </Teleport>\n</template>\n\n<script setup lang=\"ts\">\nimport { useHelpCenter } from '@/composables/useHelpCenter'\nimport ReleaseNotificationToast from '@/platform/updates/components/ReleaseNotificationToast.vue'\nimport WhatsNewPopup from '@/platform/updates/components/WhatsNewPopup.vue'\n\nimport HelpCenterMenuContent from './HelpCenterMenuContent.vue'\n\nconst { isSmall = false } = defineProps<{\n isSmall?: boolean\n}>()\n\nconst {\n isHelpCenterVisible,\n triggerLocation,\n sidebarLocation,\n closeHelpCenter,\n handleWhatsNewDismissed\n} = useHelpCenter()\n</script>\n\n<style scoped>\n.help-center-backdrop {\n position: absolute;\n top: 0;\n left: 0;\n right: 0;\n bottom: 0;\n z-index: 9999;\n background: transparent;\n}\n\n.help-center-popup {\n position: absolute;\n bottom: 1rem;\n z-index: 10000;\n animation: slideInUp 0.2s ease-out;\n pointer-events: auto;\n}\n\n.help-center-popup.sidebar-left {\n left: 1rem;\n}\n\n.help-center-popup.sidebar-left.small-sidebar {\n left: 1rem;\n}\n\n.help-center-popup.sidebar-right {\n right: 1rem;\n}\n\n.help-center-popup.topbar-right {\n top: 2rem;\n right: 1rem;\n bottom: auto;\n animation: slideInDown 0.2s ease-out;\n}\n\n@keyframes slideInDown {\n from {\n opacity: 0;\n transform: translateY(-20px);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n\n@keyframes slideInUp {\n from {\n opacity: 0;\n transform: translateY(20px);\n }\n\n to {\n opacity: 1;\n transform: translateY(0);\n }\n}\n</style>\n","<template>\n <svg\n :class=\"iconClass\"\n :width=\"size\"\n :height=\"size\"\n viewBox=\"0 0 18 18\"\n xmlns=\"http://www.w3.org/2000/svg\"\n >\n <path\n d=\"M14.8193 0.600586C15.1248 0.600586 15.3296 0.70893 15.459 0.881836C15.5914 1.05888 15.6471 1.33774 15.5527 1.66895L14.8037 4.30176C14.7063 4.64386 14.4729 4.97024 14.1641 5.21191C13.8544 5.45415 13.496 5.58984 13.1699 5.58984H13.1689L9.5791 5.59668H7.90625C7.52654 5.59668 7.19496 5.84986 7.09082 6.21289L5.69434 11.0889C5.63007 11.3133 5.66134 11.5534 5.77734 11.7529L5.83203 11.8359C5.99177 12.0491 6.24252 12.1758 6.50977 12.1758H6.51074L8.88281 12.1709H11.4971C11.7643 12.171 11.9541 12.254 12.084 12.3906L12.1357 12.4521C12.2685 12.6295 12.3249 12.9089 12.2305 13.2402L11.4805 15.8721C11.383 16.2144 11.1498 16.5415 10.8408 16.7832C10.5314 17.0252 10.1736 17.161 9.84766 17.1611H9.84668L6.25684 17.168H3.64258C3.33762 17.1679 3.13349 17.0588 3.00391 16.8857C2.87135 16.7087 2.81482 16.43 2.90918 16.0986L3.39551 14.3887C3.46841 14.1327 3.41794 13.8576 3.25879 13.6445V13.6436C3.09901 13.4303 2.84745 13.3037 2.58008 13.3037H1.18066C0.875088 13.3037 0.670398 13.1953 0.541016 13.0225C0.408483 12.8451 0.351891 12.5655 0.446289 12.2344L2.11914 6.38965L2.30371 5.74707V5.74609C2.40139 5.40341 2.63456 5.07671 2.94336 4.83496C3.25302 4.59258 3.61143 4.45705 3.9375 4.45703H5.6123C5.94484 4.45703 6.24083 4.26316 6.37891 3.9707L6.42773 3.83984L6.98145 1.89551C7.07894 1.55317 7.31212 1.22614 7.62109 0.984375C7.93074 0.742127 8.2892 0.606445 8.61523 0.606445H8.61621L12.1982 0.600586H14.8193Z\"\n v-bind=\"attributes\"\n />\n </svg>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\ninterface Props {\n size?: number | string\n color?: string\n class?: string\n mode?: 'outline' | 'fill'\n}\nconst {\n size = 16,\n color = 'currentColor',\n mode = 'outline',\n class: className\n} = defineProps<Props>()\nconst iconClass = computed(() => className || '')\nconst attributes = computed(() => ({\n stroke: mode === 'outline' ? color : undefined,\n strokeWidth: mode === 'outline' ? 1 : undefined,\n fill: mode === 'fill' ? color : 'none'\n}))\n</script>\n","<template>\n <div :class=\"containerClasses\">\n <slot></slot>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport { cn } from '@/utils/tailwindUtil'\n\nconst { fullHeight = true } = defineProps<{\n fullHeight?: boolean\n}>()\n\nconst containerClasses = computed(() =>\n cn('flex-1 w-full', fullHeight && 'h-full')\n)\n</script>\n","<template>\n <div :class=\"containerClasses\">\n <slot name=\"top\"></slot>\n <slot name=\"bottom\"></slot>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport { cn } from '@/utils/tailwindUtil'\n\nconst {\n size = 'regular',\n variant = 'default',\n rounded = 'md',\n customAspectRatio,\n hasBorder = true,\n hasBackground = true,\n hasShadow = true,\n hasCursor = true,\n class: customClass = ''\n} = defineProps<{\n size?: 'mini' | 'compact' | 'regular' | 'portrait' | 'tall'\n variant?: 'default' | 'ghost' | 'outline'\n rounded?: 'none' | 'md' | 'lg' | 'xl'\n customAspectRatio?: string\n hasBorder?: boolean\n hasBackground?: boolean\n hasShadow?: boolean\n hasCursor?: boolean\n class?: string\n}>()\n\n// Base structure classes\nconst structureClasses = 'flex flex-col overflow-hidden'\n\n// Rounded corners\nconst roundedClasses = {\n none: 'rounded-none',\n md: 'rounded',\n lg: 'rounded-lg',\n xl: 'rounded-xl'\n} as const\n\nconst containerClasses = computed(() => {\n // Variant styles\n const variantClasses = {\n default: cn(\n hasBackground && 'bg-modal-card-background',\n hasBorder && 'border border-border-default',\n hasShadow && 'shadow-sm',\n hasCursor && 'cursor-pointer'\n ),\n ghost: cn(\n hasCursor && 'cursor-pointer',\n 'p-2 transition-colors duration-200'\n ),\n outline: cn(\n hasBorder && 'border-2 border-border-subtle',\n hasCursor && 'cursor-pointer',\n 'hover:border-border-subtle/50 transition-colors'\n )\n }\n\n // Size/aspect ratio\n const aspectRatio = customAspectRatio\n ? `aspect-[${customAspectRatio}]`\n : {\n mini: 'aspect-100/120',\n compact: 'aspect-240/311',\n regular: 'aspect-256/308',\n portrait: 'aspect-256/325',\n tall: 'aspect-256/353'\n }[size]\n\n return cn(\n structureClasses,\n roundedClasses[rounded],\n variantClasses[variant],\n aspectRatio,\n customClass\n )\n})\n</script>\n","<template>\n <div :class=\"topStyle\">\n <slot class=\"absolute top-0 left-0 h-full w-full\"></slot>\n\n <div v-if=\"slots['top-left']\" :class=\"slotClasses['top-left']\">\n <slot name=\"top-left\"></slot>\n </div>\n\n <div v-if=\"slots['top-right']\" :class=\"slotClasses['top-right']\">\n <slot name=\"top-right\"></slot>\n </div>\n\n <div v-if=\"slots['center-left']\" :class=\"slotClasses['center-left']\">\n <slot name=\"center-left\"></slot>\n </div>\n\n <div v-if=\"slots['center-right']\" :class=\"slotClasses['center-right']\">\n <slot name=\"center-right\"></slot>\n </div>\n\n <div v-if=\"slots['bottom-left']\" :class=\"slotClasses['bottom-left']\">\n <slot name=\"bottom-left\"></slot>\n </div>\n\n <div v-if=\"slots['bottom-right']\" :class=\"slotClasses['bottom-right']\">\n <slot name=\"bottom-right\"></slot>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, useSlots } from 'vue'\n\nimport { cn } from '@/utils/tailwindUtil'\n\nconst slots = useSlots()\n\nconst {\n ratio = 'square',\n topLeftClass,\n topRightClass,\n centerLeftClass,\n centerRightClass,\n bottomLeftClass,\n bottomRightClass\n} = defineProps<{\n ratio?: 'square' | 'landscape'\n topLeftClass?: string\n topRightClass?: string\n centerLeftClass?: string\n centerRightClass?: string\n bottomLeftClass?: string\n bottomRightClass?: string\n}>()\n\nconst topStyle = computed(() => {\n const baseClasses = 'relative p-0 overflow-hidden'\n\n const ratioClasses = {\n square: 'aspect-square',\n landscape: 'aspect-48/27'\n }\n\n return `${baseClasses} ${ratioClasses[ratio]}`\n})\n\n// Get default classes for each slot position\nconst defaultSlotClasses = {\n 'top-left': 'absolute top-2 left-2 flex flex-wrap justify-start gap-2',\n 'top-right': 'absolute top-2 right-2 flex flex-wrap justify-end gap-2',\n 'center-left':\n 'absolute top-1/2 left-2 flex -translate-y-1/2 flex-wrap justify-start gap-2',\n 'center-right':\n 'absolute top-1/2 right-2 flex -translate-y-1/2 flex-wrap justify-end gap-2',\n 'bottom-left': 'absolute bottom-2 left-2 flex flex-wrap justify-start gap-2',\n 'bottom-right': 'absolute right-2 bottom-2 flex flex-wrap justify-end gap-2'\n}\n\n// Compute all slot classes once and cache them\nconst slotClasses = computed(() => ({\n 'top-left': cn(defaultSlotClasses['top-left'], topLeftClass),\n 'top-right': cn(defaultSlotClasses['top-right'], topRightClass),\n 'center-left': cn(defaultSlotClasses['center-left'], centerLeftClass),\n 'center-right': cn(defaultSlotClasses['center-right'], centerRightClass),\n 'bottom-left': cn(defaultSlotClasses['bottom-left'], bottomLeftClass),\n 'bottom-right': cn(defaultSlotClasses['bottom-right'], bottomRightClass)\n}))\n</script>\n","<template>\n <div :class=\"chipClasses\">\n <slot name=\"icon\"></slot>\n <span>{{ label }}</span>\n </div>\n</template>\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport { cn } from '@/utils/tailwindUtil'\n\nconst { label, variant = 'dark' } = defineProps<{\n label: string\n variant?: 'dark' | 'light' | 'gray'\n}>()\n\nconst baseClasses =\n 'inline-flex shrink-0 items-center justify-center gap-1 rounded px-2 py-1 text-xs font-bold'\n\nconst variantStyles = {\n dark: 'bg-zinc-500/40 text-white/90',\n light: cn('backdrop-blur-[2px] bg-base-background/50 text-base-foreground'),\n gray: cn(\n 'backdrop-blur-[2px] bg-modal-card-tag-background text-base-foreground'\n )\n}\n\nconst chipClasses = computed(() => {\n return cn(baseClasses, variantStyles[variant])\n})\n</script>\n","<template>\n <div\n class=\"relative aspect-square w-full overflow-hidden rounded-t-lg select-none\"\n >\n <div\n v-if=\"!error\"\n ref=\"contentRef\"\n class=\"h-full w-full transform-gpu transition-transform duration-1000 ease-out\"\n :style=\"\n isHovered ? { transform: `scale(${1 + hoverZoom / 100})` } : undefined\n \"\n >\n <slot />\n </div>\n <div v-else class=\"flex h-full w-full items-center justify-center\">\n <img\n src=\"/assets/images/default-template.png\"\n draggable=\"false\"\n class=\"h-full w-full transform-gpu object-cover transition-transform duration-300 ease-out\"\n />\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useEventListener } from '@vueuse/core'\nimport { onMounted, ref } from 'vue'\n\nconst error = ref(false)\nconst contentRef = ref<HTMLElement | null>(null)\n\nconst { hoverZoom = 4 } = defineProps<{\n hoverZoom?: number\n isHovered?: boolean\n}>()\n\nonMounted(() => {\n const images = Array.from(contentRef.value?.getElementsByTagName('img') ?? [])\n images.forEach((img) => {\n useEventListener(img, 'error', () => {\n error.value = true\n })\n })\n})\n</script>\n<style scoped>\nimg {\n transition: transform 1s cubic-bezier(0.2, 0, 0.4, 1);\n}\n</style>\n","<template>\n <BaseThumbnail :is-hovered=\"isHovered\">\n <LazyImage\n :src=\"baseImageSrc\"\n :alt=\"alt\"\n :image-class=\"\n isVideoType\n ? 'w-full h-full object-cover'\n : 'max-w-full max-h-64 object-contain'\n \"\n />\n <div ref=\"containerRef\" class=\"absolute inset-0\">\n <LazyImage\n :src=\"overlayImageSrc\"\n :alt=\"alt\"\n :image-class=\"\n isVideoType\n ? 'w-full h-full object-cover'\n : 'max-w-full max-h-64 object-contain'\n \"\n :image-style=\"{\n clipPath: `inset(0 ${100 - sliderPosition}% 0 0)`\n }\"\n />\n <div\n class=\"pointer-events-none absolute inset-y-0 z-10 w-0.5 bg-white/30 backdrop-blur-sm\"\n :style=\"{\n left: `${sliderPosition}%`\n }\"\n />\n </div>\n </BaseThumbnail>\n</template>\n\n<script setup lang=\"ts\">\nimport { useMouseInElement } from '@vueuse/core'\nimport { ref, watch } from 'vue'\n\nimport LazyImage from '@/components/common/LazyImage.vue'\nimport BaseThumbnail from '@/components/templates/thumbnails/BaseThumbnail.vue'\n\nconst SLIDER_START_POSITION = 50\n\nconst { baseImageSrc, overlayImageSrc, isHovered, isVideo } = defineProps<{\n baseImageSrc: string\n overlayImageSrc: string\n alt: string\n isHovered?: boolean\n isVideo?: boolean\n}>()\n\nconst isVideoType =\n isVideo ||\n baseImageSrc?.toLowerCase().endsWith('.webp') ||\n overlayImageSrc?.toLowerCase().endsWith('.webp') ||\n false\n\nconst sliderPosition = ref(SLIDER_START_POSITION)\nconst containerRef = ref<HTMLElement | null>(null)\n\nconst { elementX, elementWidth, isOutside } = useMouseInElement(containerRef)\n\n// Update slider position based on mouse position when hovered\nwatch(\n [() => isHovered, elementX, elementWidth, isOutside],\n ([isHovered, x, width, outside]) => {\n if (!isHovered) return\n if (!outside) {\n sliderPosition.value = (x / width) * 100\n }\n }\n)\n</script>\n","<template>\n <BaseThumbnail :hover-zoom=\"hoverZoom\" :is-hovered=\"isHovered\">\n <LazyImage\n :src=\"src\"\n :alt=\"alt\"\n :image-class=\"[\n 'transform-gpu transition-transform duration-300 ease-out',\n isVideoType\n ? 'w-full h-full object-cover'\n : 'max-w-full max-h-64 object-contain'\n ]\"\n :image-style=\"\n isHovered ? { transform: `scale(${1 + hoverZoom / 100})` } : undefined\n \"\n />\n </BaseThumbnail>\n</template>\n\n<script setup lang=\"ts\">\nimport LazyImage from '@/components/common/LazyImage.vue'\nimport BaseThumbnail from '@/components/templates/thumbnails/BaseThumbnail.vue'\n\nconst { src, isVideo } = defineProps<{\n src: string\n alt: string\n hoverZoom: number\n isHovered?: boolean\n isVideo?: boolean\n}>()\n\nconst isVideoType = isVideo ?? (src?.toLowerCase().endsWith('.webp') || false)\n</script>\n","<template>\n <BaseThumbnail :is-hovered=\"isHovered\">\n <div class=\"relative h-full w-full\">\n <div class=\"absolute inset-0\">\n <LazyImage\n :src=\"baseImageSrc\"\n :alt=\"alt\"\n :image-class=\"baseImageClass\"\n />\n </div>\n <div class=\"absolute inset-0 z-10\">\n <LazyImage\n :src=\"overlayImageSrc\"\n :alt=\"alt\"\n :image-class=\"overlayImageClass\"\n />\n </div>\n </div>\n </BaseThumbnail>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport LazyImage from '@/components/common/LazyImage.vue'\nimport BaseThumbnail from '@/components/templates/thumbnails/BaseThumbnail.vue'\n\nconst { baseImageSrc, overlayImageSrc, isVideo, isHovered } = defineProps<{\n baseImageSrc: string\n overlayImageSrc: string\n alt: string\n isHovered: boolean\n isVideo?: boolean\n}>()\n\nconst isVideoType =\n isVideo ||\n baseImageSrc?.toLowerCase().endsWith('.webp') ||\n overlayImageSrc?.toLowerCase().endsWith('.webp') ||\n false\n\nconst baseImageClass = computed(() => {\n const sizeClasses = isVideoType\n ? 'size-full object-cover'\n : 'size-full object-contain'\n return sizeClasses\n})\n\nconst overlayImageClass = computed(() => {\n const baseClasses = 'size-full transition-opacity duration-300'\n const sizeClasses = isVideoType ? 'object-cover' : 'object-contain'\n const opacityClasses = isHovered ? 'opacity-100' : 'opacity-0'\n return `${baseClasses} ${sizeClasses} ${opacityClasses}`\n})\n</script>\n","import { computed, ref, shallowRef, watch } from 'vue'\nimport type { Ref } from 'vue'\n\ninterface LazyPaginationOptions {\n itemsPerPage?: number\n initialPage?: number\n}\n\nexport function useLazyPagination<T>(\n items: Ref<T[]> | T[],\n options: LazyPaginationOptions = {}\n) {\n const { itemsPerPage = 12, initialPage = 1 } = options\n\n const currentPage = ref(initialPage)\n const isLoading = ref(false)\n const loadedPages = shallowRef(new Set<number>([]))\n\n // Get reactive items array\n const itemsArray = computed(() => {\n const itemData = 'value' in items ? items.value : items\n return Array.isArray(itemData) ? itemData : []\n })\n\n // Simulate pagination by slicing the items\n const paginatedItems = computed(() => {\n const itemData = itemsArray.value\n if (itemData.length === 0) {\n return []\n }\n\n const loadedPageNumbers = Array.from(loadedPages.value).sort(\n (a, b) => a - b\n )\n const maxLoadedPage = Math.max(...loadedPageNumbers, 0)\n const endIndex = maxLoadedPage * itemsPerPage\n return itemData.slice(0, endIndex)\n })\n\n const hasMoreItems = computed(() => {\n const itemData = itemsArray.value\n if (itemData.length === 0) {\n return false\n }\n\n const loadedPagesArray = Array.from(loadedPages.value)\n const maxLoadedPage = Math.max(...loadedPagesArray, 0)\n return maxLoadedPage * itemsPerPage < itemData.length\n })\n\n const totalPages = computed(() => {\n const itemData = itemsArray.value\n if (itemData.length === 0) {\n return 0\n }\n return Math.ceil(itemData.length / itemsPerPage)\n })\n\n const loadNextPage = async () => {\n if (isLoading.value || !hasMoreItems.value) return\n\n isLoading.value = true\n const loadedPagesArray = Array.from(loadedPages.value)\n const nextPage = Math.max(...loadedPagesArray, 0) + 1\n\n // Simulate network delay\n // await new Promise((resolve) => setTimeout(resolve, 5000))\n\n const newLoadedPages = new Set(loadedPages.value)\n newLoadedPages.add(nextPage)\n loadedPages.value = newLoadedPages\n currentPage.value = nextPage\n isLoading.value = false\n }\n\n // Initialize with first page\n watch(\n () => itemsArray.value.length,\n (length) => {\n if (length > 0 && loadedPages.value.size === 0) {\n loadedPages.value = new Set([1])\n }\n },\n { immediate: true }\n )\n\n const reset = () => {\n currentPage.value = initialPage\n loadedPages.value = new Set([])\n isLoading.value = false\n\n // Immediately load first page if we have items\n const itemData = itemsArray.value\n if (itemData.length > 0) {\n loadedPages.value = new Set([1])\n }\n }\n\n return {\n paginatedItems,\n isLoading,\n hasMoreItems,\n currentPage,\n totalPages,\n loadNextPage,\n reset\n }\n}\n","/**\n * Store for template ranking scores.\n * Loads pre-computed usage scores from static JSON.\n * Internal ranks come from template.searchRank in index.json.\n * See docs/TEMPLATE_RANKING.md for details.\n */\n\nimport { defineStore } from 'pinia'\nimport { ref } from 'vue'\n\nexport const useTemplateRankingStore = defineStore('templateRanking', () => {\n const largestUsageScore = ref<number>()\n\n const normalizeUsageScore = (usage: number): number => {\n return usage / (largestUsageScore.value ?? usage)\n }\n\n /**\n * Compute freshness score based on template date.\n * Returns 1.0 for brand new, decays to 0.1 over ~6 months.\n */\n const computeFreshness = (dateStr: string | undefined): number => {\n if (!dateStr) return 0.5 // Default for templates without dates\n\n const date = new Date(dateStr)\n if (isNaN(date.getTime())) return 0.5\n\n const daysSinceAdded = (Date.now() - date.getTime()) / (1000 * 60 * 60 * 24)\n return Math.max(0.1, 1.0 / (1 + daysSinceAdded / 90))\n }\n\n /**\n * Compute composite score for \"default\" sort.\n * Formula: usage × 0.5 + internal × 0.3 + freshness × 0.2\n */\n const computeDefaultScore = (\n dateStr: string | undefined,\n searchRank: number | undefined,\n usage: number = 0\n ): number => {\n const internal = (searchRank ?? 5) / 10 // Normalize 1-10 to 0-1\n const freshness = computeFreshness(dateStr)\n\n return normalizeUsageScore(usage) * 0.5 + internal * 0.3 + freshness * 0.2\n }\n\n /**\n * Compute composite score for \"popular\" sort.\n * Formula: usage × 0.9 + freshness × 0.1\n */\n const computePopularScore = (\n dateStr: string | undefined,\n usage: number = 0\n ): number => {\n const freshness = computeFreshness(dateStr)\n\n return normalizeUsageScore(usage) * 0.9 + freshness * 0.1\n }\n\n return {\n largestUsageScore,\n computeFreshness,\n computeDefaultScore,\n computePopularScore\n }\n})\n","import { refDebounced, watchDebounced } from '@vueuse/core'\nimport Fuse from 'fuse.js'\nimport type { IFuseOptions } from 'fuse.js'\nimport { computed, ref, watch } from 'vue'\nimport type { Ref } from 'vue'\n\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useTelemetry } from '@/platform/telemetry'\nimport type { TemplateInfo } from '@/platform/workflow/templates/types/template'\nimport { useTemplateRankingStore } from '@/stores/templateRankingStore'\nimport { debounce } from 'es-toolkit/compat'\nimport { api } from '@/scripts/api'\n\n// Fuse.js configuration for fuzzy search\nconst defaultFuseOptions: IFuseOptions<TemplateInfo> = {\n keys: [\n { name: 'name', weight: 0.3 },\n { name: 'title', weight: 0.3 },\n { name: 'description', weight: 0.1 },\n { name: 'tags', weight: 0.2 },\n { name: 'models', weight: 0.3 }\n ],\n threshold: 0.33,\n includeScore: true,\n includeMatches: true\n}\n\nexport function useTemplateFiltering(\n templates: Ref<TemplateInfo[]> | TemplateInfo[]\n) {\n const settingStore = useSettingStore()\n const rankingStore = useTemplateRankingStore()\n\n const searchQuery = ref('')\n const selectedModels = ref<string[]>(\n settingStore.get('Comfy.Templates.SelectedModels')\n )\n const selectedUseCases = ref<string[]>(\n settingStore.get('Comfy.Templates.SelectedUseCases')\n )\n const selectedRunsOn = ref<string[]>(\n settingStore.get('Comfy.Templates.SelectedRunsOn')\n )\n const sortBy = ref<\n | 'default'\n | 'recommended'\n | 'popular'\n | 'alphabetical'\n | 'newest'\n | 'vram-low-to-high'\n | 'model-size-low-to-high'\n >(settingStore.get('Comfy.Templates.SortBy'))\n\n const fuseOptions = ref<IFuseOptions<TemplateInfo>>(defaultFuseOptions)\n\n const templatesArray = computed(() => {\n const templateData = 'value' in templates ? templates.value : templates\n return Array.isArray(templateData) ? templateData : []\n })\n\n const fuse = computed(() => new Fuse(templatesArray.value, fuseOptions.value))\n\n const availableModels = computed(() => {\n const modelSet = new Set<string>()\n templatesArray.value.forEach((template) => {\n if (Array.isArray(template.models)) {\n template.models.forEach((model) => modelSet.add(model))\n }\n })\n return Array.from(modelSet).sort()\n })\n\n const availableUseCases = computed(() => {\n const tagSet = new Set<string>()\n templatesArray.value.forEach((template) => {\n if (template.tags && Array.isArray(template.tags)) {\n template.tags.forEach((tag) => tagSet.add(tag))\n }\n })\n return Array.from(tagSet).sort()\n })\n\n const availableRunsOn = computed(() => {\n return ['ComfyUI', 'External or Remote API']\n })\n\n const debouncedSearchQuery = refDebounced(searchQuery, 50)\n\n const filteredBySearch = computed(() => {\n if (!debouncedSearchQuery.value.trim()) {\n return templatesArray.value\n }\n\n const results = fuse.value.search(debouncedSearchQuery.value)\n return results.map((result) => result.item)\n })\n\n const filteredByModels = computed(() => {\n if (selectedModels.value.length === 0) {\n return filteredBySearch.value\n }\n\n return filteredBySearch.value.filter((template) => {\n if (!template.models || !Array.isArray(template.models)) {\n return false\n }\n return selectedModels.value.some((selectedModel) =>\n template.models?.includes(selectedModel)\n )\n })\n })\n\n const filteredByUseCases = computed(() => {\n if (selectedUseCases.value.length === 0) {\n return filteredByModels.value\n }\n\n return filteredByModels.value.filter((template) => {\n if (!template.tags || !Array.isArray(template.tags)) {\n return false\n }\n return selectedUseCases.value.some((selectedTag) =>\n template.tags?.includes(selectedTag)\n )\n })\n })\n\n const filteredByRunsOn = computed(() => {\n if (selectedRunsOn.value.length === 0) {\n return filteredByUseCases.value\n }\n\n return filteredByUseCases.value.filter((template) => {\n // Use openSource field to determine where template runs\n // openSource === false -> External/Remote API\n // openSource !== false -> ComfyUI (includes true and undefined)\n const isExternalAPI = template.openSource === false\n const isComfyUI = template.openSource !== false\n\n return selectedRunsOn.value.some((selectedRunsOn) => {\n if (selectedRunsOn === 'External or Remote API') {\n return isExternalAPI\n } else if (selectedRunsOn === 'ComfyUI') {\n return isComfyUI\n }\n return false\n })\n })\n })\n\n const getVramMetric = (template: TemplateInfo) => {\n if (\n typeof template.vram === 'number' &&\n Number.isFinite(template.vram) &&\n template.vram > 0\n ) {\n return template.vram\n }\n return Number.POSITIVE_INFINITY\n }\n\n watch(\n filteredByRunsOn,\n (templates) => {\n rankingStore.largestUsageScore = Math.max(\n ...templates.map((t) => t.usage || 0)\n )\n },\n { immediate: true }\n )\n\n const sortedTemplates = computed(() => {\n const templates = [...filteredByRunsOn.value]\n\n switch (sortBy.value) {\n case 'recommended':\n // Curated: usage × 0.5 + internal × 0.3 + freshness × 0.2\n return templates.sort((a, b) => {\n const scoreA = rankingStore.computeDefaultScore(\n a.date,\n a.searchRank,\n a.usage\n )\n const scoreB = rankingStore.computeDefaultScore(\n b.date,\n b.searchRank,\n b.usage\n )\n return scoreB - scoreA\n })\n case 'popular':\n // User-driven: usage × 0.9 + freshness × 0.1\n return templates.sort((a, b) => {\n const scoreA = rankingStore.computePopularScore(a.date, a.usage)\n const scoreB = rankingStore.computePopularScore(b.date, b.usage)\n return scoreB - scoreA\n })\n case 'alphabetical':\n return templates.sort((a, b) => {\n const nameA = a.title || a.name || ''\n const nameB = b.title || b.name || ''\n return nameA.localeCompare(nameB)\n })\n case 'newest':\n return templates.sort((a, b) => {\n const dateA = new Date(a.date || '1970-01-01')\n const dateB = new Date(b.date || '1970-01-01')\n return dateB.getTime() - dateA.getTime()\n })\n case 'vram-low-to-high':\n return templates.sort((a, b) => {\n const vramA = getVramMetric(a)\n const vramB = getVramMetric(b)\n\n if (vramA === vramB) {\n const nameA = a.title || a.name || ''\n const nameB = b.title || b.name || ''\n return nameA.localeCompare(nameB)\n }\n\n if (vramA === Number.POSITIVE_INFINITY) return 1\n if (vramB === Number.POSITIVE_INFINITY) return -1\n\n return vramA - vramB\n })\n case 'model-size-low-to-high':\n return templates.sort((a, b) => {\n const sizeA =\n typeof a.size === 'number' ? a.size : Number.POSITIVE_INFINITY\n const sizeB =\n typeof b.size === 'number' ? b.size : Number.POSITIVE_INFINITY\n if (sizeA === sizeB) return 0\n return sizeA - sizeB\n })\n case 'default':\n default:\n return templates\n }\n })\n\n const filteredTemplates = computed(() => sortedTemplates.value)\n\n const resetFilters = () => {\n searchQuery.value = ''\n selectedModels.value = []\n selectedUseCases.value = []\n selectedRunsOn.value = []\n sortBy.value = 'default'\n }\n\n const removeModelFilter = (model: string) => {\n selectedModels.value = selectedModels.value.filter((m) => m !== model)\n }\n\n const removeUseCaseFilter = (tag: string) => {\n selectedUseCases.value = selectedUseCases.value.filter((t) => t !== tag)\n }\n\n const removeRunsOnFilter = (runsOn: string) => {\n selectedRunsOn.value = selectedRunsOn.value.filter((r) => r !== runsOn)\n }\n\n const filteredCount = computed(() => filteredTemplates.value.length)\n const totalCount = computed(() => templatesArray.value.length)\n\n // Template filter tracking (debounced to avoid excessive events)\n const debouncedTrackFilterChange = debounce(() => {\n useTelemetry()?.trackTemplateFilterChanged({\n search_query: searchQuery.value || undefined,\n selected_models: selectedModels.value,\n selected_use_cases: selectedUseCases.value,\n selected_runs_on: selectedRunsOn.value,\n sort_by: sortBy.value,\n filtered_count: filteredCount.value,\n total_count: totalCount.value\n })\n }, 500)\n\n const loadFuseOptions = async () => {\n const fetchedOptions = await api.getFuseOptions()\n if (fetchedOptions) {\n fuseOptions.value = fetchedOptions\n }\n }\n\n // Watch for filter changes and track them\n watch(\n [searchQuery, selectedModels, selectedUseCases, selectedRunsOn, sortBy],\n () => {\n // Only track if at least one filter is active (to avoid tracking initial state)\n const hasActiveFilters =\n searchQuery.value.trim() !== '' ||\n selectedModels.value.length > 0 ||\n selectedUseCases.value.length > 0 ||\n selectedRunsOn.value.length > 0 ||\n sortBy.value !== 'default'\n\n if (hasActiveFilters) {\n debouncedTrackFilterChange()\n }\n },\n { deep: true }\n )\n\n // Persist filter changes to settings (debounced to avoid excessive saves)\n watchDebounced(\n selectedModels,\n (newValue) => {\n void settingStore.set('Comfy.Templates.SelectedModels', newValue)\n },\n { debounce: 500, deep: true }\n )\n\n watchDebounced(\n selectedUseCases,\n (newValue) => {\n void settingStore.set('Comfy.Templates.SelectedUseCases', newValue)\n },\n { debounce: 500, deep: true }\n )\n\n watchDebounced(\n selectedRunsOn,\n (newValue) => {\n void settingStore.set('Comfy.Templates.SelectedRunsOn', newValue)\n },\n { debounce: 500, deep: true }\n )\n\n watchDebounced(\n sortBy,\n (newValue) => {\n void settingStore.set('Comfy.Templates.SortBy', newValue)\n },\n { debounce: 500 }\n )\n\n return {\n // State\n searchQuery,\n selectedModels,\n selectedUseCases,\n selectedRunsOn,\n sortBy,\n\n // Computed\n filteredTemplates,\n availableModels,\n availableUseCases,\n availableRunsOn,\n filteredCount,\n totalCount,\n\n // Methods\n resetFilters,\n removeModelFilter,\n removeUseCaseFilter,\n removeRunsOnFilter,\n loadFuseOptions\n }\n}\n","import { computed, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { isCloud } from '@/platform/distribution/types'\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'\nimport type {\n TemplateGroup,\n TemplateInfo,\n WorkflowTemplates\n} from '@/platform/workflow/templates/types/template'\nimport { api } from '@/scripts/api'\nimport { app } from '@/scripts/app'\nimport { useDialogStore } from '@/stores/dialogStore'\n\nexport function useTemplateWorkflows() {\n const { t } = useI18n()\n const workflowTemplatesStore = useWorkflowTemplatesStore()\n const dialogStore = useDialogStore()\n\n // State\n const selectedTemplate = ref<WorkflowTemplates | null>(null)\n const loadingTemplateId = ref<string | null>(null)\n\n // Computed\n const isTemplatesLoaded = computed(() => workflowTemplatesStore.isLoaded)\n const allTemplateGroups = computed<TemplateGroup[]>(\n () => workflowTemplatesStore.groupedTemplates\n )\n\n /**\n * Loads all template workflows from the API\n */\n const loadTemplates = async () => {\n if (!workflowTemplatesStore.isLoaded) {\n await workflowTemplatesStore.loadWorkflowTemplates()\n }\n return workflowTemplatesStore.isLoaded\n }\n\n /**\n * Selects the first template category as default\n */\n const selectFirstTemplateCategory = () => {\n if (allTemplateGroups.value.length > 0) {\n const firstCategory = allTemplateGroups.value[0].modules[0]\n selectTemplateCategory(firstCategory)\n }\n }\n\n /**\n * Selects a template category\n */\n const selectTemplateCategory = (category: WorkflowTemplates | null) => {\n selectedTemplate.value = category\n return category !== null\n }\n\n /**\n * Gets template thumbnail URL\n */\n const getTemplateThumbnailUrl = (\n template: TemplateInfo,\n sourceModule: string,\n index = '1'\n ) => {\n const basePath =\n sourceModule === 'default'\n ? api.fileURL(`/templates/${template.name}`)\n : api.apiURL(`/workflow_templates/${sourceModule}/${template.name}`)\n\n const indexSuffix = sourceModule === 'default' && index ? `-${index}` : ''\n return `${basePath}${indexSuffix}.${template.mediaSubtype}`\n }\n\n /**\n * Gets formatted template title\n */\n const getTemplateTitle = (template: TemplateInfo, sourceModule: string) => {\n const fallback =\n template.title ?? template.name ?? `${sourceModule} Template`\n return sourceModule === 'default'\n ? (template.localizedTitle ?? fallback)\n : fallback\n }\n\n /**\n * Gets formatted template description\n */\n const getTemplateDescription = (template: TemplateInfo) => {\n return (\n (template.localizedDescription || template.description)\n ?.replace(/[-_]/g, ' ')\n .trim() ?? ''\n )\n }\n\n /**\n * Loads a workflow template\n */\n const loadWorkflowTemplate = async (id: string, sourceModule: string) => {\n if (!isTemplatesLoaded.value) return false\n\n loadingTemplateId.value = id\n let json\n\n try {\n // Handle \"All\" category as a special case\n if (sourceModule === 'all') {\n // Find \"All\" category in the ComfyUI Examples group\n const comfyExamplesGroup = allTemplateGroups.value.find(\n (g) =>\n g.label ===\n t('templateWorkflows.category.ComfyUI Examples', 'ComfyUI Examples')\n )\n const allCategory = comfyExamplesGroup?.modules.find(\n (m) => m.moduleName === 'all'\n )\n const template = allCategory?.templates.find((t) => t.name === id)\n\n if (!template || !template.sourceModule) return false\n\n // Use the stored source module for loading\n sourceModule = template.sourceModule\n }\n\n // Regular case for normal categories\n json = await fetchTemplateJson(id, sourceModule)\n\n const workflowName =\n sourceModule === 'default'\n ? t(`templateWorkflows.template.${id}`, id)\n : id\n\n if (isCloud) {\n useTelemetry()?.trackTemplate({\n workflow_name: id,\n template_source: sourceModule\n })\n }\n\n dialogStore.closeDialog()\n await app.loadGraphData(json, true, true, workflowName, {\n openSource: 'template'\n })\n\n return true\n } catch (error) {\n console.error('Error loading workflow template:', error)\n return false\n } finally {\n loadingTemplateId.value = null\n }\n }\n\n /**\n * Fetches template JSON from the appropriate endpoint\n */\n const fetchTemplateJson = async (id: string, sourceModule: string) => {\n if (sourceModule === 'default') {\n // Default templates provided by frontend are served on this separate endpoint\n return fetch(api.fileURL(`/templates/${id}.json`)).then((r) => r.json())\n } else {\n return fetch(\n api.apiURL(`/workflow_templates/${sourceModule}/${id}.json`)\n ).then((r) => r.json())\n }\n }\n\n return {\n // State\n selectedTemplate,\n loadingTemplateId,\n\n // Computed\n isTemplatesLoaded,\n allTemplateGroups,\n\n // Methods\n loadTemplates,\n selectFirstTemplateCategory,\n selectTemplateCategory,\n getTemplateThumbnailUrl,\n getTemplateTitle,\n getTemplateDescription,\n loadWorkflowTemplate\n }\n}\n","import type { CSSProperties } from 'vue'\n\ninterface GridOptions {\n /** Minimum width for each grid item (default: 15rem) */\n minWidth?: string\n /** Maximum width for each grid item (default: 1fr) */\n maxWidth?: string\n /** Padding around the grid (default: 0) */\n padding?: string\n /** Gap between grid items (default: 1rem) */\n gap?: string\n /** Fixed number of columns (overrides auto-fill with minmax) */\n columns?: number\n}\n\n/**\n * @deprecated Just use tailwind utilities directly.\n * TODO: Create a common grid layout component if needed.\n * Creates CSS grid styles for responsive grid layouts\n * @param options Grid configuration options\n * @returns CSS properties object for grid styling\n */\nexport function createGridStyle(options: GridOptions = {}): CSSProperties {\n const {\n minWidth = '15rem',\n maxWidth = '1fr',\n padding = '0',\n gap = '1rem',\n columns\n } = options\n\n // Runtime validation for columns\n if (columns !== undefined && columns < 1) {\n console.warn('createGridStyle: columns must be >= 1, defaulting to 1')\n }\n\n const gridTemplateColumns = columns\n ? `repeat(${Math.max(1, columns ?? 1)}, 1fr)`\n : `repeat(auto-fill, minmax(${minWidth}, ${maxWidth}))`\n\n return {\n display: 'grid',\n gridTemplateColumns,\n padding,\n gap\n }\n}\n","<template>\n <BaseModalLayout\n :content-title=\"$t('templateWorkflows.title', 'Workflow Templates')\"\n class=\"workflow-template-selector-dialog\"\n >\n <template #leftPanel>\n <LeftSidePanel v-model=\"selectedNavItem\" :nav-items=\"navItems\">\n <template #header-icon>\n <i class=\"icon-[comfy--template]\" />\n </template>\n <template #header-title>\n <span class=\"text-neutral text-base\">{{\n $t('sideToolbar.templates', 'Templates')\n }}</span>\n </template>\n </LeftSidePanel>\n </template>\n\n <template #header>\n <SearchBox v-model=\"searchQuery\" size=\"lg\" class=\"max-w-[384px]\" />\n </template>\n\n <template #header-right-area>\n <div class=\"flex gap-2\">\n <Button\n v-if=\"filteredCount !== totalCount\"\n variant=\"secondary\"\n size=\"lg\"\n @click=\"resetFilters\"\n >\n <i class=\"icon-[lucide--filter-x]\" />\n <span>{{\n $t('templateWorkflows.resetFilters', 'Clear Filters')\n }}</span>\n </Button>\n </div>\n </template>\n\n <template #contentFilter>\n <div class=\"relative flex flex-wrap justify-between gap-2 px-6 pb-4\">\n <div class=\"flex flex-wrap gap-2\">\n <!-- Model Filter -->\n <MultiSelect\n v-model=\"selectedModelObjects\"\n v-model:search-query=\"modelSearchText\"\n class=\"w-[250px]\"\n :label=\"modelFilterLabel\"\n :options=\"modelOptions\"\n :show-search-box=\"true\"\n :show-selected-count=\"true\"\n :show-clear-button=\"true\"\n >\n <template #icon>\n <i class=\"icon-[lucide--cpu]\" />\n </template>\n </MultiSelect>\n\n <!-- Use Case Filter -->\n <MultiSelect\n v-model=\"selectedUseCaseObjects\"\n :label=\"useCaseFilterLabel\"\n :options=\"useCaseOptions\"\n :show-search-box=\"true\"\n :show-selected-count=\"true\"\n :show-clear-button=\"true\"\n >\n <template #icon>\n <i class=\"icon-[lucide--target]\" />\n </template>\n </MultiSelect>\n\n <!-- Runs On Filter -->\n <MultiSelect\n v-model=\"selectedRunsOnObjects\"\n :label=\"runsOnFilterLabel\"\n :options=\"runsOnOptions\"\n :show-search-box=\"true\"\n :show-selected-count=\"true\"\n :show-clear-button=\"true\"\n >\n <template #icon>\n <i class=\"icon-[lucide--server]\" />\n </template>\n </MultiSelect>\n </div>\n\n <!-- Sort Options -->\n <div>\n <SingleSelect\n v-model=\"sortBy\"\n :label=\"$t('templateWorkflows.sorting', 'Sort by')\"\n :options=\"sortOptions\"\n class=\"w-62.5\"\n >\n <template #icon>\n <i class=\"icon-[lucide--arrow-up-down] text-muted-foreground\" />\n </template>\n </SingleSelect>\n </div>\n </div>\n <div\n v-if=\"!isLoading\"\n class=\"text-neutral px-6 pt-4 pb-2 text-2xl font-semibold\"\n >\n <span>\n {{ pageTitle }}\n </span>\n </div>\n </template>\n\n <template #content>\n <!-- No Results State (only show when loaded and no results) -->\n <div\n v-if=\"!isLoading && filteredTemplates.length === 0\"\n class=\"flex h-64 flex-col items-center justify-center text-neutral-500\"\n >\n <i class=\"mb-4 icon-[lucide--search] h-12 w-12 opacity-50\" />\n <p class=\"mb-2 text-lg\">\n {{ $t('templateWorkflows.noResults', 'No templates found') }}\n </p>\n <p class=\"text-sm\">\n {{\n $t(\n 'templateWorkflows.noResultsHint',\n 'Try adjusting your search or filters'\n )\n }}\n </p>\n </div>\n <div v-else>\n <!-- Title -->\n <span\n v-if=\"isLoading\"\n class=\"inline-block h-8 w-48 animate-pulse rounded bg-dialog-surface\"\n ></span>\n\n <!-- Template Cards Grid -->\n <div\n :key=\"templateListKey\"\n :style=\"gridStyle\"\n data-testid=\"template-workflows-content\"\n >\n <!-- Loading Skeletons (show while loading initial data) -->\n <CardContainer\n v-for=\"n in isLoading ? 12 : 0\"\n :key=\"`initial-skeleton-${n}`\"\n size=\"compact\"\n variant=\"ghost\"\n rounded=\"lg\"\n class=\"hover:bg-base-background\"\n >\n <template #top>\n <CardTop ratio=\"landscape\">\n <template #default>\n <div\n class=\"h-full w-full animate-pulse bg-dialog-surface\"\n ></div>\n </template>\n </CardTop>\n </template>\n <template #bottom>\n <CardBottom>\n <div class=\"px-4 py-3\">\n <div\n class=\"mb-2 h-6 animate-pulse rounded bg-dialog-surface\"\n ></div>\n <div\n class=\"h-4 animate-pulse rounded bg-dialog-surface\"\n ></div>\n </div>\n </CardBottom>\n </template>\n </CardContainer>\n\n <!-- Actual Template Cards -->\n <CardContainer\n v-for=\"template in isLoading ? [] : displayTemplates\"\n v-show=\"isTemplateVisibleOnDistribution(template)\"\n :key=\"template.name\"\n ref=\"cardRefs\"\n size=\"compact\"\n variant=\"ghost\"\n rounded=\"lg\"\n :data-testid=\"`template-workflow-${template.name}`\"\n class=\"hover:bg-base-background\"\n @mouseenter=\"hoveredTemplate = template.name\"\n @mouseleave=\"hoveredTemplate = null\"\n @click=\"onLoadWorkflow(template)\"\n >\n <template #top>\n <CardTop ratio=\"square\">\n <template #default>\n <!-- Template Thumbnail -->\n <div\n class=\"relative h-full w-full overflow-hidden rounded-lg\"\n >\n <template v-if=\"template.mediaType === 'audio'\">\n <AudioThumbnail :src=\"getBaseThumbnailSrc(template)\" />\n </template>\n <template\n v-else-if=\"template.thumbnailVariant === 'compareSlider'\"\n >\n <CompareSliderThumbnail\n :base-image-src=\"getBaseThumbnailSrc(template)\"\n :overlay-image-src=\"getOverlayThumbnailSrc(template)\"\n :alt=\"\n getTemplateTitle(\n template,\n getEffectiveSourceModule(template)\n )\n \"\n :is-hovered=\"hoveredTemplate === template.name\"\n :is-video=\"\n template.mediaType === 'video' ||\n template.mediaSubtype === 'webp'\n \"\n />\n </template>\n <template\n v-else-if=\"template.thumbnailVariant === 'hoverDissolve'\"\n >\n <HoverDissolveThumbnail\n :base-image-src=\"getBaseThumbnailSrc(template)\"\n :overlay-image-src=\"getOverlayThumbnailSrc(template)\"\n :alt=\"\n getTemplateTitle(\n template,\n getEffectiveSourceModule(template)\n )\n \"\n :is-hovered=\"hoveredTemplate === template.name\"\n :is-video=\"\n template.mediaType === 'video' ||\n template.mediaSubtype === 'webp'\n \"\n />\n </template>\n <template v-else>\n <DefaultThumbnail\n :src=\"getBaseThumbnailSrc(template)\"\n :alt=\"\n getTemplateTitle(\n template,\n getEffectiveSourceModule(template)\n )\n \"\n :is-hovered=\"hoveredTemplate === template.name\"\n :is-video=\"\n template.mediaType === 'video' ||\n template.mediaSubtype === 'webp'\n \"\n :hover-zoom=\"\n template.thumbnailVariant === 'zoomHover' ? 16 : 5\n \"\n />\n </template>\n <ProgressSpinner\n v-if=\"loadingTemplate === template.name\"\n class=\"absolute inset-0 z-10 m-auto h-12 w-12\"\n />\n </div>\n </template>\n <template #bottom-right>\n <template v-if=\"template.tags && template.tags.length > 0\">\n <SquareChip\n v-for=\"tag in template.tags\"\n :key=\"tag\"\n :label=\"tag\"\n />\n </template>\n </template>\n </CardTop>\n </template>\n <template #bottom>\n <CardBottom>\n <div class=\"flex flex-col gap-2 pt-3\">\n <h3\n class=\"m-0 line-clamp-1 text-sm\"\n :title=\"\n getTemplateTitle(\n template,\n getEffectiveSourceModule(template)\n )\n \"\n >\n {{\n getTemplateTitle(\n template,\n getEffectiveSourceModule(template)\n )\n }}\n </h3>\n <div class=\"flex justify-between gap-2\">\n <div class=\"flex-1\">\n <p\n class=\"m-0 line-clamp-2 text-sm text-muted\"\n :title=\"getTemplateDescription(template)\"\n >\n {{ getTemplateDescription(template) }}\n </p>\n </div>\n <div\n v-if=\"template.tutorialUrl\"\n class=\"flex flex-col-reverse justify-center\"\n >\n <Button\n v-if=\"hoveredTemplate === template.name\"\n v-tooltip.bottom=\"$t('g.seeTutorial')\"\n v-bind=\"$attrs\"\n variant=\"inverted\"\n size=\"icon\"\n @click.stop=\"openTutorial(template)\"\n >\n <i class=\"icon-[lucide--info] size-4\" />\n </Button>\n </div>\n </div>\n </div>\n </CardBottom>\n </template>\n </CardContainer>\n\n <!-- Loading More Skeletons -->\n <CardContainer\n v-for=\"n in isLoadingMore ? 6 : 0\"\n :key=\"`skeleton-${n}`\"\n size=\"compact\"\n variant=\"ghost\"\n rounded=\"lg\"\n class=\"hover:bg-base-background\"\n >\n <template #top>\n <CardTop ratio=\"square\">\n <template #default>\n <div\n class=\"h-full w-full animate-pulse bg-dialog-surface\"\n ></div>\n </template>\n </CardTop>\n </template>\n <template #bottom>\n <CardBottom>\n <div class=\"px-4 py-3\">\n <div\n class=\"mb-2 h-6 animate-pulse rounded bg-dialog-surface\"\n ></div>\n <div\n class=\"h-4 animate-pulse rounded bg-dialog-surface\"\n ></div>\n </div>\n </CardBottom>\n </template>\n </CardContainer>\n </div>\n </div>\n\n <!-- Load More Trigger -->\n <div\n v-if=\"!isLoading && hasMoreTemplates\"\n ref=\"loadTrigger\"\n class=\"mt-4 flex h-4 w-full items-center justify-center\"\n >\n <div v-if=\"isLoadingMore\" class=\"text-sm text-muted\">\n {{ $t('templateWorkflows.loadingMore', 'Loading more...') }}\n </div>\n </div>\n\n <!-- Results Summary -->\n <div v-if=\"!isLoading\" class=\"mt-6 px-6 text-sm text-muted\">\n {{\n $t('templateWorkflows.resultsCount', {\n count: filteredCount,\n total: totalCount\n })\n }}\n </div>\n </template>\n </BaseModalLayout>\n</template>\n\n<script setup lang=\"ts\">\nimport { useAsyncState } from '@vueuse/core'\nimport ProgressSpinner from 'primevue/progressspinner'\nimport { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport CardBottom from '@/components/card/CardBottom.vue'\nimport CardContainer from '@/components/card/CardContainer.vue'\nimport CardTop from '@/components/card/CardTop.vue'\nimport SquareChip from '@/components/chip/SquareChip.vue'\nimport SearchBox from '@/components/common/SearchBox.vue'\nimport MultiSelect from '@/components/input/MultiSelect.vue'\nimport SingleSelect from '@/components/input/SingleSelect.vue'\nimport AudioThumbnail from '@/components/templates/thumbnails/AudioThumbnail.vue'\nimport CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'\nimport DefaultThumbnail from '@/components/templates/thumbnails/DefaultThumbnail.vue'\nimport HoverDissolveThumbnail from '@/components/templates/thumbnails/HoverDissolveThumbnail.vue'\nimport Button from '@/components/ui/button/Button.vue'\nimport BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'\nimport LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'\nimport { useIntersectionObserver } from '@/composables/useIntersectionObserver'\nimport { useLazyPagination } from '@/composables/useLazyPagination'\nimport { useTemplateFiltering } from '@/composables/useTemplateFiltering'\nimport { isCloud } from '@/platform/distribution/types'\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useTemplateWorkflows } from '@/platform/workflow/templates/composables/useTemplateWorkflows'\nimport { useWorkflowTemplatesStore } from '@/platform/workflow/templates/repositories/workflowTemplatesStore'\nimport type { TemplateInfo } from '@/platform/workflow/templates/types/template'\nimport { TemplateIncludeOnDistributionEnum } from '@/platform/workflow/templates/types/template'\nimport { useSystemStatsStore } from '@/stores/systemStatsStore'\nimport type { NavGroupData, NavItemData } from '@/types/navTypes'\nimport { OnCloseKey } from '@/types/widgetTypes'\nimport { createGridStyle } from '@/utils/gridUtil'\n\nconst { t } = useI18n()\n\nconst { onClose: originalOnClose } = defineProps<{\n onClose: () => void\n}>()\n\n// Track session time for telemetry\nconst sessionStartTime = ref<number>(0)\nconst templateWasSelected = ref(false)\n\nonMounted(() => {\n sessionStartTime.value = Date.now()\n})\n\nconst systemStatsStore = useSystemStatsStore()\n\nconst distributions = computed(() => {\n // eslint-disable-next-line no-undef\n switch (__DISTRIBUTION__) {\n case 'cloud':\n return [TemplateIncludeOnDistributionEnum.Cloud]\n case 'localhost':\n return [TemplateIncludeOnDistributionEnum.Local]\n case 'desktop':\n default:\n if (systemStatsStore.systemStats?.system.os === 'darwin') {\n return [\n TemplateIncludeOnDistributionEnum.Desktop,\n TemplateIncludeOnDistributionEnum.Mac\n ]\n }\n return [\n TemplateIncludeOnDistributionEnum.Desktop,\n TemplateIncludeOnDistributionEnum.Windows\n ]\n }\n})\n\n// Wrap onClose to track session end\nconst onClose = () => {\n if (isCloud) {\n const timeSpentSeconds = Math.floor(\n (Date.now() - sessionStartTime.value) / 1000\n )\n\n useTelemetry()?.trackTemplateLibraryClosed({\n template_selected: templateWasSelected.value,\n time_spent_seconds: timeSpentSeconds\n })\n }\n\n originalOnClose()\n}\n\nprovide(OnCloseKey, onClose)\n\n// Workflow templates store and composable\nconst workflowTemplatesStore = useWorkflowTemplatesStore()\nconst {\n loadTemplates,\n loadWorkflowTemplate,\n getTemplateThumbnailUrl,\n getTemplateTitle,\n getTemplateDescription\n} = useTemplateWorkflows()\n\nconst getEffectiveSourceModule = (template: TemplateInfo) =>\n template.sourceModule || 'default'\n\nconst getBaseThumbnailSrc = (template: TemplateInfo) => {\n const sm = getEffectiveSourceModule(template)\n return getTemplateThumbnailUrl(template, sm, sm === 'default' ? '1' : '')\n}\n\nconst getOverlayThumbnailSrc = (template: TemplateInfo) => {\n const sm = getEffectiveSourceModule(template)\n return getTemplateThumbnailUrl(template, sm, sm === 'default' ? '2' : '')\n}\n\n// Open tutorial in new tab\nconst openTutorial = (template: TemplateInfo) => {\n if (template.tutorialUrl) {\n window.open(template.tutorialUrl, '_blank')\n }\n}\n\n// Get navigation items from the store, with skeleton items while loading\nconst navItems = computed<(NavItemData | NavGroupData)[]>(() => {\n // Show skeleton navigation items while loading\n if (isLoading.value) {\n return [\n {\n id: 'skeleton-all',\n label: 'All Templates',\n icon: 'icon-[lucide--layout-grid]'\n },\n {\n id: 'skeleton-basics',\n label: 'Basics',\n icon: 'icon-[lucide--graduation-cap]'\n },\n {\n title: 'Generation Type',\n items: [\n { id: 'skeleton-1', label: '...', icon: 'icon-[lucide--loader-2]' },\n { id: 'skeleton-2', label: '...', icon: 'icon-[lucide--loader-2]' }\n ]\n },\n {\n title: 'Closed Source Models',\n items: [\n { id: 'skeleton-3', label: '...', icon: 'icon-[lucide--loader-2]' }\n ]\n }\n ]\n }\n return workflowTemplatesStore.navGroupedTemplates\n})\n\nconst gridStyle = computed(() => createGridStyle())\n\n// Get enhanced templates for better filtering\nconst allTemplates = computed(() => {\n return workflowTemplatesStore.enhancedTemplates\n})\n\n// Navigation\nconst selectedNavItem = ref<string | null>('all')\n\n// Filter templates based on selected navigation item\nconst navigationFilteredTemplates = computed(() => {\n if (!selectedNavItem.value) {\n return allTemplates.value\n }\n\n return workflowTemplatesStore.filterTemplatesByCategory(selectedNavItem.value)\n})\n\n// Template filtering\nconst {\n searchQuery,\n selectedModels,\n selectedUseCases,\n selectedRunsOn,\n sortBy,\n filteredTemplates,\n availableModels,\n availableUseCases,\n availableRunsOn,\n filteredCount,\n totalCount,\n resetFilters,\n loadFuseOptions\n} = useTemplateFiltering(navigationFilteredTemplates)\n\n/**\n * Coordinates state between the selected navigation item and the sort order to\n * create deterministic, predictable behavior.\n * @param source The origin of the change ('nav' or 'sort').\n */\nconst coordinateNavAndSort = (source: 'nav' | 'sort') => {\n const isPopularNav = selectedNavItem.value === 'popular'\n const isPopularSort = sortBy.value === 'popular'\n\n if (source === 'nav') {\n if (isPopularNav && !isPopularSort) {\n // When navigating to 'Popular' category, automatically set sort to 'Popular'.\n sortBy.value = 'popular'\n } else if (!isPopularNav && isPopularSort) {\n // When navigating away from 'Popular' category while sort is 'Popular', reset sort to default.\n sortBy.value = 'default'\n }\n } else if (source === 'sort') {\n // When sort is changed away from 'Popular' while in the 'Popular' category,\n // reset the category to 'All Templates' to avoid a confusing state.\n if (isPopularNav && !isPopularSort) {\n selectedNavItem.value = 'all'\n }\n }\n}\n\n// Watch for changes from the two sources ('nav' and 'sort') and trigger the coordinator.\nwatch(selectedNavItem, () => coordinateNavAndSort('nav'))\nwatch(sortBy, () => coordinateNavAndSort('sort'))\n\n// Convert between string array and object array for MultiSelect component\nconst selectedModelObjects = computed({\n get() {\n return selectedModels.value.map((model) => ({ name: model, value: model }))\n },\n set(value: { name: string; value: string }[]) {\n selectedModels.value = value.map((item) => item.value)\n }\n})\n\nconst selectedUseCaseObjects = computed({\n get() {\n return selectedUseCases.value.map((useCase) => ({\n name: useCase,\n value: useCase\n }))\n },\n set(value: { name: string; value: string }[]) {\n selectedUseCases.value = value.map((item) => item.value)\n }\n})\n\nconst selectedRunsOnObjects = computed({\n get() {\n return selectedRunsOn.value.map((runsOn) => ({\n name: runsOn,\n value: runsOn\n }))\n },\n set(value: { name: string; value: string }[]) {\n selectedRunsOn.value = value.map((item) => item.value)\n }\n})\n\n// Loading states\nconst loadingTemplate = ref<string | null>(null)\nconst hoveredTemplate = ref<string | null>(null)\nconst cardRefs = ref<HTMLElement[]>([])\n\n// Force re-render key for templates when sorting changes\nconst templateListKey = ref(0)\n\n// Search text for model filter\nconst modelSearchText = ref<string>('')\n\n// Filter options\nconst modelOptions = computed(() =>\n availableModels.value.map((model) => ({\n name: model,\n value: model\n }))\n)\n\nconst useCaseOptions = computed(() =>\n availableUseCases.value.map((useCase) => ({\n name: useCase,\n value: useCase\n }))\n)\n\nconst runsOnOptions = computed(() =>\n availableRunsOn.value.map((runsOn) => ({\n name: runsOn,\n value: runsOn\n }))\n)\n\n// Filter labels\nconst modelFilterLabel = computed(() => {\n if (selectedModelObjects.value.length === 0) {\n return t('templateWorkflows.modelFilter', 'Model Filter')\n } else if (selectedModelObjects.value.length === 1) {\n return selectedModelObjects.value[0].name\n } else {\n return t('templateWorkflows.modelsSelected', {\n count: selectedModelObjects.value.length\n })\n }\n})\n\nconst useCaseFilterLabel = computed(() => {\n if (selectedUseCaseObjects.value.length === 0) {\n return t('templateWorkflows.useCaseFilter', 'Use Case')\n } else if (selectedUseCaseObjects.value.length === 1) {\n return selectedUseCaseObjects.value[0].name\n } else {\n return t('templateWorkflows.useCasesSelected', {\n count: selectedUseCaseObjects.value.length\n })\n }\n})\n\nconst runsOnFilterLabel = computed(() => {\n if (selectedRunsOnObjects.value.length === 0) {\n return t('templateWorkflows.runsOnFilter', 'Runs On')\n } else if (selectedRunsOnObjects.value.length === 1) {\n return selectedRunsOnObjects.value[0].name\n } else {\n return t('templateWorkflows.runsOnSelected', {\n count: selectedRunsOnObjects.value.length\n })\n }\n})\n\n// Sort options\nconst sortOptions = computed(() => [\n {\n name: t('templateWorkflows.sort.default', 'Default'),\n value: 'default'\n },\n {\n name: t('templateWorkflows.sort.recommended', 'Recommended'),\n value: 'recommended'\n },\n {\n name: t('templateWorkflows.sort.popular', 'Popular'),\n value: 'popular'\n },\n { name: t('templateWorkflows.sort.newest', 'Newest'), value: 'newest' },\n {\n name: t('templateWorkflows.sort.vramLowToHigh', 'VRAM Usage (Low to High)'),\n value: 'vram-low-to-high'\n },\n {\n name: t(\n 'templateWorkflows.sort.modelSizeLowToHigh',\n 'Model Size (Low to High)'\n ),\n value: 'model-size-low-to-high'\n },\n {\n name: t('templateWorkflows.sort.alphabetical', 'Alphabetical (A-Z)'),\n value: 'alphabetical'\n }\n])\n\n// Lazy pagination setup\nconst loadTrigger = ref<HTMLElement | null>(null)\nconst shouldUsePagination = computed(() => !searchQuery.value.trim())\n\nconst {\n paginatedItems: paginatedTemplates,\n isLoading: isLoadingMore,\n hasMoreItems: hasMoreTemplates,\n loadNextPage,\n reset: resetPagination\n} = useLazyPagination(filteredTemplates, { itemsPerPage: 24 }) // Load 24 items per page\n\n// Display templates (all when searching, paginated when not)\nconst displayTemplates = computed(() => {\n return shouldUsePagination.value\n ? paginatedTemplates.value\n : filteredTemplates.value\n})\n\n// Set up intersection observer for lazy loading\nuseIntersectionObserver(loadTrigger, () => {\n if (\n shouldUsePagination.value &&\n hasMoreTemplates.value &&\n !isLoadingMore.value\n ) {\n void loadNextPage()\n }\n})\n\n// Reset pagination when filters change\nwatch(\n [\n searchQuery,\n selectedNavItem,\n sortBy,\n selectedModels,\n selectedUseCases,\n selectedRunsOn\n ],\n () => {\n resetPagination()\n // Clear loading state and force re-render of template list\n loadingTemplate.value = null\n templateListKey.value++\n }\n)\n\n// Methods\nconst onLoadWorkflow = async (template: any) => {\n loadingTemplate.value = template.name\n try {\n await loadWorkflowTemplate(\n template.name,\n getEffectiveSourceModule(template)\n )\n templateWasSelected.value = true\n onClose()\n } finally {\n loadingTemplate.value = null\n }\n}\n\nconst pageTitle = computed(() => {\n const navItem = navItems.value.find((item) =>\n 'id' in item\n ? item.id === selectedNavItem.value\n : item.items?.some((sub) => sub.id === selectedNavItem.value)\n )\n\n if (!navItem) {\n return t('templateWorkflows.allTemplates', 'All Templates')\n }\n\n return 'id' in navItem\n ? navItem.label\n : navItem.items?.find((i) => i.id === selectedNavItem.value)?.label ||\n t('templateWorkflows.allTemplates', 'All Templates')\n})\n\n// Initialize templates loading with useAsyncState\nconst { isLoading } = useAsyncState(\n async () => {\n await Promise.all([\n loadTemplates(),\n workflowTemplatesStore.loadWorkflowTemplates(),\n loadFuseOptions()\n ])\n return true\n },\n false, // initial state\n {\n immediate: true // Start loading immediately\n }\n)\n\nconst isTemplateVisibleOnDistribution = (template: TemplateInfo) => {\n return (template.includeOnDistributions?.length ?? 0) > 0\n ? distributions.value.some((d) =>\n template.includeOnDistributions?.includes(d)\n )\n : true\n}\n\nonBeforeUnmount(() => {\n cardRefs.value = [] // Release DOM refs\n})\n</script>\n\n<style>\n/* Ensure the workflow template selector dialog fits within provided dialog */\n.workflow-template-selector-dialog.base-widget-layout {\n width: 100% !important;\n max-width: 1400px;\n height: 100% !important;\n aspect-ratio: auto !important;\n}\n\n@media (min-width: 1600px) {\n .workflow-template-selector-dialog.base-widget-layout {\n max-width: 1600px;\n }\n}\n</style>\n","import WorkflowTemplateSelectorDialog from '@/components/custom/widget/WorkflowTemplateSelectorDialog.vue'\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useDialogService } from '@/services/dialogService'\nimport { useDialogStore } from '@/stores/dialogStore'\n\nconst DIALOG_KEY = 'global-workflow-template-selector'\n\nexport const useWorkflowTemplateSelectorDialog = () => {\n const dialogService = useDialogService()\n const dialogStore = useDialogStore()\n\n function hide() {\n dialogStore.closeDialog({ key: DIALOG_KEY })\n }\n\n function show(source: 'sidebar' | 'menu' | 'command' = 'command') {\n useTelemetry()?.trackTemplateLibraryOpened({ source })\n\n dialogService.showLayoutDialog({\n key: DIALOG_KEY,\n component: WorkflowTemplateSelectorDialog,\n props: {\n onClose: hide\n },\n dialogComponentProps: {\n pt: {\n content: { class: '!px-0 overflow-hidden h-full !py-0' },\n root: {\n style:\n 'width: 90vw; height: 85vh; max-width: 1400px; display: flex;'\n }\n }\n }\n })\n }\n\n return {\n show,\n hide\n }\n}\n","import { useEventListener } from '@vueuse/core'\n\nexport const whileMouseDown = (\n elementOrEvent: HTMLElement | Event,\n callback: (iteration: number) => void,\n interval: number = 30\n) => {\n const element =\n elementOrEvent instanceof HTMLElement\n ? elementOrEvent\n : (elementOrEvent.target as HTMLElement)\n\n let iteration = 0\n\n const intervalId = setInterval(() => {\n callback(iteration++)\n }, interval)\n\n const dispose = () => {\n clearInterval(intervalId)\n disposeGlobal()\n disposeLocal()\n }\n\n // Listen for mouseup globally to catch cases where user drags out of element\n const disposeGlobal = useEventListener(document, 'mouseup', dispose)\n const disposeLocal = useEventListener(element, 'mouseup', dispose)\n\n return {\n dispose: dispose\n }\n}\n","<template>\n <div\n v-tooltip=\"{\n value: t('sideToolbar.labels.menu'),\n showDelay: 300,\n hideDelay: 300\n }\"\n class=\"comfy-menu-button-wrapper flex shrink-0 cursor-pointer flex-col items-center justify-center p-2 transition-colors\"\n :class=\"{\n 'comfy-menu-button-active': menuRef?.visible\n }\"\n @click=\"onLogoMenuClick($event)\"\n >\n <div class=\"flex h-8 w-8 items-center justify-center rounded-lg bg-black\">\n <ComfyLogo\n alt=\"ComfyUI Logo\"\n class=\"comfyui-logo h-[18px] w-[18px] text-white\"\n mode=\"fill\"\n />\n </div>\n </div>\n\n <TieredMenu\n ref=\"menuRef\"\n :model=\"translatedItems\"\n :popup=\"true\"\n class=\"comfy-command-menu\"\n @show=\"onMenuShow\"\n >\n <template #item=\"{ item, props }\">\n <a\n v-if=\"item.key !== 'nodes-2.0-toggle'\"\n class=\"p-menubar-item-link px-4 py-2\"\n v-bind=\"props.action\"\n :href=\"item.url\"\n target=\"_blank\"\n :class=\"typeof item.class === 'function' ? item.class() : item.class\"\n @mousedown=\"\n isZoomCommand(item) ? handleZoomMouseDown(item, $event) : undefined\n \"\n @click=\"handleItemClick(item, $event)\"\n >\n <i\n v-if=\"hasActiveStateSiblings(item)\"\n class=\"p-menubar-item-icon pi pi-check text-sm\"\n :class=\"{ invisible: !item.comfyCommand?.active?.() }\"\n />\n <span\n v-else-if=\"\n item.icon && item.comfyCommand?.id !== 'Comfy.NewBlankWorkflow'\n \"\n class=\"p-menubar-item-icon text-sm\"\n :class=\"item.icon\"\n />\n <span class=\"p-menubar-item-label text-nowrap\">{{ item.label }}</span>\n <i\n v-if=\"item.comfyCommand?.id === 'Comfy.NewBlankWorkflow'\"\n class=\"ml-auto\"\n :class=\"item.icon\"\n />\n <span\n v-if=\"item?.comfyCommand?.keybinding\"\n class=\"keybinding-tag ml-auto rounded border border-surface p-1 text-xs text-nowrap text-muted\"\n >\n {{ item.comfyCommand.keybinding.combo.toString() }}\n </span>\n <i v-if=\"item.items\" class=\"pi pi-angle-right ml-auto\" />\n </a>\n <div\n v-else\n class=\"flex items-center justify-between px-4 py-2\"\n @click.stop=\"handleNodes2ToggleClick\"\n >\n <span class=\"p-menubar-item-label text-nowrap\">{{ item.label }}</span>\n <Tag severity=\"info\" class=\"ml-2 text-xs\">{{ $t('g.beta') }}</Tag>\n <ToggleSwitch\n v-model=\"nodes2Enabled\"\n class=\"ml-4\"\n :aria-label=\"item.label\"\n :pt=\"{\n root: {\n style: {\n width: '38px',\n height: '20px'\n }\n },\n handle: {\n style: {\n width: '16px',\n height: '16px'\n }\n }\n }\"\n @click.stop\n @update:model-value=\"onNodes2ToggleChange\"\n />\n </div>\n </template>\n </TieredMenu>\n</template>\n\n<script setup lang=\"ts\">\nimport type { MenuItem } from 'primevue/menuitem'\nimport Tag from 'primevue/tag'\nimport TieredMenu from 'primevue/tieredmenu'\nimport type { TieredMenuMethods, TieredMenuState } from 'primevue/tieredmenu'\nimport ToggleSwitch from 'primevue/toggleswitch'\nimport { computed, nextTick, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport SettingDialogHeader from '@/components/dialog/header/SettingDialogHeader.vue'\nimport ComfyLogo from '@/components/icons/ComfyLogo.vue'\nimport { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'\nimport SettingDialogContent from '@/platform/settings/components/SettingDialogContent.vue'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useColorPaletteService } from '@/services/colorPaletteService'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { useDialogStore } from '@/stores/dialogStore'\nimport { useMenuItemStore } from '@/stores/menuItemStore'\nimport { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'\nimport { normalizeI18nKey } from '@/utils/formatUtil'\nimport { whileMouseDown } from '@/utils/mouseDownUtil'\nimport { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'\nimport { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'\n\nconst { t } = useI18n()\nconst commandStore = useCommandStore()\nconst menuItemStore = useMenuItemStore()\nconst colorPaletteStore = useColorPaletteStore()\nconst colorPaletteService = useColorPaletteService()\nconst dialogStore = useDialogStore()\nconst managerState = useManagerState()\nconst settingStore = useSettingStore()\n\nconst menuRef = ref<\n ({ dirty: boolean } & TieredMenuMethods & TieredMenuState) | null\n>(null)\n\nconst nodes2Enabled = computed({\n get: () => settingStore.get('Comfy.VueNodes.Enabled') ?? false,\n set: async (value: boolean) => {\n await settingStore.set('Comfy.VueNodes.Enabled', value)\n }\n})\n\nconst telemetry = useTelemetry()\n\nfunction onLogoMenuClick(event: MouseEvent) {\n telemetry?.trackUiButtonClicked({\n button_id: 'sidebar_comfy_menu_opened'\n })\n menuRef.value?.toggle(event)\n}\n\nconst translateMenuItem = (item: MenuItem): MenuItem => {\n const label = typeof item.label === 'function' ? item.label() : item.label\n const translatedLabel = label\n ? t(`menuLabels.${normalizeI18nKey(label)}`, label)\n : undefined\n\n return {\n ...item,\n label: translatedLabel,\n items: item.items?.map(translateMenuItem)\n }\n}\n\nconst showSettings = (defaultPanel?: string) => {\n dialogStore.showDialog({\n key: 'global-settings',\n headerComponent: SettingDialogHeader,\n component: SettingDialogContent,\n props: {\n defaultPanel\n }\n })\n}\n\nconst showManageExtensions = async () => {\n await managerState.openManager({\n initialTab: ManagerTab.All,\n showToastOnLegacyError: false\n })\n}\n\nconst themeMenuItems = computed(() => {\n return colorPaletteStore.palettes.map((palette) => ({\n key: `theme-${palette.id}`,\n label: palette.name,\n parentPath: 'theme',\n comfyCommand: {\n active: () => colorPaletteStore.activePaletteId === palette.id\n },\n command: async () => {\n await colorPaletteService.loadColorPalette(palette.id)\n }\n }))\n})\n\nconst extraMenuItems = computed(() => [\n { separator: true },\n {\n key: 'theme',\n label: t('menu.theme'),\n items: themeMenuItems.value\n },\n {\n key: 'nodes-2.0-toggle',\n label: 'Nodes 2.0'\n },\n { separator: true },\n {\n key: 'browse-templates',\n label: t('menuLabels.Browse Templates'),\n icon: 'icon-[comfy--template]',\n command: () => useWorkflowTemplateSelectorDialog().show('menu')\n },\n {\n key: 'settings',\n label: t('g.settings'),\n icon: 'mdi mdi-cog-outline',\n command: () => {\n telemetry?.trackUiButtonClicked({\n button_id: 'sidebar_settings_menu_opened'\n })\n showSettings()\n }\n },\n {\n key: 'manage-extensions',\n label: t('menu.manageExtensions'),\n icon: 'mdi mdi-puzzle-outline',\n command: showManageExtensions\n }\n])\n\nconst translatedItems = computed(() => {\n const items = menuItemStore.menuItems.map(translateMenuItem)\n let helpIndex = items.findIndex((item) => item.key === 'Help')\n let helpItem: MenuItem | undefined\n\n if (helpIndex !== -1) {\n items[helpIndex].icon = 'mdi mdi-help-circle-outline'\n // If help is not the last item (i.e. we have extension commands), separate them\n const isLastItem = helpIndex !== items.length - 1\n helpItem = items.splice(\n helpIndex,\n 1,\n ...(isLastItem\n ? [\n {\n separator: true\n }\n ]\n : [])\n )[0]\n }\n helpIndex = items.length\n\n items.splice(\n helpIndex,\n 0,\n ...extraMenuItems.value,\n ...(helpItem\n ? [\n {\n separator: true\n },\n helpItem\n ]\n : [])\n )\n\n return items\n})\n\nconst onMenuShow = () => {\n void nextTick(() => {\n // Force the menu to show submenus on hover\n if (menuRef.value) {\n menuRef.value.dirty = true\n }\n })\n}\n\nconst isZoomCommand = (item: MenuItem) => {\n return (\n item.comfyCommand?.id === 'Comfy.Canvas.ZoomIn' ||\n item.comfyCommand?.id === 'Comfy.Canvas.ZoomOut'\n )\n}\n\nconst handleZoomMouseDown = (item: MenuItem, event: MouseEvent) => {\n if (item.comfyCommand) {\n whileMouseDown(\n event,\n async () => {\n await commandStore.execute(item.comfyCommand!.id)\n },\n 50\n )\n }\n}\n\nconst handleItemClick = (item: MenuItem, event: MouseEvent) => {\n // Prevent the menu from closing for zoom commands or commands that have active state\n if (isZoomCommand(item) || item.comfyCommand?.active) {\n event.preventDefault()\n event.stopPropagation()\n if (item.comfyCommand?.active) {\n item.command?.({\n item,\n originalEvent: event\n })\n }\n return false\n }\n}\n\nconst hasActiveStateSiblings = (item: MenuItem): boolean => {\n // Check if this item has siblings with active state (either from store or theme items)\n return (\n item.parentPath &&\n (item.parentPath === 'theme' ||\n menuItemStore.menuItemHasActiveStateChildren[item.parentPath])\n )\n}\n\nconst handleNodes2ToggleClick = () => {\n return false\n}\n\nconst onNodes2ToggleChange = async (value: boolean) => {\n await settingStore.set('Comfy.VueNodes.Enabled', value)\n telemetry?.trackUiButtonClicked({\n button_id: `menu_nodes_2.0_toggle_${value ? 'enabled' : 'disabled'}`\n })\n}\n</script>\n\n<style scoped>\n.comfy-menu-button-wrapper {\n width: var(--sidebar-width);\n height: var(--sidebar-item-height);\n}\n\n.comfy-menu-button-wrapper:hover {\n background: var(--interface-panel-hover-surface);\n}\n\n.comfy-menu-button-active,\n.comfy-menu-button-active:hover {\n background: var(--interface-panel-selected-surface);\n}\n\n.keybinding-tag {\n background: var(--p-content-hover-background);\n border-color: var(--p-content-border-color);\n border-style: solid;\n}\n</style>\n\n<style>\n.comfy-command-menu {\n --p-tieredmenu-item-focus-background: color-mix(\n in srgb,\n var(--fg-color) 15%,\n transparent\n );\n --p-tieredmenu-item-active-background: color-mix(\n in srgb,\n var(--fg-color) 10%,\n transparent\n );\n}\n\n.comfy-command-menu ul {\n background-color: var(--comfy-menu-bg) !important;\n}\n</style>\n","<script setup lang=\"ts\">\nimport Button from '@/components/ui/button/Button.vue'\nimport { t } from '@/i18n'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useCommandStore } from '@/stores/commandStore'\n\nconst canvasStore = useCanvasStore()\n</script>\n<template>\n <div class=\"p-1 bg-secondary-background rounded-lg w-10\">\n <Button\n size=\"icon\"\n :title=\"t('linearMode.linearMode')\"\n :variant=\"canvasStore.linearMode ? 'inverted' : 'secondary'\"\n @click=\"useCommandStore().execute('Comfy.ToggleLinear')\"\n >\n <i class=\"icon-[lucide--panels-top-left]\" />\n </Button>\n <Button\n size=\"icon\"\n :title=\"t('linearMode.graphMode')\"\n :variant=\"canvasStore.linearMode ? 'secondary' : 'inverted'\"\n @click=\"useCommandStore().execute('Comfy.ToggleLinear')\"\n >\n <i class=\"icon-[comfy--workflow]\" />\n </Button>\n </div>\n</template>\n","<template>\n <Button\n v-tooltip=\"{\n value: computedTooltip,\n showDelay: 300,\n hideDelay: 300\n }\"\n :class=\"\n cn(\n 'side-bar-button cursor-pointer border-none',\n selected && 'side-bar-button-selected'\n )\n \"\n variant=\"muted-textonly\"\n :aria-label=\"computedTooltip\"\n @click=\"emit('click', $event)\"\n >\n <div class=\"side-bar-button-content\">\n <slot name=\"icon\">\n <OverlayBadge v-if=\"shouldShowBadge\" :value=\"overlayValue\">\n <i\n v-if=\"typeof icon === 'string'\"\n :class=\"icon + ' side-bar-button-icon'\"\n />\n <component :is=\"icon\" v-else class=\"side-bar-button-icon\" />\n </OverlayBadge>\n <i\n v-else-if=\"typeof icon === 'string'\"\n :class=\"icon + ' side-bar-button-icon'\"\n />\n <component\n :is=\"icon\"\n v-else-if=\"typeof icon === 'object'\"\n class=\"side-bar-button-icon\"\n />\n </slot>\n <span v-if=\"label && !isSmall\" class=\"side-bar-button-label\">{{\n t(label)\n }}</span>\n </div>\n </Button>\n</template>\n\n<script setup lang=\"ts\">\nimport OverlayBadge from 'primevue/overlaybadge'\nimport { computed } from 'vue'\nimport type { Component } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { cn } from '@/utils/tailwindUtil'\n\nconst { t } = useI18n()\nconst {\n icon = '',\n selected = false,\n tooltip = '',\n tooltipSuffix = '',\n iconBadge = '',\n label = '',\n isSmall = false\n} = defineProps<{\n icon?: string | Component\n selected?: boolean\n tooltip?: string\n tooltipSuffix?: string\n iconBadge?: string | (() => string | null)\n label?: string\n isSmall?: boolean\n}>()\n\nconst emit = defineEmits<{\n (e: 'click', event: MouseEvent): void\n}>()\nconst overlayValue = computed(() =>\n typeof iconBadge === 'function' ? (iconBadge() ?? '') : iconBadge\n)\nconst shouldShowBadge = computed(() => !!overlayValue.value)\nconst computedTooltip = computed(() => t(tooltip) + tooltipSuffix)\n</script>\n\n<style>\n.side-bar-button-icon {\n font-size: var(--sidebar-icon-size) !important;\n}\n\n.side-bar-button-selected {\n background-color: var(--interface-panel-selected-surface);\n color: var(--content-hover-fg);\n}\n.side-bar-button:hover {\n background-color: var(--interface-panel-hover-surface);\n color: var(--content-hover-fg);\n}\n\n.side-bar-button-selected .side-bar-button-icon {\n font-size: var(--sidebar-icon-size) !important;\n}\n</style>\n\n<style scoped>\n@reference '../../assets/css/style.css';\n\n.side-bar-button {\n width: var(--sidebar-width);\n height: var(--sidebar-item-height);\n border-radius: 0;\n flex-shrink: 0;\n}\n\n.side-tool-bar-end .side-bar-button {\n height: var(--sidebar-width);\n}\n\n.side-bar-button-content {\n @apply flex flex-col items-center gap-2;\n}\n\n.side-bar-button-label {\n @apply text-[10px] text-center;\n line-height: 1;\n}\n\n.comfyui-body-left .side-bar-button.side-bar-button-selected,\n.comfyui-body-left .side-bar-button.side-bar-button-selected:hover {\n border-left: 4px solid var(--p-button-text-primary-color);\n}\n\n.comfyui-body-right .side-bar-button.side-bar-button-selected,\n.comfyui-body-right .side-bar-button.side-bar-button-selected:hover {\n border-right: 4px solid var(--p-button-text-primary-color);\n}\n</style>\n","<template>\n <SidebarIcon\n icon=\"icon-[ph--terminal-bold]\"\n :label=\"$t('sideToolbar.labels.console')\"\n :tooltip=\"$t('menu.toggleBottomPanel')\"\n :selected=\"bottomPanelStore.activePanel == 'terminal'\"\n @click=\"toggleConsole\"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'\n\nimport SidebarIcon from './SidebarIcon.vue'\n\nconst bottomPanelStore = useBottomPanelStore()\n\n/**\n * Toggle console bottom panel and track UI button click.\n */\nconst toggleConsole = () => {\n useTelemetry()?.trackUiButtonClicked({\n button_id: 'sidebar_bottom_panel_console_toggled'\n })\n bottomPanelStore.toggleBottomPanel()\n}\n</script>\n","<template>\n <SidebarIcon\n icon=\"icon-[lucide--settings]\"\n :label=\"$t('g.settings')\"\n :tooltip=\"tooltipText\"\n @click=\"showSettingsDialog\"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useCommandStore } from '@/stores/commandStore'\n\nimport SidebarIcon from './SidebarIcon.vue'\n\nconst { t } = useI18n()\nconst { getCommand, formatKeySequence } = useCommandStore()\nconst command = getCommand('Comfy.ShowSettingsDialog')\n\nconst tooltipText = computed(\n () => `${t('g.settings')} (${formatKeySequence(command)})`\n)\n\n/**\n * Toggle keyboard shortcuts panel and track UI button click.\n */\nconst showSettingsDialog = () => {\n command.function()\n useTelemetry()?.trackUiButtonClicked({\n button_id: 'sidebar_settings_button_clicked'\n })\n}\n</script>\n","<template>\n <SidebarIcon\n icon=\"icon-[lucide--keyboard]\"\n :label=\"$t('shortcuts.shortcuts')\"\n :tooltip=\"tooltipText\"\n :selected=\"isShortcutsPanelVisible\"\n @click=\"toggleShortcutsPanel\"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'\n\nimport SidebarIcon from './SidebarIcon.vue'\n\nconst { t } = useI18n()\nconst bottomPanelStore = useBottomPanelStore()\nconst commandStore = useCommandStore()\nconst command = commandStore.getCommand('Workspace.ToggleBottomPanel.Shortcuts')\nconst { formatKeySequence } = commandStore\n\nconst isShortcutsPanelVisible = computed(\n () => bottomPanelStore.activePanel === 'shortcuts'\n)\n\nconst tooltipText = computed(\n () => `${t('shortcuts.keyboardShortcuts')} (${formatKeySequence(command)})`\n)\n\n/**\n * Toggle keyboard shortcuts panel and track UI button click.\n */\nconst toggleShortcutsPanel = () => {\n useTelemetry()?.trackUiButtonClicked({\n button_id: 'sidebar_shortcuts_panel_toggled'\n })\n bottomPanelStore.togglePanel('shortcuts')\n}\n</script>\n","<template>\n <SidebarIcon\n icon=\"pi pi-question-circle\"\n class=\"comfy-help-center-btn\"\n :label=\"$t('menu.help')\"\n :tooltip=\"$t('sideToolbar.helpCenter')\"\n :icon-badge=\"shouldShowRedDot ? '•' : ''\"\n :is-small=\"isSmall\"\n @click=\"toggleHelpCenter\"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport { useHelpCenter } from '@/composables/useHelpCenter'\n\nimport SidebarIcon from './SidebarIcon.vue'\n\ndefineProps<{\n isSmall: boolean\n}>()\n\nconst { shouldShowRedDot, toggleHelpCenter } = useHelpCenter()\n</script>\n\n<style scoped>\n:deep(.p-badge) {\n background: #ff3b30;\n color: #ff3b30;\n min-width: 8px;\n height: 8px;\n padding: 0;\n border-radius: 9999px;\n font-size: 0;\n margin-top: 4px;\n margin-right: 4px;\n border: none;\n outline: none;\n box-shadow: none;\n}\n\n:deep(.p-badge.p-badge-dot) {\n width: 8px !important;\n}\n</style>\n","<template>\n <SidebarIcon\n icon=\"pi pi-sign-out\"\n :tooltip=\"tooltip\"\n :label=\"$t('sideToolbar.logout')\"\n @click=\"logout\"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useUserStore } from '@/stores/userStore'\n\nimport SidebarIcon from './SidebarIcon.vue'\n\nconst { t } = useI18n()\nconst userStore = useUserStore()\n\nconst tooltip = computed(\n () => `${t('sideToolbar.logout')} (${userStore.currentUser?.username})`\n)\nconst logout = async () => {\n await userStore.logout()\n window.location.reload()\n}\n</script>\n","<template>\n <SidebarIcon\n icon=\"icon-[comfy--template]\"\n :tooltip=\"$t('sideToolbar.templates')\"\n :label=\"$t('sideToolbar.labels.templates')\"\n :is-small=\"isSmall\"\n class=\"templates-tab-button\"\n @click=\"openTemplates\"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport { useWorkflowTemplateSelectorDialog } from '@/composables/useWorkflowTemplateSelectorDialog'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useTelemetry } from '@/platform/telemetry'\n\nimport SidebarIcon from './SidebarIcon.vue'\n\nconst settingStore = useSettingStore()\n\nconst isSmall = computed(\n () => settingStore.get('Comfy.Sidebar.Size') === 'small'\n)\n\n/**\n * Open templates dialog from sidebar and track UI button click.\n */\nconst openTemplates = () => {\n useTelemetry()?.trackUiButtonClicked({\n button_id: 'sidebar_templates_dialog_opened'\n })\n useWorkflowTemplateSelectorDialog().show('sidebar')\n}\n</script>\n","<template>\n <nav\n ref=\"sideToolbarRef\"\n class=\"side-tool-bar-container flex h-full flex-col items-center bg-transparent [.floating-sidebar]:-mr-2\"\n :class=\"{\n 'small-sidebar': isSmall,\n 'connected-sidebar pointer-events-auto': isConnected,\n 'floating-sidebar': !isConnected,\n 'overflowing-sidebar': isOverflowing,\n 'border-r border-[var(--interface-stroke)] shadow-interface': isConnected\n }\"\n >\n <div\n :class=\"\n isOverflowing\n ? 'side-tool-bar-container overflow-y-auto'\n : 'flex flex-col h-full'\n \"\n >\n <div ref=\"topToolbarRef\" :class=\"groupClasses\">\n <ComfyMenuButton />\n <SidebarIcon\n v-for=\"tab in tabs\"\n :key=\"tab.id\"\n :icon=\"tab.icon\"\n :icon-badge=\"tab.iconBadge\"\n :tooltip=\"tab.tooltip\"\n :tooltip-suffix=\"getTabTooltipSuffix(tab)\"\n :label=\"tab.label || tab.title\"\n :is-small=\"isSmall\"\n :selected=\"tab.id === selectedTab?.id\"\n :class=\"tab.id + '-tab-button'\"\n @click=\"onTabClick(tab)\"\n />\n <SidebarTemplatesButton />\n </div>\n\n <div ref=\"bottomToolbarRef\" class=\"mt-auto\" :class=\"groupClasses\">\n <SidebarLogoutIcon\n v-if=\"userStore.isMultiUserServer\"\n :is-small=\"isSmall\"\n />\n <SidebarHelpCenterIcon v-if=\"!isIntegratedTabBar\" :is-small=\"isSmall\" />\n <SidebarBottomPanelToggleButton :is-small=\"isSmall\" />\n <SidebarShortcutsToggleButton :is-small=\"isSmall\" />\n <SidebarSettingsButton :is-small=\"isSmall\" />\n <ModeToggle\n v-if=\"menuItemStore.hasSeenLinear || flags.linearToggleEnabled\"\n />\n </div>\n </div>\n <HelpCenterPopups :is-small=\"isSmall\" />\n </nav>\n</template>\n\n<script setup lang=\"ts\">\nimport { useResizeObserver } from '@vueuse/core'\nimport { debounce } from 'es-toolkit/compat'\nimport { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'\n\nimport HelpCenterPopups from '@/components/helpcenter/HelpCenterPopups.vue'\nimport ComfyMenuButton from '@/components/sidebar/ComfyMenuButton.vue'\nimport ModeToggle from '@/components/sidebar/ModeToggle.vue'\nimport SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'\nimport SidebarSettingsButton from '@/components/sidebar/SidebarSettingsButton.vue'\nimport SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'\nimport { useFeatureFlags } from '@/composables/useFeatureFlags'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { useKeybindingStore } from '@/stores/keybindingStore'\nimport { useMenuItemStore } from '@/stores/menuItemStore'\nimport { useUserStore } from '@/stores/userStore'\nimport { useWorkspaceStore } from '@/stores/workspaceStore'\nimport type { SidebarTabExtension } from '@/types/extensionTypes'\nimport { cn } from '@/utils/tailwindUtil'\n\nimport SidebarHelpCenterIcon from './SidebarHelpCenterIcon.vue'\nimport SidebarIcon from './SidebarIcon.vue'\nimport SidebarLogoutIcon from './SidebarLogoutIcon.vue'\nimport SidebarTemplatesButton from './SidebarTemplatesButton.vue'\n\nconst workspaceStore = useWorkspaceStore()\nconst settingStore = useSettingStore()\nconst userStore = useUserStore()\nconst commandStore = useCommandStore()\nconst canvasStore = useCanvasStore()\nconst menuItemStore = useMenuItemStore()\nconst sideToolbarRef = ref<HTMLElement>()\nconst topToolbarRef = ref<HTMLElement>()\nconst bottomToolbarRef = ref<HTMLElement>()\nconst { flags } = useFeatureFlags()\n\nconst isSmall = computed(\n () => settingStore.get('Comfy.Sidebar.Size') === 'small'\n)\nconst sidebarLocation = computed<'left' | 'right'>(() =>\n settingStore.get('Comfy.Sidebar.Location')\n)\nconst sidebarStyle = computed(() => settingStore.get('Comfy.Sidebar.Style'))\nconst isIntegratedTabBar = computed(\n () => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'\n)\nconst isConnected = computed(\n () =>\n selectedTab.value ||\n isOverflowing.value ||\n sidebarStyle.value === 'connected'\n)\n\nconst tabs = computed(() => workspaceStore.getSidebarTabs())\nconst selectedTab = computed(() => workspaceStore.sidebarTab.activeSidebarTab)\n\n/**\n * Handle sidebar tab icon click.\n * - Emits UI button telemetry for known tabs\n * - Delegates to the corresponding toggle command\n */\nconst onTabClick = async (item: SidebarTabExtension) => {\n const telemetry = useTelemetry()\n\n const isNodeLibraryTab = item.id === 'node-library'\n const isModelLibraryTab = item.id === 'model-library'\n const isWorkflowsTab = item.id === 'workflows'\n const isAssetsTab = item.id === 'assets'\n\n if (isNodeLibraryTab)\n telemetry?.trackUiButtonClicked({\n button_id: 'sidebar_tab_node_library_selected'\n })\n else if (isModelLibraryTab)\n telemetry?.trackUiButtonClicked({\n button_id: 'sidebar_tab_model_library_selected'\n })\n else if (isWorkflowsTab)\n telemetry?.trackUiButtonClicked({\n button_id: 'sidebar_tab_workflows_selected'\n })\n else if (isAssetsTab)\n telemetry?.trackUiButtonClicked({\n button_id: 'sidebar_tab_assets_media_selected'\n })\n\n await commandStore.commands\n .find((cmd) => cmd.id === `Workspace.ToggleSidebarTab.${item.id}`)\n ?.function?.()\n}\n\nconst keybindingStore = useKeybindingStore()\nconst getTabTooltipSuffix = (tab: SidebarTabExtension) => {\n const keybinding = keybindingStore.getKeybindingByCommandId(\n `Workspace.ToggleSidebarTab.${tab.id}`\n )\n return keybinding ? ` (${keybinding.combo.toString()})` : ''\n}\n\nconst isOverflowing = ref(false)\nconst groupClasses = computed(() =>\n cn(\n 'sidebar-item-group flex flex-col items-center overflow-hidden flex-shrink-0',\n !isConnected.value && 'rounded-lg shadow-interface pointer-events-auto'\n )\n)\n\nconst ENTER_OVERFLOW_MARGIN = 20\nconst EXIT_OVERFLOW_MARGIN = 50\n\nconst checkOverflow = debounce(() => {\n if (!sideToolbarRef.value || !topToolbarRef.value || !bottomToolbarRef.value)\n return\n\n const containerHeight = sideToolbarRef.value.clientHeight\n const topHeight = topToolbarRef.value.scrollHeight\n const bottomHeight = bottomToolbarRef.value.scrollHeight\n const contentHeight = topHeight + bottomHeight\n\n if (isOverflowing.value) {\n isOverflowing.value = containerHeight < contentHeight + EXIT_OVERFLOW_MARGIN\n } else {\n isOverflowing.value =\n containerHeight < contentHeight + ENTER_OVERFLOW_MARGIN\n }\n}, 16)\n\nonMounted(() => {\n if (!sideToolbarRef.value) return\n\n const overflowObserver = useResizeObserver(\n sideToolbarRef.value,\n checkOverflow\n )\n\n checkOverflow()\n\n onBeforeUnmount(() => {\n overflowObserver.stop()\n })\n\n watch(\n [isSmall, sidebarLocation],\n async () => {\n if (canvasStore.canvas) {\n if (sidebarLocation.value === 'left') {\n await nextTick()\n canvasStore.canvas.fpsInfoLocation = [\n sideToolbarRef.value?.getBoundingClientRect()?.right,\n null\n ]\n } else {\n canvasStore.canvas.fpsInfoLocation = null\n }\n canvasStore.canvas.setDirty(false, true)\n }\n },\n { immediate: true }\n )\n})\n</script>\n\n<style>\n/* Global CSS variables for sidebar\n * These variables need to be global (not scoped) because they are used by\n * teleported components like WhatsNewPopup that render outside the sidebar\n * but need to reference sidebar dimensions for proper positioning.\n */\n:root {\n --sidebar-padding: 4px;\n --sidebar-icon-size: 1rem;\n\n --sidebar-default-floating-width: 48px;\n --sidebar-default-connected-width: calc(\n var(--sidebar-default-floating-width) + var(--sidebar-padding) * 2\n );\n --sidebar-default-item-height: 56px;\n\n --sidebar-small-floating-width: 48px;\n --sidebar-small-connected-width: calc(\n var(--sidebar-small-floating-width) + var(--sidebar-padding) * 2\n );\n --sidebar-small-item-height: 48px;\n\n --sidebar-width: var(--sidebar-default-floating-width);\n --sidebar-item-height: var(--sidebar-default-item-height);\n}\n\n:root:has(.side-tool-bar-container.small-sidebar) {\n --sidebar-width: var(--sidebar-small-floating-width);\n --sidebar-item-height: var(--sidebar-small-item-height);\n}\n\n:root:has(.side-tool-bar-container.connected-sidebar) {\n --sidebar-width: var(--sidebar-default-connected-width);\n}\n\n:root:has(.side-tool-bar-container.small-sidebar.connected-sidebar) {\n --sidebar-width: var(--sidebar-small-connected-width);\n}\n</style>\n\n<style scoped>\n@reference \"tailwindcss\";\n\n.floating-sidebar {\n padding: var(--sidebar-padding);\n}\n\n.floating-sidebar .sidebar-item-group {\n border-color: var(--p-panel-border-color);\n}\n\n.connected-sidebar {\n padding: var(--sidebar-padding) 0;\n background-color: var(--comfy-menu-bg);\n}\n\n.sidebar-item-group {\n background-color: var(--comfy-menu-bg);\n border: 1px solid transparent;\n}\n\n.overflowing-sidebar :deep(.comfy-menu-button-wrapper) {\n position: sticky;\n top: 0;\n z-index: 1;\n background-color: var(--comfy-menu-bg);\n}\n</style>\n","import { defineStore } from 'pinia'\nimport { computed } from 'vue'\n\nimport type { TopbarBadge } from '@/types/comfy'\n\nimport { useExtensionStore } from './extensionStore'\n\nexport const useTopbarBadgeStore = defineStore('topbarBadge', () => {\n const extensionStore = useExtensionStore()\n\n const badges = computed<TopbarBadge[]>(() =>\n extensionStore.extensions.flatMap((e) => e.topbarBadges ?? [])\n )\n\n return {\n badges\n }\n})\n","<template>\n <div class=\"flex h-full shrink-0 items-center\">\n <TopbarBadge\n v-for=\"badge in topbarBadgeStore.badges\"\n :key=\"badge.text\"\n :badge\n :display-mode=\"displayMode\"\n :reverse-order=\"reverseOrder\"\n :no-padding=\"noPadding\"\n />\n </div>\n</template>\n\n<script lang=\"ts\" setup>\nimport { breakpointsTailwind, useBreakpoints } from '@vueuse/core'\nimport { computed } from 'vue'\n\nimport { useTopbarBadgeStore } from '@/stores/topbarBadgeStore'\n\nimport TopbarBadge from './TopbarBadge.vue'\n\nwithDefaults(\n defineProps<{\n reverseOrder?: boolean\n noPadding?: boolean\n }>(),\n {\n reverseOrder: false,\n noPadding: false\n }\n)\n\nconst breakpoints = useBreakpoints(breakpointsTailwind)\nconst isXl = breakpoints.greaterOrEqual('xl')\nconst isLg = breakpoints.greaterOrEqual('lg')\n\nconst displayMode = computed<'full' | 'compact' | 'icon-only'>(() => {\n if (isXl.value) return 'full'\n if (isLg.value) return 'compact'\n return 'icon-only'\n})\n\nconst topbarBadgeStore = useTopbarBadgeStore()\n</script>\n","<template>\n <span class=\"relative inline-flex items-center justify-center size-[1em]\">\n <i :class=\"mainIcon\" class=\"text-[1em]\" />\n <i\n :class=\"\n cn(\n subIcon,\n 'absolute leading-none pointer-events-none',\n positionX === 'left' ? 'left-0' : 'right-0',\n positionY === 'top' ? 'top-0' : 'bottom-0'\n )\n \"\n :style=\"subIconStyle\"\n />\n </span>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport { cn } from '@/utils/tailwindUtil'\n\ntype Position = 'top' | 'bottom' | 'left' | 'right'\n\nexport interface OverlayIconProps {\n mainIcon: string\n subIcon: string\n positionX?: Position\n positionY?: Position\n offsetX?: number\n offsetY?: number\n subIconScale?: number\n}\nconst {\n mainIcon,\n subIcon,\n positionX = 'right',\n positionY = 'bottom',\n offsetX = 0,\n offsetY = 0,\n subIconScale = 0.6\n} = defineProps<OverlayIconProps>()\n\nconst textShadow = [\n `-1px -1px 0 rgba(0, 0, 0, 0.7)`,\n `1px -1px 0 rgba(0, 0, 0, 0.7)`,\n `-1px 1px 0 rgba(0, 0, 0, 0.7)`,\n `1px 1px 0 rgba(0, 0, 0, 0.7)`,\n `-1px 0 0 rgba(0, 0, 0, 0.7)`,\n `1px 0 0 rgba(0, 0, 0, 0.7)`,\n `0 -1px 0 rgba(0, 0, 0, 0.7)`,\n `0 1px 0 rgba(0, 0, 0, 0.7)`\n].join(', ')\n\nconst subIconStyle = computed(() => ({\n fontSize: `${subIconScale}em`,\n textShadow,\n ...(offsetX !== 0 && {\n [positionX === 'left' ? 'left' : 'right']: `${offsetX}px`\n }),\n ...(offsetY !== 0 && {\n [positionY === 'top' ? 'top' : 'bottom']: `${offsetY}px`\n })\n}))\n</script>\n","<template>\n <Button\n class=\"comfy-help-center-btn relative text-base-foreground\"\n variant=\"textonly\"\n @click=\"toggleHelpCenter\"\n >\n <div class=\"not-md:hidden\">{{ $t('menu.helpAndFeedback') }}</div>\n <i class=\"icon-[lucide--circle-help] ml-0.5\" />\n <span\n v-if=\"shouldShowRedDot\"\n class=\"absolute top-[7px] right-[7px] size-1.5 rounded-full bg-[#ff3b30]\"\n />\n </Button>\n</template>\n\n<script setup lang=\"ts\">\nimport Button from '@/components/ui/button/Button.vue'\nimport { useHelpCenter } from '@/composables/useHelpCenter'\n\nconst { shouldShowRedDot, toggleHelpCenter } = useHelpCenter('topbar')\n</script>\n","<template>\n <div\n ref=\"positionRef\"\n class=\"absolute bottom-0 left-1/2 -translate-x-1/2\"\n ></div>\n <Popover\n ref=\"popoverRef\"\n append-to=\"body\"\n :pt=\"{\n root: {\n class: 'workflow-popover-fade fit-content',\n 'data-popover-id': id\n }\n }\"\n @mouseenter=\"cancelHidePopover\"\n @mouseleave=\"hidePopover\"\n >\n <div class=\"workflow-preview-content\">\n <div\n v-if=\"thumbnailUrl && !isActiveTab\"\n class=\"workflow-preview-thumbnail relative\"\n >\n <img\n :src=\"thumbnailUrl\"\n class=\"block h-[200px] rounded-lg object-cover p-2\"\n :style=\"{ width: `${POPOVER_WIDTH}px` }\"\n />\n </div>\n <div class=\"workflow-preview-footer\">\n <span class=\"workflow-preview-name\">{{ workflowFilename }}</span>\n </div>\n </div>\n </Popover>\n</template>\n\n<script setup lang=\"ts\">\nimport Popover from 'primevue/popover'\nimport { nextTick, ref, toRefs, useId } from 'vue'\n\nconst POPOVER_WIDTH = 250\n\ninterface Props {\n workflowFilename: string\n thumbnailUrl?: string\n isActiveTab: boolean\n}\n\nconst props = defineProps<Props>()\nconst { thumbnailUrl, isActiveTab } = toRefs(props)\nconst popoverRef = ref<InstanceType<typeof Popover> | null>(null)\nconst positionRef = ref<HTMLElement | null>(null)\nlet hideTimeout: ReturnType<typeof setTimeout> | null = null\nlet showTimeout: ReturnType<typeof setTimeout> | null = null\nconst id = useId()\n\nconst showPopover = (event: Event) => {\n // Clear any existing timeouts\n if (hideTimeout) {\n clearTimeout(hideTimeout)\n hideTimeout = null\n }\n if (showTimeout) {\n clearTimeout(showTimeout)\n showTimeout = null\n }\n\n // Show popover after a short delay\n showTimeout = setTimeout(async () => {\n if (popoverRef.value && positionRef.value) {\n popoverRef.value.show(event, positionRef.value)\n await nextTick()\n // PrimeVue has a bug where when the tabs are scrolled, it positions the element incorrectly\n // Manually set the position to the middle of the tab and prevent it from going off the left/right edge\n const el = document.querySelector(\n `.workflow-popover-fade[data-popover-id=\"${id}\"]`\n ) as HTMLElement\n if (el) {\n const middle = positionRef.value!.getBoundingClientRect().left\n const popoverWidth = el.getBoundingClientRect().width\n const halfWidth = popoverWidth / 2\n let pos = middle - halfWidth\n let shift = 0\n\n // Calculate shift when clamping is needed\n if (pos < 0) {\n shift = pos - 8 // Negative shift to move arrow left\n pos = 8\n } else if (pos + popoverWidth > window.innerWidth) {\n const newPos = window.innerWidth - popoverWidth - 16\n shift = pos - newPos // Positive shift to move arrow right\n pos = newPos\n }\n\n if (shift + halfWidth < 0) {\n shift = -halfWidth + 24\n }\n\n el.style.left = `${pos}px`\n el.style.setProperty('--shift', `${shift}px`)\n }\n }\n }, 200) // 200ms delay before showing\n}\n\nconst cancelHidePopover = () => {\n // Temporarily disable this functionality until we need the popover to be interactive:\n /*\n if (hideTimeout) {\n clearTimeout(hideTimeout)\n hideTimeout = null\n }\n */\n}\n\nconst hidePopover = () => {\n // Clear show timeout if mouse leaves before popover appears\n if (showTimeout) {\n clearTimeout(showTimeout)\n showTimeout = null\n }\n\n hideTimeout = setTimeout(() => {\n if (popoverRef.value) {\n popoverRef.value.hide()\n }\n }, 100) // Minimal delay to allow moving to popover\n}\n\nconst togglePopover = (event: Event) => {\n if (popoverRef.value) {\n popoverRef.value.toggle(event)\n }\n}\n\ndefineExpose({\n showPopover,\n hidePopover,\n togglePopover\n})\n</script>\n\n<style scoped>\n@reference '../../assets/css/style.css';\n\n.workflow-preview-content {\n @apply flex flex-col rounded-xl overflow-hidden;\n max-width: var(--popover-width);\n background-color: var(--comfy-menu-bg);\n color: var(--fg-color);\n}\n\n.workflow-preview-thumbnail {\n @apply relative p-2;\n}\n\n.workflow-preview-thumbnail img {\n @apply shadow-md;\n background-color: color-mix(in srgb, var(--comfy-menu-bg) 70%, black);\n}\n\n.dark-theme .workflow-preview-thumbnail img {\n @apply shadow-lg;\n}\n\n.workflow-preview-footer {\n @apply pt-1 pb-2 px-3;\n}\n\n.workflow-preview-name {\n @apply block text-sm font-medium overflow-hidden text-ellipsis whitespace-nowrap;\n color: var(--fg-color);\n}\n</style>\n\n<style>\n@reference '../../assets/css/style.css';\n\n.workflow-popover-fade {\n --p-popover-background: transparent;\n --p-popover-content-padding: 0;\n @apply bg-transparent rounded-xl shadow-lg;\n transition: opacity 0.15s ease-out !important;\n}\n\n.workflow-popover-fade.p-popover-flipped {\n @apply -translate-y-full;\n}\n\n.dark-theme .workflow-popover-fade {\n @apply shadow-2xl;\n}\n\n.workflow-popover-fade.p-popover::after,\n.workflow-popover-fade.p-popover::before {\n --p-popover-border-color: var(--comfy-menu-secondary-bg);\n left: 50%;\n transform: translateX(calc(-50% + var(--shift)));\n margin-left: 0;\n}\n</style>\n","<template>\n <div\n ref=\"workflowTabRef\"\n class=\"workflow-tab group flex gap-2 p-2\"\n v-bind=\"$attrs\"\n @mouseenter=\"handleMouseEnter\"\n @mouseleave=\"handleMouseLeave\"\n @click=\"handleClick\"\n >\n <i\n v-if=\"workflowOption.workflow.activeState?.extra?.linearMode\"\n class=\"icon-[lucide--panels-top-left] bg-primary-background\"\n />\n <span class=\"workflow-label inline-block max-w-[150px] truncate text-sm\">\n {{ workflowOption.workflow.filename }}\n </span>\n <div class=\"relative\">\n <span\n v-if=\"shouldShowStatusIndicator\"\n class=\"absolute top-1/2 left-1/2 z-10 w-4 -translate-1/2 bg-(--comfy-menu-bg) text-2xl font-bold group-hover:hidden\"\n >•</span\n >\n <Button\n class=\"close-button invisible w-auto p-0\"\n variant=\"muted-textonly\"\n size=\"icon-sm\"\n :aria-label=\"t('g.close')\"\n @click.stop=\"onCloseWorkflow(workflowOption)\"\n >\n <i class=\"pi pi-times\" />\n </Button>\n </div>\n </div>\n\n <WorkflowTabPopover\n ref=\"popoverRef\"\n :workflow-filename=\"workflowOption.workflow.filename\"\n :thumbnail-url=\"thumbnailUrl\"\n :is-active-tab=\"isActiveTab\"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onUnmounted, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport {\n usePragmaticDraggable,\n usePragmaticDroppable\n} from '@/composables/usePragmaticDragAndDrop'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useWorkflowService } from '@/platform/workflow/core/services/workflowService'\nimport type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { useWorkflowThumbnail } from '@/renderer/core/thumbnail/useWorkflowThumbnail'\nimport { useWorkspaceStore } from '@/stores/workspaceStore'\n\nimport WorkflowTabPopover from './WorkflowTabPopover.vue'\n\ninterface WorkflowOption {\n value: string\n workflow: ComfyWorkflow\n}\n\nconst props = defineProps<{\n workflowOption: WorkflowOption\n}>()\n\nconst { t } = useI18n()\n\nconst workspaceStore = useWorkspaceStore()\nconst workflowStore = useWorkflowStore()\nconst settingStore = useSettingStore()\nconst workflowTabRef = ref<HTMLElement | null>(null)\nconst popoverRef = ref<InstanceType<typeof WorkflowTabPopover> | null>(null)\nconst workflowThumbnail = useWorkflowThumbnail()\n\n// Use computed refs to cache autosave settings\nconst autoSaveSetting = computed(() =>\n settingStore.get('Comfy.Workflow.AutoSave')\n)\nconst autoSaveDelay = computed(() =>\n settingStore.get('Comfy.Workflow.AutoSaveDelay')\n)\n\nconst shouldShowStatusIndicator = computed(() => {\n if (workspaceStore.shiftDown) {\n // Branch 1: Shift key is held down, do not show the status indicator.\n return false\n }\n if (!props.workflowOption.workflow.isPersisted) {\n // Branch 2: Workflow is not persisted, show the status indicator.\n return true\n }\n if (props.workflowOption.workflow.isModified) {\n // Branch 3: Workflow is modified.\n if (autoSaveSetting.value === 'off') {\n // Sub-branch 3a: Autosave is off, so show the status indicator.\n return true\n }\n if (autoSaveSetting.value === 'after delay' && autoSaveDelay.value > 3000) {\n // Sub-branch 3b: Autosave delay is too high, so show the status indicator.\n return true\n }\n // Sub-branch 3c: Workflow is modified but no condition applies, do not show the status indicator.\n return false\n }\n // Default: do not show the status indicator. This should not be reachable.\n return false\n})\n\nconst isActiveTab = computed(() => {\n return workflowStore.activeWorkflow?.key === props.workflowOption.workflow.key\n})\n\nconst thumbnailUrl = computed(() => {\n return workflowThumbnail.getThumbnail(props.workflowOption.workflow.key)\n})\n\n// Event handlers that delegate to the popover component\nconst handleMouseEnter = (event: Event) => {\n popoverRef.value?.showPopover(event)\n}\n\nconst handleMouseLeave = () => {\n popoverRef.value?.hidePopover()\n}\n\nconst handleClick = (event: Event) => {\n popoverRef.value?.togglePopover(event)\n}\n\nconst closeWorkflows = async (options: WorkflowOption[]) => {\n for (const opt of options) {\n if (\n !(await useWorkflowService().closeWorkflow(opt.workflow, {\n warnIfUnsaved: !workspaceStore.shiftDown,\n hint: t('sideToolbar.workflowTab.dirtyCloseHint')\n }))\n ) {\n // User clicked cancel\n break\n }\n }\n}\n\nconst onCloseWorkflow = async (option: WorkflowOption) => {\n await closeWorkflows([option])\n}\nconst tabGetter = () => workflowTabRef.value as HTMLElement\n\nusePragmaticDraggable(tabGetter, {\n getInitialData: () => {\n return {\n workflowKey: props.workflowOption.workflow.key\n }\n }\n})\n\nusePragmaticDroppable(tabGetter, {\n getData: () => {\n return {\n workflowKey: props.workflowOption.workflow.key\n }\n },\n onDrop: (e) => {\n const fromIndex = workflowStore.openWorkflows.findIndex(\n (wf) => wf.key === e.source.data.workflowKey\n )\n const toIndex = workflowStore.openWorkflows.findIndex(\n (wf) => wf.key === e.location.current.dropTargets[0]?.data.workflowKey\n )\n if (fromIndex !== toIndex) {\n workflowStore.reorderWorkflows(fromIndex, toIndex)\n }\n }\n})\n\nonUnmounted(() => {\n popoverRef.value?.hidePopover()\n})\n</script>\n\n<style>\n.p-tooltip.workflow-tab-tooltip {\n z-index: 1200 !important;\n}\n</style>\n","<template>\n <div>\n <Button\n v-tooltip=\"{ value: $t('g.moreWorkflows'), showDelay: 300 }\"\n class=\"rounded-none h-full w-auto aspect-square\"\n variant=\"muted-textonly\"\n size=\"icon\"\n :aria-label=\"$t('g.moreWorkflows')\"\n @click=\"menu?.toggle($event)\"\n >\n <i class=\"pi pi-ellipsis-h\" />\n </Button>\n <Menu\n ref=\"menu\"\n :model=\"menuItems\"\n :popup=\"true\"\n class=\"max-h-[40vh] overflow-auto\"\n />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport Menu from 'primevue/menu'\nimport { computed, ref } from 'vue'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { useWorkflowService } from '@/platform/workflow/core/services/workflowService'\nimport type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'\n\nconst props = defineProps<{\n workflows: ComfyWorkflow[]\n activeWorkflow: ComfyWorkflow | null\n}>()\n\nconst menu = ref<InstanceType<typeof Menu> | null>(null)\nconst workflowService = useWorkflowService()\n\nconst menuItems = computed(() =>\n props.workflows.map((workflow: ComfyWorkflow) => ({\n label: workflow.filename,\n icon:\n props.activeWorkflow?.key === workflow.key ? 'pi pi-check' : undefined,\n command: () => {\n void workflowService.openWorkflow(workflow)\n }\n }))\n)\n</script>\n","<template>\n <div\n ref=\"containerRef\"\n class=\"workflow-tabs-container flex h-full max-w-full flex-auto flex-row overflow-hidden\"\n :class=\"{ 'workflow-tabs-container-desktop': isDesktop }\"\n >\n <Button\n v-if=\"showOverflowArrows\"\n variant=\"muted-textonly\"\n size=\"icon\"\n class=\"overflow-arrow overflow-arrow-left h-full w-auto aspect-square\"\n :aria-label=\"$t('g.scrollLeft')\"\n :disabled=\"!leftArrowEnabled\"\n @mousedown=\"whileMouseDown($event, () => scroll(-1))\"\n >\n <i class=\"icon-[lucide--chevron-left] size-full\" />\n </Button>\n <ScrollPanel\n class=\"no-drag overflow-hidden\"\n :pt:content=\"{\n class: 'p-0 w-full flex',\n onwheel: handleWheel\n }\"\n pt:bar-x=\"h-1\"\n >\n <SelectButton\n class=\"workflow-tabs bg-transparent\"\n :class=\"props.class\"\n :model-value=\"selectedWorkflow\"\n :options=\"options\"\n option-label=\"label\"\n data-key=\"value\"\n @update:model-value=\"onWorkflowChange\"\n >\n <template #option=\"{ option }\">\n <WorkflowTab\n :workflow-option=\"option\"\n @contextmenu=\"showContextMenu($event, option)\"\n @click.middle=\"onCloseWorkflow(option)\"\n />\n </template>\n </SelectButton>\n </ScrollPanel>\n <Button\n v-if=\"showOverflowArrows\"\n variant=\"muted-textonly\"\n size=\"icon\"\n class=\"overflow-arrow overflow-arrow-right h-full w-auto aspect-square\"\n :aria-label=\"$t('g.scrollRight')\"\n :disabled=\"!rightArrowEnabled\"\n @mousedown=\"whileMouseDown($event, () => scroll(1))\"\n >\n <i class=\"icon-[lucide--chevron-right] size-full\" />\n </Button>\n <WorkflowOverflowMenu\n v-if=\"showOverflowArrows\"\n :workflows=\"workflowStore.openWorkflows\"\n :active-workflow=\"workflowStore.activeWorkflow\"\n />\n <Button\n v-tooltip=\"{ value: $t('sideToolbar.newBlankWorkflow'), showDelay: 300 }\"\n class=\"new-blank-workflow-button no-drag shrink-0 rounded-none h-full w-auto aspect-square\"\n variant=\"muted-textonly\"\n size=\"icon\"\n :aria-label=\"$t('sideToolbar.newBlankWorkflow')\"\n @click=\"() => commandStore.execute('Comfy.NewBlankWorkflow')\"\n >\n <i class=\"pi pi-plus\" />\n </Button>\n <div\n v-if=\"isIntegratedTabBar\"\n class=\"ml-auto flex shrink-0 items-center gap-2 px-2\"\n >\n <TopMenuHelpButton />\n <CurrentUserButton\n v-if=\"isLoggedIn\"\n :show-arrow=\"false\"\n compact\n class=\"shrink-0 p-1\"\n />\n <LoginButton v-else-if=\"isDesktop\" class=\"p-1\" />\n </div>\n <ContextMenu ref=\"menu\" :model=\"contextMenuItems\">\n <template #itemicon=\"{ item }\">\n <OverlayIcon v-if=\"item.overlayIcon\" v-bind=\"item.overlayIcon\" />\n <i v-else-if=\"item.icon\" :class=\"item.icon\" />\n </template>\n </ContextMenu>\n <div v-if=\"isDesktop\" class=\"window-actions-spacer app-drag shrink-0\" />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useScroll } from '@vueuse/core'\nimport ContextMenu from 'primevue/contextmenu'\nimport ScrollPanel from 'primevue/scrollpanel'\nimport SelectButton from 'primevue/selectbutton'\nimport { computed, nextTick, onUpdated, ref, watch } from 'vue'\nimport type { WatchStopHandle } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport OverlayIcon from '@/components/common/OverlayIcon.vue'\nimport type { OverlayIconProps } from '@/components/common/OverlayIcon.vue'\nimport CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'\nimport LoginButton from '@/components/topbar/LoginButton.vue'\nimport TopMenuHelpButton from '@/components/topbar/TopMenuHelpButton.vue'\nimport WorkflowTab from '@/components/topbar/WorkflowTab.vue'\nimport Button from '@/components/ui/button/Button.vue'\nimport { useCurrentUser } from '@/composables/auth/useCurrentUser'\nimport { useOverflowObserver } from '@/composables/element/useOverflowObserver'\nimport { useWorkflowActionsMenu } from '@/composables/useWorkflowActionsMenu'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useWorkflowService } from '@/platform/workflow/core/services/workflowService'\nimport type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { useWorkspaceStore } from '@/stores/workspaceStore'\nimport { isElectron } from '@/utils/envUtil'\nimport { whileMouseDown } from '@/utils/mouseDownUtil'\n\nimport WorkflowOverflowMenu from './WorkflowOverflowMenu.vue'\n\ninterface WorkflowOption {\n value: string\n workflow: ComfyWorkflow\n}\n\nconst props = defineProps<{\n class?: string\n}>()\n\nconst { t } = useI18n()\nconst settingStore = useSettingStore()\nconst workspaceStore = useWorkspaceStore()\nconst workflowStore = useWorkflowStore()\nconst workflowService = useWorkflowService()\nconst commandStore = useCommandStore()\nconst { isLoggedIn } = useCurrentUser()\n\nconst isIntegratedTabBar = computed(\n () => settingStore.get('Comfy.UI.TabBarLayout') === 'Integrated'\n)\n\nconst rightClickedTab = ref<WorkflowOption | undefined>()\nconst menu = ref()\nconst containerRef = ref<HTMLElement | null>(null)\nconst showOverflowArrows = ref(false)\nconst leftArrowEnabled = ref(false)\nconst rightArrowEnabled = ref(false)\n\nconst isDesktop = isElectron()\n\nconst workflowToOption = (workflow: ComfyWorkflow): WorkflowOption => ({\n value: workflow.path,\n workflow\n})\n\nconst options = computed<WorkflowOption[]>(() =>\n workflowStore.openWorkflows.map(workflowToOption)\n)\nconst selectedWorkflow = computed<WorkflowOption | null>(() =>\n workflowStore.activeWorkflow\n ? workflowToOption(workflowStore.activeWorkflow as ComfyWorkflow)\n : null\n)\n\nconst onWorkflowChange = async (option: WorkflowOption) => {\n // Prevent unselecting the current workflow\n if (!option) {\n return\n }\n // Prevent reloading the current workflow\n if (selectedWorkflow.value?.value === option.value) {\n return\n }\n\n await workflowService.openWorkflow(option.workflow)\n}\n\nconst closeWorkflows = async (options: WorkflowOption[]) => {\n for (const opt of options) {\n if (\n !(await workflowService.closeWorkflow(opt.workflow, {\n warnIfUnsaved: !workspaceStore.shiftDown\n }))\n ) {\n // User clicked cancel\n break\n }\n }\n}\n\nconst onCloseWorkflow = async (option: WorkflowOption) => {\n await closeWorkflows([option])\n}\n\nconst showContextMenu = (event: MouseEvent, option: WorkflowOption) => {\n rightClickedTab.value = option\n menu.value.show(event)\n}\n\nconst rightClickedWorkflow = computed(\n () => rightClickedTab.value?.workflow ?? null\n)\n\nconst { menuItems: baseMenuItems } = useWorkflowActionsMenu(\n () => commandStore.execute('Comfy.RenameWorkflow'),\n {\n includeDelete: false,\n workflow: rightClickedWorkflow\n }\n)\n\nconst contextMenuItems = computed(() => {\n const tab = rightClickedTab.value\n if (!tab) return []\n const index = options.value.findIndex((v) => v.workflow === tab.workflow)\n\n return [\n ...baseMenuItems.value,\n {\n label: t('tabMenu.closeTab'),\n icon: 'pi pi-times',\n command: () => onCloseWorkflow(tab)\n },\n {\n label: t('tabMenu.closeTabsToLeft'),\n overlayIcon: {\n mainIcon: 'pi pi-times',\n subIcon: 'pi pi-arrow-left',\n positionX: 'right',\n positionY: 'bottom',\n subIconScale: 0.5\n } as OverlayIconProps,\n command: () => closeWorkflows(options.value.slice(0, index)),\n disabled: index <= 0\n },\n {\n label: t('tabMenu.closeTabsToRight'),\n overlayIcon: {\n mainIcon: 'pi pi-times',\n subIcon: 'pi pi-arrow-right',\n positionX: 'right',\n positionY: 'bottom',\n subIconScale: 0.5\n } as OverlayIconProps,\n command: () => closeWorkflows(options.value.slice(index + 1)),\n disabled: index === options.value.length - 1\n },\n {\n label: t('tabMenu.closeOtherTabs'),\n overlayIcon: {\n mainIcon: 'pi pi-times',\n subIcon: 'pi pi-arrows-h',\n positionX: 'right',\n positionY: 'bottom',\n subIconScale: 0.5\n } as OverlayIconProps,\n command: () =>\n closeWorkflows([\n ...options.value.slice(index + 1),\n ...options.value.slice(0, index)\n ]),\n disabled: options.value.length <= 1\n }\n ]\n})\n\n// Horizontal scroll on wheel\nconst handleWheel = (event: WheelEvent) => {\n const scrollElement = event.currentTarget as HTMLElement\n const scrollAmount = event.deltaX || event.deltaY\n scrollElement.scroll({\n left: scrollElement.scrollLeft + scrollAmount\n })\n}\n\nconst scrollContent = computed(\n () =>\n (containerRef.value?.querySelector(\n '.p-scrollpanel-content'\n ) as HTMLElement | null) ?? null\n)\n\nconst scroll = (direction: number) => {\n const el = scrollContent.value\n if (!el) return\n el.scrollBy({ left: direction * 20 })\n}\n\nconst ensureActiveTabVisible = async (\n options: { waitForDom?: boolean } = {}\n) => {\n if (!selectedWorkflow.value) return\n\n if (options.waitForDom !== false) {\n await nextTick()\n }\n\n const containerElement = containerRef.value\n if (!containerElement) return\n\n const activeTabElement = containerElement.querySelector(\n '.p-togglebutton-checked'\n )\n if (!activeTabElement) return\n\n activeTabElement.scrollIntoView({ block: 'nearest', inline: 'nearest' })\n}\n\n// Scroll to active offscreen tab when opened\nwatch(\n () => workflowStore.activeWorkflow,\n () => {\n void ensureActiveTabVisible()\n },\n { immediate: true }\n)\n\nlet overflowObserver: ReturnType<typeof useOverflowObserver> | null = null\nlet stopArrivedWatch: WatchStopHandle | null = null\nlet stopOverflowWatch: WatchStopHandle | null = null\n\nwatch(\n scrollContent,\n (el, _prev, onCleanup) => {\n stopArrivedWatch?.()\n stopOverflowWatch?.()\n overflowObserver?.dispose()\n\n if (!el) return\n\n const scrollState = useScroll(el)\n\n stopArrivedWatch = watch(\n [\n () => scrollState.arrivedState.left,\n () => scrollState.arrivedState.right\n ],\n ([atLeft, atRight]) => {\n leftArrowEnabled.value = !atLeft\n rightArrowEnabled.value = !atRight\n },\n { immediate: true }\n )\n\n overflowObserver = useOverflowObserver(el)\n stopOverflowWatch = watch(\n overflowObserver.isOverflowing,\n (isOverflow) => {\n showOverflowArrows.value = isOverflow\n if (!isOverflow) return\n void nextTick(() => {\n // Force a new check after arrows are updated\n scrollState.measure()\n void ensureActiveTabVisible({ waitForDom: false })\n })\n },\n { immediate: true }\n )\n\n onCleanup(() => {\n stopArrivedWatch?.()\n stopOverflowWatch?.()\n overflowObserver?.dispose()\n })\n },\n { immediate: true }\n)\n\nonUpdated(() => {\n if (!overflowObserver?.disposed.value) {\n overflowObserver?.checkOverflow()\n }\n})\n</script>\n\n<style scoped>\n@reference '../../assets/css/style.css';\n\n.workflow-tabs-container {\n background-color: var(--comfy-menu-bg);\n}\n\n:deep(.p-togglebutton) {\n @apply p-0 bg-transparent rounded-none shrink relative border-0 border-r border-solid;\n border-right-color: var(--border-color);\n min-width: 90px;\n}\n\n.overflow-arrow {\n @apply px-2 rounded-none;\n}\n\n.overflow-arrow[disabled] {\n @apply opacity-25;\n}\n\n:deep(.p-togglebutton > .p-togglebutton-content) {\n @apply max-w-full;\n}\n\n:deep(.workflow-tab) {\n @apply max-w-full;\n}\n\n:deep(.p-togglebutton::before) {\n @apply hidden;\n}\n\n:deep(.p-togglebutton:first-child) {\n @apply border-l border-solid;\n border-left-color: var(--border-color);\n}\n\n:deep(.p-togglebutton:not(:first-child)) {\n @apply border-l-0;\n}\n\n:deep(.p-togglebutton.p-togglebutton-checked) {\n @apply border-b border-solid h-full;\n border-bottom-color: var(--p-button-text-primary-color);\n}\n\n:deep(.p-togglebutton:not(.p-togglebutton-checked)) {\n @apply opacity-75;\n}\n\n:deep(.p-togglebutton-checked) .close-button,\n:deep(.p-togglebutton:hover) .close-button {\n @apply visible;\n}\n\n:deep(.p-scrollpanel-content) {\n @apply h-full;\n}\n\n:deep(.workflow-tabs) {\n display: flex;\n}\n\n/* Scrollbar half opacity to avoid blocking the active tab bottom border */\n:deep(.p-scrollpanel:hover .p-scrollpanel-bar),\n:deep(.p-scrollpanel:active .p-scrollpanel-bar) {\n @apply opacity-50;\n}\n\n:deep(.p-selectbutton) {\n @apply rounded-none h-full;\n}\n\n.workflow-tabs-container-desktop {\n max-width: env(titlebar-area-width, 100vw);\n}\n\n.window-actions-spacer {\n @apply flex-auto;\n /* If we are using custom titlebar, then we need to add a gap for the user to drag the window */\n --window-actions-spacer-width: min(75px, env(titlebar-area-width, 0) * 9999);\n min-width: var(--window-actions-spacer-width);\n}\n</style>\n","/**\n * Composable for syncing LiteGraph with the Layout system\n *\n * Implements one-way sync from Layout Store to LiteGraph.\n * The layout store is the single source of truth.\n */\nimport { onUnmounted, ref } from 'vue'\n\nimport type { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { layoutStore } from '@/renderer/core/layout/store/layoutStore'\n\n/**\n * Composable for syncing LiteGraph with the Layout system\n * This replaces the bidirectional sync with a one-way sync\n */\nexport function useLayoutSync() {\n const unsubscribe = ref<() => void>()\n\n /**\n * Start syncing from Layout → LiteGraph\n */\n function startSync(canvas: ReturnType<typeof useCanvasStore>['canvas']) {\n if (!canvas?.graph) return\n\n // Cancel last subscription\n stopSync()\n // Subscribe to layout changes\n unsubscribe.value = layoutStore.onChange((change) => {\n // Apply changes to LiteGraph regardless of source\n // The layout store is the single source of truth\n for (const nodeId of change.nodeIds) {\n const layout = layoutStore.getNodeLayoutRef(nodeId).value\n if (!layout) continue\n\n const liteNode = canvas.graph?.getNodeById(parseInt(nodeId))\n if (!liteNode) continue\n\n if (\n liteNode.pos[0] !== layout.position.x ||\n liteNode.pos[1] !== layout.position.y\n ) {\n liteNode.pos[0] = layout.position.x\n liteNode.pos[1] = layout.position.y\n }\n\n // Note: layout.size.height is the content height without title.\n // LiteGraph's measure() will add titleHeight to get boundingRect.\n // Do NOT use addNodeTitleHeight here - that would double-count the title.\n if (\n liteNode.size[0] !== layout.size.width ||\n liteNode.size[1] !== layout.size.height\n ) {\n // Use setSize() to trigger onResize callback\n liteNode.setSize([layout.size.width, layout.size.height])\n }\n }\n\n // Trigger single redraw for all changes\n canvas.setDirty(true, true)\n })\n }\n\n function stopSync() {\n unsubscribe.value?.()\n unsubscribe.value = undefined\n }\n\n onUnmounted(stopSync)\n\n return {\n startSync,\n stopSync\n }\n}\n","import { createSharedComposable, whenever } from '@vueuse/core'\nimport { shallowRef, watch } from 'vue'\n\nimport { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'\nimport type { GraphNodeManager } from '@/composables/graph/useGraphNodeManager'\nimport { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'\nimport type { LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'\nimport { layoutStore } from '@/renderer/core/layout/store/layoutStore'\nimport { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync'\nimport { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil'\nimport { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale'\nimport { app as comfyApp } from '@/scripts/app'\n\nfunction useVueNodeLifecycleIndividual() {\n const canvasStore = useCanvasStore()\n const layoutMutations = useLayoutMutations()\n const { shouldRenderVueNodes } = useVueFeatureFlags()\n const nodeManager = shallowRef<GraphNodeManager | null>(null)\n const { startSync } = useLayoutSync()\n\n const initializeNodeManager = () => {\n // Use canvas graph if available (handles subgraph contexts), fallback to app graph\n const activeGraph = comfyApp.canvas?.graph\n if (!activeGraph || nodeManager.value) return\n\n // Initialize the core node manager\n const manager = useGraphNodeManager(activeGraph)\n nodeManager.value = manager\n\n // Initialize layout system with existing nodes from active graph\n const nodes = activeGraph._nodes.map((node: LGraphNode) => ({\n id: node.id.toString(),\n pos: [node.pos[0], node.pos[1]] as [number, number],\n size: [node.size[0], removeNodeTitleHeight(node.size[1])] as [\n number,\n number\n ]\n }))\n layoutStore.initializeFromLiteGraph(nodes)\n\n // Seed reroutes into the Layout Store so hit-testing uses the new path\n for (const reroute of activeGraph.reroutes.values()) {\n const [x, y] = reroute.pos\n const parent = reroute.parentId ?? undefined\n const linkIds = Array.from(reroute.linkIds)\n layoutMutations.createReroute(reroute.id, { x, y }, parent, linkIds)\n }\n\n // Seed existing links into the Layout Store (topology only)\n for (const link of activeGraph._links.values()) {\n layoutMutations.createLink(\n link.id,\n link.origin_id,\n link.origin_slot,\n link.target_id,\n link.target_slot\n )\n }\n\n // Initialize layout sync (one-way: Layout Store → LiteGraph)\n startSync(canvasStore.canvas)\n }\n\n const disposeNodeManagerAndSyncs = () => {\n if (!nodeManager.value) return\n\n try {\n nodeManager.value.cleanup()\n } catch {\n /* empty */\n }\n nodeManager.value = null\n }\n\n // Watch for Vue nodes enabled state changes\n watch(\n () => shouldRenderVueNodes.value && Boolean(comfyApp.canvas?.graph),\n (enabled) => {\n if (enabled) {\n initializeNodeManager()\n ensureCorrectLayoutScale(\n comfyApp.canvas?.graph?.extra.workflowRendererVersion\n )\n }\n },\n { immediate: true }\n )\n\n whenever(\n () => !shouldRenderVueNodes.value,\n () => {\n ensureCorrectLayoutScale(\n comfyApp.canvas?.graph?.extra.workflowRendererVersion\n )\n disposeNodeManagerAndSyncs()\n comfyApp.canvas?.setDirty(true, true)\n }\n )\n\n // Consolidated watch for slot layout sync management\n watch(\n () => shouldRenderVueNodes.value,\n (vueMode, oldVueMode) => {\n const modeChanged = vueMode !== oldVueMode\n\n // Clear stale slot layouts when switching modes\n if (modeChanged) {\n layoutStore.clearAllSlotLayouts()\n }\n },\n { immediate: true, flush: 'sync' }\n )\n\n // Handle case where Vue nodes are enabled but graph starts empty\n const setupEmptyGraphListener = () => {\n const activeGraph = comfyApp.canvas?.graph\n if (\n !shouldRenderVueNodes.value ||\n nodeManager.value ||\n activeGraph?._nodes.length !== 0\n ) {\n return\n }\n const originalOnNodeAdded = activeGraph.onNodeAdded\n activeGraph.onNodeAdded = function (node: LGraphNode) {\n // Restore original handler\n activeGraph.onNodeAdded = originalOnNodeAdded\n\n // Initialize node manager if needed\n if (shouldRenderVueNodes.value && !nodeManager.value) {\n initializeNodeManager()\n }\n\n // Call original handler\n if (originalOnNodeAdded) {\n originalOnNodeAdded.call(this, node)\n }\n }\n }\n\n // Cleanup function for component unmounting\n const cleanup = () => {\n if (nodeManager.value) {\n nodeManager.value.cleanup()\n nodeManager.value = null\n }\n }\n\n return {\n nodeManager,\n\n // Lifecycle methods\n initializeNodeManager,\n disposeNodeManagerAndSyncs,\n setupEmptyGraphListener,\n cleanup\n }\n}\n\nexport const useVueNodeLifecycle = createSharedComposable(\n useVueNodeLifecycleIndividual\n)\n","import { formatCreditsFromUsd } from '@/base/credits/comfyCredits'\nimport type { INodeInputSlot, LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport type { IComboWidget } from '@/lib/litegraph/src/types/widgets'\n\n/**\n * Meshy credit pricing constant.\n * 1 Meshy credit = $0.04 USD\n * Change this value to update all Meshy node prices.\n */\nconst MESHY_CREDIT_PRICE_USD = 0.04\n\n/** Convert Meshy credits to USD */\nconst meshyCreditsToUsd = (credits: number): number =>\n credits * MESHY_CREDIT_PRICE_USD\n\nconst DEFAULT_NUMBER_OPTIONS: Intl.NumberFormatOptions = {\n minimumFractionDigits: 0,\n maximumFractionDigits: 0\n}\n\ntype CreditFormatOptions = {\n suffix?: string\n note?: string\n approximate?: boolean\n separator?: string\n}\n\nconst formatCreditsValue = (usd: number): string =>\n formatCreditsFromUsd({\n usd,\n numberOptions: DEFAULT_NUMBER_OPTIONS\n })\n\nconst makePrefix = (approximate?: boolean) => (approximate ? '~' : '')\n\nconst makeSuffix = (suffix?: string) => suffix ?? '/Run'\n\nconst appendNote = (note?: string) => (note ? ` ${note}` : '')\n\nconst formatCreditsLabel = (\n usd: number,\n { suffix, note, approximate }: CreditFormatOptions = {}\n): string =>\n `${makePrefix(approximate)}${formatCreditsValue(usd)} credits${makeSuffix(suffix)}${appendNote(note)}`\n\nconst formatCreditsRangeLabel = (\n minUsd: number,\n maxUsd: number,\n { suffix, note, approximate }: CreditFormatOptions = {}\n): string => {\n const min = formatCreditsValue(minUsd)\n const max = formatCreditsValue(maxUsd)\n const rangeValue = min === max ? min : `${min}-${max}`\n return `${makePrefix(approximate)}${rangeValue} credits${makeSuffix(suffix)}${appendNote(note)}`\n}\n\nconst formatCreditsListLabel = (\n usdValues: number[],\n { suffix, note, approximate, separator }: CreditFormatOptions = {}\n): string => {\n const parts = usdValues.map((value) => formatCreditsValue(value))\n const value = parts.join(separator ?? '/')\n return `${makePrefix(approximate)}${value} credits${makeSuffix(suffix)}${appendNote(note)}`\n}\n\n/**\n * Function that calculates dynamic pricing based on node widget values\n */\ntype PricingFunction = (node: LGraphNode) => string\n\n/**\n * Safely executes a pricing function with error handling\n * Returns null if the function throws an error, allowing the node to still render\n */\nfunction safePricingExecution(\n fn: PricingFunction,\n node: LGraphNode,\n fallback: string = ''\n): string {\n try {\n return fn(node)\n } catch (error) {\n // Log error in development but don't throw to avoid breaking node rendering\n if (process.env.NODE_ENV === 'development') {\n console.warn(\n 'Pricing calculation failed for node:',\n node.constructor?.nodeData?.name,\n error\n )\n }\n return fallback\n }\n}\n\n/**\n * Helper function to calculate Runway duration-based pricing\n * @param node - The LiteGraph node\n * @returns Formatted price string\n */\nconst calculateRunwayDurationPrice = (node: LGraphNode): string => {\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n\n if (!durationWidget) return formatCreditsLabel(0.0715, { suffix: '/second' })\n\n const duration = Number(durationWidget.value)\n const validDuration = isNaN(duration) ? 5 : duration\n const cost = 0.0715 * validDuration\n return formatCreditsLabel(cost)\n}\n\nconst makeOmniProDurationCalculator =\n (pricePerSecond: number): PricingFunction =>\n (node: LGraphNode): string => {\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n if (!durationWidget)\n return formatCreditsLabel(pricePerSecond, { suffix: '/second' })\n\n const seconds = parseFloat(String(durationWidget.value))\n if (!Number.isFinite(seconds))\n return formatCreditsLabel(pricePerSecond, { suffix: '/second' })\n\n const cost = pricePerSecond * seconds\n return formatCreditsLabel(cost)\n }\n\nconst klingMotionControlPricingCalculator: PricingFunction = (\n node: LGraphNode\n): string => {\n const modeWidget = node.widgets?.find(\n (w) => w.name === 'mode'\n ) as IComboWidget\n\n if (!modeWidget) {\n return formatCreditsListLabel([0.07, 0.112], {\n suffix: '/second',\n note: '(std/pro)'\n })\n }\n\n const mode = String(modeWidget.value).toLowerCase()\n\n if (mode === 'pro') return formatCreditsLabel(0.112, { suffix: '/second' })\n if (mode === 'std') return formatCreditsLabel(0.07, { suffix: '/second' })\n\n return formatCreditsListLabel([0.07, 0.112], {\n suffix: '/second',\n note: '(std/pro)'\n })\n}\n\nconst pixversePricingCalculator = (node: LGraphNode): string => {\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration_seconds'\n ) as IComboWidget\n const qualityWidget = node.widgets?.find(\n (w) => w.name === 'quality'\n ) as IComboWidget\n const motionModeWidget = node.widgets?.find(\n (w) => w.name === 'motion_mode'\n ) as IComboWidget\n\n if (!durationWidget || !qualityWidget) {\n return formatCreditsRangeLabel(0.45, 1.2, {\n note: '(varies with duration, quality & motion mode)'\n })\n }\n\n const duration = String(durationWidget.value)\n const quality = String(qualityWidget.value)\n const motionMode = String(motionModeWidget?.value)\n\n // Basic pricing based on duration and quality\n if (duration.includes('5')) {\n if (quality.includes('1080p')) return formatCreditsLabel(1.2)\n if (quality.includes('720p') && motionMode?.includes('fast'))\n return formatCreditsLabel(1.2)\n if (quality.includes('720p') && motionMode?.includes('normal'))\n return formatCreditsLabel(0.6)\n if (quality.includes('540p') && motionMode?.includes('fast'))\n return formatCreditsLabel(0.9)\n if (quality.includes('540p') && motionMode?.includes('normal'))\n return formatCreditsLabel(0.45)\n if (quality.includes('360p') && motionMode?.includes('fast'))\n return formatCreditsLabel(0.9)\n if (quality.includes('360p') && motionMode?.includes('normal'))\n return formatCreditsLabel(0.45)\n } else if (duration.includes('8')) {\n if (quality.includes('540p') && motionMode?.includes('normal'))\n return formatCreditsLabel(0.9)\n if (quality.includes('540p') && motionMode?.includes('fast'))\n return formatCreditsLabel(1.2)\n if (quality.includes('360p') && motionMode?.includes('normal'))\n return formatCreditsLabel(0.9)\n if (quality.includes('360p') && motionMode?.includes('fast'))\n return formatCreditsLabel(1.2)\n if (quality.includes('1080p') && motionMode?.includes('normal'))\n return formatCreditsLabel(1.2)\n if (quality.includes('1080p') && motionMode?.includes('fast'))\n return formatCreditsLabel(1.2)\n if (quality.includes('720p') && motionMode?.includes('normal'))\n return formatCreditsLabel(1.2)\n if (quality.includes('720p') && motionMode?.includes('fast'))\n return formatCreditsLabel(1.2)\n }\n\n return formatCreditsLabel(0.9)\n}\n\nconst byteDanceVideoPricingCalculator = (node: LGraphNode): string => {\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n const resolutionWidget = node.widgets?.find(\n (w) => w.name === 'resolution'\n ) as IComboWidget\n const generateAudioWidget = node.widgets?.find(\n (w) => w.name === 'generate_audio'\n ) as IComboWidget | undefined\n\n if (!modelWidget || !durationWidget || !resolutionWidget) return 'Token-based'\n\n const model = String(modelWidget.value).toLowerCase()\n const resolution = String(resolutionWidget.value).toLowerCase()\n const seconds = parseFloat(String(durationWidget.value))\n const generateAudio =\n generateAudioWidget &&\n String(generateAudioWidget.value).toLowerCase() === 'true'\n const priceByModel: Record<string, Record<string, [number, number]>> = {\n 'seedance-1-5-pro': {\n '480p': [0.12, 0.12],\n '720p': [0.26, 0.26],\n '1080p': [0.58, 0.59]\n },\n 'seedance-1-0-pro': {\n '480p': [0.23, 0.24],\n '720p': [0.51, 0.56],\n '1080p': [1.18, 1.22]\n },\n 'seedance-1-0-pro-fast': {\n '480p': [0.09, 0.1],\n '720p': [0.21, 0.23],\n '1080p': [0.47, 0.49]\n },\n 'seedance-1-0-lite': {\n '480p': [0.17, 0.18],\n '720p': [0.37, 0.41],\n '1080p': [0.85, 0.88]\n }\n }\n\n const modelKey = model.includes('seedance-1-5-pro')\n ? 'seedance-1-5-pro'\n : model.includes('seedance-1-0-pro-fast')\n ? 'seedance-1-0-pro-fast'\n : model.includes('seedance-1-0-pro')\n ? 'seedance-1-0-pro'\n : model.includes('seedance-1-0-lite')\n ? 'seedance-1-0-lite'\n : ''\n\n const resKey = resolution.includes('1080')\n ? '1080p'\n : resolution.includes('720')\n ? '720p'\n : resolution.includes('480')\n ? '480p'\n : ''\n\n const baseRange =\n modelKey && resKey ? priceByModel[modelKey]?.[resKey] : undefined\n if (!baseRange) return 'Token-based'\n\n const [min10s, max10s] = baseRange\n const scale = seconds / 10\n const audioMultiplier =\n modelKey === 'seedance-1-5-pro' && generateAudio ? 2 : 1\n const minCost = min10s * scale * audioMultiplier\n const maxCost = max10s * scale * audioMultiplier\n\n if (minCost === maxCost) return formatCreditsLabel(minCost)\n return formatCreditsRangeLabel(minCost, maxCost)\n}\n\nconst ltxvPricingCalculator = (node: LGraphNode): string => {\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n const resolutionWidget = node.widgets?.find(\n (w) => w.name === 'resolution'\n ) as IComboWidget\n\n const fallback = formatCreditsRangeLabel(0.04, 0.24, {\n suffix: '/second'\n })\n if (!modelWidget || !durationWidget || !resolutionWidget) return fallback\n\n const model = String(modelWidget.value).toLowerCase()\n const resolution = String(resolutionWidget.value).toLowerCase()\n const seconds = parseFloat(String(durationWidget.value))\n const priceByModel: Record<string, Record<string, number>> = {\n 'ltx-2 (pro)': {\n '1920x1080': 0.06,\n '2560x1440': 0.12,\n '3840x2160': 0.24\n },\n 'ltx-2 (fast)': {\n '1920x1080': 0.04,\n '2560x1440': 0.08,\n '3840x2160': 0.16\n }\n }\n\n const modelTable = priceByModel[model]\n if (!modelTable) return fallback\n\n const pps = modelTable[resolution]\n if (!pps) return fallback\n\n const cost = pps * seconds\n return formatCreditsLabel(cost)\n}\n\nconst klingVideoWithAudioPricingCalculator: PricingFunction = (\n node: LGraphNode\n): string => {\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n const generateAudioWidget = node.widgets?.find(\n (w) => w.name === 'generate_audio'\n ) as IComboWidget\n\n if (!durationWidget || !generateAudioWidget) {\n return formatCreditsRangeLabel(0.35, 1.4, {\n note: '(varies with duration & audio)'\n })\n }\n\n const duration = String(durationWidget.value)\n const generateAudio =\n String(generateAudioWidget.value).toLowerCase() === 'true'\n\n if (duration === '5') {\n return generateAudio ? formatCreditsLabel(0.7) : formatCreditsLabel(0.35)\n }\n\n if (duration === '10') {\n return generateAudio ? formatCreditsLabel(1.4) : formatCreditsLabel(0.7)\n }\n\n // Fallback for unexpected duration values\n return formatCreditsRangeLabel(0.35, 1.4, {\n note: '(varies with duration & audio)'\n })\n}\n\n// ---- constants ----\nconst SORA_SIZES = {\n BASIC: new Set(['720x1280', '1280x720']),\n PRO: new Set(['1024x1792', '1792x1024'])\n}\nconst ALL_SIZES = new Set([...SORA_SIZES.BASIC, ...SORA_SIZES.PRO])\n\n// ---- sora-2 pricing helpers ----\nfunction validateSora2Selection(\n modelRaw: string,\n duration: number,\n sizeRaw: string\n): string | undefined {\n const model = modelRaw?.toLowerCase() ?? ''\n const size = sizeRaw?.toLowerCase() ?? ''\n\n if (!duration || Number.isNaN(duration)) return 'Set duration (4s / 8s / 12s)'\n if (!size) return 'Set size (720x1280, 1280x720, 1024x1792, 1792x1024)'\n if (!ALL_SIZES.has(size))\n return 'Invalid size. Must be 720x1280, 1280x720, 1024x1792, or 1792x1024.'\n\n if (model.includes('sora-2-pro')) return undefined\n\n if (model.includes('sora-2') && !SORA_SIZES.BASIC.has(size))\n return 'sora-2 supports only 720x1280 or 1280x720'\n\n if (!model.includes('sora-2')) return 'Unsupported model'\n\n return undefined\n}\n\nfunction perSecForSora2(modelRaw: string, sizeRaw: string): number {\n const model = modelRaw?.toLowerCase() ?? ''\n const size = sizeRaw?.toLowerCase() ?? ''\n\n if (model.includes('sora-2-pro')) {\n return SORA_SIZES.PRO.has(size) ? 0.5 : 0.3\n }\n if (model.includes('sora-2')) return 0.1\n\n return SORA_SIZES.PRO.has(size) ? 0.5 : 0.1\n}\n\nfunction formatRunPrice(perSec: number, duration: number) {\n return formatCreditsLabel(Number((perSec * duration).toFixed(2)))\n}\n\n// ---- pricing calculator ----\nconst sora2PricingCalculator: PricingFunction = (node: LGraphNode): string => {\n const getWidgetValue = (name: string) =>\n String(node.widgets?.find((w) => w.name === name)?.value ?? '')\n\n const model = getWidgetValue('model')\n const size = getWidgetValue('size')\n const duration = Number(\n node.widgets?.find((w) => ['duration', 'duration_s'].includes(w.name))\n ?.value\n )\n\n if (!model || !size || !duration) return 'Set model, duration & size'\n\n const validationError = validateSora2Selection(model, duration, size)\n if (validationError) return validationError\n\n const perSec = perSecForSora2(model, size)\n return formatRunPrice(perSec, duration)\n}\n\n/**\n * Pricing for Tripo 3D generation nodes (Text / Image / Multiview)\n * based on Tripo credits:\n *\n * Turbo / V3 / V2.5 / V2.0:\n * Text -> 10 (no texture) / 20 (standard texture)\n * Image -> 20 (no texture) / 30 (standard texture)\n * Multiview -> 20 (no texture) / 30 (standard texture)\n *\n * V1.4:\n * Text -> 20\n * Image -> 30\n * (Multiview treated same as Image if used)\n *\n * Advanced extras (added on top of generation credits):\n * quad -> +5 credits\n * style -> +5 credits (if style != \"None\")\n * HD texture -> +10 credits (texture_quality = \"detailed\")\n * detailed geometry -> +20 credits (geometry_quality = \"detailed\")\n *\n * 1 credit = $0.01\n */\nconst calculateTripo3DGenerationPrice = (\n node: LGraphNode,\n task: 'text' | 'image' | 'multiview'\n): string => {\n const getWidget = (name: string): IComboWidget | undefined =>\n node.widgets?.find((w) => w.name === name) as IComboWidget | undefined\n\n const getString = (name: string, defaultValue: string): string => {\n const widget = getWidget(name)\n if (!widget || widget.value === undefined || widget.value === null) {\n return defaultValue\n }\n return String(widget.value)\n }\n\n const getBool = (name: string, defaultValue: boolean): boolean => {\n const widget = getWidget(name)\n if (!widget || widget.value === undefined || widget.value === null) {\n return defaultValue\n }\n\n const v = widget.value\n if (typeof v === 'number') return v !== 0\n const lower = String(v).toLowerCase()\n if (lower === 'true') return true\n if (lower === 'false') return false\n\n return defaultValue\n }\n\n // ---- read widget values with sensible defaults (mirroring backend) ----\n const modelVersionRaw = getString('model_version', '').toLowerCase()\n if (modelVersionRaw === '')\n return formatCreditsRangeLabel(0.1, 0.65, {\n note: '(varies with quad, style, texture & quality)'\n })\n const styleRaw = getString('style', 'None')\n const hasStyle = styleRaw.toLowerCase() !== 'none'\n\n // Backend defaults: texture=true, pbr=true, quad=false, qualities=\"standard\"\n const hasTexture = getBool('texture', false)\n const hasPbr = getBool('pbr', false)\n const quad = getBool('quad', false)\n\n const textureQualityRaw = getString(\n 'texture_quality',\n 'standard'\n ).toLowerCase()\n const geometryQualityRaw = getString(\n 'geometry_quality',\n 'standard'\n ).toLowerCase()\n\n const isHdTexture = textureQualityRaw === 'detailed'\n const isDetailedGeometry = geometryQualityRaw === 'detailed'\n\n const withTexture = hasTexture || hasPbr\n\n let baseCredits: number\n\n if (modelVersionRaw.includes('v1.4')) {\n // V1.4 model: Text=20, Image=30, Refine=30\n if (task === 'text') {\n baseCredits = 20\n } else {\n // treat Multiview same as Image if V1.4 is ever used there\n baseCredits = 30\n }\n } else {\n // V3.0, V2.5, V2.0 models\n if (!withTexture) {\n if (task === 'text') {\n baseCredits = 10 // Text to 3D without texture\n } else {\n baseCredits = 20 // Image/Multiview to 3D without texture\n }\n } else {\n if (task === 'text') {\n baseCredits = 20 // Text to 3D with standard texture\n } else {\n baseCredits = 30 // Image/Multiview to 3D with standard texture\n }\n }\n }\n\n // ---- advanced extras on top of base generation ----\n let credits = baseCredits\n\n if (hasStyle) credits += 5 // Style\n if (quad) credits += 5 // Quad Topology\n if (isHdTexture) credits += 10 // HD Texture\n if (isDetailedGeometry) credits += 20 // Detailed Geometry Quality\n\n const dollars = credits * 0.01\n return formatCreditsLabel(dollars)\n}\n\n/**\n * Meshy Image to 3D pricing calculator.\n * Pricing based on should_texture widget:\n * - Without texture: 20 credits\n * - With texture: 30 credits\n */\nconst calculateMeshyImageToModelPrice = (node: LGraphNode): string => {\n const shouldTextureWidget = node.widgets?.find(\n (w) => w.name === 'should_texture'\n ) as IComboWidget\n\n if (!shouldTextureWidget) {\n return formatCreditsRangeLabel(\n meshyCreditsToUsd(20),\n meshyCreditsToUsd(30),\n { note: '(varies with texture)' }\n )\n }\n\n const shouldTexture = String(shouldTextureWidget.value).toLowerCase()\n const credits = shouldTexture === 'true' ? 30 : 20\n return formatCreditsLabel(meshyCreditsToUsd(credits))\n}\n\n/**\n * Meshy Multi-Image to 3D pricing calculator.\n * Pricing based on should_texture widget:\n * - Without texture: 5 credits\n * - With texture: 15 credits\n */\nconst calculateMeshyMultiImageToModelPrice = (node: LGraphNode): string => {\n const shouldTextureWidget = node.widgets?.find(\n (w) => w.name === 'should_texture'\n ) as IComboWidget\n\n if (!shouldTextureWidget) {\n return formatCreditsRangeLabel(\n meshyCreditsToUsd(5),\n meshyCreditsToUsd(15),\n { note: '(varies with texture)' }\n )\n }\n\n const shouldTexture = String(shouldTextureWidget.value).toLowerCase()\n const credits = shouldTexture === 'true' ? 15 : 5\n return formatCreditsLabel(meshyCreditsToUsd(credits))\n}\n\n/**\n * Static pricing data for API nodes, now supporting both strings and functions\n */\nconst apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =\n {\n FluxProCannyNode: {\n displayPrice: formatCreditsLabel(0.05)\n },\n FluxProDepthNode: {\n displayPrice: formatCreditsLabel(0.05)\n },\n FluxProExpandNode: {\n displayPrice: formatCreditsLabel(0.05)\n },\n FluxProFillNode: {\n displayPrice: formatCreditsLabel(0.05)\n },\n FluxProUltraImageNode: {\n displayPrice: formatCreditsLabel(0.06)\n },\n FluxProKontextProNode: {\n displayPrice: formatCreditsLabel(0.04)\n },\n FluxProKontextMaxNode: {\n displayPrice: formatCreditsLabel(0.08)\n },\n Flux2ProImageNode: {\n displayPrice: (node: LGraphNode): string => {\n const widthW = node.widgets?.find(\n (w) => w.name === 'width'\n ) as IComboWidget\n const heightW = node.widgets?.find(\n (w) => w.name === 'height'\n ) as IComboWidget\n\n const w = Number(widthW?.value)\n const h = Number(heightW?.value)\n if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {\n // global min/max for this node given schema bounds (1MP..4MP output)\n return formatCreditsRangeLabel(0.03, 0.15)\n }\n\n // Is the 'images' input connected?\n const imagesInput = node.inputs?.find(\n (i) => i.name === 'images'\n ) as INodeInputSlot\n const hasRefs =\n typeof imagesInput?.link !== 'undefined' && imagesInput.link != null\n\n // Output cost: ceil((w*h)/MP); first MP $0.03, each additional $0.015\n const MP = 1024 * 1024\n const outMP = Math.max(1, Math.floor((w * h + MP - 1) / MP))\n const outputCost = 0.03 + 0.015 * Math.max(outMP - 1, 0)\n\n if (hasRefs) {\n // Unknown ref count/size on the frontend:\n // min extra is $0.015, max extra is $0.120 (8 MP cap / 8 refs)\n const minTotal = outputCost + 0.015\n const maxTotal = outputCost + 0.12\n return formatCreditsRangeLabel(minTotal, maxTotal, {\n approximate: true\n })\n }\n\n // Precise text-to-image price\n return formatCreditsLabel(outputCost)\n }\n },\n Flux2MaxImageNode: {\n displayPrice: (node: LGraphNode): string => {\n const widthW = node.widgets?.find(\n (w) => w.name === 'width'\n ) as IComboWidget\n const heightW = node.widgets?.find(\n (w) => w.name === 'height'\n ) as IComboWidget\n\n const w = Number(widthW?.value)\n const h = Number(heightW?.value)\n if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {\n // global min/max for this node given schema bounds (1MP..4MP output)\n return formatCreditsRangeLabel(0.07, 0.35)\n }\n\n // Is the 'images' input connected?\n const imagesInput = node.inputs?.find(\n (i) => i.name === 'images'\n ) as INodeInputSlot\n const hasRefs =\n typeof imagesInput?.link !== 'undefined' && imagesInput.link != null\n\n // Output cost: ceil((w*h)/MP); first MP $0.07, each additional $0.03\n const MP = 1024 * 1024\n const outMP = Math.max(1, Math.floor((w * h + MP - 1) / MP))\n const outputCost = 0.07 + 0.03 * Math.max(outMP - 1, 0)\n\n if (hasRefs) {\n // Unknown ref count/size on the frontend:\n // min extra is $0.03, max extra is $0.24 (8 MP cap / 8 refs)\n const minTotal = outputCost + 0.03\n const maxTotal = outputCost + 0.24\n return formatCreditsRangeLabel(minTotal, maxTotal)\n }\n\n return formatCreditsLabel(outputCost)\n }\n },\n OpenAIVideoSora2: {\n displayPrice: sora2PricingCalculator\n },\n IdeogramV1: {\n displayPrice: (node: LGraphNode): string => {\n const numImagesWidget = node.widgets?.find(\n (w) => w.name === 'num_images'\n ) as IComboWidget\n const turboWidget = node.widgets?.find(\n (w) => w.name === 'turbo'\n ) as IComboWidget\n\n if (!numImagesWidget)\n return formatCreditsRangeLabel(0.03, 0.09, {\n suffix: ' x num_images/Run'\n })\n\n const numImages = Number(numImagesWidget.value) || 1\n const turbo = String(turboWidget?.value).toLowerCase() === 'true'\n const basePrice = turbo ? 0.0286 : 0.0858\n const cost = Number((basePrice * numImages).toFixed(2))\n return formatCreditsLabel(cost)\n }\n },\n IdeogramV2: {\n displayPrice: (node: LGraphNode): string => {\n const numImagesWidget = node.widgets?.find(\n (w) => w.name === 'num_images'\n ) as IComboWidget\n const turboWidget = node.widgets?.find(\n (w) => w.name === 'turbo'\n ) as IComboWidget\n\n if (!numImagesWidget)\n return formatCreditsRangeLabel(0.07, 0.11, {\n suffix: ' x num_images/Run'\n })\n\n const numImages = Number(numImagesWidget.value) || 1\n const turbo = String(turboWidget?.value).toLowerCase() === 'true'\n const basePrice = turbo ? 0.0715 : 0.1144\n const cost = Number((basePrice * numImages).toFixed(2))\n return formatCreditsLabel(cost)\n }\n },\n IdeogramV3: {\n displayPrice: (node: LGraphNode): string => {\n const renderingSpeedWidget = node.widgets?.find(\n (w) => w.name === 'rendering_speed'\n ) as IComboWidget\n const numImagesWidget = node.widgets?.find(\n (w) => w.name === 'num_images'\n ) as IComboWidget\n const characterInput = node.inputs?.find(\n (i) => i.name === 'character_image'\n ) as INodeInputSlot\n const hasCharacter =\n typeof characterInput?.link !== 'undefined' &&\n characterInput.link != null\n\n if (!renderingSpeedWidget)\n return formatCreditsRangeLabel(0.04, 0.11, {\n suffix: ' x num_images/Run',\n note: '(varies with rendering speed & num_images)'\n })\n\n const numImages = Number(numImagesWidget?.value) || 1\n let basePrice = 0.0858 // default balanced price\n\n const renderingSpeed = String(renderingSpeedWidget.value)\n if (renderingSpeed.toLowerCase().includes('quality')) {\n if (hasCharacter) {\n basePrice = 0.286\n } else {\n basePrice = 0.1287\n }\n } else if (renderingSpeed.toLowerCase().includes('default')) {\n if (hasCharacter) {\n basePrice = 0.2145\n } else {\n basePrice = 0.0858\n }\n } else if (renderingSpeed.toLowerCase().includes('turbo')) {\n if (hasCharacter) {\n basePrice = 0.143\n } else {\n basePrice = 0.0429\n }\n }\n\n const totalCost = Number((basePrice * numImages).toFixed(2))\n return formatCreditsLabel(totalCost)\n }\n },\n KlingCameraControlI2VNode: {\n displayPrice: formatCreditsLabel(0.49)\n },\n KlingCameraControlT2VNode: {\n displayPrice: formatCreditsLabel(0.14)\n },\n KlingDualCharacterVideoEffectNode: {\n displayPrice: (node: LGraphNode): string => {\n const modeWidget = node.widgets?.find(\n (w) => w.name === 'mode'\n ) as IComboWidget\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model_name'\n ) as IComboWidget\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n if (!modeWidget || !modelWidget || !durationWidget)\n return formatCreditsRangeLabel(0.14, 2.8, {\n note: '(varies with model, mode & duration)'\n })\n\n const modeValue = String(modeWidget.value)\n const durationValue = String(durationWidget.value)\n const modelValue = String(modelWidget.value)\n\n // Same pricing matrix as KlingTextToVideoNode\n if (modelValue.includes('v1-6') || modelValue.includes('v1-5')) {\n if (modeValue.includes('pro')) {\n return durationValue.includes('10')\n ? formatCreditsLabel(0.98)\n : formatCreditsLabel(0.49)\n } else {\n return durationValue.includes('10')\n ? formatCreditsLabel(0.56)\n : formatCreditsLabel(0.28)\n }\n } else if (modelValue.includes('v1')) {\n if (modeValue.includes('pro')) {\n return durationValue.includes('10')\n ? formatCreditsLabel(0.98)\n : formatCreditsLabel(0.49)\n } else {\n return durationValue.includes('10')\n ? formatCreditsLabel(0.28)\n : formatCreditsLabel(0.14)\n }\n }\n\n return formatCreditsLabel(0.14)\n }\n },\n KlingImage2VideoNode: {\n displayPrice: (node: LGraphNode): string => {\n const modeWidget = node.widgets?.find(\n (w) => w.name === 'mode'\n ) as IComboWidget\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model_name'\n ) as IComboWidget\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n\n if (!modeWidget) {\n if (!modelWidget)\n return formatCreditsRangeLabel(0.14, 2.8, {\n note: '(varies with model, mode & duration)'\n })\n\n const modelValue = String(modelWidget.value)\n if (\n modelValue.includes('v2-1-master') ||\n modelValue.includes('v2-master')\n ) {\n return formatCreditsLabel(1.4)\n } else if (\n modelValue.includes('v1-6') ||\n modelValue.includes('v1-5')\n ) {\n return formatCreditsLabel(0.28)\n }\n return formatCreditsLabel(0.14)\n }\n\n const modeValue = String(modeWidget.value)\n const durationValue = String(durationWidget.value)\n const modelValue = String(modelWidget.value)\n\n // Same pricing matrix as KlingTextToVideoNode\n if (modelValue.includes('v2-5-turbo')) {\n if (durationValue.includes('10')) {\n return formatCreditsLabel(0.7)\n }\n return formatCreditsLabel(0.35) // 5s default\n } else if (\n modelValue.includes('v2-1-master') ||\n modelValue.includes('v2-master')\n ) {\n if (durationValue.includes('10')) {\n return formatCreditsLabel(2.8)\n }\n return formatCreditsLabel(1.4) // 5s default\n } else if (\n modelValue.includes('v2-1') ||\n modelValue.includes('v1-6') ||\n modelValue.includes('v1-5')\n ) {\n if (modeValue.includes('pro')) {\n return durationValue.includes('10')\n ? formatCreditsLabel(0.98)\n : formatCreditsLabel(0.49)\n } else {\n return durationValue.includes('10')\n ? formatCreditsLabel(0.56)\n : formatCreditsLabel(0.28)\n }\n } else if (modelValue.includes('v1')) {\n if (modeValue.includes('pro')) {\n return durationValue.includes('10')\n ? formatCreditsLabel(0.98)\n : formatCreditsLabel(0.49)\n } else {\n return durationValue.includes('10')\n ? formatCreditsLabel(0.28)\n : formatCreditsLabel(0.14)\n }\n }\n\n return formatCreditsLabel(0.14)\n }\n },\n KlingImageGenerationNode: {\n displayPrice: (node: LGraphNode): string => {\n const imageInputWidget = node.inputs?.find((i) => i.name === 'image')\n // If link is not null => image is connected => modality is image to image\n const modality = imageInputWidget?.link\n ? 'image to image'\n : 'text to image'\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model_name'\n ) as IComboWidget\n const nWidget = node.widgets?.find(\n (w) => w.name === 'n'\n ) as IComboWidget\n\n if (!modelWidget)\n return formatCreditsRangeLabel(0.0035, 0.028, {\n suffix: ' x n/Run',\n note: '(varies with modality & model)'\n })\n\n const model = String(modelWidget.value)\n const n = Number(nWidget?.value) || 1\n let basePrice = 0.014 // default\n\n if (modality.includes('text to image')) {\n if (model.includes('kling-v1-5') || model.includes('kling-v2')) {\n basePrice = 0.014\n } else if (model.includes('kling-v1')) {\n basePrice = 0.0035\n }\n } else if (modality.includes('image to image')) {\n if (model.includes('kling-v1-5')) {\n basePrice = 0.028\n } else if (model.includes('kling-v1')) {\n basePrice = 0.0035\n }\n }\n\n const totalCost = basePrice * n\n return formatCreditsLabel(totalCost)\n }\n },\n KlingLipSyncAudioToVideoNode: {\n displayPrice: formatCreditsLabel(0.1, { approximate: true })\n },\n KlingLipSyncTextToVideoNode: {\n displayPrice: formatCreditsLabel(0.1, { approximate: true })\n },\n KlingSingleImageVideoEffectNode: {\n displayPrice: (node: LGraphNode): string => {\n const effectSceneWidget = node.widgets?.find(\n (w) => w.name === 'effect_scene'\n ) as IComboWidget\n\n if (!effectSceneWidget)\n return formatCreditsRangeLabel(0.28, 0.49, {\n note: '(varies with effect scene)'\n })\n\n const effectScene = String(effectSceneWidget.value)\n if (\n effectScene.includes('fuzzyfuzzy') ||\n effectScene.includes('squish')\n ) {\n return formatCreditsLabel(0.28)\n } else if (effectScene.includes('dizzydizzy')) {\n return formatCreditsLabel(0.49)\n } else if (effectScene.includes('bloombloom')) {\n return formatCreditsLabel(0.49)\n } else if (effectScene.includes('expansion')) {\n return formatCreditsLabel(0.28)\n }\n\n return formatCreditsLabel(0.28)\n }\n },\n KlingStartEndFrameNode: {\n displayPrice: (node: LGraphNode): string => {\n // Same pricing as KlingTextToVideoNode per CSV (\"Same as text to video\")\n const modeWidget = node.widgets?.find(\n (w) => w.name === 'mode'\n ) as IComboWidget\n if (!modeWidget)\n return formatCreditsRangeLabel(0.14, 2.8, {\n note: '(varies with model, mode & duration)'\n })\n\n const modeValue = String(modeWidget.value)\n\n // Same pricing matrix as KlingTextToVideoNode\n if (modeValue.includes('v2-5-turbo')) {\n if (modeValue.includes('10')) {\n return formatCreditsLabel(0.7)\n }\n return formatCreditsLabel(0.35) // 5s default\n } else if (modeValue.includes('v2-1')) {\n if (modeValue.includes('10s')) {\n return formatCreditsLabel(0.98) // pro, 10s\n }\n return formatCreditsLabel(0.49) // pro, 5s default\n } else if (modeValue.includes('v2-master')) {\n if (modeValue.includes('10s')) {\n return formatCreditsLabel(2.8)\n }\n return formatCreditsLabel(1.4) // 5s default\n } else if (modeValue.includes('v1-6')) {\n if (modeValue.includes('pro')) {\n return modeValue.includes('10s')\n ? formatCreditsLabel(0.98)\n : formatCreditsLabel(0.49)\n } else {\n return modeValue.includes('10s')\n ? formatCreditsLabel(0.56)\n : formatCreditsLabel(0.28)\n }\n } else if (modeValue.includes('v1')) {\n if (modeValue.includes('pro')) {\n return modeValue.includes('10s')\n ? formatCreditsLabel(0.98)\n : formatCreditsLabel(0.49)\n } else {\n return modeValue.includes('10s')\n ? formatCreditsLabel(0.28)\n : formatCreditsLabel(0.14)\n }\n }\n\n return formatCreditsLabel(0.14)\n }\n },\n KlingTextToVideoNode: {\n displayPrice: (node: LGraphNode): string => {\n const modeWidget = node.widgets?.find(\n (w) => w.name === 'mode'\n ) as IComboWidget\n if (!modeWidget)\n return formatCreditsRangeLabel(0.14, 2.8, {\n note: '(varies with model, mode & duration)'\n })\n\n const modeValue = String(modeWidget.value)\n\n // Pricing matrix from CSV data based on mode string content\n if (modeValue.includes('v2-5-turbo')) {\n if (modeValue.includes('10')) {\n return formatCreditsLabel(0.7)\n }\n return formatCreditsLabel(0.35) // 5s default\n } else if (modeValue.includes('v2-1-master')) {\n if (modeValue.includes('10s')) {\n return formatCreditsLabel(2.8) // price is the same as for v2-master model\n }\n return formatCreditsLabel(1.4) // price is the same as for v2-master model\n } else if (modeValue.includes('v2-master')) {\n if (modeValue.includes('10s')) {\n return formatCreditsLabel(2.8)\n }\n return formatCreditsLabel(1.4) // 5s default\n } else if (modeValue.includes('v1-6')) {\n if (modeValue.includes('pro')) {\n return modeValue.includes('10s')\n ? formatCreditsLabel(0.98)\n : formatCreditsLabel(0.49)\n } else {\n return modeValue.includes('10s')\n ? formatCreditsLabel(0.56)\n : formatCreditsLabel(0.28)\n }\n } else if (modeValue.includes('v1')) {\n if (modeValue.includes('pro')) {\n return modeValue.includes('10s')\n ? formatCreditsLabel(0.98)\n : formatCreditsLabel(0.49)\n } else {\n return modeValue.includes('10s')\n ? formatCreditsLabel(0.28)\n : formatCreditsLabel(0.14)\n }\n }\n\n return formatCreditsLabel(0.14)\n }\n },\n KlingVideoExtendNode: {\n displayPrice: formatCreditsLabel(0.28)\n },\n KlingVirtualTryOnNode: {\n displayPrice: formatCreditsLabel(0.07)\n },\n KlingOmniProTextToVideoNode: {\n displayPrice: makeOmniProDurationCalculator(0.112)\n },\n KlingOmniProFirstLastFrameNode: {\n displayPrice: makeOmniProDurationCalculator(0.112)\n },\n KlingOmniProImageToVideoNode: {\n displayPrice: makeOmniProDurationCalculator(0.112)\n },\n KlingOmniProVideoToVideoNode: {\n displayPrice: makeOmniProDurationCalculator(0.168)\n },\n KlingMotionControl: {\n displayPrice: klingMotionControlPricingCalculator\n },\n KlingOmniProEditVideoNode: {\n displayPrice: formatCreditsLabel(0.168, { suffix: '/second' })\n },\n KlingOmniProImageNode: {\n displayPrice: formatCreditsLabel(0.028)\n },\n KlingTextToVideoWithAudio: {\n displayPrice: klingVideoWithAudioPricingCalculator\n },\n KlingImageToVideoWithAudio: {\n displayPrice: klingVideoWithAudioPricingCalculator\n },\n LumaImageToVideoNode: {\n displayPrice: (node: LGraphNode): string => {\n // Same pricing as LumaVideoNode per CSV\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n const resolutionWidget = node.widgets?.find(\n (w) => w.name === 'resolution'\n ) as IComboWidget\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n\n if (!modelWidget || !resolutionWidget || !durationWidget) {\n return formatCreditsRangeLabel(0.2, 16.4, {\n note: '(varies with model, resolution & duration)'\n })\n }\n\n const model = String(modelWidget.value)\n const resolution = String(resolutionWidget.value).toLowerCase()\n const duration = String(durationWidget.value)\n\n if (model.includes('ray-flash-2')) {\n if (duration.includes('5s')) {\n if (resolution.includes('4k')) return formatCreditsLabel(3.13)\n if (resolution.includes('1080p')) return formatCreditsLabel(0.79)\n if (resolution.includes('720p')) return formatCreditsLabel(0.34)\n if (resolution.includes('540p')) return formatCreditsLabel(0.2)\n } else if (duration.includes('9s')) {\n if (resolution.includes('4k')) return formatCreditsLabel(5.65)\n if (resolution.includes('1080p')) return formatCreditsLabel(1.42)\n if (resolution.includes('720p')) return formatCreditsLabel(0.61)\n if (resolution.includes('540p')) return formatCreditsLabel(0.36)\n }\n } else if (model.includes('ray-2')) {\n if (duration.includes('5s')) {\n if (resolution.includes('4k')) return formatCreditsLabel(9.11)\n if (resolution.includes('1080p')) return formatCreditsLabel(2.27)\n if (resolution.includes('720p')) return formatCreditsLabel(1.02)\n if (resolution.includes('540p')) return formatCreditsLabel(0.57)\n } else if (duration.includes('9s')) {\n if (resolution.includes('4k')) return formatCreditsLabel(16.4)\n if (resolution.includes('1080p')) return formatCreditsLabel(4.1)\n if (resolution.includes('720p')) return formatCreditsLabel(1.83)\n if (resolution.includes('540p')) return formatCreditsLabel(1.03)\n }\n } else if (model.includes('ray-1-6')) {\n return formatCreditsLabel(0.5)\n }\n\n return formatCreditsLabel(0.79)\n }\n },\n LumaVideoNode: {\n displayPrice: (node: LGraphNode): string => {\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n const resolutionWidget = node.widgets?.find(\n (w) => w.name === 'resolution'\n ) as IComboWidget\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n\n if (!modelWidget || !resolutionWidget || !durationWidget) {\n return formatCreditsRangeLabel(0.2, 16.4, {\n note: '(varies with model, resolution & duration)'\n })\n }\n\n const model = String(modelWidget.value)\n const resolution = String(resolutionWidget.value).toLowerCase()\n const duration = String(durationWidget.value)\n\n if (model.includes('ray-flash-2')) {\n if (duration.includes('5s')) {\n if (resolution.includes('4k')) return formatCreditsLabel(3.13)\n if (resolution.includes('1080p')) return formatCreditsLabel(0.79)\n if (resolution.includes('720p')) return formatCreditsLabel(0.34)\n if (resolution.includes('540p')) return formatCreditsLabel(0.2)\n } else if (duration.includes('9s')) {\n if (resolution.includes('4k')) return formatCreditsLabel(5.65)\n if (resolution.includes('1080p')) return formatCreditsLabel(1.42)\n if (resolution.includes('720p')) return formatCreditsLabel(0.61)\n if (resolution.includes('540p')) return formatCreditsLabel(0.36)\n }\n } else if (model.includes('ray-2')) {\n if (duration.includes('5s')) {\n if (resolution.includes('4k')) return formatCreditsLabel(9.11)\n if (resolution.includes('1080p')) return formatCreditsLabel(2.27)\n if (resolution.includes('720p')) return formatCreditsLabel(1.02)\n if (resolution.includes('540p')) return formatCreditsLabel(0.57)\n } else if (duration.includes('9s')) {\n if (resolution.includes('4k')) return formatCreditsLabel(16.4)\n if (resolution.includes('1080p')) return formatCreditsLabel(4.1)\n if (resolution.includes('720p')) return formatCreditsLabel(1.83)\n if (resolution.includes('540p')) return formatCreditsLabel(1.03)\n }\n } else if (model.includes('ray-1-6')) {\n return formatCreditsLabel(0.5)\n }\n\n return formatCreditsLabel(0.79)\n }\n },\n MinimaxImageToVideoNode: {\n displayPrice: formatCreditsLabel(0.43)\n },\n MinimaxTextToVideoNode: {\n displayPrice: formatCreditsLabel(0.43)\n },\n MinimaxHailuoVideoNode: {\n displayPrice: (node: LGraphNode): string => {\n const resolutionWidget = node.widgets?.find(\n (w) => w.name === 'resolution'\n ) as IComboWidget\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n\n if (!resolutionWidget || !durationWidget) {\n return formatCreditsRangeLabel(0.28, 0.56, {\n note: '(varies with resolution & duration)'\n })\n }\n\n const resolution = String(resolutionWidget.value)\n const duration = String(durationWidget.value)\n\n if (resolution.includes('768P')) {\n if (duration.includes('6')) return formatCreditsLabel(0.28)\n if (duration.includes('10')) return formatCreditsLabel(0.56)\n } else if (resolution.includes('1080P')) {\n if (duration.includes('6')) return formatCreditsLabel(0.49)\n }\n\n return formatCreditsLabel(0.43) // default median\n }\n },\n OpenAIDalle2: {\n displayPrice: (node: LGraphNode): string => {\n const sizeWidget = node.widgets?.find(\n (w) => w.name === 'size'\n ) as IComboWidget\n const nWidget = node.widgets?.find(\n (w) => w.name === 'n'\n ) as IComboWidget\n\n if (!sizeWidget)\n return formatCreditsRangeLabel(0.016, 0.02, {\n suffix: ' x n/Run',\n note: '(varies with size & n)'\n })\n\n const size = String(sizeWidget.value)\n const n = Number(nWidget?.value) || 1\n let basePrice = 0.02 // default\n\n if (size.includes('1024x1024')) {\n basePrice = 0.02\n } else if (size.includes('512x512')) {\n basePrice = 0.018\n } else if (size.includes('256x256')) {\n basePrice = 0.016\n }\n\n const totalCost = Number((basePrice * n).toFixed(3))\n return formatCreditsLabel(totalCost)\n }\n },\n OpenAIDalle3: {\n displayPrice: (node: LGraphNode): string => {\n // Get size and quality widgets\n const sizeWidget = node.widgets?.find(\n (w) => w.name === 'size'\n ) as IComboWidget\n const qualityWidget = node.widgets?.find(\n (w) => w.name === 'quality'\n ) as IComboWidget\n\n if (!sizeWidget || !qualityWidget)\n return formatCreditsRangeLabel(0.04, 0.12, {\n note: '(varies with size & quality)'\n })\n\n const size = String(sizeWidget.value)\n const quality = String(qualityWidget.value)\n\n // Pricing matrix based on CSV data\n if (size.includes('1024x1024')) {\n return quality.includes('hd')\n ? formatCreditsLabel(0.08)\n : formatCreditsLabel(0.04)\n } else if (size.includes('1792x1024') || size.includes('1024x1792')) {\n return quality.includes('hd')\n ? formatCreditsLabel(0.12)\n : formatCreditsLabel(0.08)\n }\n\n // Default value\n return formatCreditsLabel(0.04)\n }\n },\n OpenAIGPTImage1: {\n displayPrice: (node: LGraphNode): string => {\n const qualityWidget = node.widgets?.find(\n (w) => w.name === 'quality'\n ) as IComboWidget\n const nWidget = node.widgets?.find(\n (w) => w.name === 'n'\n ) as IComboWidget\n\n if (!qualityWidget)\n return formatCreditsRangeLabel(0.011, 0.3, {\n suffix: ' x n/Run',\n note: '(varies with quality & n)'\n })\n\n const quality = String(qualityWidget.value)\n const n = Number(nWidget?.value) || 1\n let range: [number, number] = [0.046, 0.07] // default medium\n\n if (quality.includes('high')) {\n range = [0.167, 0.3]\n } else if (quality.includes('medium')) {\n range = [0.046, 0.07]\n } else if (quality.includes('low')) {\n range = [0.011, 0.02]\n }\n\n if (n === 1) {\n return formatCreditsRangeLabel(range[0], range[1])\n }\n return formatCreditsRangeLabel(range[0], range[1], {\n suffix: ` x ${n}/Run`\n })\n }\n },\n PixverseImageToVideoNode: {\n displayPrice: pixversePricingCalculator\n },\n PixverseTextToVideoNode: {\n displayPrice: pixversePricingCalculator\n },\n PixverseTransitionVideoNode: {\n displayPrice: pixversePricingCalculator\n },\n RecraftCreativeUpscaleNode: {\n displayPrice: formatCreditsLabel(0.25)\n },\n RecraftCrispUpscaleNode: {\n displayPrice: formatCreditsLabel(0.004)\n },\n RecraftGenerateColorFromImageNode: {\n displayPrice: (node: LGraphNode): string => {\n const nWidget = node.widgets?.find(\n (w) => w.name === 'n'\n ) as IComboWidget\n if (!nWidget) return formatCreditsLabel(0.04, { suffix: ' x n/Run' })\n\n const n = Number(nWidget.value) || 1\n const cost = Number((0.04 * n).toFixed(2))\n return formatCreditsLabel(cost)\n }\n },\n RecraftGenerateImageNode: {\n displayPrice: (node: LGraphNode): string => {\n const nWidget = node.widgets?.find(\n (w) => w.name === 'n'\n ) as IComboWidget\n if (!nWidget) return formatCreditsLabel(0.04, { suffix: ' x n/Run' })\n\n const n = Number(nWidget.value) || 1\n const cost = Number((0.04 * n).toFixed(2))\n return formatCreditsLabel(cost)\n }\n },\n RecraftGenerateVectorImageNode: {\n displayPrice: (node: LGraphNode): string => {\n const nWidget = node.widgets?.find(\n (w) => w.name === 'n'\n ) as IComboWidget\n if (!nWidget) return formatCreditsLabel(0.08, { suffix: ' x n/Run' })\n\n const n = Number(nWidget.value) || 1\n const cost = Number((0.08 * n).toFixed(2))\n return formatCreditsLabel(cost)\n }\n },\n RecraftImageInpaintingNode: {\n displayPrice: (node: LGraphNode): string => {\n const nWidget = node.widgets?.find(\n (w) => w.name === 'n'\n ) as IComboWidget\n if (!nWidget) return formatCreditsLabel(0.04, { suffix: ' x n/Run' })\n\n const n = Number(nWidget.value) || 1\n const cost = Number((0.04 * n).toFixed(2))\n return formatCreditsLabel(cost)\n }\n },\n RecraftImageToImageNode: {\n displayPrice: (node: LGraphNode): string => {\n const nWidget = node.widgets?.find(\n (w) => w.name === 'n'\n ) as IComboWidget\n if (!nWidget) return formatCreditsLabel(0.04, { suffix: ' x n/Run' })\n\n const n = Number(nWidget.value) || 1\n const cost = Number((0.04 * n).toFixed(2))\n return formatCreditsLabel(cost)\n }\n },\n RecraftRemoveBackgroundNode: {\n displayPrice: formatCreditsLabel(0.01)\n },\n RecraftReplaceBackgroundNode: {\n displayPrice: formatCreditsLabel(0.04)\n },\n RecraftTextToImageNode: {\n displayPrice: (node: LGraphNode): string => {\n const nWidget = node.widgets?.find(\n (w) => w.name === 'n'\n ) as IComboWidget\n if (!nWidget) return formatCreditsLabel(0.04, { suffix: ' x n/Run' })\n\n const n = Number(nWidget.value) || 1\n const cost = Number((0.04 * n).toFixed(2))\n return formatCreditsLabel(cost)\n }\n },\n RecraftTextToVectorNode: {\n displayPrice: (node: LGraphNode): string => {\n const nWidget = node.widgets?.find(\n (w) => w.name === 'n'\n ) as IComboWidget\n if (!nWidget) return formatCreditsLabel(0.08, { suffix: ' x n/Run' })\n\n const n = Number(nWidget.value) || 1\n const cost = Number((0.08 * n).toFixed(2))\n return formatCreditsLabel(cost)\n }\n },\n RecraftVectorizeImageNode: {\n displayPrice: (node: LGraphNode): string => {\n const nWidget = node.widgets?.find(\n (w) => w.name === 'n'\n ) as IComboWidget\n if (!nWidget) return formatCreditsLabel(0.01, { suffix: ' x n/Run' })\n\n const n = Number(nWidget.value) || 1\n const cost = Number((0.01 * n).toFixed(2))\n return formatCreditsLabel(cost)\n }\n },\n StabilityStableImageSD_3_5Node: {\n displayPrice: (node: LGraphNode): string => {\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n\n if (!modelWidget)\n return formatCreditsRangeLabel(0.035, 0.065, {\n note: '(varies with model)'\n })\n\n const model = String(modelWidget.value).toLowerCase()\n if (model.includes('large')) {\n return formatCreditsLabel(0.065)\n } else if (model.includes('medium')) {\n return formatCreditsLabel(0.035)\n }\n\n return formatCreditsLabel(0.035)\n }\n },\n StabilityStableImageUltraNode: {\n displayPrice: formatCreditsLabel(0.08)\n },\n StabilityUpscaleConservativeNode: {\n displayPrice: formatCreditsLabel(0.25)\n },\n StabilityUpscaleCreativeNode: {\n displayPrice: formatCreditsLabel(0.25)\n },\n StabilityUpscaleFastNode: {\n displayPrice: formatCreditsLabel(0.01)\n },\n StabilityTextToAudio: {\n displayPrice: formatCreditsLabel(0.2)\n },\n StabilityAudioToAudio: {\n displayPrice: formatCreditsLabel(0.2)\n },\n StabilityAudioInpaint: {\n displayPrice: formatCreditsLabel(0.2)\n },\n VeoVideoGenerationNode: {\n displayPrice: (node: LGraphNode): string => {\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration_seconds'\n ) as IComboWidget\n\n if (!durationWidget)\n return formatCreditsRangeLabel(2.5, 5.0, {\n note: '(varies with duration)'\n })\n\n const price = 0.5 * Number(durationWidget.value)\n return formatCreditsLabel(price)\n }\n },\n Veo3VideoGenerationNode: {\n displayPrice: (node: LGraphNode): string => {\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n const generateAudioWidget = node.widgets?.find(\n (w) => w.name === 'generate_audio'\n ) as IComboWidget\n\n if (!modelWidget || !generateAudioWidget) {\n return formatCreditsRangeLabel(0.8, 3.2, {\n note: '(varies with model & audio generation)'\n })\n }\n\n const model = String(modelWidget.value)\n const generateAudio =\n String(generateAudioWidget.value).toLowerCase() === 'true'\n\n if (\n model.includes('veo-3.0-fast-generate-001') ||\n model.includes('veo-3.1-fast-generate')\n ) {\n return generateAudio\n ? formatCreditsLabel(1.2)\n : formatCreditsLabel(0.8)\n } else if (\n model.includes('veo-3.0-generate-001') ||\n model.includes('veo-3.1-generate')\n ) {\n return generateAudio\n ? formatCreditsLabel(3.2)\n : formatCreditsLabel(1.6)\n }\n\n // Default fallback\n return formatCreditsRangeLabel(0.8, 3.2)\n }\n },\n Veo3FirstLastFrameNode: {\n displayPrice: (node: LGraphNode): string => {\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n const generateAudioWidget = node.widgets?.find(\n (w) => w.name === 'generate_audio'\n ) as IComboWidget\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n\n if (!modelWidget || !generateAudioWidget || !durationWidget) {\n return formatCreditsRangeLabel(0.4, 3.2, {\n note: '(varies with model & audio generation)'\n })\n }\n\n const model = String(modelWidget.value)\n const generateAudio =\n String(generateAudioWidget.value).toLowerCase() === 'true'\n const seconds = parseFloat(String(durationWidget.value))\n\n let pricePerSecond: number | null = null\n if (model.includes('veo-3.1-fast-generate')) {\n pricePerSecond = generateAudio ? 0.15 : 0.1\n } else if (model.includes('veo-3.1-generate')) {\n pricePerSecond = generateAudio ? 0.4 : 0.2\n }\n if (pricePerSecond === null) {\n return formatCreditsRangeLabel(0.4, 3.2)\n }\n const cost = pricePerSecond * seconds\n return formatCreditsLabel(cost)\n }\n },\n LumaImageNode: {\n displayPrice: (node: LGraphNode): string => {\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n const aspectRatioWidget = node.widgets?.find(\n (w) => w.name === 'aspect_ratio'\n ) as IComboWidget\n\n if (!modelWidget || !aspectRatioWidget) {\n return formatCreditsRangeLabel(0.0064, 0.026, {\n note: '(varies with model & aspect ratio)'\n })\n }\n\n const model = String(modelWidget.value)\n\n if (model.includes('photon-flash-1')) {\n return formatCreditsLabel(0.0027)\n } else if (model.includes('photon-1')) {\n return formatCreditsLabel(0.0104)\n }\n\n return formatCreditsLabel(0.0246)\n }\n },\n LumaImageModifyNode: {\n displayPrice: (node: LGraphNode): string => {\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n\n if (!modelWidget) {\n return formatCreditsRangeLabel(0.0027, 0.0104, {\n note: '(varies with model)'\n })\n }\n\n const model = String(modelWidget.value)\n\n if (model.includes('photon-flash-1')) {\n return formatCreditsLabel(0.0027)\n } else if (model.includes('photon-1')) {\n return formatCreditsLabel(0.0104)\n }\n\n return formatCreditsLabel(0.0246)\n }\n },\n MoonvalleyTxt2VideoNode: {\n displayPrice: (node: LGraphNode): string => {\n const lengthWidget = node.widgets?.find(\n (w) => w.name === 'length'\n ) as IComboWidget\n\n // If no length widget exists, default to 5s pricing\n if (!lengthWidget) return formatCreditsLabel(1.5)\n\n const length = String(lengthWidget.value)\n if (length === '5s') {\n return formatCreditsLabel(1.5)\n } else if (length === '10s') {\n return formatCreditsLabel(3.0)\n }\n\n return formatCreditsLabel(1.5)\n }\n },\n MoonvalleyImg2VideoNode: {\n displayPrice: (node: LGraphNode): string => {\n const lengthWidget = node.widgets?.find(\n (w) => w.name === 'length'\n ) as IComboWidget\n\n // If no length widget exists, default to 5s pricing\n if (!lengthWidget) return formatCreditsLabel(1.5)\n\n const length = String(lengthWidget.value)\n if (length === '5s') {\n return formatCreditsLabel(1.5)\n } else if (length === '10s') {\n return formatCreditsLabel(3.0)\n }\n\n return formatCreditsLabel(1.5)\n }\n },\n MoonvalleyVideo2VideoNode: {\n displayPrice: (node: LGraphNode): string => {\n const lengthWidget = node.widgets?.find(\n (w) => w.name === 'length'\n ) as IComboWidget\n\n // If no length widget exists, default to 5s pricing\n if (!lengthWidget) return formatCreditsLabel(2.25)\n\n const length = String(lengthWidget.value)\n if (length === '5s') {\n return formatCreditsLabel(2.25)\n } else if (length === '10s') {\n return formatCreditsLabel(4.0)\n }\n\n return formatCreditsLabel(2.25)\n }\n },\n // Runway nodes - using actual node names from ComfyUI\n RunwayTextToImageNode: {\n displayPrice: formatCreditsLabel(0.11)\n },\n RunwayImageToVideoNodeGen3a: {\n displayPrice: calculateRunwayDurationPrice\n },\n RunwayImageToVideoNodeGen4: {\n displayPrice: calculateRunwayDurationPrice\n },\n RunwayFirstLastFrameNode: {\n displayPrice: calculateRunwayDurationPrice\n },\n // Rodin nodes - all have the same pricing structure\n Rodin3D_Regular: {\n displayPrice: formatCreditsLabel(0.4)\n },\n Rodin3D_Detail: {\n displayPrice: formatCreditsLabel(0.4)\n },\n Rodin3D_Smooth: {\n displayPrice: formatCreditsLabel(0.4)\n },\n Rodin3D_Sketch: {\n displayPrice: formatCreditsLabel(0.4)\n },\n // Tripo nodes - using actual node names from ComfyUI\n TripoTextToModelNode: {\n displayPrice: (node: LGraphNode): string =>\n calculateTripo3DGenerationPrice(node, 'text')\n },\n TripoImageToModelNode: {\n displayPrice: (node: LGraphNode): string =>\n calculateTripo3DGenerationPrice(node, 'image')\n },\n TripoMultiviewToModelNode: {\n displayPrice: (node: LGraphNode): string =>\n calculateTripo3DGenerationPrice(node, 'multiview')\n },\n TripoTextureNode: {\n displayPrice: (node: LGraphNode): string => {\n const textureQualityWidget = node.widgets?.find(\n (w) => w.name === 'texture_quality'\n ) as IComboWidget\n\n if (!textureQualityWidget)\n return formatCreditsRangeLabel(0.1, 0.2, {\n note: '(varies with quality)'\n })\n\n const textureQuality = String(textureQualityWidget.value)\n return textureQuality.includes('detailed')\n ? formatCreditsLabel(0.2)\n : formatCreditsLabel(0.1)\n }\n },\n TripoRigNode: {\n displayPrice: '$0.25/Run'\n },\n TripoConversionNode: {\n displayPrice: (node: LGraphNode): string => {\n const getWidgetValue = (name: string) =>\n node.widgets?.find((w) => w.name === name)?.value\n\n const getNumber = (name: string, defaultValue: number): number => {\n const raw = getWidgetValue(name)\n if (raw === undefined || raw === null || raw === '')\n return defaultValue\n if (typeof raw === 'number')\n return Number.isFinite(raw) ? raw : defaultValue\n const n = Number(raw)\n return Number.isFinite(n) ? n : defaultValue\n }\n\n const getBool = (name: string, defaultValue: boolean): boolean => {\n const v = getWidgetValue(name)\n if (v === undefined || v === null) return defaultValue\n\n if (typeof v === 'number') return v !== 0\n const lower = String(v).toLowerCase()\n if (lower === 'true') return true\n if (lower === 'false') return false\n return defaultValue\n }\n\n let hasAdvancedParam = false\n\n // ---- booleans that trigger advanced when true ----\n if (getBool('quad', false)) hasAdvancedParam = true\n if (getBool('force_symmetry', false)) hasAdvancedParam = true\n if (getBool('flatten_bottom', false)) hasAdvancedParam = true\n if (getBool('pivot_to_center_bottom', false)) hasAdvancedParam = true\n if (getBool('with_animation', false)) hasAdvancedParam = true\n if (getBool('pack_uv', false)) hasAdvancedParam = true\n if (getBool('bake', false)) hasAdvancedParam = true\n if (getBool('export_vertex_colors', false)) hasAdvancedParam = true\n if (getBool('animate_in_place', false)) hasAdvancedParam = true\n\n // ---- numeric params with special default sentinels ----\n const faceLimit = getNumber('face_limit', -1)\n if (faceLimit !== -1) hasAdvancedParam = true\n\n const textureSize = getNumber('texture_size', 4096)\n if (textureSize !== 4096) hasAdvancedParam = true\n\n const flattenBottomThreshold = getNumber(\n 'flatten_bottom_threshold',\n 0.0\n )\n if (flattenBottomThreshold !== 0.0) hasAdvancedParam = true\n\n const scaleFactor = getNumber('scale_factor', 1.0)\n if (scaleFactor !== 1.0) hasAdvancedParam = true\n\n // ---- string / combo params with non-default values ----\n const textureFormatRaw = String(\n getWidgetValue('texture_format') ?? 'JPEG'\n ).toUpperCase()\n if (textureFormatRaw !== 'JPEG') hasAdvancedParam = true\n\n const partNamesRaw = String(getWidgetValue('part_names') ?? '')\n if (partNamesRaw.trim().length > 0) hasAdvancedParam = true\n\n const fbxPresetRaw = String(\n getWidgetValue('fbx_preset') ?? 'blender'\n ).toLowerCase()\n if (fbxPresetRaw !== 'blender') hasAdvancedParam = true\n\n const exportOrientationRaw = String(\n getWidgetValue('export_orientation') ?? 'default'\n ).toLowerCase()\n if (exportOrientationRaw !== 'default') hasAdvancedParam = true\n\n const credits = hasAdvancedParam ? 10 : 5\n return formatCreditsLabel(credits * 0.01)\n }\n },\n TripoRetargetNode: {\n displayPrice: formatCreditsLabel(0.1)\n },\n TripoRefineNode: {\n displayPrice: formatCreditsLabel(0.3)\n },\n MeshyTextToModelNode: {\n displayPrice: formatCreditsLabel(meshyCreditsToUsd(20))\n },\n MeshyRefineNode: {\n displayPrice: formatCreditsLabel(meshyCreditsToUsd(10))\n },\n MeshyImageToModelNode: {\n displayPrice: calculateMeshyImageToModelPrice\n },\n MeshyMultiImageToModelNode: {\n displayPrice: calculateMeshyMultiImageToModelPrice\n },\n MeshyRigModelNode: {\n displayPrice: formatCreditsLabel(meshyCreditsToUsd(5))\n },\n MeshyAnimateModelNode: {\n displayPrice: formatCreditsLabel(meshyCreditsToUsd(3))\n },\n MeshyTextureNode: {\n displayPrice: formatCreditsLabel(meshyCreditsToUsd(10))\n },\n // Google/Gemini nodes\n GeminiNode: {\n displayPrice: (node: LGraphNode): string => {\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n\n if (!modelWidget) return 'Token-based'\n\n const model = String(modelWidget.value)\n\n if (model.includes('gemini-2.5-flash-preview-04-17')) {\n return formatCreditsListLabel([0.0003, 0.0025], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n } else if (model.includes('gemini-2.5-flash')) {\n return formatCreditsListLabel([0.0003, 0.0025], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n } else if (model.includes('gemini-2.5-pro-preview-05-06')) {\n return formatCreditsListLabel([0.00125, 0.01], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n } else if (model.includes('gemini-2.5-pro')) {\n return formatCreditsListLabel([0.00125, 0.01], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n } else if (model.includes('gemini-3-pro-preview')) {\n return formatCreditsListLabel([0.002, 0.012], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n }\n // For other Gemini models, show token-based pricing info\n return 'Token-based'\n }\n },\n GeminiImageNode: {\n displayPrice: formatCreditsLabel(0.039, {\n suffix: '/Image (1K)',\n approximate: true\n })\n },\n GeminiImage2Node: {\n displayPrice: (node: LGraphNode): string => {\n const resolutionWidget = node.widgets?.find(\n (w) => w.name === 'resolution'\n ) as IComboWidget\n\n if (!resolutionWidget) return 'Token-based'\n\n const resolution = String(resolutionWidget.value)\n if (resolution.includes('1K')) {\n return formatCreditsLabel(0.134, {\n suffix: '/Image',\n approximate: true\n })\n } else if (resolution.includes('2K')) {\n return formatCreditsLabel(0.134, {\n suffix: '/Image',\n approximate: true\n })\n } else if (resolution.includes('4K')) {\n return formatCreditsLabel(0.24, {\n suffix: '/Image',\n approximate: true\n })\n }\n return 'Token-based'\n }\n },\n // OpenAI nodes\n OpenAIChatNode: {\n displayPrice: (node: LGraphNode): string => {\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n\n if (!modelWidget) return 'Token-based'\n\n const model = String(modelWidget.value)\n\n // Specific pricing for exposed models based on official pricing data (converted to per 1K tokens)\n if (model.includes('o4-mini')) {\n return formatCreditsListLabel([0.0011, 0.0044], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n } else if (model.includes('o1-pro')) {\n return formatCreditsListLabel([0.15, 0.6], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n } else if (model.includes('o1')) {\n return formatCreditsListLabel([0.015, 0.06], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n } else if (model.includes('o3-mini')) {\n return formatCreditsListLabel([0.0011, 0.0044], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n } else if (model.includes('o3')) {\n return formatCreditsListLabel([0.01, 0.04], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n } else if (model.includes('gpt-4o')) {\n return formatCreditsListLabel([0.0025, 0.01], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n } else if (model.includes('gpt-4.1-nano')) {\n return formatCreditsListLabel([0.0001, 0.0004], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n } else if (model.includes('gpt-4.1-mini')) {\n return formatCreditsListLabel([0.0004, 0.0016], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n } else if (model.includes('gpt-4.1')) {\n return formatCreditsListLabel([0.002, 0.008], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n } else if (model.includes('gpt-5-nano')) {\n return formatCreditsListLabel([0.00005, 0.0004], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n } else if (model.includes('gpt-5-mini')) {\n return formatCreditsListLabel([0.00025, 0.002], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n } else if (model.includes('gpt-5')) {\n return formatCreditsListLabel([0.00125, 0.01], {\n suffix: ' per 1K tokens',\n approximate: true,\n separator: '-'\n })\n }\n return 'Token-based'\n }\n },\n ViduTextToVideoNode: {\n displayPrice: formatCreditsLabel(0.4)\n },\n ViduImageToVideoNode: {\n displayPrice: formatCreditsLabel(0.4)\n },\n ViduReferenceVideoNode: {\n displayPrice: formatCreditsLabel(0.4)\n },\n ViduStartEndToVideoNode: {\n displayPrice: formatCreditsLabel(0.4)\n },\n ByteDanceImageNode: {\n displayPrice: (node: LGraphNode): string => {\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n\n if (!modelWidget) return 'Token-based'\n\n const model = String(modelWidget.value)\n\n if (model.includes('seedream-3-0-t2i')) {\n return formatCreditsLabel(0.03)\n }\n return 'Token-based'\n }\n },\n ByteDanceImageEditNode: {\n displayPrice: (node: LGraphNode): string => {\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n\n if (!modelWidget) return 'Token-based'\n\n const model = String(modelWidget.value)\n\n if (model.includes('seededit-3-0-i2i')) {\n return formatCreditsLabel(0.03)\n }\n return 'Token-based'\n }\n },\n ByteDanceSeedreamNode: {\n displayPrice: (node: LGraphNode): string => {\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n\n const model = String(modelWidget?.value ?? '').toLowerCase()\n let pricePerImage = 0.03 // default for seedream-4-0-250828 and fallback\n if (model.includes('seedream-4-5-251128')) {\n pricePerImage = 0.04\n } else if (model.includes('seedream-4-0-250828')) {\n pricePerImage = 0.03\n }\n return formatCreditsLabel(pricePerImage, {\n suffix: ' x images/Run',\n approximate: true\n })\n }\n },\n ByteDanceTextToVideoNode: {\n displayPrice: byteDanceVideoPricingCalculator\n },\n ByteDanceImageToVideoNode: {\n displayPrice: byteDanceVideoPricingCalculator\n },\n ByteDanceFirstLastFrameNode: {\n displayPrice: byteDanceVideoPricingCalculator\n },\n ByteDanceImageReferenceNode: {\n displayPrice: byteDanceVideoPricingCalculator\n },\n WanTextToVideoApi: {\n displayPrice: (node: LGraphNode): string => {\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n const resolutionWidget = node.widgets?.find(\n (w) => w.name === 'size'\n ) as IComboWidget\n\n if (!durationWidget || !resolutionWidget)\n return formatCreditsRangeLabel(0.05, 0.15, { suffix: '/second' })\n\n const seconds = parseFloat(String(durationWidget.value))\n const resolutionStr = String(resolutionWidget.value).toLowerCase()\n\n const resKey = resolutionStr.includes('1080')\n ? '1080p'\n : resolutionStr.includes('720')\n ? '720p'\n : resolutionStr.includes('480')\n ? '480p'\n : (resolutionStr.match(/^\\s*(\\d{3,4}p)/)?.[1] ?? '')\n\n const pricePerSecond: Record<string, number> = {\n '480p': 0.05,\n '720p': 0.1,\n '1080p': 0.15\n }\n\n const pps = pricePerSecond[resKey]\n if (isNaN(seconds) || !pps)\n return formatCreditsRangeLabel(0.05, 0.15, { suffix: '/second' })\n\n const cost = Number((pps * seconds).toFixed(2))\n return formatCreditsLabel(cost)\n }\n },\n WanImageToVideoApi: {\n displayPrice: (node: LGraphNode): string => {\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n const resolutionWidget = node.widgets?.find(\n (w) => w.name === 'resolution'\n ) as IComboWidget\n\n if (!durationWidget || !resolutionWidget)\n return formatCreditsRangeLabel(0.05, 0.15, { suffix: '/second' })\n\n const seconds = parseFloat(String(durationWidget.value))\n const resolution = String(resolutionWidget.value).trim().toLowerCase()\n\n const pricePerSecond: Record<string, number> = {\n '480p': 0.05,\n '720p': 0.1,\n '1080p': 0.15\n }\n\n const pps = pricePerSecond[resolution]\n if (isNaN(seconds) || !pps)\n return formatCreditsRangeLabel(0.05, 0.15, { suffix: '/second' })\n\n const cost = Number((pps * seconds).toFixed(2))\n return formatCreditsLabel(cost)\n }\n },\n WanTextToImageApi: {\n displayPrice: formatCreditsLabel(0.03)\n },\n WanImageToImageApi: {\n displayPrice: formatCreditsLabel(0.03)\n },\n LtxvApiTextToVideo: {\n displayPrice: ltxvPricingCalculator\n },\n LtxvApiImageToVideo: {\n displayPrice: ltxvPricingCalculator\n },\n WanReferenceVideoApi: {\n displayPrice: (node: LGraphNode): string => {\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n const sizeWidget = node.widgets?.find(\n (w) => w.name === 'size'\n ) as IComboWidget\n\n if (!durationWidget || !sizeWidget) {\n return formatCreditsRangeLabel(0.7, 1.5, {\n note: '(varies with size & duration)'\n })\n }\n\n const seconds = parseFloat(String(durationWidget.value))\n const sizeStr = String(sizeWidget.value).toLowerCase()\n\n const rate = sizeStr.includes('1080p') ? 0.15 : 0.1\n const inputMin = 2 * rate\n const inputMax = 5 * rate\n const outputPrice = seconds * rate\n\n const minTotal = inputMin + outputPrice\n const maxTotal = inputMax + outputPrice\n\n return formatCreditsRangeLabel(minTotal, maxTotal)\n }\n },\n Vidu2TextToVideoNode: {\n displayPrice: (node: LGraphNode): string => {\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n const resolutionWidget = node.widgets?.find(\n (w) => w.name === 'resolution'\n ) as IComboWidget\n\n if (!durationWidget || !resolutionWidget) {\n return formatCreditsRangeLabel(0.075, 0.6, {\n note: '(varies with duration & resolution)'\n })\n }\n\n const duration = parseFloat(String(durationWidget.value))\n const resolution = String(resolutionWidget.value).toLowerCase()\n\n // Text-to-Video uses Q2 model only\n // 720P: Starts at $0.075, +$0.025/sec\n // 1080P: Starts at $0.10, +$0.05/sec\n let basePrice: number\n let pricePerSecond: number\n\n if (resolution.includes('1080')) {\n basePrice = 0.1\n pricePerSecond = 0.05\n } else {\n // 720P default\n basePrice = 0.075\n pricePerSecond = 0.025\n }\n\n if (!Number.isFinite(duration) || duration <= 0) {\n return formatCreditsRangeLabel(0.075, 0.6, {\n note: '(varies with duration & resolution)'\n })\n }\n\n const cost = basePrice + pricePerSecond * (duration - 1)\n return formatCreditsLabel(cost)\n }\n },\n Vidu2ImageToVideoNode: {\n displayPrice: (node: LGraphNode): string => {\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n const resolutionWidget = node.widgets?.find(\n (w) => w.name === 'resolution'\n ) as IComboWidget\n\n if (!modelWidget || !durationWidget || !resolutionWidget) {\n return formatCreditsRangeLabel(0.04, 1.0, {\n note: '(varies with model, duration & resolution)'\n })\n }\n\n const model = String(modelWidget.value).toLowerCase()\n const duration = parseFloat(String(durationWidget.value))\n const resolution = String(resolutionWidget.value).toLowerCase()\n const is1080p = resolution.includes('1080')\n\n let basePrice: number\n let pricePerSecond: number\n\n if (model.includes('q2-pro-fast')) {\n // Q2-pro-fast: 720P $0.04+$0.01/sec, 1080P $0.08+$0.02/sec\n basePrice = is1080p ? 0.08 : 0.04\n pricePerSecond = is1080p ? 0.02 : 0.01\n } else if (model.includes('q2-pro')) {\n // Q2-pro: 720P $0.075+$0.05/sec, 1080P $0.275+$0.075/sec\n basePrice = is1080p ? 0.275 : 0.075\n pricePerSecond = is1080p ? 0.075 : 0.05\n } else if (model.includes('q2-turbo')) {\n // Q2-turbo: 720P special pricing, 1080P $0.175+$0.05/sec\n if (is1080p) {\n basePrice = 0.175\n pricePerSecond = 0.05\n } else {\n // 720P: $0.04 at 1s, $0.05 at 2s, +$0.05/sec beyond 2s\n if (duration <= 1) {\n return formatCreditsLabel(0.04)\n }\n if (duration <= 2) {\n return formatCreditsLabel(0.05)\n }\n const cost = 0.05 + 0.05 * (duration - 2)\n return formatCreditsLabel(cost)\n }\n } else {\n return formatCreditsRangeLabel(0.04, 1.0, {\n note: '(varies with model, duration & resolution)'\n })\n }\n\n if (!Number.isFinite(duration) || duration <= 0) {\n return formatCreditsRangeLabel(0.04, 1.0, {\n note: '(varies with model, duration & resolution)'\n })\n }\n\n const cost = basePrice + pricePerSecond * (duration - 1)\n return formatCreditsLabel(cost)\n }\n },\n Vidu2ReferenceVideoNode: {\n displayPrice: (node: LGraphNode): string => {\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n const resolutionWidget = node.widgets?.find(\n (w) => w.name === 'resolution'\n ) as IComboWidget\n const audioWidget = node.widgets?.find(\n (w) => w.name === 'audio'\n ) as IComboWidget\n\n if (!durationWidget) {\n return formatCreditsRangeLabel(0.125, 1.5, {\n note: '(varies with duration, resolution & audio)'\n })\n }\n\n const duration = parseFloat(String(durationWidget.value))\n const resolution = String(resolutionWidget?.value ?? '').toLowerCase()\n const is1080p = resolution.includes('1080')\n\n // Check if audio is enabled (adds $0.75)\n const audioValue = audioWidget?.value\n const hasAudio =\n audioValue !== undefined &&\n audioValue !== null &&\n String(audioValue).toLowerCase() !== 'false' &&\n String(audioValue).toLowerCase() !== 'none' &&\n audioValue !== ''\n\n // Reference-to-Video uses Q2 model\n // 720P: Starts at $0.125, +$0.025/sec\n // 1080P: Starts at $0.375, +$0.05/sec\n let basePrice: number\n let pricePerSecond: number\n\n if (is1080p) {\n basePrice = 0.375\n pricePerSecond = 0.05\n } else {\n // 720P default\n basePrice = 0.125\n pricePerSecond = 0.025\n }\n\n let cost = basePrice\n if (Number.isFinite(duration) && duration > 0) {\n cost = basePrice + pricePerSecond * (duration - 1)\n }\n\n // Audio adds $0.75 on top\n if (hasAudio) {\n cost += 0.075\n }\n\n return formatCreditsLabel(cost)\n }\n },\n Vidu2StartEndToVideoNode: {\n displayPrice: (node: LGraphNode): string => {\n const modelWidget = node.widgets?.find(\n (w) => w.name === 'model'\n ) as IComboWidget\n const durationWidget = node.widgets?.find(\n (w) => w.name === 'duration'\n ) as IComboWidget\n const resolutionWidget = node.widgets?.find(\n (w) => w.name === 'resolution'\n ) as IComboWidget\n\n if (!modelWidget || !durationWidget || !resolutionWidget) {\n return formatCreditsRangeLabel(0.04, 1.0, {\n note: '(varies with model, duration & resolution)'\n })\n }\n\n const model = String(modelWidget.value).toLowerCase()\n const duration = parseFloat(String(durationWidget.value))\n const resolution = String(resolutionWidget.value).toLowerCase()\n const is1080p = resolution.includes('1080')\n\n let basePrice: number\n let pricePerSecond: number\n\n if (model.includes('q2-pro-fast')) {\n // Q2-pro-fast: 720P $0.04+$0.01/sec, 1080P $0.08+$0.02/sec\n basePrice = is1080p ? 0.08 : 0.04\n pricePerSecond = is1080p ? 0.02 : 0.01\n } else if (model.includes('q2-pro')) {\n // Q2-pro: 720P $0.075+$0.05/sec, 1080P $0.275+$0.075/sec\n basePrice = is1080p ? 0.275 : 0.075\n pricePerSecond = is1080p ? 0.075 : 0.05\n } else if (model.includes('q2-turbo')) {\n // Q2-turbo: 720P special pricing, 1080P $0.175+$0.05/sec\n if (is1080p) {\n basePrice = 0.175\n pricePerSecond = 0.05\n } else {\n // 720P: $0.04 at 1s, $0.05 at 2s, +$0.05/sec beyond 2s\n if (!Number.isFinite(duration) || duration <= 1) {\n return formatCreditsLabel(0.04)\n }\n if (duration <= 2) {\n return formatCreditsLabel(0.05)\n }\n const cost = 0.05 + 0.05 * (duration - 2)\n return formatCreditsLabel(cost)\n }\n } else {\n return formatCreditsRangeLabel(0.04, 1.0, {\n note: '(varies with model, duration & resolution)'\n })\n }\n\n if (!Number.isFinite(duration) || duration <= 0) {\n return formatCreditsLabel(basePrice)\n }\n\n const cost = basePrice + pricePerSecond * (duration - 1)\n return formatCreditsLabel(cost)\n }\n }\n }\n\n/**\n * Composable to get node pricing information for API nodes\n */\nexport const useNodePricing = () => {\n /**\n * Get the price display for a node\n */\n const getNodeDisplayPrice = (node: LGraphNode): string => {\n if (!node.constructor?.nodeData?.api_node) return ''\n\n const nodeName = node.constructor.nodeData.name\n const priceConfig = apiNodeCosts[nodeName]\n\n if (!priceConfig) return ''\n\n // If it's a function, call it with the node to get dynamic pricing\n if (typeof priceConfig.displayPrice === 'function') {\n return safePricingExecution(priceConfig.displayPrice, node, '')\n }\n\n // Otherwise return the static price\n return priceConfig.displayPrice\n }\n\n const getNodePricingConfig = (node: LGraphNode) =>\n apiNodeCosts[node.constructor.nodeData?.name ?? '']\n\n const getRelevantWidgetNames = (nodeType: string): string[] => {\n const widgetMap: Record<string, string[]> = {\n KlingTextToVideoNode: ['mode', 'model_name', 'duration'],\n KlingImage2VideoNode: ['mode', 'model_name', 'duration'],\n KlingImageGenerationNode: ['modality', 'model_name', 'n'],\n KlingDualCharacterVideoEffectNode: ['mode', 'model_name', 'duration'],\n KlingSingleImageVideoEffectNode: ['effect_scene'],\n KlingStartEndFrameNode: ['mode', 'model_name', 'duration'],\n KlingTextToVideoWithAudio: ['duration', 'generate_audio'],\n KlingImageToVideoWithAudio: ['duration', 'generate_audio'],\n KlingOmniProTextToVideoNode: ['duration'],\n KlingOmniProFirstLastFrameNode: ['duration'],\n KlingOmniProImageToVideoNode: ['duration'],\n KlingOmniProVideoToVideoNode: ['duration'],\n KlingMotionControl: ['mode'],\n MinimaxHailuoVideoNode: ['resolution', 'duration'],\n OpenAIDalle3: ['size', 'quality'],\n OpenAIDalle2: ['size', 'n'],\n OpenAIVideoSora2: ['model', 'size', 'duration'],\n OpenAIGPTImage1: ['quality', 'n'],\n IdeogramV1: ['num_images', 'turbo'],\n IdeogramV2: ['num_images', 'turbo'],\n IdeogramV3: ['rendering_speed', 'num_images', 'character_image'],\n FluxProKontextProNode: [],\n FluxProKontextMaxNode: [],\n Flux2ProImageNode: ['width', 'height', 'images'],\n Flux2MaxImageNode: ['width', 'height', 'images'],\n VeoVideoGenerationNode: ['duration_seconds'],\n Veo3VideoGenerationNode: ['model', 'generate_audio'],\n Veo3FirstLastFrameNode: ['model', 'generate_audio', 'duration'],\n LumaVideoNode: ['model', 'resolution', 'duration'],\n LumaImageToVideoNode: ['model', 'resolution', 'duration'],\n LumaImageNode: ['model', 'aspect_ratio'],\n LumaImageModifyNode: ['model', 'aspect_ratio'],\n PixverseTextToVideoNode: ['duration_seconds', 'quality', 'motion_mode'],\n PixverseTransitionVideoNode: [\n 'duration_seconds',\n 'motion_mode',\n 'quality'\n ],\n PixverseImageToVideoNode: ['duration_seconds', 'quality', 'motion_mode'],\n StabilityStableImageSD_3_5Node: ['model'],\n RecraftTextToImageNode: ['n'],\n RecraftImageToImageNode: ['n'],\n RecraftImageInpaintingNode: ['n'],\n RecraftTextToVectorNode: ['n'],\n RecraftVectorizeImageNode: ['n'],\n RecraftGenerateColorFromImageNode: ['n'],\n RecraftGenerateImageNode: ['n'],\n RecraftGenerateVectorImageNode: ['n'],\n MoonvalleyTxt2VideoNode: ['length'],\n MoonvalleyImg2VideoNode: ['length'],\n MoonvalleyVideo2VideoNode: ['length'],\n // Runway nodes\n RunwayImageToVideoNodeGen3a: ['duration'],\n RunwayImageToVideoNodeGen4: ['duration'],\n RunwayFirstLastFrameNode: ['duration'],\n // Tripo nodes\n TripoTextToModelNode: [\n 'model_version',\n 'quad',\n 'style',\n 'texture',\n 'pbr',\n 'texture_quality',\n 'geometry_quality'\n ],\n TripoImageToModelNode: [\n 'model_version',\n 'quad',\n 'style',\n 'texture',\n 'pbr',\n 'texture_quality',\n 'geometry_quality'\n ],\n TripoMultiviewToModelNode: [\n 'model_version',\n 'quad',\n 'texture',\n 'pbr',\n 'texture_quality',\n 'geometry_quality'\n ],\n TripoConversionNode: [\n 'quad',\n 'face_limit',\n 'texture_size',\n 'texture_format',\n 'force_symmetry',\n 'flatten_bottom',\n 'flatten_bottom_threshold',\n 'pivot_to_center_bottom',\n 'scale_factor',\n 'with_animation',\n 'pack_uv',\n 'bake',\n 'part_names',\n 'fbx_preset',\n 'export_vertex_colors',\n 'export_orientation',\n 'animate_in_place'\n ],\n TripoTextureNode: ['texture_quality'],\n // Meshy nodes\n MeshyImageToModelNode: ['should_texture'],\n MeshyMultiImageToModelNode: ['should_texture'],\n // Google/Gemini nodes\n GeminiNode: ['model'],\n GeminiImage2Node: ['resolution'],\n // OpenAI nodes\n OpenAIChatNode: ['model'],\n // ByteDance\n ByteDanceImageNode: ['model'],\n ByteDanceImageEditNode: ['model'],\n ByteDanceSeedreamNode: [\n 'model',\n 'sequential_image_generation',\n 'max_images'\n ],\n ByteDanceTextToVideoNode: [\n 'model',\n 'duration',\n 'resolution',\n 'generate_audio'\n ],\n ByteDanceImageToVideoNode: [\n 'model',\n 'duration',\n 'resolution',\n 'generate_audio'\n ],\n ByteDanceFirstLastFrameNode: [\n 'model',\n 'duration',\n 'resolution',\n 'generate_audio'\n ],\n ByteDanceImageReferenceNode: ['model', 'duration', 'resolution'],\n WanTextToVideoApi: ['duration', 'size'],\n WanImageToVideoApi: ['duration', 'resolution'],\n WanReferenceVideoApi: ['duration', 'size'],\n LtxvApiTextToVideo: ['model', 'duration', 'resolution'],\n LtxvApiImageToVideo: ['model', 'duration', 'resolution'],\n Vidu2TextToVideoNode: ['model', 'duration', 'resolution'],\n Vidu2ImageToVideoNode: ['model', 'duration', 'resolution'],\n Vidu2ReferenceVideoNode: ['audio', 'duration', 'resolution'],\n Vidu2StartEndToVideoNode: ['model', 'duration', 'resolution']\n }\n return widgetMap[nodeType] || []\n }\n\n return {\n getNodeDisplayPrice,\n getNodePricingConfig,\n getRelevantWidgetNames\n }\n}\n","import type { LGraph, LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport { LGraphBadge } from '@/lib/litegraph/src/litegraph'\n\nimport { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'\nimport { adjustColor } from '@/utils/colorUtil'\n\nconst componentIconSvg = new Image()\ncomponentIconSvg.src =\n \"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='oklch(83.01%25 0.163 83.16)' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M15.536 11.293a1 1 0 0 0 0 1.414l2.376 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0zm-13.239 0a1 1 0 0 0 0 1.414l2.377 2.377a1 1 0 0 0 1.414 0l2.377-2.377a1 1 0 0 0 0-1.414L6.088 8.916a1 1 0 0 0-1.414 0zm6.619 6.619a1 1 0 0 0 0 1.415l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.415l-2.377-2.376a1 1 0 0 0-1.414 0zm0-13.238a1 1 0 0 0 0 1.414l2.377 2.376a1 1 0 0 0 1.414 0l2.377-2.376a1 1 0 0 0 0-1.414l-2.377-2.377a1 1 0 0 0-1.414 0z'/%3E%3C/svg%3E\"\n\nexport const usePriceBadge = () => {\n function updateSubgraphCredits(node: LGraphNode) {\n if (!node.isSubgraphNode()) return\n node.badges = node.badges.filter((b) => !isCreditsBadge(b))\n const newBadges = collectCreditsBadges(node.subgraph)\n if (newBadges.length > 1) {\n node.badges.push(getCreditsBadge('Partner Nodes x ' + newBadges.length))\n } else {\n node.badges.push(...newBadges)\n }\n }\n function collectCreditsBadges(\n graph: LGraph,\n visited: Set<string> = new Set()\n ): (LGraphBadge | (() => LGraphBadge))[] {\n if (visited.has(graph.id)) return []\n visited.add(graph.id)\n const badges = []\n for (const node of graph.nodes) {\n badges.push(\n ...(node.isSubgraphNode()\n ? collectCreditsBadges(node.subgraph, visited)\n : node.badges.filter((b) => isCreditsBadge(b)))\n )\n }\n return badges\n }\n\n function isCreditsBadge(badge: LGraphBadge | (() => LGraphBadge)): boolean {\n const badgeInstance = typeof badge === 'function' ? badge() : badge\n return badgeInstance.icon?.image === componentIconSvg\n }\n\n const colorPaletteStore = useColorPaletteStore()\n function getCreditsBadge(price: string): LGraphBadge {\n const isLightTheme = colorPaletteStore.completedActivePalette.light_theme\n\n return new LGraphBadge({\n text: price,\n iconOptions: {\n image: componentIconSvg,\n size: 8\n },\n fgColor:\n colorPaletteStore.completedActivePalette.colors.litegraph_base\n .BADGE_FG_COLOR,\n bgColor: isLightTheme\n ? adjustColor('#8D6932', { lightness: 0.5 })\n : '#8D6932'\n })\n }\n return {\n getCreditsBadge,\n updateSubgraphCredits\n }\n}\n","import { computedWithControl } from '@vueuse/core'\nimport { ref } from 'vue'\nimport type { ComputedRef } from 'vue'\n\nimport { useChainCallback } from '@/composables/functional/useChainCallback'\nimport type { LGraphNode } from '@/lib/litegraph/src/litegraph'\n\ninterface UseComputedWithWidgetWatchOptions {\n /**\n * Names of widgets to observe for changes.\n * If not provided, all widgets will be observed.\n */\n widgetNames?: string[]\n\n /**\n * Whether to trigger a canvas redraw when widget values change.\n * @default false\n */\n triggerCanvasRedraw?: boolean\n}\n\n/**\n * A composable that creates a computed that has a node's widget values as a dependencies.\n * Essentially `computedWithControl` (https://vueuse.org/shared/computedWithControl/) where\n * the explicitly defined extra dependencies are LGraphNode widgets.\n *\n * @param node - The LGraphNode whose widget values are to be watched\n * @param options - Configuration options for the watcher\n * @returns A function to create computed that responds to widget changes\n *\n * @example\n * ```ts\n * const computedWithWidgetWatch = useComputedWithWidgetWatch(node, {\n * widgetNames: ['width', 'height'],\n * triggerCanvasRedraw: true\n * })\n *\n * const dynamicPrice = computedWithWidgetWatch(() => {\n * return calculatePrice(node)\n * })\n * ```\n */\nexport const useComputedWithWidgetWatch = (\n node: LGraphNode,\n options: UseComputedWithWidgetWatchOptions = {}\n) => {\n const { widgetNames, triggerCanvasRedraw = false } = options\n\n // Create a reactive trigger based on widget values\n const widgetValues = ref<Record<string, any>>({})\n\n // Initialize widget observers\n if (node.widgets) {\n const widgetsToObserve = widgetNames\n ? node.widgets.filter((widget) => widgetNames.includes(widget.name))\n : node.widgets\n\n // Initialize current values\n const currentValues: Record<string, any> = {}\n widgetsToObserve.forEach((widget) => {\n currentValues[widget.name] = widget.value\n })\n widgetValues.value = currentValues\n\n widgetsToObserve.forEach((widget) => {\n widget.callback = useChainCallback(widget.callback, () => {\n // Update the reactive widget values\n widgetValues.value = {\n ...widgetValues.value,\n [widget.name]: widget.value\n }\n\n // Optionally trigger a canvas redraw\n if (triggerCanvasRedraw) {\n node.graph?.setDirtyCanvas(true, true)\n }\n })\n })\n if (widgetNames && widgetNames.length > widgetsToObserve.length) {\n //Inputs have been included\n const indexesToObserve = widgetNames\n .map((name) =>\n widgetsToObserve.some((w) => w.name == name)\n ? -1\n : node.inputs.findIndex((i) => i.name == name)\n )\n .filter((i) => i >= 0)\n node.onConnectionsChange = useChainCallback(\n node.onConnectionsChange,\n (_type: unknown, index: number, isConnected: boolean) => {\n if (!indexesToObserve.includes(index)) return\n widgetValues.value = {\n ...widgetValues.value,\n [indexesToObserve[index]]: isConnected\n }\n if (triggerCanvasRedraw) {\n node.graph?.setDirtyCanvas(true, true)\n }\n }\n )\n }\n }\n\n // Returns a function that creates a computed that responds to widget changes.\n // The computed will be re-evaluated whenever any observed widget changes.\n return <T>(computeFn: () => T): ComputedRef<T> => {\n return computedWithControl(widgetValues, computeFn)\n }\n}\n","import _ from 'es-toolkit/compat'\nimport { computed, onMounted, watch } from 'vue'\n\nimport { useNodePricing } from '@/composables/node/useNodePricing'\nimport { usePriceBadge } from '@/composables/node/usePriceBadge'\nimport { useComputedWithWidgetWatch } from '@/composables/node/useWatchWidget'\nimport { BadgePosition, LGraphBadge } from '@/lib/litegraph/src/litegraph'\nimport type { LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { app } from '@/scripts/app'\nimport { useExtensionStore } from '@/stores/extensionStore'\nimport type { ComfyNodeDefImpl } from '@/stores/nodeDefStore'\nimport { useNodeDefStore } from '@/stores/nodeDefStore'\nimport { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'\nimport { NodeBadgeMode } from '@/types/nodeSource'\n\n/**\n * Add LGraphBadge to LGraphNode based on settings.\n *\n * Following badges are added:\n * - Node ID badge\n * - Node source badge\n * - Node life cycle badge\n * - API node credits badge\n */\nexport const useNodeBadge = () => {\n const settingStore = useSettingStore()\n const extensionStore = useExtensionStore()\n const colorPaletteStore = useColorPaletteStore()\n const priceBadge = usePriceBadge()\n\n const nodeSourceBadgeMode = computed(\n () =>\n settingStore.get('Comfy.NodeBadge.NodeSourceBadgeMode') as NodeBadgeMode\n )\n const nodeIdBadgeMode = computed(\n () => settingStore.get('Comfy.NodeBadge.NodeIdBadgeMode') as NodeBadgeMode\n )\n const nodeLifeCycleBadgeMode = computed(\n () =>\n settingStore.get(\n 'Comfy.NodeBadge.NodeLifeCycleBadgeMode'\n ) as NodeBadgeMode\n )\n\n const showApiPricingBadge = computed(() =>\n settingStore.get('Comfy.NodeBadge.ShowApiPricing')\n )\n\n watch(\n [\n nodeSourceBadgeMode,\n nodeIdBadgeMode,\n nodeLifeCycleBadgeMode,\n showApiPricingBadge\n ],\n () => {\n app.canvas?.setDirty(true, true)\n }\n )\n\n const nodeDefStore = useNodeDefStore()\n function badgeTextVisible(\n nodeDef: ComfyNodeDefImpl | null,\n badgeMode: NodeBadgeMode\n ): boolean {\n return !(\n badgeMode === NodeBadgeMode.None ||\n (nodeDef?.isCoreNode && badgeMode === NodeBadgeMode.HideBuiltIn)\n )\n }\n\n onMounted(() => {\n const nodePricing = useNodePricing()\n\n extensionStore.registerExtension({\n name: 'Comfy.NodeBadge',\n nodeCreated(node: LGraphNode) {\n node.badgePosition = BadgePosition.TopRight\n\n const badge = computed(() => {\n const nodeDef = nodeDefStore.fromLGraphNode(node)\n return new LGraphBadge({\n text: _.truncate(\n [\n badgeTextVisible(nodeDef, nodeIdBadgeMode.value)\n ? `#${node.id}`\n : '',\n badgeTextVisible(nodeDef, nodeLifeCycleBadgeMode.value)\n ? (nodeDef?.nodeLifeCycleBadgeText ?? '')\n : '',\n badgeTextVisible(nodeDef, nodeSourceBadgeMode.value)\n ? (nodeDef?.nodeSource?.badgeText ?? '')\n : ''\n ]\n .filter((s) => s.length > 0)\n .join(' '),\n {\n length: 31\n }\n ),\n fgColor:\n colorPaletteStore.completedActivePalette.colors.litegraph_base\n .BADGE_FG_COLOR,\n bgColor:\n colorPaletteStore.completedActivePalette.colors.litegraph_base\n .BADGE_BG_COLOR\n })\n })\n\n node.badges.push(() => badge.value)\n\n if (node.constructor.nodeData?.api_node && showApiPricingBadge.value) {\n // Get the pricing function to determine if this node has dynamic pricing\n const pricingConfig = nodePricing.getNodePricingConfig(node)\n const hasDynamicPricing =\n typeof pricingConfig?.displayPrice === 'function'\n\n let creditsBadge\n const createBadge = () => {\n const price = nodePricing.getNodeDisplayPrice(node)\n return priceBadge.getCreditsBadge(price)\n }\n\n if (hasDynamicPricing) {\n // For dynamic pricing nodes, use computed that watches widget changes\n const relevantWidgetNames = nodePricing.getRelevantWidgetNames(\n node.constructor.nodeData?.name\n )\n\n const computedWithWidgetWatch = useComputedWithWidgetWatch(node, {\n widgetNames: relevantWidgetNames,\n triggerCanvasRedraw: true\n })\n\n creditsBadge = computedWithWidgetWatch(createBadge)\n } else {\n // For static pricing nodes, use regular computed\n creditsBadge = computed(createBadge)\n }\n\n node.badges.push(() => creditsBadge.value)\n }\n },\n init() {\n app.canvas.canvas.addEventListener<'litegraph:set-graph'>(\n 'litegraph:set-graph',\n () => {\n for (const node of app.canvas.graph?.nodes ?? [])\n priceBadge.updateSubgraphCredits(node)\n }\n )\n app.canvas.canvas.addEventListener<'subgraph-converted'>(\n 'subgraph-converted',\n (e) => priceBadge.updateSubgraphCredits(e.detail.subgraphNode)\n )\n },\n afterConfigureGraph() {\n for (const node of app.canvas.graph?.nodes ?? [])\n priceBadge.updateSubgraphCredits(node)\n }\n })\n })\n}\n","import type { Ref } from 'vue'\n\nimport { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'\nimport { usePragmaticDroppable } from '@/composables/usePragmaticDragAndDrop'\nimport type { LGraphNode } from '@/lib/litegraph/src/litegraph'\nimport { LiteGraph } from '@/lib/litegraph/src/litegraph'\nimport { useWorkflowService } from '@/platform/workflow/core/services/workflowService'\nimport { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'\nimport { app as comfyApp } from '@/scripts/app'\nimport { useLitegraphService } from '@/services/litegraphService'\nimport { ComfyModelDef } from '@/stores/modelStore'\nimport type { ModelNodeProvider } from '@/stores/modelToNodeStore'\nimport { useModelToNodeStore } from '@/stores/modelToNodeStore'\nimport { ComfyNodeDefImpl } from '@/stores/nodeDefStore'\nimport type { RenderedTreeExplorerNode } from '@/types/treeExplorerTypes'\n\nexport const useCanvasDrop = (canvasRef: Ref<HTMLCanvasElement | null>) => {\n const modelToNodeStore = useModelToNodeStore()\n const litegraphService = useLitegraphService()\n const workflowService = useWorkflowService()\n\n usePragmaticDroppable(() => canvasRef.value, {\n getDropEffect: (args): Exclude<DataTransfer['dropEffect'], 'none'> =>\n args.source.data.type === 'tree-explorer-node' ? 'copy' : 'move',\n onDrop: async (event) => {\n const loc = event.location.current.input\n const dndData = event.source.data\n\n if (dndData.type === 'tree-explorer-node') {\n const node = dndData.data as RenderedTreeExplorerNode\n const conv = useSharedCanvasPositionConversion()\n const basePos = conv.clientPosToCanvasPos([loc.clientX, loc.clientY])\n\n if (node.data instanceof ComfyNodeDefImpl) {\n const nodeDef = node.data\n const pos = [...basePos]\n // Add an offset on y to make sure after adding the node, the cursor\n // is on the node (top left corner)\n pos[1] += LiteGraph.NODE_TITLE_HEIGHT\n litegraphService.addNodeOnGraph(nodeDef, { pos })\n } else if (node.data instanceof ComfyModelDef) {\n const model = node.data\n const pos = basePos\n const nodeAtPos = comfyApp.canvas.graph?.getNodeOnPos(pos[0], pos[1])\n let targetProvider: ModelNodeProvider | null = null\n let targetGraphNode: LGraphNode | null = null\n if (nodeAtPos) {\n const providers = modelToNodeStore.getAllNodeProviders(\n model.directory\n )\n for (const provider of providers) {\n if (provider.nodeDef.name === nodeAtPos.comfyClass) {\n targetGraphNode = nodeAtPos\n targetProvider = provider\n }\n }\n }\n if (!targetGraphNode) {\n const provider = modelToNodeStore.getNodeProvider(model.directory)\n if (provider) {\n targetGraphNode = litegraphService.addNodeOnGraph(\n provider.nodeDef,\n {\n pos\n }\n )\n targetProvider = provider\n }\n }\n if (targetGraphNode) {\n const widget = targetGraphNode.widgets?.find(\n (widget) => widget.name === targetProvider?.key\n )\n if (widget) {\n widget.value = model.file_name\n }\n }\n } else if (node.data instanceof ComfyWorkflow) {\n const workflow = node.data\n await workflowService.insertWorkflow(workflow, { position: basePos })\n }\n }\n }\n })\n}\n","import { st, te } from '@/i18n'\nimport { legacyMenuCompat } from '@/lib/litegraph/src/contextMenuCompat'\nimport type {\n IContextMenuOptions,\n IContextMenuValue,\n INodeInputSlot,\n IWidget\n} from '@/lib/litegraph/src/litegraph'\nimport { LGraphCanvas, LiteGraph } from '@/lib/litegraph/src/litegraph'\nimport { app } from '@/scripts/app'\nimport { normalizeI18nKey } from '@/utils/formatUtil'\n\n/**\n * Add translation for litegraph context menu.\n */\nexport const useContextMenuTranslation = () => {\n // Install compatibility layer BEFORE any extensions load\n legacyMenuCompat.install(LGraphCanvas.prototype, 'getCanvasMenuOptions')\n\n const { getCanvasMenuOptions } = LGraphCanvas.prototype\n const getCanvasCenterMenuOptions = function (\n this: LGraphCanvas,\n ...args: Parameters<typeof getCanvasMenuOptions>\n ) {\n const res: (IContextMenuValue | null)[] = getCanvasMenuOptions.apply(\n this,\n args\n )\n\n // Add items from new extension API\n const newApiItems = app.collectCanvasMenuItems(this)\n for (const item of newApiItems) {\n res.push(item)\n }\n\n // Add legacy monkey-patched items\n const legacyItems = legacyMenuCompat.extractLegacyItems(\n 'getCanvasMenuOptions',\n this,\n ...args\n )\n for (const item of legacyItems) {\n res.push(item)\n }\n\n // Translate all items\n for (const item of res) {\n if (item?.content) {\n item.content = st(`contextMenu.${item.content}`, item.content)\n }\n }\n return res\n }\n\n LGraphCanvas.prototype.getCanvasMenuOptions = getCanvasCenterMenuOptions\n\n legacyMenuCompat.registerWrapper(\n 'getCanvasMenuOptions',\n getCanvasCenterMenuOptions,\n getCanvasMenuOptions,\n LGraphCanvas.prototype\n )\n\n // Install compatibility layer for getNodeMenuOptions\n legacyMenuCompat.install(LGraphCanvas.prototype, 'getNodeMenuOptions')\n\n // Wrap getNodeMenuOptions to add new API items\n const nodeMenuFn = LGraphCanvas.prototype.getNodeMenuOptions\n const getNodeMenuOptionsWithExtensions = function (\n this: LGraphCanvas,\n ...args: Parameters<typeof nodeMenuFn>\n ) {\n const res = nodeMenuFn.apply(this, args) as (IContextMenuValue | null)[]\n\n // Add items from new extension API\n const node = args[0]\n const newApiItems = app.collectNodeMenuItems(node)\n for (const item of newApiItems) {\n res.push(item)\n }\n\n // Add legacy monkey-patched items\n const legacyItems = legacyMenuCompat.extractLegacyItems(\n 'getNodeMenuOptions',\n this,\n ...args\n )\n for (const item of legacyItems) {\n res.push(item)\n }\n\n return res\n }\n\n LGraphCanvas.prototype.getNodeMenuOptions = getNodeMenuOptionsWithExtensions\n\n legacyMenuCompat.registerWrapper(\n 'getNodeMenuOptions',\n getNodeMenuOptionsWithExtensions,\n nodeMenuFn,\n LGraphCanvas.prototype\n )\n\n function translateMenus(\n values: readonly (IContextMenuValue | string | null)[] | undefined,\n options: IContextMenuOptions\n ) {\n if (!values) return\n const reInput = /Convert (.*) to input/\n const reWidget = /Convert (.*) to widget/\n const cvt = st('contextMenu.Convert ', 'Convert ')\n const tinp = st('contextMenu. to input', ' to input')\n const twgt = st('contextMenu. to widget', ' to widget')\n for (const value of values) {\n if (typeof value === 'string') continue\n\n translateMenus(value?.submenu?.options, options)\n if (!value?.content) {\n continue\n }\n if (te(`contextMenu.${value.content}`)) {\n value.content = st(`contextMenu.${value.content}`, value.content)\n }\n\n // for capture translation text of input and widget\n const extraInfo = (options.extra ||\n options.parentMenu?.options?.extra) as\n | { inputs?: INodeInputSlot[]; widgets?: IWidget[] }\n | undefined\n // widgets and inputs\n const matchInput = value.content?.match(reInput)\n if (matchInput) {\n let match = matchInput[1]\n extraInfo?.inputs?.find((i: INodeInputSlot) => {\n if (i.name != match) return false\n match = i.label ? i.label : i.name\n })\n extraInfo?.widgets?.find((i: IWidget) => {\n if (i.name != match) return false\n match = i.label ? i.label : i.name\n })\n value.content = cvt + match + tinp\n continue\n }\n const matchWidget = value.content?.match(reWidget)\n if (matchWidget) {\n let match = matchWidget[1]\n extraInfo?.inputs?.find((i: INodeInputSlot) => {\n if (i.name != match) return false\n match = i.label ? i.label : i.name\n })\n extraInfo?.widgets?.find((i: IWidget) => {\n if (i.name != match) return false\n match = i.label ? i.label : i.name\n })\n value.content = cvt + match + twgt\n continue\n }\n }\n }\n\n const OriginalContextMenu = LiteGraph.ContextMenu\n function ContextMenu(\n values: (IContextMenuValue | string)[],\n options: IContextMenuOptions\n ) {\n if (options.title) {\n options.title = st(\n `nodeDefs.${normalizeI18nKey(options.title)}.display_name`,\n options.title\n )\n }\n translateMenus(values, options)\n const ctx = new OriginalContextMenu(values, options)\n return ctx\n }\n\n LiteGraph.ContextMenu = ContextMenu as unknown as typeof LiteGraph.ContextMenu\n LiteGraph.ContextMenu.prototype = OriginalContextMenu.prototype\n}\n","import { useEventListener } from '@vueuse/core'\n\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers'\n\nconst clipboardHTMLWrapper = [\n '<meta charset=\"utf-8\"><div><span data-metadata=\"',\n '\"></span></div><span style=\"white-space:pre-wrap;\">Text</span>'\n]\n\n/**\n * Adds a handler on copy that serializes selected nodes to JSON\n */\nexport const useCopy = () => {\n const canvasStore = useCanvasStore()\n\n useEventListener(document, 'copy', (e) => {\n if (shouldIgnoreCopyPaste(e.target)) {\n // Default system copy\n return\n }\n // copy nodes and clear clipboard\n const canvas = canvasStore.canvas\n if (canvas?.selectedItems) {\n const serializedData = canvas.copyToClipboard()\n // Use TextEncoder to handle Unicode characters properly\n const base64Data = btoa(\n String.fromCharCode(\n ...Array.from(new TextEncoder().encode(serializedData))\n )\n )\n // clearData doesn't remove images from clipboard\n e.clipboardData?.setData(\n 'text/html',\n clipboardHTMLWrapper.join(base64Data)\n )\n e.preventDefault()\n e.stopImmediatePropagation()\n return false\n }\n })\n}\n","import {\n ContextMenu,\n DragAndScale,\n LGraph,\n LGraphBadge,\n LGraphCanvas,\n LGraphGroup,\n LGraphNode,\n LLink,\n LiteGraph\n} from '@/lib/litegraph/src/litegraph'\n\n/**\n * Assign all properties of LiteGraph to window to make it backward compatible.\n */\nexport const useGlobalLitegraph = () => {\n // @ts-expect-error fixme ts strict error\n window['LiteGraph'] = LiteGraph\n // @ts-expect-error fixme ts strict error\n window['LGraph'] = LGraph\n // @ts-expect-error fixme ts strict error\n window['LLink'] = LLink\n // @ts-expect-error fixme ts strict error\n window['LGraphNode'] = LGraphNode\n // @ts-expect-error fixme ts strict error\n window['LGraphGroup'] = LGraphGroup\n // @ts-expect-error fixme ts strict error\n window['DragAndScale'] = DragAndScale\n // @ts-expect-error fixme ts strict error\n window['LGraphCanvas'] = LGraphCanvas\n // @ts-expect-error fixme ts strict error\n window['ContextMenu'] = ContextMenu\n // @ts-expect-error fixme ts strict error\n window['LGraphBadge'] = LGraphBadge\n}\n","import { watchEffect } from 'vue'\n\nimport {\n CanvasPointer,\n LGraphNode,\n LiteGraph\n} from '@/lib/litegraph/src/litegraph'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\n\n/**\n * Watch for changes in the setting store and update the LiteGraph settings accordingly.\n */\nexport const useLitegraphSettings = () => {\n const settingStore = useSettingStore()\n const canvasStore = useCanvasStore()\n\n watchEffect(() => {\n const canvasInfoEnabled = settingStore.get('Comfy.Graph.CanvasInfo')\n if (canvasStore.canvas) {\n canvasStore.canvas.show_info = canvasInfoEnabled\n canvasStore.canvas.draw(false, true)\n }\n })\n\n watchEffect(() => {\n const zoomSpeed = settingStore.get('Comfy.Graph.ZoomSpeed')\n if (canvasStore.canvas) {\n canvasStore.canvas.zoom_speed = zoomSpeed\n }\n })\n\n watchEffect(() => {\n LiteGraph.snaps_for_comfy = settingStore.get(\n 'Comfy.Node.AutoSnapLinkToSlot'\n )\n })\n\n watchEffect(() => {\n LiteGraph.snap_highlights_node = settingStore.get(\n 'Comfy.Node.SnapHighlightsNode'\n )\n })\n\n watchEffect(() => {\n LGraphNode.keepAllLinksOnBypass = settingStore.get(\n 'Comfy.Node.BypassAllLinksOnDelete'\n )\n })\n\n watchEffect(() => {\n LiteGraph.middle_click_slot_add_default_node = settingStore.get(\n 'Comfy.Node.MiddleClickRerouteNode'\n )\n })\n\n watchEffect(() => {\n const linkRenderMode = settingStore.get('Comfy.LinkRenderMode')\n if (canvasStore.canvas) {\n canvasStore.canvas.links_render_mode = linkRenderMode\n canvasStore.canvas.setDirty(/* fg */ false, /* bg */ true)\n }\n })\n\n watchEffect(() => {\n const minFontSizeForLOD = settingStore.get(\n 'LiteGraph.Canvas.MinFontSizeForLOD'\n )\n if (canvasStore.canvas) {\n canvasStore.canvas.min_font_size_for_lod = minFontSizeForLOD\n canvasStore.canvas.setDirty(/* fg */ true, /* bg */ true)\n }\n })\n\n watchEffect(() => {\n const linkMarkerShape = settingStore.get('Comfy.Graph.LinkMarkers')\n const { canvas } = canvasStore\n if (canvas) {\n canvas.linkMarkerShape = linkMarkerShape\n canvas.setDirty(false, true)\n }\n })\n\n watchEffect(() => {\n const maximumFps = settingStore.get('LiteGraph.Canvas.MaximumFps')\n const { canvas } = canvasStore\n if (canvas) canvas.maximumFps = maximumFps\n })\n\n watchEffect(() => {\n const dragZoomEnabled = settingStore.get('Comfy.Graph.CtrlShiftZoom')\n const { canvas } = canvasStore\n if (canvas) canvas.dragZoomEnabled = dragZoomEnabled\n })\n\n watchEffect(() => {\n const liveSelection = settingStore.get('Comfy.Graph.LiveSelection')\n const { canvas } = canvasStore\n if (canvas) canvas.liveSelection = liveSelection\n })\n\n watchEffect(() => {\n CanvasPointer.doubleClickTime = settingStore.get(\n 'Comfy.Pointer.DoubleClickTime'\n )\n })\n\n watchEffect(() => {\n CanvasPointer.bufferTime = settingStore.get('Comfy.Pointer.ClickBufferTime')\n })\n\n watchEffect(() => {\n CanvasPointer.maxClickDrift = settingStore.get('Comfy.Pointer.ClickDrift')\n })\n\n watchEffect(() => {\n LiteGraph.CANVAS_GRID_SIZE = settingStore.get('Comfy.SnapToGrid.GridSize')\n })\n\n watchEffect(() => {\n LiteGraph.alwaysSnapToGrid = settingStore.get('pysssss.SnapToGrid')\n })\n\n watchEffect(() => {\n LiteGraph.context_menu_scaling = settingStore.get(\n 'LiteGraph.ContextMenu.Scaling'\n )\n })\n\n watchEffect(() => {\n LiteGraph.Reroute.maxSplineOffset = settingStore.get(\n 'LiteGraph.Reroute.SplineOffset'\n )\n })\n\n watchEffect(() => {\n const navigationMode = settingStore.get('Comfy.Canvas.NavigationMode') as\n | 'standard'\n | 'legacy'\n | 'custom'\n\n LiteGraph.canvasNavigationMode = navigationMode\n LiteGraph.macTrackpadGestures = navigationMode === 'standard'\n })\n\n watchEffect(() => {\n const leftMouseBehavior = settingStore.get(\n 'Comfy.Canvas.LeftMouseClickBehavior'\n ) as 'panning' | 'select'\n LiteGraph.leftMouseClickBehavior = leftMouseBehavior\n })\n\n watchEffect(() => {\n const mouseWheelScroll = settingStore.get(\n 'Comfy.Canvas.MouseWheelScroll'\n ) as 'panning' | 'zoom'\n LiteGraph.mouseWheelScroll = mouseWheelScroll\n })\n\n watchEffect(() => {\n LiteGraph.saveViewportWithGraph = settingStore.get(\n 'Comfy.EnableWorkflowViewRestore'\n )\n })\n}\n","import { LinkMarkerShape, LiteGraph } from '@/lib/litegraph/src/litegraph'\nimport { isCloud } from '@/platform/distribution/types'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport type { SettingParams } from '@/platform/settings/types'\nimport type { ColorPalettes } from '@/schemas/colorPaletteSchema'\nimport type { Keybinding } from '@/schemas/keyBindingSchema'\nimport { NodeBadgeMode } from '@/types/nodeSource'\nimport { LinkReleaseTriggerAction } from '@/types/searchBoxTypes'\nimport { breakpointsTailwind } from '@vueuse/core'\n\n/**\n * Core settings are essential configuration parameters required for ComfyUI's basic functionality.\n * These settings must be present in the settings store and cannot be omitted.\n *\n * IMPORTANT: To prevent ID conflicts, settings should be marked as deprecated rather than removed\n * when they are no longer needed.\n */\nexport const CORE_SETTINGS: SettingParams[] = [\n {\n id: 'Comfy.Memory.AllowManualUnload',\n name: 'Allow manual unload of models and execution cache via user command',\n type: 'hidden',\n defaultValue: isCloud ? false : true,\n versionAdded: '1.18.0'\n },\n {\n id: 'Comfy.Validation.Workflows',\n name: 'Validate workflows',\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'Comfy.NodeSearchBoxImpl',\n category: ['Comfy', 'Node Search Box', 'Implementation'],\n experimental: true,\n name: 'Node search box implementation',\n type: 'combo',\n options: ['default', 'litegraph (legacy)'],\n defaultValue: 'default'\n },\n {\n id: 'Comfy.LinkRelease.Action',\n category: ['LiteGraph', 'LinkRelease', 'Action'],\n name: 'Action on link release (No modifier)',\n type: 'combo',\n options: Object.values(LinkReleaseTriggerAction),\n defaultValue: LinkReleaseTriggerAction.CONTEXT_MENU,\n defaultsByInstallVersion: {\n '1.24.1': LinkReleaseTriggerAction.SEARCH_BOX\n }\n },\n {\n id: 'Comfy.LinkRelease.ActionShift',\n category: ['LiteGraph', 'LinkRelease', 'ActionShift'],\n name: 'Action on link release (Shift)',\n type: 'combo',\n options: Object.values(LinkReleaseTriggerAction),\n defaultValue: LinkReleaseTriggerAction.SEARCH_BOX,\n defaultsByInstallVersion: {\n '1.24.1': LinkReleaseTriggerAction.CONTEXT_MENU\n }\n },\n {\n id: 'Comfy.NodeSearchBoxImpl.NodePreview',\n category: ['Comfy', 'Node Search Box', 'NodePreview'],\n name: 'Node preview',\n tooltip: 'Only applies to the default implementation',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'Comfy.NodeSearchBoxImpl.ShowCategory',\n category: ['Comfy', 'Node Search Box', 'ShowCategory'],\n name: 'Show node category in search results',\n tooltip: 'Only applies to the default implementation',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'Comfy.NodeSearchBoxImpl.ShowIdName',\n category: ['Comfy', 'Node Search Box', 'ShowIdName'],\n name: 'Show node id name in search results',\n tooltip: 'Only applies to the default implementation',\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'Comfy.NodeSearchBoxImpl.ShowNodeFrequency',\n category: ['Comfy', 'Node Search Box', 'ShowNodeFrequency'],\n name: 'Show node frequency in search results',\n tooltip: 'Only applies to the default implementation',\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'Comfy.Sidebar.Location',\n category: ['Appearance', 'Sidebar', 'Location'],\n name: 'Sidebar location',\n type: 'combo',\n options: ['left', 'right'],\n defaultValue: 'left'\n },\n {\n id: 'Comfy.Sidebar.Size',\n category: ['Appearance', 'Sidebar', 'Size'],\n name: 'Sidebar size',\n type: 'combo',\n options: ['normal', 'small'],\n // Default to small if the window is less than 1536px(2xl) wide.\n defaultValue: () => (window.innerWidth < 1536 ? 'small' : 'normal')\n },\n {\n id: 'Comfy.Sidebar.UnifiedWidth',\n category: ['Appearance', 'Sidebar', 'UnifiedWidth'],\n name: 'Unified sidebar width',\n type: 'boolean',\n defaultValue: true,\n versionAdded: '1.18.1'\n },\n {\n id: 'Comfy.Sidebar.Style',\n category: ['Appearance', 'Sidebar', 'Style'],\n name: 'Sidebar style',\n type: 'combo',\n options: ['floating', 'connected'],\n defaultValue: 'connected'\n },\n {\n id: 'Comfy.TextareaWidget.FontSize',\n category: ['Appearance', 'Node Widget', 'TextareaWidget', 'FontSize'],\n name: 'Textarea widget font size',\n type: 'slider',\n defaultValue: 10,\n attrs: {\n min: 8,\n max: 24\n }\n },\n {\n id: 'Comfy.TextareaWidget.Spellcheck',\n category: ['Comfy', 'Node Widget', 'TextareaWidget', 'Spellcheck'],\n name: 'Textarea widget spellcheck',\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'Comfy.Workflow.SortNodeIdOnSave',\n name: 'Sort node IDs when saving workflow',\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'Comfy.Canvas.NavigationMode',\n category: ['LiteGraph', 'Canvas Navigation', 'NavigationMode'],\n name: 'Navigation Mode',\n defaultValue: 'legacy',\n type: 'combo',\n sortOrder: 100,\n options: [\n { value: 'standard', text: 'Standard (New)' },\n { value: 'legacy', text: 'Drag Navigation' },\n { value: 'custom', text: 'Custom' }\n ],\n versionAdded: '1.25.0',\n defaultsByInstallVersion: {\n '1.25.0': 'legacy'\n },\n onChange: async (newValue: string, oldValue?: string) => {\n if (!oldValue) return\n const settingStore = useSettingStore()\n\n if (newValue === 'standard') {\n // Update related settings to match standard mode - select + panning\n await settingStore.set('Comfy.Canvas.LeftMouseClickBehavior', 'select')\n await settingStore.set('Comfy.Canvas.MouseWheelScroll', 'panning')\n } else if (newValue === 'legacy') {\n // Update related settings to match legacy mode - panning + zoom\n await settingStore.set('Comfy.Canvas.LeftMouseClickBehavior', 'panning')\n await settingStore.set('Comfy.Canvas.MouseWheelScroll', 'zoom')\n }\n }\n },\n {\n id: 'Comfy.Canvas.LeftMouseClickBehavior',\n category: ['LiteGraph', 'Canvas Navigation', 'LeftMouseClickBehavior'],\n name: 'Left Mouse Click Behavior',\n defaultValue: 'panning',\n type: 'radio',\n sortOrder: 50,\n options: [\n { value: 'panning', text: 'Panning' },\n { value: 'select', text: 'Select' }\n ],\n versionAdded: '1.27.4',\n onChange: async (newValue: string) => {\n const settingStore = useSettingStore()\n\n const navigationMode = settingStore.get('Comfy.Canvas.NavigationMode')\n\n if (navigationMode !== 'custom') {\n if (\n (newValue === 'select' && navigationMode === 'standard') ||\n (newValue === 'panning' && navigationMode === 'legacy')\n ) {\n return\n }\n\n // only set to custom if it doesn't match the preset modes\n await settingStore.set('Comfy.Canvas.NavigationMode', 'custom')\n }\n }\n },\n {\n id: 'Comfy.Canvas.MouseWheelScroll',\n category: ['LiteGraph', 'Canvas Navigation', 'MouseWheelScroll'],\n name: 'Mouse Wheel Scroll',\n defaultValue: 'zoom',\n type: 'radio',\n options: [\n { value: 'panning', text: 'Panning' },\n { value: 'zoom', text: 'Zoom in/out' }\n ],\n versionAdded: '1.27.4',\n onChange: async (newValue: string) => {\n const settingStore = useSettingStore()\n\n const navigationMode = settingStore.get('Comfy.Canvas.NavigationMode')\n\n if (navigationMode !== 'custom') {\n if (\n (newValue === 'panning' && navigationMode === 'standard') ||\n (newValue === 'zoom' && navigationMode === 'legacy')\n ) {\n return\n }\n\n // only set to custom if it doesn't match the preset modes\n await settingStore.set('Comfy.Canvas.NavigationMode', 'custom')\n }\n }\n },\n {\n id: 'Comfy.Graph.CanvasInfo',\n category: ['LiteGraph', 'Canvas', 'CanvasInfo'],\n name: 'Show canvas info on bottom left corner (fps, etc.)',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'Comfy.Node.ShowDeprecated',\n name: 'Show deprecated nodes in search',\n tooltip:\n 'Deprecated nodes are hidden by default in the UI, but remain functional in existing workflows that use them.',\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'Comfy.Node.ShowExperimental',\n name: 'Show experimental nodes in search',\n tooltip:\n 'Experimental nodes are marked as such in the UI and may be subject to significant changes or removal in future versions. Use with caution in production workflows',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'Comfy.Node.Opacity',\n category: ['Appearance', 'Node', 'Opacity'],\n name: 'Node opacity',\n type: 'slider',\n defaultValue: 1,\n attrs: {\n min: 0.01,\n max: 1,\n step: 0.01\n }\n },\n {\n id: 'Comfy.Workflow.ShowMissingNodesWarning',\n name: 'Show missing nodes warning',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'Comfy.Workflow.ShowMissingModelsWarning',\n name: 'Show missing models warning',\n type: isCloud ? 'hidden' : 'boolean',\n defaultValue: isCloud ? false : true,\n experimental: true\n },\n {\n id: 'Comfy.Workflow.WarnBlueprintOverwrite',\n name: 'Require confirmation to overwrite an existing subgraph blueprint',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'Comfy.Graph.ZoomSpeed',\n category: ['LiteGraph', 'Canvas', 'ZoomSpeed'],\n name: 'Canvas zoom speed',\n type: 'slider',\n defaultValue: 1.1,\n attrs: {\n min: 1.01,\n max: 2.5,\n step: 0.01\n }\n },\n // Bookmarks are stored in the settings store.\n // Bookmarks are in format of category/display_name. e.g. \"conditioning/CLIPTextEncode\"\n {\n id: 'Comfy.NodeLibrary.Bookmarks',\n name: 'Node library bookmarks with display name (deprecated)',\n type: 'hidden',\n defaultValue: [],\n deprecated: true\n },\n {\n id: 'Comfy.NodeLibrary.Bookmarks.V2',\n name: 'Node library bookmarks v2 with unique name',\n type: 'hidden',\n defaultValue: []\n },\n // Stores mapping from bookmark folder name to its customization.\n {\n id: 'Comfy.NodeLibrary.BookmarksCustomization',\n name: 'Node library bookmarks customization',\n type: 'hidden',\n defaultValue: {}\n },\n {\n id: 'Comfy.GroupSelectedNodes.Padding',\n category: ['LiteGraph', 'Group', 'Padding'],\n name: 'Group selected nodes padding',\n type: 'slider',\n defaultValue: 10,\n attrs: {\n min: 0,\n max: 100\n }\n },\n {\n id: 'Comfy.Node.DoubleClickTitleToEdit',\n category: ['LiteGraph', 'Node', 'DoubleClickTitleToEdit'],\n name: 'Double click node title to edit',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'Comfy.Node.AllowImageSizeDraw',\n category: ['LiteGraph', 'Node Widget', 'AllowImageSizeDraw'],\n name: 'Show width × height below the image preview',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'Comfy.Group.DoubleClickTitleToEdit',\n category: ['LiteGraph', 'Group', 'DoubleClickTitleToEdit'],\n name: 'Double click group title to edit',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'Comfy.Window.UnloadConfirmation',\n name: 'Show confirmation when closing window',\n type: 'boolean',\n defaultValue: true,\n versionModified: '1.7.12'\n },\n {\n id: 'Comfy.TreeExplorer.ItemPadding',\n category: ['Appearance', 'Tree Explorer', 'ItemPadding'],\n name: 'Tree explorer item padding',\n type: 'slider',\n defaultValue: 2,\n attrs: {\n min: 0,\n max: 8,\n step: 1\n }\n },\n {\n id: 'Comfy.ModelLibrary.AutoLoadAll',\n name: 'Automatically load all model folders',\n tooltip:\n 'If true, all folders will load as soon as you open the model library (this may cause delays while it loads). If false, root level model folders will only load once you click on them.',\n type: isCloud ? 'hidden' : 'boolean',\n defaultValue: false\n },\n {\n id: 'Comfy.ModelLibrary.NameFormat',\n name: 'What name to display in the model library tree view',\n tooltip:\n 'Select \"filename\" to render a simplified view of the raw filename (without directory or \".safetensors\" extension) in the model list. Select \"title\" to display the configurable model metadata title.',\n type: 'combo',\n options: ['filename', 'title'],\n defaultValue: 'title'\n },\n {\n id: 'Comfy.Locale',\n name: 'Language',\n type: 'combo',\n options: [\n { value: 'en', text: 'English' },\n { value: 'zh', text: '中文' },\n { value: 'zh-TW', text: '繁體中文' },\n { value: 'ru', text: 'Русский' },\n { value: 'ja', text: '日本語' },\n { value: 'ko', text: '한국어' },\n { value: 'fr', text: 'Français' },\n { value: 'es', text: 'Español' },\n { value: 'ar', text: 'عربي' },\n { value: 'tr', text: 'Türkçe' },\n { value: 'pt-BR', text: 'Português (BR)' },\n { value: 'fa', text: 'فارسی' }\n ],\n defaultValue: () => navigator.language.split('-')[0] || 'en'\n },\n {\n id: 'Comfy.NodeBadge.NodeSourceBadgeMode',\n category: ['LiteGraph', 'Node', 'NodeSourceBadgeMode'],\n name: 'Node source badge mode',\n type: 'combo',\n options: Object.values(NodeBadgeMode),\n defaultValue: NodeBadgeMode.HideBuiltIn\n },\n {\n id: 'Comfy.NodeBadge.NodeIdBadgeMode',\n category: ['LiteGraph', 'Node', 'NodeIdBadgeMode'],\n name: 'Node ID badge mode',\n type: 'combo',\n options: [NodeBadgeMode.None, NodeBadgeMode.ShowAll],\n defaultValue: NodeBadgeMode.None\n },\n {\n id: 'Comfy.NodeBadge.NodeLifeCycleBadgeMode',\n category: ['LiteGraph', 'Node', 'NodeLifeCycleBadgeMode'],\n name: 'Node life cycle badge mode',\n type: 'combo',\n options: [NodeBadgeMode.None, NodeBadgeMode.ShowAll],\n defaultValue: NodeBadgeMode.ShowAll\n },\n {\n id: 'Comfy.NodeBadge.ShowApiPricing',\n category: ['Comfy', 'API Nodes'],\n name: 'Show API node pricing badge',\n type: 'boolean',\n defaultValue: true,\n versionAdded: '1.20.3'\n },\n {\n id: 'Comfy.Notification.ShowVersionUpdates',\n category: ['Comfy', 'Notification Preferences'],\n name: 'Show version updates',\n tooltip: 'Show updates for new models, and major new features.',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'Comfy.ConfirmClear',\n category: ['Comfy', 'Workflow', 'ConfirmClear'],\n name: 'Require confirmation when clearing workflow',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'Comfy.PromptFilename',\n category: ['Comfy', 'Workflow', 'PromptFilename'],\n name: 'Prompt for filename when saving workflow',\n type: 'boolean',\n defaultValue: true\n },\n /**\n * file format for preview\n *\n * format;quality\n *\n * ex)\n * webp;50 -> webp, quality 50\n * jpeg;80 -> rgb, jpeg, quality 80\n *\n * @type {string}\n */\n {\n id: 'Comfy.PreviewFormat',\n category: ['LiteGraph', 'Node Widget', 'PreviewFormat'],\n name: 'Preview image format',\n tooltip:\n 'When displaying a preview in the image widget, convert it to a lightweight image, e.g. webp, jpeg, webp;50, etc.',\n type: 'text',\n defaultValue: ''\n },\n {\n id: 'Comfy.DisableSliders',\n category: ['LiteGraph', 'Node Widget', 'DisableSliders'],\n name: 'Disable node widget sliders',\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'Comfy.DisableFloatRounding',\n category: ['LiteGraph', 'Node Widget', 'DisableFloatRounding'],\n name: 'Disable default float widget rounding.',\n tooltip:\n '(requires page reload) Cannot disable round when round is set by the node in the backend.',\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'Comfy.FloatRoundingPrecision',\n category: ['LiteGraph', 'Node Widget', 'FloatRoundingPrecision'],\n name: 'Float widget rounding decimal places [0 = auto].',\n tooltip: '(requires page reload)',\n type: 'slider',\n attrs: {\n min: 0,\n max: 6,\n step: 1\n },\n defaultValue: 0\n },\n {\n id: 'LiteGraph.Node.TooltipDelay',\n name: 'Tooltip Delay',\n type: 'number',\n attrs: {\n min: 100,\n max: 3000,\n step: 50\n },\n defaultValue: 500,\n versionAdded: '1.9.0'\n },\n {\n id: 'Comfy.EnableTooltips',\n category: ['LiteGraph', 'Node', 'EnableTooltips'],\n name: 'Enable Tooltips',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'Comfy.DevMode',\n name: 'Enable dev mode options (API save, etc.)',\n type: 'boolean',\n defaultValue: false,\n onChange: (value) => {\n const element = document.getElementById('comfy-dev-save-api-button')\n if (element) {\n element.style.display = value ? 'flex' : 'none'\n }\n }\n },\n {\n id: 'Comfy.UI.TabBarLayout',\n category: ['Appearance', 'General'],\n name: 'Tab Bar Layout',\n type: 'combo',\n options: ['Default', 'Integrated'],\n tooltip:\n 'Controls the layout of the tab bar. \"Integrated\" moves Help and User controls into the tab bar area.',\n defaultValue: 'Default'\n },\n {\n id: 'Comfy.UseNewMenu',\n category: ['Comfy', 'Menu', 'UseNewMenu'],\n defaultValue: 'Top',\n name: 'Use new menu',\n type: 'combo',\n options: ['Disabled', 'Top'],\n tooltip: 'Enable the redesigned top menu bar.',\n migrateDeprecatedValue: (value: string) => {\n // Floating is now supported by dragging the docked actionbar off.\n if (value === 'Floating') {\n return 'Top'\n } else if (value === 'Bottom') {\n return 'Top'\n }\n return value\n }\n },\n {\n id: 'Comfy.Workflow.WorkflowTabsPosition',\n name: 'Opened workflows position',\n type: 'combo',\n options: ['Sidebar', 'Topbar'],\n defaultValue: 'Topbar',\n migrateDeprecatedValue: (value: string) => {\n if (value === 'Topbar (2nd-row)') {\n return 'Topbar'\n }\n return value\n }\n },\n {\n id: 'Comfy.Graph.CanvasMenu',\n category: ['LiteGraph', 'Canvas', 'CanvasMenu'],\n name: 'Show graph canvas menu',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'Comfy.QueueButton.BatchCountLimit',\n name: 'Batch count limit',\n tooltip:\n 'The maximum number of tasks added to the queue at one button click',\n type: 'number',\n defaultValue: isCloud ? 4 : 100,\n versionAdded: '1.3.5'\n },\n {\n id: 'Comfy.Keybinding.UnsetBindings',\n name: 'Keybindings unset by the user',\n type: 'hidden',\n defaultValue: [] as Keybinding[],\n versionAdded: '1.3.7',\n versionModified: '1.7.3',\n migrateDeprecatedValue: (\n value: (Keybinding & { targetSelector?: string })[]\n ) => {\n return value.map((keybinding) => {\n if (keybinding.targetSelector === '#graph-canvas') {\n keybinding.targetElementId = 'graph-canvas-container'\n }\n return keybinding\n })\n }\n },\n {\n id: 'Comfy.Keybinding.NewBindings',\n name: 'Keybindings set by the user',\n type: 'hidden',\n defaultValue: [] as Keybinding[],\n versionAdded: '1.3.7'\n },\n {\n id: 'Comfy.Extension.Disabled',\n name: 'Disabled extension names',\n type: 'hidden',\n defaultValue: [] as string[],\n versionAdded: '1.3.11'\n },\n {\n id: 'Comfy.LinkRenderMode',\n category: ['LiteGraph', 'Graph', 'LinkRenderMode'],\n name: 'Link Render Mode',\n defaultValue: 2,\n type: 'combo',\n options: [\n { value: LiteGraph.STRAIGHT_LINK, text: 'Straight' },\n { value: LiteGraph.LINEAR_LINK, text: 'Linear' },\n { value: LiteGraph.SPLINE_LINK, text: 'Spline' },\n { value: LiteGraph.HIDDEN_LINK, text: 'Hidden' }\n ]\n },\n {\n id: 'Comfy.Node.AutoSnapLinkToSlot',\n category: ['LiteGraph', 'Node', 'AutoSnapLinkToSlot'],\n name: 'Auto snap link to node slot',\n tooltip:\n 'When dragging a link over a node, the link automatically snap to a viable input slot on the node',\n type: 'boolean',\n defaultValue: true,\n versionAdded: '1.3.29'\n },\n {\n id: 'Comfy.Node.SnapHighlightsNode',\n category: ['LiteGraph', 'Node', 'SnapHighlightsNode'],\n name: 'Snap highlights node',\n tooltip:\n 'When dragging a link over a node with viable input slot, highlight the node',\n type: 'boolean',\n defaultValue: true,\n versionAdded: '1.3.29'\n },\n {\n id: 'Comfy.Node.BypassAllLinksOnDelete',\n category: ['LiteGraph', 'Node', 'BypassAllLinksOnDelete'],\n name: 'Keep all links when deleting nodes',\n tooltip:\n 'When deleting a node, attempt to reconnect all of its input and output links (bypassing the deleted node)',\n type: 'boolean',\n defaultValue: true,\n versionAdded: '1.3.40'\n },\n {\n id: 'Comfy.Node.MiddleClickRerouteNode',\n category: ['LiteGraph', 'Node', 'MiddleClickRerouteNode'],\n name: 'Middle-click creates a new Reroute node',\n type: 'boolean',\n defaultValue: true,\n versionAdded: '1.3.42'\n },\n {\n id: 'Comfy.Graph.LinkMarkers',\n category: ['LiteGraph', 'Link', 'LinkMarkers'],\n name: 'Link midpoint markers',\n defaultValue: LinkMarkerShape.Circle,\n type: 'combo',\n options: [\n { value: LinkMarkerShape.None, text: 'None' },\n { value: LinkMarkerShape.Circle, text: 'Circle' },\n { value: LinkMarkerShape.Arrow, text: 'Arrow' }\n ],\n versionAdded: '1.3.42'\n },\n {\n id: 'Comfy.DOMClippingEnabled',\n category: ['LiteGraph', 'Node', 'DOMClippingEnabled'],\n name: 'Enable DOM element clipping (enabling may reduce performance)',\n type: 'boolean',\n defaultValue: true\n },\n {\n id: 'Comfy.Graph.CtrlShiftZoom',\n category: ['LiteGraph', 'Canvas', 'CtrlShiftZoom'],\n name: 'Enable fast-zoom shortcut (Ctrl + Shift + Drag)',\n type: 'boolean',\n defaultValue: true,\n versionAdded: '1.4.0'\n },\n {\n id: 'Comfy.Graph.LiveSelection',\n category: ['LiteGraph', 'Canvas', 'LiveSelection'],\n name: 'Live selection',\n tooltip:\n 'When enabled, nodes are selected/deselected in real-time as you drag the selection rectangle, similar to other design tools.',\n type: 'boolean',\n defaultValue: false,\n versionAdded: '1.36.1'\n },\n {\n id: 'Comfy.Pointer.ClickDrift',\n category: ['LiteGraph', 'Pointer', 'ClickDrift'],\n name: 'Pointer click drift (maximum distance)',\n tooltip:\n 'If the pointer moves more than this distance while holding a button down, it is considered dragging (rather than clicking).\\n\\nHelps prevent objects from being unintentionally nudged if the pointer is moved whilst clicking.',\n experimental: true,\n type: 'slider',\n attrs: {\n min: 0,\n max: 20,\n step: 1\n },\n defaultValue: 6,\n versionAdded: '1.4.3'\n },\n {\n id: 'Comfy.Pointer.ClickBufferTime',\n category: ['LiteGraph', 'Pointer', 'ClickBufferTime'],\n name: 'Pointer click drift delay',\n tooltip:\n 'After pressing a pointer button down, this is the maximum time (in milliseconds) that pointer movement can be ignored for.\\n\\nHelps prevent objects from being unintentionally nudged if the pointer is moved whilst clicking.',\n experimental: true,\n type: 'slider',\n attrs: {\n min: 0,\n max: 1000,\n step: 25\n },\n defaultValue: 150,\n versionAdded: '1.4.3'\n },\n {\n id: 'Comfy.Pointer.DoubleClickTime',\n category: ['LiteGraph', 'Pointer', 'DoubleClickTime'],\n name: 'Double click interval (maximum)',\n tooltip:\n 'The maximum time in milliseconds between the two clicks of a double-click. Increasing this value may assist if double-clicks are sometimes not registered.',\n type: 'slider',\n attrs: {\n min: 100,\n max: 1000,\n step: 50\n },\n defaultValue: 300,\n versionAdded: '1.4.3'\n },\n {\n id: 'Comfy.SnapToGrid.GridSize',\n category: ['LiteGraph', 'Canvas', 'GridSize'],\n name: 'Snap to grid size',\n type: 'slider',\n attrs: {\n min: 1,\n max: 500\n },\n tooltip:\n 'When dragging and resizing nodes while holding shift they will be aligned to the grid, this controls the size of that grid.',\n defaultValue: LiteGraph.CANVAS_GRID_SIZE\n },\n // Keep the 'pysssss.SnapToGrid' setting id so we don't need to migrate setting values.\n // Using a new setting id can cause existing users to lose their existing settings.\n {\n id: 'pysssss.SnapToGrid',\n category: ['LiteGraph', 'Canvas', 'AlwaysSnapToGrid'],\n name: 'Always snap to grid',\n type: 'boolean',\n defaultValue: false,\n versionAdded: '1.3.13'\n },\n {\n id: 'Comfy.Server.ServerConfigValues',\n name: 'Server config values for frontend display',\n tooltip: 'Server config values used for frontend display only',\n type: 'hidden',\n // Mapping from server config id to value.\n defaultValue: {} as Record<string, unknown>,\n versionAdded: '1.4.8'\n },\n {\n id: 'Comfy.Server.LaunchArgs',\n name: 'Server launch arguments',\n tooltip:\n 'These are the actual arguments that are passed to the server when it is launched.',\n type: 'hidden',\n defaultValue: {} as Record<string, string>,\n versionAdded: '1.4.8'\n },\n {\n id: 'Comfy.Queue.MaxHistoryItems',\n name: 'Queue history size',\n tooltip: 'The maximum number of tasks that show in the queue history.',\n type: 'slider',\n attrs: {\n min: 2,\n max: 256,\n step: 2\n },\n defaultValue: 64,\n versionAdded: '1.4.12'\n },\n {\n id: 'Comfy.Queue.History.Expanded',\n name: 'Queue history expanded',\n type: 'hidden',\n defaultValue: false,\n versionAdded: '1.37.0'\n },\n {\n id: 'Comfy.Execution.PreviewMethod',\n category: ['Comfy', 'Execution', 'PreviewMethod'],\n name: 'Live preview method',\n tooltip:\n 'Live preview method during image generation. \"default\" uses the server CLI setting.',\n type: 'combo',\n options: ['default', 'none', 'auto', 'latent2rgb', 'taesd'],\n defaultValue: 'default',\n versionAdded: '1.36.0'\n },\n {\n id: 'LiteGraph.Canvas.MaximumFps',\n name: 'Maximum FPS',\n tooltip:\n 'The maximum frames per second that the canvas is allowed to render. Caps GPU usage at the cost of smoothness. If 0, the screen refresh rate is used. Default: 0',\n type: 'slider',\n attrs: {\n min: 0,\n max: 120\n },\n defaultValue: 0,\n versionAdded: '1.5.1'\n },\n {\n id: 'Comfy.EnableWorkflowViewRestore',\n category: ['Comfy', 'Workflow', 'EnableWorkflowViewRestore'],\n name: 'Save and restore canvas position and zoom level in workflows',\n type: 'boolean',\n defaultValue: true,\n versionModified: '1.5.4'\n },\n {\n id: 'Comfy.Workflow.ConfirmDelete',\n name: 'Show confirmation when deleting workflows',\n type: 'boolean',\n defaultValue: true,\n versionAdded: '1.5.6'\n },\n {\n id: 'Comfy.ColorPalette',\n name: 'The active color palette id',\n type: 'hidden',\n defaultValue: 'dark',\n versionModified: '1.6.7',\n migrateDeprecatedValue(value: string) {\n // Legacy custom palettes were prefixed with 'custom_'\n return value.startsWith('custom_') ? value.replace('custom_', '') : value\n }\n },\n {\n id: 'Comfy.CustomColorPalettes',\n name: 'Custom color palettes',\n type: 'hidden',\n defaultValue: {} as ColorPalettes,\n versionModified: '1.6.7'\n },\n {\n id: 'Comfy.WidgetControlMode',\n category: ['Comfy', 'Node Widget', 'WidgetControlMode'],\n name: 'Widget control mode',\n tooltip:\n 'Controls when widget values are updated (randomize/increment/decrement), either before the prompt is queued or after.',\n type: 'combo',\n defaultValue: 'after',\n options: ['before', 'after'],\n versionModified: '1.6.10'\n },\n {\n id: 'Comfy.TutorialCompleted',\n name: 'Tutorial completed',\n type: 'hidden',\n defaultValue: false,\n versionAdded: '1.8.7'\n },\n {\n id: 'Comfy.InstalledVersion',\n name: 'The frontend version that was running when the user first installed ComfyUI',\n type: 'hidden',\n defaultValue: null,\n versionAdded: '1.24.0'\n },\n {\n id: 'LiteGraph.ContextMenu.Scaling',\n name: 'Scale node combo widget menus (lists) when zoomed in',\n defaultValue: false,\n type: 'boolean',\n versionAdded: '1.8.8'\n },\n\n {\n id: 'LiteGraph.Canvas.LowQualityRenderingZoomThreshold',\n type: 'hidden',\n deprecated: true,\n name: 'Low quality rendering zoom threshold (deprecated)',\n tooltip:\n 'Zoom level threshold for performance mode. Lower values (0.1) = quality at all zoom levels. Higher values (1.0) = performance mode even when zoomed in. Performance mode simplifies rendering by hiding text labels, shadows, and details.',\n attrs: {\n min: 0.1,\n max: 1,\n step: 0.01\n },\n defaultValue: 0.6,\n versionAdded: '1.9.1',\n versionModified: '1.26.7'\n },\n {\n id: 'LiteGraph.Canvas.MinFontSizeForLOD',\n name: 'Zoom Node Level of Detail - font size threshold',\n tooltip:\n 'Controls when the nodes switch to low quality LOD rendering. Uses font size in pixels to determine when to switch. Set to 0 to disable. Values 1-24 set the minimum font size threshold for LOD - higher values (24px) = switch nodes to simplified rendering sooner when zooming out, lower values (1px) = maintain full node quality longer.',\n type: 'slider',\n attrs: {\n min: 0,\n max: 24,\n step: 1\n },\n defaultValue: 8,\n versionAdded: '1.26.7',\n hideInVueNodes: true\n },\n {\n id: 'Comfy.Canvas.SelectionToolbox',\n category: ['LiteGraph', 'Canvas', 'SelectionToolbox'],\n name: 'Show selection toolbox',\n type: 'boolean',\n defaultValue: true,\n versionAdded: '1.10.5'\n },\n {\n id: 'LiteGraph.Reroute.SplineOffset',\n name: 'Reroute spline offset',\n tooltip: 'The bezier control point offset from the reroute centre point',\n type: 'slider',\n defaultValue: 20,\n attrs: {\n min: 0,\n max: 400\n },\n versionAdded: '1.15.7'\n },\n {\n id: 'Comfy.Toast.DisableReconnectingToast',\n name: 'Disable toasts when reconnecting or reconnected',\n type: 'hidden',\n defaultValue: false,\n versionAdded: '1.15.12'\n },\n {\n id: 'Comfy.Minimap.Visible',\n name: 'Display minimap on canvas',\n type: 'hidden',\n defaultValue: window.innerWidth >= breakpointsTailwind.lg,\n versionAdded: '1.25.0'\n },\n {\n id: 'Comfy.Minimap.NodeColors',\n name: 'Display node with its original color on minimap',\n type: 'hidden',\n defaultValue: false,\n versionAdded: '1.26.0'\n },\n {\n id: 'Comfy.Minimap.ShowLinks',\n name: 'Display links on minimap',\n type: 'hidden',\n defaultValue: true,\n versionAdded: '1.26.0'\n },\n {\n id: 'Comfy.Minimap.ShowGroups',\n name: 'Display node groups on minimap',\n type: 'hidden',\n defaultValue: true,\n versionAdded: '1.26.0'\n },\n {\n id: 'Comfy.Minimap.RenderBypassState',\n name: 'Render bypass state on minimap',\n type: 'hidden',\n defaultValue: true,\n versionAdded: '1.26.0'\n },\n {\n id: 'Comfy.Minimap.RenderErrorState',\n name: 'Render error state on minimap',\n type: 'hidden',\n defaultValue: true,\n versionAdded: '1.26.0'\n },\n {\n id: 'Comfy.Workflow.AutoSaveDelay',\n name: 'Auto Save Delay (ms)',\n defaultValue: 1000,\n type: 'number',\n tooltip: 'Only applies if Auto Save is set to \"after delay\".',\n versionAdded: '1.16.0'\n },\n {\n id: 'Comfy.Workflow.AutoSave',\n name: 'Auto Save',\n type: 'combo',\n options: ['off', 'after delay'], // Room for other options like on focus change, tab change, window change\n defaultValue: 'off', // Popular request by users (https://github.com/Comfy-Org/ComfyUI_frontend/issues/1584#issuecomment-2536610154)\n versionAdded: '1.16.0'\n },\n {\n id: 'Comfy.Workflow.Persist',\n name: 'Persist workflow state and restore on page (re)load',\n type: 'boolean',\n defaultValue: true,\n versionAdded: '1.16.1'\n },\n {\n id: 'LiteGraph.Node.DefaultPadding',\n name: 'Always shrink new nodes',\n tooltip:\n 'Resize nodes to the smallest possible size when created. When disabled, a newly added node will be widened slightly to show widget values.',\n type: 'boolean',\n defaultValue: false,\n versionAdded: '1.18.0'\n },\n {\n id: 'Comfy.Canvas.BackgroundImage',\n category: ['Appearance', 'Canvas', 'Background'],\n name: 'Canvas background image',\n type: 'backgroundImage',\n tooltip:\n 'Image URL for the canvas background. You can right-click an image in the outputs panel and select \"Set as Background\" to use it, or upload your own image using the upload button.',\n defaultValue: '',\n versionAdded: '1.20.4',\n versionModified: '1.20.5'\n },\n // Release data stored in settings\n {\n id: 'Comfy.Release.Version',\n name: 'Last seen release version',\n type: 'hidden',\n defaultValue: ''\n },\n {\n id: 'Comfy.Release.Status',\n name: 'Release status',\n type: 'hidden',\n defaultValue: 'skipped'\n },\n {\n id: 'Comfy.Release.Timestamp',\n name: 'Release seen timestamp',\n type: 'hidden',\n defaultValue: 0\n },\n\n /**\n * Template Library Filter Settings\n */\n {\n id: 'Comfy.Templates.SelectedModels',\n name: 'Template library - Selected model filters',\n type: 'hidden',\n defaultValue: []\n },\n {\n id: 'Comfy.Templates.SelectedUseCases',\n name: 'Template library - Selected use case filters',\n type: 'hidden',\n defaultValue: []\n },\n {\n id: 'Comfy.Templates.SelectedRunsOn',\n name: 'Template library - Selected runs on filters',\n type: 'hidden',\n defaultValue: []\n },\n {\n id: 'Comfy.Templates.SortBy',\n name: 'Template library - Sort preference',\n type: 'hidden',\n defaultValue: 'default'\n },\n\n /**\n * Nodes 2.0 Settings\n */\n {\n id: 'Comfy.VueNodes.Enabled',\n category: ['Comfy', 'Nodes 2.0', 'VueNodesEnabled'],\n name: 'Modern Node Design (Nodes 2.0)',\n type: 'boolean',\n tooltip:\n 'Modern: DOM-based rendering with enhanced interactivity, native browser features, and updated visual design. Classic: Traditional canvas rendering.',\n defaultValue: false,\n sortOrder: 100,\n experimental: true,\n versionAdded: '1.27.1'\n },\n {\n id: 'Comfy.VueNodes.AutoScaleLayout',\n category: ['Comfy', 'Nodes 2.0', 'AutoScaleLayout'],\n name: 'Auto-scale layout (Nodes 2.0)',\n tooltip:\n 'Automatically scale node positions when switching to Nodes 2.0 rendering to prevent overlap',\n type: 'boolean',\n sortOrder: 50,\n experimental: true,\n defaultValue: true,\n versionAdded: '1.30.3'\n },\n {\n id: 'Comfy.Assets.UseAssetAPI',\n name: 'Use Asset API for model library',\n type: 'hidden',\n tooltip: 'Use new Asset API for model browsing',\n defaultValue: isCloud ? true : false,\n experimental: true\n },\n {\n id: 'Comfy.VersionCompatibility.DisableWarnings',\n name: 'Disable version compatibility warnings',\n type: 'hidden',\n defaultValue: false,\n versionAdded: '1.34.1'\n },\n {\n id: 'Comfy.RightSidePanel.IsOpen',\n name: 'Right side panel open state',\n type: 'hidden',\n defaultValue: false,\n versionAdded: '1.37.0'\n },\n {\n id: 'Comfy.Queue.QPOV2',\n category: ['Comfy', 'Queue', 'Layout'],\n name: 'Use the unified job queue in the Assets side panel',\n type: 'boolean',\n tooltip:\n 'Replaces the floating job queue panel with an equivalent job queue embedded in the Assets side panel. You can disable this to return to the floating panel layout.',\n defaultValue: true,\n experimental: true\n }\n]\n","import { computed, onUnmounted, watch } from 'vue'\n\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useWorkflowService } from '@/platform/workflow/core/services/workflowService'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { api } from '@/scripts/api'\n\nexport function useWorkflowAutoSave() {\n const workflowStore = useWorkflowStore()\n const settingStore = useSettingStore()\n const workflowService = useWorkflowService()\n\n // Use computed refs to cache autosave settings\n const autoSaveSetting = computed(() =>\n settingStore.get('Comfy.Workflow.AutoSave')\n )\n const autoSaveDelay = computed(() =>\n settingStore.get('Comfy.Workflow.AutoSaveDelay')\n )\n\n let autoSaveTimeout: NodeJS.Timeout | null = null\n let isSaving = false\n let needsAutoSave = false\n\n const scheduleAutoSave = () => {\n // Clear any existing timeout\n if (autoSaveTimeout) {\n clearTimeout(autoSaveTimeout)\n autoSaveTimeout = null\n }\n\n // If autosave is enabled\n if (autoSaveSetting.value === 'after delay') {\n // If a save is in progress, mark that we need an autosave after saving\n if (isSaving) {\n needsAutoSave = true\n return\n }\n const delay = autoSaveDelay.value\n autoSaveTimeout = setTimeout(async () => {\n const activeWorkflow = workflowStore.activeWorkflow\n if (activeWorkflow?.isModified && activeWorkflow.isPersisted) {\n try {\n isSaving = true\n await workflowService.saveWorkflow(activeWorkflow)\n } catch (err) {\n console.error('Auto save failed:', err)\n } finally {\n isSaving = false\n if (needsAutoSave) {\n needsAutoSave = false\n scheduleAutoSave()\n }\n }\n }\n }, delay)\n }\n }\n\n // Watch for autosave setting changes\n watch(\n autoSaveSetting,\n (newSetting) => {\n // Clear any existing timeout when settings change\n if (autoSaveTimeout) {\n clearTimeout(autoSaveTimeout)\n autoSaveTimeout = null\n }\n\n // If there's an active modified workflow and autosave is enabled, schedule a save\n if (\n newSetting === 'after delay' &&\n workflowStore.activeWorkflow?.isModified\n ) {\n scheduleAutoSave()\n }\n },\n { immediate: true }\n )\n\n // Listen for graph changes and schedule autosave when they occur\n const onGraphChanged = () => {\n scheduleAutoSave()\n }\n\n api.addEventListener('graphChanged', onGraphChanged)\n\n onUnmounted(() => {\n if (autoSaveTimeout) {\n clearTimeout(autoSaveTimeout)\n autoSaveTimeout = null\n }\n api.removeEventListener('graphChanged', onGraphChanged)\n })\n}\n","import { useToast } from 'primevue/usetoast'\nimport { useI18n } from 'vue-i18n'\nimport { useRoute, useRouter } from 'vue-router'\n\nimport { clearPreservedQuery } from '@/platform/navigation/preservedQueryManager'\nimport { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\n\nimport { useTemplateWorkflows } from './useTemplateWorkflows'\n\n/**\n * Composable for loading templates from URL query parameters\n *\n * Supports URLs like:\n * - /?template=flux_simple (loads with default source)\n * - /?template=flux_simple&source=custom (loads from custom source)\n * - /?template=flux_simple&mode=linear (loads template in linear mode)\n *\n * Input validation:\n * - Template, source, and mode parameters must match: ^[a-zA-Z0-9_-]+$\n * - Invalid formats are rejected with console warnings\n */\nexport function useTemplateUrlLoader() {\n const route = useRoute()\n const router = useRouter()\n const { t } = useI18n()\n const toast = useToast()\n const templateWorkflows = useTemplateWorkflows()\n const canvasStore = useCanvasStore()\n const TEMPLATE_NAMESPACE = PRESERVED_QUERY_NAMESPACES.TEMPLATE\n const SUPPORTED_MODES = ['linear'] as const\n type SupportedMode = (typeof SUPPORTED_MODES)[number]\n\n /**\n * Validates parameter format to prevent path traversal and injection attacks\n * Allows: letters, numbers, underscores, hyphens, and dots (for version numbers)\n * Blocks: path separators (/, \\), special chars that could enable injection\n */\n const isValidParameter = (param: string): boolean => {\n return /^[a-zA-Z0-9_.-]+$/.test(param)\n }\n\n /**\n * Type guard to check if a value is a supported mode\n */\n const isSupportedMode = (mode: string): mode is SupportedMode => {\n return SUPPORTED_MODES.includes(mode as SupportedMode)\n }\n\n /**\n * Removes template, source, and mode parameters from URL\n */\n const cleanupUrlParams = () => {\n const newQuery = { ...route.query }\n delete newQuery.template\n delete newQuery.source\n delete newQuery.mode\n void router.replace({ query: newQuery })\n }\n\n /**\n * Loads template from URL query parameters if present\n * Handles errors internally and shows appropriate user feedback\n */\n const loadTemplateFromUrl = async () => {\n const templateParam = route.query.template\n\n if (!templateParam || typeof templateParam !== 'string') {\n return\n }\n\n if (!isValidParameter(templateParam)) {\n console.warn(\n `[useTemplateUrlLoader] Invalid template parameter format: ${templateParam}`\n )\n return\n }\n\n const sourceParam = (route.query.source as string | undefined) || 'default'\n\n if (!isValidParameter(sourceParam)) {\n console.warn(\n `[useTemplateUrlLoader] Invalid source parameter format: ${sourceParam}`\n )\n return\n }\n\n const modeParam = route.query.mode as string | undefined\n\n if (\n modeParam &&\n (typeof modeParam !== 'string' || !isValidParameter(modeParam))\n ) {\n console.warn(\n `[useTemplateUrlLoader] Invalid mode parameter format: ${modeParam}`\n )\n return\n }\n\n if (modeParam && !isSupportedMode(modeParam)) {\n console.warn(\n `[useTemplateUrlLoader] Unsupported mode parameter: ${modeParam}. Supported modes: ${SUPPORTED_MODES.join(', ')}`\n )\n }\n\n try {\n await templateWorkflows.loadTemplates()\n\n const success = await templateWorkflows.loadWorkflowTemplate(\n templateParam,\n sourceParam\n )\n\n if (!success) {\n toast.add({\n severity: 'error',\n summary: t('g.error'),\n detail: t('templateWorkflows.error.templateNotFound', {\n templateName: templateParam\n }),\n life: 3000\n })\n } else if (modeParam === 'linear') {\n // Set linear mode after successful template load\n canvasStore.linearMode = true\n }\n } catch (error) {\n console.error(\n '[useTemplateUrlLoader] Failed to load template from URL:',\n error\n )\n toast.add({\n severity: 'error',\n summary: t('g.error'),\n detail: t('g.errorLoadingTemplate'),\n life: 3000\n })\n } finally {\n cleanupUrlParams()\n clearPreservedQuery(TEMPLATE_NAMESPACE)\n }\n }\n\n return {\n loadTemplateFromUrl\n }\n}\n","import { tryOnScopeDispose } from '@vueuse/core'\nimport { computed, watch } from 'vue'\nimport { useRoute, useRouter } from 'vue-router'\n\nimport {\n hydratePreservedQuery,\n mergePreservedQueryIntoQuery\n} from '@/platform/navigation/preservedQueryManager'\nimport { PRESERVED_QUERY_NAMESPACES } from '@/platform/navigation/preservedQueryNamespaces'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useWorkflowService } from '@/platform/workflow/core/services/workflowService'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { useTemplateUrlLoader } from '@/platform/workflow/templates/composables/useTemplateUrlLoader'\nimport { api } from '@/scripts/api'\nimport { app as comfyApp } from '@/scripts/app'\nimport { getStorageValue, setStorageValue } from '@/scripts/utils'\nimport { useCommandStore } from '@/stores/commandStore'\n\nexport function useWorkflowPersistence() {\n const workflowStore = useWorkflowStore()\n const settingStore = useSettingStore()\n const route = useRoute()\n const router = useRouter()\n const templateUrlLoader = useTemplateUrlLoader()\n const TEMPLATE_NAMESPACE = PRESERVED_QUERY_NAMESPACES.TEMPLATE\n\n const ensureTemplateQueryFromIntent = async () => {\n hydratePreservedQuery(TEMPLATE_NAMESPACE)\n const mergedQuery = mergePreservedQueryIntoQuery(\n TEMPLATE_NAMESPACE,\n route.query\n )\n\n if (mergedQuery) {\n await router.replace({ query: mergedQuery })\n }\n\n return mergedQuery ?? route.query\n }\n\n const workflowPersistenceEnabled = computed(() =>\n settingStore.get('Comfy.Workflow.Persist')\n )\n\n const persistCurrentWorkflow = () => {\n if (!workflowPersistenceEnabled.value) return\n const workflow = JSON.stringify(comfyApp.rootGraph.serialize())\n\n try {\n localStorage.setItem('workflow', workflow)\n if (api.clientId) {\n sessionStorage.setItem(`workflow:${api.clientId}`, workflow)\n }\n } catch (error) {\n // Only log our own keys and aggregate stats\n const ourKeys = Object.keys(sessionStorage).filter(\n (key) => key.startsWith('workflow:') || key === 'workflow'\n )\n console.error('QuotaExceededError details:', {\n workflowSizeKB: Math.round(workflow.length / 1024),\n totalStorageItems: Object.keys(sessionStorage).length,\n ourWorkflowKeys: ourKeys.length,\n ourWorkflowSizes: ourKeys.map((key) => ({\n key,\n sizeKB: Math.round(sessionStorage[key].length / 1024)\n })),\n error: error instanceof Error ? error.message : String(error)\n })\n throw error\n }\n }\n\n const loadWorkflowFromStorage = async (\n json: string | null,\n workflowName: string | null\n ) => {\n if (!json) return false\n const workflow = JSON.parse(json)\n await comfyApp.loadGraphData(workflow, true, true, workflowName)\n return true\n }\n\n const loadPreviousWorkflowFromStorage = async () => {\n const workflowName = getStorageValue('Comfy.PreviousWorkflow')\n const clientId = api.initialClientId ?? api.clientId\n\n // Try loading from session storage first\n if (clientId) {\n const sessionWorkflow = sessionStorage.getItem(`workflow:${clientId}`)\n if (await loadWorkflowFromStorage(sessionWorkflow, workflowName)) {\n return true\n }\n }\n\n // Fall back to local storage\n const localWorkflow = localStorage.getItem('workflow')\n return await loadWorkflowFromStorage(localWorkflow, workflowName)\n }\n\n const loadDefaultWorkflow = async () => {\n if (!settingStore.get('Comfy.TutorialCompleted')) {\n await settingStore.set('Comfy.TutorialCompleted', true)\n await useWorkflowService().loadBlankWorkflow()\n await useCommandStore().execute('Comfy.BrowseTemplates')\n } else {\n await comfyApp.loadGraphData()\n }\n }\n\n const initializeWorkflow = async () => {\n if (!workflowPersistenceEnabled.value) return\n\n try {\n const restored = await loadPreviousWorkflowFromStorage()\n if (!restored) {\n await loadDefaultWorkflow()\n }\n } catch (err) {\n console.error('Error loading previous workflow', err)\n await loadDefaultWorkflow()\n }\n }\n\n const loadTemplateFromUrlIfPresent = async () => {\n const query = await ensureTemplateQueryFromIntent()\n const hasTemplateUrl = query.template && typeof query.template === 'string'\n\n if (hasTemplateUrl) {\n await templateUrlLoader.loadTemplateFromUrl()\n }\n }\n\n // Setup watchers\n watch(\n () => workflowStore.activeWorkflow?.key,\n (activeWorkflowKey) => {\n if (!activeWorkflowKey) return\n setStorageValue('Comfy.PreviousWorkflow', activeWorkflowKey)\n // When the activeWorkflow changes, the graph has already been loaded.\n // Saving the current state of the graph to the localStorage.\n persistCurrentWorkflow()\n }\n )\n api.addEventListener('graphChanged', persistCurrentWorkflow)\n\n // Clean up event listener when component unmounts\n tryOnScopeDispose(() => {\n api.removeEventListener('graphChanged', persistCurrentWorkflow)\n })\n\n // Restore workflow tabs states\n const openWorkflows = computed(() => workflowStore.openWorkflows)\n const activeWorkflow = computed(() => workflowStore.activeWorkflow)\n const restoreState = computed<{ paths: string[]; activeIndex: number }>(\n () => {\n if (!openWorkflows.value || !activeWorkflow.value) {\n return { paths: [], activeIndex: -1 }\n }\n\n const paths = openWorkflows.value\n .filter((workflow) => workflow?.isPersisted)\n .map((workflow) => workflow.path)\n const activeIndex = openWorkflows.value.findIndex(\n (workflow) => workflow.path === activeWorkflow.value?.path\n )\n\n return { paths, activeIndex }\n }\n )\n\n // Get storage values before setting watchers\n const storedWorkflows = JSON.parse(\n getStorageValue('Comfy.OpenWorkflowsPaths') || '[]'\n )\n const storedActiveIndex = JSON.parse(\n getStorageValue('Comfy.ActiveWorkflowIndex') || '-1'\n )\n\n watch(restoreState, ({ paths, activeIndex }) => {\n if (workflowPersistenceEnabled.value) {\n setStorageValue('Comfy.OpenWorkflowsPaths', JSON.stringify(paths))\n setStorageValue('Comfy.ActiveWorkflowIndex', JSON.stringify(activeIndex))\n }\n })\n\n const restoreWorkflowTabsState = () => {\n if (!workflowPersistenceEnabled.value) return\n const isRestorable = storedWorkflows?.length > 0 && storedActiveIndex >= 0\n if (isRestorable) {\n workflowStore.openWorkflowsInBackground({\n left: storedWorkflows.slice(0, storedActiveIndex),\n right: storedWorkflows.slice(storedActiveIndex)\n })\n }\n }\n\n return {\n initializeWorkflow,\n loadTemplateFromUrlIfPresent,\n restoreWorkflowTabsState\n }\n}\n","import { useDebounceFn, useEventListener } from '@vueuse/core'\nimport { ref } from 'vue'\nimport type { MaybeRefOrGetter } from 'vue'\n\ninterface TransformSettlingOptions {\n /**\n * Delay in ms before transform is considered \"settled\" after last interaction\n * @default 200\n */\n settleDelay?: number\n /**\n * Whether to use passive event listeners (better performance but can't preventDefault)\n * @default true\n */\n passive?: boolean\n}\n\n/**\n * Tracks when canvas zoom transforms are actively changing vs settled.\n *\n * This composable helps optimize rendering quality during zoom transformations.\n * When the user is actively zooming, we can reduce rendering quality\n * for better performance. Once the transform \"settles\" (stops changing), we can\n * trigger high-quality re-rasterization.\n *\n * The settling concept prevents constant quality switching during interactions\n * by waiting for a period of inactivity before considering the transform complete.\n *\n * Uses VueUse's useEventListener for automatic cleanup and useDebounceFn for\n * efficient settle detection.\n *\n * @example\n * ```ts\n * const { isTransforming } = useTransformSettling(canvasRef, {\n * settleDelay: 200\n * })\n *\n * // Use in CSS classes or rendering logic\n * const cssClass = computed(() => ({\n * 'low-quality': isTransforming.value,\n * 'high-quality': !isTransforming.value\n * }))\n * ```\n */\nexport function useTransformSettling(\n target: MaybeRefOrGetter<HTMLElement | null | undefined>,\n options: TransformSettlingOptions = {}\n) {\n const { settleDelay = 256, passive = true } = options\n\n const isTransforming = ref(false)\n\n /**\n * Mark transform as active\n */\n const markTransformActive = () => {\n isTransforming.value = true\n }\n\n /**\n * Mark transform as settled (debounced)\n */\n const markTransformSettled = useDebounceFn(() => {\n isTransforming.value = false\n }, settleDelay)\n\n /**\n * Handle zoom transform event - mark active then queue settle\n */\n const handleWheel = () => {\n markTransformActive()\n void markTransformSettled()\n }\n\n // Register wheel event listener with auto-cleanup\n useEventListener(target, 'wheel', handleWheel, {\n capture: true,\n passive\n })\n\n return {\n isTransforming\n }\n}\n","/**\n * Composable for managing transform state synchronized with LiteGraph canvas\n *\n * This composable is a critical part of the hybrid rendering architecture that\n * allows Vue components to render in perfect alignment with LiteGraph's canvas.\n *\n * ## Core Concept\n *\n * LiteGraph uses a canvas for rendering connections, grid, and handling interactions.\n * Vue components need to render nodes on top of this canvas. The challenge is\n * synchronizing the coordinate systems:\n *\n * - LiteGraph: Uses canvas coordinates with its own transform matrix\n * - Vue/DOM: Uses screen coordinates with CSS transforms\n *\n * ## Solution: Transform Container Pattern\n *\n * Instead of transforming individual nodes (O(n) complexity), we:\n * 1. Mirror LiteGraph's transform matrix to a single CSS container\n * 2. Place all Vue nodes as children with simple absolute positioning\n * 3. Achieve O(1) transform updates regardless of node count\n *\n * ## Coordinate Systems\n *\n * - **Canvas coordinates**: LiteGraph's internal coordinate system\n * - **Screen coordinates**: Browser's viewport coordinate system\n * - **Transform sync**: camera.x/y/z mirrors canvas.ds.offset/scale\n *\n * ## Performance Benefits\n *\n * - GPU acceleration via CSS transforms\n * - No layout thrashing (only transform changes)\n * - Efficient viewport culling calculations\n * - Scales to 1000+ nodes while maintaining 60 FPS\n *\n * @example\n * ```typescript\n * const { camera, transformStyle, canvasToScreen } = useTransformState()\n *\n * // In template\n * <div :style=\"transformStyle\">\n * <NodeComponent\n * v-for=\"node in nodes\"\n * :style=\"{ left: node.x + 'px', top: node.y + 'px' }\"\n * />\n * </div>\n *\n * // Convert coordinates\n * const screenPos = canvasToScreen({ x: nodeX, y: nodeY })\n * ```\n */\nimport { computed, reactive, readonly } from 'vue'\n\nimport type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'\nimport { createSharedComposable } from '@vueuse/core'\n\ninterface Point {\n x: number\n y: number\n}\n\ninterface Camera {\n x: number\n y: number\n z: number // scale/zoom\n}\n\nfunction useTransformStateIndividual() {\n // Reactive state mirroring LiteGraph's canvas transform\n const camera = reactive<Camera>({\n x: 0,\n y: 0,\n z: 1\n })\n\n // Computed transform string for CSS\n const transformStyle = computed(() => ({\n // Match LiteGraph DragAndScale.toCanvasContext():\n // ctx.scale(scale); ctx.translate(offset)\n // CSS applies right-to-left, so \"scale() translate()\" -> translate first, then scale\n // Effective mapping: screen = (canvas + offset) * scale\n transform: `scale(${camera.z}) translate(${camera.x}px, ${camera.y}px)`,\n transformOrigin: '0 0'\n }))\n\n /**\n * Synchronizes Vue's reactive camera state with LiteGraph's canvas transform\n *\n * Called every frame via RAF to ensure Vue components stay aligned with canvas.\n * This is the heart of the hybrid rendering system - it bridges the gap between\n * LiteGraph's canvas transforms and Vue's reactive system.\n *\n * @param canvas - LiteGraph canvas instance with DragAndScale (ds) transform state\n */\n function syncWithCanvas(canvas: LGraphCanvas) {\n if (!canvas || !canvas.ds) return\n\n // Mirror LiteGraph's transform state to Vue's reactive state\n // ds.offset = pan offset, ds.scale = zoom level\n camera.x = canvas.ds.offset[0]\n camera.y = canvas.ds.offset[1]\n camera.z = canvas.ds.scale || 1\n }\n\n /**\n * Converts canvas coordinates to screen coordinates\n *\n * Applies the same transform that LiteGraph uses for rendering.\n * Essential for positioning Vue components to align with canvas elements.\n *\n * Formula: screen = (canvas + offset) * scale\n *\n * @param point - Point in canvas coordinate system\n * @returns Point in screen coordinate system\n */\n function canvasToScreen(point: Point): Point {\n return {\n x: (point.x + camera.x) * camera.z,\n y: (point.y + camera.y) * camera.z\n }\n }\n\n /**\n * Converts screen coordinates to canvas coordinates\n *\n * Inverse of canvasToScreen. Useful for hit testing and converting\n * mouse events back to canvas space.\n *\n * Formula: canvas = screen / scale - offset\n *\n * @param point - Point in screen coordinate system\n * @returns Point in canvas coordinate system\n */\n const screenToCanvas = (point: Point): Point => {\n return {\n x: point.x / camera.z - camera.x,\n y: point.y / camera.z - camera.y\n }\n }\n\n // Get node's screen bounds for culling\n function getNodeScreenBounds(\n pos: [number, number],\n size: [number, number]\n ): DOMRect {\n const topLeft = canvasToScreen({ x: pos[0], y: pos[1] })\n const width = size[0] * camera.z\n const height = size[1] * camera.z\n\n return new DOMRect(topLeft.x, topLeft.y, width, height)\n }\n\n // Helper: Calculate zoom-adjusted margin for viewport culling\n function calculateAdjustedMargin(baseMargin: number): number {\n if (camera.z < 0.1) return Math.min(baseMargin * 5, 2.0)\n if (camera.z > 3.0) return Math.max(baseMargin * 0.5, 0.05)\n return baseMargin\n }\n\n // Helper: Check if node is too small to be visible at current zoom\n function isNodeTooSmall(nodeSize: [number, number]): boolean {\n const nodeScreenSize = Math.max(nodeSize[0], nodeSize[1]) * camera.z\n return nodeScreenSize < 4\n }\n\n // Helper: Calculate expanded viewport bounds with margin\n function getExpandedViewportBounds(\n viewport: { width: number; height: number },\n margin: number\n ) {\n const marginX = viewport.width * margin\n const marginY = viewport.height * margin\n return {\n left: -marginX,\n right: viewport.width + marginX,\n top: -marginY,\n bottom: viewport.height + marginY\n }\n }\n\n // Helper: Test if node intersects with viewport bounds\n function testViewportIntersection(\n screenPos: { x: number; y: number },\n nodeSize: [number, number],\n bounds: { left: number; right: number; top: number; bottom: number }\n ): boolean {\n const nodeRight = screenPos.x + nodeSize[0] * camera.z\n const nodeBottom = screenPos.y + nodeSize[1] * camera.z\n\n return !(\n nodeRight < bounds.left ||\n screenPos.x > bounds.right ||\n nodeBottom < bounds.top ||\n screenPos.y > bounds.bottom\n )\n }\n\n // Check if node is within viewport with frustum and size-based culling\n function isNodeInViewport(\n nodePos: [number, number],\n nodeSize: [number, number],\n viewport: { width: number; height: number },\n margin: number = 0.2\n ): boolean {\n // Early exit for tiny nodes\n if (isNodeTooSmall(nodeSize)) return false\n\n const screenPos = canvasToScreen({ x: nodePos[0], y: nodePos[1] })\n const adjustedMargin = calculateAdjustedMargin(margin)\n const bounds = getExpandedViewportBounds(viewport, adjustedMargin)\n\n return testViewportIntersection(screenPos, nodeSize, bounds)\n }\n\n // Get viewport bounds in canvas coordinates (for spatial index queries)\n function getViewportBounds(\n viewport: { width: number; height: number },\n margin: number = 0.2\n ) {\n const marginX = viewport.width * margin\n const marginY = viewport.height * margin\n\n const topLeft = screenToCanvas({ x: -marginX, y: -marginY })\n const bottomRight = screenToCanvas({\n x: viewport.width + marginX,\n y: viewport.height + marginY\n })\n\n return {\n x: topLeft.x,\n y: topLeft.y,\n width: bottomRight.x - topLeft.x,\n height: bottomRight.y - topLeft.y\n }\n }\n\n return {\n camera: readonly(camera),\n transformStyle,\n syncWithCanvas,\n canvasToScreen,\n screenToCanvas,\n getNodeScreenBounds,\n isNodeInViewport,\n getViewportBounds\n }\n}\n\nexport const useTransformState = createSharedComposable(\n useTransformStateIndividual\n)\n","<template>\n <div\n data-testid=\"transform-pane\"\n :class=\"\n cn(\n 'absolute inset-0 w-full h-full pointer-events-none',\n isInteracting ? 'transform-pane--interacting' : 'will-change-auto'\n )\n \"\n :style=\"transformStyle\"\n >\n <!-- Vue nodes will be rendered here -->\n <slot />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useRafFn } from '@vueuse/core'\nimport { computed } from 'vue'\n\nimport type { LGraphCanvas } from '@/lib/litegraph/src/litegraph'\nimport { useTransformSettling } from '@/renderer/core/layout/transform/useTransformSettling'\nimport { useTransformState } from '@/renderer/core/layout/transform/useTransformState'\nimport { cn } from '@/utils/tailwindUtil'\n\ninterface TransformPaneProps {\n canvas?: LGraphCanvas\n}\n\nconst props = defineProps<TransformPaneProps>()\n\nconst { transformStyle, syncWithCanvas } = useTransformState()\n\nconst canvasElement = computed(() => props.canvas?.canvas)\nconst { isTransforming: isInteracting } = useTransformSettling(canvasElement, {\n settleDelay: 16\n})\n\nuseRafFn(\n () => {\n if (!props.canvas) {\n return\n }\n syncWithCanvas(props.canvas)\n },\n { immediate: true }\n)\n</script>\n\n<style scoped>\n.transform-pane--interacting {\n will-change: transform;\n}\n</style>\n","<template>\n <div\n v-if=\"visible && initialized\"\n ref=\"minimapRef\"\n class=\"minimap-main-container absolute right-0 bottom-[54px] z-1000 flex\"\n >\n <MiniMapPanel\n v-if=\"showOptionsPanel\"\n :panel-styles=\"panelStyles\"\n :node-colors=\"nodeColors\"\n :show-links=\"showLinks\"\n :show-groups=\"showGroups\"\n :render-bypass=\"renderBypass\"\n :render-error=\"renderError\"\n @update-option=\"updateOption\"\n />\n\n <div\n ref=\"containerRef\"\n class=\"litegraph-minimap relative border border-interface-stroke bg-comfy-menu-bg shadow-interface\"\n :style=\"containerStyles\"\n >\n <Button\n class=\"absolute top-0 left-0 z-10\"\n size=\"icon\"\n variant=\"muted-textonly\"\n :aria-label=\"$t('g.settings')\"\n @click.stop=\"toggleOptionsPanel\"\n >\n <i class=\"icon-[lucide--settings-2]\" />\n </Button>\n <Button\n class=\"absolute top-0 right-0 z-10\"\n size=\"icon\"\n variant=\"muted-textonly\"\n :aria-label=\"$t('g.close')\"\n data-testid=\"close-minmap-button\"\n @click.stop=\"() => commandStore.execute('Comfy.Canvas.ToggleMinimap')\"\n >\n <i class=\"icon-[lucide--x]\" />\n </Button>\n\n <hr\n class=\"absolute top-6 h-px border-0 bg-node-component-border\"\n :style=\"{\n width: containerStyles.width\n }\"\n />\n\n <canvas\n ref=\"canvasRef\"\n :width=\"width\"\n :height=\"height\"\n class=\"minimap-canvas\"\n />\n\n <div class=\"minimap-viewport\" :style=\"viewportStyles\" />\n\n <div\n class=\"absolute inset-0 touch-none\"\n @pointerdown=\"handlePointerDown\"\n @pointermove=\"handlePointerMove\"\n @pointerup=\"handlePointerUp\"\n @pointerleave=\"handlePointerUp\"\n @pointercancel=\"handlePointerCancel\"\n @wheel=\"handleWheel\"\n />\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { onMounted, onUnmounted, ref, useTemplateRef } from 'vue'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { useMinimap } from '@/renderer/extensions/minimap/composables/useMinimap'\nimport { useCommandStore } from '@/stores/commandStore'\n\nimport MiniMapPanel from './MiniMapPanel.vue'\n\nconst commandStore = useCommandStore()\nconst minimapRef = ref<HTMLDivElement>()\nconst containerRef = useTemplateRef<HTMLDivElement>('containerRef')\nconst canvasRef = useTemplateRef<HTMLCanvasElement>('canvasRef')\n\nconst {\n initialized,\n visible,\n containerStyles,\n viewportStyles,\n width,\n height,\n panelStyles,\n nodeColors,\n showLinks,\n showGroups,\n renderBypass,\n renderError,\n updateOption,\n destroy,\n handlePointerDown,\n handlePointerMove,\n handlePointerUp,\n handlePointerCancel,\n handleWheel,\n setMinimapRef\n} = useMinimap({\n containerRefMaybe: containerRef,\n canvasRefMaybe: canvasRef\n})\n\nconst showOptionsPanel = ref(false)\n\nconst toggleOptionsPanel = () => {\n showOptionsPanel.value = !showOptionsPanel.value\n}\n\nonMounted(() => {\n if (minimapRef.value) {\n setMinimapRef(minimapRef.value)\n }\n})\n\nonUnmounted(() => {\n destroy()\n})\n</script>\n\n<style scoped>\n.litegraph-minimap {\n overflow: hidden;\n}\n\n.minimap-canvas {\n display: block;\n width: 100%;\n height: 100%;\n pointer-events: none;\n}\n\n.minimap-viewport {\n position: absolute;\n top: 0;\n left: 0;\n pointer-events: none;\n}\n</style>\n","/**\n * Checks if a pointer/mouse event has multi-select modifier keys pressed.\n * Multi-select keys are: Ctrl (Windows/Linux), Cmd (Mac), or Shift\n *\n * @param event - The pointer or mouse event to check\n * @returns true if any multi-select modifier key is pressed\n */\nexport function isMultiSelectKey(event: PointerEvent | MouseEvent): boolean {\n return event.ctrlKey || event.metaKey || event.shiftKey\n}\n","/**\n * Node Event Handlers Composable\n *\n * Handles all Vue node interaction events including:\n * - Node selection with multi-select support\n * - Node collapse/expand state management\n * - Node title editing and updates\n * - Layout mutations for visual feedback\n * - Integration with LiteGraph canvas selection system\n */\nimport { createSharedComposable } from '@vueuse/core'\n\nimport { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'\nimport { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'\nimport { isMultiSelectKey } from '@/renderer/extensions/vueNodes/utils/selectionUtils'\nimport type { NodeId } from '@/renderer/core/layout/types'\n\nfunction useNodeEventHandlersIndividual() {\n const canvasStore = useCanvasStore()\n const { nodeManager } = useVueNodeLifecycle()\n const { bringNodeToFront } = useNodeZIndex()\n const { shouldHandleNodePointerEvents } = useCanvasInteractions()\n\n /**\n * Handle node selection events\n * Supports single selection and multi-select with Ctrl/Cmd\n */\n function handleNodeSelect(event: PointerEvent, nodeId: NodeId) {\n if (!shouldHandleNodePointerEvents.value) return\n\n if (!canvasStore.canvas || !nodeManager.value) return\n\n const node = nodeManager.value.getNode(nodeId)\n if (!node) return\n\n const multiSelect = isMultiSelectKey(event)\n const selectedItemsCount = canvasStore.selectedItems.length\n const preserveExistingSelection =\n !multiSelect && node.selected && selectedItemsCount > 1\n\n if (multiSelect) {\n if (!node.selected) {\n canvasStore.canvas.select(node)\n }\n } else if (!preserveExistingSelection) {\n // Regular click -> single select\n canvasStore.canvas.deselectAll()\n canvasStore.canvas.select(node)\n }\n\n // Bring node to front when clicked (similar to LiteGraph behavior)\n // Skip if node is pinned to avoid unwanted movement\n if (!node.flags?.pinned) {\n bringNodeToFront(nodeId)\n }\n\n // Update canvas selection tracking\n canvasStore.updateSelectedItems()\n }\n\n /**\n * Handle node collapse/expand state changes\n * Uses LiteGraph's native collapse method for proper state management\n */\n function handleNodeCollapse(nodeId: NodeId, collapsed: boolean) {\n if (!shouldHandleNodePointerEvents.value) return\n\n if (!nodeManager.value) return\n\n const node = nodeManager.value.getNode(nodeId)\n if (!node) return\n\n // Use LiteGraph's collapse method if the state needs to change\n const currentCollapsed = node.flags?.collapsed ?? false\n if (currentCollapsed !== collapsed) {\n node.collapse()\n }\n }\n\n /**\n * Handle node title updates\n * Updates the title in LiteGraph for persistence across sessions\n */\n function handleNodeTitleUpdate(nodeId: NodeId, newTitle: string) {\n if (!shouldHandleNodePointerEvents.value) return\n\n if (!nodeManager.value) return\n\n const node = nodeManager.value.getNode(nodeId)\n if (!node) return\n\n // Update the node title in LiteGraph for persistence\n node.title = newTitle\n }\n\n /**\n * Handle node right-click context menu events\n * Integrates with LiteGraph's context menu system\n */\n function handleNodeRightClick(event: PointerEvent, nodeId: NodeId) {\n if (!shouldHandleNodePointerEvents.value) return\n\n if (!canvasStore.canvas || !nodeManager.value) return\n\n const node = nodeManager.value.getNode(nodeId)\n if (!node) return\n\n // Prevent default context menu\n event.preventDefault()\n\n // Select the node if not already selected\n if (!node.selected) {\n handleNodeSelect(event, nodeId)\n }\n\n // Let LiteGraph handle the context menu\n // The canvas will handle showing the appropriate context menu\n }\n\n function toggleNodeSelectionAfterPointerUp(\n nodeId: NodeId,\n multiSelect: boolean\n ) {\n if (!shouldHandleNodePointerEvents.value) return\n\n if (!canvasStore.canvas || !nodeManager.value) return\n\n const node = nodeManager.value.getNode(nodeId)\n if (!node) return\n\n if (!multiSelect) {\n canvasStore.canvas.deselectAll()\n canvasStore.canvas.select(node)\n canvasStore.updateSelectedItems()\n // Bring node to front when selected (unless pinned)\n if (!node.flags?.pinned) {\n bringNodeToFront(nodeId)\n }\n return\n }\n\n if (node.selected) {\n canvasStore.canvas.deselect(node)\n } else {\n canvasStore.canvas.select(node)\n // Bring node to front when selected (unless pinned)\n if (!node.flags?.pinned) {\n bringNodeToFront(nodeId)\n }\n }\n\n canvasStore.updateSelectedItems()\n }\n\n return {\n // Core event handlers\n handleNodeSelect,\n handleNodeCollapse,\n handleNodeTitleUpdate,\n handleNodeRightClick,\n\n // Batch operations\n toggleNodeSelectionAfterPointerUp\n }\n}\n\nexport const useNodeEventHandlers = createSharedComposable(\n useNodeEventHandlersIndividual\n)\n","import { computed } from 'vue'\n\nimport { snapPoint } from '@/lib/litegraph/src/measure'\nimport { useSettingStore } from '@/platform/settings/settingStore'\n\n/**\n * Composable for node snap-to-grid functionality\n *\n * Provides reactive access to snap settings and utilities for applying\n * snap-to-grid behavior to Vue nodes during drag and resize operations.\n */\nexport function useNodeSnap() {\n const settingStore = useSettingStore()\n\n // Reactive snap settings\n const gridSize = computed(() => settingStore.get('Comfy.SnapToGrid.GridSize'))\n const alwaysSnap = computed(() => settingStore.get('pysssss.SnapToGrid'))\n\n /**\n * Determines if snap-to-grid should be applied based on shift key and settings\n * @param event - The pointer event to check for shift key\n * @returns true if snapping should be applied\n */\n function shouldSnap(event: PointerEvent): boolean {\n return event.shiftKey || alwaysSnap.value\n }\n\n /**\n * Applies snap-to-grid to a position\n * @param position - Position object with x, y coordinates\n * @returns The snapped position as a new object\n */\n function applySnapToPosition(position: { x: number; y: number }): {\n x: number\n y: number\n } {\n const size = gridSize.value\n if (!size) return { ...position }\n\n const posArray: [number, number] = [position.x, position.y]\n if (snapPoint(posArray, size)) {\n return { x: posArray[0], y: posArray[1] }\n }\n return { ...position }\n }\n\n /**\n * Applies snap-to-grid to a size (width/height)\n * @param size - Size object with width, height\n * @returns The snapped size as a new object\n */\n function applySnapToSize(size: { width: number; height: number }): {\n width: number\n height: number\n } {\n const gridSizeValue = gridSize.value\n if (!gridSizeValue) return { ...size }\n\n const sizeArray: [number, number] = [size.width, size.height]\n if (snapPoint(sizeArray, gridSizeValue)) {\n return { width: sizeArray[0], height: sizeArray[1] }\n }\n return { ...size }\n }\n\n return {\n gridSize,\n alwaysSnap,\n shouldSnap,\n applySnapToPosition,\n applySnapToSize\n }\n}\n","import { tryOnScopeDispose, useEventListener } from '@vueuse/core'\nimport { shallowRef } from 'vue'\n\nimport { app } from '@/scripts/app'\n\n/**\n * Composable for synchronizing shift key state from Vue nodes to LiteGraph canvas.\n *\n * Enables snap-to-grid preview rendering in LiteGraph during Vue node drag/resize operations\n * by dispatching synthetic keyboard events to the canvas element.\n *\n * @returns Object containing trackShiftKey function for shift state synchronization lifecycle\n *\n * @example\n * ```ts\n * const { trackShiftKey } = useShiftKeySync()\n *\n * function startDrag(event: PointerEvent) {\n * const stopTracking = trackShiftKey(event)\n * // ... drag logic\n * // Call stopTracking() on pointerup to cleanup listeners\n * }\n * ```\n */\nexport function useShiftKeySync() {\n const shiftKeyState = shallowRef(false)\n let canvasEl: HTMLCanvasElement | null = null\n\n /**\n * Synchronizes shift key state to LiteGraph canvas by dispatching synthetic keyboard events.\n *\n * Only dispatches events when shift state actually changes to minimize overhead.\n * Canvas reference is lazily initialized on first sync.\n *\n * @param isShiftPressed - Current shift key state to synchronize\n */\n function syncShiftState(isShiftPressed: boolean) {\n if (isShiftPressed === shiftKeyState.value) return\n\n // Lazy-initialize canvas reference on first use\n if (!canvasEl) {\n canvasEl = app.canvas?.canvas ?? null\n if (!canvasEl) return // Canvas not ready yet\n }\n\n shiftKeyState.value = isShiftPressed\n canvasEl.dispatchEvent(\n new KeyboardEvent(isShiftPressed ? 'keydown' : 'keyup', {\n key: 'Shift',\n shiftKey: isShiftPressed,\n bubbles: true\n })\n )\n }\n\n /**\n * Tracks shift key state during drag/resize operations and synchronizes to canvas.\n *\n * Attaches window-level keyboard event listeners for the duration of the operation.\n * Listeners are automatically cleaned up when the returned function is called.\n *\n * @param initialEvent - Initial pointer event containing shift key state at drag/resize start\n * @returns Cleanup function that removes event listeners - must be called when operation ends\n *\n * @example\n * ```ts\n * function startDrag(event: PointerEvent) {\n * const stopTracking = trackShiftKey(event)\n *\n * const handlePointerUp = () => {\n * stopTracking() // Cleanup listeners\n * }\n * }\n * ```\n */\n function trackShiftKey(initialEvent: PointerEvent): () => void {\n // Sync initial shift state\n syncShiftState(initialEvent.shiftKey)\n\n // Listen for shift key press/release during the operation\n const handleKeyEvent = (e: KeyboardEvent) => {\n if (e.key !== 'Shift') return\n syncShiftState(e.shiftKey)\n }\n\n const stopKeydown = useEventListener(window, 'keydown', handleKeyEvent, {\n passive: true\n })\n const stopKeyup = useEventListener(window, 'keyup', handleKeyEvent, {\n passive: true\n })\n\n // Return cleanup function that stops both listeners\n return () => {\n stopKeydown()\n stopKeyup()\n }\n }\n\n // Cleanup on component unmount\n tryOnScopeDispose(() => {\n shiftKeyState.value = false\n canvasEl = null\n })\n\n return { trackShiftKey }\n}\n","import { storeToRefs } from 'pinia'\nimport { toValue } from 'vue'\n\nimport type { LGraphGroup } from '@/lib/litegraph/src/LGraphGroup'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'\nimport { layoutStore } from '@/renderer/core/layout/store/layoutStore'\nimport { LayoutSource } from '@/renderer/core/layout/types'\nimport type {\n NodeBoundsUpdate,\n NodeId,\n Point\n} from '@/renderer/core/layout/types'\nimport { useNodeSnap } from '@/renderer/extensions/vueNodes/composables/useNodeSnap'\nimport { useShiftKeySync } from '@/renderer/extensions/vueNodes/composables/useShiftKeySync'\nimport { useTransformState } from '@/renderer/core/layout/transform/useTransformState'\nimport { isLGraphGroup } from '@/utils/litegraphUtil'\nimport { createSharedComposable } from '@vueuse/core'\n\nexport const useNodeDrag = createSharedComposable(useNodeDragIndividual)\n\nfunction useNodeDragIndividual() {\n const mutations = useLayoutMutations()\n const { selectedNodeIds, selectedItems } = storeToRefs(useCanvasStore())\n\n // Get transform utilities from TransformPane if available\n const transformState = useTransformState()\n\n // Snap-to-grid functionality\n const { shouldSnap, applySnapToPosition } = useNodeSnap()\n\n // Shift key sync for LiteGraph canvas preview\n const { trackShiftKey } = useShiftKeySync()\n\n // Drag state\n let dragStartPos: Point | null = null\n let dragStartMouse: Point | null = null\n let otherSelectedNodesStartPositions: Map<string, Point> | null = null\n let rafId: number | null = null\n let stopShiftSync: (() => void) | null = null\n\n // For groups: track the last applied canvas delta to compute frame delta\n let lastCanvasDelta: Point | null = null\n let selectedGroups: LGraphGroup[] | null = null\n\n function startDrag(event: PointerEvent, nodeId: NodeId) {\n const layout = toValue(layoutStore.getNodeLayoutRef(nodeId))\n if (!layout) return\n const position = layout.position ?? { x: 0, y: 0 }\n\n // Track shift key state and sync to canvas for snap preview\n stopShiftSync = trackShiftKey(event)\n\n dragStartPos = { ...position }\n dragStartMouse = { x: event.clientX, y: event.clientY }\n\n const selectedNodes = toValue(selectedNodeIds)\n\n // capture the starting positions of all other selected nodes\n // Only move other selected items if the dragged node is part of the selection\n const isDraggedNodeInSelection = selectedNodes?.has(nodeId)\n\n if (isDraggedNodeInSelection && selectedNodes.size > 1) {\n otherSelectedNodesStartPositions = new Map()\n\n for (const id of selectedNodes) {\n // Skip the current node being dragged\n if (id === nodeId) continue\n\n const nodeLayout = layoutStore.getNodeLayoutRef(id).value\n if (nodeLayout) {\n otherSelectedNodesStartPositions.set(id, { ...nodeLayout.position })\n }\n }\n } else {\n otherSelectedNodesStartPositions = null\n }\n\n // Capture selected groups only if the dragged node is part of the selection\n // This prevents groups from moving when dragging an unrelated node\n if (isDraggedNodeInSelection) {\n selectedGroups = toValue(selectedItems).filter(isLGraphGroup)\n lastCanvasDelta = { x: 0, y: 0 }\n } else {\n selectedGroups = null\n lastCanvasDelta = null\n }\n\n mutations.setSource(LayoutSource.Vue)\n }\n\n function handleDrag(event: PointerEvent, nodeId: NodeId) {\n if (!dragStartPos || !dragStartMouse) {\n return\n }\n\n // Throttle position updates using requestAnimationFrame for better performance\n if (rafId !== null) return // Skip if frame already scheduled\n\n const { target, pointerId } = event\n if (target instanceof HTMLElement && !target.hasPointerCapture(pointerId)) {\n // Delay capture to drag to allow for the Node cloning\n target.setPointerCapture(pointerId)\n }\n rafId = requestAnimationFrame(() => {\n rafId = null\n\n if (!dragStartPos || !dragStartMouse) return\n\n // Calculate mouse delta in screen coordinates\n const mouseDelta = {\n x: event.clientX - dragStartMouse.x,\n y: event.clientY - dragStartMouse.y\n }\n\n // Convert to canvas coordinates\n const canvasOrigin = transformState.screenToCanvas({ x: 0, y: 0 })\n const canvasWithDelta = transformState.screenToCanvas(mouseDelta)\n const canvasDelta = {\n x: canvasWithDelta.x - canvasOrigin.x,\n y: canvasWithDelta.y - canvasOrigin.y\n }\n\n // Calculate new position for the current node\n const newPosition = {\n x: dragStartPos.x + canvasDelta.x,\n y: dragStartPos.y + canvasDelta.y\n }\n\n // Apply mutation through the layout system (Vue batches DOM updates automatically)\n mutations.moveNode(nodeId, newPosition)\n\n // If we're dragging multiple selected nodes, move them all together\n if (\n otherSelectedNodesStartPositions &&\n otherSelectedNodesStartPositions.size > 0\n ) {\n for (const [\n otherNodeId,\n startPos\n ] of otherSelectedNodesStartPositions) {\n const newOtherPosition = {\n x: startPos.x + canvasDelta.x,\n y: startPos.y + canvasDelta.y\n }\n mutations.moveNode(otherNodeId, newOtherPosition)\n }\n }\n\n // Move selected groups using frame delta (difference from last frame)\n // This matches LiteGraph's behavior which uses delta-based movement\n if (selectedGroups && selectedGroups.length > 0 && lastCanvasDelta) {\n const frameDelta = {\n x: canvasDelta.x - lastCanvasDelta.x,\n y: canvasDelta.y - lastCanvasDelta.y\n }\n\n for (const group of selectedGroups) {\n group.move(frameDelta.x, frameDelta.y, true)\n }\n }\n\n lastCanvasDelta = canvasDelta\n })\n }\n\n function endDrag(event: PointerEvent, nodeId: NodeId | undefined) {\n // Apply snap to final position if snap was active (matches LiteGraph behavior)\n if (shouldSnap(event) && nodeId) {\n const boundsUpdates: NodeBoundsUpdate[] = []\n\n // Snap main node\n const currentLayout = toValue(layoutStore.getNodeLayoutRef(nodeId))\n if (currentLayout) {\n const currentPos = currentLayout.position\n const snappedPos = applySnapToPosition({ ...currentPos })\n\n // Only add update if position actually changed\n if (snappedPos.x !== currentPos.x || snappedPos.y !== currentPos.y) {\n boundsUpdates.push({\n nodeId,\n bounds: {\n x: snappedPos.x,\n y: snappedPos.y,\n width: currentLayout.size.width,\n height: currentLayout.size.height\n }\n })\n }\n }\n\n // Also snap other selected nodes\n // Capture all positions at the start to ensure consistent state\n if (\n otherSelectedNodesStartPositions &&\n otherSelectedNodesStartPositions.size > 0\n ) {\n for (const otherNodeId of otherSelectedNodesStartPositions.keys()) {\n const nodeLayout = layoutStore.getNodeLayoutRef(otherNodeId).value\n if (nodeLayout) {\n const currentPos = { ...nodeLayout.position }\n const snappedPos = applySnapToPosition(currentPos)\n\n // Only add update if position actually changed\n if (\n snappedPos.x !== currentPos.x ||\n snappedPos.y !== currentPos.y\n ) {\n boundsUpdates.push({\n nodeId: otherNodeId,\n bounds: {\n x: snappedPos.x,\n y: snappedPos.y,\n width: nodeLayout.size.width,\n height: nodeLayout.size.height\n }\n })\n }\n }\n }\n }\n\n // Apply all snap updates in a single batched transaction\n if (boundsUpdates.length > 0) {\n layoutStore.batchUpdateNodeBounds(boundsUpdates)\n }\n }\n\n dragStartPos = null\n dragStartMouse = null\n otherSelectedNodesStartPositions = null\n selectedGroups = null\n lastCanvasDelta = null\n\n // Stop tracking shift key state\n stopShiftSync?.()\n stopShiftSync = null\n\n // Cancel any pending animation frame\n if (rafId !== null) {\n cancelAnimationFrame(rafId)\n rafId = null\n }\n }\n\n return {\n startDrag,\n handleDrag,\n endDrag\n }\n}\n","import { onScopeDispose, ref, toValue } from 'vue'\nimport type { MaybeRefOrGetter } from 'vue'\n\nimport { isMiddlePointerInput } from '@/base/pointerUtils'\nimport { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'\nimport { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'\nimport { layoutStore } from '@/renderer/core/layout/store/layoutStore'\nimport { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'\nimport { isMultiSelectKey } from '@/renderer/extensions/vueNodes/utils/selectionUtils'\nimport { useNodeDrag } from '@/renderer/extensions/vueNodes/layout/useNodeDrag'\n\nexport function useNodePointerInteractions(\n nodeIdRef: MaybeRefOrGetter<string>\n) {\n const { startDrag, endDrag, handleDrag } = useNodeDrag()\n // Use canvas interactions for proper wheel event handling and pointer event capture control\n const { forwardEventToCanvas, shouldHandleNodePointerEvents } =\n useCanvasInteractions()\n const { handleNodeSelect, toggleNodeSelectionAfterPointerUp } =\n useNodeEventHandlers()\n const { nodeManager } = useVueNodeLifecycle()\n\n const forwardMiddlePointerIfNeeded = (event: PointerEvent) => {\n if (!isMiddlePointerInput(event)) return false\n forwardEventToCanvas(event)\n return true\n }\n\n let hasDraggingStarted = false\n\n const startPosition = ref({ x: 0, y: 0 })\n\n const DRAG_THRESHOLD = 3 // pixels\n\n function onPointerdown(event: PointerEvent) {\n if (forwardMiddlePointerIfNeeded(event)) return\n\n // Only start drag on left-click (button 0)\n if (event.button !== 0) return\n\n // Don't handle pointer events when canvas is in panning mode - forward to canvas instead\n if (!shouldHandleNodePointerEvents.value) {\n forwardEventToCanvas(event)\n return\n }\n\n const nodeId = toValue(nodeIdRef)\n if (!nodeId) {\n console.warn(\n 'LGraphNode: nodeData is null/undefined in handlePointerDown'\n )\n return\n }\n\n // IMPORTANT: Read from actual LGraphNode to get correct state\n if (nodeManager.value?.getNode(nodeId)?.flags?.pinned) {\n return\n }\n\n startPosition.value = { x: event.clientX, y: event.clientY }\n\n safeDragStart(event, nodeId)\n }\n\n function onPointermove(event: PointerEvent) {\n if (forwardMiddlePointerIfNeeded(event)) return\n\n // Don't activate drag while resizing\n if (layoutStore.isResizingVueNodes.value) return\n\n const nodeId = toValue(nodeIdRef)\n\n if (nodeManager.value?.getNode(nodeId)?.flags?.pinned) {\n return\n }\n\n const multiSelect = isMultiSelectKey(event)\n\n const lmbDown = event.buttons & 1\n if (lmbDown && multiSelect && !layoutStore.isDraggingVueNodes.value) {\n layoutStore.isDraggingVueNodes.value = true\n handleNodeSelect(event, nodeId)\n safeDragStart(event, nodeId)\n return\n }\n // Check if we should start dragging (pointer moved beyond threshold)\n if (lmbDown && !layoutStore.isDraggingVueNodes.value) {\n const dx = event.clientX - startPosition.value.x\n const dy = event.clientY - startPosition.value.y\n const distance = Math.sqrt(dx * dx + dy * dy)\n\n if (distance > DRAG_THRESHOLD) {\n layoutStore.isDraggingVueNodes.value = true\n handleNodeSelect(event, nodeId)\n }\n }\n\n if (layoutStore.isDraggingVueNodes.value) {\n handleDrag(event, nodeId)\n }\n }\n\n function cleanupDragState() {\n layoutStore.isDraggingVueNodes.value = false\n }\n\n function safeDragStart(event: PointerEvent, nodeId: string) {\n try {\n startDrag(event, nodeId)\n } finally {\n hasDraggingStarted = true\n }\n }\n\n function safeDragEnd(event: PointerEvent) {\n try {\n const nodeId = toValue(nodeIdRef)\n endDrag(event, nodeId)\n } catch (error) {\n console.error('Error during endDrag:', error)\n } finally {\n hasDraggingStarted = false\n cleanupDragState()\n }\n }\n\n function onPointerup(event: PointerEvent) {\n if (forwardMiddlePointerIfNeeded(event)) return\n // Don't handle pointer events when canvas is in panning mode - forward to canvas instead\n const canHandlePointer = shouldHandleNodePointerEvents.value\n if (!canHandlePointer) {\n forwardEventToCanvas(event)\n return\n }\n const wasDragging = layoutStore.isDraggingVueNodes.value\n\n if (hasDraggingStarted || wasDragging) {\n safeDragEnd(event)\n\n if (wasDragging) {\n return\n }\n }\n\n // Skip selection handling for right-click (button 2) - context menu handles its own selection\n if (event.button === 2) return\n\n const multiSelect = isMultiSelectKey(event)\n\n const nodeId = toValue(nodeIdRef)\n if (nodeId) {\n toggleNodeSelectionAfterPointerUp(nodeId, multiSelect)\n }\n }\n\n function onPointercancel(event: PointerEvent) {\n if (!layoutStore.isDraggingVueNodes.value) return\n safeDragEnd(event)\n }\n\n /**\n * Handles right-click during drag operations\n * Cancels the current drag to prevent context menu from appearing while dragging\n */\n function onContextmenu(event: MouseEvent) {\n if (!layoutStore.isDraggingVueNodes.value) return\n\n event.preventDefault()\n // Simply cleanup state without calling endDrag to avoid synthetic event creation\n cleanupDragState()\n }\n\n // Cleanup on unmount to prevent resource leaks\n onScopeDispose(() => {\n cleanupDragState()\n })\n\n const pointerHandlers = {\n onPointerdown,\n onPointermove,\n onPointerup,\n onPointercancel,\n onContextmenu\n } as const\n\n return {\n pointerHandlers\n }\n}\n","/**\n * Generic Vue Element Tracking System\n *\n * Automatically tracks DOM size and position changes for Vue-rendered elements\n * and syncs them to the layout store. Uses a single shared ResizeObserver for\n * performance, with elements identified by configurable data attributes.\n *\n * Supports different element types (nodes, slots, widgets, etc.) with\n * customizable data attributes and update handlers.\n */\nimport { getCurrentInstance, onMounted, onUnmounted, toValue } from 'vue'\nimport type { MaybeRefOrGetter } from 'vue'\n\nimport { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'\nimport { LiteGraph } from '@/lib/litegraph/src/litegraph'\nimport { layoutStore } from '@/renderer/core/layout/store/layoutStore'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport type { Bounds, NodeId } from '@/renderer/core/layout/types'\nimport { LayoutSource } from '@/renderer/core/layout/types'\n\nimport { syncNodeSlotLayoutsFromDOM } from './useSlotElementTracking'\n\n/**\n * Generic update item for element bounds tracking\n */\ninterface ElementBoundsUpdate {\n /** Element identifier (could be nodeId, widgetId, slotId, etc.) */\n id: string\n /** Updated bounds */\n bounds: Bounds\n}\n\n/**\n * Configuration for different types of tracked elements\n */\ninterface ElementTrackingConfig {\n /** Data attribute name (e.g., 'nodeId') */\n dataAttribute: string\n /** Handler for processing bounds updates */\n updateHandler: (updates: ElementBoundsUpdate[]) => void\n}\n\n/**\n * Registry of tracking configurations by element type\n */\nconst trackingConfigs: Map<string, ElementTrackingConfig> = new Map([\n [\n 'node',\n {\n dataAttribute: 'nodeId',\n updateHandler: (updates) => {\n const nodeUpdates = updates.map(({ id, bounds }) => ({\n nodeId: id as NodeId,\n bounds\n }))\n layoutStore.batchUpdateNodeBounds(nodeUpdates)\n }\n }\n ]\n])\n\n// Single ResizeObserver instance for all Vue elements\nconst resizeObserver = new ResizeObserver((entries) => {\n if (useCanvasStore().linearMode) return\n // Canvas is ready when this code runs; no defensive guards needed.\n const conv = useSharedCanvasPositionConversion()\n // Group updates by type, then flush via each config's handler\n const updatesByType = new Map<string, ElementBoundsUpdate[]>()\n // Track nodes whose slots should be resynced after node size changes\n const nodesNeedingSlotResync = new Set<string>()\n\n for (const entry of entries) {\n if (!(entry.target instanceof HTMLElement)) continue\n const element = entry.target\n\n // Find which type this element belongs to\n let elementType: string | undefined\n let elementId: string | undefined\n\n for (const [type, config] of trackingConfigs) {\n const id = element.dataset[config.dataAttribute]\n if (id) {\n elementType = type\n elementId = id\n break\n }\n }\n\n if (!elementType || !elementId) continue\n\n // Use borderBoxSize when available; fall back to contentRect for older engines/tests\n // Border box is the border included FULL wxh DOM value.\n const borderBox = Array.isArray(entry.borderBoxSize)\n ? entry.borderBoxSize[0]\n : {\n inlineSize: entry.contentRect.width,\n blockSize: entry.contentRect.height\n }\n const width = borderBox.inlineSize\n const height = borderBox.blockSize\n\n // Screen-space rect\n const rect = element.getBoundingClientRect()\n const [cx, cy] = conv.clientPosToCanvasPos([rect.left, rect.top])\n const topLeftCanvas = { x: cx, y: cy }\n const bounds: Bounds = {\n x: topLeftCanvas.x,\n y: topLeftCanvas.y + LiteGraph.NODE_TITLE_HEIGHT,\n width: Math.max(0, width),\n height: Math.max(0, height)\n }\n\n let updates = updatesByType.get(elementType)\n if (!updates) {\n updates = []\n updatesByType.set(elementType, updates)\n }\n updates.push({ id: elementId, bounds })\n\n // If this entry is a node, mark it for slot layout resync\n if (elementType === 'node' && elementId) {\n nodesNeedingSlotResync.add(elementId)\n }\n }\n\n layoutStore.setSource(LayoutSource.DOM)\n\n // Flush per-type\n for (const [type, updates] of updatesByType) {\n const config = trackingConfigs.get(type)\n if (config && updates.length) config.updateHandler(updates)\n }\n\n // After node bounds are updated, refresh slot cached offsets and layouts\n if (nodesNeedingSlotResync.size > 0) {\n for (const nodeId of nodesNeedingSlotResync) {\n syncNodeSlotLayoutsFromDOM(nodeId)\n }\n }\n})\n\n/**\n * Tracks DOM element size/position changes for a Vue component and syncs to layout store\n *\n * Sets up automatic ResizeObserver tracking when the component mounts and cleans up\n * when unmounted. The tracked element is identified by a data attribute set on the\n * component's root DOM element.\n *\n * @param appIdentifier - Application-level identifier for this tracked element (not a DOM ID)\n * Example: node ID like 'node-123', widget ID like 'widget-456'\n * @param trackingType - Type of element being tracked, determines which tracking config to use\n * Example: 'node' for Vue nodes, 'widget' for UI widgets\n *\n * @example\n * ```ts\n * // Track a Vue node component with ID 'my-node-123'\n * useVueElementTracking('my-node-123', 'node')\n *\n * // Would set data-node-id=\"my-node-123\" on the component's root element\n * // and sync size changes to layoutStore.batchUpdateNodeBounds()\n * ```\n */\nexport function useVueElementTracking(\n appIdentifierMaybe: MaybeRefOrGetter<string>,\n trackingType: string\n) {\n const appIdentifier = toValue(appIdentifierMaybe)\n onMounted(() => {\n const element = getCurrentInstance()?.proxy?.$el\n if (!(element instanceof HTMLElement) || !appIdentifier) return\n\n const config = trackingConfigs.get(trackingType)\n if (!config) return\n\n // Set the data attribute expected by the RO pipeline for this type\n element.dataset[config.dataAttribute] = appIdentifier\n resizeObserver.observe(element)\n })\n\n onUnmounted(() => {\n const element = getCurrentInstance()?.proxy?.$el\n if (!(element instanceof HTMLElement)) return\n\n const config = trackingConfigs.get(trackingType)\n if (!config) return\n\n // Remove the data attribute and observer\n delete element.dataset[config.dataAttribute]\n resizeObserver.unobserve(element)\n })\n}\n","import { storeToRefs } from 'pinia'\nimport { computed, toValue } from 'vue'\nimport type { MaybeRefOrGetter } from 'vue'\n\nimport { useExecutionStore } from '@/stores/executionStore'\n\n/**\n * Composable for managing execution state of Vue-based nodes\n *\n * Provides reactive access to execution state and progress for a specific node\n * by injecting execution data from the parent GraphCanvas provider.\n *\n * @param nodeLocatorIdMaybe - Locator ID (root or subgraph scoped) of the node to track\n * @returns Object containing reactive execution state and progress\n */\nexport const useNodeExecutionState = (\n nodeLocatorIdMaybe: MaybeRefOrGetter<string | undefined>\n) => {\n const locatorId = computed(() => toValue(nodeLocatorIdMaybe) ?? '')\n const { nodeLocationProgressStates, isIdle } =\n storeToRefs(useExecutionStore())\n\n const progressState = computed(() => {\n const id = locatorId.value\n return id ? nodeLocationProgressStates.value[id] : undefined\n })\n\n const executing = computed(\n () => !isIdle.value && progressState.value?.state === 'running'\n )\n\n const progress = computed(() => {\n const state = progressState.value\n return state && state.max > 0 ? state.value / state.max : undefined\n })\n\n const progressPercentage = computed(() => {\n const prog = progress.value\n return prog !== undefined ? Math.round(prog * 100) : undefined\n })\n\n const executionState = computed(() => {\n const state = progressState.value\n if (!state) return 'idle'\n return state.state\n })\n\n return {\n executing,\n progress,\n progressPercentage,\n progressState,\n executionState\n }\n}\n","import { computed, onUnmounted, toValue } from 'vue'\nimport type { MaybeRefOrGetter } from 'vue'\n\nimport { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'\nimport { layoutStore } from '@/renderer/core/layout/store/layoutStore'\nimport { LayoutSource } from '@/renderer/core/layout/types'\nimport type { Point } from '@/renderer/core/layout/types'\n\n/**\n * Composable for individual Vue node components\n * Uses customRef for shared write access with Canvas renderer\n */\nexport function useNodeLayout(nodeIdMaybe: MaybeRefOrGetter<string>) {\n const nodeId = toValue(nodeIdMaybe)\n const mutations = useLayoutMutations()\n\n // Get the customRef for this node (shared write access)\n const layoutRef = layoutStore.getNodeLayoutRef(nodeId)\n\n // Clean up refs and triggers when Vue component unmounts\n onUnmounted(() => {\n layoutStore.cleanupNodeRef(nodeId)\n })\n\n // Computed properties for easy access\n const position = computed(() => {\n const layout = layoutRef.value\n const pos = layout?.position ?? { x: 0, y: 0 }\n return pos\n })\n const size = computed(\n () => layoutRef.value?.size ?? { width: 200, height: 100 }\n )\n\n const zIndex = computed(() => layoutRef.value?.zIndex ?? 0)\n\n /**\n * Update node position directly (without drag)\n */\n function moveNodeTo(position: Point) {\n mutations.setSource(LayoutSource.Vue)\n mutations.moveNode(nodeId, position)\n }\n\n return {\n // Reactive state (via customRef)\n position,\n size,\n zIndex,\n\n // Mutations\n moveNodeTo\n }\n}\n","import { storeToRefs } from 'pinia'\nimport { computed, toValue } from 'vue'\nimport type { MaybeRefOrGetter, Ref } from 'vue'\n\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { useNodeOutputStore } from '@/stores/imagePreviewStore'\n\nexport const useNodePreviewState = (\n nodeIdMaybe: MaybeRefOrGetter<string>,\n options?: {\n isCollapsed?: Ref<boolean>\n }\n) => {\n const nodeId = toValue(nodeIdMaybe)\n const workflowStore = useWorkflowStore()\n const { nodePreviewImages } = storeToRefs(useNodeOutputStore())\n\n const locatorId = computed(() => workflowStore.nodeIdToNodeLocatorId(nodeId))\n\n const previewUrls = computed(() => {\n const key = locatorId.value\n if (!key) return undefined\n const urls = nodePreviewImages.value[key]\n return urls?.length ? urls : undefined\n })\n\n const hasPreview = computed(() => !!previewUrls.value?.length)\n\n const latestPreviewUrl = computed(() => {\n const urls = previewUrls.value\n return urls?.length ? urls.at(-1) : ''\n })\n\n const shouldShowPreviewImg = computed(() => {\n if (!options?.isCollapsed) {\n return hasPreview.value\n }\n return !options.isCollapsed.value && hasPreview.value\n })\n\n return {\n locatorId,\n previewUrls,\n hasPreview,\n latestPreviewUrl,\n shouldShowPreviewImg\n }\n}\n","import { useEventListener } from '@vueuse/core'\nimport { ref } from 'vue'\n\nimport type { Point, Size } from '@/renderer/core/layout/types'\nimport { layoutStore } from '@/renderer/core/layout/store/layoutStore'\nimport { useNodeSnap } from '@/renderer/extensions/vueNodes/composables/useNodeSnap'\nimport { useShiftKeySync } from '@/renderer/extensions/vueNodes/composables/useShiftKeySync'\nimport { useTransformState } from '@/renderer/core/layout/transform/useTransformState'\n\ninterface ResizeCallbackPayload {\n size: Size\n}\n\n/**\n * Composable for node resizing functionality (bottom-right corner only)\n *\n * Provides resize handle interaction that integrates with the layout system.\n * Handles pointer capture, coordinate calculations, and size constraints.\n */\nexport function useNodeResize(\n resizeCallback: (payload: ResizeCallbackPayload, element: HTMLElement) => void\n) {\n const transformState = useTransformState()\n\n const isResizing = ref(false)\n const resizeStartPointer = ref<Point | null>(null)\n const resizeStartSize = ref<Size | null>(null)\n\n // Snap-to-grid functionality\n const { shouldSnap, applySnapToSize } = useNodeSnap()\n\n // Shift key sync for LiteGraph canvas preview\n const { trackShiftKey } = useShiftKeySync()\n\n const startResize = (event: PointerEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n const target = event.currentTarget\n if (!(target instanceof HTMLElement)) return\n\n const nodeElement = target.closest('[data-node-id]')\n if (!(nodeElement instanceof HTMLElement)) return\n\n const rect = nodeElement.getBoundingClientRect()\n const scale = transformState.camera.z\n\n const startSize: Size = {\n width: rect.width / scale,\n height: rect.height / scale\n }\n\n // Track shift key state and sync to canvas for snap preview\n const stopShiftSync = trackShiftKey(event)\n\n // Capture pointer to ensure we get all move/up events\n target.setPointerCapture(event.pointerId)\n\n // Mark as resizing to prevent drag from activating\n layoutStore.isResizingVueNodes.value = true\n isResizing.value = true\n resizeStartPointer.value = { x: event.clientX, y: event.clientY }\n resizeStartSize.value = startSize\n\n const handlePointerMove = (moveEvent: PointerEvent) => {\n if (\n !isResizing.value ||\n !resizeStartPointer.value ||\n !resizeStartSize.value\n ) {\n return\n }\n\n const scale = transformState.camera.z\n const deltaX =\n (moveEvent.clientX - resizeStartPointer.value.x) / (scale || 1)\n const deltaY =\n (moveEvent.clientY - resizeStartPointer.value.y) / (scale || 1)\n\n let newSize: Size = {\n width: resizeStartSize.value.width + deltaX,\n height: resizeStartSize.value.height + deltaY\n }\n\n // Apply snap if shift is held\n if (shouldSnap(moveEvent)) {\n newSize = applySnapToSize(newSize)\n }\n\n const nodeElement = target.closest('[data-node-id]')\n if (nodeElement instanceof HTMLElement) {\n resizeCallback({ size: newSize }, nodeElement)\n }\n }\n\n const handlePointerUp = (upEvent: PointerEvent) => {\n if (isResizing.value) {\n isResizing.value = false\n layoutStore.isResizingVueNodes.value = false\n resizeStartPointer.value = null\n resizeStartSize.value = null\n\n // Stop tracking shift key state\n stopShiftSync()\n\n target.releasePointerCapture(upEvent.pointerId)\n stopMoveListen()\n stopUpListen()\n }\n }\n\n const stopMoveListen = useEventListener('pointermove', handlePointerMove)\n const stopUpListen = useEventListener('pointerup', handlePointerUp)\n }\n\n return {\n startResize,\n isResizing\n }\n}\n","<template>\n <div v-if=\"imageUrl\" class=\"flex h-full min-h-16 w-full min-w-16 flex-col\">\n <!-- Image Container -->\n <div\n class=\"relative h-88 w-full grow overflow-hidden rounded-[5px] bg-node-component-surface\"\n >\n <!-- Error State -->\n <div\n v-if=\"imageError\"\n class=\"text-pure-white flex h-full w-full flex-col items-center justify-center text-center\"\n >\n <i-lucide:image-off class=\"mb-1 size-8 text-smoke-500\" />\n <p class=\"text-xs text-smoke-400\">{{ $t('g.imageFailedToLoad') }}</p>\n </div>\n\n <!-- Main Image -->\n <img\n v-else\n :src=\"imageUrl\"\n :alt=\"$t('g.liveSamplingPreview')\"\n class=\"pointer-events-none h-full w-full object-contain object-center\"\n @load=\"handleImageLoad\"\n @error=\"handleImageError\"\n />\n </div>\n\n <!-- Image Dimensions -->\n <div class=\"text-node-component-header-text mt-1 text-center text-xs\">\n {{\n imageError\n ? $t('g.errorLoadingImage')\n : actualDimensions || $t('g.calculatingDimensions')\n }}\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { ref, watch } from 'vue'\n\ninterface LivePreviewProps {\n /** Image URL to display */\n imageUrl: string | null\n}\n\nconst props = defineProps<LivePreviewProps>()\n\nconst actualDimensions = ref<string | null>(null)\nconst imageError = ref(false)\n\nwatch(\n () => props.imageUrl,\n () => {\n // Reset states when URL changes\n actualDimensions.value = null\n imageError.value = false\n }\n)\n\nconst handleImageLoad = (event: Event) => {\n if (!event.target || !(event.target instanceof HTMLImageElement)) return\n const img = event.target\n imageError.value = false\n if (img.naturalWidth && img.naturalHeight) {\n actualDimensions.value = `${img.naturalWidth} x ${img.naturalHeight}`\n }\n}\n\nconst handleImageError = () => {\n imageError.value = true\n actualDimensions.value = null\n}\n</script>\n","<template>\n <div\n v-if=\"imageUrls.length > 0\"\n class=\"video-preview group relative flex size-full min-h-16 min-w-16 flex-col px-2\"\n @keydown=\"handleKeyDown\"\n >\n <!-- Video Wrapper -->\n <div\n ref=\"videoWrapperEl\"\n class=\"relative h-full w-full grow overflow-hidden rounded-[5px] bg-node-component-surface\"\n tabindex=\"0\"\n role=\"region\"\n :aria-label=\"$t('g.videoPreview')\"\n :aria-busy=\"showLoader\"\n @mouseenter=\"handleMouseEnter\"\n @mouseleave=\"handleMouseLeave\"\n @focusin=\"handleFocusIn\"\n @focusout=\"handleFocusOut\"\n >\n <!-- Error State -->\n <div\n v-if=\"videoError\"\n role=\"alert\"\n class=\"flex size-full flex-col items-center justify-center bg-muted-background text-center text-base-foreground py-8\"\n >\n <i\n class=\"mb-2 icon-[lucide--video-off] h-12 w-12 text-base-foreground\"\n />\n <p class=\"text-sm text-base-foreground\">\n {{ $t('g.videoFailedToLoad') }}\n </p>\n <p class=\"mt-1 text-xs text-base-foreground\">\n {{ getVideoFilename(currentVideoUrl) }}\n </p>\n </div>\n\n <!-- Loading State -->\n <Skeleton\n v-if=\"showLoader && !videoError\"\n class=\"absolute inset-0 size-full\"\n border-radius=\"5px\"\n width=\"100%\"\n height=\"100%\"\n />\n\n <!-- Main Video -->\n <video\n v-if=\"!videoError\"\n :src=\"currentVideoUrl\"\n :class=\"cn('block size-full object-contain', showLoader && 'invisible')\"\n controls\n loop\n playsinline\n @loadeddata=\"handleVideoLoad\"\n @error=\"handleVideoError\"\n />\n\n <!-- Floating Action Buttons (appear on hover) -->\n <div\n v-if=\"isHovered || isFocused\"\n class=\"actions absolute top-2 right-2 flex gap-2.5\"\n >\n <!-- Download Button -->\n <button\n :class=\"actionButtonClass\"\n :title=\"$t('g.downloadVideo')\"\n :aria-label=\"$t('g.downloadVideo')\"\n @click=\"handleDownload\"\n >\n <i class=\"icon-[lucide--download] h-4 w-4\" />\n </button>\n\n <!-- Close Button -->\n <button\n :class=\"actionButtonClass\"\n :title=\"$t('g.removeVideo')\"\n :aria-label=\"$t('g.removeVideo')\"\n @click=\"handleRemove\"\n >\n <i class=\"icon-[lucide--x] h-4 w-4\" />\n </button>\n </div>\n\n <!-- Multiple Videos Navigation -->\n <div\n v-if=\"hasMultipleVideos\"\n class=\"absolute right-2 bottom-2 left-2 flex justify-center gap-1\"\n >\n <button\n v-for=\"(_, index) in imageUrls\"\n :key=\"index\"\n :class=\"getNavigationDotClass(index)\"\n :aria-label=\"\n $t('g.viewVideoOfTotal', {\n index: index + 1,\n total: imageUrls.length\n })\n \"\n @click=\"setCurrentIndex(index)\"\n />\n </div>\n </div>\n\n <!-- Video Dimensions -->\n <div class=\"mt-2 text-center text-xs text-muted-foreground\">\n <span v-if=\"videoError\" class=\"text-red-400\">\n {{ $t('g.errorLoadingVideo') }}\n </span>\n <span v-else-if=\"showLoader\" class=\"text-smoke-400\">\n {{ $t('g.loading') }}...\n </span>\n <span v-else>\n {{ actualDimensions || $t('g.calculatingDimensions') }}\n </span>\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useToast } from 'primevue'\nimport Skeleton from 'primevue/skeleton'\nimport { computed, ref, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { downloadFile } from '@/base/common/downloadUtil'\nimport { useNodeOutputStore } from '@/stores/imagePreviewStore'\nimport { cn } from '@/utils/tailwindUtil'\n\ninterface VideoPreviewProps {\n /** Array of video URLs to display */\n readonly imageUrls: readonly string[] // Named imageUrls for consistency with parent components\n /** Optional node ID for context-aware actions */\n readonly nodeId?: string\n}\n\nconst props = defineProps<VideoPreviewProps>()\n\nconst { t } = useI18n()\nconst nodeOutputStore = useNodeOutputStore()\n\nconst actionButtonClass =\n 'flex h-8 min-h-8 items-center justify-center gap-2.5 rounded-lg border-0 bg-button-surface px-2 py-2 text-button-surface-contrast shadow-sm transition-colors duration-200 hover:bg-button-hover-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-button-surface-contrast focus-visible:ring-offset-2 focus-visible:ring-offset-transparent cursor-pointer'\n\n// Component state\nconst currentIndex = ref(0)\nconst isHovered = ref(false)\nconst isFocused = ref(false)\nconst actualDimensions = ref<string | null>(null)\nconst videoError = ref(false)\nconst showLoader = ref(false)\n\nconst videoWrapperEl = ref<HTMLDivElement>()\n\n// Computed values\nconst currentVideoUrl = computed(() => props.imageUrls[currentIndex.value])\nconst hasMultipleVideos = computed(() => props.imageUrls.length > 1)\n\n// Watch for URL changes and reset state\nwatch(\n () => props.imageUrls,\n (newUrls) => {\n // Reset current index if it's out of bounds\n if (currentIndex.value >= newUrls.length) {\n currentIndex.value = 0\n }\n\n // Reset loading and error states when URLs change\n actualDimensions.value = null\n videoError.value = false\n showLoader.value = newUrls.length > 0\n },\n { deep: true, immediate: true }\n)\n\n// Event handlers\nconst handleVideoLoad = (event: Event) => {\n if (!event.target || !(event.target instanceof HTMLVideoElement)) return\n const video = event.target\n showLoader.value = false\n videoError.value = false\n if (video.videoWidth && video.videoHeight) {\n actualDimensions.value = `${video.videoWidth} x ${video.videoHeight}`\n }\n}\n\nconst handleVideoError = () => {\n showLoader.value = false\n videoError.value = true\n actualDimensions.value = null\n}\n\nconst handleDownload = () => {\n try {\n downloadFile(currentVideoUrl.value)\n } catch (error) {\n useToast().add({\n severity: 'error',\n summary: 'Error',\n detail: t('g.failedToDownloadVideo'),\n life: 3000,\n group: 'video-preview'\n })\n }\n}\n\nconst handleRemove = () => {\n if (!props.nodeId) return\n nodeOutputStore.removeNodeOutputs(props.nodeId)\n}\n\nconst setCurrentIndex = (index: number) => {\n if (index >= 0 && index < props.imageUrls.length) {\n currentIndex.value = index\n actualDimensions.value = null\n showLoader.value = true\n videoError.value = false\n }\n}\n\nconst handleMouseEnter = () => {\n isHovered.value = true\n}\n\nconst handleMouseLeave = () => {\n isHovered.value = false\n}\n\nconst handleFocusIn = () => {\n isFocused.value = true\n}\n\nconst handleFocusOut = (event: FocusEvent) => {\n if (!videoWrapperEl.value?.contains(event.relatedTarget as Node)) {\n isFocused.value = false\n }\n}\n\nconst getNavigationDotClass = (index: number) => {\n return [\n 'w-2 h-2 rounded-full transition-all duration-200 border-0 cursor-pointer',\n index === currentIndex.value ? 'bg-white' : 'bg-white/50 hover:bg-white/80'\n ]\n}\n\nconst handleKeyDown = (event: KeyboardEvent) => {\n if (props.imageUrls.length <= 1) return\n\n switch (event.key) {\n case 'ArrowLeft':\n event.preventDefault()\n setCurrentIndex(\n currentIndex.value > 0\n ? currentIndex.value - 1\n : props.imageUrls.length - 1\n )\n break\n case 'ArrowRight':\n event.preventDefault()\n setCurrentIndex(\n currentIndex.value < props.imageUrls.length - 1\n ? currentIndex.value + 1\n : 0\n )\n break\n case 'Home':\n event.preventDefault()\n setCurrentIndex(0)\n break\n case 'End':\n event.preventDefault()\n setCurrentIndex(props.imageUrls.length - 1)\n break\n }\n}\n\nconst getVideoFilename = (url: string): string => {\n try {\n return new URL(url).searchParams.get('filename') || 'Unknown file'\n } catch {\n return 'Invalid URL'\n }\n}\n</script>\n","<template>\n <div\n v-if=\"imageUrls.length > 0\"\n class=\"image-preview group relative flex size-full min-h-16 min-w-16 flex-col px-2 justify-center\"\n @keydown=\"handleKeyDown\"\n >\n <!-- Image Wrapper -->\n <div\n ref=\"imageWrapperEl\"\n class=\"h-full w-full overflow-hidden rounded-[5px] bg-muted-background relative\"\n tabindex=\"0\"\n role=\"img\"\n :aria-label=\"$t('g.imagePreview')\"\n :aria-busy=\"showLoader\"\n @mouseenter=\"handleMouseEnter\"\n @mouseleave=\"handleMouseLeave\"\n @focusin=\"handleFocusIn\"\n @focusout=\"handleFocusOut\"\n >\n <!-- Error State -->\n <div\n v-if=\"imageError\"\n role=\"alert\"\n class=\"flex size-full flex-col items-center justify-center bg-muted-background text-center text-base-foreground py-8\"\n >\n <i\n class=\"mb-2 icon-[lucide--image-off] h-12 w-12 text-base-foreground\"\n />\n <p class=\"text-sm text-base-foreground\">\n {{ $t('g.imageFailedToLoad') }}\n </p>\n <p class=\"mt-1 text-xs text-base-foreground\">\n {{ getImageFilename(currentImageUrl) }}\n </p>\n </div>\n <!-- Loading State -->\n <div v-if=\"showLoader && !imageError\" class=\"size-full\">\n <Skeleton border-radius=\"5px\" width=\"100%\" height=\"100%\" />\n </div>\n <!-- Main Image -->\n <img\n v-if=\"!imageError\"\n ref=\"currentImageEl\"\n :src=\"currentImageUrl\"\n :alt=\"imageAltText\"\n class=\"block size-full object-contain pointer-events-none\"\n @load=\"handleImageLoad\"\n @error=\"handleImageError\"\n />\n\n <!-- Floating Action Buttons (appear on hover and focus) -->\n <div\n v-if=\"isHovered || isFocused\"\n class=\"actions absolute top-2 right-2 flex gap-2.5\"\n >\n <!-- Mask/Edit Button -->\n <button\n v-if=\"!hasMultipleImages\"\n :class=\"actionButtonClass\"\n :title=\"$t('g.editOrMaskImage')\"\n :aria-label=\"$t('g.editOrMaskImage')\"\n @click=\"handleEditMask\"\n >\n <i-comfy:mask class=\"h-4 w-4\" />\n </button>\n\n <!-- Download Button -->\n <button\n :class=\"actionButtonClass\"\n :title=\"$t('g.downloadImage')\"\n :aria-label=\"$t('g.downloadImage')\"\n @click=\"handleDownload\"\n >\n <i class=\"icon-[lucide--download] h-4 w-4\" />\n </button>\n\n <!-- Close Button -->\n <button\n :class=\"actionButtonClass\"\n :title=\"$t('g.removeImage')\"\n :aria-label=\"$t('g.removeImage')\"\n @click=\"handleRemove\"\n >\n <i class=\"icon-[lucide--x] h-4 w-4\" />\n </button>\n </div>\n </div>\n\n <!-- Image Dimensions -->\n <div class=\"pt-2 text-center text-xs text-base-foreground\">\n <span v-if=\"imageError\" class=\"text-red-400\">\n {{ $t('g.errorLoadingImage') }}\n </span>\n <span v-else-if=\"showLoader\" class=\"text-base-foreground\">\n {{ $t('g.loading') }}...\n </span>\n <span v-else>\n {{ actualDimensions || $t('g.calculatingDimensions') }}\n </span>\n </div>\n <!-- Multiple Images Navigation -->\n <div\n v-if=\"hasMultipleImages\"\n class=\"flex flex-wrap justify-center gap-1 pt-4\"\n >\n <button\n v-for=\"(_, index) in imageUrls\"\n :key=\"index\"\n :class=\"getNavigationDotClass(index)\"\n :aria-current=\"index === currentIndex ? 'true' : undefined\"\n :aria-label=\"\n $t('g.viewImageOfTotal', {\n index: index + 1,\n total: imageUrls.length\n })\n \"\n @click=\"setCurrentIndex(index)\"\n />\n </div>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { useTimeoutFn } from '@vueuse/core'\nimport { useToast } from 'primevue'\nimport Skeleton from 'primevue/skeleton'\nimport { computed, ref, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { downloadFile } from '@/base/common/downloadUtil'\nimport { app } from '@/scripts/app'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { useNodeOutputStore } from '@/stores/imagePreviewStore'\n\ninterface ImagePreviewProps {\n /** Array of image URLs to display */\n readonly imageUrls: readonly string[]\n /** Optional node ID for context-aware actions */\n readonly nodeId?: string\n}\n\nconst props = defineProps<ImagePreviewProps>()\n\nconst { t } = useI18n()\nconst commandStore = useCommandStore()\nconst nodeOutputStore = useNodeOutputStore()\n\nconst actionButtonClass =\n 'flex h-8 min-h-8 items-center justify-center gap-2.5 rounded-lg border-0 bg-button-surface px-2 py-2 text-button-surface-contrast shadow-sm transition-colors duration-200 hover:bg-button-hover-surface focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-button-surface-contrast focus-visible:ring-offset-2 focus-visible:ring-offset-transparent cursor-pointer'\n\n// Component state\nconst currentIndex = ref(0)\nconst isHovered = ref(false)\nconst isFocused = ref(false)\nconst actualDimensions = ref<string | null>(null)\nconst imageError = ref(false)\nconst showLoader = ref(false)\n\nconst currentImageEl = ref<HTMLImageElement>()\nconst imageWrapperEl = ref<HTMLDivElement>()\n\nconst { start: startDelayedLoader, stop: stopDelayedLoader } = useTimeoutFn(\n () => {\n showLoader.value = true\n },\n 250,\n // Make sure it doesnt run on component mount\n { immediate: false }\n)\n\n// Computed values\nconst currentImageUrl = computed(() => props.imageUrls[currentIndex.value])\nconst hasMultipleImages = computed(() => props.imageUrls.length > 1)\nconst imageAltText = computed(() => `Node output ${currentIndex.value + 1}`)\n\n// Watch for URL changes and reset state\nwatch(\n () => props.imageUrls,\n (newUrls) => {\n // Reset current index if it's out of bounds\n if (currentIndex.value >= newUrls.length) {\n currentIndex.value = 0\n }\n\n // Reset loading and error states when URLs change\n actualDimensions.value = null\n\n imageError.value = false\n if (newUrls.length > 0) startDelayedLoader()\n },\n { deep: true, immediate: true }\n)\n\n// Event handlers\nconst handleImageLoad = (event: Event) => {\n if (!event.target || !(event.target instanceof HTMLImageElement)) return\n const img = event.target\n stopDelayedLoader()\n showLoader.value = false\n imageError.value = false\n if (img.naturalWidth && img.naturalHeight) {\n actualDimensions.value = `${img.naturalWidth} x ${img.naturalHeight}`\n }\n}\n\nconst handleImageError = () => {\n stopDelayedLoader()\n showLoader.value = false\n imageError.value = true\n actualDimensions.value = null\n}\n\n// In vueNodes mode, we need to set them manually before opening the mask editor.\nconst setupNodeForMaskEditor = () => {\n if (!props.nodeId || !currentImageEl.value) return\n const node = app.rootGraph?.getNodeById(props.nodeId)\n if (!node) return\n node.imageIndex = currentIndex.value\n node.imgs = [currentImageEl.value]\n app.canvas?.select(node)\n}\n\nconst handleEditMask = () => {\n setupNodeForMaskEditor()\n void commandStore.execute('Comfy.MaskEditor.OpenMaskEditor')\n}\n\nconst handleDownload = () => {\n try {\n downloadFile(currentImageUrl.value)\n } catch (error) {\n useToast().add({\n severity: 'error',\n summary: 'Error',\n detail: t('g.failedToDownloadImage'),\n life: 3000,\n group: 'image-preview'\n })\n }\n}\n\nconst handleRemove = () => {\n if (!props.nodeId) return\n nodeOutputStore.removeNodeOutputs(props.nodeId)\n}\n\nconst setCurrentIndex = (index: number) => {\n if (currentIndex.value === index) return\n if (index >= 0 && index < props.imageUrls.length) {\n currentIndex.value = index\n startDelayedLoader()\n imageError.value = false\n }\n}\n\nconst handleMouseEnter = () => {\n isHovered.value = true\n}\n\nconst handleMouseLeave = () => {\n isHovered.value = false\n}\n\nconst handleFocusIn = () => {\n isFocused.value = true\n}\n\nconst handleFocusOut = (event: FocusEvent) => {\n // Only unfocus if focus is leaving the wrapper entirely\n if (!imageWrapperEl.value?.contains(event.relatedTarget as Node)) {\n isFocused.value = false\n }\n}\n\nconst getNavigationDotClass = (index: number) => {\n return [\n 'w-2 h-2 rounded-full transition-all duration-200 border-0 cursor-pointer p-0',\n index === currentIndex.value\n ? 'bg-base-foreground'\n : 'bg-base-foreground/50 hover:bg-base-foreground/80'\n ]\n}\n\nconst handleKeyDown = (event: KeyboardEvent) => {\n if (props.imageUrls.length <= 1) return\n\n switch (event.key) {\n case 'ArrowLeft':\n event.preventDefault()\n setCurrentIndex(\n currentIndex.value > 0\n ? currentIndex.value - 1\n : props.imageUrls.length - 1\n )\n break\n case 'ArrowRight':\n event.preventDefault()\n setCurrentIndex(\n currentIndex.value < props.imageUrls.length - 1\n ? currentIndex.value + 1\n : 0\n )\n break\n case 'Home':\n event.preventDefault()\n setCurrentIndex(0)\n break\n case 'End':\n event.preventDefault()\n setCurrentIndex(props.imageUrls.length - 1)\n break\n }\n}\n\nconst getImageFilename = (url: string): string => {\n try {\n return new URL(url).searchParams.get('filename') || 'Unknown file'\n } catch {\n return 'Invalid URL'\n }\n}\n</script>\n","<template>\n <div v-if=\"renderError\" class=\"node-error p-2 text-sm text-red-500\">\n {{ st('nodeErrors.content', 'Node Content Error') }}\n </div>\n <div v-else class=\"lg-node-content flex grow flex-col\">\n <!-- Default slot for custom content -->\n <slot>\n <VideoPreview\n v-if=\"hasMedia && media?.type === 'video'\"\n :image-urls=\"media.urls\"\n :node-id=\"nodeId\"\n class=\"mt-2\"\n />\n <ImagePreview\n v-else-if=\"hasMedia && media?.type === 'image'\"\n :image-urls=\"media.urls\"\n :node-id=\"nodeId\"\n class=\"mt-2\"\n />\n </slot>\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, onErrorCaptured, ref } from 'vue'\n\nimport type { VueNodeData } from '@/composables/graph/useGraphNodeManager'\nimport { useErrorHandling } from '@/composables/useErrorHandling'\nimport { st } from '@/i18n'\n\nimport VideoPreview from '../VideoPreview.vue'\nimport ImagePreview from './ImagePreview.vue'\n\ninterface NodeContentProps {\n nodeData?: VueNodeData\n media?: {\n type: 'image' | 'video'\n urls: string[]\n }\n}\n\nconst props = defineProps<NodeContentProps>()\n\nconst hasMedia = computed(() => props.media && props.media.urls.length > 0)\n\n// Get node ID from nodeData\nconst nodeId = computed(() => props.nodeData?.id?.toString())\n\n// Error boundary implementation\nconst renderError = ref<string | null>(null)\nconst { toastErrorHandler } = useErrorHandling()\n\nonErrorCaptured((error) => {\n renderError.value = error.message\n toastErrorHandler(error)\n return false\n})\n</script>\n","<template>\n <div v-if=\"renderError\" class=\"node-error p-2 text-sm text-red-500\">\n {{ st('nodeErrors.render', 'Node Render Error') }}\n </div>\n <div\n v-else\n ref=\"nodeContainerRef\"\n tabindex=\"0\"\n :data-node-id=\"nodeData.id\"\n :class=\"\n cn(\n 'bg-component-node-background lg-node absolute text-sm',\n 'contain-style contain-layout min-w-[225px] min-h-(--node-height) w-(--node-width)',\n shapeClass,\n 'touch-none flex flex-col',\n 'border-1 border-solid border-component-node-border',\n // hover (only when node should handle events)\n shouldHandleNodePointerEvents &&\n 'hover:ring-7 ring-node-component-ring',\n 'outline-transparent outline-2 focus-visible:outline-node-component-outline',\n borderClass,\n outlineClass,\n cursorClass,\n {\n [`${beforeShapeClass} before:pointer-events-none before:absolute before:bg-bypass/60 before:inset-0`]:\n bypassed,\n [`${beforeShapeClass} before:pointer-events-none before:absolute before:inset-0`]:\n muted,\n 'ring-4 ring-primary-500 bg-primary-500/10': isDraggingOver\n },\n\n shouldHandleNodePointerEvents\n ? 'pointer-events-auto'\n : 'pointer-events-none',\n !isCollapsed && ' pb-1'\n )\n \"\n :style=\"[\n {\n transform: `translate(${position.x ?? 0}px, ${(position.y ?? 0) - LiteGraph.NODE_TITLE_HEIGHT}px)`,\n zIndex: zIndex,\n opacity: nodeOpacity,\n '--component-node-background': applyLightThemeColor(nodeData.bgcolor)\n }\n ]\"\n v-bind=\"remainingPointerHandlers\"\n @pointerdown=\"nodeOnPointerdown\"\n @wheel=\"handleWheel\"\n @contextmenu=\"handleContextMenu\"\n @dragover.prevent=\"handleDragOver\"\n @dragleave=\"handleDragLeave\"\n @drop.stop.prevent=\"handleDrop\"\n >\n <div\n v-if=\"displayHeader\"\n class=\"flex flex-col justify-center items-center relative\"\n >\n <template v-if=\"isCollapsed\">\n <SlotConnectionDot\n v-if=\"hasInputs\"\n multi\n class=\"absolute left-0 -translate-x-1/2\"\n />\n <SlotConnectionDot\n v-if=\"hasOutputs\"\n multi\n class=\"absolute right-0 translate-x-1/2\"\n />\n <NodeSlots :node-data=\"nodeData\" unified />\n </template>\n <NodeHeader\n :node-data=\"nodeData\"\n :collapsed=\"isCollapsed\"\n @collapse=\"handleCollapse\"\n @update:title=\"handleHeaderTitleUpdate\"\n @enter-subgraph=\"handleEnterSubgraph\"\n />\n </div>\n\n <div\n v-if=\"isCollapsed && executing && progress !== undefined\"\n :class=\"\n cn(\n 'absolute inset-x-4 -bottom-[1px] translate-y-1/2 rounded-full',\n progressClasses\n )\n \"\n :style=\"{ width: `${Math.min(progress * 100, 100)}%` }\"\n />\n\n <template v-if=\"!isCollapsed\">\n <div class=\"relative mb-1\">\n <!-- Progress bar for executing state -->\n <div\n v-if=\"executing && progress !== undefined\"\n :class=\"\n cn(\n 'absolute inset-x-0 top-1/2 -translate-y-1/2',\n !!(progress < 1) && 'rounded-r-full',\n progressClasses\n )\n \"\n :style=\"{ width: `${Math.min(progress * 100, 100)}%` }\"\n />\n </div>\n\n <div\n class=\"flex flex-1 flex-col gap-1 pb-2\"\n :data-testid=\"`node-body-${nodeData.id}`\"\n >\n <NodeSlots :node-data=\"nodeData\" />\n\n <NodeWidgets v-if=\"nodeData.widgets?.length\" :node-data=\"nodeData\" />\n\n <div v-if=\"hasCustomContent\" class=\"min-h-0 flex-1 flex\">\n <NodeContent :node-data=\"nodeData\" :media=\"nodeMedia\" />\n </div>\n <!-- Live mid-execution preview images -->\n <div v-if=\"shouldShowPreviewImg\" class=\"min-h-0 flex-1 px-4\">\n <LivePreview :image-url=\"latestPreviewUrl || null\" />\n </div>\n\n <!-- Show advanced inputs button for subgraph nodes -->\n <div v-if=\"showAdvancedInputsButton\" class=\"flex justify-center px-3\">\n <button\n :class=\"\n cn(\n WidgetInputBaseClass,\n 'w-full h-7 flex justify-center items-center gap-2 text-sm px-3 outline-0 ring-0 truncate',\n 'transition-all cursor-pointer hover:bg-accent-background duration-150 active:scale-95'\n )\n \"\n @click.stop=\"handleShowAdvancedInputs\"\n >\n <i class=\"icon-[lucide--settings-2] size-4\" />\n <span>{{ t('rightSidePanel.showAdvancedInputsButton') }}</span>\n </button>\n </div>\n </div>\n </template>\n\n <!-- Resize handle (bottom-right only) -->\n <div\n v-if=\"!isCollapsed && nodeData.resizable !== false\"\n role=\"button\"\n :aria-label=\"t('g.resizeFromBottomRight')\"\n :class=\"cn(baseResizeHandleClasses, 'right-0 bottom-0 cursor-se-resize')\"\n @pointerdown.stop=\"handleResizePointerDown\"\n />\n </div>\n</template>\n\n<script setup lang=\"ts\">\nimport { storeToRefs } from 'pinia'\nimport { computed, nextTick, onErrorCaptured, onMounted, ref, watch } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport type { VueNodeData } from '@/composables/graph/useGraphNodeManager'\nimport { showNodeOptions } from '@/composables/graph/useMoreOptionsMenu'\nimport { useErrorHandling } from '@/composables/useErrorHandling'\nimport { st } from '@/i18n'\nimport {\n LGraphCanvas,\n LGraphEventMode,\n LiteGraph,\n RenderShape\n} from '@/lib/litegraph/src/litegraph'\nimport { SubgraphNode } from '@/lib/litegraph/src/subgraph/SubgraphNode'\nimport { TitleMode } from '@/lib/litegraph/src/types/globalEnums'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'\nimport { layoutStore } from '@/renderer/core/layout/store/layoutStore'\nimport SlotConnectionDot from '@/renderer/extensions/vueNodes/components/SlotConnectionDot.vue'\nimport { useNodeEventHandlers } from '@/renderer/extensions/vueNodes/composables/useNodeEventHandlers'\nimport { useNodePointerInteractions } from '@/renderer/extensions/vueNodes/composables/useNodePointerInteractions'\nimport { useNodeZIndex } from '@/renderer/extensions/vueNodes/composables/useNodeZIndex'\nimport { useVueElementTracking } from '@/renderer/extensions/vueNodes/composables/useVueNodeResizeTracking'\nimport { useNodeExecutionState } from '@/renderer/extensions/vueNodes/execution/useNodeExecutionState'\nimport { useNodeDrag } from '@/renderer/extensions/vueNodes/layout/useNodeDrag'\nimport { useNodeLayout } from '@/renderer/extensions/vueNodes/layout/useNodeLayout'\nimport { useNodePreviewState } from '@/renderer/extensions/vueNodes/preview/useNodePreviewState'\nimport { nonWidgetedInputs } from '@/renderer/extensions/vueNodes/utils/nodeDataUtils'\nimport { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils'\nimport { app } from '@/scripts/app'\nimport { useExecutionStore } from '@/stores/executionStore'\nimport { useNodeOutputStore } from '@/stores/imagePreviewStore'\nimport { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'\nimport { isTransparent } from '@/utils/colorUtil'\nimport {\n getLocatorIdFromNodeData,\n getNodeByLocatorId\n} from '@/utils/graphTraversalUtil'\nimport { cn } from '@/utils/tailwindUtil'\n\nimport { useNodeResize } from '../interactions/resize/useNodeResize'\nimport { WidgetInputBaseClass } from '../widgets/components/layout'\nimport LivePreview from './LivePreview.vue'\nimport NodeContent from './NodeContent.vue'\nimport NodeHeader from './NodeHeader.vue'\nimport NodeSlots from './NodeSlots.vue'\nimport NodeWidgets from './NodeWidgets.vue'\n\n// Extended props for main node component\ninterface LGraphNodeProps {\n nodeData: VueNodeData\n error?: string | null\n}\n\nconst { nodeData, error = null } = defineProps<LGraphNodeProps>()\n\nconst { t } = useI18n()\n\nconst { handleNodeCollapse, handleNodeTitleUpdate, handleNodeRightClick } =\n useNodeEventHandlers()\nconst { bringNodeToFront } = useNodeZIndex()\n\nuseVueElementTracking(() => nodeData.id, 'node')\n\nconst { selectedNodeIds } = storeToRefs(useCanvasStore())\nconst isSelected = computed(() => {\n return selectedNodeIds.value.has(nodeData.id)\n})\n\nconst nodeLocatorId = computed(() => getLocatorIdFromNodeData(nodeData))\nconst { executing, progress } = useNodeExecutionState(nodeLocatorId)\nconst executionStore = useExecutionStore()\nconst hasExecutionError = computed(\n () => executionStore.lastExecutionErrorNodeId === nodeData.id\n)\n\nconst hasAnyError = computed((): boolean => {\n return !!(\n hasExecutionError.value ||\n nodeData.hasErrors ||\n error ||\n (executionStore.lastNodeErrors?.[nodeData.id]?.errors.length ?? 0) > 0\n )\n})\n\nconst displayHeader = computed(() => nodeData.titleMode !== TitleMode.NO_TITLE)\n\nconst isCollapsed = computed(() => nodeData.flags?.collapsed ?? false)\nconst bypassed = computed(\n (): boolean => nodeData.mode === LGraphEventMode.BYPASS\n)\nconst muted = computed((): boolean => nodeData.mode === LGraphEventMode.NEVER)\n\nconst nodeOpacity = computed(() => {\n const globalOpacity = useSettingStore().get('Comfy.Node.Opacity') ?? 1\n\n // For muted/bypassed nodes, apply the 0.5 multiplier on top of global opacity\n if (bypassed.value || muted.value) {\n return globalOpacity * 0.5\n }\n\n return globalOpacity\n})\n\nconst hasInputs = computed(() => nonWidgetedInputs(nodeData).length > 0)\nconst hasOutputs = computed((): boolean => !!nodeData.outputs?.length)\n\n// Use canvas interactions for proper wheel event handling and pointer event capture control\nconst { handleWheel, shouldHandleNodePointerEvents } = useCanvasInteractions()\n\n// Error boundary implementation\nconst renderError = ref<string | null>(null)\nconst { toastErrorHandler } = useErrorHandling()\n\nonErrorCaptured((error) => {\n renderError.value = error.message\n toastErrorHandler(error)\n return false // Prevent error propagation\n})\n\nconst { position, size, zIndex } = useNodeLayout(() => nodeData.id)\nconst { pointerHandlers } = useNodePointerInteractions(() => nodeData.id)\nconst { onPointerdown, ...remainingPointerHandlers } = pointerHandlers\nconst { startDrag } = useNodeDrag()\n\nasync function nodeOnPointerdown(event: PointerEvent) {\n if (event.altKey && lgraphNode.value) {\n const result = LGraphCanvas.cloneNodes([lgraphNode.value])\n if (result?.created?.length) {\n const [newNode] = result.created\n startDrag(event, `${newNode.id}`)\n layoutStore.isDraggingVueNodes.value = true\n await nextTick()\n bringNodeToFront(`${newNode.id}`)\n return\n }\n }\n onPointerdown(event)\n}\n\n// Handle right-click context menu\nconst handleContextMenu = (event: MouseEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n // First handle the standard right-click behavior (selection)\n handleNodeRightClick(event as PointerEvent, nodeData.id)\n\n // Show the node options menu at the cursor position\n showNodeOptions(event)\n}\n\nonMounted(() => {\n initSizeStyles()\n})\n\n/**\n * Set initial DOM size from layout store, but respect intrinsic content minimum.\n * Important: nodes can mount in a collapsed state, and the collapse watcher won't\n * run initially. Match the collapsed runtime behavior by writing to the correct\n * CSS variables on mount.\n */\nfunction initSizeStyles() {\n const el = nodeContainerRef.value\n const { width, height } = size.value\n if (!el) return\n\n const suffix = isCollapsed.value ? '-x' : ''\n\n el.style.setProperty(`--node-width${suffix}`, `${width}px`)\n el.style.setProperty(`--node-height${suffix}`, `${height}px`)\n}\n\nconst baseResizeHandleClasses =\n 'absolute h-3 w-3 opacity-0 pointer-events-auto focus-visible:outline focus-visible:outline-2 focus-visible:outline-white/40'\n\nconst MIN_NODE_WIDTH = 225\n\nconst { startResize } = useNodeResize((result, element) => {\n if (isCollapsed.value) return\n\n // Clamp width to minimum to avoid conflicts with CSS min-width\n const clampedWidth = Math.max(result.size.width, MIN_NODE_WIDTH)\n\n // Apply size directly to DOM element - ResizeObserver will pick this up\n element.style.setProperty('--node-width', `${clampedWidth}px`)\n element.style.setProperty('--node-height', `${result.size.height}px`)\n})\n\nconst handleResizePointerDown = (event: PointerEvent) => {\n if (event.button !== 0) return\n if (!shouldHandleNodePointerEvents.value) return\n if (nodeData.flags?.pinned) return\n if (nodeData.resizable === false) return\n startResize(event)\n}\n\nwatch(isCollapsed, (collapsed) => {\n const element = nodeContainerRef.value\n if (!element) return\n const [from, to] = collapsed ? ['', '-x'] : ['-x', '']\n const currentWidth = element.style.getPropertyValue(`--node-width${from}`)\n element.style.setProperty(`--node-width${to}`, currentWidth)\n element.style.setProperty(`--node-width${from}`, '')\n\n const currentHeight = element.style.getPropertyValue(`--node-height${from}`)\n element.style.setProperty(`--node-height${to}`, currentHeight)\n element.style.setProperty(`--node-height${from}`, '')\n})\n\n// Check if node has custom content (like image/video outputs)\nconst hasCustomContent = computed(() => {\n // Show custom content if node has media outputs\n return !!nodeMedia.value && nodeMedia.value.urls.length > 0\n})\n\n// Computed classes and conditions for better reusability\nconst progressClasses = 'h-2 bg-primary-500 transition-all duration-300'\n\nconst { latestPreviewUrl, shouldShowPreviewImg } = useNodePreviewState(\n () => nodeData.id,\n {\n isCollapsed\n }\n)\n\nconst borderClass = computed(() => {\n if (hasAnyError.value) return 'border-node-stroke-error'\n //FIXME need a better way to detecting transparency\n if (\n !displayHeader.value &&\n nodeData.bgcolor &&\n isTransparent(nodeData.bgcolor)\n )\n return 'border-0'\n return ''\n})\n\nconst outlineClass = computed(() => {\n return cn(\n isSelected.value && 'outline-node-component-outline',\n hasAnyError.value && 'outline-node-stroke-error',\n executing.value && 'outline-node-stroke-executing'\n )\n})\n\nconst cursorClass = computed(() => {\n return cn(\n nodeData.flags?.pinned\n ? 'cursor-default'\n : layoutStore.isDraggingVueNodes.value\n ? 'cursor-grabbing'\n : 'cursor-grab'\n )\n})\n\nconst shapeClass = computed(() => {\n switch (nodeData.shape) {\n case RenderShape.BOX:\n return 'rounded-none'\n case RenderShape.CARD:\n return 'rounded-tl-2xl rounded-br-2xl rounded-tr-none rounded-bl-none'\n default:\n return 'rounded-2xl'\n }\n})\n\nconst beforeShapeClass = computed(() => {\n switch (nodeData.shape) {\n case RenderShape.BOX:\n return 'before:rounded-none'\n case RenderShape.CARD:\n return 'before:rounded-tl-2xl before:rounded-br-2xl before:rounded-tr-none before:rounded-bl-none'\n default:\n return 'before:rounded-2xl'\n }\n})\n\n// Event handlers\nconst handleCollapse = () => {\n handleNodeCollapse(nodeData.id, !isCollapsed.value)\n}\n\nconst handleHeaderTitleUpdate = (newTitle: string) => {\n handleNodeTitleUpdate(nodeData.id, newTitle)\n}\n\nconst handleEnterSubgraph = () => {\n useTelemetry()?.trackUiButtonClicked({\n button_id: 'graph_node_open_subgraph_clicked'\n })\n const graph = app.rootGraph\n if (!graph) {\n console.warn('LGraphNode: No graph available for subgraph navigation')\n return\n }\n\n const locatorId = getLocatorIdFromNodeData(nodeData)\n\n const litegraphNode = getNodeByLocatorId(graph, locatorId)\n\n if (!litegraphNode?.isSubgraphNode() || !('subgraph' in litegraphNode)) {\n console.warn('LGraphNode: Node is not a valid subgraph node', litegraphNode)\n return\n }\n\n const canvas = app.canvas\n if (!canvas || typeof canvas.openSubgraph !== 'function') {\n console.warn('LGraphNode: Canvas or openSubgraph method not available')\n return\n }\n\n canvas.openSubgraph(litegraphNode.subgraph, litegraphNode)\n}\n\nconst nodeOutputs = useNodeOutputStore()\n\nconst nodeOutputLocatorId = computed(() =>\n nodeData.subgraphId ? `${nodeData.subgraphId}:${nodeData.id}` : nodeData.id\n)\n\nconst lgraphNode = computed(() => {\n const locatorId = getLocatorIdFromNodeData(nodeData)\n return getNodeByLocatorId(app.rootGraph, locatorId)\n})\n\nconst showAdvancedInputsButton = computed(() => {\n const node = lgraphNode.value\n if (!node || !(node instanceof SubgraphNode)) return false\n\n // Check if there are hidden inputs (widgets not promoted)\n const interiorNodes = node.subgraph.nodes\n const allInteriorWidgets = interiorNodes.flatMap((n) => n.widgets ?? [])\n\n return allInteriorWidgets.some((w) => !w.computedDisabled && !w.promoted)\n})\n\nfunction handleShowAdvancedInputs() {\n const rightSidePanelStore = useRightSidePanelStore()\n rightSidePanelStore.focusSection('advanced-inputs')\n}\n\nconst nodeMedia = computed(() => {\n const newOutputs = nodeOutputs.nodeOutputs[nodeOutputLocatorId.value]\n const node = lgraphNode.value\n\n if (!node || !newOutputs?.images?.length) return undefined\n\n const urls = nodeOutputs.getNodeImageUrls(node)\n if (!urls?.length) return undefined\n\n // Determine media type from previewMediaType or fallback to input slot types\n // Note: Despite the field name \"images\", videos are also included in outputs\n // TODO: fix the backend to return videos using the videos key instead of the images key\n const hasVideoInput = node.inputs?.some((input) => input.type === 'VIDEO')\n const type =\n node.previewMediaType === 'video' ||\n (!node.previewMediaType && hasVideoInput)\n ? 'video'\n : 'image'\n\n return { type, urls } as const\n})\n\nconst nodeContainerRef = ref<HTMLDivElement>()\n\n// Drag and drop support\nconst isDraggingOver = ref(false)\n\nfunction handleDragOver(event: DragEvent) {\n const node = lgraphNode.value\n if (!node || !node.onDragOver) {\n isDraggingOver.value = false\n return\n }\n\n // Call the litegraph node's onDragOver callback to check if files are valid\n const canDrop = node.onDragOver(event)\n isDraggingOver.value = canDrop\n}\n\nfunction handleDragLeave() {\n isDraggingOver.value = false\n}\n\nasync function handleDrop(event: DragEvent) {\n isDraggingOver.value = false\n\n const node = lgraphNode.value\n if (!node || !node.onDragDrop) {\n return\n }\n\n // Forward the drop event to the litegraph node's onDragDrop callback\n await node.onDragDrop(event)\n}\n</script>\n","import type { useSettingStore } from '@/platform/settings/settingStore'\n\nlet pendingCallbacks: Array<() => Promise<void>> = []\nlet isNewUserDetermined = false\nlet isNewUserCached: boolean | null = null\n\nexport const newUserService = () => {\n function checkIsNewUser(\n settingStore: ReturnType<typeof useSettingStore>\n ): boolean {\n const isNewUserSettings =\n Object.keys(settingStore.settingValues).length === 0 ||\n !settingStore.get('Comfy.TutorialCompleted')\n const hasNoWorkflow = !localStorage.getItem('workflow')\n const hasNoPreviousWorkflow = !localStorage.getItem(\n 'Comfy.PreviousWorkflow'\n )\n\n return isNewUserSettings && hasNoWorkflow && hasNoPreviousWorkflow\n }\n\n async function registerInitCallback(callback: () => Promise<void>) {\n if (isNewUserDetermined) {\n if (isNewUserCached) {\n try {\n await callback()\n } catch (error) {\n console.error('New user initialization callback failed:', error)\n }\n }\n } else {\n pendingCallbacks.push(callback)\n }\n }\n\n async function initializeIfNewUser(\n settingStore: ReturnType<typeof useSettingStore>\n ) {\n if (isNewUserDetermined) return\n\n isNewUserCached = checkIsNewUser(settingStore)\n isNewUserDetermined = true\n\n if (!isNewUserCached) {\n pendingCallbacks = []\n return\n }\n\n await settingStore.set(\n 'Comfy.InstalledVersion',\n __COMFYUI_FRONTEND_VERSION__\n )\n\n for (const callback of pendingCallbacks) {\n try {\n await callback()\n } catch (error) {\n console.error('New user initialization callback failed:', error)\n }\n }\n\n pendingCallbacks = []\n }\n\n function isNewUser(): boolean | null {\n return isNewUserDetermined ? isNewUserCached : null\n }\n\n return {\n registerInitCallback,\n initializeIfNewUser,\n isNewUser\n }\n}\n","<template>\n <div\n v-if=\"isVisible\"\n class=\"pointer-events-none absolute z-9999 border border-blue-400 bg-blue-500/20\"\n :style=\"rectangleStyle\"\n />\n</template>\n\n<script setup lang=\"ts\">\nimport { useRafFn } from '@vueuse/core'\nimport { computed, ref } from 'vue'\n\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\n\nconst canvasStore = useCanvasStore()\n\nconst selectionRect = ref<{\n x: number\n y: number\n w: number\n h: number\n} | null>(null)\n\nuseRafFn(() => {\n const canvas = canvasStore.canvas\n if (!canvas) {\n selectionRect.value = null\n return\n }\n\n const { pointer, dragging_rectangle } = canvas\n\n if (dragging_rectangle && pointer.eDown && pointer.eMove) {\n const x = pointer.eDown.safeOffsetX\n const y = pointer.eDown.safeOffsetY\n const w = pointer.eMove.safeOffsetX - x\n const h = pointer.eMove.safeOffsetY - y\n\n selectionRect.value = { x, y, w, h }\n } else {\n selectionRect.value = null\n }\n})\n\nconst isVisible = computed(() => selectionRect.value !== null)\n\nconst rectangleStyle = computed(() => {\n const rect = selectionRect.value\n if (!rect) return {}\n\n const left = rect.w >= 0 ? rect.x : rect.x + rect.w\n const top = rect.h >= 0 ? rect.y : rect.y + rect.h\n const width = Math.abs(rect.w)\n const height = Math.abs(rect.h)\n\n return {\n left: `${left}px`,\n top: `${top}px`,\n width: `${width}px`,\n height: `${height}px`\n }\n})\n</script>\n","<template>\n <!-- Load splitter overlay only after comfyApp is ready. -->\n <!-- If load immediately, the top-level splitter stateKey won't be correctly\n synced with the stateStorage (localStorage). -->\n <LiteGraphCanvasSplitterOverlay v-if=\"comfyAppReady\">\n <template v-if=\"showUI\" #workflow-tabs>\n <div\n v-if=\"workflowTabsPosition === 'Topbar'\"\n class=\"workflow-tabs-container pointer-events-auto relative h-9.5 w-full\"\n >\n <!-- Native drag area for Electron -->\n <div\n v-if=\"isNativeWindow() && workflowTabsPosition !== 'Topbar'\"\n class=\"app-drag fixed top-0 left-0 z-10 h-[var(--comfy-topbar-height)] w-full\"\n />\n <div\n class=\"flex h-full items-center border-b border-interface-stroke bg-comfy-menu-bg shadow-interface\"\n >\n <WorkflowTabs />\n <TopbarBadges />\n </div>\n </div>\n </template>\n <template v-if=\"showUI\" #side-toolbar>\n <SideToolbar />\n </template>\n <template v-if=\"showUI\" #side-bar-panel>\n <div\n class=\"sidebar-content-container h-full w-full overflow-x-hidden overflow-y-auto\"\n >\n <ExtensionSlot v-if=\"activeSidebarTab\" :extension=\"activeSidebarTab\" />\n </div>\n </template>\n <template v-if=\"showUI\" #topmenu>\n <TopMenuSection />\n </template>\n <template v-if=\"showUI\" #bottom-panel>\n <BottomPanel />\n </template>\n <template v-if=\"showUI\" #right-side-panel>\n <NodePropertiesPanel />\n </template>\n <template #graph-canvas-panel>\n <GraphCanvasMenu v-if=\"canvasMenuEnabled\" class=\"pointer-events-auto\" />\n <MiniMap\n v-if=\"comfyAppReady && minimapEnabled && betaMenuEnabled\"\n class=\"pointer-events-auto\"\n />\n </template>\n </LiteGraphCanvasSplitterOverlay>\n <canvas\n id=\"graph-canvas\"\n ref=\"canvasRef\"\n tabindex=\"1\"\n class=\"absolute inset-0 size-full touch-none\"\n />\n\n <!-- TransformPane for Vue node rendering -->\n <TransformPane\n v-if=\"shouldRenderVueNodes && comfyApp.canvas && comfyAppReady\"\n :canvas=\"comfyApp.canvas\"\n @wheel.capture=\"canvasInteractions.forwardEventToCanvas\"\n >\n <!-- Vue nodes rendered based on graph nodes -->\n <LGraphNode\n v-for=\"nodeData in allNodes\"\n :key=\"nodeData.id\"\n :node-data=\"nodeData\"\n :error=\"\n executionStore.lastExecutionError?.node_id === nodeData.id\n ? 'Execution error'\n : null\n \"\n :zoom-level=\"canvasStore.canvas?.ds?.scale || 1\"\n :data-node-id=\"nodeData.id\"\n />\n </TransformPane>\n\n <!-- Selection rectangle overlay - rendered in DOM layer to appear above DOM widgets -->\n <SelectionRectangle v-if=\"comfyAppReady\" />\n\n <NodeTooltip v-if=\"tooltipEnabled\" />\n <NodeSearchboxPopover ref=\"nodeSearchboxPopoverRef\" />\n\n <!-- Initialize components after comfyApp is ready. useAbsolutePosition requires\n canvasStore.canvas to be initialized. -->\n <template v-if=\"comfyAppReady\">\n <TitleEditor />\n <SelectionToolbox v-if=\"selectionToolboxEnabled\" />\n <!-- Render legacy DOM widgets only when Vue nodes are disabled -->\n <DomWidgets v-if=\"!shouldRenderVueNodes\" />\n </template>\n</template>\n\n<script setup lang=\"ts\">\nimport { useEventListener, whenever } from '@vueuse/core'\nimport {\n computed,\n nextTick,\n onMounted,\n onUnmounted,\n ref,\n shallowRef,\n watch,\n watchEffect\n} from 'vue'\n\nimport LiteGraphCanvasSplitterOverlay from '@/components/LiteGraphCanvasSplitterOverlay.vue'\nimport TopMenuSection from '@/components/TopMenuSection.vue'\nimport BottomPanel from '@/components/bottomPanel/BottomPanel.vue'\nimport ExtensionSlot from '@/components/common/ExtensionSlot.vue'\nimport DomWidgets from '@/components/graph/DomWidgets.vue'\nimport GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'\nimport NodeTooltip from '@/components/graph/NodeTooltip.vue'\nimport SelectionToolbox from '@/components/graph/SelectionToolbox.vue'\nimport TitleEditor from '@/components/graph/TitleEditor.vue'\nimport NodePropertiesPanel from '@/components/rightSidePanel/RightSidePanel.vue'\nimport NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'\nimport SideToolbar from '@/components/sidebar/SideToolbar.vue'\nimport TopbarBadges from '@/components/topbar/TopbarBadges.vue'\nimport WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'\nimport { useChainCallback } from '@/composables/functional/useChainCallback'\nimport type { VueNodeData } from '@/composables/graph/useGraphNodeManager'\nimport { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'\nimport { useNodeBadge } from '@/composables/node/useNodeBadge'\nimport { useCanvasDrop } from '@/composables/useCanvasDrop'\nimport { useContextMenuTranslation } from '@/composables/useContextMenuTranslation'\nimport { useCopy } from '@/composables/useCopy'\nimport { useGlobalLitegraph } from '@/composables/useGlobalLitegraph'\nimport { usePaste } from '@/composables/usePaste'\nimport { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'\nimport { mergeCustomNodesI18n, t } from '@/i18n'\nimport { LiteGraph } from '@/lib/litegraph/src/litegraph'\nimport { useLitegraphSettings } from '@/platform/settings/composables/useLitegraphSettings'\nimport { CORE_SETTINGS } from '@/platform/settings/constants/coreSettings'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useToastStore } from '@/platform/updates/common/toastStore'\nimport { useWorkflowService } from '@/platform/workflow/core/services/workflowService'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { useWorkflowAutoSave } from '@/platform/workflow/persistence/composables/useWorkflowAutoSave'\nimport { useWorkflowPersistence } from '@/platform/workflow/persistence/composables/useWorkflowPersistence'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport { useCanvasInteractions } from '@/renderer/core/canvas/useCanvasInteractions'\nimport TransformPane from '@/renderer/core/layout/transform/TransformPane.vue'\nimport MiniMap from '@/renderer/extensions/minimap/MiniMap.vue'\nimport LGraphNode from '@/renderer/extensions/vueNodes/components/LGraphNode.vue'\nimport { UnauthorizedError, api } from '@/scripts/api'\nimport { app as comfyApp } from '@/scripts/app'\nimport { ChangeTracker } from '@/scripts/changeTracker'\nimport { IS_CONTROL_WIDGET, updateControlWidgetLabel } from '@/scripts/widgets'\nimport { useColorPaletteService } from '@/services/colorPaletteService'\nimport { newUserService } from '@/services/newUserService'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { useExecutionStore } from '@/stores/executionStore'\nimport { useNodeDefStore } from '@/stores/nodeDefStore'\nimport { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'\nimport { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'\nimport { useWorkspaceStore } from '@/stores/workspaceStore'\nimport { isNativeWindow } from '@/utils/envUtil'\nimport { forEachNode } from '@/utils/graphTraversalUtil'\n\nimport SelectionRectangle from './SelectionRectangle.vue'\n\nconst emit = defineEmits<{\n ready: []\n}>()\nconst canvasRef = ref<HTMLCanvasElement | null>(null)\nconst nodeSearchboxPopoverRef = shallowRef<InstanceType<\n typeof NodeSearchboxPopover\n> | null>(null)\nconst settingStore = useSettingStore()\nconst nodeDefStore = useNodeDefStore()\nconst workspaceStore = useWorkspaceStore()\nconst canvasStore = useCanvasStore()\nconst executionStore = useExecutionStore()\nconst toastStore = useToastStore()\nconst colorPaletteStore = useColorPaletteStore()\nconst colorPaletteService = useColorPaletteService()\nconst canvasInteractions = useCanvasInteractions()\n\nconst betaMenuEnabled = computed(\n () => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'\n)\nconst workflowTabsPosition = computed(() =>\n settingStore.get('Comfy.Workflow.WorkflowTabsPosition')\n)\nconst canvasMenuEnabled = computed(() =>\n settingStore.get('Comfy.Graph.CanvasMenu')\n)\nconst tooltipEnabled = computed(() => settingStore.get('Comfy.EnableTooltips'))\nconst selectionToolboxEnabled = computed(() =>\n settingStore.get('Comfy.Canvas.SelectionToolbox')\n)\nconst activeSidebarTab = computed(() => {\n return workspaceStore.sidebarTab.activeSidebarTab\n})\nconst showUI = computed(\n () => !workspaceStore.focusMode && betaMenuEnabled.value\n)\n\nconst minimapEnabled = computed(() => settingStore.get('Comfy.Minimap.Visible'))\n\n// Feature flags\nconst { shouldRenderVueNodes } = useVueFeatureFlags()\n\n// Vue node system\nconst vueNodeLifecycle = useVueNodeLifecycle()\n\nconst handleVueNodeLifecycleReset = async () => {\n if (shouldRenderVueNodes.value) {\n vueNodeLifecycle.disposeNodeManagerAndSyncs()\n await nextTick()\n vueNodeLifecycle.initializeNodeManager()\n }\n}\n\nwatch(() => canvasStore.currentGraph, handleVueNodeLifecycleReset)\n\nwatch(\n () => canvasStore.isInSubgraph,\n async (newValue, oldValue) => {\n if (oldValue && !newValue) {\n useWorkflowStore().updateActiveGraph()\n }\n await handleVueNodeLifecycleReset()\n }\n)\n\nconst allNodes = computed((): VueNodeData[] =>\n Array.from(vueNodeLifecycle.nodeManager.value?.vueNodeData?.values() ?? [])\n)\n\nwatchEffect(() => {\n LiteGraph.nodeOpacity = settingStore.get('Comfy.Node.Opacity')\n})\nwatchEffect(() => {\n LiteGraph.nodeLightness = colorPaletteStore.completedActivePalette.light_theme\n ? 0.5\n : undefined\n})\n\nwatchEffect(() => {\n nodeDefStore.showDeprecated = settingStore.get('Comfy.Node.ShowDeprecated')\n})\n\nwatchEffect(() => {\n nodeDefStore.showExperimental = settingStore.get(\n 'Comfy.Node.ShowExperimental'\n )\n})\n\nwatchEffect(() => {\n const spellcheckEnabled = settingStore.get('Comfy.TextareaWidget.Spellcheck')\n const textareas = document.querySelectorAll<HTMLTextAreaElement>(\n 'textarea.comfy-multiline-input'\n )\n\n textareas.forEach((textarea: HTMLTextAreaElement) => {\n textarea.spellcheck = spellcheckEnabled\n // Force recheck to ensure visual update\n textarea.focus()\n textarea.blur()\n })\n})\n\nwatch(\n () => settingStore.get('Comfy.WidgetControlMode'),\n () => {\n if (!canvasStore.canvas) return\n\n forEachNode(comfyApp.rootGraph, (n) => {\n if (!n.widgets) return\n for (const w of n.widgets) {\n if (!w[IS_CONTROL_WIDGET]) continue\n updateControlWidgetLabel(w)\n if (!w.linkedWidgets) continue\n for (const l of w.linkedWidgets) {\n updateControlWidgetLabel(l)\n }\n }\n })\n canvasStore.canvas.setDirty(true)\n }\n)\n\nwatch(\n [() => canvasStore.canvas, () => settingStore.get('Comfy.ColorPalette')],\n async ([canvas, currentPaletteId]) => {\n if (!canvas) return\n\n await colorPaletteService.loadColorPalette(currentPaletteId)\n }\n)\n\nwatch(\n () => settingStore.get('Comfy.Canvas.BackgroundImage'),\n async () => {\n if (!canvasStore.canvas) return\n const currentPaletteId = colorPaletteStore.activePaletteId\n if (!currentPaletteId) return\n\n // Reload color palette to apply background image\n await colorPaletteService.loadColorPalette(currentPaletteId)\n // Mark background canvas as dirty\n canvasStore.canvas.setDirty(false, true)\n }\n)\nwatch(\n () => colorPaletteStore.activePaletteId,\n async (newValue) => {\n await settingStore.set('Comfy.ColorPalette', newValue)\n }\n)\n\n// Update the progress of executing nodes\nwatch(\n () =>\n [executionStore.nodeLocationProgressStates, canvasStore.canvas] as const,\n ([nodeLocationProgressStates, canvas]) => {\n if (!canvas?.graph) return\n for (const node of canvas.graph.nodes) {\n const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(node.id)\n const progressState = nodeLocationProgressStates[nodeLocatorId]\n if (progressState && progressState.state === 'running') {\n node.progress = progressState.value / progressState.max\n } else {\n node.progress = undefined\n }\n }\n\n // Force canvas redraw to ensure progress updates are visible\n canvas.setDirty(true, false)\n },\n { deep: true }\n)\n\n// Update node slot errors for LiteGraph nodes\n// (Vue nodes read from store directly)\nwatch(\n () => executionStore.lastNodeErrors,\n (lastNodeErrors) => {\n if (!comfyApp.graph) return\n\n forEachNode(comfyApp.rootGraph, (node) => {\n // Clear existing errors\n for (const slot of node.inputs) {\n delete slot.hasErrors\n }\n for (const slot of node.outputs) {\n delete slot.hasErrors\n }\n\n const nodeErrors = lastNodeErrors?.[node.id]\n if (!nodeErrors) return\n\n const validErrors = nodeErrors.errors.filter(\n (error) => error.extra_info?.input_name !== undefined\n )\n\n validErrors.forEach((error) => {\n const inputName = error.extra_info!.input_name!\n const inputIndex = node.findInputSlot(inputName)\n if (inputIndex !== -1) {\n node.inputs[inputIndex].hasErrors = true\n }\n })\n })\n\n comfyApp.canvas.setDirty(true, true)\n }\n)\n\nuseEventListener(\n canvasRef,\n 'litegraph:no-items-selected',\n () => {\n toastStore.add({\n severity: 'warn',\n summary: t('toastMessages.nothingSelected'),\n life: 2000\n })\n },\n { passive: true }\n)\n\nconst loadCustomNodesI18n = async () => {\n try {\n const i18nData = await api.getCustomNodesI18n()\n mergeCustomNodesI18n(i18nData)\n } catch (error) {\n console.error('Failed to load custom nodes i18n', error)\n }\n}\n\nconst comfyAppReady = ref(false)\nconst workflowPersistence = useWorkflowPersistence()\nuseCanvasDrop(canvasRef)\nuseLitegraphSettings()\nuseNodeBadge()\n\nonMounted(async () => {\n useGlobalLitegraph()\n useContextMenuTranslation()\n useCopy()\n usePaste()\n useWorkflowAutoSave()\n useVueFeatureFlags()\n\n comfyApp.vueAppReady = true\n\n workspaceStore.spinner = true\n // ChangeTracker needs to be initialized before setup, as it will overwrite\n // some listeners of litegraph canvas.\n ChangeTracker.init()\n await loadCustomNodesI18n()\n try {\n await settingStore.loadSettingValues()\n } catch (error) {\n if (error instanceof UnauthorizedError) {\n localStorage.removeItem('Comfy.userId')\n localStorage.removeItem('Comfy.userName')\n window.location.reload()\n } else {\n throw error\n }\n }\n CORE_SETTINGS.forEach(settingStore.addSetting)\n\n await newUserService().initializeIfNewUser(settingStore)\n\n // @ts-expect-error fixme ts strict error\n await comfyApp.setup(canvasRef.value)\n canvasStore.canvas = comfyApp.canvas\n canvasStore.canvas.render_canvas_border = false\n workspaceStore.spinner = false\n useSearchBoxStore().setPopoverRef(nodeSearchboxPopoverRef.value)\n\n window.app = comfyApp\n window.graph = comfyApp.graph\n\n comfyAppReady.value = true\n\n vueNodeLifecycle.setupEmptyGraphListener()\n\n comfyApp.canvas.onSelectionChange = useChainCallback(\n comfyApp.canvas.onSelectionChange,\n () => canvasStore.updateSelectedItems()\n )\n\n // Load color palette\n colorPaletteStore.customPalettes = settingStore.get(\n 'Comfy.CustomColorPalettes'\n )\n\n // Restore saved workflow and workflow tabs state\n await workflowPersistence.initializeWorkflow()\n workflowPersistence.restoreWorkflowTabsState()\n\n // Load template from URL if present\n await workflowPersistence.loadTemplateFromUrlIfPresent()\n\n // Initialize release store to fetch releases from comfy-api (fire-and-forget)\n const { useReleaseStore } =\n await import('@/platform/updates/common/releaseStore')\n const releaseStore = useReleaseStore()\n void releaseStore.initialize()\n\n // Start watching for locale change after the initial value is loaded.\n watch(\n () => settingStore.get('Comfy.Locale'),\n async () => {\n await useCommandStore().execute('Comfy.RefreshNodeDefinitions')\n await useWorkflowService().reloadCurrentWorkflow()\n }\n )\n\n whenever(\n () => useCanvasStore().canvas,\n (canvas) => {\n useEventListener(canvas.canvas, 'litegraph:set-graph', () => {\n useWorkflowStore().updateActiveGraph()\n })\n },\n { immediate: true }\n )\n\n emit('ready')\n})\n\nonUnmounted(() => {\n vueNodeLifecycle.cleanup()\n})\n</script>\n","<template>\n <Toast group=\"reroute-migration\">\n <template #message>\n <div class=\"flex flex-auto flex-col items-start\">\n <div class=\"my-4 text-lg font-medium\">\n {{ t('toastMessages.migrateToLitegraphReroute') }}\n </div>\n <Button class=\"self-end\" size=\"sm\" @click=\"migrateToLitegraphReroute\">\n {{ t('g.migrate') }}\n </Button>\n </div>\n </template>\n </Toast>\n</template>\n\n<script setup lang=\"ts\">\nimport { useToast } from 'primevue'\nimport Toast from 'primevue/toast'\nimport { useI18n } from 'vue-i18n'\n\nimport Button from '@/components/ui/button/Button.vue'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport type { WorkflowJSON04 } from '@/platform/workflow/validation/schemas/workflowSchema'\nimport { app } from '@/scripts/app'\nimport { migrateLegacyRerouteNodes } from '@/utils/migration/migrateReroute'\n\nconst { t } = useI18n()\nconst toast = useToast()\n\nconst workflowStore = useWorkflowStore()\nconst migrateToLitegraphReroute = async () => {\n const workflowJSON = app.rootGraph.serialize() as unknown as WorkflowJSON04\n const migratedWorkflowJSON = migrateLegacyRerouteNodes(workflowJSON)\n await app.loadGraphData(\n migratedWorkflowJSON,\n false,\n false,\n workflowStore.activeWorkflow\n )\n toast.removeGroup('reroute-migration')\n}\n</script>\n","import { useTitle } from '@vueuse/core'\nimport { computed } from 'vue'\n\nimport { t } from '@/i18n'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { useExecutionStore } from '@/stores/executionStore'\nimport { useWorkspaceStore } from '@/stores/workspaceStore'\n\nconst DEFAULT_TITLE = 'ComfyUI'\nconst TITLE_SUFFIX = ' - ComfyUI'\n\nexport const useBrowserTabTitle = () => {\n const executionStore = useExecutionStore()\n const settingStore = useSettingStore()\n const workflowStore = useWorkflowStore()\n const workspaceStore = useWorkspaceStore()\n\n const executionText = computed(() =>\n executionStore.isIdle\n ? ''\n : `[${Math.round(executionStore.executionProgress * 100)}%]`\n )\n\n const newMenuEnabled = computed(\n () => settingStore.get('Comfy.UseNewMenu') !== 'Disabled'\n )\n\n const isAutoSaveEnabled = computed(\n () => settingStore.get('Comfy.Workflow.AutoSave') === 'after delay'\n )\n\n const isActiveWorkflowModified = computed(\n () => !!workflowStore.activeWorkflow?.isModified\n )\n const isActiveWorkflowPersisted = computed(\n () => !!workflowStore.activeWorkflow?.isPersisted\n )\n\n const shouldShowUnsavedIndicator = computed(() => {\n if (workspaceStore.shiftDown) return false\n if (isAutoSaveEnabled.value) return false\n if (!isActiveWorkflowPersisted.value) return true\n if (isActiveWorkflowModified.value) return true\n return false\n })\n\n const isUnsavedText = computed(() =>\n shouldShowUnsavedIndicator.value ? ' *' : ''\n )\n const workflowNameText = computed(() => {\n const workflowName = workflowStore.activeWorkflow?.filename\n return workflowName\n ? isUnsavedText.value + workflowName + TITLE_SUFFIX\n : DEFAULT_TITLE\n })\n\n const nodeExecutionTitle = computed(() => {\n // Check if any nodes are in progress\n const nodeProgressEntries = Object.entries(\n executionStore.nodeProgressStates\n )\n const runningNodes = nodeProgressEntries.filter(\n ([_, state]) => state.state === 'running'\n )\n\n if (runningNodes.length === 0) {\n return ''\n }\n\n // If multiple nodes are running\n if (runningNodes.length > 1) {\n return `${executionText.value}[${runningNodes.length} ${t('g.nodesRunning', 'nodes running')}]`\n }\n\n // If only one node is running\n const [nodeId, state] = runningNodes[0]\n const progress = Math.round((state.value / state.max) * 100)\n const nodeType =\n executionStore.activePrompt?.workflow?.changeTracker?.activeState.nodes.find(\n (n) => String(n.id) === nodeId\n )?.type || 'Node'\n\n return `${executionText.value}[${progress}%] ${nodeType}`\n })\n\n const workflowTitle = computed(\n () =>\n executionText.value +\n (newMenuEnabled.value ? workflowNameText.value : DEFAULT_TITLE)\n )\n\n const title = computed(() => nodeExecutionTitle.value || workflowTitle.value)\n useTitle(title)\n}\n","<template>\n <div class=\"size-full bg-modal-panel-background pr-6 pb-8 pl-4\">\n <slot></slot>\n </div>\n</template>\n","<template>\n <BaseModalLayout :content-title=\"$t('assetBrowser.checkpoints')\">\n <template #leftPanel>\n <LeftSidePanel v-model=\"selectedNavItem\" :nav-items=\"tempNavigation\">\n <template #header-icon>\n <i class=\"text-neutral icon-[lucide--puzzle]\" />\n </template>\n <template #header-title>\n <span class=\"text-neutral text-base\">{{ $t('g.title') }}</span>\n </template>\n </LeftSidePanel>\n </template>\n\n <template #header>\n <SearchBox v-model=\"searchQuery\" size=\"lg\" class=\"max-w-[384px]\" />\n </template>\n\n <template #header-right-area>\n <div class=\"flex gap-2\">\n <Button variant=\"primary\" @click=\"() => {}\">\n <i class=\"icon-[lucide--upload]\" />\n <span>{{ $t('g.upload') }}</span>\n </Button>\n <MoreButton>\n <template #default=\"{ close }\">\n <Button\n variant=\"secondary\"\n @click=\"\n () => {\n close()\n }\n \"\n >\n <i class=\"icon-[lucide--download]\" />\n <span>{{ $t('g.settings') }}</span>\n </Button>\n <Button\n variant=\"primary\"\n @click=\"\n () => {\n close()\n }\n \"\n >\n <i class=\"icon-[lucide--scroll]\" />\n <span>{{ $t('g.profile') }}</span>\n </Button>\n </template>\n </MoreButton>\n </div>\n </template>\n\n <template #contentFilter>\n <div class=\"relative flex gap-2 px-6 pb-4\">\n <MultiSelect\n v-model=\"selectedFrameworks\"\n v-model:search-query=\"searchText\"\n class=\"w-[250px]\"\n :label=\"$t('assetBrowser.selectFrameworks')\"\n :options=\"frameworkOptions\"\n :show-search-box=\"true\"\n :show-selected-count=\"true\"\n :show-clear-button=\"true\"\n />\n <MultiSelect\n v-model=\"selectedProjects\"\n :label=\"$t('assetBrowser.selectProjects')\"\n :options=\"projectOptions\"\n />\n <SingleSelect\n v-model=\"selectedSort\"\n :label=\"$t('assetBrowser.sortingType')\"\n :options=\"sortOptions\"\n class=\"w-[135px]\"\n >\n <template #icon>\n <i class=\"icon-[lucide--filter]\" />\n </template>\n </SingleSelect>\n </div>\n </template>\n\n <template #content>\n <!-- Card Examples -->\n <div :style=\"gridStyle\">\n <CardContainer v-for=\"i in 100\" :key=\"i\" size=\"regular\">\n <template #top>\n <CardTop ratio=\"landscape\">\n <template #default>\n <div class=\"h-full w-full bg-blue-500\"></div>\n </template>\n <template #top-right>\n <Button\n size=\"icon\"\n class=\"!bg-white !text-neutral-900\"\n @click=\"() => {}\"\n >\n <i class=\"icon-[lucide--info]\" />\n </Button>\n </template>\n <template #bottom-right>\n <SquareChip label=\"png\" />\n <SquareChip label=\"1.2 MB\" />\n <SquareChip label=\"LoRA\">\n <template #icon>\n <i class=\"icon-[lucide--folder]\" />\n </template>\n </SquareChip>\n </template>\n </CardTop>\n </template>\n <template #bottom>\n <CardBottom></CardBottom>\n </template>\n </CardContainer>\n </div>\n </template>\n\n <template #rightPanel>\n <RightSidePanel></RightSidePanel>\n </template>\n </BaseModalLayout>\n</template>\n\n<script setup lang=\"ts\">\nimport { computed, provide, ref } from 'vue'\n\nimport MoreButton from '@/components/button/MoreButton.vue'\nimport CardBottom from '@/components/card/CardBottom.vue'\nimport CardContainer from '@/components/card/CardContainer.vue'\nimport CardTop from '@/components/card/CardTop.vue'\nimport SquareChip from '@/components/chip/SquareChip.vue'\nimport SearchBox from '@/components/common/SearchBox.vue'\nimport MultiSelect from '@/components/input/MultiSelect.vue'\nimport SingleSelect from '@/components/input/SingleSelect.vue'\nimport Button from '@/components/ui/button/Button.vue'\nimport BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'\nimport LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'\nimport RightSidePanel from '@/components/widget/panel/RightSidePanel.vue'\nimport type { NavGroupData, NavItemData } from '@/types/navTypes'\nimport { OnCloseKey } from '@/types/widgetTypes'\nimport { createGridStyle } from '@/utils/gridUtil'\n\nconst frameworkOptions = ref([\n { name: 'Vue', value: 'vue' },\n { name: 'React', value: 'react' },\n { name: 'Angular', value: 'angular' },\n { name: 'Svelte', value: 'svelte' }\n])\n\nconst projectOptions = ref([\n { name: 'Project A', value: 'proj-a' },\n { name: 'Project B', value: 'proj-b' },\n { name: 'Project C', value: 'proj-c' }\n])\n\nconst sortOptions = ref([\n { name: 'Popular', value: 'popular' },\n { name: 'Latest', value: 'latest' },\n { name: 'A → Z', value: 'az' }\n])\n\nconst tempNavigation = ref<(NavItemData | NavGroupData)[]>([\n { id: 'installed', label: 'Installed', icon: 'icon-[lucide--download]' },\n {\n title: 'TAGS',\n items: [\n { id: 'tag-sd15', label: 'SD 1.5', icon: 'icon-[lucide--tag]' },\n { id: 'tag-sdxl', label: 'SDXL', icon: 'icon-[lucide--tag]' },\n { id: 'tag-utility', label: 'Utility', icon: 'icon-[lucide--tag]' }\n ]\n },\n {\n title: 'CATEGORIES',\n items: [\n { id: 'cat-models', label: 'Models', icon: 'icon-[lucide--layers]' },\n { id: 'cat-nodes', label: 'Nodes', icon: 'icon-[lucide--grid-3x3]' }\n ]\n }\n])\n\nconst { onClose } = defineProps<{\n onClose: () => void\n}>()\n\nprovide(OnCloseKey, onClose)\n\nconst searchQuery = ref<string>('')\nconst searchText = ref<string>('')\nconst selectedFrameworks = ref([])\nconst selectedProjects = ref([])\nconst selectedSort = ref<string>('popular')\n\nconst selectedNavItem = ref<string | null>('installed')\n\nconst gridStyle = computed(() => createGridStyle())\n</script>\n","import SampleModelSelector from '@/components/widget/SampleModelSelector.vue'\nimport { useDialogService } from '@/services/dialogService'\nimport { useDialogStore } from '@/stores/dialogStore'\n\nconst DIALOG_KEY = 'global-model-selector'\n\nexport const useModelSelectorDialog = () => {\n const dialogService = useDialogService()\n const dialogStore = useDialogStore()\n\n function hide() {\n dialogStore.closeDialog({ key: DIALOG_KEY })\n }\n\n function show() {\n dialogService.showLayoutDialog({\n key: DIALOG_KEY,\n component: SampleModelSelector,\n props: {\n onClose: hide\n }\n })\n }\n\n return {\n show,\n hide\n }\n}\n","import { LiteGraph } from '@/lib/litegraph/src/litegraph'\nimport type { LGraphNode, Point } from '@/lib/litegraph/src/litegraph'\nimport { assetItemSchema } from '@/platform/assets/schemas/assetSchema'\nimport type { AssetItem } from '@/platform/assets/schemas/assetSchema'\nimport {\n MISSING_TAG,\n MODELS_TAG\n} from '@/platform/assets/services/assetService'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { app } from '@/scripts/app'\nimport { useLitegraphService } from '@/services/litegraphService'\nimport { useModelToNodeStore } from '@/stores/modelToNodeStore'\n\ninterface CreateNodeOptions {\n position?: Point\n}\n\ntype NodeCreationErrorCode =\n | 'INVALID_ASSET'\n | 'NO_PROVIDER'\n | 'NODE_CREATION_FAILED'\n | 'MISSING_WIDGET'\n | 'NO_GRAPH'\n\ninterface NodeCreationError {\n code: NodeCreationErrorCode\n message: string\n assetId: string\n details?: Record<string, unknown>\n}\n\ntype Result<T, E> = { success: true; value: T } | { success: false; error: E }\n\n/**\n * Creates a LiteGraph node from an asset item.\n *\n * **Boundary Function**: Bridges Vue reactive domain with LiteGraph canvas domain.\n *\n * @param asset - Asset item to create node from (Vue domain)\n * @param options - Optional position and configuration\n * @returns Result with LiteGraph node (Canvas domain) or error details\n *\n * @remarks\n * This function performs side effects on the canvas graph. Validation failures\n * return error results rather than throwing to allow graceful degradation in UI contexts.\n * Widget validation occurs before graph mutation to prevent orphaned nodes.\n */\nexport function createModelNodeFromAsset(\n asset: AssetItem,\n options?: CreateNodeOptions\n): Result<LGraphNode, NodeCreationError> {\n const validatedAsset = assetItemSchema.safeParse(asset)\n\n if (!validatedAsset.success) {\n const errorMessage = validatedAsset.error.errors\n .map((e) => `${e.path.join('.')}: ${e.message}`)\n .join(', ')\n console.error('Invalid asset item:', errorMessage)\n return {\n success: false,\n error: {\n code: 'INVALID_ASSET',\n message: 'Asset schema validation failed',\n assetId: asset.id,\n details: { validationErrors: errorMessage }\n }\n }\n }\n\n const validAsset = validatedAsset.data\n\n const userMetadata = validAsset.user_metadata\n if (!userMetadata) {\n console.error(`Asset ${validAsset.id} missing required user_metadata`)\n return {\n success: false,\n error: {\n code: 'INVALID_ASSET',\n message: 'Asset missing required user_metadata',\n assetId: validAsset.id\n }\n }\n }\n\n const filename = userMetadata.filename\n if (typeof filename !== 'string' || filename.length === 0) {\n console.error(\n `Asset ${validAsset.id} has invalid user_metadata.filename (expected non-empty string, got ${typeof filename})`\n )\n return {\n success: false,\n error: {\n code: 'INVALID_ASSET',\n message: `Invalid filename (expected non-empty string, got ${typeof filename})`,\n assetId: validAsset.id\n }\n }\n }\n\n if (validAsset.tags.length === 0) {\n console.error(\n `Asset ${validAsset.id} has no tags defined (expected at least one category tag)`\n )\n return {\n success: false,\n error: {\n code: 'INVALID_ASSET',\n message: 'Asset has no tags defined',\n assetId: validAsset.id\n }\n }\n }\n\n const category = validAsset.tags.find(\n (tag) => tag !== MODELS_TAG && tag !== MISSING_TAG\n )\n if (!category) {\n console.error(\n `Asset ${validAsset.id} has no valid category tag. Available tags: ${validAsset.tags.join(', ')} (expected tag other than '${MODELS_TAG}' or '${MISSING_TAG}')`\n )\n return {\n success: false,\n error: {\n code: 'INVALID_ASSET',\n message: 'Asset has no valid category tag',\n assetId: validAsset.id,\n details: { availableTags: validAsset.tags }\n }\n }\n }\n\n const modelToNodeStore = useModelToNodeStore()\n const provider = modelToNodeStore.getNodeProvider(category)\n if (!provider) {\n console.error(`No node provider registered for category: ${category}`)\n return {\n success: false,\n error: {\n code: 'NO_PROVIDER',\n message: `No node provider registered for category: ${category}`,\n assetId: validAsset.id,\n details: { category }\n }\n }\n }\n\n const litegraphService = useLitegraphService()\n const pos = options?.position ?? litegraphService.getCanvasCenter()\n\n const node = LiteGraph.createNode(\n provider.nodeDef.name,\n provider.nodeDef.display_name,\n { pos }\n )\n\n if (!node) {\n console.error(`Failed to create node for type: ${provider.nodeDef.name}`)\n return {\n success: false,\n error: {\n code: 'NODE_CREATION_FAILED',\n message: `Failed to create node for type: ${provider.nodeDef.name}`,\n assetId: validAsset.id,\n details: { nodeType: provider.nodeDef.name }\n }\n }\n }\n\n const workflowStore = useWorkflowStore()\n const targetGraph = workflowStore.isSubgraphActive\n ? workflowStore.activeSubgraph\n : app.canvas.graph\n\n if (!targetGraph) {\n console.error('No active graph available')\n return {\n success: false,\n error: {\n code: 'NO_GRAPH',\n message: 'No active graph available',\n assetId: validAsset.id\n }\n }\n }\n\n const widget = node.widgets?.find((w) => w.name === provider.key)\n if (!widget) {\n console.error(\n `Widget ${provider.key} not found on node ${provider.nodeDef.name}`\n )\n return {\n success: false,\n error: {\n code: 'MISSING_WIDGET',\n message: `Widget ${provider.key} not found on node ${provider.nodeDef.name}`,\n assetId: validAsset.id,\n details: { widgetName: provider.key, nodeType: provider.nodeDef.name }\n }\n }\n }\n\n // Set widget value BEFORE adding to graph so the node is created with correct value\n widget.value = filename\n\n // Now add the node to the graph with the correct widget value already set\n targetGraph.add(node)\n\n return { success: true, value: node }\n}\n","import { isCloud } from '@/platform/distribution/types'\n\n/**\n * Zendesk ticket form field IDs.\n */\nconst ZENDESK_FIELDS = {\n /** Distribution tag (cloud vs OSS) */\n DISTRIBUTION: 'tf_42243568391700',\n /** User email (anonymous requester) */\n ANONYMOUS_EMAIL: 'tf_anonymous_requester_email',\n /** User email (authenticated) */\n EMAIL: 'tf_40029135130388',\n /** User ID */\n USER_ID: 'tf_42515251051412'\n} as const\n\nconst SUPPORT_BASE_URL = 'https://support.comfy.org/hc/en-us/requests/new'\n\n/**\n * Builds the support URL with optional user information for pre-filling.\n * Users without login information will still get a valid support URL without pre-fill.\n *\n * @param params - User information to pre-fill in the support form\n * @returns Complete Zendesk support URL with query parameters\n */\nexport function buildSupportUrl(params?: {\n userEmail?: string | null\n userId?: string | null\n}): string {\n const searchParams = new URLSearchParams({\n [ZENDESK_FIELDS.DISTRIBUTION]: isCloud ? 'ccloud' : 'oss'\n })\n\n if (params?.userEmail) {\n searchParams.append(ZENDESK_FIELDS.ANONYMOUS_EMAIL, params.userEmail)\n searchParams.append(ZENDESK_FIELDS.EMAIL, params.userEmail)\n }\n if (params?.userId) {\n searchParams.append(ZENDESK_FIELDS.USER_ID, params.userId)\n }\n\n return `${SUPPORT_BASE_URL}?${searchParams.toString()}`\n}\n","import { useCurrentUser } from '@/composables/auth/useCurrentUser'\nimport { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'\nimport { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'\nimport { useSubgraphOperations } from '@/composables/graph/useSubgraphOperations'\nimport { useExternalLink } from '@/composables/useExternalLink'\nimport { useModelSelectorDialog } from '@/composables/useModelSelectorDialog'\nimport {\n DEFAULT_DARK_COLOR_PALETTE,\n DEFAULT_LIGHT_COLOR_PALETTE\n} from '@/constants/coreColorPalettes'\nimport { tryToggleWidgetPromotion } from '@/core/graph/subgraph/proxyWidgetUtils'\nimport { t } from '@/i18n'\nimport {\n LGraphEventMode,\n LGraphGroup,\n LGraphNode,\n LiteGraph\n} from '@/lib/litegraph/src/litegraph'\nimport type { Point } from '@/lib/litegraph/src/litegraph'\nimport { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'\nimport { createModelNodeFromAsset } from '@/platform/assets/utils/createModelNodeFromAsset'\nimport { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { buildSupportUrl } from '@/platform/support/config'\nimport { useTelemetry } from '@/platform/telemetry'\nimport type { ExecutionTriggerSource } from '@/platform/telemetry/types'\nimport { useToastStore } from '@/platform/updates/common/toastStore'\nimport { useWorkflowService } from '@/platform/workflow/core/services/workflowService'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore'\nimport {\n useCanvasStore,\n useTitleEditorStore\n} from '@/renderer/core/canvas/canvasStore'\nimport { api } from '@/scripts/api'\nimport { app } from '@/scripts/app'\nimport { useDialogService } from '@/services/dialogService'\nimport { useLitegraphService } from '@/services/litegraphService'\nimport type { ComfyCommand } from '@/stores/commandStore'\nimport { useExecutionStore } from '@/stores/executionStore'\nimport { useHelpCenterStore } from '@/stores/helpCenterStore'\nimport {\n useQueueSettingsStore,\n useQueueStore,\n useQueueUIStore\n} from '@/stores/queueStore'\nimport { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'\nimport { useSubgraphStore } from '@/stores/subgraphStore'\nimport { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'\nimport { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'\nimport { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'\nimport { useSearchBoxStore } from '@/stores/workspace/searchBoxStore'\nimport { useWorkspaceStore } from '@/stores/workspaceStore'\nimport {\n getAllNonIoNodesInSubgraph,\n getExecutionIdsForSelectedNodes\n} from '@/utils/graphTraversalUtil'\nimport { filterOutputNodes } from '@/utils/nodeFilterUtil'\nimport {\n ManagerUIState,\n useManagerState\n} from '@/workbench/extensions/manager/composables/useManagerState'\nimport { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'\n\nimport { useWorkflowTemplateSelectorDialog } from './useWorkflowTemplateSelectorDialog'\n\nimport { useMaskEditorStore } from '@/stores/maskEditorStore'\nimport { useDialogStore } from '@/stores/dialogStore'\n\nconst { isActiveSubscription, showSubscriptionDialog } = useSubscription()\n\nconst moveSelectedNodesVersionAdded = '1.22.2'\nexport function useCoreCommands(): ComfyCommand[] {\n const workflowService = useWorkflowService()\n const workflowStore = useWorkflowStore()\n const dialogService = useDialogService()\n const colorPaletteStore = useColorPaletteStore()\n const firebaseAuthActions = useFirebaseAuthActions()\n const toastStore = useToastStore()\n const canvasStore = useCanvasStore()\n const executionStore = useExecutionStore()\n const telemetry = useTelemetry()\n const { staticUrls, buildDocsUrl } = useExternalLink()\n const settingStore = useSettingStore()\n\n const bottomPanelStore = useBottomPanelStore()\n\n const dialogStore = useDialogStore()\n const maskEditorStore = useMaskEditorStore()\n\n const { getSelectedNodes, toggleSelectedNodesMode } =\n useSelectedLiteGraphItems()\n const getTracker = () => workflowStore.activeWorkflow?.changeTracker\n\n function isQueuePanelV2Enabled() {\n return settingStore.get('Comfy.Queue.QPOV2')\n }\n\n async function toggleQueuePanelV2() {\n await settingStore.set('Comfy.Queue.QPOV2', !isQueuePanelV2Enabled())\n }\n\n const moveSelectedNodes = (\n positionUpdater: (pos: Point, gridSize: number) => Point\n ) => {\n const selectedNodes = getSelectedNodes()\n if (selectedNodes.length === 0) return\n\n const gridSize = useSettingStore().get('Comfy.SnapToGrid.GridSize')\n selectedNodes.forEach((node) => {\n node.pos = positionUpdater(node.pos, gridSize)\n })\n app.canvas.state.selectionChanged = true\n app.canvas.setDirty(true, true)\n }\n\n const commands = [\n {\n id: 'Comfy.NewBlankWorkflow',\n icon: 'pi pi-plus',\n label: 'New Blank Workflow',\n menubarLabel: 'New',\n category: 'essentials' as const,\n function: async () => {\n const previousWorkflowHadNodes = app.rootGraph._nodes.length > 0\n await workflowService.loadBlankWorkflow()\n telemetry?.trackWorkflowCreated({\n workflow_type: 'blank',\n previous_workflow_had_nodes: previousWorkflowHadNodes\n })\n }\n },\n {\n id: 'Comfy.OpenWorkflow',\n icon: 'pi pi-folder-open',\n label: 'Open Workflow',\n menubarLabel: 'Open',\n category: 'essentials' as const,\n function: () => {\n app.ui.loadFile()\n }\n },\n {\n id: 'Comfy.LoadDefaultWorkflow',\n icon: 'pi pi-code',\n label: 'Load Default Workflow',\n function: async () => {\n const previousWorkflowHadNodes = app.rootGraph._nodes.length > 0\n await workflowService.loadDefaultWorkflow()\n telemetry?.trackWorkflowCreated({\n workflow_type: 'default',\n previous_workflow_had_nodes: previousWorkflowHadNodes\n })\n }\n },\n {\n id: 'Comfy.SaveWorkflow',\n icon: 'pi pi-save',\n label: 'Save Workflow',\n menubarLabel: 'Save',\n category: 'essentials' as const,\n function: async () => {\n const workflow = useWorkflowStore().activeWorkflow as ComfyWorkflow\n if (!workflow) return\n\n await workflowService.saveWorkflow(workflow)\n }\n },\n {\n id: 'Comfy.PublishSubgraph',\n icon: 'pi pi-save',\n label: 'Publish Subgraph',\n menubarLabel: 'Publish',\n function: async () => {\n await useSubgraphStore().publishSubgraph()\n }\n },\n {\n id: 'Comfy.SaveWorkflowAs',\n icon: 'pi pi-save',\n label: 'Save Workflow As',\n menubarLabel: 'Save As',\n category: 'essentials' as const,\n function: async () => {\n const workflow = useWorkflowStore().activeWorkflow as ComfyWorkflow\n if (!workflow) return\n\n await workflowService.saveWorkflowAs(workflow)\n }\n },\n {\n id: 'Comfy.RenameWorkflow',\n icon: 'pi pi-pencil',\n label: 'Rename Workflow',\n menubarLabel: 'Rename',\n function: async () => {\n const workflow = workflowStore.activeWorkflow\n if (!workflow || !workflow.isPersisted) return\n\n const newName = await dialogService.prompt({\n title: t('g.rename'),\n message: t('workflowService.enterFilename') + ':',\n defaultValue: workflow.filename\n })\n if (!newName || newName === workflow.filename) return\n\n const newPath = workflow.directory + '/' + newName + '.json'\n await workflowService.renameWorkflow(workflow, newPath)\n }\n },\n {\n id: 'Comfy.ExportWorkflow',\n icon: 'pi pi-download',\n label: 'Export Workflow',\n menubarLabel: 'Export',\n category: 'essentials' as const,\n function: async () => {\n await workflowService.exportWorkflow('workflow', 'workflow')\n }\n },\n {\n id: 'Comfy.ExportWorkflowAPI',\n icon: 'pi pi-download',\n label: 'Export Workflow (API Format)',\n menubarLabel: 'Export (API)',\n function: async () => {\n await workflowService.exportWorkflow('workflow_api', 'output')\n }\n },\n {\n id: 'Comfy.Undo',\n icon: 'pi pi-undo',\n label: 'Undo',\n category: 'essentials' as const,\n function: async () => {\n // If Mask Editor is open, use its history instead of the graph\n if (dialogStore.isDialogOpen('global-mask-editor')) {\n maskEditorStore.canvasHistory.undo()\n } else {\n await getTracker()?.undo?.()\n }\n }\n },\n {\n id: 'Comfy.Redo',\n icon: 'pi pi-refresh',\n label: 'Redo',\n category: 'essentials' as const,\n function: async () => {\n if (dialogStore.isDialogOpen('global-mask-editor')) {\n maskEditorStore.canvasHistory.redo()\n } else {\n await getTracker()?.redo?.()\n }\n }\n },\n {\n id: 'Comfy.ClearWorkflow',\n icon: 'pi pi-trash',\n label: 'Clear Workflow',\n category: 'essentials' as const,\n function: () => {\n const settingStore = useSettingStore()\n if (\n !settingStore.get('Comfy.ConfirmClear') ||\n confirm('Clear workflow?')\n ) {\n app.clean()\n if (app.canvas.subgraph) {\n // `clear` is not implemented on subgraphs and the parent class's\n // (`LGraph`) `clear` breaks the subgraph structure. For subgraphs,\n // just clear the nodes but preserve input/output nodes and structure\n const subgraph = app.canvas.subgraph\n const nonIoNodes = getAllNonIoNodesInSubgraph(subgraph)\n nonIoNodes.forEach((node) => subgraph.remove(node))\n }\n api.dispatchCustomEvent('graphCleared')\n }\n }\n },\n {\n id: 'Comfy.Canvas.ResetView',\n icon: 'pi pi-expand',\n label: 'Reset View',\n function: () => {\n useLitegraphService().resetView()\n }\n },\n {\n id: 'Comfy.OpenClipspace',\n icon: 'pi pi-clipboard',\n label: 'Clipspace',\n function: () => {\n app.openClipspace()\n }\n },\n {\n id: 'Comfy.RefreshNodeDefinitions',\n icon: 'pi pi-refresh',\n label: 'Refresh Node Definitions',\n category: 'essentials' as const,\n function: async () => {\n await app.refreshComboInNodes()\n }\n },\n {\n id: 'Comfy.Interrupt',\n icon: 'pi pi-stop',\n label: 'Interrupt',\n category: 'essentials' as const,\n function: async () => {\n await api.interrupt(executionStore.activePromptId)\n toastStore.add({\n severity: 'info',\n summary: t('g.interrupted'),\n detail: t('toastMessages.interrupted'),\n life: 1000\n })\n }\n },\n {\n id: 'Comfy.ClearPendingTasks',\n icon: 'pi pi-stop',\n label: 'Clear Pending Tasks',\n category: 'essentials' as const,\n function: async () => {\n await useQueueStore().clear(['queue'])\n toastStore.add({\n severity: 'info',\n summary: t('g.confirmed'),\n detail: t('toastMessages.pendingTasksDeleted'),\n life: 3000\n })\n }\n },\n {\n id: 'Comfy.BrowseTemplates',\n icon: 'pi pi-folder-open',\n label: 'Browse Templates',\n function: () => {\n useWorkflowTemplateSelectorDialog().show()\n }\n },\n {\n id: 'Comfy.Canvas.ZoomIn',\n icon: 'pi pi-plus',\n label: 'Zoom In',\n category: 'view-controls' as const,\n function: () => {\n const ds = app.canvas.ds\n ds.changeScale(\n ds.scale * 1.1,\n ds.element ? [ds.element.width / 2, ds.element.height / 2] : undefined\n )\n app.canvas.setDirty(true, true)\n }\n },\n {\n id: 'Comfy.Canvas.ZoomOut',\n icon: 'pi pi-minus',\n label: 'Zoom Out',\n category: 'view-controls' as const,\n function: () => {\n const ds = app.canvas.ds\n ds.changeScale(\n ds.scale / 1.1,\n ds.element ? [ds.element.width / 2, ds.element.height / 2] : undefined\n )\n app.canvas.setDirty(true, true)\n }\n },\n {\n id: 'Experimental.ToggleVueNodes',\n label: () =>\n `Experimental: ${\n useSettingStore().get('Comfy.VueNodes.Enabled') ? 'Disable' : 'Enable'\n } Nodes 2.0`,\n function: async () => {\n const settingStore = useSettingStore()\n const current = settingStore.get('Comfy.VueNodes.Enabled') ?? false\n await settingStore.set('Comfy.VueNodes.Enabled', !current)\n }\n },\n {\n id: 'Comfy.Canvas.FitView',\n icon: 'pi pi-expand',\n label: 'Fit view to selected nodes',\n menubarLabel: 'Zoom to fit',\n category: 'view-controls' as const,\n function: () => {\n if (app.canvas.empty) {\n toastStore.add({\n severity: 'error',\n summary: t('toastMessages.emptyCanvas'),\n life: 3000\n })\n return\n }\n app.canvas.fitViewToSelectionAnimated()\n }\n },\n {\n id: 'Comfy.Canvas.ToggleLock',\n icon: 'pi pi-lock',\n label: 'Canvas Toggle Lock',\n category: 'view-controls' as const,\n function: () => {\n app.canvas.state.readOnly = !app.canvas.state.readOnly\n }\n },\n {\n id: 'Comfy.Canvas.Lock',\n icon: 'pi pi-lock',\n label: 'Lock Canvas',\n category: 'view-controls' as const,\n function: () => {\n app.canvas.state.readOnly = true\n }\n },\n {\n id: 'Comfy.Canvas.Unlock',\n icon: 'pi pi-lock-open',\n label: 'Unlock Canvas',\n function: () => {\n app.canvas.state.readOnly = false\n }\n },\n {\n id: 'Comfy.Canvas.ToggleLinkVisibility',\n icon: 'pi pi-eye',\n label: 'Canvas Toggle Link Visibility',\n menubarLabel: 'Node Links',\n versionAdded: '1.3.6',\n\n function: (() => {\n const settingStore = useSettingStore()\n let lastLinksRenderMode = LiteGraph.SPLINE_LINK\n\n return async () => {\n const currentMode = settingStore.get('Comfy.LinkRenderMode')\n\n if (currentMode === LiteGraph.HIDDEN_LINK) {\n // If links are hidden, restore the last positive value or default to spline mode\n await settingStore.set('Comfy.LinkRenderMode', lastLinksRenderMode)\n } else {\n // If links are visible, store the current mode and hide links\n lastLinksRenderMode = currentMode\n await settingStore.set(\n 'Comfy.LinkRenderMode',\n LiteGraph.HIDDEN_LINK\n )\n }\n }\n })(),\n active: () =>\n useSettingStore().get('Comfy.LinkRenderMode') !== LiteGraph.HIDDEN_LINK\n },\n {\n id: 'Comfy.Canvas.ToggleMinimap',\n icon: 'pi pi-map',\n label: 'Canvas Toggle Minimap',\n menubarLabel: 'Minimap',\n versionAdded: '1.24.1',\n function: async () => {\n const settingStore = useSettingStore()\n await settingStore.set(\n 'Comfy.Minimap.Visible',\n !settingStore.get('Comfy.Minimap.Visible')\n )\n },\n active: () => useSettingStore().get('Comfy.Minimap.Visible')\n },\n {\n id: 'Comfy.Queue.ToggleOverlay',\n icon: 'pi pi-history',\n label: () => t('queue.toggleJobHistory'),\n menubarLabel: () => t('queue.jobHistory'),\n versionAdded: '1.37.0',\n category: 'view-controls' as const,\n function: () => {\n useQueueUIStore().toggleOverlay()\n },\n active: () => useQueueUIStore().isOverlayExpanded\n },\n {\n id: 'Comfy.QueuePrompt',\n icon: 'pi pi-play',\n label: 'Queue Prompt',\n versionAdded: '1.3.7',\n category: 'essentials' as const,\n function: async (metadata?: {\n subscribe_to_run?: boolean\n trigger_source?: ExecutionTriggerSource\n }) => {\n useTelemetry()?.trackRunButton(metadata)\n if (!isActiveSubscription.value) {\n showSubscriptionDialog()\n return\n }\n\n const batchCount = useQueueSettingsStore().batchCount\n\n useTelemetry()?.trackWorkflowExecution()\n\n await app.queuePrompt(0, batchCount)\n }\n },\n {\n id: 'Comfy.QueuePromptFront',\n icon: 'pi pi-play',\n label: 'Queue Prompt (Front)',\n versionAdded: '1.3.7',\n category: 'essentials' as const,\n function: async (metadata?: {\n subscribe_to_run?: boolean\n trigger_source?: ExecutionTriggerSource\n }) => {\n useTelemetry()?.trackRunButton(metadata)\n if (!isActiveSubscription.value) {\n showSubscriptionDialog()\n return\n }\n\n const batchCount = useQueueSettingsStore().batchCount\n\n useTelemetry()?.trackWorkflowExecution()\n\n await app.queuePrompt(-1, batchCount)\n }\n },\n {\n id: 'Comfy.QueueSelectedOutputNodes',\n icon: 'pi pi-play',\n label: 'Queue Selected Output Nodes',\n versionAdded: '1.19.6',\n function: async (metadata?: {\n subscribe_to_run?: boolean\n trigger_source?: ExecutionTriggerSource\n }) => {\n useTelemetry()?.trackRunButton(metadata)\n if (!isActiveSubscription.value) {\n showSubscriptionDialog()\n return\n }\n\n const batchCount = useQueueSettingsStore().batchCount\n const selectedNodes = getSelectedNodes()\n const selectedOutputNodes = filterOutputNodes(selectedNodes)\n\n if (selectedOutputNodes.length === 0) {\n toastStore.add({\n severity: 'error',\n summary: t('toastMessages.nothingToQueue'),\n detail: t('toastMessages.pleaseSelectOutputNodes'),\n life: 3000\n })\n return\n }\n\n // Get execution IDs for all selected output nodes and their descendants\n const executionIds =\n getExecutionIdsForSelectedNodes(selectedOutputNodes)\n\n if (executionIds.length === 0) {\n toastStore.add({\n severity: 'error',\n summary: t('toastMessages.failedToQueue'),\n detail: t('toastMessages.failedExecutionPathResolution'),\n life: 3000\n })\n return\n }\n useTelemetry()?.trackWorkflowExecution()\n await app.queuePrompt(0, batchCount, executionIds)\n }\n },\n {\n id: 'Comfy.ShowSettingsDialog',\n icon: 'pi pi-cog',\n label: 'Show Settings Dialog',\n versionAdded: '1.3.7',\n category: 'view-controls' as const,\n function: () => {\n dialogService.showSettingsDialog()\n }\n },\n {\n id: 'Comfy.Graph.GroupSelectedNodes',\n icon: 'pi pi-sitemap',\n label: 'Group Selected Nodes',\n versionAdded: '1.3.7',\n category: 'essentials' as const,\n function: () => {\n const { canvas } = app\n if (!canvas.selectedItems?.size) {\n toastStore.add({\n severity: 'error',\n summary: t('toastMessages.nothingToGroup'),\n detail: t('toastMessages.pleaseSelectNodesToGroup'),\n life: 3000\n })\n return\n }\n const group = new LGraphGroup()\n const padding = useSettingStore().get(\n 'Comfy.GroupSelectedNodes.Padding'\n )\n group.resizeTo(canvas.selectedItems, padding)\n canvas.graph?.add(group)\n\n group.recomputeInsideNodes()\n\n useTitleEditorStore().titleEditorTarget = group\n }\n },\n {\n id: 'Workspace.NextOpenedWorkflow',\n icon: 'pi pi-step-forward',\n label: 'Next Opened Workflow',\n versionAdded: '1.3.9',\n function: async () => {\n await workflowService.loadNextOpenedWorkflow()\n }\n },\n {\n id: 'Workspace.PreviousOpenedWorkflow',\n icon: 'pi pi-step-backward',\n label: 'Previous Opened Workflow',\n versionAdded: '1.3.9',\n function: async () => {\n await workflowService.loadPreviousOpenedWorkflow()\n }\n },\n {\n id: 'Comfy.Canvas.ToggleSelectedNodes.Mute',\n icon: 'pi pi-volume-off',\n label: 'Mute/Unmute Selected Nodes',\n versionAdded: '1.3.11',\n category: 'essentials' as const,\n function: () => {\n toggleSelectedNodesMode(LGraphEventMode.NEVER)\n app.canvas.setDirty(true, true)\n }\n },\n {\n id: 'Comfy.Canvas.ToggleSelectedNodes.Bypass',\n icon: 'pi pi-shield',\n label: 'Bypass/Unbypass Selected Nodes',\n versionAdded: '1.3.11',\n category: 'essentials' as const,\n function: () => {\n toggleSelectedNodesMode(LGraphEventMode.BYPASS)\n app.canvas.setDirty(true, true)\n }\n },\n {\n id: 'Comfy.Canvas.ToggleSelectedNodes.Pin',\n icon: 'pi pi-pin',\n label: 'Pin/Unpin Selected Nodes',\n versionAdded: '1.3.11',\n category: 'essentials' as const,\n function: () => {\n getSelectedNodes().forEach((node) => {\n node.pin(!node.pinned)\n })\n app.canvas.setDirty(true, true)\n }\n },\n {\n id: 'Comfy.Canvas.ToggleSelected.Pin',\n icon: 'pi pi-pin',\n label: 'Pin/Unpin Selected Items',\n versionAdded: '1.3.33',\n function: () => {\n for (const item of app.canvas.selectedItems) {\n if (item instanceof LGraphNode || item instanceof LGraphGroup) {\n item.pin(!item.pinned)\n }\n }\n app.canvas.setDirty(true, true)\n }\n },\n {\n id: 'Comfy.Canvas.Resize',\n icon: 'pi pi-minus',\n label: 'Resize Selected Nodes',\n versionAdded: '',\n function: () => {\n getSelectedNodes().forEach((node) => {\n const optimalSize = node.computeSize()\n node.setSize([optimalSize[0], optimalSize[1]])\n })\n app.canvas.setDirty(true, true)\n }\n },\n {\n id: 'Comfy.Canvas.ToggleSelectedNodes.Collapse',\n icon: 'pi pi-minus',\n label: 'Collapse/Expand Selected Nodes',\n versionAdded: '1.3.11',\n function: () => {\n getSelectedNodes().forEach((node) => {\n node.collapse()\n })\n app.canvas.setDirty(true, true)\n }\n },\n {\n id: 'Comfy.ToggleTheme',\n icon: 'pi pi-moon',\n label: 'Toggle Theme (Dark/Light)',\n versionAdded: '1.3.12',\n function: (() => {\n let previousDarkTheme: string = DEFAULT_DARK_COLOR_PALETTE.id\n let previousLightTheme: string = DEFAULT_LIGHT_COLOR_PALETTE.id\n\n return async () => {\n const settingStore = useSettingStore()\n const theme = colorPaletteStore.completedActivePalette\n if (theme.light_theme) {\n previousLightTheme = theme.id\n await settingStore.set('Comfy.ColorPalette', previousDarkTheme)\n } else {\n previousDarkTheme = theme.id\n await settingStore.set('Comfy.ColorPalette', previousLightTheme)\n }\n }\n })()\n },\n {\n id: 'Workspace.ToggleBottomPanel',\n icon: 'pi pi-list',\n label: 'Toggle Bottom Panel',\n menubarLabel: 'Bottom Panel',\n versionAdded: '1.3.22',\n category: 'view-controls' as const,\n function: () => {\n bottomPanelStore.toggleBottomPanel()\n },\n active: () => bottomPanelStore.bottomPanelVisible\n },\n {\n id: 'Workspace.ToggleFocusMode',\n icon: 'pi pi-eye',\n label: 'Toggle Focus Mode',\n menubarLabel: 'Focus Mode',\n versionAdded: '1.3.27',\n category: 'view-controls' as const,\n function: () => {\n useWorkspaceStore().toggleFocusMode()\n },\n active: () => useWorkspaceStore().focusMode\n },\n {\n id: 'Comfy.Graph.FitGroupToContents',\n icon: 'pi pi-expand',\n label: 'Fit Group To Contents',\n versionAdded: '1.4.9',\n function: () => {\n for (const group of app.canvas.selectedItems) {\n if (group instanceof LGraphGroup) {\n group.recomputeInsideNodes()\n const padding = useSettingStore().get(\n 'Comfy.GroupSelectedNodes.Padding'\n )\n group.resizeTo(group.children, padding)\n app.canvas.setDirty(false, true)\n }\n }\n }\n },\n {\n id: 'Comfy.Help.OpenComfyUIIssues',\n icon: 'pi pi-github',\n label: 'Open ComfyUI Issues',\n menubarLabel: 'ComfyUI Issues',\n versionAdded: '1.5.5',\n function: () => {\n telemetry?.trackHelpResourceClicked({\n resource_type: 'github',\n is_external: true,\n source: 'menu'\n })\n window.open(staticUrls.githubIssues, '_blank')\n }\n },\n {\n id: 'Comfy.Help.OpenComfyUIDocs',\n icon: 'pi pi-info-circle',\n label: 'Open ComfyUI Docs',\n menubarLabel: 'ComfyUI Docs',\n versionAdded: '1.5.5',\n function: () => {\n telemetry?.trackHelpResourceClicked({\n resource_type: 'docs',\n is_external: true,\n source: 'menu'\n })\n window.open(buildDocsUrl('/', { includeLocale: true }), '_blank')\n }\n },\n {\n id: 'Comfy.Help.OpenComfyOrgDiscord',\n icon: 'pi pi-discord',\n label: 'Open Comfy-Org Discord',\n menubarLabel: 'Comfy-Org Discord',\n versionAdded: '1.5.5',\n function: () => {\n telemetry?.trackHelpResourceClicked({\n resource_type: 'discord',\n is_external: true,\n source: 'menu'\n })\n window.open(staticUrls.discord, '_blank')\n }\n },\n {\n id: 'Workspace.SearchBox.Toggle',\n icon: 'pi pi-search',\n label: 'Toggle Search Box',\n versionAdded: '1.5.7',\n function: () => {\n useSearchBoxStore().toggleVisible()\n }\n },\n {\n id: 'Comfy.Help.AboutComfyUI',\n icon: 'pi pi-info-circle',\n label: 'Open About ComfyUI',\n menubarLabel: 'About ComfyUI',\n versionAdded: '1.6.4',\n function: () => {\n dialogService.showSettingsDialog('about')\n }\n },\n {\n id: 'Comfy.DuplicateWorkflow',\n icon: 'pi pi-clone',\n label: 'Duplicate Current Workflow',\n versionAdded: '1.6.15',\n function: async () => {\n await workflowService.duplicateWorkflow(workflowStore.activeWorkflow!)\n }\n },\n {\n id: 'Workspace.CloseWorkflow',\n icon: 'pi pi-times',\n label: 'Close Current Workflow',\n versionAdded: '1.7.3',\n function: async () => {\n if (workflowStore.activeWorkflow)\n await workflowService.closeWorkflow(workflowStore.activeWorkflow)\n }\n },\n {\n id: 'Comfy.ContactSupport',\n icon: 'pi pi-question',\n label: 'Contact Support',\n versionAdded: '1.17.8',\n function: () => {\n const { userEmail, resolvedUserInfo } = useCurrentUser()\n const supportUrl = buildSupportUrl({\n userEmail: userEmail.value,\n userId: resolvedUserInfo.value?.id\n })\n window.open(supportUrl, '_blank')\n }\n },\n {\n id: 'Comfy.Help.OpenComfyUIForum',\n icon: 'pi pi-comments',\n label: 'Open ComfyUI Forum',\n menubarLabel: 'ComfyUI Forum',\n versionAdded: '1.8.2',\n function: () => {\n telemetry?.trackHelpResourceClicked({\n resource_type: 'help_feedback',\n is_external: true,\n source: 'menu'\n })\n window.open(staticUrls.forum, '_blank')\n }\n },\n {\n id: 'Comfy.Canvas.DeleteSelectedItems',\n icon: 'pi pi-trash',\n label: 'Delete Selected Items',\n versionAdded: '1.10.5',\n function: () => {\n app.canvas.deleteSelected()\n app.canvas.setDirty(true, true)\n }\n },\n {\n id: 'Comfy.Manager.CustomNodesManager.ShowCustomNodesMenu',\n icon: 'pi pi-puzzle',\n label: 'Custom Nodes Manager',\n versionAdded: '1.12.10',\n function: async () => {\n await useManagerState().openManager({\n showToastOnLegacyError: true\n })\n }\n },\n {\n id: 'Comfy.Manager.ShowUpdateAvailablePacks',\n icon: 'pi pi-sync',\n label: 'Check for Custom Node Updates',\n versionAdded: '1.17.0',\n function: async () => {\n const managerState = useManagerState()\n const state = managerState.managerUIState.value\n\n // For DISABLED state, show error toast instead of opening settings\n if (state === ManagerUIState.DISABLED) {\n toastStore.add({\n severity: 'error',\n summary: t('g.error'),\n detail: t('manager.notAvailable'),\n life: 3000\n })\n return\n }\n\n await managerState.openManager({\n initialTab: ManagerTab.UpdateAvailable,\n showToastOnLegacyError: false\n })\n }\n },\n {\n id: 'Comfy.Manager.ShowMissingPacks',\n icon: 'pi pi-exclamation-circle',\n label: 'Install Missing Custom Nodes',\n versionAdded: '1.17.0',\n function: async () => {\n await useManagerState().openManager({\n initialTab: ManagerTab.Missing,\n showToastOnLegacyError: false\n })\n }\n },\n {\n id: 'Comfy.User.OpenSignInDialog',\n icon: 'pi pi-user',\n label: 'Open Sign In Dialog',\n versionAdded: '1.17.6',\n function: async () => {\n await dialogService.showSignInDialog()\n }\n },\n {\n id: 'Comfy.User.SignOut',\n icon: 'pi pi-sign-out',\n label: 'Sign Out',\n versionAdded: '1.18.1',\n function: async () => {\n await firebaseAuthActions.logout()\n }\n },\n {\n id: 'Comfy.Canvas.MoveSelectedNodes.Up',\n icon: 'pi pi-arrow-up',\n label: 'Move Selected Nodes Up',\n versionAdded: moveSelectedNodesVersionAdded,\n function: () => moveSelectedNodes(([x, y], gridSize) => [x, y - gridSize])\n },\n {\n id: 'Comfy.Canvas.MoveSelectedNodes.Down',\n icon: 'pi pi-arrow-down',\n label: 'Move Selected Nodes Down',\n versionAdded: moveSelectedNodesVersionAdded,\n function: () => moveSelectedNodes(([x, y], gridSize) => [x, y + gridSize])\n },\n {\n id: 'Comfy.Canvas.MoveSelectedNodes.Left',\n icon: 'pi pi-arrow-left',\n label: 'Move Selected Nodes Left',\n versionAdded: moveSelectedNodesVersionAdded,\n function: () => moveSelectedNodes(([x, y], gridSize) => [x - gridSize, y])\n },\n {\n id: 'Comfy.Canvas.MoveSelectedNodes.Right',\n icon: 'pi pi-arrow-right',\n label: 'Move Selected Nodes Right',\n versionAdded: moveSelectedNodesVersionAdded,\n function: () => moveSelectedNodes(([x, y], gridSize) => [x + gridSize, y])\n },\n {\n id: 'Comfy.Graph.ConvertToSubgraph',\n icon: 'icon-[lucide--shrink]',\n label: 'Convert Selection to Subgraph',\n versionAdded: '1.20.1',\n category: 'essentials' as const,\n function: () => {\n const canvas = canvasStore.getCanvas()\n const graph = canvas.subgraph ?? canvas.graph\n if (!graph) throw new TypeError('Canvas has no graph or subgraph set.')\n\n const res = graph.convertToSubgraph(canvas.selectedItems)\n if (!res) {\n toastStore.add({\n severity: 'error',\n summary: t('toastMessages.cannotCreateSubgraph'),\n detail: t('toastMessages.failedToConvertToSubgraph'),\n life: 3000\n })\n return\n }\n\n const { node } = res\n canvas.select(node)\n canvasStore.updateSelectedItems()\n }\n },\n {\n id: 'Comfy.Graph.UnpackSubgraph',\n icon: 'icon-[lucide--expand]',\n label: 'Unpack the selected Subgraph',\n versionAdded: '1.26.3',\n function: () => {\n const { unpackSubgraph } = useSubgraphOperations()\n unpackSubgraph()\n }\n },\n {\n id: 'Comfy.Graph.EditSubgraphWidgets',\n label: 'Edit Subgraph Widgets',\n icon: 'icon-[lucide--settings-2]',\n versionAdded: '1.28.5',\n function: () => {\n useRightSidePanelStore().openPanel('subgraph')\n }\n },\n {\n id: 'Comfy.Graph.ToggleWidgetPromotion',\n icon: 'icon-[lucide--arrow-left-right]',\n label: 'Toggle promotion of hovered widget',\n versionAdded: '1.30.1',\n function: tryToggleWidgetPromotion\n },\n {\n id: 'Comfy.OpenManagerDialog',\n icon: 'mdi mdi-puzzle-outline',\n label: 'Manager',\n function: async () => {\n await useManagerState().openManager({\n initialTab: ManagerTab.All,\n showToastOnLegacyError: false\n })\n }\n },\n {\n id: 'Comfy.ToggleHelpCenter',\n icon: 'pi pi-question-circle',\n label: 'Help Center',\n function: () => {\n useHelpCenterStore().toggle()\n },\n active: () => useHelpCenterStore().isVisible\n },\n {\n id: 'Comfy.ToggleCanvasInfo',\n icon: 'pi pi-info-circle',\n label: 'Canvas Performance',\n function: async () => {\n const settingStore = useSettingStore()\n const currentValue = settingStore.get('Comfy.Graph.CanvasInfo')\n await settingStore.set('Comfy.Graph.CanvasInfo', !currentValue)\n },\n active: () => useSettingStore().get('Comfy.Graph.CanvasInfo')\n },\n {\n id: 'Workspace.ToggleBottomPanel.Shortcuts',\n icon: 'pi pi-key',\n label: 'Show Keybindings Dialog',\n versionAdded: '1.24.1',\n category: 'view-controls' as const,\n function: () => {\n bottomPanelStore.togglePanel('shortcuts')\n }\n },\n {\n id: 'Comfy.Graph.ExitSubgraph',\n icon: 'pi pi-arrow-up',\n label: 'Exit Subgraph',\n versionAdded: '1.20.1',\n function: () => {\n const canvas = useCanvasStore().getCanvas()\n const navigationStore = useSubgraphNavigationStore()\n if (!canvas.graph) return\n\n canvas.setGraph(\n navigationStore.navigationStack.at(-2) ?? canvas.graph.rootGraph\n )\n }\n },\n {\n id: 'Comfy.Dev.ShowModelSelector',\n icon: 'pi pi-box',\n label: 'Show Model Selector (Dev)',\n versionAdded: '1.26.2',\n category: 'view-controls' as const,\n function: () => {\n const modelSelectorDialog = useModelSelectorDialog()\n modelSelectorDialog.show()\n }\n },\n {\n id: 'Comfy.Manager.CustomNodesManager.ShowLegacyCustomNodesMenu',\n icon: 'pi pi-bars',\n label: 'Custom Nodes (Legacy)',\n versionAdded: '1.16.4',\n function: async () => {\n await useManagerState().openManager({\n legacyCommand: 'Comfy.Manager.CustomNodesManager.ToggleVisibility',\n showToastOnLegacyError: true,\n isLegacyOnly: true\n })\n }\n },\n {\n id: 'Comfy.Manager.ShowLegacyManagerMenu',\n icon: 'mdi mdi-puzzle',\n label: 'Manager Menu (Legacy)',\n versionAdded: '1.16.4',\n function: async () => {\n await useManagerState().openManager({\n showToastOnLegacyError: true,\n isLegacyOnly: true\n })\n }\n },\n {\n id: 'Comfy.Memory.UnloadModels',\n icon: 'mdi mdi-vacuum-outline',\n label: 'Unload Models',\n versionAdded: '1.16.4',\n function: async () => {\n if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {\n useToastStore().add({\n severity: 'error',\n summary: t('g.error'),\n detail: t('g.commandProhibited', {\n command: 'Comfy.Memory.UnloadModels'\n }),\n life: 3000\n })\n return\n }\n await api.freeMemory({ freeExecutionCache: false })\n }\n },\n {\n id: 'Comfy.Memory.UnloadModelsAndExecutionCache',\n icon: 'mdi mdi-vacuum-outline',\n label: 'Unload Models and Execution Cache',\n versionAdded: '1.16.4',\n function: async () => {\n if (!useSettingStore().get('Comfy.Memory.AllowManualUnload')) {\n useToastStore().add({\n severity: 'error',\n summary: t('g.error'),\n detail: t('g.commandProhibited', {\n command: 'Comfy.Memory.UnloadModelsAndExecutionCache'\n }),\n life: 3000\n })\n return\n }\n await api.freeMemory({ freeExecutionCache: true })\n }\n },\n {\n id: 'Comfy.BrowseModelAssets',\n icon: 'pi pi-folder-open',\n label: 'Experimental: Browse Model Assets',\n versionAdded: '1.28.3',\n function: async () => {\n if (!useSettingStore().get('Comfy.Assets.UseAssetAPI')) {\n const confirmed = await dialogService.confirm({\n title: 'Enable Asset API',\n message:\n 'The Asset API is currently disabled. Would you like to enable it?',\n type: 'default'\n })\n\n if (!confirmed) return\n\n const settingStore = useSettingStore()\n await settingStore.set('Comfy.Assets.UseAssetAPI', true)\n await workflowService.reloadCurrentWorkflow()\n }\n const assetBrowserDialog = useAssetBrowserDialog()\n await assetBrowserDialog.browse({\n assetType: 'models',\n title: t('sideToolbar.modelLibrary'),\n onAssetSelected: (asset) => {\n const result = createModelNodeFromAsset(asset)\n if (!result.success) {\n toastStore.add({\n severity: 'error',\n summary: t('g.error'),\n detail: t('assetBrowser.failedToCreateNode')\n })\n console.error('Node creation failed:', result.error)\n }\n }\n })\n }\n },\n {\n id: 'Comfy.ToggleAssetAPI',\n icon: 'pi pi-database',\n label: () =>\n `Experimental: ${\n useSettingStore().get('Comfy.Assets.UseAssetAPI')\n ? 'Disable'\n : 'Enable'\n } AssetAPI`,\n function: async () => {\n const settingStore = useSettingStore()\n const current = settingStore.get('Comfy.Assets.UseAssetAPI') ?? false\n await settingStore.set('Comfy.Assets.UseAssetAPI', !current)\n await useWorkflowService().reloadCurrentWorkflow() // ensure changes take effect immediately\n }\n },\n {\n id: 'Comfy.ToggleQPOV2',\n icon: 'pi pi-list',\n label: 'Toggle Queue Panel V2',\n function: toggleQueuePanelV2\n },\n {\n id: 'Comfy.ToggleLinear',\n icon: 'pi pi-database',\n label: 'Toggle Simple Mode',\n function: () => {\n const newMode = !canvasStore.linearMode\n app.rootGraph.extra.linearMode = newMode\n workflowStore.activeWorkflow?.changeTracker?.checkState()\n canvasStore.linearMode = newMode\n }\n }\n ]\n\n return commands.map((command) => ({ ...command, source: 'System' }))\n}\n","import { useFavicon } from '@vueuse/core'\nimport { watch } from 'vue'\n\nimport { useExecutionStore } from '@/stores/executionStore'\n\nexport const useProgressFavicon = () => {\n const defaultFavicon = '/assets/images/favicon_progress_16x16/frame_9.png'\n const favicon = useFavicon(defaultFavicon)\n const executionStore = useExecutionStore()\n const totalFrames = 10\n\n watch(\n [() => executionStore.executionProgress, () => executionStore.isIdle],\n ([progress, isIdle]) => {\n if (isIdle) {\n favicon.value = defaultFavicon\n } else {\n const frame = Math.min(\n Math.max(0, Math.floor(progress * totalFrames)),\n totalFrames - 1\n )\n favicon.value = `/assets/images/favicon_progress_16x16/frame_${frame}.png`\n }\n }\n )\n}\n","export enum LatentPreviewMethod {\n NoPreviews = 'none',\n Auto = 'auto',\n Latent2RGB = 'latent2rgb',\n TAESD = 'taesd'\n}\n\nexport enum LogLevel {\n DEBUG = 'DEBUG',\n INFO = 'INFO',\n WARNING = 'WARNING',\n ERROR = 'ERROR',\n CRITICAL = 'CRITICAL'\n}\n\nexport enum HashFunction {\n MD5 = 'md5',\n SHA1 = 'sha1',\n SHA256 = 'sha256',\n SHA512 = 'sha512'\n}\n\nexport enum CudaMalloc {\n // Let server decide whether to use CUDA malloc based on the current environment\n Auto = 'auto',\n // Disable CUDA malloc\n Disable = 'disable',\n // Enable CUDA malloc\n Enable = 'enable'\n}\n\nexport enum FloatingPointPrecision {\n AUTO = 'auto',\n FP64 = 'fp64',\n FP32 = 'fp32',\n FP16 = 'fp16',\n BF16 = 'bf16',\n FP8E4M3FN = 'fp8_e4m3fn',\n FP8E5M2 = 'fp8_e5m2'\n}\n\nexport enum CrossAttentionMethod {\n Auto = 'auto',\n Split = 'split',\n Quad = 'quad',\n Pytorch = 'pytorch'\n}\n\nexport enum VramManagement {\n Auto = 'auto',\n GPUOnly = 'gpu-only',\n HighVram = 'highvram',\n NormalVram = 'normalvram',\n LowVram = 'lowvram',\n NoVram = 'novram',\n CPU = 'cpu'\n}\n","import type { FormItem } from '@/platform/settings/types'\nimport {\n CrossAttentionMethod,\n CudaMalloc,\n FloatingPointPrecision,\n HashFunction,\n LatentPreviewMethod,\n LogLevel,\n VramManagement\n} from '@/types/serverArgs'\n\nexport type ServerConfigValue = string | number | true | null | undefined\n\nexport interface ServerConfig<T> extends FormItem {\n id: string\n defaultValue: T\n category?: string[]\n // Override the default value getter with a custom function.\n getValue?: (value: T) => Record<string, ServerConfigValue>\n}\n\nexport const SERVER_CONFIG_ITEMS: ServerConfig<any>[] = [\n // Network settings\n {\n id: 'listen',\n name: 'Host: The IP address to listen on',\n category: ['Network'],\n type: 'text',\n defaultValue: '127.0.0.1'\n },\n {\n id: 'port',\n name: 'Port: The port to listen on',\n category: ['Network'],\n type: 'number',\n // The default launch port for desktop app is 8000 instead of 8188.\n defaultValue: 8000\n },\n {\n id: 'tls-keyfile',\n name: 'TLS Key File: Path to TLS key file for HTTPS',\n category: ['Network'],\n type: 'text',\n defaultValue: ''\n },\n {\n id: 'tls-certfile',\n name: 'TLS Certificate File: Path to TLS certificate file for HTTPS',\n category: ['Network'],\n type: 'text',\n defaultValue: ''\n },\n {\n id: 'enable-cors-header',\n name: 'Enable CORS header: Use \"*\" for all origins or specify domain',\n category: ['Network'],\n type: 'text',\n defaultValue: ''\n },\n {\n id: 'max-upload-size',\n name: 'Maximum upload size (MB)',\n category: ['Network'],\n type: 'number',\n defaultValue: 100\n },\n\n // CUDA settings\n {\n id: 'cuda-device',\n name: 'CUDA device index to use',\n category: ['CUDA'],\n type: 'number',\n defaultValue: null\n },\n {\n id: 'cuda-malloc',\n name: 'Use CUDA malloc for memory allocation',\n category: ['CUDA'],\n type: 'combo',\n options: Object.values(CudaMalloc),\n defaultValue: CudaMalloc.Auto,\n getValue: (value: CudaMalloc) => {\n switch (value) {\n case CudaMalloc.Auto:\n return {}\n case CudaMalloc.Enable:\n return {\n ['cuda-malloc']: true\n }\n case CudaMalloc.Disable:\n return {\n ['disable-cuda-malloc']: true\n }\n }\n }\n },\n\n // Precision settings\n {\n id: 'global-precision',\n name: 'Global floating point precision',\n category: ['Inference'],\n type: 'combo',\n options: [\n FloatingPointPrecision.AUTO,\n FloatingPointPrecision.FP32,\n FloatingPointPrecision.FP16\n ],\n defaultValue: FloatingPointPrecision.AUTO,\n tooltip: 'Global floating point precision',\n getValue: (value: FloatingPointPrecision) => {\n switch (value) {\n case FloatingPointPrecision.AUTO:\n return {}\n case FloatingPointPrecision.FP32:\n return {\n ['force-fp32']: true\n }\n case FloatingPointPrecision.FP16:\n return {\n ['force-fp16']: true\n }\n default:\n return {}\n }\n }\n },\n\n // UNET precision\n {\n id: 'unet-precision',\n name: 'UNET precision',\n category: ['Inference'],\n type: 'combo',\n options: [\n FloatingPointPrecision.AUTO,\n FloatingPointPrecision.FP64,\n FloatingPointPrecision.FP32,\n FloatingPointPrecision.FP16,\n FloatingPointPrecision.BF16,\n FloatingPointPrecision.FP8E4M3FN,\n FloatingPointPrecision.FP8E5M2\n ],\n defaultValue: FloatingPointPrecision.AUTO,\n tooltip: 'UNET precision',\n getValue: (value: FloatingPointPrecision) => {\n switch (value) {\n case FloatingPointPrecision.AUTO:\n return {}\n default:\n return {\n [`${value.toLowerCase()}-unet`]: true\n }\n }\n }\n },\n\n // VAE settings\n {\n id: 'vae-precision',\n name: 'VAE precision',\n category: ['Inference'],\n type: 'combo',\n options: [\n FloatingPointPrecision.AUTO,\n FloatingPointPrecision.FP16,\n FloatingPointPrecision.FP32,\n FloatingPointPrecision.BF16\n ],\n defaultValue: FloatingPointPrecision.AUTO,\n tooltip: 'VAE precision',\n getValue: (value: FloatingPointPrecision) => {\n switch (value) {\n case FloatingPointPrecision.AUTO:\n return {}\n default:\n return {\n [`${value.toLowerCase()}-vae`]: true\n }\n }\n }\n },\n {\n id: 'cpu-vae',\n name: 'Run VAE on CPU',\n category: ['Inference'],\n type: 'boolean',\n defaultValue: false\n },\n\n // Text Encoder settings\n {\n id: 'text-encoder-precision',\n name: 'Text Encoder precision',\n category: ['Inference'],\n type: 'combo',\n options: [\n FloatingPointPrecision.AUTO,\n FloatingPointPrecision.FP8E4M3FN,\n FloatingPointPrecision.FP8E5M2,\n FloatingPointPrecision.FP16,\n FloatingPointPrecision.FP32\n ],\n defaultValue: FloatingPointPrecision.AUTO,\n tooltip: 'Text Encoder precision',\n getValue: (value: FloatingPointPrecision) => {\n switch (value) {\n case FloatingPointPrecision.AUTO:\n return {}\n default:\n return {\n [`${value.toLowerCase()}-text-enc`]: true\n }\n }\n }\n },\n\n // Memory and performance settings\n {\n id: 'force-channels-last',\n name: 'Force channels-last memory format',\n category: ['Memory'],\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'directml',\n name: 'DirectML device index',\n category: ['Memory'],\n type: 'number',\n defaultValue: null\n },\n {\n id: 'disable-ipex-optimize',\n name: 'Disable IPEX optimization',\n category: ['Memory'],\n type: 'boolean',\n defaultValue: false\n },\n\n // Preview settings\n {\n id: 'preview-method',\n name: 'Method used for latent previews',\n category: ['Preview'],\n type: 'combo',\n options: Object.values(LatentPreviewMethod),\n defaultValue: LatentPreviewMethod.NoPreviews\n },\n {\n id: 'preview-size',\n name: 'Size of preview images',\n category: ['Preview'],\n type: 'slider',\n defaultValue: 512,\n attrs: {\n min: 128,\n max: 2048,\n step: 128\n }\n },\n\n // Cache settings\n {\n id: 'cache-classic',\n name: 'Use classic cache system',\n category: ['Cache'],\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'cache-lru',\n name: 'Use LRU caching with a maximum of N node results cached.',\n category: ['Cache'],\n type: 'number',\n defaultValue: null,\n tooltip: 'May use more RAM/VRAM.'\n },\n\n // Attention settings\n {\n id: 'cross-attention-method',\n name: 'Cross attention method',\n category: ['Attention'],\n type: 'combo',\n options: Object.values(CrossAttentionMethod),\n defaultValue: CrossAttentionMethod.Auto,\n getValue: (value: CrossAttentionMethod) => {\n switch (value) {\n case CrossAttentionMethod.Auto:\n return {}\n default:\n return {\n [`use-${value.toLowerCase()}-cross-attention`]: true\n }\n }\n }\n },\n {\n id: 'disable-xformers',\n name: 'Disable xFormers optimization',\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'force-upcast-attention',\n name: 'Force attention upcast',\n category: ['Attention'],\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'dont-upcast-attention',\n name: 'Prevent attention upcast',\n category: ['Attention'],\n type: 'boolean',\n defaultValue: false\n },\n\n // VRAM management\n {\n id: 'vram-management',\n name: 'VRAM management mode',\n category: ['Memory'],\n type: 'combo',\n options: Object.values(VramManagement),\n defaultValue: VramManagement.Auto,\n getValue: (value: VramManagement) => {\n switch (value) {\n case VramManagement.Auto:\n return {}\n default:\n return {\n [value]: true\n }\n }\n }\n },\n {\n id: 'reserve-vram',\n name: 'Reserved VRAM (GB)',\n category: ['Memory'],\n type: 'number',\n defaultValue: null,\n tooltip:\n 'Set the amount of vram in GB you want to reserve for use by your OS/other software. By default some amount is reserved depending on your OS.'\n },\n\n // Misc settings\n {\n id: 'default-hashing-function',\n name: 'Default hashing function for model files',\n type: 'combo',\n options: Object.values(HashFunction),\n defaultValue: HashFunction.SHA256\n },\n {\n id: 'disable-smart-memory',\n name: 'Disable smart memory management',\n tooltip:\n 'Force ComfyUI to aggressively offload to regular ram instead of keeping models in vram when it can.',\n category: ['Memory'],\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'deterministic',\n name: 'Make pytorch use slower deterministic algorithms when it can.',\n type: 'boolean',\n defaultValue: false,\n tooltip: 'Note that this might not make images deterministic in all cases.'\n },\n {\n id: 'fast',\n name: 'Enable some untested and potentially quality deteriorating optimizations.',\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'dont-print-server',\n name: \"Don't print server output to console.\",\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'disable-metadata',\n name: 'Disable saving prompt metadata in files.',\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'enable-manager-legacy-ui',\n name: 'Use legacy Manager UI',\n tooltip: 'Uses the legacy ComfyUI-Manager UI instead of the new UI.',\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'disable-all-custom-nodes',\n name: 'Disable loading all custom nodes.',\n type: 'boolean',\n defaultValue: false\n },\n {\n id: 'log-level',\n name: 'Logging verbosity level',\n type: 'combo',\n options: Object.values(LogLevel),\n defaultValue: LogLevel.INFO,\n getValue: (value: LogLevel) => {\n return {\n verbose: value\n }\n }\n },\n // Directories\n {\n id: 'input-directory',\n name: 'Input directory',\n category: ['Directories'],\n type: 'text',\n defaultValue: ''\n },\n {\n id: 'output-directory',\n name: 'Output directory',\n category: ['Directories'],\n type: 'text',\n defaultValue: ''\n }\n]\n","<script setup lang=\"ts\">\nimport { cn } from '@/utils/tailwindUtil'\n\nconst { visible } = defineProps<{\n visible: boolean\n}>()\n\nconst isExpanded = defineModel<boolean>('expanded', { default: false })\n\nfunction toggle() {\n isExpanded.value = !isExpanded.value\n}\n</script>\n\n<template>\n <Teleport to=\"body\">\n <Transition\n enter-active-class=\"transition-all duration-300 ease-out\"\n enter-from-class=\"translate-y-full opacity-0\"\n enter-to-class=\"translate-y-0 opacity-100\"\n leave-active-class=\"transition-all duration-200 ease-in\"\n leave-from-class=\"translate-y-0 opacity-100\"\n leave-to-class=\"translate-y-full opacity-0\"\n >\n <div\n v-if=\"visible\"\n role=\"status\"\n aria-live=\"polite\"\n class=\"fixed inset-x-0 bottom-6 z-9999 mx-auto w-4/5 max-w-3xl overflow-hidden rounded-lg border border-border-default bg-base-background shadow-lg\"\n >\n <div\n :class=\"\n cn(\n 'overflow-hidden transition-all duration-300',\n isExpanded ? 'max-h-[400px]' : 'max-h-0'\n )\n \"\n >\n <slot :is-expanded />\n </div>\n\n <slot name=\"footer\" :is-expanded :toggle />\n </div>\n </Transition>\n </Teleport>\n</template>\n","<script setup lang=\"ts\">\ntype Severity = 'default' | 'secondary' | 'warn' | 'danger' | 'contrast'\n\nconst { label, severity = 'default' } = defineProps<{\n label: string\n severity?: Severity\n}>()\n\nfunction badgeClasses(sev: Severity): string {\n const baseClasses =\n 'inline-flex h-3.5 items-center justify-center rounded-full px-1 text-xxxs font-semibold uppercase'\n\n switch (sev) {\n case 'danger':\n return `${baseClasses} bg-destructive-background text-white`\n case 'contrast':\n return `${baseClasses} bg-base-foreground text-base-background`\n case 'warn':\n return `${baseClasses} bg-warning-background text-base-background`\n case 'secondary':\n return `${baseClasses} bg-secondary-background text-base-foreground`\n default:\n return `${baseClasses} bg-primary-background text-base-foreground`\n }\n}\n</script>\n\n<template>\n <span :class=\"badgeClasses(severity)\">{{ label }}</span>\n</template>\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport StatusBadge from '@/components/common/StatusBadge.vue'\nimport type { AssetDownload } from '@/stores/assetDownloadStore'\nimport { cn } from '@/utils/tailwindUtil'\n\nconst { job } = defineProps<{\n job: AssetDownload\n}>()\n\nconst { t } = useI18n()\n\nconst progressPercent = computed(() => Math.round(job.progress * 100))\nconst isCompleted = computed(() => job.status === 'completed')\nconst isFailed = computed(() => job.status === 'failed')\nconst isRunning = computed(() => job.status === 'running')\nconst isPending = computed(() => job.status === 'created')\n</script>\n\n<template>\n <div\n :class=\"\n cn(\n 'flex items-center justify-between rounded-lg bg-modal-card-background px-4 py-3',\n isCompleted && 'opacity-50'\n )\n \"\n >\n <div class=\"min-w-0 flex-1\">\n <span class=\"block truncate text-sm text-base-foreground\">{{\n job.assetName\n }}</span>\n </div>\n\n <div class=\"flex flex-shrink-0 items-center gap-2\">\n <template v-if=\"isFailed\">\n <i\n class=\"icon-[lucide--circle-alert] size-4 text-destructive-background\"\n />\n <StatusBadge :label=\"t('progressToast.failed')\" severity=\"danger\" />\n </template>\n\n <template v-else-if=\"isCompleted\">\n <StatusBadge :label=\"t('progressToast.finished')\" severity=\"contrast\" />\n </template>\n\n <template v-else-if=\"isRunning\">\n <i\n class=\"icon-[lucide--loader-circle] size-4 animate-spin text-base-foreground\"\n />\n <span class=\"text-xs text-base-foreground\">\n {{ progressPercent }}%\n </span>\n </template>\n\n <template v-else-if=\"isPending\">\n <span class=\"text-xs text-muted-foreground\">\n {{ t('progressToast.pending') }}\n </span>\n </template>\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { whenever } from '@vueuse/core'\nimport Popover from 'primevue/popover'\nimport { computed, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport HoneyToast from '@/components/honeyToast/HoneyToast.vue'\nimport ProgressToastItem from '@/components/toast/ProgressToastItem.vue'\nimport Button from '@/components/ui/button/Button.vue'\nimport { useAssetDownloadStore } from '@/stores/assetDownloadStore'\nimport { cn } from '@/utils/tailwindUtil'\n\nconst { t } = useI18n()\nconst assetDownloadStore = useAssetDownloadStore()\n\nconst visible = computed(() => assetDownloadStore.hasDownloads)\n\nconst isExpanded = ref(false)\nconst activeFilter = ref<'all' | 'completed' | 'failed'>('all')\nconst filterPopoverRef = ref<InstanceType<typeof Popover> | null>(null)\n\nwhenever(\n () => !isExpanded.value,\n () => filterPopoverRef.value?.hide()\n)\n\nconst filterOptions = [\n { value: 'all', label: 'all' },\n { value: 'completed', label: 'completed' },\n { value: 'failed', label: 'failed' }\n] as const\n\nfunction onFilterClick(event: Event) {\n filterPopoverRef.value?.toggle(event)\n}\n\nfunction setFilter(filter: typeof activeFilter.value) {\n activeFilter.value = filter\n filterPopoverRef.value?.hide()\n}\n\nconst downloadJobs = computed(() => assetDownloadStore.downloadList)\nconst completedJobs = computed(() =>\n assetDownloadStore.finishedDownloads.filter((d) => d.status === 'completed')\n)\nconst failedJobs = computed(() =>\n assetDownloadStore.finishedDownloads.filter((d) => d.status === 'failed')\n)\n\nconst isInProgress = computed(() => assetDownloadStore.hasActiveDownloads)\nconst currentJobName = computed(() => {\n const activeJob = downloadJobs.value.find((job) => job.status === 'running')\n return activeJob?.assetName || t('progressToast.downloadingModel')\n})\n\nconst completedCount = computed(\n () => completedJobs.value.length + failedJobs.value.length\n)\nconst totalCount = computed(() => downloadJobs.value.length)\n\nconst filteredJobs = computed(() => {\n switch (activeFilter.value) {\n case 'completed':\n return completedJobs.value\n case 'failed':\n return failedJobs.value\n default:\n return downloadJobs.value\n }\n})\n\nconst activeFilterLabel = computed(() => {\n const option = filterOptions.find((f) => f.value === activeFilter.value)\n return option\n ? t(`progressToast.filter.${option.label}`)\n : t('progressToast.filter.all')\n})\n\nfunction closeDialog() {\n assetDownloadStore.clearFinishedDownloads()\n isExpanded.value = false\n}\n</script>\n\n<template>\n <HoneyToast v-model:expanded=\"isExpanded\" :visible>\n <template #default>\n <div\n class=\"flex h-12 items-center justify-between border-b border-border-default px-4\"\n >\n <h3 class=\"text-sm font-bold text-base-foreground\">\n {{ t('progressToast.importingModels') }}\n </h3>\n <div class=\"flex items-center gap-2\">\n <Button\n variant=\"secondary\"\n size=\"md\"\n class=\"gap-1.5 px-2\"\n @click=\"onFilterClick\"\n >\n <i class=\"icon-[lucide--list-filter] size-4\" />\n <span>{{ activeFilterLabel }}</span>\n <i class=\"icon-[lucide--chevron-down] size-3\" />\n </Button>\n <Popover\n ref=\"filterPopoverRef\"\n :dismissable=\"true\"\n :close-on-escape=\"true\"\n unstyled\n :base-z-index=\"9999\"\n :pt=\"{\n root: { class: 'absolute z-50' },\n content: {\n class:\n 'bg-transparent border-none p-0 pt-2 rounded-lg shadow-lg'\n }\n }\"\n >\n <div\n class=\"flex min-w-30 flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3\"\n >\n <Button\n v-for=\"option in filterOptions\"\n :key=\"option.value\"\n variant=\"textonly\"\n size=\"sm\"\n :class=\"\n cn(\n 'w-full justify-start bg-transparent',\n activeFilter === option.value &&\n 'bg-secondary-background-selected'\n )\n \"\n @click=\"setFilter(option.value)\"\n >\n {{ t(`progressToast.filter.${option.label}`) }}\n </Button>\n </div>\n </Popover>\n </div>\n </div>\n\n <div class=\"relative max-h-75 overflow-y-auto px-4 py-4\">\n <div\n v-if=\"filteredJobs.length > 3\"\n class=\"absolute right-1 top-4 h-12 w-1 rounded-full bg-muted-foreground\"\n />\n\n <div class=\"flex flex-col gap-2\">\n <ProgressToastItem\n v-for=\"job in filteredJobs\"\n :key=\"job.taskId\"\n :job=\"job\"\n />\n </div>\n\n <div\n v-if=\"filteredJobs.length === 0\"\n class=\"flex flex-col items-center justify-center py-6 text-center\"\n >\n <span class=\"text-sm text-muted-foreground\">\n {{\n t('progressToast.noImportsInQueue', {\n filter: activeFilterLabel\n })\n }}\n </span>\n </div>\n </div>\n </template>\n\n <template #footer=\"{ toggle }\">\n <div\n class=\"flex h-12 items-center justify-between gap-2 border-t border-border-default px-4\"\n >\n <div class=\"flex min-w-0 flex-1 items-center gap-2 text-sm\">\n <template v-if=\"isInProgress\">\n <i\n class=\"icon-[lucide--loader-circle] size-4 flex-shrink-0 animate-spin text-muted-foreground\"\n />\n <span\n class=\"min-w-0 flex-1 truncate font-bold text-base-foreground\"\n >\n {{ currentJobName }}\n </span>\n </template>\n <template v-else-if=\"failedJobs.length > 0\">\n <i\n class=\"icon-[lucide--circle-alert] size-4 flex-shrink-0 text-destructive-background\"\n />\n <span class=\"min-w-0 truncate font-bold text-base-foreground\">\n {{\n t('progressToast.downloadsFailed', {\n count: failedJobs.length\n })\n }}\n </span>\n </template>\n <template v-else>\n <i\n class=\"icon-[lucide--check-circle] size-4 flex-shrink-0 text-jade-600\"\n />\n <span class=\"min-w-0 truncate font-bold text-base-foreground\">\n {{ t('progressToast.allDownloadsCompleted') }}\n </span>\n </template>\n </div>\n\n <div class=\"flex flex-shrink-0 items-center gap-2\">\n <span\n v-if=\"isInProgress\"\n class=\"whitespace-nowrap text-sm text-muted-foreground\"\n >\n {{\n t('progressToast.progressCount', {\n completed: completedCount,\n total: totalCount\n })\n }}\n </span>\n\n <div class=\"flex items-center\">\n <Button\n variant=\"muted-textonly\"\n size=\"icon\"\n :aria-label=\"\n isExpanded ? t('contextMenu.Collapse') : t('contextMenu.Expand')\n \"\n @click.stop=\"toggle\"\n >\n <i\n :class=\"\n cn(\n 'size-4',\n isExpanded\n ? 'icon-[lucide--chevron-down]'\n : 'icon-[lucide--chevron-up]'\n )\n \"\n />\n </Button>\n\n <Button\n v-if=\"!isInProgress\"\n variant=\"muted-textonly\"\n size=\"icon\"\n :aria-label=\"t('g.close')\"\n @click.stop=\"closeDialog\"\n >\n <i class=\"icon-[lucide--x] size-4\" />\n </Button>\n </div>\n </div>\n </div>\n </template>\n </HoneyToast>\n</template>\n","import { until, useStorage } from '@vueuse/core'\nimport { defineStore } from 'pinia'\nimport { gt, valid } from 'semver'\nimport { computed } from 'vue'\n\nimport config from '@/config'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useSystemStatsStore } from '@/stores/systemStatsStore'\n\nconst DISMISSAL_DURATION_MS = 7 * 24 * 60 * 60 * 1000 // 7 days\n\nexport const useVersionCompatibilityStore = defineStore(\n 'versionCompatibility',\n () => {\n const systemStatsStore = useSystemStatsStore()\n const settingStore = useSettingStore()\n\n const frontendVersion = computed(() => config.app_version)\n const backendVersion = computed(\n () => systemStatsStore.systemStats?.system?.comfyui_version ?? ''\n )\n const requiredFrontendVersion = computed(\n () =>\n systemStatsStore.systemStats?.system?.required_frontend_version ?? ''\n )\n\n const isFrontendOutdated = computed(() => {\n if (\n !frontendVersion.value ||\n !requiredFrontendVersion.value ||\n !valid(frontendVersion.value) ||\n !valid(requiredFrontendVersion.value)\n ) {\n return false\n }\n // Returns true if required version is greater than frontend version\n return gt(requiredFrontendVersion.value, frontendVersion.value)\n })\n\n const isFrontendNewer = computed(() => {\n // We don't warn about frontend being newer than backend\n // Only warn when frontend is outdated (behind required version)\n return false\n })\n\n const hasVersionMismatch = computed(() => {\n return isFrontendOutdated.value\n })\n\n const versionKey = computed(() => {\n if (\n !frontendVersion.value ||\n !backendVersion.value ||\n !requiredFrontendVersion.value\n ) {\n return null\n }\n return `${frontendVersion.value}-${backendVersion.value}-${requiredFrontendVersion.value}`\n })\n\n // Use reactive storage for dismissals - creates a reactive ref that syncs with localStorage\n // All version mismatch dismissals are stored in a single object for clean localStorage organization\n const dismissalStorage = useStorage(\n 'comfy.versionMismatch.dismissals',\n {} as Record<string, number>,\n localStorage,\n {\n serializer: {\n read: (value: string) => {\n try {\n return JSON.parse(value)\n } catch {\n return {}\n }\n },\n write: (value: Record<string, number>) => JSON.stringify(value)\n }\n }\n )\n\n const isDismissed = computed(() => {\n if (!versionKey.value) return false\n\n const dismissedUntil = dismissalStorage.value[versionKey.value]\n if (!dismissedUntil) return false\n\n // Check if dismissal has expired\n return Date.now() < dismissedUntil\n })\n\n const warningsDisabled = computed(() =>\n settingStore.get('Comfy.VersionCompatibility.DisableWarnings')\n )\n\n const shouldShowWarning = computed(() => {\n return (\n hasVersionMismatch.value &&\n !isDismissed.value &&\n !warningsDisabled.value\n )\n })\n\n const warningMessage = computed(() => {\n if (isFrontendOutdated.value) {\n return {\n type: 'outdated' as const,\n frontendVersion: frontendVersion.value,\n requiredVersion: requiredFrontendVersion.value\n }\n }\n return null\n })\n\n async function checkVersionCompatibility() {\n if (!systemStatsStore.systemStats) {\n await until(systemStatsStore.isInitialized)\n }\n }\n\n function dismissWarning() {\n if (!versionKey.value) return\n\n const dismissUntil = Date.now() + DISMISSAL_DURATION_MS\n dismissalStorage.value = {\n ...dismissalStorage.value,\n [versionKey.value]: dismissUntil\n }\n }\n\n async function initialize() {\n await checkVersionCompatibility()\n }\n\n return {\n frontendVersion,\n backendVersion,\n requiredFrontendVersion,\n hasVersionMismatch,\n shouldShowWarning,\n warningMessage,\n isFrontendOutdated,\n isFrontendNewer,\n checkVersionCompatibility,\n dismissWarning,\n initialize\n }\n }\n)\n","import { whenever } from '@vueuse/core'\nimport { computed, nextTick, onMounted } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { useToastStore } from './toastStore'\nimport { useVersionCompatibilityStore } from './versionCompatibilityStore'\n\ninterface UseFrontendVersionMismatchWarningOptions {\n immediate?: boolean\n}\n\n/**\n * Composable for handling frontend version mismatch warnings.\n *\n * Displays toast notifications when the frontend version is incompatible with the backend,\n * either because the frontend is outdated or newer than the backend expects.\n * Automatically dismisses warnings when shown and persists dismissal state for 7 days.\n *\n * @param options - Configuration options\n * @param options.immediate - If true, automatically shows warning when version mismatch is detected\n * @returns Object with methods and computed properties for managing version warnings\n *\n * @example\n * ```ts\n * // Show warning immediately when mismatch detected\n * const { showWarning, shouldShowWarning } = useFrontendVersionMismatchWarning({ immediate: true })\n *\n * // Manual control\n * const { showWarning } = useFrontendVersionMismatchWarning()\n * showWarning() // Call when needed\n * ```\n */\nexport function useFrontendVersionMismatchWarning(\n options: UseFrontendVersionMismatchWarningOptions = {}\n) {\n const { immediate = false } = options\n const { t } = useI18n()\n const toastStore = useToastStore()\n const versionCompatibilityStore = useVersionCompatibilityStore()\n\n // Track if we've already shown the warning\n let hasShownWarning = false\n\n const showWarning = () => {\n // Prevent showing the warning multiple times\n if (hasShownWarning) return\n\n const message = versionCompatibilityStore.warningMessage\n if (!message) return\n\n const detailMessage = t('g.frontendOutdated', {\n frontendVersion: message.frontendVersion,\n requiredVersion: message.requiredVersion\n })\n\n const fullMessage = t('g.versionMismatchWarningMessage', {\n warning: t('g.versionMismatchWarning'),\n detail: detailMessage\n })\n\n toastStore.addAlert(fullMessage)\n hasShownWarning = true\n\n // Automatically dismiss the warning so it won't show again for 7 days\n versionCompatibilityStore.dismissWarning()\n }\n\n onMounted(async () => {\n // Only set up the watcher if immediate is true\n if (immediate) {\n // Wait for next tick to ensure reactive updates from settings load have propagated\n await nextTick()\n\n whenever(\n () => versionCompatibilityStore.shouldShowWarning,\n () => {\n showWarning()\n },\n {\n immediate: true,\n once: true\n }\n )\n }\n })\n\n return {\n showWarning,\n shouldShowWarning: computed(\n () => versionCompatibilityStore.shouldShowWarning\n ),\n dismissWarning: versionCompatibilityStore.dismissWarning,\n hasVersionMismatch: computed(\n () => versionCompatibilityStore.hasVersionMismatch\n )\n }\n}\n","import { api } from '@/scripts/api'\nimport { app } from '@/scripts/app'\nimport {\n useQueuePendingTaskCountStore,\n useQueueSettingsStore\n} from '@/stores/queueStore'\n\nexport function setupAutoQueueHandler() {\n const queueCountStore = useQueuePendingTaskCountStore()\n const queueSettingsStore = useQueueSettingsStore()\n\n let graphHasChanged = false\n let internalCount = 0 // Use an internal counter here so it is instantly updated when re-queuing\n api.addEventListener('graphChanged', () => {\n if (queueSettingsStore.mode === 'change') {\n if (internalCount) {\n graphHasChanged = true\n } else {\n graphHasChanged = false\n // Queue the prompt in the background\n void app.queuePrompt(0, queueSettingsStore.batchCount)\n internalCount++\n }\n }\n })\n\n queueCountStore.$subscribe(\n async () => {\n internalCount = queueCountStore.count\n if (!internalCount && !app.lastExecutionError) {\n if (\n queueSettingsStore.mode === 'instant' ||\n (queueSettingsStore.mode === 'change' && graphHasChanged)\n ) {\n graphHasChanged = false\n await app.queuePrompt(0, queueSettingsStore.batchCount)\n }\n }\n },\n { detached: true }\n )\n}\n","<script setup lang=\"ts\">\nimport { whenever } from '@vueuse/core'\nimport { useTemplateRef } from 'vue'\n\nimport Popover from '@/components/ui/Popover.vue'\nimport Button from '@/components/ui/button/Button.vue'\n\ndefineProps<{\n dataTfWidget: string\n}>()\n\nconst feedbackRef = useTemplateRef('feedbackRef')\n\nwhenever(feedbackRef, () => {\n const scriptEl = document.createElement('script')\n scriptEl.src = '//embed.typeform.com/next/embed.js'\n feedbackRef.value?.appendChild(scriptEl)\n})\n</script>\n<template>\n <Popover>\n <template #button>\n <Button variant=\"inverted\" class=\"rounded-full size-12\">\n <i class=\"icon-[lucide--circle-question-mark] size-6\" />\n </Button>\n </template>\n <div ref=\"feedbackRef\" data-tf-auto-resize :data-tf-widget />\n </Popover>\n</template>\n","<script setup lang=\"ts\">\nimport { ref } from 'vue'\n\nimport { cn } from '@/utils/tailwindUtil'\n\ndefineProps<{\n onDragOver?: (e: DragEvent) => boolean\n onDragDrop?: (e: DragEvent) => Promise<boolean> | boolean\n dropIndicator?: {\n iconClass?: string\n imageUrl?: string\n label?: string\n onClick?: (e: MouseEvent) => void\n }\n}>()\n\nconst canAcceptDrop = ref(false)\n</script>\n<template>\n <div\n v-if=\"onDragOver && onDragDrop\"\n :class=\"\n cn(\n 'rounded-lg ring-inset ring-primary-500',\n canAcceptDrop && 'ring-4 bg-primary-500/10'\n )\n \"\n @dragover.prevent=\"canAcceptDrop = onDragOver?.($event)\"\n @dragleave=\"canAcceptDrop = false\"\n @drop.stop.prevent=\"\n (e: DragEvent) => {\n onDragDrop!(e)\n canAcceptDrop = false\n }\n \"\n >\n <slot />\n <div\n v-if=\"dropIndicator\"\n :class=\"\n cn(\n 'flex flex-col items-center justify-center gap-2 border-dashed rounded-lg border h-25 border-border-subtle m-3 py-2',\n dropIndicator?.onClick && 'cursor-pointer'\n )\n \"\n @click.prevent=\"dropIndicator?.onClick?.($event)\"\n >\n <img\n v-if=\"dropIndicator?.imageUrl\"\n class=\"h-23\"\n :src=\"dropIndicator?.imageUrl\"\n />\n <template v-else>\n <span v-if=\"dropIndicator.label\" v-text=\"dropIndicator.label\" />\n <i v-if=\"dropIndicator.iconClass\" :class=\"dropIndicator.iconClass\" />\n </template>\n </div>\n </div>\n <slot v-else />\n</template>\n","<script setup lang=\"ts\">\nimport { useEventListener, useTimeout } from '@vueuse/core'\nimport { partition } from 'es-toolkit'\nimport { storeToRefs } from 'pinia'\nimport { computed, ref, shallowRef } from 'vue'\n\nimport Popover from '@/components/ui/Popover.vue'\nimport Button from '@/components/ui/button/Button.vue'\nimport { extractVueNodeData } from '@/composables/graph/useGraphNodeManager'\nimport { t } from '@/i18n'\nimport type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'\nimport SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'\nimport { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'\nimport { isCloud } from '@/platform/distribution/types'\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport DropZone from '@/renderer/extensions/linearMode/DropZone.vue'\nimport NodeWidgets from '@/renderer/extensions/vueNodes/components/NodeWidgets.vue'\nimport { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils'\nimport WidgetInputNumberInput from '@/renderer/extensions/vueNodes/widgets/components/WidgetInputNumber.vue'\nimport { api } from '@/scripts/api'\nimport { app } from '@/scripts/app'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { useExecutionStore } from '@/stores/executionStore'\nimport { useQueueSettingsStore } from '@/stores/queueStore'\nimport type { SimplifiedWidget } from '@/types/simplifiedWidget'\nimport { cn } from '@/utils/tailwindUtil'\n\nconst commandStore = useCommandStore()\nconst executionStore = useExecutionStore()\nconst { batchCount } = storeToRefs(useQueueSettingsStore())\nconst { isActiveSubscription } = useSubscription()\nconst workflowStore = useWorkflowStore()\n\nconst props = defineProps<{\n toastTo?: string | HTMLElement\n notesTo?: string | HTMLElement\n mobile?: boolean\n}>()\n\nconst jobFinishedQueue = ref(true)\nconst { ready: jobToastTimeout, start: resetJobToastTimeout } = useTimeout(\n 5000,\n { controls: true, immediate: false }\n)\n\nconst graphNodes = shallowRef<LGraphNode[]>(app.rootGraph.nodes)\nuseEventListener(\n app.rootGraph.events,\n 'configured',\n () => (graphNodes.value = app.rootGraph.nodes)\n)\n\nfunction getDropIndicator(node: LGraphNode) {\n if (node.type !== 'LoadImage') return undefined\n\n const filename = node.widgets?.[0]?.value\n const resultItem = { type: 'input', filename: `${filename}` }\n\n return {\n iconClass: 'icon-[lucide--image]',\n imageUrl: filename\n ? api.apiURL(\n `/view?${new URLSearchParams(resultItem)}${app.getPreviewFormatParam()}`\n )\n : undefined,\n label: t('linearMode.dragAndDropImage'),\n onClick: () => node.widgets?.[1]?.callback?.(undefined)\n }\n}\n\nfunction nodeToNodeData(node: LGraphNode) {\n const dropIndicator = getDropIndicator(node)\n const nodeData = extractVueNodeData(node)\n for (const widget of nodeData.widgets ?? []) widget.slotMetadata = undefined\n\n return {\n ...nodeData,\n //note lastNodeErrors uses exeuctionid, node.id is execution for root\n hasErrors: !!executionStore.lastNodeErrors?.[node.id],\n\n dropIndicator,\n onDragDrop: node.onDragDrop,\n onDragOver: node.onDragOver\n }\n}\nconst partitionedNodes = computed(() => {\n const parts = partition(\n graphNodes.value\n .filter((node) => node.mode === 0 && node.widgets?.length)\n .map(nodeToNodeData)\n .reverse(),\n (node) => ['MarkdownNote', 'Note'].includes(node.type)\n )\n for (const noteNode of parts[0]) {\n for (const widget of noteNode.widgets ?? [])\n widget.options = { ...widget.options, read_only: true }\n }\n return parts\n})\n\nconst batchCountWidget: SimplifiedWidget<number> = {\n options: { precision: 0, min: 1, max: isCloud ? 4 : 99 },\n value: 1,\n name: t('linearMode.runCount'),\n type: 'number'\n} as const\n\n//TODO: refactor out of this file.\n//code length is small, but changes should propagate\nasync function runButtonClick(e: Event) {\n if (!jobFinishedQueue.value) return\n try {\n jobFinishedQueue.value = false\n resetJobToastTimeout()\n const isShiftPressed = 'shiftKey' in e && e.shiftKey\n const commandId = isShiftPressed\n ? 'Comfy.QueuePromptFront'\n : 'Comfy.QueuePrompt'\n\n if (batchCount.value > 1) {\n useTelemetry()?.trackUiButtonClicked({\n button_id: 'queue_run_multiple_batches_submitted'\n })\n }\n await commandStore.execute(commandId, {\n metadata: {\n subscribe_to_run: false,\n trigger_source: 'linear'\n }\n })\n } finally {\n //TODO: Error state indicator for failed queue?\n jobFinishedQueue.value = true\n }\n}\n\ndefineExpose({ runButtonClick })\n</script>\n<template>\n <div class=\"flex flex-col min-w-80 md:h-full\">\n <section\n v-if=\"mobile\"\n data-testid=\"linear-run-button\"\n class=\"p-4 pb-6 border-t border-node-component-border\"\n >\n <WidgetInputNumberInput\n v-model=\"batchCount\"\n :widget=\"batchCountWidget\"\n class=\"*:[.min-w-0]:w-24 grid-cols-[auto_96px]!\"\n />\n <SubscribeToRunButton v-if=\"!isActiveSubscription\" class=\"w-full mt-4\" />\n <div v-else class=\"flex mt-4 gap-2\">\n <Button\n variant=\"primary\"\n class=\"grow-1\"\n size=\"lg\"\n @click=\"runButtonClick\"\n >\n <i class=\"icon-[lucide--play]\" />\n {{ t('menu.run') }}\n </Button>\n <Button\n v-if=\"!executionStore.isIdle\"\n variant=\"destructive\"\n size=\"lg\"\n class=\"w-10 p-2\"\n @click=\"commandStore.execute('Comfy.Interrupt')\"\n >\n <i class=\"icon-[lucide--x]\" />\n </Button>\n </div>\n </section>\n <section\n data-testid=\"linear-workflow-info\"\n class=\"h-12 border-x border-border-subtle py-2 px-4 gap-2 bg-comfy-menu-bg flex items-center md:contain-size\"\n >\n <span\n class=\"font-bold truncate\"\n v-text=\"workflowStore.activeWorkflow?.filename\"\n />\n <div class=\"flex-1\" />\n <Popover\n v-if=\"partitionedNodes[0].length\"\n align=\"start\"\n class=\"overflow-y-auto overflow-x-clip max-h-(--reka-popover-content-available-height) z-100\"\n :reference=\"notesTo\"\n side=\"left\"\n :to=\"notesTo\"\n >\n <template #button>\n <Button variant=\"muted-textonly\">\n <i class=\"icon-[lucide--info]\" />\n </Button>\n </template>\n <div>\n <template\n v-for=\"(nodeData, index) in partitionedNodes[0]\"\n :key=\"nodeData.id\"\n >\n <div\n v-if=\"index !== 0\"\n class=\"w-full border-t border-border-subtle\"\n />\n <NodeWidgets\n :node-data\n :style=\"{ background: applyLightThemeColor(nodeData.bgcolor) }\"\n class=\"py-3 gap-y-3 **:[.col-span-2]:grid-cols-1 not-has-[textarea]:flex-0 rounded-lg max-w-100\"\n />\n </template>\n </div>\n </Popover>\n <Button v-if=\"false\"> {{ t('menuLabels.publish') }} </Button>\n </section>\n <div\n class=\"border gap-2 md:h-full border-[var(--interface-stroke)] bg-comfy-menu-bg flex flex-col px-2\"\n >\n <section\n data-testid=\"linear-widgets\"\n class=\"grow-1 md:overflow-y-auto md:contain-size\"\n >\n <template\n v-for=\"(nodeData, index) of partitionedNodes[1]\"\n :key=\"nodeData.id\"\n >\n <div\n v-if=\"index !== 0\"\n class=\"w-full border-t-1 border-node-component-border\"\n />\n <DropZone\n :on-drag-over=\"nodeData.onDragOver\"\n :on-drag-drop=\"nodeData.onDragDrop\"\n :drop-indicator=\"mobile ? undefined : nodeData.dropIndicator\"\n class=\"text-muted-foreground\"\n >\n <NodeWidgets\n :node-data\n :class=\"\n cn(\n 'py-3 gap-y-3 **:[.col-span-2]:grid-cols-1 not-has-[textarea]:flex-0 rounded-lg',\n nodeData.hasErrors &&\n 'ring-2 ring-inset ring-node-stroke-error'\n )\n \"\n :style=\"{ background: applyLightThemeColor(nodeData.bgcolor) }\"\n />\n </DropZone>\n </template>\n </section>\n <section\n v-if=\"!mobile\"\n data-testid=\"linear-run-button\"\n class=\"p-4 pb-6 border-t border-node-component-border\"\n >\n <WidgetInputNumberInput\n v-model=\"batchCount\"\n :widget=\"batchCountWidget\"\n class=\"*:[.min-w-0]:w-24 grid-cols-[auto_96px]!\"\n />\n <SubscribeToRunButton\n v-if=\"!isActiveSubscription\"\n class=\"w-full mt-4\"\n />\n <div v-else class=\"flex mt-4 gap-2\">\n <Button\n variant=\"primary\"\n class=\"grow-1\"\n size=\"lg\"\n @click=\"runButtonClick\"\n >\n <i class=\"icon-[lucide--play]\" />\n {{ t('menu.run') }}\n </Button>\n <Button\n v-if=\"!executionStore.isIdle\"\n variant=\"destructive\"\n size=\"lg\"\n class=\"w-10 p-2\"\n @click=\"commandStore.execute('Comfy.Interrupt')\"\n >\n <i class=\"icon-[lucide--x]\" />\n </Button>\n </div>\n </section>\n </div>\n </div>\n <Teleport\n v-if=\"(!jobToastTimeout || !jobFinishedQueue) && toastTo\"\n defer\n :to=\"toastTo\"\n >\n <div\n class=\"bg-base-foreground text-base-background rounded-sm flex h-8 p-1 pr-2 gap-2 items-center\"\n >\n <i\n v-if=\"jobFinishedQueue\"\n class=\"icon-[lucide--check] size-5 bg-success-background\"\n />\n <i v-else class=\"icon-[lucide--loader-circle] size-4 animate-spin\" />\n <span v-text=\"t('queue.jobAddedToQueue')\" />\n </div>\n </Teleport>\n <Teleport v-if=\"false\" defer :to=\"notesTo\">\n <div\n class=\"bg-base-background text-muted-foreground flex flex-col w-90 gap-2 rounded-2xl border-1 border-border-subtle py-3\"\n ></div>\n </Teleport>\n</template>\n","<script setup lang=\"ts\">\nimport { computed, ref, useTemplateRef } from 'vue'\n\nconst zoomPane = useTemplateRef('zoomPane')\n\nconst zoom = ref(1.0)\nconst panX = ref(0.0)\nconst panY = ref(0.0)\n\nfunction handleWheel(e: WheelEvent) {\n const zoomPaneEl = zoomPane.value\n if (!zoomPaneEl) return\n\n zoom.value -= e.deltaY\n const { x, y, width, height } = zoomPaneEl.getBoundingClientRect()\n const offsetX = e.clientX - x - width / 2\n const offsetY = e.clientY - y - height / 2\n const scaler = 1.1 ** (e.deltaY / -30)\n\n panY.value = panY.value * scaler - offsetY * (scaler - 1)\n panX.value = panX.value * scaler - offsetX * (scaler - 1)\n}\n\nlet dragging = false\nfunction handleDown(e: PointerEvent) {\n if (e.button !== 0) return\n\n const zoomPaneEl = zoomPane.value\n if (!zoomPaneEl) return\n zoomPaneEl.parentElement?.focus()\n\n zoomPaneEl.setPointerCapture(e.pointerId)\n dragging = true\n}\nfunction handleMove(e: PointerEvent) {\n if (!dragging) return\n panX.value += e.movementX\n panY.value += e.movementY\n}\n\nconst transform = computed(() => {\n const scale = 1.1 ** (zoom.value / 30)\n const matrix = [scale, 0, 0, scale, panX.value, panY.value]\n return `matrix(${matrix.join(',')})`\n})\n</script>\n<template>\n <div\n ref=\"zoomPane\"\n class=\"contain-size place-content-center\"\n @wheel=\"handleWheel\"\n @pointerdown.prevent=\"handleDown\"\n @pointermove=\"handleMove\"\n @pointerup=\"dragging = false\"\n @pointercancel=\"dragging = false\"\n >\n <slot :style=\"{ transform }\" />\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { ref, useTemplateRef } from 'vue'\n\nimport ZoomPane from '@/components/ui/ZoomPane.vue'\n\nconst { src } = defineProps<{\n src: string\n mobile?: boolean\n}>()\n\nconst imageRef = useTemplateRef('imageRef')\nconst width = ref('')\nconst height = ref('')\n</script>\n<template>\n <ZoomPane v-if=\"!mobile\" v-slot=\"slotProps\" class=\"flex-1 w-full\">\n <img\n ref=\"imageRef\"\n :src\n v-bind=\"slotProps\"\n class=\"h-full object-contain w-full\"\n @load=\"\n () => {\n if (!imageRef) return\n width = `${imageRef.naturalWidth}`\n height = `${imageRef.naturalHeight}`\n }\n \"\n />\n </ZoomPane>\n <img\n v-else\n ref=\"imageRef\"\n class=\"w-full\"\n :src\n @load=\"\n () => {\n if (!imageRef) return\n width = `${imageRef.naturalWidth}`\n height = `${imageRef.naturalHeight}`\n }\n \"\n />\n <span class=\"self-center md:z-10\" v-text=\"`${width} x ${height}`\" />\n</template>\n","<script setup lang=\"ts\">\nimport { useTemplateRef, watch } from 'vue'\n\nimport AnimationControls from '@/components/load3d/controls/AnimationControls.vue'\nimport { useLoad3dViewer } from '@/composables/useLoad3dViewer'\n\nconst { modelUrl } = defineProps<{\n modelUrl: string\n}>()\n\nconst containerRef = useTemplateRef('containerRef')\n\nconst viewer = useLoad3dViewer()\n\nwatch([containerRef, () => modelUrl], async () => {\n if (!containerRef.value || !modelUrl) return\n\n await viewer.initializeStandaloneViewer(containerRef.value, modelUrl)\n})\n\n//TODO: refactor to add control buttons\n</script>\n<template>\n <div\n ref=\"containerRef\"\n class=\"relative w-full h-full\"\n @mouseenter=\"viewer.handleMouseEnter\"\n @mouseleave=\"viewer.handleMouseLeave\"\n @resize=\"viewer.handleResize\"\n >\n <div class=\"pointer-events-none absolute top-0 left-0 size-full\">\n <AnimationControls\n v-if=\"viewer.animations.value && viewer.animations.value.length > 0\"\n v-model:animations=\"viewer.animations.value\"\n v-model:playing=\"viewer.playing.value\"\n v-model:selected-speed=\"viewer.selectedSpeed.value\"\n v-model:selected-animation=\"viewer.selectedAnimation.value\"\n v-model:animation-progress=\"viewer.animationProgress.value\"\n v-model:animation-duration=\"viewer.animationDuration.value\"\n @seek=\"viewer.handleSeek\"\n />\n </div>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { ref, useTemplateRef } from 'vue'\n\nconst { src } = defineProps<{\n src: string\n}>()\n\nconst videoRef = useTemplateRef('videoRef')\nconst width = ref('')\nconst height = ref('')\n</script>\n<template>\n <video\n ref=\"videoRef\"\n :src\n controls\n v-bind=\"$attrs\"\n @loadedmetadata=\"\n () => {\n if (!videoRef) return\n width = `${videoRef.videoWidth}`\n height = `${videoRef.videoHeight}`\n }\n \"\n />\n <span class=\"self-center z-10\" v-text=\"`${width} x ${height}`\" />\n</template>\n","import { t } from '@/i18n'\n\nimport type { ResultItemImpl } from '@/stores/queueStore'\n\nexport type StatItem = { content?: string; iconClass?: string }\nexport const mediaTypes: Record<string, StatItem> = {\n '3d': {\n content: t('sideToolbar.mediaAssets.filter3D'),\n iconClass: 'icon-[lucide--box]'\n },\n audio: {\n content: t('sideToolbar.mediaAssets.filterAudio'),\n iconClass: 'icon-[lucide--audio-lines]'\n },\n images: {\n content: t('sideToolbar.mediaAssets.filterImage'),\n iconClass: 'icon-[lucide--image]'\n },\n text: {\n content: t('sideToolbar.mediaAssets.filterText'),\n iconClass: 'icon-[lucide--text]'\n },\n video: {\n content: t('sideToolbar.mediaAssets.filterVideo'),\n iconClass: 'icon-[lucide--video]'\n }\n}\n\nexport function getMediaType(output?: ResultItemImpl) {\n if (!output) return ''\n if (output.isVideo) return 'video'\n return output.mediaType\n}\n","<script setup lang=\"ts\">\nimport { computed } from 'vue'\n\nimport { downloadFile } from '@/base/common/downloadUtil'\nimport Popover from '@/components/ui/Popover.vue'\nimport Button from '@/components/ui/button/Button.vue'\nimport { d, t } from '@/i18n'\nimport { useMediaAssetActions } from '@/platform/assets/composables/useMediaAssetActions'\nimport { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'\nimport type { AssetItem } from '@/platform/assets/schemas/assetSchema'\nimport { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'\nimport { extractWorkflowFromAsset } from '@/platform/workflow/utils/workflowExtractionUtil'\nimport ImagePreview from '@/renderer/extensions/linearMode/ImagePreview.vue'\nimport Preview3d from '@/renderer/extensions/linearMode/Preview3d.vue'\nimport VideoPreview from '@/renderer/extensions/linearMode/VideoPreview.vue'\nimport {\n getMediaType,\n mediaTypes\n} from '@/renderer/extensions/linearMode/mediaTypes'\nimport type { StatItem } from '@/renderer/extensions/linearMode/mediaTypes'\nimport { app } from '@/scripts/app'\nimport type { ResultItemImpl } from '@/stores/queueStore'\nimport { formatDuration } from '@/utils/dateTimeUtil'\nimport { collectAllNodes } from '@/utils/graphTraversalUtil'\nimport { executeWidgetsCallback } from '@/utils/litegraphUtil'\n\nconst mediaActions = useMediaAssetActions()\n\nconst { runButtonClick, selectedItem, selectedOutput } = defineProps<{\n latentPreview?: string\n runButtonClick?: (e: Event) => void\n selectedItem?: AssetItem\n selectedOutput?: ResultItemImpl\n mobile?: boolean\n}>()\n\nconst dateOptions = {\n month: 'short',\n day: 'numeric',\n year: 'numeric'\n} as const\nconst timeOptions = {\n hour: 'numeric',\n minute: 'numeric',\n second: 'numeric'\n} as const\n\nfunction formatTime(time: string) {\n if (!time) return ''\n const date = new Date(time)\n return `${d(date, dateOptions)} | ${d(date, timeOptions)}`\n}\n\nconst itemStats = computed<StatItem[]>(() => {\n if (!selectedItem) return []\n const user_metadata = getOutputAssetMetadata(selectedItem.user_metadata)\n if (!user_metadata) return []\n\n const { allOutputs } = user_metadata\n return [\n { content: formatTime(selectedItem.created_at) },\n { content: formatDuration(user_metadata.executionTimeInSeconds) },\n allOutputs && { content: t('g.asset', allOutputs.length) },\n (selectedOutput && mediaTypes[getMediaType(selectedOutput)]) ?? {}\n ].filter((i) => !!i)\n})\n\nfunction downloadAsset(item?: AssetItem) {\n const user_metadata = getOutputAssetMetadata(item?.user_metadata)\n for (const output of user_metadata?.allOutputs ?? [])\n downloadFile(output.url, output.filename)\n}\n\nasync function loadWorkflow(item: AssetItem | undefined) {\n if (!item) return\n const { workflow } = await extractWorkflowFromAsset(item)\n if (!workflow) return\n\n if (workflow.id !== app.rootGraph.id) return app.loadGraphData(workflow)\n //update graph to new version, set old to top of undo queue\n const changeTracker = useWorkflowStore().activeWorkflow?.changeTracker\n if (!changeTracker) return app.loadGraphData(workflow)\n changeTracker.redoQueue = []\n changeTracker.updateState([workflow], changeTracker.undoQueue)\n}\n\nasync function rerun(e: Event) {\n if (!runButtonClick) return\n await loadWorkflow(selectedItem)\n //FIXME don't use timeouts here\n //Currently seeds fail to properly update even with timeouts?\n await new Promise((r) => setTimeout(r, 500))\n executeWidgetsCallback(collectAllNodes(app.rootGraph), 'afterQueued')\n\n runButtonClick(e)\n}\n</script>\n<template>\n <section\n v-if=\"selectedItem\"\n data-testid=\"linear-output-info\"\n class=\"flex flex-wrap gap-2 p-1 w-full md:z-10 tabular-nums justify-between text-sm\"\n >\n <div class=\"flex gap-3 text-nowrap\">\n <div\n v-for=\"({ content, iconClass }, index) in itemStats\"\n :key=\"index\"\n class=\"flex items-center justify-items-center gap-1 tabular-nums\"\n >\n <i v-if=\"iconClass\" :class=\"iconClass\" />\n {{ content }}\n </div>\n </div>\n <div class=\"flex gap-3 justify-self-end\">\n <Button size=\"md\" @click=\"rerun\">\n {{ t('linearMode.rerun') }}\n <i class=\"icon-[lucide--refresh-cw]\" />\n </Button>\n <Button size=\"md\" @click=\"() => loadWorkflow(selectedItem)\">\n {{ t('linearMode.reuseParameters') }}\n <i class=\"icon-[lucide--list-restart]\" />\n </Button>\n <div class=\"border-r border-border-subtle mx-1\" />\n <Button\n size=\"icon\"\n @click=\"\n () => {\n if (selectedOutput?.url) downloadFile(selectedOutput.url)\n }\n \"\n >\n <i class=\"icon-[lucide--download]\" />\n </Button>\n <Popover\n :entries=\"[\n [\n {\n icon: 'icon-[lucide--download]',\n label: t('linearMode.downloadAll'),\n action: () => downloadAsset(selectedItem!)\n }\n ],\n [\n {\n icon: 'icon-[lucide--trash-2]',\n label: t('queue.jobMenu.deleteAsset'),\n action: () => mediaActions.confirmDelete(selectedItem!)\n }\n ]\n ]\"\n />\n </div>\n </section>\n <ImagePreview\n v-if=\"latentPreview ?? getMediaType(selectedOutput) === 'images'\"\n :mobile\n :src=\"latentPreview ?? selectedOutput!.url\"\n />\n <VideoPreview\n v-else-if=\"getMediaType(selectedOutput) === 'video'\"\n :src=\"selectedOutput!.url\"\n class=\"object-contain flex-1 md:contain-size\"\n />\n <audio\n v-else-if=\"getMediaType(selectedOutput) === 'audio'\"\n class=\"w-full m-auto\"\n controls\n :src=\"selectedOutput!.url\"\n />\n <article\n v-else-if=\"getMediaType(selectedOutput) === 'text'\"\n class=\"w-full max-w-128 m-auto my-12 overflow-y-auto\"\n v-text=\"selectedOutput!.url\"\n />\n <Preview3d\n v-else-if=\"getMediaType(selectedOutput) === '3d'\"\n :model-url=\"selectedOutput!.url\"\n />\n <img\n v-else\n class=\"pointer-events-none flex-1 max-h-full md:contain-size brightness-50 opacity-10\"\n src=\"/assets/images/comfy-logo-mono.svg\"\n />\n</template>\n","<script setup lang=\"ts\">\nimport { useEventListener, useInfiniteScroll, useScroll } from '@vueuse/core'\nimport { computed, ref, toRaw, useTemplateRef, watch } from 'vue'\n\nimport ModeToggle from '@/components/sidebar/ModeToggle.vue'\nimport SidebarIcon from '@/components/sidebar/SidebarIcon.vue'\nimport SidebarTemplatesButton from '@/components/sidebar/SidebarTemplatesButton.vue'\nimport WorkflowsSidebarTab from '@/components/sidebar/tabs/WorkflowsSidebarTab.vue'\nimport Button from '@/components/ui/button/Button.vue'\nimport { CanvasPointer } from '@/lib/litegraph/src/CanvasPointer'\nimport { useMediaAssets } from '@/platform/assets/composables/media/useMediaAssets'\nimport { getOutputAssetMetadata } from '@/platform/assets/schemas/assetMetadataSchema'\nimport type { AssetItem } from '@/platform/assets/schemas/assetSchema'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport {\n getMediaType,\n mediaTypes\n} from '@/renderer/extensions/linearMode/mediaTypes'\nimport { useQueueStore } from '@/stores/queueStore'\nimport type { ResultItemImpl } from '@/stores/queueStore'\nimport { useWorkspaceStore } from '@/stores/workspaceStore'\nimport { cn } from '@/utils/tailwindUtil'\n\nconst displayWorkflows = ref(false)\nconst outputs = useMediaAssets('output')\nconst queueStore = useQueueStore()\nconst settingStore = useSettingStore()\n\nconst workflowTab = useWorkspaceStore()\n .getSidebarTabs()\n .find((w) => w.id === 'workflows')\n\nvoid outputs.fetchMediaList()\n\ndefineProps<{\n scrollResetButtonTo?: string | HTMLElement\n mobile?: boolean\n}>()\nconst emit = defineEmits<{\n updateSelection: [\n selection: [AssetItem | undefined, ResultItemImpl | undefined, boolean]\n ]\n}>()\n\ndefineExpose({ onWheel })\n\nconst selectedIndex = ref<[number, number]>([-1, 0])\n\nwatch(selectedIndex, () => {\n const [index] = selectedIndex.value\n emit('updateSelection', [\n outputs.media.value[index],\n selectedOutput.value,\n selectedIndex.value[0] <= 0\n ])\n})\n\nconst outputsRef = useTemplateRef('outputsRef')\nconst { reset: resetInfiniteScroll } = useInfiniteScroll(\n outputsRef,\n outputs.loadMore,\n { canLoadMore: () => outputs.hasMore.value }\n)\nfunction resetOutputsScroll() {\n //TODO need to also prune outputs entries?\n resetInfiniteScroll()\n outputsRef.value?.scrollTo(0, 0)\n}\nconst { y: outputScrollState } = useScroll(outputsRef)\n\nwatch(selectedIndex, () => {\n const [index, key] = selectedIndex.value\n if (!outputsRef.value) return\n\n const outputElement = outputsRef.value?.children?.[index]?.children?.[key]\n if (!outputElement) return\n\n //container: 'nearest' is nice, but bleeding edge and chrome only\n outputElement.scrollIntoView({ block: 'nearest' })\n})\n\nfunction allOutputs(item?: AssetItem) {\n const user_metadata = getOutputAssetMetadata(item?.user_metadata)\n if (!user_metadata?.allOutputs) return []\n\n return user_metadata.allOutputs\n}\n\nconst selectedOutput = computed(() => {\n const [index, key] = selectedIndex.value\n if (index < 0) return undefined\n\n const output = allOutputs(outputs.media.value[index])[key]\n if (output) return output\n\n return allOutputs(outputs.media.value[0])[0]\n})\n\nwatch(\n () => outputs.media.value,\n (newAssets, oldAssets) => {\n if (newAssets.length === oldAssets.length || oldAssets.length === 0) return\n if (selectedIndex.value[0] <= 0) {\n //force update\n selectedIndex.value = [0, 0]\n return\n }\n\n const oldId = toRaw(oldAssets[selectedIndex.value[0]]?.id)\n const newIndex = toRaw(newAssets).findIndex((asset) => asset?.id === oldId)\n\n if (newIndex === -1) selectedIndex.value = [0, 0]\n else selectedIndex.value = [newIndex, selectedIndex.value[1]]\n }\n)\n\nfunction gotoNextOutput() {\n const [index, key] = selectedIndex.value\n if (index < 0 || key < 0) {\n selectedIndex.value = [0, 0]\n return\n }\n const currentItem = outputs.media.value[index]\n if (allOutputs(currentItem)[key + 1]) {\n selectedIndex.value = [index, key + 1]\n return\n }\n if (outputs.media.value[index + 1]) {\n selectedIndex.value = [index + 1, 0]\n }\n //do nothing, no next output\n}\n\nfunction gotoPreviousOutput() {\n const [index, key] = selectedIndex.value\n if (key > 0) {\n selectedIndex.value = [index, key - 1]\n return\n }\n\n if (index > 0) {\n const currentItem = outputs.media.value[index - 1]\n selectedIndex.value = [index - 1, allOutputs(currentItem).length - 1]\n return\n }\n\n selectedIndex.value = [0, 0]\n}\n\nlet pointer = new CanvasPointer(document.body)\nlet scrollOffset = 0\nfunction onWheel(e: WheelEvent) {\n if (!e.ctrlKey && !e.metaKey) return\n e.preventDefault()\n e.stopPropagation()\n\n if (!pointer.isTrackpadGesture(e)) {\n if (e.deltaY > 0) gotoNextOutput()\n else gotoPreviousOutput()\n return\n }\n scrollOffset += e.deltaY\n while (scrollOffset >= 60) {\n scrollOffset -= 60\n gotoNextOutput()\n }\n while (scrollOffset <= -60) {\n scrollOffset += 60\n gotoPreviousOutput()\n }\n}\n\nuseEventListener(document.body, 'keydown', (e: KeyboardEvent) => {\n if (\n (e.key !== 'ArrowDown' && e.key !== 'ArrowUp') ||\n e.target instanceof HTMLTextAreaElement ||\n e.target instanceof HTMLInputElement\n )\n return\n\n e.preventDefault()\n e.stopPropagation()\n if (e.key === 'ArrowDown') gotoNextOutput()\n else gotoPreviousOutput()\n})\n</script>\n<template>\n <div\n :class=\"\n cn(\n 'min-w-38 flex bg-comfy-menu-bg md:h-full border-border-subtle',\n settingStore.get('Comfy.Sidebar.Location') === 'right'\n ? 'flex-row-reverse border-l'\n : 'md:border-r'\n )\n \"\n v-bind=\"$attrs\"\n >\n <div\n v-if=\"!mobile\"\n class=\"h-full flex flex-col w-14 shrink-0 overflow-hidden items-center p-2\"\n >\n <template v-if=\"workflowTab\">\n <SidebarIcon\n :icon=\"workflowTab.icon\"\n :icon-badge=\"workflowTab.iconBadge\"\n :tooltip=\"workflowTab.tooltip\"\n :label=\"workflowTab.label || workflowTab.title\"\n :class=\"workflowTab.id + '-tab-button'\"\n :selected=\"displayWorkflows\"\n :is-small=\"settingStore.get('Comfy.Sidebar.Size') === 'small'\"\n @click=\"displayWorkflows = !displayWorkflows\"\n />\n </template>\n <SidebarTemplatesButton />\n <div class=\"flex-1\" />\n <ModeToggle />\n </div>\n <div class=\"border-border-subtle md:border-r\" />\n <WorkflowsSidebarTab v-if=\"displayWorkflows\" class=\"min-w-50 grow-1\" />\n <article\n v-else\n ref=\"outputsRef\"\n data-testid=\"linear-outputs\"\n class=\"h-24 md:h-full min-w-24 grow-1 p-3 overflow-x-auto overflow-y-clip md:overflow-y-auto md:overflow-x-clip md:border-r-1 border-node-component-border flex md:flex-col items-center contain-size\"\n >\n <section\n v-if=\"\n queueStore.runningTasks.length > 0 ||\n queueStore.pendingTasks.length > 0\n \"\n data-testid=\"linear-job\"\n class=\"py-3 not-md:h-24 md:w-full aspect-square px-1 relative\"\n >\n <i\n v-if=\"queueStore.runningTasks.length > 0\"\n class=\"icon-[lucide--loader-circle] size-full animate-spin\"\n />\n <i v-else class=\"icon-[lucide--ellipsis] size-full animate-pulse\" />\n <div\n v-if=\"\n queueStore.runningTasks.length + queueStore.pendingTasks.length > 1\n \"\n class=\"absolute top-0 right-0 p-1 min-w-5 h-5 flex justify-center items-center rounded-full bg-primary-background text-text-primary\"\n v-text=\"\n queueStore.runningTasks.length + queueStore.pendingTasks.length\n \"\n />\n </section>\n <template v-for=\"(item, index) in outputs.media.value\" :key=\"index\">\n <div\n class=\"border-border-subtle not-md:border-l md:border-t first:border-none not-md:h-21 md:w-full m-3\"\n />\n <template v-for=\"(output, key) in allOutputs(item)\" :key>\n <img\n v-if=\"getMediaType(output) === 'images'\"\n :class=\"\n cn(\n 'p-1 rounded-lg aspect-square object-cover not-md:h-20 md:w-full',\n index === selectedIndex[0] &&\n key === selectedIndex[1] &&\n 'border-2'\n )\n \"\n :src=\"output.url\"\n @click=\"selectedIndex = [index, key]\"\n />\n <div\n v-else\n :class=\"\n cn(\n 'p-1 rounded-lg aspect-square w-full',\n index === selectedIndex[0] &&\n key === selectedIndex[1] &&\n 'border-2'\n )\n \"\n @click=\"selectedIndex = [index, key]\"\n >\n <i\n :class=\"\n cn(mediaTypes[getMediaType(output)]?.iconClass, 'size-full')\n \"\n />\n </div>\n </template>\n </template>\n </article>\n </div>\n <Teleport\n v-if=\"outputScrollState && scrollResetButtonTo\"\n :to=\"scrollResetButtonTo\"\n >\n <Button\n :class=\"\n cn(\n 'p-3 size-10 bg-base-foreground',\n settingStore.get('Comfy.Sidebar.Location') === 'left'\n ? 'left-4'\n : 'right-4'\n )\n \"\n @click=\"resetOutputsScroll\"\n >\n <i class=\"icon-[lucide--arrow-up] size-4 bg-base-background\" />\n </Button>\n </Teleport>\n</template>\n","<script setup lang=\"ts\">\nimport {\n breakpointsTailwind,\n unrefElement,\n useBreakpoints,\n whenever\n} from '@vueuse/core'\nimport Splitter from 'primevue/splitter'\nimport SplitterPanel from 'primevue/splitterpanel'\nimport { ref, useTemplateRef } from 'vue'\n\nimport TopbarBadges from '@/components/topbar/TopbarBadges.vue'\nimport WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'\nimport TypeformPopoverButton from '@/components/ui/TypeformPopoverButton.vue'\nimport { t } from '@/i18n'\nimport type { AssetItem } from '@/platform/assets/schemas/assetSchema'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport LinearControls from '@/renderer/extensions/linearMode/LinearControls.vue'\nimport LinearPreview from '@/renderer/extensions/linearMode/LinearPreview.vue'\nimport OutputHistory from '@/renderer/extensions/linearMode/OutputHistory.vue'\nimport { useNodeOutputStore } from '@/stores/imagePreviewStore'\nimport type { ResultItemImpl } from '@/stores/queueStore'\n\nconst nodeOutputStore = useNodeOutputStore()\nconst settingStore = useSettingStore()\n\nconst mobileDisplay = useBreakpoints(breakpointsTailwind).smaller('md')\n\nconst hasPreview = ref(false)\nwhenever(\n () => nodeOutputStore.latestPreview[0],\n () => (hasPreview.value = true)\n)\n\nconst selectedItem = ref<AssetItem>()\nconst selectedOutput = ref<ResultItemImpl>()\nconst canShowPreview = ref(true)\nconst outputHistoryRef = useTemplateRef('outputHistoryRef')\n\nconst topLeftRef = useTemplateRef('topLeftRef')\nconst topRightRef = useTemplateRef('topRightRef')\nconst bottomLeftRef = useTemplateRef('bottomLeftRef')\nconst bottomRightRef = useTemplateRef('bottomRightRef')\nconst linearWorkflowRef = useTemplateRef('linearWorkflowRef')\n</script>\n<template>\n <div\n class=\"absolute w-full h-full\"\n @wheel.capture=\"(e: WheelEvent) => outputHistoryRef?.onWheel(e)\"\n >\n <div class=\"workflow-tabs-container pointer-events-auto h-9.5 w-full\">\n <div class=\"flex h-full items-center\">\n <WorkflowTabs />\n <TopbarBadges />\n </div>\n </div>\n <div\n v-if=\"mobileDisplay\"\n class=\"justify-center border-border-subtle border-t overflow-y-scroll h-[calc(100%-38px)] bg-comfy-menu-bg\"\n >\n <div class=\"flex flex-col text-muted-foreground\">\n <LinearPreview\n :latent-preview=\"\n canShowPreview && hasPreview\n ? nodeOutputStore.latestPreview[0]\n : undefined\n \"\n :run-button-click=\"linearWorkflowRef?.runButtonClick\"\n :selected-item\n :selected-output\n mobile\n />\n </div>\n <OutputHistory\n ref=\"outputHistoryRef\"\n mobile\n @update-selection=\"\n ([item, output, canShow]) => {\n selectedItem = item\n selectedOutput = output\n canShowPreview = canShow\n hasPreview = false\n }\n \"\n />\n <LinearControls ref=\"linearWorkflowRef\" mobile />\n <div class=\"text-base-foreground flex items-center gap-4 justify-end m-4\">\n <a\n href=\"https://form.typeform.com/to/gmVqFi8l\"\n v-text=\"t('linearMode.beta')\"\n />\n <TypeformPopoverButton data-tf-widget=\"gmVqFi8l\" />\n </div>\n </div>\n <Splitter\n v-else\n class=\"h-[calc(100%-38px)] w-full bg-comfy-menu-secondary-bg\"\n :pt=\"{ gutter: { class: 'bg-transparent w-4 -mx-3' } }\"\n @resizestart=\"({ originalEvent }) => originalEvent.preventDefault()\"\n >\n <SplitterPanel\n id=\"linearLeftPanel\"\n :size=\"1\"\n class=\"min-w-min outline-none\"\n >\n <OutputHistory\n v-if=\"settingStore.get('Comfy.Sidebar.Location') === 'left'\"\n ref=\"outputHistoryRef\"\n :scroll-reset-button-to=\"unrefElement(bottomLeftRef) ?? undefined\"\n @update-selection=\"\n ([item, output, canShow]) => {\n selectedItem = item\n selectedOutput = output\n canShowPreview = canShow\n hasPreview = false\n }\n \"\n />\n <LinearControls\n v-else\n ref=\"linearWorkflowRef\"\n :toast-to=\"unrefElement(bottomLeftRef) ?? undefined\"\n :notes-to=\"unrefElement(topLeftRef) ?? undefined\"\n />\n <div />\n </SplitterPanel>\n <SplitterPanel\n id=\"linearCenterPanel\"\n :size=\"98\"\n class=\"flex flex-col min-w-min gap-4 mx-2 px-10 pt-8 pb-4 relative text-muted-foreground outline-none\"\n >\n <LinearPreview\n :latent-preview=\"\n canShowPreview && hasPreview\n ? nodeOutputStore.latestPreview[0]\n : undefined\n \"\n :run-button-click=\"linearWorkflowRef?.runButtonClick\"\n :selected-item\n :selected-output\n />\n <div ref=\"topLeftRef\" class=\"absolute z-21 top-4 left-4\" />\n <div ref=\"topRightRef\" class=\"absolute z-21 top-4 right-4\" />\n <div ref=\"bottomLeftRef\" class=\"absolute z-20 bottom-4 left-4\" />\n <div ref=\"bottomRightRef\" class=\"absolute z-20 bottom-24 right-4\" />\n <div\n class=\"absolute z-20 bottom-4 right-4 text-base-foreground flex items-center gap-4\"\n >\n <div v-text=\"t('linearMode.beta')\" />\n <TypeformPopoverButton\n data-tf-widget=\"gmVqFi8l\"\n :align=\"\n settingStore.get('Comfy.Sidebar.Location') === 'left'\n ? 'end'\n : 'start'\n \"\n />\n </div>\n </SplitterPanel>\n <SplitterPanel\n id=\"linearRightPanel\"\n :size=\"1\"\n class=\"min-w-min outline-none\"\n >\n <LinearControls\n v-if=\"settingStore.get('Comfy.Sidebar.Location') === 'left'\"\n ref=\"linearWorkflowRef\"\n :toast-to=\"unrefElement(bottomRightRef) ?? undefined\"\n :notes-to=\"unrefElement(topRightRef) ?? undefined\"\n />\n <OutputHistory\n v-else\n ref=\"outputHistoryRef\"\n :scroll-reset-button-to=\"unrefElement(bottomRightRef) ?? undefined\"\n @update-selection=\"\n ([item, output, canShow]) => {\n selectedItem = item\n selectedOutput = output\n canShowPreview = canShow\n hasPreview = false\n }\n \"\n />\n <div />\n </SplitterPanel>\n </Splitter>\n </div>\n</template>\n","<script setup lang=\"ts\">\nimport { useEventListener, useScroll, whenever } from '@vueuse/core'\nimport Panel from 'primevue/panel'\nimport TabMenu from 'primevue/tabmenu'\nimport { computed, onBeforeUnmount, onMounted, ref } from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport DotSpinner from '@/components/common/DotSpinner.vue'\nimport HoneyToast from '@/components/honeyToast/HoneyToast.vue'\nimport Button from '@/components/ui/button/Button.vue'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useWorkflowService } from '@/platform/workflow/core/services/workflowService'\nimport { api } from '@/scripts/api'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { useConflictDetection } from '@/workbench/extensions/manager/composables/useConflictDetection'\nimport { useComfyManagerService } from '@/workbench/extensions/manager/services/comfyManagerService'\nimport { useComfyManagerStore } from '@/workbench/extensions/manager/stores/comfyManagerStore'\n\nconst { t } = useI18n()\nconst comfyManagerStore = useComfyManagerStore()\nconst settingStore = useSettingStore()\nconst { runFullConflictAnalysis } = useConflictDetection()\n\nconst isExpanded = ref(false)\nconst activeTabIndex = ref(0)\n\nconst tabs = computed(() => [\n { label: t('manager.installationQueue') },\n {\n label: t('manager.failed', {\n count: comfyManagerStore.failedTasksIds.length\n })\n }\n])\n\nconst focusedLogs = computed(() => {\n if (activeTabIndex.value === 0) {\n return comfyManagerStore.succeededTasksLogs\n }\n return comfyManagerStore.failedTasksLogs\n})\n\nconst visible = computed(() => comfyManagerStore.taskLogs.length > 0)\n\nconst isRestarting = ref(false)\nconst isRestartCompleted = ref(false)\n\nconst isInProgress = computed(\n () => comfyManagerStore.isProcessingTasks || isRestarting.value\n)\n\nconst isTaskInProgress = (index: number) => {\n const log = focusedLogs.value[index]\n if (!log) return false\n\n const taskQueue = comfyManagerStore.taskQueue\n if (!taskQueue) return false\n\n const allQueueTasks = [\n ...(taskQueue.running_queue || []),\n ...(taskQueue.pending_queue || [])\n ]\n\n return allQueueTasks.some((task) => task.ui_id === log.taskId)\n}\n\nconst completedTasksCount = computed(() => {\n return (\n comfyManagerStore.succeededTasksIds.length +\n comfyManagerStore.failedTasksIds.length\n )\n})\n\nconst totalTasksCount = computed(() => {\n const completedTasks = Object.keys(comfyManagerStore.taskHistory).length\n const taskQueue = comfyManagerStore.taskQueue\n const queuedTasks = taskQueue\n ? (taskQueue.running_queue?.length || 0) +\n (taskQueue.pending_queue?.length || 0)\n : 0\n return completedTasks + queuedTasks\n})\n\nconst currentTaskName = computed(() => {\n if (isRestarting.value) {\n return t('manager.restartingBackend')\n }\n if (isRestartCompleted.value) {\n return t('manager.extensionsSuccessfullyInstalled')\n }\n if (!comfyManagerStore.taskLogs.length)\n return t('manager.installingDependencies')\n const task = comfyManagerStore.taskLogs.at(-1)\n return task?.taskName ?? t('manager.installingDependencies')\n})\n\nconst collapsedPanels = ref<Record<number, boolean>>({})\nfunction togglePanel(index: number) {\n collapsedPanels.value[index] = !collapsedPanels.value[index]\n}\n\nconst sectionsContainerRef = ref<HTMLElement | null>(null)\nconst { y: scrollY } = useScroll(sectionsContainerRef, {\n eventListenerOptions: { passive: true }\n})\n\nconst lastPanelRef = ref<HTMLElement | null>(null)\nconst isUserScrolling = ref(false)\nconst lastPanelLogs = computed(() => focusedLogs.value?.at(-1)?.logs)\n\nfunction isAtBottom(el: HTMLElement | null) {\n if (!el) return false\n const threshold = 20\n return Math.abs(el.scrollHeight - el.scrollTop - el.clientHeight) < threshold\n}\n\nfunction scrollLastPanelToBottom() {\n if (!lastPanelRef.value || isUserScrolling.value) return\n lastPanelRef.value.scrollTop = lastPanelRef.value.scrollHeight\n}\n\nfunction scrollContentToBottom() {\n scrollY.value = sectionsContainerRef.value?.scrollHeight ?? 0\n}\n\nfunction resetUserScrolling() {\n isUserScrolling.value = false\n}\n\nfunction handleScroll(e: Event) {\n const target = e.target as HTMLElement\n if (target !== lastPanelRef.value) return\n isUserScrolling.value = !isAtBottom(target)\n}\n\nfunction onLogsAdded() {\n if (isUserScrolling.value) return\n scrollLastPanelToBottom()\n}\n\nwhenever(lastPanelLogs, onLogsAdded, { flush: 'post', deep: true })\nwhenever(() => isExpanded.value, scrollContentToBottom)\nwhenever(() => !isExpanded.value, resetUserScrolling)\n\nfunction closeToast() {\n comfyManagerStore.resetTaskState()\n isExpanded.value = false\n}\n\nasync function handleRestart() {\n const originalToastSetting = settingStore.get(\n 'Comfy.Toast.DisableReconnectingToast'\n )\n\n try {\n await settingStore.set('Comfy.Toast.DisableReconnectingToast', true)\n\n isRestarting.value = true\n\n const onReconnect = async () => {\n try {\n comfyManagerStore.setStale()\n await useCommandStore().execute('Comfy.RefreshNodeDefinitions')\n await useWorkflowService().reloadCurrentWorkflow()\n void runFullConflictAnalysis()\n } finally {\n await settingStore.set(\n 'Comfy.Toast.DisableReconnectingToast',\n originalToastSetting\n )\n isRestarting.value = false\n isRestartCompleted.value = true\n\n setTimeout(() => {\n closeToast()\n }, 3000)\n }\n }\n\n useEventListener(api, 'reconnected', onReconnect, { once: true })\n\n await useComfyManagerService().rebootComfyUI()\n } catch (error) {\n await settingStore.set(\n 'Comfy.Toast.DisableReconnectingToast',\n originalToastSetting\n )\n isRestarting.value = false\n isRestartCompleted.value = false\n closeToast()\n throw error\n }\n}\n\nonMounted(() => {\n scrollContentToBottom()\n})\n\nonBeforeUnmount(() => {\n isExpanded.value = false\n})\n</script>\n\n<template>\n <HoneyToast v-model:expanded=\"isExpanded\" :visible>\n <template #default>\n <div v-if=\"isExpanded\" class=\"flex items-center px-4 py-2\">\n <TabMenu\n v-model:active-index=\"activeTabIndex\"\n :model=\"tabs\"\n class=\"w-full border-none\"\n :pt=\"{\n menu: { class: 'border-none' },\n menuitem: { class: 'font-medium' },\n action: { class: 'px-4 py-2' }\n }\"\n />\n </div>\n\n <div\n ref=\"sectionsContainerRef\"\n class=\"scroll-container max-h-[450px] overflow-y-auto px-6 py-4\"\n :style=\"{\n scrollbarWidth: 'thin',\n scrollbarColor: 'rgba(156, 163, 175, 0.5) transparent'\n }\"\n >\n <div v-for=\"(log, index) in focusedLogs\" :key=\"index\">\n <Panel\n :expanded=\"collapsedPanels[index] === true\"\n toggleable\n class=\"shadow-elevation-1 mt-2 rounded-lg\"\n >\n <template #header>\n <div class=\"flex w-full items-center justify-between py-2\">\n <div class=\"flex flex-col text-sm leading-normal font-medium\">\n <span>{{ log.taskName }}</span>\n <span class=\"text-muted\">\n {{\n isTaskInProgress(index)\n ? t('g.inProgress')\n : t('g.completed') + ' ✓'\n }}\n </span>\n </div>\n </div>\n </template>\n <template #toggleicon>\n <Button\n variant=\"textonly\"\n class=\"text-neutral-300\"\n @click=\"togglePanel(index)\"\n >\n <i\n :class=\"\n collapsedPanels[index]\n ? 'pi pi-chevron-right'\n : 'pi pi-chevron-down'\n \"\n />\n </Button>\n </template>\n <div\n :ref=\"\n index === focusedLogs.length - 1\n ? (el) => (lastPanelRef = el as HTMLElement)\n : undefined\n \"\n class=\"h-64 overflow-y-auto rounded-lg bg-black\"\n :class=\"{\n 'h-64': index !== focusedLogs.length - 1,\n grow: index === focusedLogs.length - 1\n }\"\n @scroll=\"handleScroll\"\n >\n <div class=\"h-full\">\n <div\n v-for=\"(logLine, logIndex) in log.logs\"\n :key=\"logIndex\"\n class=\"text-muted\"\n >\n <pre class=\"break-words whitespace-pre-wrap\">{{\n logLine\n }}</pre>\n </div>\n </div>\n </div>\n </Panel>\n </div>\n </div>\n </template>\n\n <template #footer=\"{ toggle }\">\n <div class=\"flex w-full items-center justify-between px-6 py-2 shadow-lg\">\n <div class=\"flex items-center text-base leading-none\">\n <div class=\"flex items-center\">\n <template v-if=\"isInProgress\">\n <DotSpinner duration=\"1s\" class=\"mr-2\" />\n <span>{{ currentTaskName }}</span>\n </template>\n <template v-else-if=\"isRestartCompleted\">\n <span class=\"mr-2\">🎉</span>\n <span>{{ currentTaskName }}</span>\n </template>\n <template v-else>\n <span class=\"mr-2\">✅</span>\n <span>{{ t('manager.restartToApplyChanges') }}</span>\n </template>\n </div>\n </div>\n <div class=\"flex items-center gap-4\">\n <span v-if=\"isInProgress\" class=\"text-sm text-muted-foreground\">\n {{ completedTasksCount }} {{ t('g.progressCountOf') }}\n {{ totalTasksCount }}\n </span>\n <div class=\"flex items-center\">\n <Button\n v-if=\"!isInProgress && !isRestartCompleted\"\n variant=\"secondary\"\n class=\"mr-4 rounded-full border-2 border-base-foreground px-3 text-base-foreground hover:bg-secondary-background-hover\"\n @click=\"handleRestart\"\n >\n {{ t('manager.applyChanges') }}\n </Button>\n <Button\n v-else-if=\"!isRestartCompleted\"\n variant=\"muted-textonly\"\n size=\"sm\"\n class=\"rounded-full font-bold\"\n :aria-label=\"\n t(isExpanded ? 'contextMenu.Collapse' : 'contextMenu.Expand')\n \"\n @click.stop=\"toggle\"\n >\n <i\n :class=\"isExpanded ? 'pi pi-chevron-up' : 'pi pi-chevron-down'\"\n />\n </Button>\n <Button\n variant=\"muted-textonly\"\n size=\"sm\"\n class=\"rounded-full font-bold\"\n :aria-label=\"t('g.close')\"\n @click.stop=\"closeToast\"\n >\n <i class=\"pi pi-times\" />\n </Button>\n </div>\n </div>\n </div>\n </template>\n </HoneyToast>\n</template>\n","<template>\n <div class=\"comfyui-body grid h-full w-full overflow-hidden\">\n <div id=\"comfyui-body-top\" class=\"comfyui-body-top\" />\n <div id=\"comfyui-body-bottom\" class=\"comfyui-body-bottom\" />\n <div id=\"comfyui-body-left\" class=\"comfyui-body-left\" />\n <div id=\"comfyui-body-right\" class=\"comfyui-body-right\" />\n <div\n v-show=\"!linearMode\"\n id=\"graph-canvas-container\"\n ref=\"graphCanvasContainerRef\"\n class=\"graph-canvas-container\"\n >\n <GraphCanvas @ready=\"onGraphReady\" />\n </div>\n <LinearView v-if=\"linearMode\" />\n </div>\n\n <GlobalToast />\n <RerouteMigrationToast />\n <ModelImportProgressDialog />\n <ManagerProgressToast />\n <UnloadWindowConfirmDialog v-if=\"!isElectron()\" />\n <MenuHamburger />\n</template>\n\n<script setup lang=\"ts\">\nimport { useEventListener } from '@vueuse/core'\nimport { storeToRefs } from 'pinia'\nimport type { ToastMessageOptions } from 'primevue/toast'\nimport { useToast } from 'primevue/usetoast'\nimport {\n computed,\n nextTick,\n onBeforeUnmount,\n onMounted,\n ref,\n watch,\n watchEffect\n} from 'vue'\nimport { useI18n } from 'vue-i18n'\n\nimport { runWhenGlobalIdle } from '@/base/common/async'\nimport MenuHamburger from '@/components/MenuHamburger.vue'\nimport UnloadWindowConfirmDialog from '@/components/dialog/UnloadWindowConfirmDialog.vue'\nimport GraphCanvas from '@/components/graph/GraphCanvas.vue'\nimport GlobalToast from '@/components/toast/GlobalToast.vue'\nimport RerouteMigrationToast from '@/components/toast/RerouteMigrationToast.vue'\nimport { useBrowserTabTitle } from '@/composables/useBrowserTabTitle'\nimport { useCoreCommands } from '@/composables/useCoreCommands'\nimport { useErrorHandling } from '@/composables/useErrorHandling'\nimport { useProgressFavicon } from '@/composables/useProgressFavicon'\nimport { SERVER_CONFIG_ITEMS } from '@/constants/serverConfig'\nimport { i18n, loadLocale } from '@/i18n'\nimport ModelImportProgressDialog from '@/platform/assets/components/ModelImportProgressDialog.vue'\nimport { isCloud } from '@/platform/distribution/types'\nimport { useSettingStore } from '@/platform/settings/settingStore'\nimport { useTelemetry } from '@/platform/telemetry'\nimport { useFrontendVersionMismatchWarning } from '@/platform/updates/common/useFrontendVersionMismatchWarning'\nimport { useVersionCompatibilityStore } from '@/platform/updates/common/versionCompatibilityStore'\nimport { useCanvasStore } from '@/renderer/core/canvas/canvasStore'\nimport type { StatusWsMessageStatus } from '@/schemas/apiSchema'\nimport { api } from '@/scripts/api'\nimport { app } from '@/scripts/app'\nimport { setupAutoQueueHandler } from '@/services/autoQueueService'\nimport { useKeybindingService } from '@/services/keybindingService'\nimport { useAssetsStore } from '@/stores/assetsStore'\nimport { useCommandStore } from '@/stores/commandStore'\nimport { useExecutionStore } from '@/stores/executionStore'\nimport { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'\nimport { useMenuItemStore } from '@/stores/menuItemStore'\nimport { useModelStore } from '@/stores/modelStore'\nimport { useNodeDefStore, useNodeFrequencyStore } from '@/stores/nodeDefStore'\nimport {\n useQueuePendingTaskCountStore,\n useQueueStore\n} from '@/stores/queueStore'\nimport { useServerConfigStore } from '@/stores/serverConfigStore'\nimport { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'\nimport { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore'\nimport { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'\nimport { useWorkspaceStore } from '@/stores/workspaceStore'\nimport { electronAPI, isElectron } from '@/utils/envUtil'\nimport LinearView from '@/views/LinearView.vue'\nimport ManagerProgressToast from '@/workbench/extensions/manager/components/ManagerProgressToast.vue'\n\nsetupAutoQueueHandler()\nuseProgressFavicon()\nuseBrowserTabTitle()\n\nconst { t } = useI18n()\nconst toast = useToast()\nconst settingStore = useSettingStore()\nconst executionStore = useExecutionStore()\nconst colorPaletteStore = useColorPaletteStore()\nconst queueStore = useQueueStore()\nconst assetsStore = useAssetsStore()\nconst versionCompatibilityStore = useVersionCompatibilityStore()\nconst graphCanvasContainerRef = ref<HTMLDivElement | null>(null)\nconst { linearMode } = storeToRefs(useCanvasStore())\n\nconst telemetry = useTelemetry()\nconst firebaseAuthStore = useFirebaseAuthStore()\nlet hasTrackedLogin = false\nlet visibilityListener: (() => void) | null = null\nlet tabCountInterval: number | null = null\nlet tabCountChannel: BroadcastChannel | null = null\n\nwatch(\n () => colorPaletteStore.completedActivePalette,\n (newTheme) => {\n const DARK_THEME_CLASS = 'dark-theme'\n if (newTheme.light_theme) {\n document.body.classList.remove(DARK_THEME_CLASS)\n } else {\n document.body.classList.add(DARK_THEME_CLASS)\n }\n\n if (isElectron()) {\n electronAPI().changeTheme({\n color: 'rgba(0, 0, 0, 0)',\n symbolColor: newTheme.colors.comfy_base['input-text']\n })\n }\n },\n { immediate: true }\n)\n\nif (isElectron()) {\n watch(\n () => queueStore.tasks,\n (newTasks, oldTasks) => {\n // Report tasks that previously running but are now completed (i.e. in history)\n const oldRunningTaskIds = new Set(\n oldTasks.filter((task) => task.isRunning).map((task) => task.promptId)\n )\n newTasks\n .filter(\n (task) => oldRunningTaskIds.has(task.promptId) && task.isHistory\n )\n .forEach((task) => {\n electronAPI().Events.incrementUserProperty(\n `execution:${task.displayStatus.toLowerCase()}`,\n 1\n )\n electronAPI().Events.trackEvent('execution', {\n status: task.displayStatus.toLowerCase()\n })\n })\n },\n { deep: true }\n )\n}\n\nwatchEffect(() => {\n const fontSize = settingStore.get('Comfy.TextareaWidget.FontSize')\n document.documentElement.style.setProperty(\n '--comfy-textarea-font-size',\n `${fontSize}px`\n )\n})\n\nwatchEffect(() => {\n const padding = settingStore.get('Comfy.TreeExplorer.ItemPadding')\n document.documentElement.style.setProperty(\n '--comfy-tree-explorer-item-padding',\n `${padding}px`\n )\n})\n\nwatchEffect(async () => {\n const locale = settingStore.get('Comfy.Locale')\n if (locale) {\n // Load the locale dynamically if not already loaded\n try {\n await loadLocale(locale)\n // Type assertion is safe here as loadLocale validates the locale exists\n i18n.global.locale.value = locale as typeof i18n.global.locale.value\n } catch (error) {\n console.error(`Failed to switch to locale \"${locale}\":`, error)\n }\n }\n})\n\nconst useNewMenu = computed(() => {\n return settingStore.get('Comfy.UseNewMenu')\n})\nwatchEffect(() => {\n if (useNewMenu.value === 'Disabled') {\n app.ui.menuContainer.style.setProperty('display', 'block')\n app.ui.restoreMenuPosition()\n } else {\n app.ui.menuContainer.style.setProperty('display', 'none')\n }\n})\n\nwatchEffect(() => {\n queueStore.maxHistoryItems = settingStore.get('Comfy.Queue.MaxHistoryItems')\n})\n\nconst init = () => {\n const coreCommands = useCoreCommands()\n useCommandStore().registerCommands(coreCommands)\n useMenuItemStore().registerCoreMenuCommands()\n useKeybindingService().registerCoreKeybindings()\n useSidebarTabStore().registerCoreSidebarTabs()\n useBottomPanelStore().registerCoreBottomPanelTabs()\n app.extensionManager = useWorkspaceStore()\n}\n\nconst queuePendingTaskCountStore = useQueuePendingTaskCountStore()\nconst sidebarTabStore = useSidebarTabStore()\n\nconst onStatus = async (e: CustomEvent<StatusWsMessageStatus>) => {\n queuePendingTaskCountStore.update(e)\n await queueStore.update()\n // Only update assets if the assets sidebar is currently open\n // When sidebar is closed, AssetsSidebarTab.vue will refresh on mount\n if (sidebarTabStore.activeSidebarTabId === 'assets' || linearMode.value) {\n await assetsStore.updateHistory()\n }\n}\n\nconst onExecutionSuccess = async () => {\n await queueStore.update()\n // Only update assets if the assets sidebar is currently open\n // When sidebar is closed, AssetsSidebarTab.vue will refresh on mount\n if (sidebarTabStore.activeSidebarTabId === 'assets') {\n await assetsStore.updateHistory()\n }\n}\n\nconst reconnectingMessage: ToastMessageOptions = {\n severity: 'error',\n summary: t('g.reconnecting')\n}\n\nconst onReconnecting = () => {\n if (!settingStore.get('Comfy.Toast.DisableReconnectingToast')) {\n toast.remove(reconnectingMessage)\n toast.add(reconnectingMessage)\n }\n}\n\nconst onReconnected = () => {\n if (!settingStore.get('Comfy.Toast.DisableReconnectingToast')) {\n toast.remove(reconnectingMessage)\n toast.add({\n severity: 'success',\n summary: t('g.reconnected'),\n life: 2000\n })\n }\n}\n\nonMounted(() => {\n api.addEventListener('status', onStatus)\n api.addEventListener('execution_success', onExecutionSuccess)\n api.addEventListener('reconnecting', onReconnecting)\n api.addEventListener('reconnected', onReconnected)\n executionStore.bindExecutionEvents()\n\n try {\n init()\n // Relocate the legacy menu container to the graph canvas container so it is below other elements\n graphCanvasContainerRef.value?.prepend(app.ui.menuContainer)\n } catch (e) {\n console.error('Failed to init ComfyUI frontend', e)\n }\n})\n\nonBeforeUnmount(() => {\n api.removeEventListener('status', onStatus)\n api.removeEventListener('execution_success', onExecutionSuccess)\n api.removeEventListener('reconnecting', onReconnecting)\n api.removeEventListener('reconnected', onReconnected)\n executionStore.unbindExecutionEvents()\n\n // Clean up page visibility listener\n if (visibilityListener) {\n document.removeEventListener('visibilitychange', visibilityListener)\n visibilityListener = null\n }\n\n // Clean up tab count tracking\n if (tabCountInterval) {\n window.clearInterval(tabCountInterval)\n tabCountInterval = null\n }\n if (tabCountChannel) {\n tabCountChannel.close()\n tabCountChannel = null\n }\n})\n\nuseEventListener(window, 'keydown', useKeybindingService().keybindHandler)\n\nconst { wrapWithErrorHandling, wrapWithErrorHandlingAsync } = useErrorHandling()\n\n// Initialize version mismatch warning in setup context\n// It will be triggered automatically when the store is ready\nuseFrontendVersionMismatchWarning({ immediate: true })\n\nvoid nextTick(() => {\n versionCompatibilityStore.initialize().catch((error) => {\n console.warn('Version compatibility check failed:', error)\n })\n})\n\nconst onGraphReady = () => {\n runWhenGlobalIdle(() => {\n // Track user login when app is ready in graph view (cloud only)\n if (isCloud && firebaseAuthStore.isAuthenticated && !hasTrackedLogin) {\n telemetry?.trackUserLoggedIn()\n hasTrackedLogin = true\n }\n\n // Set up page visibility tracking (cloud only)\n if (isCloud && telemetry && !visibilityListener) {\n visibilityListener = () => {\n telemetry.trackPageVisibilityChanged({\n visibility_state: document.visibilityState as 'visible' | 'hidden'\n })\n }\n document.addEventListener('visibilitychange', visibilityListener)\n }\n\n // Set up tab count tracking (cloud only)\n if (isCloud && telemetry && !tabCountInterval) {\n tabCountChannel = new BroadcastChannel('comfyui-tab-count')\n const activeTabs = new Map<string, number>()\n const currentTabId = crypto.randomUUID()\n\n // Listen for heartbeats from other tabs\n tabCountChannel.onmessage = (event) => {\n if (\n event.data.type === 'heartbeat' &&\n event.data.tabId !== currentTabId\n ) {\n activeTabs.set(event.data.tabId, Date.now())\n }\n }\n\n // 5-minute heartbeat interval\n tabCountInterval = window.setInterval(() => {\n const now = Date.now()\n\n // Clean up stale tabs (no heartbeat for 45 seconds)\n activeTabs.forEach((lastHeartbeat, tabId) => {\n if (now - lastHeartbeat > 45000) {\n activeTabs.delete(tabId)\n }\n })\n\n // Broadcast our heartbeat\n tabCountChannel?.postMessage({ type: 'heartbeat', tabId: currentTabId })\n\n // Track tab count (include current tab)\n const tabCount = activeTabs.size + 1\n telemetry.trackTabCount({ tab_count: tabCount })\n }, 60000 * 5)\n\n // Send initial heartbeat\n tabCountChannel.postMessage({ type: 'heartbeat', tabId: currentTabId })\n }\n\n // Setting values now available after comfyApp.setup.\n // Load keybindings.\n wrapWithErrorHandling(useKeybindingService().registerUserKeybindings)()\n\n // Load server config\n wrapWithErrorHandling(useServerConfigStore().loadServerConfig)(\n SERVER_CONFIG_ITEMS,\n settingStore.get('Comfy.Server.ServerConfigValues')\n )\n\n // Load model folders\n void wrapWithErrorHandlingAsync(useModelStore().loadModelFolders)()\n\n // Non-blocking load of node frequencies\n void wrapWithErrorHandlingAsync(\n useNodeFrequencyStore().loadNodeFrequencies\n )()\n\n // Node defs now available after comfyApp.setup.\n // Explicitly initialize nodeSearchService to avoid indexing delay when\n // node search is triggered\n useNodeDefStore().nodeSearchService.searchNode('')\n }, 1000)\n}\n</script>\n\n<style scoped>\n.comfyui-body {\n grid-template-columns: auto 1fr auto;\n grid-template-rows: auto 1fr auto;\n}\n\n/**\n +------------------+------------------+------------------+\n | |\n | .comfyui-body- |\n | top |\n | (spans all cols) |\n | |\n +------------------+------------------+------------------+\n | | | |\n | .comfyui-body- | #graph-canvas | .comfyui-body- |\n | left | | right |\n | | | |\n | | | |\n +------------------+------------------+------------------+\n | |\n | .comfyui-body- |\n | bottom |\n | (spans all cols) |\n | |\n +------------------+------------------+------------------+\n*/\n\n.comfyui-body-top {\n order: -5;\n /* Span across all columns */\n grid-column: 1/-1;\n /* Position at the first row */\n grid-row: 1;\n /* Top menu bar dropdown needs to be above of graph canvas splitter overlay which is z-index: 999 */\n /* Top menu bar z-index needs to be higher than bottom menu bar z-index as by default\n pysssss's image feed is located at body-bottom, and it can overlap with the queue button, which\n is located in body-top. */\n z-index: 1001;\n display: flex;\n flex-direction: column;\n}\n\n.comfyui-body-left {\n order: -4;\n /* Position in the first column */\n grid-column: 1;\n /* Position below the top element */\n grid-row: 2;\n z-index: 10;\n display: flex;\n}\n\n.graph-canvas-container {\n width: 100%;\n height: 100%;\n order: -3;\n grid-column: 2;\n grid-row: 2;\n position: relative;\n overflow: clip;\n}\n\n.comfyui-body-right {\n order: -2;\n z-index: 10;\n grid-column: 3;\n grid-row: 2;\n}\n\n.comfyui-body-bottom {\n order: 4;\n /* Span across all columns */\n grid-column: 1/-1;\n grid-row: 3;\n /* Bottom menu bar dropdown needs to be above of graph canvas splitter overlay which is z-index: 999 */\n z-index: 1000;\n display: flex;\n flex-direction: column;\n}\n</style>\n"],"file":"GraphView-BCkpNGgz.js"}