Compare commits

..

73 Commits

Author SHA1 Message Date
Luke Mino-Altherr
8d90716662 [demo] Add feature flag support to CloudRunButtonWrapper
- Wrap SubscribeToRunButton with FeatureFlaggedRunButton for cloud users
- Enable experimental button variants for cloud subscription flow
- Maintains existing behavior: ComfyQueueButton (with subscription) or SubscribeToRunButton (without)
2025-12-04 18:04:42 -08:00
Luke Mino-Altherr
0b0af89321 [demo] PostHog feature flag integration for Run button experiment
- Add FeatureFlaggedRunButton wrapper component
- Add ExperimentalRunButton with JSON-driven styling
- Extend useFeatureFlags to support remoteConfig and variant payloads
- Support 4 experimental button variants (bold-gradient, animated, playful, minimal)
- Add documentation for feature flag payload structure

NOTE: This is a DEMO and should NOT be merged
2025-12-04 16:12:52 -08:00
Luke Mino-Altherr
d314172b98 [feat] Add remote config support for model upload and asset update feature flags (#7143)
Feature flags for model upload button and asset update options now check
remote config from `/api/features` first, falling back to websocket
feature flags.

- **What**: Added `model_upload_button_enabled` and
`asset_update_options_enabled` to `RemoteConfig` type
- **What**: Updated feature flag getters to prioritize remote config
over websocket flags
- **Why**: Enables dynamic feature control without requiring websocket
connection, consistent with other feature flags pattern

- Pattern consistency with other remote config feature flags
- Proper fallback behavior when remote config is unavailable

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7143-feat-Add-remote-config-support-for-model-upload-and-asset-update-feature-flags-2bf6d73d3650819cb364f0ab69d77dd0)
by [Unito](https://www.unito.io)

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-04 11:10:35 -08:00
Comfy Org PR Bot
6c3408592e [backport cloud/1.33] cloud: increase feature flag polling interval to 10min (from 30s) (#7111)
Backport of #7100 to `cloud/1.33`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7111-backport-cloud-1-33-cloud-increase-feature-flag-polling-interval-to-10min-from-30s-2be6d73d3650817ea746fa49eb896a2d)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2025-12-02 19:28:06 -07:00
Comfy Org PR Bot
b6632443dc [backport cloud/1.33] fix: normalize path separators in comfyAPIPlugin for Windows compatibility (#7090)
Backport of #7087 to `cloud/1.33`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7090-backport-cloud-1-33-fix-normalize-path-separators-in-comfyAPIPlugin-for-Windows-compat-2bd6d73d365081228497fa23b58b8978)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2025-12-02 19:27:41 -07:00
Comfy Org PR Bot
c8a1df3a05 [backport cloud/1.33] feat(api-nodes-pricing): add prices for Kling O1 video model (#7079)
Backport of #7077 to `cloud/1.33`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7079-backport-cloud-1-33-feat-api-nodes-pricing-add-prices-for-Kling-O1-video-model-2bc6d73d365081c4ad83d1c5317a9135)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
2025-12-01 14:18:13 -07:00
Comfy Org PR Bot
39204135ba [backport cloud/1.33] [fix] Prevent drag activation during Vue node resize (#7071)
Backport of #7064 to `cloud/1.33`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7071-backport-cloud-1-33-fix-Prevent-drag-activation-during-Vue-node-resize-2bc6d73d36508129b6d4ee43e79de500)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: DrJKL <DrJKL0424@gmail.com>
2025-11-30 20:44:35 -08:00
Christian Byrne
ffa55cb92b [backport cloud/1.33] Simplify Vue node resize to bottom-right corner only (#7063) (#7068)
## Summary
- Backport of #7063 to cloud/1.33
- Simplifies Vue node resize to bottom-right corner only

Cherry-picked from d76c59cb14

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7068-backport-cloud-1-33-Simplify-Vue-node-resize-to-bottom-right-corner-only-7063-2bc6d73d36508149bd85c3ae534387b6)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-11-30 20:20:35 -08:00
Comfy Org PR Bot
3856e0deea [backport cloud/1.33] fix: loader node widget value shows placeholder instead of filename on cloud (#7046)
Backport of #7005 to `cloud/1.33`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7046-backport-cloud-1-33-fix-loader-node-widget-value-shows-placeholder-instead-of-filename-2bb6d73d365081bd9531c08bbaeb8634)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2025-11-29 19:14:34 -07:00
Comfy Org PR Bot
7b589b5502 [backport cloud/1.33] mark vue nodes menu toggle with beta tag (#7052)
Backport of #7047 to `cloud/1.33`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7052-backport-cloud-1-33-mark-vue-nodes-menu-toggle-with-beta-tag-2bb6d73d365081da9bb6cb1859c7bf5a)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2025-11-29 19:14:19 -07:00
Comfy Org PR Bot
72a2581068 [backport cloud/1.33] feat(api-nodes-pricing): add prices for ByteDance seedance-1-0-pro-fast model (#7030)
Backport of #7026 to `cloud/1.33`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7030-backport-cloud-1-33-feat-api-nodes-pricing-add-prices-for-ByteDance-seedance-1-0-pro--2b96d73d365081ff9e4feb7b1d147dc5)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Piskun <13381981+bigcat88@users.noreply.github.com>
2025-11-29 15:09:55 -07:00
Comfy Org PR Bot
22aea29a0d [backport cloud/1.33] [feat] Show "Finished in" duration for completed jobs in cloud (#7013)
Backport of #6895 to `cloud/1.33`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7013-backport-cloud-1-33-feat-Show-Finished-in-duration-for-completed-jobs-in-cloud-2b86d73d365081419f90ee82f5e45253)
by [Unito](https://www.unito.io)

Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-11-27 18:31:20 -07:00
Comfy Org PR Bot
637c1995b4 [backport cloud/1.33] [fix] Re-encode cloud-subscription video to VP9 for Safari compatibility (#7012)
Backport of #7006 to `cloud/1.33`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7012-backport-cloud-1-33-fix-Re-encode-cloud-subscription-video-to-VP9-for-Safari-compatib-2b86d73d365081a0be8ac57b8afce8a8)
by [Unito](https://www.unito.io)

Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
2025-11-27 17:56:57 -07:00
Comfy Org PR Bot
550ca0c911 [backport cloud/1.33] Remove app.graph usage from widgetInput code (#7011)
Backport of #7008 to `cloud/1.33`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7011-backport-cloud-1-33-Remove-app-graph-usage-from-widgetInput-code-2b86d73d365081de9460daecfde4bb87)
by [Unito](https://www.unito.io)

Co-authored-by: AustinMroz <austin@comfy.org>
2025-11-27 17:56:50 -07:00
Comfy Org PR Bot
896867b03c [backport cloud/1.33] fix: add filter for combo widgets (#7003)
Backport of #6999 to `cloud/1.33`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7003-backport-cloud-1-33-fix-add-filter-for-combo-widgets-2b86d73d3650818daa83c680abf3b5c4)
by [Unito](https://www.unito.io)

Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
2025-11-27 14:20:42 -07:00
Christian Byrne
0cd0218946 [backport cloud/1.33] fix: Vue Node <-> Litegraph node height offset normalization (#6978)
## Summary
Backport of #6966 onto cloud/1.33.

- cherry-picked 29dbfa3f
- accepted upstream snapshot updates (only zoomed-in ctrl+shift PNG
conflicted)

## Testing
- pnpm typecheck

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6978-backport-cloud-1-33-fix-Vue-Node-Litegraph-node-height-offset-normalization-2b86d73d365081a19a81f4fac0fd2e91)
by [Unito](https://www.unito.io)

Co-authored-by: github-actions <github-actions@github.com>
2025-11-26 22:38:13 -07:00
Christian Byrne
334404aa3b [backport cloud/1.33] fix: remove LOD from vue nodes (#6983)
## Summary
Backport of #6950 onto cloud/1.33 (clean cherry-pick of 4b87b1fdc).

## Testing
- pnpm typecheck

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6983-backport-cloud-1-33-fix-remove-LOD-from-vue-nodes-2b86d73d36508119bafbc5a5a6a5ad42)
by [Unito](https://www.unito.io)

Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-11-26 21:40:50 -07:00
Christian Byrne
31d842217b [backport cloud/1.33] fix: don't use registry when only checking for presence of missing nodes (#6972)
## Summary
Backport of #6965 onto cloud/1.33 (clean cherry-pick of 83f04490b).

## Testing
- pnpm typecheck

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6972-backport-cloud-1-33-fix-don-t-use-registry-when-only-checking-for-presence-of-missing--2b86d73d36508150af46da84e754df3a)
by [Unito](https://www.unito.io)
2025-11-26 17:50:40 -07:00
Comfy Org PR Bot
3dd3e26003 [backport cloud/1.33] feat: open template via URL in linear mode (#6968)
Backport of #6945 to `cloud/1.33`

Automatically created by backport workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6968-backport-cloud-1-33-feat-open-template-via-URL-in-linear-mode-2b76d73d365081c4b5d5c0727106fc29)
by [Unito](https://www.unito.io)

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2025-11-26 17:32:19 -07:00
Alexander Brown
e6332046b0 BYOM: Model Import Wizard (#6949)
## Summary

Design alignment for the model import wizard.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6949-BYOM-Model-Import-Wizard-2b76d73d365081a48632c40430e05c93)
by [Unito](https://www.unito.io)
2025-11-25 19:19:16 -08:00
AustinMroz
5fa76e23d9 Reduce width of run button dock zone (#6925)
Tiny PR to make the docking area line up with the new size of the
actionbar after #6723
<img width="550" height="106" alt="image"
src="https://github.com/user-attachments/assets/911d510e-351f-484f-807a-17f5428dea79"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6925-Reduce-width-of-run-button-dock-zone-2b66d73d36508137aa0cc18178172264)
by [Unito](https://www.unito.io)
2025-11-25 18:19:33 -07:00
Simula_r
fcfb5437a9 feat: mobile breakpoint for vue nodes banner (#6942)
## Summary

Add mobile breakpoint to vue nodes banner

## Changes

- **What**: main.json and vue nodes banner

## Screenshots (if applicable)

<img width="500" height="615" alt="image"
src="https://github.com/user-attachments/assets/fd8cc621-c335-41c9-bbee-4ec0ae04b226"
/>
<img width="980" height="615" alt="image"
src="https://github.com/user-attachments/assets/30e17fc2-fc91-44a3-b9f0-85d5146e861b"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6942-feat-mobile-breakpoint-for-vue-nodes-banner-2b66d73d36508139b69afd7e7134b72e)
by [Unito](https://www.unito.io)
2025-11-25 18:18:01 -07:00
Comfy Org PR Bot
5ff3a0ed52 1.33.9 (#6941)
Patch version increment to 1.33.9

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6941-1-33-9-2b66d73d365081cc9b0de993e427287d)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-11-25 16:59:12 -07:00
Christian Byrne
5fa0295ff5 feat: update About panel (system stats and version info) to work on cloud (#6940)
## Summary

Updates the About Panel in the settings to work with the cloud-specific
`GET /system_stats` response schema.

| Before | After |
| ------ | ----- |
| <img width="1922" height="1436" alt="Selection_2392"
src="https://github.com/user-attachments/assets/3b97bf38-7eeb-4f46-9c59-eb681f5d7401"
/> | <img width="1922" height="1436" alt="Selection_2391"
src="https://github.com/user-attachments/assets/1d30e604-654a-4d48-ba05-4cac3b54c2ba"
/> |

## Screenshots (if applicable)

OSS version stays the same:

<img width="1922" height="1436" alt="Selection_2393"
src="https://github.com/user-attachments/assets/40e1eeeb-fc5a-4ad0-b37f-dc5d0374901e"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6940-feat-update-About-panel-system-stats-and-version-info-to-work-on-cloud-2b66d73d365081f69b6fedfe9507ba92)
by [Unito](https://www.unito.io)
2025-11-25 16:34:34 -07:00
Christian Byrne
1348a0934a feat: update node search result rankings with 2025 usage data (#6939)
Updates the usage data for nodes to improve the search result algorithm.
Old data was from 2024. New data is from Nov 2025 and comes from the
search result interactions only (rather than workflow membership). Old
data is normalized against new data's scale and kept in fields where it
wouldn't have been otherwise overwritten.

Here's a helpful visualization of the before and after for the ranking:
https://www.diffchecker.com/Aa5XTN7F/

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6939-feat-update-node-search-result-rankings-with-2025-usage-data-2b66d73d3650818f9bb5d8765f7f7eaa)
by [Unito](https://www.unito.io)
2025-11-25 16:23:21 -07:00
Christian Byrne
01f8e77251 fix: backport workflow fails with PR title has double quotes (#6934)
Fixes issue when PR title or merge commit has double quotes (e.g.,
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/19684310613/job/56385939600).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6934-fix-backport-workflow-fails-with-PR-title-has-double-quotes-2b66d73d3650815992dadb6142676754)
by [Unito](https://www.unito.io)
2025-11-25 15:04:31 -07:00
Benjamin Lu
31c03b669e Bump desktop-ui to 0.0.4 (#6933)
This contains a change to clarify to what is the base path issue after
restrictions were tightened.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6933-Bump-desktop-ui-to-0-0-4-2b66d73d3650816e9f3bed7202455ddb)
by [Unito](https://www.unito.io)
2025-11-25 13:46:21 -08:00
Alexander Piskun
c9da19b5b5 feat(api-nodes-pricing): add prices for Veo3FirstLastFrameNode (#6920)
## Summary

Price badges for this PR:
https://github.com/comfyanonymous/ComfyUI/pull/10878

If we can include this in an upcoming release, that would be absolutely
great.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6920-feat-api-nodes-pricing-add-prices-for-Veo3FirstLastFrameNode-2b66d73d365081bdb49be98d953a7a0b)
by [Unito](https://www.unito.io)
2025-11-25 11:56:39 -08:00
Alexander Piskun
10222860eb feat(api-nodes-pricing): add prices for Flux2ProImageNode (#6921)
## Summary

Price badges for https://github.com/comfyanonymous/ComfyUI/pull/10880

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6921-feat-api-nodes-pricing-add-prices-for-Flux2ProImageNode-2b66d73d365081f2b269c77df7ef93d6)
by [Unito](https://www.unito.io)
2025-11-25 11:56:10 -08:00
Benjamin Lu
4597b7e600 [feat] Improve queue job item UX based on design feedback (#6893)
## Summary
- Running jobs now show cancel button at all times (always visible, not
just on hover)
- Cancel/delete buttons use destructive red styling by default with
hover state
- Changed pending job icon from clock to loader-circle with spin
animation
- Fixed icon buttons to be square (size-6) instead of rectangular
- Added TODO comment for future declarative button config system
- Pending hint ("Job added to queue") now shows only once per entry and
no longer resets when other jobs update
- Spinner animation now applies only to the pending loader icon;
completed/check icons no longer spin
- Queue overlay hover/active state also triggers when hovering the top
menu bar so controls stay visible

## Design Spec

https://www.notion.so/comfy-org/Design-Queue-Dialog-Job-Ordering-and-Cancel-Button-Visibility-2b46d73d365081748a43d5cc9fbe2639

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6893-feat-Improve-queue-job-item-UX-based-on-design-feedback-2b56d73d365081a2bc7ef6f6fea1c739)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-25 11:23:39 -08:00
AustinMroz
6782d04f00 Support display_name on frontend (#6922)
The v3 schema allows defining a `display_name` on inputs, but this was
previously ignored on the frontend. It is now used to designate a
default value for the label of a widget or input.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6922-Support-display_name-on-frontend-2b66d73d365081d992cbea07abc27a0f)
by [Unito](https://www.unito.io)
2025-11-25 10:36:14 -08:00
Benjamin Lu
4bb5c12fac Add cloud backport tagging workflow (#6896)
## Summary
- add workflow to tag merged backport-labeled PRs into cloud/* with
cloud/v<package.json version>
- validate branch/version alignment and skip when tag already exists
- use .nvmrc (Node 24) for setup-node

## Question
- This workflow expects the version bump to already be in the PR when
you merge, as it fails if the tag already exists to keep immutability.
Should we autobump in this case?
2025-11-24 22:56:33 -08:00
Alexander Brown
8b5cfe7e55 Lint: Adding more checks for non internationalized strings (#5625)
## Summary

Catch more user visible (or audible) text that isn't
internationalizable.

## Changes

- **What**: Linter now checks other attributes for raw text.

## Review Focus

What other properties have leaked English text to non-English locales
that aren't in here?
2025-11-24 21:55:47 -08:00
Alexander Brown
135169003f Devex: Remove Importmap plugin (#6899)
## Summary

See [this
page](https://www.notion.so/comfy-org/Remove-importmap-and-replace-with-better-solution-if-it-exists-2ab6d73d3650801d83afe006fa0d9929?source=copy_link).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6899-Devex-Remove-Importmap-plugin-2b66d73d365081b28167c9ae70100092)
by [Unito](https://www.unito.io)
2025-11-24 20:39:46 -08:00
Alexander Brown
d58a464c9c Style: Widget spacing and Markdown padding (#6902)
## Summary

Less padding, looks nice.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6902-Style-Widget-spacing-and-Markdown-padding-2b66d73d365081cca409dbabc7b883bb)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-11-24 18:31:21 -08:00
Alexander Brown
e54b972550 Chore: Update CODEOWNERS (#6901)
## Summary

Add Global Owners and a team, update/remove references to some cherished
former colleagues.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6901-Chore-Update-CODEOWNERS-2b66d73d365081f8b687ef678567b30a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
2025-11-24 17:34:41 -08:00
Alexander Brown
9bd63dbe6a Cleanup: Consistent use of Nodes 2.0 in user facing strings. (#6898)
https://github.com/Comfy-Org/ComfyUI_frontend/issues/6888

## Summary

Did a quick pass to find user facing strings. Not sure if this gets them
all.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6898-Cleanup-Consistent-use-of-Nodes-2-0-in-user-facing-strings-2b66d73d36508124aee2f5f8373a4b60)
by [Unito](https://www.unito.io)
2025-11-24 17:26:16 -08:00
Johnpaul Chiwetelu
a21c813d11 Style button widgets (#6900)
This pull request introduces improvements to widget customization and UI
consistency in the application. The most notable changes are the
addition of support for icon classes in widget options, updates to
button rendering logic, and enhanced visual consistency for button
components.

Widget customization enhancements:
* Added an optional `iconClass` property to the `IWidgetOptions`
interface in `widgets.ts`, allowing widgets to specify custom icons.

UI and rendering updates:
* Updated `WidgetButton.vue` to render the widget label and, if
provided, an icon using the new `iconClass` option. Also standardized
button styling and label usage.
* Improved button styling in `WidgetRecordAudio.vue` for better visual
consistency with other components.
<img width="662" height="534" alt="Screenshot 2025-11-25 at 01 36 45"
src="https://github.com/user-attachments/assets/43bbe226-07fd-48be-9b98-78b08a726b1b"
/>


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6900-Style-button-widgets-2b66d73d3650818ebeadd9315a47ba0f)
by [Unito](https://www.unito.io)
2025-11-25 01:12:30 +00:00
Alexander Brown
df373af987 Feat: Restore settings button to the sidebar. (#6892)
## Summary

Opens settings with a single click!
<img width="247" height="252" alt="image"
src="https://github.com/user-attachments/assets/c571d5e0-4973-4570-a542-fff343364ec0"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6892-Feat-Restore-settings-button-to-the-sidebar-2b56d73d36508102a48aec318d468303)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-11-24 12:23:29 -08:00
Alexander Brown
c06a7279e2 Style: Grid for widgets (#6891)
## Summary

Keeps the controls and widgets a consistent width, but lets the size be
more flexible

## Screenshots

<!-- Add screenshots or video recording to help explain your changes -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6891-Style-Grid-for-widgets-2b56d73d365081a29c30d337f3be1af6)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-11-24 11:46:24 -08:00
Benjamin Lu
ffc17c054b Proposal: Run Storybook CI on all pull requests (#6880)
## Summary
Allow the Storybook workflow to run on every PR branch instead of only
`main`.

## Changes
- **What**: remove the `branches: [main]` filter from the Storybook CI
workflow so it triggers for all pull_request events.

## Review Focus
- Confirm broader triggering is desired for all branches while Chromatic
deployment remains restricted to `version-bump-*` or manual runs.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6880-Run-Storybook-CI-on-all-pull-requests-2b56d73d36508169a32afcf50f8e8fd8)
by [Unito](https://www.unito.io)

Co-authored-by: sno <snomiao@gmail.com>
2025-11-23 23:29:47 -08:00
Comfy Org PR Bot
ddb00d02d5 1.33.8 (#6885)
Patch version increment to 1.33.8

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6885-1-33-8-2b56d73d3650815c8660c30e7d625bdb)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-11-23 22:33:34 -08:00
Benjamin Lu
4815d6b14c Add test id for startup status text (#6882)
## Summary
- add `data-testid="startup-status-text"` to the status text in
StartupDisplay
- keep the status element targetable for Playwright masks

## Why
Desktop’s Playwright tests mask the troubleshooting version line; this
test id is needed there to keep screenshots stable across releases.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6882-Add-test-id-for-startup-status-text-2b56d73d365081b6a2e4ddca9aa985a4)
by [Unito](https://www.unito.io)
2025-11-23 22:54:14 -07:00
Benjamin Lu
a9653ba9c7 Remove unsupported workflow description fields (#6881)
## Summary
Remove top-level `description` keys from workflows because they are not
valid in GitHub Actions workflow syntax (see
https://docs.github.com/en/actions/reference/workflows-and-actions/workflow-syntax?utm_source=chatgpt.com).

## Changes
- **What**: delete the unsupported `description:` field from all
workflow YAMLs under `.github/workflows/`.

## Review Focus
- Confirm workflows still show intended names and triggers without the
invalid `description` metadata.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6881-Remove-unsupported-workflow-description-fields-2b56d73d365081ed9f20eb7f57956bc6)
by [Unito](https://www.unito.io)
2025-11-23 22:53:51 -07:00
Christian Byrne
30bafcd019 hide "unload models" and "unload cache" menu entries on cloud (#6879)
Hides these features which the user does not need when on cloud.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6879-hide-unload-models-and-unload-cache-menu-entries-on-cloud-2b46d73d3650816a8e22e913a848e4ac)
by [Unito](https://www.unito.io)
2025-11-23 22:53:18 -07:00
Christian Byrne
b789791fd9 fix: workflow that creates release branch fails (#6878)
Fixes 'create-release-branch' workflow. The script was emitting a
multiline output using `echo "results<<'EOF'" … echo "EOF"`. GitHub
treats the opening delimiter literally (`'EOF'` with quotes). Because
the closing line omits the quotes, the runner never sees a matching
terminator, so it aborts the file-command write and marks the step as
failed even though the preceding git pushes succeeded.

Example of error:
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/19520246619/job/55881876566

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6878-fix-workflow-that-creates-release-branch-fails-2b46d73d365081549651eacacd5cbfec)
by [Unito](https://www.unito.io)
2025-11-23 22:51:47 -07:00
Christian Byrne
723f53751e fix: duplicate "refresh node definitions" in menu entries (#6876)
The "Refresh node definitions" menu entry was added in the Manager
upstream but while in development was also added in a separate commit,
leading to duplicate menu entries:

<img width="1460" height="324" alt="image"
src="https://github.com/user-attachments/assets/66347cb3-1c52-457e-a4f1-8b32b615a1ca"
/>

This PR removes the second one.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6876-fix-duplicate-refresh-node-definitions-in-menu-entries-2b46d73d365081b98bdfcc60dc9bad36)
by [Unito](https://www.unito.io)
2025-11-23 22:51:36 -07:00
Christian Byrne
2539a7d2ce fix: tabindex prop should be number type in MultiSelect component (#6875)
Change to use number prop to fix warnings:

```
WorkflowTemplateSelectorDialog.vue:7 [Vue warn]: Invalid prop: type check failed for prop "tabindex". Expected Number with value 0, got String with value "0".
  at <MultiSelect modelValue= [] onUpdate:modelValue=fn class="w-[250px]"  ... >
  at <MultiSelect modelValue= [] onUpdate:modelValue=fn search-query=""  ... >
  at <BaseModalLayout content-title="Get Started with a Template" class="workflow-template-selector-dialog" maximized=false >
  at <WorkflowTemplateSelectorDialog ref_for=true onClose=fn<hide> maximized=false >
  at <BaseTransition onEnter=fn onAfterEnter=fn<bound onAfterEnter> onBeforeLeave=fn<bound onBeforeLeave>  ... > 
  at <Transition name="p-dialog" onEnter=fn<bound onEnter> onAfterEnter=fn<bound onAfterEnter>  ... > 
  at <Portal appendTo="body" > 
  at <Dialog key="global-workflow-template-selector" visible=true onUpdate:visible=fn<onUpdate:visible>  ... >
  at <GlobalDialog > 
  at <App>
```

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6875-fix-tabindex-prop-should-be-number-type-in-MultiSelect-component-2b46d73d3650816d8288fec4cc0f7e7f)
by [Unito](https://www.unito.io)
2025-11-23 22:51:26 -07:00
Christian Byrne
c9556d7aff fix: ordering of Vue mode LOD setting initialization in browser tests (#6884)
Fixes test "should toggle LOD based on zoom threshold" failing (example:
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/19618176782/job/56174006314).

Test execution order:

1. Sets MinFontSizeForLOD = 8
2. Calls setup()
3. App initializes → useVueNodeLifecycle runs → useRenderModeSetting
executes with immediate: true
4. Overrides setting back to 0 (Vue mode value)
5. LOD never activates (threshold = 0)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6884-fix-ordering-of-Vue-mode-LOD-setting-initialization-in-browser-tests-2b56d73d365081209676fb14d2c04d28)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-11-23 22:51:07 -07:00
AustinMroz
58b051a473 Share button and Assets Panel in Linear Mode (#6794)
- Re-enables the share button in Linear Mode and have it export the
current workflow
- Not as nice as having it copy an actual URL, but good enough for the
interim and it help with dead space
- Display the Media Assets Panel on the left hand side to replace the
removed Queue Panel

<img width="806" alt="image"
src="https://github.com/user-attachments/assets/93786dfa-8fbb-4368-8594-b9c98bbeb79e"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6794-Share-button-and-Assets-Panel-in-Linear-Mode-2b26d73d36508178aef9ededa38d47f1)
by [Unito](https://www.unito.io)
2025-11-23 12:25:30 -07:00
Alexander Brown
0b33470744 Minor: transformState and setting error cleanup (#6841)
## Summary

Fixes the routing of TransformState through the node and the console
error from a setting that ends up being undefined via
useRenderModeSetting.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6841-Minor-transformState-and-setting-error-cleanup-2b46d73d3650817a8da7fca5bc56ea9a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-11-23 12:24:37 -07:00
Comfy Org PR Bot
fb3ce74d2f 1.33.7 (#6856)
Patch version increment to 1.33.7

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6856-1-33-7-2b46d73d365081c4b709d125df63c98d)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-11-23 11:47:49 -07:00
Comfy Org PR Bot
86d3f0ebd5 1.33.6 (#6837)
Patch version increment to 1.33.6

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6837-1-33-6-2b46d73d3650815194b4fd885b13b574)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-11-23 11:31:54 -07:00
Jin Yi
09c888e338 Fix: Selected assets count not updating in Imported tab (#6842)
## Summary
- Fix bug where the "Selected assets count" displayed as 0 in the
Imported tab when selecting assets

## Root Cause
The `getOutputCount` function was returning `0` when
`user_metadata.outputCount` was not present.

- **Generated tab**: Works correctly because `outputCount` metadata is
set during generation
- **Imported tab**: `outputCount` metadata is never set, so it always
returns `0` → selected count shows as 0

## Solution
Changed the default return value from `0` to `1` when `outputCount`
metadata is not present, ensuring every asset counts as at least 1.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2025-11-23 00:57:36 -08:00
Alexander Brown
6d41e8b6e4 Feat: Load Image (from Outputs) support in Vue Nodes (#6836)
## Summary

Expose the Auto-refresh and manual refresh controls.
Fixes an issue with the Dropdown where it was index dependent so wasn't
updating correctly as new items came in.

## Screenshots

<!-- Add screenshots or video recording to help explain your changes -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6836-Feat-Load-Image-from-Outputs-support-in-Vue-Nodes-2b46d73d365081f1b44fcf2054d653da)
by [Unito](https://www.unito.io)
2025-11-22 23:59:00 -08:00
Terry Jia
274f77869b Fix: Opening mask editor on context menu (#6825)
## Summary

Fix issue of opening mask editor on context menu, reported in
https://github.com/Comfy-Org/ComfyUI_frontend/issues/6824

## Screenshots (if applicable)


https://github.com/user-attachments/assets/666d2769-d848-4b08-b54b-0cf5ed799b35

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6825-Fix-Opening-mask-editor-on-context-menu-2b36d73d3650810781a3c25a23ba488a)
by [Unito](https://www.unito.io)
2025-11-22 20:46:29 -05:00
Alexander Brown
f5c9f69678 Style: Fix the filter/search/sort controls on the Template Select Modal (#6835)
## Summary

Background and text colors.

## Screenshot
<img width="1186" height="148" alt="image"
src="https://github.com/user-attachments/assets/0ff3b0d5-6aae-45c5-9ebf-060a9973489c"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6835-Style-Fix-the-filter-search-sort-controls-on-the-Template-Select-Modal-2b36d73d3650816b9850e1b9f7feb25e)
by [Unito](https://www.unito.io)
2025-11-22 15:30:14 -08:00
Alexander Brown
c1e237255a Fix: TextArea context menu (#6834)
## Summary

Allow the default browser context menu within textareas on Vue Nodes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6834-Fix-TextArea-context-menu-2b36d73d3650814e9706e76163dda59a)
by [Unito](https://www.unito.io)
2025-11-22 15:18:04 -08:00
AustinMroz
a91b9f288f When filtering by IO type, put wildcards last (#6829)
| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/628057b2-4844-490d-9899-fce82d8e2d58"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/2825f15f-c084-4e3d-8b22-cc0aa3febdce"
/> |

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6829-When-filtering-by-IO-type-put-wildcards-last-2b36d73d36508196a41fed40b62f5b84)
by [Unito](https://www.unito.io)
2025-11-22 13:43:05 -07:00
Tristan Sommer
4adcf09cca GPU accelerated maskeditor rendering (#6767)
## GPU accelerated brush engine for the mask editor

- Full GPU acceleration using TypeGPU and type-safe shaders
- Catmull-Rom Spline Smoothing
- arc-length equidistant resampling
- much improved performance, even for huge images
- photoshop like opacity clamping for brush strokes
- much improved soft brushes
- fallback to CPU fully implemented, much improved CPU rendering
features as well

### Tested Browsers
- Chrome (fully supported)
- Safari 26 (fully supported, prev versions CPU fallback)
- Firefox (CPU fallback, flags needed for full support)



https://github.com/user-attachments/assets/b7b5cb8a-2290-4a95-ae7d-180e11fccdb0



https://github.com/user-attachments/assets/4297aaa5-f249-499a-9b74-869677f1c73b



https://github.com/user-attachments/assets/602b4783-3e2b-489e-bcb9-70534bcaac5e

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6767-GPU-accelerated-maskeditor-rendering-2b16d73d3650818cb294e1fca03f6169)
by [Unito](https://www.unito.io)
2025-11-22 09:07:16 -05:00
Christian Byrne
1dbb3fc1b9 make vue node settings appear higher in the settings dialog (#6820)
makes setting groups/categories be sorted by highest internal setting
field `sortOrder` and adds high `sortOrder` values to the Vue Nodes
(Nodes 2.0) settings.


<img width="2282" height="1872" alt="Selection_2371"
src="https://github.com/user-attachments/assets/71e7e76b-4637-42b5-9f0c-2617622cda23"
/>
2025-11-21 20:32:18 -08:00
Alexander Brown
d6c5b33fce Fix: Vue <--> Litegraph scaling logic. (#6745)
Consistent names and order of operations. I think this fixes the
resizing too.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6745-refactor-Reorganizing-scaling-logic-2b06d73d365081eebc8ff93c87fa69fb)
by [Unito](https://www.unito.io)
2025-11-22 01:52:20 +00:00
Alexander Brown
f5608435b4 Fix: Clear apiKey on failed auth (#6816)
## Summary

Handles the case where an API key is structurally valid but not in our
DB.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6816-Fix-Clear-apiKey-on-failed-auth-2b26d73d3650817ab34edfa380795178)
by [Unito](https://www.unito.io)
2025-11-21 17:26:39 -07:00
Alexander Brown
9da82f47ef Feat: Alt+Drag to clone - Vue Nodes (#6789)
## Summary

Replicate the alt+drag to clone behavior present in litegraph.

## Changes

- **What**: Simplify the interaction/drag handling, now with less state!
- **What**: Alt+Click+Drag a node to clone it

## Screenshots (if applicable)



https://github.com/user-attachments/assets/469e33c2-de0c-4e64-a344-1e9d9339d528



<!-- Add screenshots or video recording to help explain your changes -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6789-WIP-Alt-Drag-to-clone-Vue-Nodes-2b16d73d36508102a871ffe97ed2831f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-11-21 14:16:03 -08:00
Alexander Brown
a8d6f7baff Feat: Show Progress Text on Vue Nodes, Markdown for Preview as Text (#6805)
## Summary

Maps the progressText / text preview to a readonly Markdown widget.

## Review Focus

Anything else to tweak about this while I'm in here?

## Screenshots
<img width="1107" height="593" alt="image"
src="https://github.com/user-attachments/assets/a445c07a-2fcb-480c-976e-bda6ff343f14"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6805-Feat-Show-Progress-Text-on-Vue-Nodes-2b26d73d3650814d93c4f2fbf0e00095)
by [Unito](https://www.unito.io)
2025-11-21 13:54:53 -08:00
Christian Byrne
a7daa5071c fix: backport workflow fails when label description has single quote (#6814)
Fixes issue with backporting workflow when the target PR has a label
whose description has quotes
([example](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/19580741998/job/56077744515))

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6814-fix-backport-workflow-fails-when-label-description-has-single-quote-2b26d73d36508143bbefe6c4314fd370)
by [Unito](https://www.unito.io)
2025-11-21 14:23:36 -07:00
Jin Yi
639975b804 fix: Prevent multiple asset card popovers from opening simultaneously (#6808)
## Summary

Ensures only one MediaAssetCard popover is open at a time.

## Changes

- Added `hide()` method exposure in MoreButton component
- Implemented parent-level state management for tracking open popover
- Added VueUse `whenever` to auto-close other popovers when one opens

## Test Plan

- Open multiple asset cards' more menus
- Verify only one popover remains open at a time

🤖 Generated with [Claude Code](https://claude.com/claude-code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6808-fix-Prevent-multiple-asset-card-popovers-from-opening-simultaneously-2b26d73d365081e1b3f0d97b869cedc5)
by [Unito](https://www.unito.io)
2025-11-21 14:01:19 -07:00
Luke Mino-Altherr
ff0d385db8 [bugfix] Fix double-click required after pasting URL in upload model dialog (#6801)
## Summary
Fixed an issue where users had to click twice to continue after pasting
a URL in the upload model dialog - once to blur the input, then again to
click the button.

## Changes
- **What**: Replaced `UrlInput` with plain `InputText` in
`UploadModelUrlInput` to emit value immediately on input instead of only
on blur
- **Cleanup**: Moved URL cleaning/normalization to the `fetchMetadata`
handler, removed unused `disableValidation` prop from `UrlInput`
component

## Review Focus
- URL normalization logic in `useUploadModelWizard.ts`

🤖 Generated with [Claude Code](https://claude.com/claude-code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6801-bugfix-Fix-double-click-required-after-pasting-URL-in-upload-model-dialog-2b26d73d3650811881aed0cc064efcc7)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-21 11:05:36 -08:00
Luke Mino-Altherr
6e2e591937 refactor: change model button terminology from Upload to Import (#6800)
## Summary
Changes user-facing text from "Upload" to "Import" for the model import
feature, as "Import" better describes importing models from external
sources like Civitai.

## Changes
- Updated button icon from `upload` to `package-plus`
- Changed all user-facing text in English locale from "Upload" to
"Import"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6800-refactor-change-model-button-terminology-from-Upload-to-Import-2b26d73d365081059ce6e4a548f943ce)
by [Unito](https://www.unito.io)

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-20 21:47:02 -08:00
Comfy Org PR Bot
27fcc4554f 1.33.5 (#6798)
Patch version increment to 1.33.5

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6798-1-33-5-2b26d73d3650814fb062c8e2c1602ac6)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-11-20 21:06:15 -07:00
Alexander Brown
e563c1be75 hotfix: Stop clicks on the textarea from propagating to the node itself (#6788)
## Summary

Selecting text shouldn't drag the node.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6788-hotfix-Stop-clicks-on-the-textarea-from-propagating-to-the-node-itself-2b16d73d3650819c8d0dc427d5758580)
by [Unito](https://www.unito.io)
2025-11-20 21:05:24 -07:00
Jin Yi
5a297e520c fix: disabling queue button (#6797)
## Summary
- Removed the `disabled` prop binding from the queue button that was
incorrectly disabling it when there were missing nodes

## Changes
- Removed `:disabled="hasMissingNodes"` from ComfyQueueButton.vue:13

## Test plan
- [x] Verify queue button is no longer incorrectly disabled when there
are missing nodes
- [x] Verify queue functionality works as expected

🤖 Generated with [Claude Code](https://claude.com/claude-code)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6797-fix-disabling-queue-button-2b26d73d3650810783cedd44fce757be)
by [Unito](https://www.unito.io)
2025-11-20 20:52:30 -07:00
Benjamin Lu
85bec5ee47 Use shared button components in queue overlay (#6793)
## Summary
- replace raw button elements in queue progress overlay UI with shared
IconButton/TextButton/IconTextButton components
- remove forced justify-start from IconTextButton base and add explicit
alignment where needed
- keep queue overlay actions consistent with button styling patterns

## Testing
- pnpm typecheck
- pnpm lint:fix

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6793-Use-shared-button-components-in-queue-overlay-2b26d73d3650814d9ebfebba74226036)
by [Unito](https://www.unito.io)
2025-11-20 20:18:33 -07:00
239 changed files with 9761 additions and 6785 deletions

View File

@@ -1,5 +1,5 @@
# Description: When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo
name: 'Api: Update Electron API Types'
description: 'When upstream electron API is updated, click dispatch to update the TypeScript type definitions in this repo'
on:
workflow_dispatch:

View File

@@ -1,5 +1,5 @@
# Description: When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo
name: 'Api: Update Manager API Types'
description: 'When upstream ComfyUI-Manager API is updated, click dispatch to update the TypeScript type definitions in this repo'
on:
# Manual trigger

View File

@@ -1,5 +1,5 @@
# Description: When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo
name: 'Api: Update Registry API Types'
description: 'When upstream comfy-api is updated, click dispatch to update the TypeScript type definitions in this repo'
on:
# Manual trigger

View File

@@ -1,5 +1,5 @@
# Description: Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq
name: "CI: JSON Validation"
description: "Validates JSON syntax in all tracked .json files (excluding tsconfig*.json) using jq"
on:
push:

View File

@@ -1,5 +1,5 @@
# Description: Linting and code formatting validation for pull requests
name: "CI: Lint Format"
description: "Linting and code formatting validation for pull requests"
on:
pull_request:

View File

@@ -1,5 +1,5 @@
# Description: Validates Python code in tools/devtools directory
name: "CI: Python Validation"
description: "Validates Python code in tools/devtools directory"
on:
pull_request:

View File

@@ -1,5 +1,5 @@
# Description: Deploys test results from forked PRs (forks can't access deployment secrets)
name: "CI: Tests E2E (Deploy for Forks)"
description: "Deploys test results from forked PRs (forks can't access deployment secrets)"
on:
workflow_run:

View File

@@ -1,5 +1,5 @@
# Description: End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages
name: "CI: Tests E2E"
description: "End-to-end testing with Playwright across multiple browsers, deploys test reports to Cloudflare Pages"
on:
push:

View File

@@ -1,5 +1,5 @@
# Description: Deploys Storybook previews from forked PRs (forks can't access deployment secrets)
name: "CI: Tests Storybook (Deploy for Forks)"
description: "Deploys Storybook previews from forked PRs (forks can't access deployment secrets)"
on:
workflow_run:

View File

@@ -1,10 +1,9 @@
# Description: Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages
name: "CI: Tests Storybook"
description: "Builds Storybook and runs visual regression testing via Chromatic, deploys previews to Cloudflare Pages"
on:
workflow_dispatch: # Allow manual triggering
pull_request:
branches: [main]
jobs:
# Post starting comment for non-forked PRs

View File

@@ -1,5 +1,5 @@
# Description: Unit and component testing with Vitest
name: "CI: Tests Unit"
description: "Unit and component testing with Vitest"
on:
push:

View File

@@ -1,5 +1,5 @@
# Description: Validates YAML syntax and style using yamllint with relaxed rules
name: "CI: YAML Validation"
description: "Validates YAML syntax and style using yamllint with relaxed rules"
on:
push:

View File

@@ -0,0 +1,69 @@
---
name: Cloud Backport Tag
on:
pull_request:
types: ['closed']
branches: [cloud/*]
jobs:
create-tag:
if: >
github.event.pull_request.merged == true &&
contains(github.event.pull_request.labels.*.name, 'backport')
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: read
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version-file: '.nvmrc'
- name: Create tag for cloud backport
id: tag
run: |
set -euo pipefail
BRANCH="${{ github.event.pull_request.base.ref }}"
if [[ ! "$BRANCH" =~ ^cloud/([0-9]+)\.([0-9]+)$ ]]; then
echo "::error::Base branch '$BRANCH' is not a cloud/x.y branch"
exit 1
fi
MAJOR="${BASH_REMATCH[1]}"
MINOR="${BASH_REMATCH[2]}"
VERSION=$(node -p "require('./package.json').version")
if [[ "$VERSION" =~ ^${MAJOR}\.${MINOR}\.([0-9]+)(-.+)?$ ]]; then
PATCH="${BASH_REMATCH[1]}"
SUFFIX="${BASH_REMATCH[2]:-}"
else
echo "::error::Version '${VERSION}' does not match cloud branch '${BRANCH}'"
exit 1
fi
TAG="cloud/v${VERSION}"
if git ls-remote --tags origin "${TAG}" | grep -Fq "refs/tags/${TAG}"; then
echo "::notice::Tag ${TAG} already exists; skipping"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
exit 0
fi
git tag "${TAG}" "${{ github.event.pull_request.merge_commit_sha }}"
git push origin "${TAG}"
echo "tag=${TAG}" >> "$GITHUB_OUTPUT"
{
echo "Created tag: ${TAG}"
echo "Branch: ${BRANCH}"
echo "Version: ${VERSION}"
echo "Commit: ${{ github.event.pull_request.merge_commit_sha }}"
} >> "$GITHUB_STEP_SUMMARY"

View File

@@ -1,5 +1,5 @@
# Description: Generates and updates translations for core ComfyUI components using OpenAI
name: "i18n: Update Core"
description: "Generates and updates translations for core ComfyUI components using OpenAI"
on:
# Manual dispatch for urgent translation updates

View File

@@ -78,8 +78,7 @@ jobs:
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
LABELS=$(gh pr view ${{ inputs.pr_number }} --json labels | jq -r '.labels[].name')
else
LABELS='${{ toJSON(github.event.pull_request.labels) }}'
LABELS=$(echo "$LABELS" | jq -r '.[].name')
LABELS=$(jq -r '.pull_request.labels[].name' "$GITHUB_EVENT_PATH")
fi
add_target() {
@@ -237,8 +236,8 @@ jobs:
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
else
PR_TITLE="${{ github.event.pull_request.title }}"
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
PR_TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH")
MERGE_COMMIT=$(jq -r '.pull_request.merge_commit_sha' "$GITHUB_EVENT_PATH")
fi
for target in ${{ steps.filter-targets.outputs.pending-targets }}; do
@@ -327,8 +326,8 @@ jobs:
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
else
PR_TITLE="${{ github.event.pull_request.title }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
PR_TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH")
PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
fi
for backport in ${{ steps.backport.outputs.success }}; do
@@ -365,9 +364,9 @@ jobs:
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
else
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
PR_NUMBER=$(jq -r '.pull_request.number' "$GITHUB_EVENT_PATH")
PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
MERGE_COMMIT=$(jq -r '.pull_request.merge_commit_sha' "$GITHUB_EVENT_PATH")
fi
for failure in ${{ steps.backport.outputs.failed }}; do

View File

@@ -1,5 +1,5 @@
# Description: AI-powered code review triggered by adding the 'claude-review' label to a PR
name: "PR: Claude Review"
description: "AI-powered code review triggered by adding the 'claude-review' label to a PR"
permissions:
contents: read

View File

@@ -148,10 +148,10 @@ jobs:
done
{
echo "results<<'EOF'"
echo "results<<EOF"
cat "$RESULTS_FILE"
echo "EOF"
} >> $GITHUB_OUTPUT
} >> "$GITHUB_OUTPUT"
- name: Ensure release labels
if: steps.check_version.outputs.is_minor_bump == 'true'

View File

@@ -1,5 +1,5 @@
# Description: Manual workflow to increment package version with semantic versioning support
name: "Release: Version Bump"
description: "Manual workflow to increment package version with semantic versioning support"
on:
workflow_dispatch:

View File

@@ -1,5 +1,5 @@
# Description: Automated weekly documentation accuracy check and update via Claude
name: "Weekly Documentation Check"
description: "Automated weekly documentation accuracy check and update via Claude"
permissions:
contents: write

View File

@@ -64,7 +64,6 @@ const config: StorybookConfig = {
deep: true,
extensions: ['vue']
})
// Note: Explicitly NOT including generateImportMapPlugin to avoid externalization
],
server: {
allowedHosts: true

View File

@@ -1,8 +1,11 @@
# Global Ownership
* @Comfy-org/comfy_frontend_devs
# Desktop/Electron
/apps/desktop-ui/ @webfiltered
/src/stores/electronDownloadStore.ts @webfiltered
/src/extensions/core/electronAdapter.ts @webfiltered
/vite.electron.config.mts @webfiltered
/apps/desktop-ui/ @benceruleanlu
/src/stores/electronDownloadStore.ts @benceruleanlu
/src/extensions/core/electronAdapter.ts @benceruleanlu
/vite.electron.config.mts @benceruleanlu
# Common UI Components
/src/components/chip/ @viva-jinyi
@@ -31,10 +34,7 @@
/src/components/graph/selectionToolbox/ @Myestery
# Minimap
/src/renderer/extensions/minimap/ @jtydhr88
# Assets
/src/platform/assets/ @arjansingh
/src/renderer/extensions/minimap/ @jtydhr88 @Myestery
# Workflow Templates
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
@@ -53,7 +53,7 @@
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
# Translations
/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer
/src/locales/ @Yorha4D @KarryCharon @shinshin86 @Comfy-Org/comfy_maintainer @Comfy-org/comfy_frontend_devs
# LLM Instructions (blank on purpose)
.claude/

View File

@@ -0,0 +1,229 @@
# Feature Flags System Explanation
## Overview
The `useFeatureFlag` hook (actually named `useFeatureFlags`) is a Vue 3 composable that provides **reactive access to server-side feature flags** received via WebSocket from the backend. It enables capability negotiation between frontend and backend, allowing the UI to adapt based on what features the server supports.
## Architecture Flow
```
1. Frontend connects via WebSocket
2. Frontend sends client feature flags (first message)
3. Backend responds with server feature flags
4. Frontend stores flags in api.serverFeatureFlags
5. Components use useFeatureFlags() to access flags reactively
```
## Core Implementation
### 1. The `useFeatureFlags` Composable
**Location:** `src/composables/useFeatureFlags.ts`
The composable returns two things:
#### A. Predefined `flags` Object
A reactive object with getter properties for commonly-used feature flags:
```typescript
const { flags } = useFeatureFlags()
// Access predefined flags
flags.supportsPreviewMetadata // boolean | undefined
flags.maxUploadSize // number | undefined
flags.supportsManagerV4 // boolean | undefined
flags.modelUploadButtonEnabled // boolean (checks remoteConfig first)
flags.assetUpdateOptionsEnabled // boolean (checks remoteConfig first)
```
**Key Points:**
- Uses Vue's `reactive()` to make the object reactive
- Each getter calls `api.getServerFeature()` which reads from `api.serverFeatureFlags`
- Some flags (like `modelUploadButtonEnabled`) check `remoteConfig` first (from `/api/features` endpoint) before falling back to WebSocket flags
- Returns a `readonly()` wrapper to prevent external mutation
#### B. Generic `featureFlag` Function
A function that creates a computed ref for any feature flag path:
```typescript
const { featureFlag } = useFeatureFlags()
// Create a reactive computed ref for any flag
const myFlag = featureFlag('custom.feature.path', false) // defaultValue is optional
// myFlag is a ComputedRef that updates when serverFeatureFlags changes
```
**Key Points:**
- Accepts any string path (supports dot notation for nested values)
- Returns a `computed()` ref that automatically updates when flags change
- Generic type parameter allows type safety: `featureFlag<boolean>('flag', false)`
### 2. The Underlying API Layer
**Location:** `src/scripts/api.ts`
The `ComfyApi` class manages feature flags:
```typescript
class ComfyApi {
// Stores flags received from backend
serverFeatureFlags: Record<string, unknown> = {}
// Retrieves a flag value using dot notation
getServerFeature<T>(featureName: string, defaultValue?: T): T {
return get(this.serverFeatureFlags, featureName, defaultValue) as T
}
}
```
**How Flags Are Received:**
1. WebSocket connection is established
2. Frontend sends client feature flags as first message
3. Backend responds with a `feature_flags` message type
4. The message handler stores it: `this.serverFeatureFlags = msg.data`
**The `get` Function:**
- Uses `es-toolkit/compat`'s `get` function (lodash-style)
- Supports dot notation: `'extension.manager.supports_v4'` accesses nested objects
- Returns `defaultValue` if the path doesn't exist
### 3. Remote Config Integration
**Location:** `src/platform/remoteConfig/remoteConfig.ts`
Some flags check `remoteConfig` first (loaded from `/api/features` endpoint):
```typescript
// Example from modelUploadButtonEnabled
return (
remoteConfig.value.model_upload_button_enabled ?? // Check remote config first
api.getServerFeature(ServerFeatureFlag.MODEL_UPLOAD_BUTTON_ENABLED, false) // Fallback
)
```
**Why Two Sources?**
- `remoteConfig`: Fetched via HTTP at app startup, can be updated without WebSocket
- WebSocket flags: Real-time capability negotiation, updated on reconnection
## Usage Patterns
### Pattern 1: Using Predefined Flags
```typescript
import { useFeatureFlags } from '@/composables/useFeatureFlags'
const { flags } = useFeatureFlags()
// In template
if (flags.supportsPreviewMetadata) {
// Use enhanced preview feature
}
// In script
const maxSize = flags.maxUploadSize ?? 100 * 1024 * 1024 // Default 100MB
```
### Pattern 2: Using Generic featureFlag Function
```typescript
import { useFeatureFlags } from '@/composables/useFeatureFlags'
const { featureFlag } = useFeatureFlags()
// Create a reactive computed ref
const customFeature = featureFlag<boolean>('extension.custom.feature', false)
// Use in template (automatically reactive)
// <div v-if="customFeature">New Feature UI</div>
// Use in script
watch(customFeature, (enabled) => {
if (enabled) {
// Feature was enabled
}
})
```
### Pattern 3: Direct API Access (Non-Reactive)
```typescript
import { api } from '@/scripts/api'
// Direct access (not reactive, use sparingly)
if (api.serverSupportsFeature('supports_preview_metadata')) {
// Feature is supported
}
const maxSize = api.getServerFeature('max_upload_size', 100 * 1024 * 1024)
```
## Reactivity Explained
The composable is **reactive** because:
1. **Predefined flags**: Use `reactive()` with getters, so when `api.serverFeatureFlags` changes, Vue's reactivity system detects it
2. **Generic featureFlag**: Returns `computed()`, which automatically tracks `api.getServerFeature()` calls and re-evaluates when flags change
3. **WebSocket updates**: When flags are updated via WebSocket, `api.serverFeatureFlags` is reassigned, triggering reactivity
## Adding New Feature Flags
### Step 1: Add to Enum (if it's a core flag)
```typescript
// In useFeatureFlags.ts
export enum ServerFeatureFlag {
// ... existing flags
MY_NEW_FEATURE = 'my_new_feature'
}
```
### Step 2: Add to flags Object (if commonly used)
```typescript
// In useFeatureFlags.ts flags object
get myNewFeature() {
return api.getServerFeature(ServerFeatureFlag.MY_NEW_FEATURE, false)
}
```
### Step 3: Use in Components
```typescript
const { flags } = useFeatureFlags()
if (flags.myNewFeature) {
// Use the feature
}
```
**OR** use the generic function without modifying the composable:
```typescript
const { featureFlag } = useFeatureFlags()
const myFeature = featureFlag('my_new_feature', false)
```
## Important Notes
1. **Flags are server-driven**: The backend controls which flags are available
2. **Default values**: Always provide sensible defaults when using `getServerFeature()`
3. **Reactivity**: The composable ensures UI updates automatically when flags change (e.g., on WebSocket reconnection)
4. **Type safety**: Use TypeScript generics with `featureFlag<T>()` for type safety
5. **Dot notation**: Feature flags can be nested, use dot notation: `'extension.manager.supports_v4'`
6. **Remote config priority**: Some flags check `remoteConfig` first, then fall back to WebSocket flags
## Testing
See `tests-ui/tests/composables/useFeatureFlags.test.ts` for examples of:
- Mocking `api.getServerFeature()`
- Testing reactive behavior
- Testing default values
- Testing nested paths
## Related Files
- `src/composables/useFeatureFlags.ts` - The main composable
- `src/scripts/api.ts` - API layer with `getServerFeature()` method
- `src/platform/remoteConfig/remoteConfig.ts` - Remote config integration
- `docs/FEATURE_FLAGS.md` - Full system documentation
- `tests-ui/tests/composables/useFeatureFlags.test.ts` - Unit tests

196
FEATURE_FLAG_PAYLOAD.md Normal file
View File

@@ -0,0 +1,196 @@
# Feature Flag Payload Shape
## Feature Flag Key
`demo-run-button-experiment`
## Expected Structure
The feature flag value should be either:
### Control (Original Button)
- `false`
- `null`
- `undefined`
- Not set
When any of these values are present, the original SplitButton will be displayed.
### Experiment (Experimental Button)
An object with the following structure:
```typescript
{
variant: string, // Required: variant name (e.g., "bold-gradient", "animated", "playful", "minimal")
payload?: { // Optional: styling and content overrides
label?: string, // Button text (default: "Run")
icon?: string, // Icon class (default: variant-specific)
backgroundColor?: string, // Background color/class (default: variant-specific)
textColor?: string, // Text color/class (default: variant-specific)
borderRadius?: string, // Border radius class (default: variant-specific)
padding?: string // Padding class (default: "px-4 py-2")
}
}
```
## Example Payloads
### Bold Gradient Variant
```json
{
"variant": "bold-gradient",
"payload": {
"label": "Run",
"icon": "icon-[lucide--zap]",
"backgroundColor": "transparent",
"textColor": "white",
"borderRadius": "rounded-xl"
}
}
```
### Animated Variant
```json
{
"variant": "animated",
"payload": {
"label": "Launch",
"icon": "icon-[lucide--rocket]",
"backgroundColor": "bg-primary-background",
"textColor": "white",
"borderRadius": "rounded-full"
}
}
```
### Playful Variant
```json
{
"variant": "playful",
"payload": {
"label": "Go!",
"icon": "icon-[lucide--sparkles]",
"backgroundColor": "bg-gradient-to-br from-yellow-400 to-orange-500",
"textColor": "white",
"borderRadius": "rounded-2xl"
}
}
```
### Minimal Variant
```json
{
"variant": "minimal",
"payload": {
"label": "Run",
"icon": "icon-[lucide--play]",
"backgroundColor": "bg-white",
"textColor": "text-gray-800",
"borderRadius": "rounded-md"
}
}
```
## Payload Properties
### `variant` (required)
- Type: `string`
- Description: The variant name that determines the base styling and behavior
- Supported values:
- `"bold-gradient"` - Gradient background with animated effect
- `"animated"` - Pulsing animation effect
- `"playful"` - Sparkle effects with playful styling
- `"minimal"` - Clean, minimal design
- Any custom variant name (will use default styling)
### `payload.label` (optional)
- Type: `string`
- Default: `"Run"` (or variant-specific default)
- Description: The text displayed on the button
### `payload.icon` (optional)
- Type: `string`
- Default: Variant-specific icon
- Description: Icon class name (e.g., `"icon-[lucide--play]"`)
- Examples:
- `"icon-[lucide--play]"` - Play icon
- `"icon-[lucide--zap]"` - Lightning bolt
- `"icon-[lucide--rocket]"` - Rocket
- `"icon-[lucide--sparkles]"` - Sparkles
### `payload.backgroundColor` (optional)
- Type: `string`
- Default: Variant-specific background
- Description: Tailwind CSS class or CSS color value for background
- Examples:
- `"bg-primary-background"` - Primary background color
- `"bg-white"` - White background
- `"transparent"` - Transparent (for gradient overlays)
- `"bg-gradient-to-br from-yellow-400 to-orange-500"` - Gradient
### `payload.textColor` (optional)
- Type: `string`
- Default: Variant-specific text color
- Description: Tailwind CSS class or CSS color value for text
- Examples:
- `"white"` - White text (CSS color)
- `"text-white"` - White text (Tailwind class)
- `"text-gray-800"` - Dark gray text
### `payload.borderRadius` (optional)
- Type: `string`
- Default: Variant-specific border radius
- Description: Tailwind CSS border radius class
- Examples:
- `"rounded-md"` - Medium border radius
- `"rounded-xl"` - Extra large border radius
- `"rounded-full"` - Fully rounded (pill shape)
- `"rounded-2xl"` - 2x extra large border radius
### `payload.padding` (optional)
- Type: `string`
- Default: `"px-4 py-2"`
- Description: Tailwind CSS padding classes
- Examples:
- `"px-4 py-2"` - Standard padding
- `"px-6 py-3"` - Larger padding
- `"px-2 py-1"` - Smaller padding
## Backend Integration
The backend should send this feature flag via WebSocket in the `feature_flags` message:
```json
{
"type": "feature_flags",
"data": {
"demo-run-button-experiment": {
"variant": "bold-gradient",
"payload": {
"label": "Run",
"icon": "icon-[lucide--zap]",
"textColor": "white",
"borderRadius": "rounded-xl"
}
}
}
}
```
Or to show the control (original button):
```json
{
"type": "feature_flags",
"data": {
"demo-run-button-experiment": false
}
}
```
## Notes
- All `payload` properties are optional - if omitted, variant-specific defaults will be used
- The `variant` property is required when the flag is truthy
- Color values can be either Tailwind classes (e.g., `"text-white"`) or CSS color values (e.g., `"white"`)
- The component will automatically handle both formats

View File

@@ -1,6 +1,6 @@
{
"name": "@comfyorg/desktop-ui",
"version": "0.0.3",
"version": "0.0.4",
"type": "module",
"nx": {
"tags": [

View File

@@ -22,7 +22,11 @@
<h1 v-if="title" class="font-inter font-bold text-3xl text-neutral-300">
{{ title }}
</h1>
<p v-if="statusText" class="text-lg text-neutral-400">
<p
v-if="statusText"
class="text-lg text-neutral-400"
data-testid="startup-status-text"
>
{{ statusText }}
</p>
</div>

View File

@@ -557,7 +557,7 @@ export class ComfyPage {
async dragAndDrop(source: Position, target: Position) {
await this.page.mouse.move(source.x, source.y)
await this.page.mouse.down()
await this.page.mouse.move(target.x, target.y)
await this.page.mouse.move(target.x, target.y, { steps: 100 })
await this.page.mouse.up()
await this.nextFrame()
}

View File

@@ -65,7 +65,9 @@ export class VueNodeHelpers {
* Select a specific Vue node by ID
*/
async selectNode(nodeId: string): Promise<void> {
await this.page.locator(`[data-node-id="${nodeId}"]`).click()
await this.page
.locator(`[data-node-id="${nodeId}"] .lg-node-header`)
.click()
}
/**
@@ -77,11 +79,13 @@ export class VueNodeHelpers {
// Select first node normally
await this.selectNode(nodeIds[0])
// Add additional nodes with Ctrl+click
// Add additional nodes with Ctrl+click on header
for (let i = 1; i < nodeIds.length; i++) {
await this.page.locator(`[data-node-id="${nodeIds[i]}"]`).click({
modifiers: ['Control']
})
await this.page
.locator(`[data-node-id="${nodeIds[i]}"] .lg-node-header`)
.click({
modifiers: ['Control']
})
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

@@ -0,0 +1,54 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
test.describe('Vue Node Resizing', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
})
test('should resize node without position drift after selecting', async ({
comfyPage
}) => {
// Get a Vue node fixture
const node = await comfyPage.vueNodes.getFixtureByTitle('Load Checkpoint')
const initialBox = await node.boundingBox()
if (!initialBox) throw new Error('Node bounding box not found')
// Select the node first (this was causing the bug)
await node.header.click()
await comfyPage.page.waitForTimeout(100) // Brief pause after selection
// Get position after selection
const selectedBox = await node.boundingBox()
if (!selectedBox)
throw new Error('Node bounding box not found after select')
// Verify position unchanged after selection
expect(selectedBox.x).toBeCloseTo(initialBox.x, 1)
expect(selectedBox.y).toBeCloseTo(initialBox.y, 1)
// Now resize from bottom-right corner
const resizeStartX = selectedBox.x + selectedBox.width - 5
const resizeStartY = selectedBox.y + selectedBox.height - 5
await comfyPage.page.mouse.move(resizeStartX, resizeStartY)
await comfyPage.page.mouse.down()
await comfyPage.page.mouse.move(resizeStartX + 50, resizeStartY + 30)
await comfyPage.page.mouse.up()
// Get final position and size
const finalBox = await node.boundingBox()
if (!finalBox) throw new Error('Node bounding box not found after resize')
// Position should NOT have changed (the bug was position drift)
expect(finalBox.x).toBeCloseTo(initialBox.x, 1)
expect(finalBox.y).toBeCloseTo(initialBox.y, 1)
// Size should have increased
expect(finalBox.width).toBeGreaterThan(initialBox.width)
expect(finalBox.height).toBeGreaterThan(initialBox.height)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

View File

@@ -1,49 +0,0 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../../fixtures/ComfyPage'
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
})
test.describe('Vue Nodes - LOD', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.setSetting('LiteGraph.Canvas.MinFontSizeForLOD', 8)
await comfyPage.setup()
await comfyPage.loadWorkflow('default')
})
test('should toggle LOD based on zoom threshold', async ({ comfyPage }) => {
await comfyPage.vueNodes.waitForNodes()
const initialNodeCount = await comfyPage.vueNodes.getNodeCount()
expect(initialNodeCount).toBeGreaterThan(0)
await expect(comfyPage.canvas).toHaveScreenshot('vue-nodes-default.png')
const vueNodesContainer = comfyPage.vueNodes.nodes
const textboxesInNodes = vueNodesContainer.getByRole('textbox')
const comboboxesInNodes = vueNodesContainer.getByRole('combobox')
await expect(textboxesInNodes.first()).toBeVisible()
await expect(comboboxesInNodes.first()).toBeVisible()
await comfyPage.zoom(120, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('vue-nodes-lod-active.png')
await expect(textboxesInNodes.first()).toBeHidden()
await expect(comboboxesInNodes.first()).toBeHidden()
await comfyPage.zoom(-120, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-nodes-lod-inactive.png'
)
await expect(textboxesInNodes.first()).toBeVisible()
await expect(comboboxesInNodes.first()).toBeVisible()
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -88,12 +88,14 @@ export function comfyAPIPlugin(isDev: boolean): Plugin {
if (result.exports.length > 0) {
const projectRoot = process.cwd()
const relativePath = path.relative(path.join(projectRoot, 'src'), id)
const relativePath = path
.relative(path.join(projectRoot, 'src'), id)
.replace(/\\/g, '/')
const shimFileName = relativePath.replace(/\.ts$/, '.js')
let shimContent = `// Shim for ${relativePath}\n`
const fileKey = relativePath.replace(/\.ts$/, '').replace(/\\/g, '/')
const fileKey = relativePath.replace(/\.ts$/, '')
const warningMessage = getWarningMessage(fileKey, shimFileName)
if (warningMessage) {

View File

@@ -1,154 +0,0 @@
import glob from 'fast-glob'
import fs from 'fs-extra'
import { dirname, join } from 'node:path'
import { type HtmlTagDescriptor, type Plugin, normalizePath } from 'vite'
interface ImportMapSource {
name: string
pattern: string | RegExp
entry: string
recursiveDependence?: boolean
override?: Record<string, Partial<ImportMapSource>>
}
const parseDeps = (root: string, pkg: string) => {
const pkgPath = join(root, 'node_modules', pkg, 'package.json')
if (fs.existsSync(pkgPath)) {
const content = fs.readFileSync(pkgPath, 'utf-8')
const pkg = JSON.parse(content)
return Object.keys(pkg.dependencies || {})
}
return []
}
/**
* Vite plugin that generates an import map for vendor chunks.
*
* This plugin creates a browser-compatible import map that maps module specifiers
* (like 'vue' or 'primevue') to their actual file locations in the build output.
* This improves module loading in modern browsers and enables better caching.
*
* The plugin:
* 1. Tracks vendor chunks during bundle generation
* 2. Creates mappings between module names and their file paths
* 3. Injects an import map script tag into the HTML head
* 4. Configures manual chunk splitting for vendor libraries
*
* @param vendorLibraries - An array of vendor libraries to split into separate chunks
* @returns {Plugin} A Vite plugin that generates and injects an import map
*/
export function generateImportMapPlugin(
importMapSources: ImportMapSource[]
): Plugin {
const importMapEntries: Record<string, string> = {}
const resolvedImportMapSources: Map<string, ImportMapSource> = new Map()
const assetDir = 'assets/lib'
let root: string
return {
name: 'generate-import-map-plugin',
// Configure manual chunks during the build process
configResolved(config) {
root = config.root
if (config.build) {
// Ensure rollupOptions exists
if (!config.build.rollupOptions) {
config.build.rollupOptions = {}
}
for (const source of importMapSources) {
resolvedImportMapSources.set(source.name, source)
if (source.recursiveDependence) {
const deps = parseDeps(root, source.name)
while (deps.length) {
const dep = deps.shift()!
const depSource = Object.assign({}, source, {
name: dep,
pattern: dep,
...source.override?.[dep]
})
resolvedImportMapSources.set(depSource.name, depSource)
const _deps = parseDeps(root, depSource.name)
deps.unshift(..._deps)
}
}
}
const external: (string | RegExp)[] = []
for (const [, source] of resolvedImportMapSources) {
external.push(source.pattern)
}
config.build.rollupOptions.external = external
}
},
generateBundle(_options) {
for (const [, source] of resolvedImportMapSources) {
if (source.entry) {
const moduleFile = join(source.name, source.entry)
const sourceFile = join(root, 'node_modules', moduleFile)
const targetFile = join(root, 'dist', assetDir, moduleFile)
importMapEntries[source.name] =
'./' + normalizePath(join(assetDir, moduleFile))
const targetDir = dirname(targetFile)
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true })
}
fs.copyFileSync(sourceFile, targetFile)
}
if (source.recursiveDependence) {
const files = glob.sync(['**/*.{js,mjs}'], {
cwd: join(root, 'node_modules', source.name)
})
for (const file of files) {
const moduleFile = join(source.name, file)
const sourceFile = join(root, 'node_modules', moduleFile)
const targetFile = join(root, 'dist', assetDir, moduleFile)
importMapEntries[normalizePath(join(source.name, dirname(file)))] =
'./' + normalizePath(join(assetDir, moduleFile))
const targetDir = dirname(targetFile)
if (!fs.existsSync(targetDir)) {
fs.mkdirSync(targetDir, { recursive: true })
}
fs.copyFileSync(sourceFile, targetFile)
}
}
}
},
transformIndexHtml(html) {
if (Object.keys(importMapEntries).length === 0) {
console.warn(
'[ImportMap Plugin] No vendor chunks found to create import map.'
)
return html
}
const importMap = {
imports: importMapEntries
}
const importMapTag: HtmlTagDescriptor = {
tag: 'script',
attrs: { type: 'importmap' },
children: JSON.stringify(importMap, null, 2),
injectTo: 'head'
}
return {
html,
tags: [importMapTag]
}
}
}
}

View File

@@ -1,2 +1 @@
export { comfyAPIPlugin } from './comfyAPIPlugin'
export { generateImportMapPlugin } from './generateImportMapPlugin'

24
cloud-loader-dropdown.md Normal file
View File

@@ -0,0 +1,24 @@
Fixes loader dropdown placeholder
===============================
Cloud loader dropdowns hydrate via `useAssetWidgetData(nodeType)`, so `dropdownItems` stays empty until the Asset API returns friendly filenames. Meanwhile `modelValue` already holds the saved asset and the watcher at [WidgetSelectDropdown.vue#L215-L227](https://github.com/Comfy-Org/ComfyUI_frontend/blob/main/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue#L215-L227) only tracks `modelValue`. It runs before assets load, fails to find a match, clears `selectedSet`, and the placeholder persists.
```ts
watch(
modelValue,
(currentValue) => {
if (currentValue === undefined) {
selectedSet.value.clear()
return
}
const item = dropdownItems.value.find((item) => item.name === currentValue)
if (item) {
selectedSet.value.clear()
selectedSet.value.add(item.id)
}
},
{ immediate: true }
)
```
Once the API resolves, `dropdownItems` recomputes but nothing resyncs because the watcher never sees that change. Desktop doesnt hit this because it still reads from `widget.options.values` immediately.

View File

@@ -191,6 +191,19 @@ export default defineConfig([
'@intlify/vue-i18n/no-raw-text': [
'error',
{
attributes: {
'/.+/': [
'aria-label',
'aria-placeholder',
'aria-roledescription',
'aria-valuetext',
'label',
'placeholder',
'title',
'v-tooltip'
],
img: ['alt']
},
// Ignore strings that are:
// 1. Less than 2 characters
// 2. Only symbols/numbers/whitespace (no letters)
@@ -200,24 +213,27 @@ export default defineConfig([
ignoreNodes: ['md-icon', 'v-icon', 'pre', 'code', 'script', 'style'],
// Brand names and technical terms that shouldn't be translated
ignoreText: [
'ComfyUI',
'GitHub',
'OpenAI',
'API',
'URL',
'JSON',
'YAML',
'GPU',
'CPU',
'RAM',
'GB',
'MB',
'KB',
'ms',
'fps',
'px',
'App Data:',
'App Path:'
'App Path:',
'ComfyUI',
'CPU',
'fps',
'GB',
'GitHub',
'GPU',
'JSON',
'KB',
'LoRA',
'MB',
'ms',
'OpenAI',
'png',
'px',
'RAM',
'URL',
'YAML',
'1.2 MB'
]
}
]

View File

@@ -0,0 +1,56 @@
<template>
<!-- If user has active subscription, show ComfyQueueButton (which already has FeatureFlaggedRunButton) -->
<ComfyQueueButton v-if="isActiveSubscription" />
<!-- If subscription required but not active, wrap SubscribeToRunButton with feature flag -->
<FeatureFlaggedRunButton
v-else
flag-key="demo-run-button-experiment"
:on-click="handleSubscribeClick"
>
<template #control>
<!-- SubscribeToRunButton handles its own click (shows subscription dialog) -->
<SubscribeToRunButton />
</template>
</FeatureFlaggedRunButton>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useCommandStore } from '@/stores/commandStore'
import ComfyQueueButton from './ComfyQueueButton.vue'
import FeatureFlaggedRunButton from './FeatureFlaggedRunButton.vue'
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
// Get subscription status - replace with actual subscription store/composable
const isActiveSubscription = computed(() => {
// TODO: Replace with actual subscription check
// Example: return useSubscriptionStore().isActiveSubscription
// For now, this would typically come from a store or composable
return false
})
const commandStore = useCommandStore()
// Handle click for experimental button when SubscribeToRunButton is shown
// For experimental variants, trigger queue prompt (same as ComfyQueueButton)
// For control, SubscribeToRunButton handles its own click (shows subscription dialog)
const handleSubscribeClick = async (e: Event) => {
// If this is called from the experimental button, trigger queue prompt
// If called from SubscribeToRunButton (control), it will handle its own logic
const isShiftPressed = 'shiftKey' in e && e.shiftKey
const commandId = isShiftPressed
? 'Comfy.QueuePromptFront'
: 'Comfy.QueuePrompt'
await commandStore.execute(commandId, {
metadata: {
subscribe_to_run: false,
trigger_source: 'button'
}
})
}
</script>

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.33.4",
"version": "1.33.9",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -75,6 +75,7 @@
"@vitest/coverage-v8": "catalog:",
"@vitest/ui": "catalog:",
"@vue/test-utils": "catalog:",
"@webgpu/types": "catalog:",
"cross-env": "catalog:",
"eslint": "catalog:",
"eslint-config-prettier": "catalog:",
@@ -112,6 +113,7 @@
"typescript": "catalog:",
"typescript-eslint": "catalog:",
"unplugin-icons": "catalog:",
"unplugin-typegpu": "catalog:",
"unplugin-vue-components": "catalog:",
"uuid": "^11.1.0",
"vite": "catalog:",
@@ -162,7 +164,6 @@
"es-toolkit": "^1.39.9",
"extendable-media-recorder": "^9.2.27",
"extendable-media-recorder-wav-encoder": "^7.0.129",
"fast-glob": "^3.3.3",
"firebase": "catalog:",
"fuse.js": "^7.0.0",
"glob": "^11.0.3",
@@ -176,6 +177,7 @@
"semver": "^7.7.2",
"three": "^0.170.0",
"tiptap-markdown": "^0.8.10",
"typegpu": "catalog:",
"vue": "catalog:",
"vue-i18n": "catalog:",
"vue-router": "catalog:",

View File

@@ -1329,57 +1329,6 @@ audio.comfy-audio.empty-audio-widget {
will-change: transform;
}
/* START LOD specific styles */
/* LOD styles - Custom CSS avoids 100+ Tailwind selectors that would slow style recalculation when .isLOD toggles */
.isLOD .lg-node {
box-shadow: none;
filter: none;
backdrop-filter: none;
text-shadow: none;
mask-image: none;
clip-path: none;
background-image: none;
text-rendering: optimizeSpeed;
border-radius: 0;
contain: layout style;
transition: none;
}
.isLOD .lg-node-header {
border-radius: 0;
pointer-events: none;
}
.isLOD .lg-node-widgets {
pointer-events: none;
}
.lod-toggle {
visibility: visible;
}
.isLOD .lod-toggle {
visibility: hidden;
}
.lod-fallback {
display: none;
}
.isLOD .lod-fallback {
display: block;
}
.isLOD .image-preview img {
image-rendering: pixelated;
}
.isLOD .slot-dot {
border-radius: 0;
}
/* END LOD specific styles */
/* ===================== Mask Editor Styles ===================== */
/* To be migrated to Tailwind later */
#maskEditor_brush {

View File

@@ -75,6 +75,17 @@ export function formatSize(value?: number) {
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
}
/**
* Formats a commit hash by truncating long (40-char) hashes to 7 chars.
* Returns the original string if not a valid full commit hash.
*/
export function formatCommitHash(value: string): string {
if (/^[a-f0-9]{40}$/i.test(value)) {
return value.slice(0, 7)
}
return value
}
/**
* Returns various filename components.
* Example:

103
pnpm-lock.yaml generated
View File

@@ -15,15 +15,9 @@ catalogs:
'@eslint/js':
specifier: ^9.35.0
version: 9.35.0
'@iconify-json/lucide':
specifier: ^1.1.178
version: 1.2.66
'@iconify/json':
specifier: ^2.2.380
version: 2.2.380
'@iconify/tailwind':
specifier: ^1.1.3
version: 1.2.0
'@intlify/eslint-plugin-vue-i18n':
specifier: ^4.1.0
version: 4.1.0
@@ -126,6 +120,9 @@ catalogs:
'@vueuse/integrations':
specifier: ^13.9.0
version: 13.9.0
'@webgpu/types':
specifier: ^0.1.66
version: 0.1.66
algoliasearch:
specifier: ^5.21.0
version: 5.21.0
@@ -246,6 +243,9 @@ catalogs:
tw-animate-css:
specifier: ^1.3.8
version: 1.3.8
typegpu:
specifier: ^0.8.2
version: 0.8.2
typescript:
specifier: ^5.9.2
version: 5.9.2
@@ -255,6 +255,9 @@ catalogs:
unplugin-icons:
specifier: ^0.22.0
version: 0.22.0
unplugin-typegpu:
specifier: 0.8.0
version: 0.8.0
unplugin-vue-components:
specifier: ^0.28.0
version: 0.28.0
@@ -422,9 +425,6 @@ importers:
extendable-media-recorder-wav-encoder:
specifier: ^7.0.129
version: 7.0.129
fast-glob:
specifier: ^3.3.3
version: 3.3.3
firebase:
specifier: 'catalog:'
version: 11.6.0
@@ -464,6 +464,9 @@ importers:
tiptap-markdown:
specifier: ^0.8.10
version: 0.8.10(@tiptap/core@2.10.4(@tiptap/pm@2.10.4))
typegpu:
specifier: 'catalog:'
version: 0.8.2
vue:
specifier: 'catalog:'
version: 3.5.13(typescript@5.9.2)
@@ -561,6 +564,9 @@ importers:
'@vue/test-utils':
specifier: 'catalog:'
version: 2.4.6
'@webgpu/types':
specifier: 'catalog:'
version: 0.1.66
cross-env:
specifier: 'catalog:'
version: 10.1.0
@@ -672,6 +678,9 @@ importers:
unplugin-icons:
specifier: 'catalog:'
version: 0.22.0(@vue/compiler-sfc@3.5.13)
unplugin-typegpu:
specifier: 'catalog:'
version: 0.8.0(typegpu@0.8.2)
unplugin-vue-components:
specifier: 'catalog:'
version: 0.28.0(@babel/parser@7.28.4)(rollup@4.22.4)(vue@3.5.13(typescript@5.9.2))
@@ -1431,6 +1440,10 @@ packages:
resolution: {integrity: sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==}
engines: {node: '>=6.9.0'}
'@babel/standalone@7.28.5':
resolution: {integrity: sha512-1DViPYJpRU50irpGMfLBQ9B4kyfQuL6X7SS7pwTeWeZX0mNkjzPi0XFqxCjSdddZXUQy4AhnQnnesA/ZHnvAdw==}
engines: {node: '>=6.9.0'}
'@babel/template@7.27.2':
resolution: {integrity: sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==}
engines: {node: '>=6.9.0'}
@@ -3790,8 +3803,8 @@ packages:
peerDependencies:
vue: ^3.5.0
'@webgpu/types@0.1.51':
resolution: {integrity: sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==}
'@webgpu/types@0.1.66':
resolution: {integrity: sha512-YA2hLrwLpDsRueNDXIMqN9NTzD6bCDkuXbOSe0heS+f8YE8usA6Gbv1prj81pzVHrbaAma7zObnIC+I6/sXJgA==}
'@xstate/fsm@1.6.5':
resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==}
@@ -6038,6 +6051,10 @@ packages:
resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==}
hasBin: true
magic-string-ast@1.0.3:
resolution: {integrity: sha512-CvkkH1i81zl7mmb94DsRiFeG9V2fR2JeuK8yDgS8oiZSFa++wWLEgZ5ufEOyLHbvSbD1gTRKv9NdX69Rnvr9JA==}
engines: {node: '>=20.19.0'}
magic-string@0.30.19:
resolution: {integrity: sha512-2N21sPY9Ws53PZvsEpVtNuSW+ScYbQdp4b9qUaL+9QkHUrGFKo56Lg9Emg5s9V/qrtNBmiR01sYhUOwu3H+VOw==}
@@ -7411,6 +7428,14 @@ packages:
tinybench@2.9.0:
resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==}
tinyest-for-wgsl@0.1.3:
resolution: {integrity: sha512-Wm5ADG1UyDxykf42S1gLYP4U9e1QP/TdtJeovQi6y68zttpiFLKqQGioHmPs9Mjysh7YMSAr/Lpuk0cD2MVdGA==}
engines: {node: '>=12.20.0'}
tinyest@0.1.2:
resolution: {integrity: sha512-aHRmouyowIq1P5jrTF+YK6pGX+WuvFtSCLbqk91yHnU3SWQRIcNIamZLM5XF6lLqB13AWz0PGPXRff2QGDsxIg==}
engines: {node: '>=12.20.0'}
tinyexec@0.3.2:
resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==}
@@ -7537,6 +7562,13 @@ packages:
resolution: {integrity: sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==}
engines: {node: '>= 0.4'}
typed-binary@4.3.2:
resolution: {integrity: sha512-HT3pIBM2njCZUmeczDaQUUErGiM6GXFCqMsHegE12HCoBtvHCkfR10JJni0TeGOTnLilTd6YFyj+YhflqQDrDQ==}
typegpu@0.8.2:
resolution: {integrity: sha512-wkMJWhJE0pSkw2G/FesjqjbtHkREyOKu1Zmyj19xfmaX5+65YFwgfQNKSK8CxqN4kJkP7JFelLDJTSYY536TYg==}
engines: {node: '>=12.20.0'}
typescript-eslint@8.44.0:
resolution: {integrity: sha512-ib7mCkYuIzYonCq9XWF5XNw+fkj2zg629PSa9KNIQ47RXFF763S5BIX4wqz1+FLPogTZoiw8KmCiRPRa8bL3qw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
@@ -7641,6 +7673,11 @@ packages:
vue-template-es2015-compiler:
optional: true
unplugin-typegpu@0.8.0:
resolution: {integrity: sha512-VJHdXSXGOkAx0WhwFczhVUjAI6HyDkrQXk20HnwyuzIE3FdqE5l9sJTCYZzoVGo3z8i/IA5TMHCDzzP0Bc97Cw==}
peerDependencies:
typegpu: ^0.8.0
unplugin-vue-components@0.28.0:
resolution: {integrity: sha512-jiTGtJ3JsRFBjgvyilfrX7yUoGKScFgbdNw+6p6kEXU+Spf/rhxzgvdfuMcvhCcLmflB/dY3pGQshYBVGOUx7Q==}
engines: {node: '>=14'}
@@ -7831,8 +7868,8 @@ packages:
vue-component-type-helpers@3.1.1:
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
vue-component-type-helpers@3.1.4:
resolution: {integrity: sha512-Uws7Ew1OzTTqHW8ZVl/qLl/HB+jf08M0NdFONbVWAx0N4gMLK8yfZDgeB77hDnBmaigWWEn5qP8T9BG59jIeyQ==}
vue-component-type-helpers@3.1.5:
resolution: {integrity: sha512-7V3yJuNWW7/1jxCcI1CswnpDsvs02Qcx/N43LkV+ZqhLj2PKj50slUflHAroNkN4UWiYfzMUUUXiNuv9khmSpQ==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
@@ -8969,6 +9006,8 @@ snapshots:
'@babel/runtime@7.28.4': {}
'@babel/standalone@7.28.5': {}
'@babel/template@7.27.2':
dependencies:
'@babel/code-frame': 7.27.1
@@ -10633,7 +10672,7 @@ snapshots:
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
type-fest: 2.19.0
vue: 3.5.13(typescript@5.9.2)
vue-component-type-helpers: 3.1.4
vue-component-type-helpers: 3.1.5
'@swc/helpers@0.5.17':
dependencies:
@@ -11016,7 +11055,7 @@ snapshots:
'@tweenjs/tween.js': 23.1.3
'@types/stats.js': 0.17.3
'@types/webxr': 0.5.20
'@webgpu/types': 0.1.51
'@webgpu/types': 0.1.66
fflate: 0.8.2
meshoptimizer: 0.18.1
@@ -11519,7 +11558,7 @@ snapshots:
dependencies:
vue: 3.5.13(typescript@5.9.2)
'@webgpu/types@0.1.51': {}
'@webgpu/types@0.1.66': {}
'@xstate/fsm@1.6.5': {}
@@ -14000,6 +14039,10 @@ snapshots:
lz-string@1.5.0: {}
magic-string-ast@1.0.3:
dependencies:
magic-string: 0.30.19
magic-string@0.30.19:
dependencies:
'@jridgewell/sourcemap-codec': 1.5.5
@@ -15864,6 +15907,12 @@ snapshots:
tinybench@2.9.0: {}
tinyest-for-wgsl@0.1.3:
dependencies:
tinyest: 0.1.2
tinyest@0.1.2: {}
tinyexec@0.3.2: {}
tinyexec@1.0.1: {}
@@ -15995,6 +16044,13 @@ snapshots:
reflect.getprototypeof: 1.0.10
optional: true
typed-binary@4.3.2: {}
typegpu@0.8.2:
dependencies:
tinyest: 0.1.2
typed-binary: 4.3.2
typescript-eslint@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2):
dependencies:
'@typescript-eslint/eslint-plugin': 8.44.0(@typescript-eslint/parser@8.44.0(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2))(eslint@9.35.0(jiti@2.4.2))(typescript@5.9.2)
@@ -16090,6 +16146,19 @@ snapshots:
transitivePeerDependencies:
- supports-color
unplugin-typegpu@0.8.0(typegpu@0.8.2):
dependencies:
'@babel/standalone': 7.28.5
defu: 6.1.4
estree-walker: 3.0.3
magic-string-ast: 1.0.3
pathe: 2.0.3
picomatch: 4.0.3
tinyest: 0.1.2
tinyest-for-wgsl: 0.1.3
typegpu: 0.8.2
unplugin: 2.3.5
unplugin-vue-components@0.28.0(@babel/parser@7.28.4)(rollup@4.22.4)(vue@3.5.13(typescript@5.9.2)):
dependencies:
'@antfu/utils': 0.7.10
@@ -16370,7 +16439,7 @@ snapshots:
vue-component-type-helpers@3.1.1: {}
vue-component-type-helpers@3.1.4: {}
vue-component-type-helpers@3.1.5: {}
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
dependencies:

View File

@@ -43,6 +43,7 @@ catalog:
'@vue/test-utils': ^2.4.6
'@vueuse/core': ^11.0.0
'@vueuse/integrations': ^13.9.0
'@webgpu/types': ^0.1.66
algoliasearch: ^5.21.0
axios: ^1.8.2
cross-env: ^10.1.0
@@ -83,9 +84,11 @@ catalog:
tailwindcss-primeui: ^0.6.1
tsx: ^4.15.6
tw-animate-css: ^1.3.8
typegpu: ^0.8.2
typescript: ^5.9.2
typescript-eslint: ^8.44.0
unplugin-icons: ^0.22.0
unplugin-typegpu: 0.8.0
unplugin-vue-components: ^0.28.0
vite: ^5.4.19
vite-plugin-dts: ^4.5.4

View File

@@ -1 +1,9 @@
Thanks to OpenArt (https://openart.ai) for providing the sorted-custom-node-map data, captured in September 2024.
Node usage data merged from two sources:
- Mixpanel "app:node_search_result_selected" events (Nov 2025): 1,112 nodes, 46,514 selections
Reflects actual user search behavior - what users choose when searching.
- OpenArt workflow data (Sept 2024): 2,600 nodes, 118,676 uses
Reflects overall popularity - what's used in workflows.
Merge strategy: New data overwrites old for 514 overlapping nodes. Old data
normalized by 2.55x scale factor to match new data. Total: 3,198 nodes.
Search-selected nodes prioritized in ranking.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because it is too large Load Diff

View File

@@ -1,5 +1,10 @@
<template>
<div v-if="!workspaceStore.focusMode" class="ml-1 flex gap-x-0.5 pt-1">
<div
v-if="!workspaceStore.focusMode"
class="ml-1 flex gap-x-0.5 pt-1"
@mouseenter="isTopMenuHovered = true"
@mouseleave="isTopMenuHovered = false"
>
<div class="min-w-0 flex-1">
<SubgraphBreadcrumb />
</div>
@@ -40,7 +45,10 @@
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
<LoginButton v-else-if="isDesktop" />
</div>
<QueueProgressOverlay v-model:expanded="isQueueOverlayExpanded" />
<QueueProgressOverlay
v-model:expanded="isQueueOverlayExpanded"
:menu-hovered="isTopMenuHovered"
/>
</div>
</div>
</template>
@@ -69,6 +77,7 @@ const isDesktop = isElectron()
const { t } = useI18n()
const isQueueOverlayExpanded = ref(false)
const queueStore = useQueueStore()
const isTopMenuHovered = ref(false)
const queuedCount = computed(() => queueStore.pendingTasks.length)
const queueHistoryTooltipConfig = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))

View File

@@ -257,7 +257,7 @@ watch(isDragging, (dragging) => {
})
const actionbarClass = computed(() =>
cn(
'w-[265px] border-dashed border-blue-500 opacity-80',
'w-[200px] border-dashed border-blue-500 opacity-80',
'm-1.5 flex items-center justify-center self-stretch',
'rounded-md before:w-50 before:-ml-50 before:h-full',
'pointer-events-auto',

View File

@@ -1,36 +1,42 @@
<template>
<div class="queue-button-group flex">
<SplitButton
v-tooltip.bottom="{
value: queueButtonTooltip,
showDelay: 600
}"
class="comfyui-queue-button"
:label="String(activeQueueModeMenuItem?.label ?? '')"
severity="primary"
size="small"
:model="queueModeMenuItems"
:disabled="hasMissingNodes"
data-testid="queue-button"
@click="queuePrompt"
<FeatureFlaggedRunButton
flag-key="demo-run-button-experiment"
:on-click="queuePrompt"
>
<template #icon>
<i :class="iconClass" />
</template>
<template #item="{ item }">
<Button
v-tooltip="{
value: item.tooltip,
<template #control>
<SplitButton
v-tooltip.bottom="{
value: queueButtonTooltip,
showDelay: 600
}"
:label="String(item.label ?? '')"
:icon="item.icon"
:severity="item.key === queueMode ? 'primary' : 'secondary'"
class="comfyui-queue-button"
:label="String(activeQueueModeMenuItem?.label ?? '')"
severity="primary"
size="small"
text
/>
:model="queueModeMenuItems"
data-testid="queue-button"
@click="queuePrompt"
>
<template #icon>
<i :class="iconClass" />
</template>
<template #item="{ item }">
<Button
v-tooltip="{
value: item.tooltip,
showDelay: 600
}"
:label="String(item.label ?? '')"
:icon="item.icon"
:severity="item.key === queueMode ? 'primary' : 'secondary'"
size="small"
text
/>
</template>
</SplitButton>
</template>
</SplitButton>
</FeatureFlaggedRunButton>
<BatchCountEdit />
</div>
</template>
@@ -45,17 +51,23 @@ import { useI18n } from 'vue-i18n'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useQueueSettingsStore } from '@/stores/queueStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'
import BatchCountEdit from '../BatchCountEdit.vue'
import FeatureFlaggedRunButton from './FeatureFlaggedRunButton.vue'
const workspaceStore = useWorkspaceStore()
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
const { hasMissingNodes } = useMissingNodes()
const nodeDefStore = useNodeDefStore()
const hasMissingNodes = computed(() =>
graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName)
)
const { t } = useI18n()
const queueModeMenuItemLookup = computed(() => {

View File

@@ -0,0 +1,252 @@
<template>
<div
:class="buttonClasses"
:style="buttonStyles"
class="experimental-run-button relative overflow-hidden transition-all duration-300"
@click="handleClick"
>
<!-- Animated background for gradient variant -->
<div
v-if="variantName === 'bold-gradient'"
class="absolute inset-0 animate-gradient bg-gradient-to-r from-purple-500 via-pink-500 to-orange-500 opacity-90"
/>
<!-- Pulsing animation for animated variant -->
<div
v-if="variantName === 'animated'"
class="absolute inset-0 animate-pulse bg-primary-background opacity-20"
/>
<!-- Sparkle effect for playful variant -->
<div
v-if="variantName === 'playful'"
class="absolute inset-0 overflow-hidden"
>
<i
v-for="i in 3"
:key="i"
:class="sparkleClasses[i - 1]"
class="absolute animate-sparkle text-yellow-300"
:style="sparkleStyles[i - 1]"
>
</i>
</div>
<!-- Button content -->
<div class="relative z-10 flex items-center justify-center gap-2">
<i :class="iconClass" class="text-lg" />
<span :class="labelClasses" :style="labelStyles" class="font-semibold">
{{ label }}
</span>
</div>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import type { FeatureFlagVariant } from '@/composables/useFeatureFlags'
const props = defineProps<{
variant: FeatureFlagVariant
onClick: () => void
}>()
const variantName = computed(() => props.variant.variant)
const payload = computed(() => props.variant.payload || {})
// Extract styling from payload with defaults
const backgroundColor = computed(
() => (payload.value.backgroundColor as string) || getDefaultColor()
)
const textColor = computed(
() => (payload.value.textColor as string) || getDefaultTextColor()
)
const borderRadius = computed(
() => (payload.value.borderRadius as string) || getDefaultBorderRadius()
)
const icon = computed(() => (payload.value.icon as string) || getDefaultIcon())
const label = computed(() => (payload.value.label as string) || 'Run')
const padding = computed(() => (payload.value.padding as string) || 'px-4 py-2')
// Size matching - should match PrimeVue small button size
const buttonSize = computed(() => {
return 'text-sm' // Match PrimeVue small button
})
function getDefaultColor(): string {
switch (variantName.value) {
case 'bold-gradient':
return 'transparent' // Gradient overlay handles it
case 'animated':
return 'bg-primary-background'
case 'playful':
return 'bg-gradient-to-br from-yellow-400 to-orange-500'
case 'minimal':
return 'bg-white border-2 border-gray-300'
default:
return 'bg-primary-background'
}
}
function getDefaultTextColor(): string {
switch (variantName.value) {
case 'bold-gradient':
return 'text-white'
case 'animated':
return 'text-white'
case 'playful':
return 'text-white'
case 'minimal':
return 'text-gray-800'
default:
return 'text-white'
}
}
function getDefaultBorderRadius(): string {
switch (variantName.value) {
case 'bold-gradient':
return 'rounded-xl'
case 'animated':
return 'rounded-full'
case 'playful':
return 'rounded-2xl'
case 'minimal':
return 'rounded-md'
default:
return 'rounded-lg'
}
}
function getDefaultIcon(): string {
switch (variantName.value) {
case 'bold-gradient':
return 'icon-[lucide--zap]'
case 'animated':
return 'icon-[lucide--rocket]'
case 'playful':
return 'icon-[lucide--sparkles]'
case 'minimal':
return 'icon-[lucide--play]'
default:
return 'icon-[lucide--play]'
}
}
const buttonClasses = computed(() => {
const base = [
'cursor-pointer',
'select-none',
'flex',
'items-center',
'justify-center',
padding.value,
borderRadius.value,
'shadow-lg',
'hover:scale-105',
'active:scale-95',
'transition-transform',
buttonSize.value
]
// Add variant-specific classes
if (variantName.value === 'bold-gradient') {
base.push('text-white', 'font-bold')
} else if (variantName.value === 'animated') {
base.push('text-white', 'font-bold', 'hover:shadow-2xl')
} else if (variantName.value === 'playful') {
base.push('text-white', 'font-bold', 'hover:rotate-1')
} else if (variantName.value === 'minimal') {
base.push('bg-white', 'text-gray-800', 'hover:bg-gray-50')
} else {
base.push(backgroundColor.value, textColor.value)
}
return base.join(' ')
})
const buttonStyles = computed(() => {
const styles: Record<string, string> = {}
// Apply custom styles from payload
if (payload.value.backgroundColor && variantName.value !== 'bold-gradient') {
styles.backgroundColor = backgroundColor.value
}
if (payload.value.textColor) {
styles.color = textColor.value
}
if (payload.value.borderRadius && !borderRadius.value.includes('rounded')) {
styles.borderRadius = borderRadius.value
}
return styles
})
const iconClass = computed(() => icon.value)
// Text color handling - can be a CSS class or a color value
const labelClasses = computed(() => {
// If textColor is a Tailwind class, return it; otherwise it's handled by inline styles
if (textColor.value.startsWith('text-')) {
return textColor.value
}
return ''
})
const labelStyles = computed(() => {
const styles: Record<string, string> = {}
// If textColor is not a Tailwind class, use it as a color value
if (!textColor.value.startsWith('text-')) {
styles.color = textColor.value
}
return styles
})
// Sparkle animation positions for playful variant
const sparkleClasses = ['top-2 left-4', 'top-4 right-6', 'bottom-2 left-1/2']
const sparkleStyles = [
{ animationDelay: '0s', animationDuration: '2s' },
{ animationDelay: '0.5s', animationDuration: '2.5s' },
{ animationDelay: '1s', animationDuration: '3s' }
]
function handleClick() {
props.onClick()
}
</script>
<style scoped>
@keyframes gradient {
0%,
100% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
}
.animate-gradient {
background-size: 200% 200%;
animation: gradient 3s ease infinite;
}
@keyframes sparkle {
0%,
100% {
opacity: 0;
transform: scale(0) rotate(0deg);
}
50% {
opacity: 1;
transform: scale(1) rotate(180deg);
}
}
.animate-sparkle {
animation: sparkle 2s ease-in-out infinite;
}
</style>

View File

@@ -0,0 +1,58 @@
<template>
<div class="feature-flagged-run-button">
<!-- Control: Original button -->
<slot v-if="!isExperimentActive" name="control" />
<!-- Experiment: Experimental button -->
<ExperimentalRunButton
v-else-if="variant"
:variant="variant"
:on-click="handleClick"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import type { FeatureFlagVariant } from '@/composables/useFeatureFlags'
import ExperimentalRunButton from './ExperimentalRunButton.vue'
const props = defineProps<{
flagKey?: string
onClick: (e: Event) => void | Promise<void>
}>()
const flagKey = computed(() => props.flagKey || 'demo-run-button-experiment')
const { featureFlag } = useFeatureFlags()
const flagValue = featureFlag<FeatureFlagVariant | boolean | null>(
flagKey.value,
false
)
const variant = computed<FeatureFlagVariant | null>(() => {
const value = flagValue.value
if (
typeof value === 'object' &&
value !== null &&
'variant' in value &&
typeof (value as FeatureFlagVariant).variant === 'string'
) {
return value as FeatureFlagVariant
}
return null
})
const isExperimentActive = computed(() => {
return variant.value !== null
})
const handleClick = () => {
// Create a synthetic event for the onClick handler
const syntheticEvent = new Event('click') as Event
props.onClick(syntheticEvent)
}
</script>

View File

@@ -64,11 +64,13 @@ import {
ComfyWorkflow,
useWorkflowStore
} from '@/platform/workflow/management/stores/workflowStore'
import { app } from '@/scripts/app'
import { useDialogService } from '@/services/dialogService'
import { useCommandStore } from '@/stores/commandStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useSubgraphNavigationStore } from '@/stores/subgraphNavigationStore'
import { appendJsonExt } from '@/utils/formatUtil'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'
interface Props {
item: MenuItem
@@ -79,7 +81,10 @@ const props = withDefaults(defineProps<Props>(), {
isActive: false
})
const { hasMissingNodes } = useMissingNodes()
const nodeDefStore = useNodeDefStore()
const hasMissingNodes = computed(() =>
graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName)
)
const { t } = useI18n()
const menu = ref<InstanceType<typeof Menu> & MenuState>()

View File

@@ -24,7 +24,7 @@ import {
import { cn } from '@/utils/tailwindUtil'
interface IconButtonProps extends BaseButtonProps {
onClick: (event: Event) => void
onClick?: (event: MouseEvent) => void
}
defineOptions({

View File

@@ -47,7 +47,7 @@ const {
} = defineProps<IconTextButtonProps>()
const buttonStyle = computed(() => {
const baseClasses = `${getBaseButtonClasses()} justify-start! gap-2`
const baseClasses = `${getBaseButtonClasses()} justify-start gap-2`
const sizeClasses = getButtonSizeClasses(size)
const typeClasses = border
? getBorderButtonTypeClasses(type)

View File

@@ -68,4 +68,8 @@ const toggle = (event: Event) => {
const hide = () => {
popover.value?.hide()
}
defineExpose({
hide
})
</script>

View File

@@ -9,29 +9,31 @@
<div class="font-medium">
{{ col.header }}
</div>
<div>{{ formatValue(systemInfo[col.field], col.field) }}</div>
<div>{{ getDisplayValue(col) }}</div>
</template>
</div>
</div>
<Divider />
<template v-if="hasDevices">
<Divider />
<div>
<h2 class="mb-4 text-2xl font-semibold">
{{ $t('g.devices') }}
</h2>
<TabView v-if="props.stats.devices.length > 1">
<TabPanel
v-for="device in props.stats.devices"
:key="device.index"
:header="device.name"
:value="device.index"
>
<DeviceInfo :device="device" />
</TabPanel>
</TabView>
<DeviceInfo v-else :device="props.stats.devices[0]" />
</div>
<div>
<h2 class="mb-4 text-2xl font-semibold">
{{ $t('g.devices') }}
</h2>
<TabView v-if="props.stats.devices.length > 1">
<TabPanel
v-for="device in props.stats.devices"
:key="device.index"
:header="device.name"
:value="device.index"
>
<DeviceInfo :device="device" />
</TabPanel>
</TabView>
<DeviceInfo v-else :device="props.stats.devices[0]" />
</div>
</template>
</div>
</template>
@@ -42,8 +44,9 @@ import TabView from 'primevue/tabview'
import { computed } from 'vue'
import DeviceInfo from '@/components/common/DeviceInfo.vue'
import { isCloud } from '@/platform/distribution/types'
import type { SystemStats } from '@/schemas/apiSchema'
import { formatSize } from '@/utils/formatUtil'
import { formatCommitHash, formatSize } from '@/utils/formatUtil'
const props = defineProps<{
stats: SystemStats
@@ -54,20 +57,53 @@ const systemInfo = computed(() => ({
argv: props.stats.system.argv.join(' ')
}))
const systemColumns: { field: keyof SystemStats['system']; header: string }[] =
[
{ field: 'os', header: 'OS' },
{ field: 'python_version', header: 'Python Version' },
{ field: 'embedded_python', header: 'Embedded Python' },
{ field: 'pytorch_version', header: 'Pytorch Version' },
{ field: 'argv', header: 'Arguments' },
{ field: 'ram_total', header: 'RAM Total' },
{ field: 'ram_free', header: 'RAM Free' }
]
const hasDevices = computed(() => props.stats.devices.length > 0)
const formatValue = (value: any, field: string) => {
if (['ram_total', 'ram_free'].includes(field)) {
return formatSize(value)
type SystemInfoKey = keyof SystemStats['system']
type ColumnDef = {
field: SystemInfoKey
header: string
format?: (value: string) => string
formatNumber?: (value: number) => string
}
/** Columns for local distribution */
const localColumns: ColumnDef[] = [
{ field: 'os', header: 'OS' },
{ field: 'python_version', header: 'Python Version' },
{ field: 'embedded_python', header: 'Embedded Python' },
{ field: 'pytorch_version', header: 'Pytorch Version' },
{ field: 'argv', header: 'Arguments' },
{ field: 'ram_total', header: 'RAM Total', formatNumber: formatSize },
{ field: 'ram_free', header: 'RAM Free', formatNumber: formatSize }
]
/** Columns for cloud distribution */
const cloudColumns: ColumnDef[] = [
{ field: 'cloud_version', header: 'Cloud Version' },
{
field: 'comfyui_version',
header: 'ComfyUI Version',
format: formatCommitHash
},
{
field: 'comfyui_frontend_version',
header: 'Frontend Version',
format: formatCommitHash
},
{ field: 'workflow_templates_version', header: 'Templates Version' }
]
const systemColumns = computed(() => (isCloud ? cloudColumns : localColumns))
const getDisplayValue = (column: ColumnDef) => {
const value = systemInfo.value[column.field]
if (column.formatNumber && typeof value === 'number') {
return column.formatNumber(value)
}
if (column.format && typeof value === 'string') {
return column.format(value)
}
return value
}

View File

@@ -35,7 +35,6 @@ import { ValidationState } from '@/utils/validationUtil'
const props = defineProps<{
modelValue: string
validateUrlFn?: (url: string) => Promise<boolean>
disableValidation?: boolean
}>()
const emit = defineEmits<{
@@ -102,8 +101,6 @@ const defaultValidateUrl = async (url: string): Promise<boolean> => {
}
const validateUrl = async (value: string) => {
if (props.disableValidation) return
if (validationState.value === ValidationState.LOADING) return
const url = cleanInput(value)

View File

@@ -92,7 +92,7 @@
class="w-62.5"
>
<template #icon>
<i class="icon-[lucide--arrow-up-down]" />
<i class="icon-[lucide--arrow-up-down] text-muted-foreground" />
</template>
</SingleSelect>
</div>

View File

@@ -17,7 +17,7 @@
root: ({ props }: MultiSelectPassThroughMethodOptions) => ({
class: cn(
'h-10 relative inline-flex cursor-pointer select-none',
'rounded-lg bg-base-background text-base-foreground',
'rounded-lg bg-secondary-background text-base-foreground',
'transition-all duration-200 ease-in-out',
'border-[2.5px] border-solid',
selectedCount > 0
@@ -83,7 +83,7 @@
role="combobox"
:aria-expanded="false"
aria-haspopup="listbox"
tabindex="0"
:tabindex="0"
>
<template
v-if="showSearchBox || showSelectedCount || showClearButton"
@@ -127,7 +127,7 @@
<!-- Trigger value (keep text scale identical) -->
<template #value>
<span class="text-sm text-muted-foreground">
<span class="text-sm">
{{ label }}
</span>
<span
@@ -140,7 +140,7 @@
<!-- Chevron size identical to current -->
<template #dropdownicon>
<i class="icon-[lucide--chevron-down] text-lg text-neutral-400" />
<i class="icon-[lucide--chevron-down] text-muted-foreground" />
</template>
<!-- Custom option row: square checkbox + label (unchanged layout/colors) -->

View File

@@ -1,6 +1,6 @@
<template>
<div :class="wrapperStyle" @click="focusInput">
<i class="icon-[lucide--search] text-muted" />
<i class="icon-[lucide--search] text-muted-foreground" />
<InputText
ref="input"
v-model="internalSearchQuery"
@@ -73,7 +73,7 @@ onMounted(() => autofocus && focusInput())
const wrapperStyle = computed(() => {
const baseClasses =
'relative flex w-full items-center gap-2 bg-base-background cursor-text'
'relative flex w-full items-center gap-2 bg-secondary-background cursor-text'
if (showBorder) {
return cn(

View File

@@ -20,7 +20,7 @@
'h-10 relative inline-flex cursor-pointer select-none items-center',
// trigger surface
'rounded-lg',
'bg-base-background text-base-foreground',
'bg-secondary-background text-base-foreground',
'border-[2.5px] border-solid border-transparent',
'transition-all duration-200 ease-in-out',
'focus-within:border-node-component-border',
@@ -84,7 +84,7 @@
>
<!-- Trigger value -->
<template #value="slotProps">
<div class="flex items-center gap-2 text-sm text-neutral-500">
<div class="flex items-center gap-2 text-sm">
<slot name="icon" />
<span
v-if="slotProps.value !== null && slotProps.value !== undefined"
@@ -100,7 +100,7 @@
<!-- Trigger caret -->
<template #dropdownicon>
<i class="icon-[lucide--chevron-down] text-base text-neutral-500" />
<i class="icon-[lucide--chevron-down] text-muted-foreground" />
</template>
<!-- Option row -->
@@ -152,7 +152,7 @@ const {
popoverMaxWidth?: string
}>()
const selectedItem = defineModel<string | null>({ required: true })
const selectedItem = defineModel<string | undefined>({ required: true })
const { t } = useI18n()

View File

@@ -26,6 +26,10 @@
<script setup lang="ts">
import { computed } from 'vue'
import {
getEffectiveBrushSize,
getEffectiveHardness
} from '@/composables/maskeditor/brushUtils'
import { BrushShape } from '@/extensions/core/maskeditor/types'
import { useMaskEditorStore } from '@/stores/maskEditorStore'
@@ -36,11 +40,14 @@ const { containerRef } = defineProps<{
const store = useMaskEditorStore()
const brushOpacity = computed(() => {
return store.brushVisible ? '1' : '0'
return store.brushVisible ? 1 : 0
})
const brushRadius = computed(() => {
return store.brushSettings.size * store.zoomRatio
const size = store.brushSettings.size
const hardness = store.brushSettings.hardness
const effectiveSize = getEffectiveBrushSize(size, hardness)
return effectiveSize * store.zoomRatio
})
const brushSize = computed(() => {
@@ -78,19 +85,26 @@ const gradientVisible = computed(() => {
})
const gradientBackground = computed(() => {
const size = store.brushSettings.size
const hardness = store.brushSettings.hardness
const effectiveSize = getEffectiveBrushSize(size, hardness)
const effectiveHardness = getEffectiveHardness(size, hardness, effectiveSize)
if (hardness === 1) {
if (effectiveHardness === 1) {
return 'rgba(255, 0, 0, 0.5)'
}
const midStop = hardness * 100
const midStop = effectiveHardness * 100
const outerStop = 100
// Add an intermediate stop to approximate the squared falloff
// At 50% of the fade region, squared falloff is 0.25 (relative to max)
const fadeMidStop = midStop + (outerStop - midStop) * 0.5
return `radial-gradient(
circle,
rgba(255, 0, 0, 0.5) 0%,
rgba(255, 0, 0, 0.25) ${midStop}%,
rgba(255, 0, 0, 0.5) ${midStop}%,
rgba(255, 0, 0, 0.125) ${fadeMidStop}%,
rgba(255, 0, 0, 0) ${outerStop}%
)`
})

View File

@@ -55,7 +55,7 @@
<SliderControl
:label="t('maskEditor.thickness')"
:min="1"
:max="100"
:max="500"
:step="1"
:model-value="store.brushSettings.size"
@update:model-value="onThicknessChange"
@@ -80,12 +80,12 @@
/>
<SliderControl
:label="t('maskEditor.smoothingPrecision')"
:label="$t('maskEditor.stepSize')"
:min="1"
:max="100"
:step="1"
:model-value="store.brushSettings.smoothingPrecision"
@update:model-value="onSmoothingPrecisionChange"
:model-value="store.brushSettings.stepSize"
@update:model-value="onStepSizeChange"
/>
</div>
</template>
@@ -119,8 +119,8 @@ const onHardnessChange = (value: number) => {
store.setBrushHardness(value)
}
const onSmoothingPrecisionChange = (value: number) => {
store.setBrushSmoothingPrecision(value)
const onStepSizeChange = (value: number) => {
store.setBrushStepSize(value)
}
const resetToDefault = () => {

View File

@@ -12,19 +12,28 @@
>
<canvas
ref="imgCanvasRef"
class="absolute top-0 left-0 w-full h-full"
class="absolute top-0 left-0 w-full h-full z-0"
@contextmenu.prevent
/>
<canvas
ref="rgbCanvasRef"
class="absolute top-0 left-0 w-full h-full"
class="absolute top-0 left-0 w-full h-full z-10"
@contextmenu.prevent
/>
<canvas
ref="maskCanvasRef"
class="absolute top-0 left-0 w-full h-full"
class="absolute top-0 left-0 w-full h-full z-30"
@contextmenu.prevent
/>
<!-- GPU Preview Canvas -->
<canvas
ref="gpuCanvasRef"
class="absolute top-0 left-0 w-full h-full pointer-events-none"
:class="{
'z-20': store.activeLayer === 'rgb',
'z-40': store.activeLayer === 'mask'
}"
/>
<div ref="canvasBackgroundRef" class="bg-white w-full h-full" />
</div>
@@ -87,6 +96,7 @@ const canvasContainerRef = ref<HTMLDivElement>()
const imgCanvasRef = ref<HTMLCanvasElement>()
const maskCanvasRef = ref<HTMLCanvasElement>()
const rgbCanvasRef = ref<HTMLCanvasElement>()
const gpuCanvasRef = ref<HTMLCanvasElement>()
const canvasBackgroundRef = ref<HTMLDivElement>()
const toolPanelRef = ref<InstanceType<typeof ToolPanel>>()
@@ -97,7 +107,7 @@ const initialized = ref(false)
const keyboard = useKeyboard()
const panZoom = usePanAndZoom()
let toolManager: ReturnType<typeof useToolManager> | null = null
const toolManager = useToolManager(keyboard, panZoom)
let resizeObserver: ResizeObserver | null = null
@@ -135,8 +145,6 @@ const initUI = async () => {
try {
await loader.loadFromNode(node)
toolManager = useToolManager(keyboard, panZoom)
const imageLoader = useImageLoader()
const image = await imageLoader.loadImages()
@@ -149,6 +157,18 @@ const initUI = async () => {
store.canvasHistory.saveInitialState()
// Initialize GPU resources
if (toolManager.brushDrawing) {
await toolManager.brushDrawing.initGPUResources()
if (gpuCanvasRef.value && toolManager.brushDrawing.initPreviewCanvas) {
// Match preview canvas resolution to mask canvas
gpuCanvasRef.value.width = maskCanvasRef.value.width
gpuCanvasRef.value.height = maskCanvasRef.value.height
toolManager.brushDrawing.initPreviewCanvas(gpuCanvasRef.value)
}
}
initialized.value = true
} catch (error) {
console.error('[MaskEditorContent] Initialization failed:', error)
@@ -172,7 +192,7 @@ onMounted(() => {
})
onBeforeUnmount(() => {
toolManager?.brushDrawing.saveBrushSettings()
toolManager.brushDrawing.saveBrushSettings()
keyboard?.removeListeners()

View File

@@ -102,6 +102,7 @@ const onInvert = () => {
const onClear = () => {
canvasTools.clearMask()
store.triggerClear()
}
const handleSave = async () => {

View File

@@ -1,8 +1,10 @@
<template>
<button
type="button"
class="group flex w-full items-center justify-between gap-3 rounded-lg border-0 bg-secondary-background p-1 text-left transition-colors duration-200 ease-in-out hover:cursor-pointer hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
<IconButton
type="secondary"
size="fit-content"
class="group w-full justify-between gap-3 rounded-lg p-1 text-left font-normal hover:cursor-pointer focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
:aria-label="props.ariaLabel"
@click="emit('click', $event)"
>
<span class="inline-flex items-center gap-2">
<span v-if="props.mode === 'allFailed'" class="inline-flex items-center">
@@ -23,7 +25,11 @@
class="inline-block h-6 w-6 overflow-hidden rounded-[6px] border-0 bg-secondary-background"
:style="{ marginLeft: idx === 0 ? '0' : '-12px' }"
>
<img :src="url" alt="preview" class="h-full w-full object-cover" />
<img
:src="url"
:alt="$t('sideToolbar.queueProgressOverlay.preview')"
class="h-full w-full object-cover"
/>
</span>
</span>
@@ -76,10 +82,11 @@
>
<i class="icon-[lucide--chevron-down] block size-4 leading-none" />
</span>
</button>
</IconButton>
</template>
<script setup lang="ts">
import IconButton from '@/components/button/IconButton.vue'
import type {
CompletionSummary,
CompletionSummaryMode
@@ -96,4 +103,8 @@ type Props = {
const props = withDefaults(defineProps<Props>(), {
thumbnailUrls: () => []
})
const emit = defineEmits<{
(e: 'click', event: MouseEvent): void
}>()
</script>

View File

@@ -42,17 +42,19 @@
t('sideToolbar.queueProgressOverlay.running')
}}</span>
</span>
<button
<IconButton
v-if="runningCount > 0"
v-tooltip.top="cancelJobTooltip"
class="inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background p-0 transition-colors hover:bg-destructive-background"
type="secondary"
size="sm"
class="size-6 bg-destructive-background hover:bg-destructive-background-hover"
:aria-label="t('sideToolbar.queueProgressOverlay.interruptAll')"
@click="$emit('interruptAll')"
>
<i
class="icon-[lucide--x] block size-4 leading-none text-text-primary"
/>
</button>
</IconButton>
</div>
<div class="flex items-center gap-2">
@@ -62,26 +64,28 @@
t('sideToolbar.queueProgressOverlay.queuedSuffix')
}}</span>
</span>
<button
<IconButton
v-if="queuedCount > 0"
v-tooltip.top="clearQueueTooltip"
class="inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background p-0 transition-colors hover:bg-destructive-background"
type="secondary"
size="sm"
class="size-6 bg-secondary-background hover:bg-destructive-background"
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
@click="$emit('clearQueued')"
>
<i
class="icon-[lucide--list-x] block size-4 leading-none text-text-primary"
/>
</button>
</IconButton>
</div>
</div>
<button
class="inline-flex h-6 min-w-[120px] flex-1 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background px-2 py-0 text-[12px] text-text-primary hover:bg-secondary-background-hover hover:opacity-90"
<TextButton
class="h-6 min-w-[120px] flex-1 px-2 py-0 text-[12px]"
type="secondary"
:label="t('sideToolbar.queueProgressOverlay.viewAllJobs')"
@click="$emit('viewAllJobs')"
>
{{ t('sideToolbar.queueProgressOverlay.viewAllJobs') }}
</button>
/>
</div>
</div>
</template>
@@ -90,6 +94,8 @@
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import TextButton from '@/components/button/TextButton.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
defineProps<{

View File

@@ -8,17 +8,20 @@
/>
<div class="flex items-center justify-between px-3">
<button
class="inline-flex grow cursor-pointer items-center justify-center gap-1 rounded border-0 bg-secondary-background p-2 text-center font-inter text-[12px] leading-none text-text-primary hover:bg-secondary-background-hover hover:opacity-90"
<IconTextButton
class="grow gap-1 p-2 text-center font-inter text-[12px] leading-none hover:opacity-90 justify-center"
type="secondary"
:label="t('sideToolbar.queueProgressOverlay.showAssets')"
:aria-label="t('sideToolbar.queueProgressOverlay.showAssets')"
@click="$emit('showAssets')"
>
<div
class="pointer-events-none block size-4 shrink-0 leading-none icon-[comfy--image-ai-edit]"
aria-hidden="true"
/>
<span>{{ t('sideToolbar.queueProgressOverlay.showAssets') }}</span>
</button>
<template #icon>
<div
class="pointer-events-none block size-4 shrink-0 leading-none icon-[comfy--image-ai-edit]"
aria-hidden="true"
/>
</template>
</IconTextButton>
<div class="ml-4 inline-flex items-center">
<div
class="inline-flex h-6 items-center text-[12px] leading-none text-text-primary opacity-90"
@@ -28,16 +31,18 @@
t('sideToolbar.queueProgressOverlay.queuedSuffix')
}}</span>
</div>
<button
<IconButton
v-if="queuedCount > 0"
class="group ml-2 inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background p-0 transition-colors hover:bg-destructive-background"
class="group ml-2 size-6 bg-secondary-background hover:bg-destructive-background"
type="secondary"
size="sm"
:aria-label="t('sideToolbar.queueProgressOverlay.clearQueued')"
@click="$emit('clearQueued')"
>
<i
class="pointer-events-none icon-[lucide--list-x] block size-4 leading-none text-text-primary transition-colors group-hover:text-base-background"
/>
</button>
</IconButton>
</div>
</div>
@@ -75,6 +80,8 @@
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import type {
JobGroup,
JobListItem,

View File

@@ -18,16 +18,18 @@
</span>
</div>
<div class="flex items-center gap-1">
<button
<IconButton
v-tooltip.top="moreTooltipConfig"
class="inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-transparent p-0 hover:bg-secondary-background hover:opacity-100"
type="transparent"
size="sm"
class="size-6 bg-transparent hover:bg-secondary-background hover:opacity-100"
:aria-label="t('sideToolbar.queueProgressOverlay.moreOptions')"
@click="onMoreClick"
>
<i
class="icon-[lucide--more-horizontal] block size-4 leading-none text-text-secondary"
/>
</button>
</IconButton>
<Popover
ref="morePopoverRef"
:dismissable="true"
@@ -45,18 +47,19 @@
<div
class="flex flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3 font-inter"
>
<button
class="inline-flex w-full cursor-pointer items-center justify-start gap-2 rounded-lg border-0 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
<IconTextButton
class="w-full justify-start gap-2 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
type="transparent"
:label="t('sideToolbar.queueProgressOverlay.clearHistory')"
:aria-label="t('sideToolbar.queueProgressOverlay.clearHistory')"
@click="onClearHistoryFromMenu"
>
<i
class="icon-[lucide--file-x-2] block size-4 leading-none text-text-secondary"
/>
<span>{{
t('sideToolbar.queueProgressOverlay.clearHistory')
}}</span>
</button>
<template #icon>
<i
class="icon-[lucide--file-x-2] block size-4 leading-none text-text-secondary"
/>
</template>
</IconTextButton>
</div>
</Popover>
</div>
@@ -69,6 +72,8 @@ import type { PopoverMethods } from 'primevue/popover'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
defineProps<{

View File

@@ -60,7 +60,7 @@
</template>
<script setup lang="ts">
import { computed, nextTick, ref } from 'vue'
import { computed, nextTick, ref, withDefaults } from 'vue'
import { useI18n } from 'vue-i18n'
import QueueOverlayActive from '@/components/queue/QueueOverlayActive.vue'
@@ -85,9 +85,15 @@ import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
type OverlayState = 'hidden' | 'empty' | 'active' | 'expanded'
const props = defineProps<{
expanded?: boolean
}>()
const props = withDefaults(
defineProps<{
expanded?: boolean
menuHovered?: boolean
}>(),
{
menuHovered: false
}
)
const emit = defineEmits<{
(e: 'update:expanded', value: boolean): void
@@ -110,6 +116,7 @@ const {
currentNodeProgressStyle
} = useQueueProgress()
const isHovered = ref(false)
const isOverlayHovered = computed(() => isHovered.value || props.menuHovered)
const internalExpanded = ref(false)
const isExpanded = computed({
get: () =>
@@ -142,7 +149,7 @@ const showBackground = computed(
() =>
overlayState.value === 'expanded' ||
overlayState.value === 'empty' ||
(overlayState.value === 'active' && isHovered.value)
(overlayState.value === 'active' && isOverlayHovered.value)
)
const isVisible = computed(() => overlayState.value !== 'hidden')
@@ -156,7 +163,7 @@ const containerClass = computed(() =>
const bottomRowClass = computed(
() =>
`flex items-center justify-end gap-4 transition-opacity duration-200 ease-in-out ${
overlayState.value === 'active' && isHovered.value
overlayState.value === 'active' && isOverlayHovered.value
? 'opacity-100 pointer-events-auto'
: 'opacity-0 pointer-events-none'
}`

View File

@@ -8,13 +8,15 @@
<p class="m-0 text-[14px] font-normal leading-none">
{{ t('sideToolbar.queueProgressOverlay.clearHistoryDialogTitle') }}
</p>
<button
class="inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-transparent p-0 text-text-secondary transition hover:bg-secondary-background hover:opacity-100"
<IconButton
type="transparent"
size="sm"
class="size-6 bg-transparent text-text-secondary hover:bg-secondary-background hover:opacity-100"
:aria-label="t('g.close')"
@click="onCancel"
>
<i class="icon-[lucide--x] block size-4 leading-none" />
</button>
</IconButton>
</header>
<div class="flex flex-col gap-4 px-4 py-4 text-[14px] text-text-secondary">
@@ -30,21 +32,19 @@
<footer class="flex items-center justify-end px-4 py-4">
<div class="flex items-center gap-4 text-[14px] leading-none">
<button
class="inline-flex min-h-[24px] cursor-pointer items-center rounded-md border-0 bg-transparent px-1 py-1 text-[14px] leading-[1] text-text-secondary transition hover:text-text-primary"
:aria-label="t('g.cancel')"
<TextButton
class="min-h-[24px] px-1 py-1 text-[14px] leading-[1] text-text-secondary hover:text-text-primary"
type="transparent"
:label="t('g.cancel')"
@click="onCancel"
>
{{ t('g.cancel') }}
</button>
<button
class="inline-flex min-h-[32px] items-center rounded-lg border-0 bg-secondary-background px-4 py-2 text-[12px] font-normal leading-[1] text-text-primary transition hover:bg-secondary-background-hover hover:text-text-primary disabled:cursor-not-allowed disabled:opacity-60"
:aria-label="t('g.clear')"
/>
<TextButton
class="min-h-[32px] px-4 py-2 text-[12px] font-normal leading-[1]"
type="secondary"
:label="t('g.clear')"
:disabled="isClearing"
@click="onConfirm"
>
{{ t('g.clear') }}
</button>
/>
</div>
</footer>
</section>
@@ -54,6 +54,8 @@
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import TextButton from '@/components/button/TextButton.vue'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { useDialogStore } from '@/stores/dialogStore'
import { useQueueStore } from '@/stores/queueStore'

View File

@@ -20,21 +20,24 @@
<div v-if="entry.kind === 'divider'" class="px-2 py-1">
<div class="h-px bg-interface-stroke" />
</div>
<button
<IconTextButton
v-else
class="inline-flex w-full cursor-pointer items-center justify-start gap-2 rounded-lg border-0 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary transition-colors duration-150 hover:bg-interface-panel-hover-surface"
class="w-full justify-start gap-2 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-interface-panel-hover-surface"
type="transparent"
:label="entry.label"
:aria-label="entry.label"
@click="onEntry(entry)"
>
<i
v-if="entry.icon"
:class="[
entry.icon,
'block size-4 shrink-0 leading-none text-text-secondary'
]"
/>
<span>{{ entry.label }}</span>
</button>
<template #icon>
<i
v-if="entry.icon"
:class="[
entry.icon,
'block size-4 shrink-0 leading-none text-text-secondary'
]"
/>
</template>
</IconTextButton>
</template>
</div>
</Popover>
@@ -44,6 +47,7 @@
import Popover from 'primevue/popover'
import { ref } from 'vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import type { MenuEntry } from '@/composables/queue/useJobMenu'
defineProps<{ entries: MenuEntry[] }>()

View File

@@ -20,17 +20,18 @@
class="flex min-w-0 items-center text-[0.75rem] leading-normal font-normal text-text-secondary"
>
<span class="block min-w-0 truncate">{{ row.value }}</span>
<button
<IconButton
v-if="row.canCopy"
type="button"
class="ml-2 inline-flex size-6 items-center justify-center rounded border-0 bg-transparent p-0 hover:opacity-90"
type="transparent"
size="sm"
class="ml-2 size-6 bg-transparent hover:opacity-90"
:aria-label="copyAriaLabel"
@click.stop="copyJobId"
>
<i
class="icon-[lucide--copy] block size-4 leading-none text-text-secondary"
/>
</button>
</IconButton>
</div>
</template>
</div>
@@ -60,25 +61,31 @@
{{ t('queue.jobDetails.errorMessage') }}
</div>
<div class="flex items-center justify-between gap-4">
<button
type="button"
class="inline-flex h-6 items-center justify-center gap-2 rounded border-none bg-transparent px-0 text-[0.75rem] leading-none text-text-secondary hover:opacity-90"
<IconTextButton
class="h-6 justify-start gap-2 bg-transparent px-0 text-[0.75rem] leading-none text-text-secondary hover:opacity-90"
type="transparent"
:label="copyAriaLabel"
:aria-label="copyAriaLabel"
icon-position="right"
@click.stop="copyErrorMessage"
>
<span>{{ copyAriaLabel }}</span>
<i class="icon-[lucide--copy] block size-3.5 leading-none" />
</button>
<button
type="button"
class="inline-flex h-6 items-center justify-center gap-2 rounded border-none bg-transparent px-0 text-[0.75rem] leading-none text-text-secondary hover:opacity-90"
<template #icon>
<i class="icon-[lucide--copy] block size-3.5 leading-none" />
</template>
</IconTextButton>
<IconTextButton
class="h-6 justify-start gap-2 bg-transparent px-0 text-[0.75rem] leading-none text-text-secondary hover:opacity-90"
type="transparent"
:label="t('queue.jobDetails.report')"
icon-position="right"
@click.stop="reportJobError"
>
<span>{{ t('queue.jobDetails.report') }}</span>
<i
class="icon-[lucide--message-circle-warning] block size-3.5 leading-none"
/>
</button>
<template #icon>
<i
class="icon-[lucide--message-circle-warning] block size-3.5 leading-none"
/>
</template>
</IconTextButton>
</div>
<div
class="col-span-2 mt-2 rounded bg-interface-panel-hover-surface px-4 py-2 text-[0.75rem] leading-normal text-text-secondary"
@@ -94,6 +101,8 @@
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { t } from '@/i18n'
import { isCloud } from '@/platform/distribution/types'

View File

@@ -2,26 +2,26 @@
<div class="flex items-center justify-between gap-2 px-3">
<div class="min-w-0 flex-1 overflow-x-auto">
<div class="inline-flex items-center gap-1 whitespace-nowrap">
<button
<TextButton
v-for="tab in visibleJobTabs"
:key="tab"
class="h-6 cursor-pointer rounded border-0 px-3 py-1 text-[12px] leading-none hover:opacity-90"
class="h-6 px-3 py-1 text-[12px] leading-none hover:opacity-90"
:type="selectedJobTab === tab ? 'secondary' : 'transparent'"
:class="[
selectedJobTab === tab
? 'bg-secondary-background text-text-primary'
: 'bg-transparent text-text-secondary'
selectedJobTab === tab ? 'text-text-primary' : 'text-text-secondary'
]"
:label="tabLabel(tab)"
@click="$emit('update:selectedJobTab', tab)"
>
{{ tabLabel(tab) }}
</button>
/>
</div>
</div>
<div class="ml-2 flex shrink-0 items-center gap-2">
<button
<IconButton
v-if="showWorkflowFilter"
v-tooltip.top="filterTooltipConfig"
class="relative inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background p-0 hover:bg-secondary-background-hover hover:opacity-90"
type="secondary"
size="sm"
class="relative size-6 bg-secondary-background hover:bg-secondary-background-hover hover:opacity-90"
:aria-label="t('sideToolbar.queueProgressOverlay.filterJobs')"
@click="onFilterClick"
>
@@ -32,7 +32,7 @@
v-if="selectedWorkflowFilter !== 'all'"
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
/>
</button>
</IconButton>
<Popover
v-if="showWorkflowFilter"
ref="filterPopoverRef"
@@ -51,46 +51,48 @@
<div
class="flex min-w-[12rem] flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3"
>
<button
class="inline-flex w-full cursor-pointer items-center justify-start gap-1 rounded-lg border-0 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
<IconTextButton
class="w-full justify-between gap-1 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
type="transparent"
icon-position="right"
:label="t('sideToolbar.queueProgressOverlay.filterAllWorkflows')"
:aria-label="
t('sideToolbar.queueProgressOverlay.filterAllWorkflows')
"
@click="selectWorkflowFilter('all')"
>
<span>{{
t('sideToolbar.queueProgressOverlay.filterAllWorkflows')
}}</span>
<span class="ml-auto inline-flex items-center">
<template #icon>
<i
v-if="selectedWorkflowFilter === 'all'"
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
/>
</span>
</button>
</template>
</IconTextButton>
<div class="mx-2 mt-1 h-px" />
<button
class="inline-flex w-full cursor-pointer items-center justify-start gap-1 rounded-lg border-0 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
<IconTextButton
class="w-full justify-between gap-1 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
type="transparent"
icon-position="right"
:label="t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')"
:aria-label="
t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')
"
@click="selectWorkflowFilter('current')"
>
<span>{{
t('sideToolbar.queueProgressOverlay.filterCurrentWorkflow')
}}</span>
<span class="ml-auto inline-flex items-center">
<template #icon>
<i
v-if="selectedWorkflowFilter === 'current'"
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
/>
</span>
</button>
</template>
</IconTextButton>
</div>
</Popover>
<button
<IconButton
v-tooltip.top="sortTooltipConfig"
class="relative inline-flex size-6 cursor-pointer items-center justify-center rounded border-0 bg-secondary-background p-0 hover:bg-secondary-background-hover hover:opacity-90"
type="secondary"
size="sm"
class="relative size-6 bg-secondary-background hover:bg-secondary-background-hover hover:opacity-90"
:aria-label="t('sideToolbar.queueProgressOverlay.sortJobs')"
@click="onSortClick"
>
@@ -101,7 +103,7 @@
v-if="selectedSortMode !== 'mostRecent'"
class="pointer-events-none absolute -top-1 -right-1 inline-block size-2 rounded-full bg-base-foreground"
/>
</button>
</IconButton>
<Popover
ref="sortPopoverRef"
:dismissable="true"
@@ -120,19 +122,21 @@
class="flex min-w-[12rem] flex-col items-stretch rounded-lg border border-interface-stroke bg-interface-panel-surface px-2 py-3"
>
<template v-for="(mode, index) in jobSortModes" :key="mode">
<button
class="inline-flex w-full cursor-pointer items-center justify-start gap-1 rounded-lg border-0 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
<IconTextButton
class="w-full justify-between gap-1 bg-transparent p-2 font-inter text-[12px] leading-none text-text-primary hover:bg-transparent hover:opacity-90"
type="transparent"
icon-position="right"
:label="sortLabel(mode)"
:aria-label="sortLabel(mode)"
@click="selectSortMode(mode)"
>
<span>{{ sortLabel(mode) }}</span>
<span class="ml-auto inline-flex items-center">
<template #icon>
<i
v-if="selectedSortMode === mode"
class="icon-[lucide--check] block size-4 leading-none text-text-secondary"
/>
</span>
</button>
</template>
</IconTextButton>
<div
v-if="index < jobSortModes.length - 1"
class="mx-2 mt-1 h-px"
@@ -149,6 +153,9 @@ import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import TextButton from '@/components/button/TextButton.vue'
import { jobSortModes, jobTabs } from '@/composables/queue/useJobList'
import type { JobSortMode, JobTab } from '@/composables/queue/useJobList'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'

View File

@@ -82,7 +82,10 @@
:src="iconImageUrl"
class="h-full w-full object-cover"
/>
<i v-else :class="[iconClass, 'size-4']" />
<i
v-else
:class="cn(iconClass, 'size-4', shouldSpin && 'animate-spin')"
/>
</div>
</div>
</div>
@@ -93,6 +96,23 @@
</div>
</div>
<!--
TODO: Refactor action buttons to use a declarative config system.
Instead of hardcoding button visibility logic in the template, define an array of
action button configs with properties like:
- icon, label, action, tooltip
- visibleStates: JobState[] (which job states show this button)
- alwaysVisible: boolean (show without hover)
- destructive: boolean (use destructive styling)
Then render buttons in two groups:
1. Always-visible buttons (outside Transition)
2. Hover-only buttons (inside Transition)
This would eliminate the current duplication where the cancel button exists
both outside (for running) and inside (for pending) the Transition.
-->
<div class="relative z-[1] flex items-center gap-2 text-text-secondary">
<Transition
mode="out-in"
@@ -108,50 +128,72 @@
key="actions"
class="inline-flex items-center gap-2 pr-1"
>
<button
<IconButton
v-if="props.state === 'failed' && computedShowClear"
v-tooltip.top="deleteTooltipConfig"
type="button"
class="inline-flex h-6 transform cursor-pointer items-center gap-1 rounded border-0 bg-modal-card-button-surface px-1 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background hover:opacity-95"
type="transparent"
size="sm"
class="size-6 transform gap-1 rounded bg-destructive-background text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background-hover hover:opacity-95"
:aria-label="t('g.delete')"
@click.stop="emit('delete')"
>
<i class="icon-[lucide--trash-2] size-4" />
</button>
<button
v-else-if="props.state !== 'completed' && computedShowClear"
</IconButton>
<IconButton
v-else-if="
props.state !== 'completed' &&
props.state !== 'running' &&
computedShowClear
"
v-tooltip.top="cancelTooltipConfig"
type="button"
class="inline-flex h-6 transform cursor-pointer items-center gap-1 rounded border-0 bg-modal-card-button-surface px-1 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background hover:opacity-95"
type="transparent"
size="sm"
class="size-6 transform gap-1 rounded bg-destructive-background text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background-hover hover:opacity-95"
:aria-label="t('g.cancel')"
@click.stop="emit('cancel')"
>
<i class="icon-[lucide--x] size-4" />
</button>
<button
</IconButton>
<TextButton
v-else-if="props.state === 'completed'"
type="button"
class="inline-flex h-6 transform cursor-pointer items-center gap-1 rounded border-0 bg-modal-card-button-surface px-2 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
class="h-6 transform gap-1 rounded bg-modal-card-button-surface px-2 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
type="transparent"
:label="t('menuLabels.View')"
:aria-label="t('menuLabels.View')"
@click.stop="emit('view')"
>
<span>{{ t('menuLabels.View') }}</span>
</button>
<button
/>
<IconButton
v-if="props.showMenu !== undefined ? props.showMenu : true"
v-tooltip.top="moreTooltipConfig"
type="button"
class="inline-flex h-6 transform cursor-pointer items-center gap-1 rounded border-0 bg-modal-card-button-surface px-1 py-0 text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
type="transparent"
size="sm"
class="size-6 transform gap-1 rounded bg-modal-card-button-surface text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:opacity-95"
:aria-label="t('g.more')"
@click.stop="emit('menu', $event)"
>
<i class="icon-[lucide--more-horizontal] size-4" />
</button>
</IconButton>
</div>
<div v-else key="secondary" class="pr-2">
<div
v-else-if="props.state !== 'running'"
key="secondary"
class="pr-2"
>
<slot name="secondary">{{ props.rightText }}</slot>
</div>
</Transition>
<!-- Running job cancel button - always visible -->
<IconButton
v-if="props.state === 'running' && computedShowClear"
v-tooltip.top="cancelTooltipConfig"
type="transparent"
size="sm"
class="size-6 transform gap-1 rounded bg-destructive-background text-text-primary transition duration-150 ease-in-out hover:-translate-y-px hover:bg-destructive-background-hover hover:opacity-95"
:aria-label="t('g.cancel')"
@click.stop="emit('cancel')"
>
<i class="icon-[lucide--x] size-4" />
</IconButton>
</div>
</div>
</div>
@@ -161,11 +203,14 @@
import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import TextButton from '@/components/button/TextButton.vue'
import JobDetailsPopover from '@/components/queue/job/JobDetailsPopover.vue'
import QueueAssetPreview from '@/components/queue/job/QueueAssetPreview.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import type { JobState } from '@/types/queue'
import { iconForJobState } from '@/utils/queueDisplay'
import { cn } from '@/utils/tailwindUtil'
const props = withDefaults(
defineProps<{
@@ -298,6 +343,13 @@ const iconClass = computed(() => {
return iconForJobState(props.state)
})
const shouldSpin = computed(
() =>
props.state === 'pending' &&
iconClass.value === iconForJobState('pending') &&
!props.iconImageUrl
)
const computedShowClear = computed(() => {
if (props.showClear !== undefined) return props.showClear
return props.state !== 'completed'

View File

@@ -73,6 +73,7 @@
@click.stop="handleNodes2ToggleClick"
>
<span class="p-menubar-item-label text-nowrap">{{ item.label }}</span>
<Tag severity="info" class="ml-2 text-xs">{{ $t('g.beta') }}</Tag>
<ToggleSwitch
v-model="nodes2Enabled"
class="ml-4"
@@ -101,6 +102,7 @@
<script setup lang="ts">
import type { MenuItem } from 'primevue/menuitem'
import Tag from 'primevue/tag'
import TieredMenu from 'primevue/tieredmenu'
import type { TieredMenuMethods, TieredMenuState } from 'primevue/tieredmenu'
import ToggleSwitch from 'primevue/toggleswitch'

View File

@@ -44,6 +44,7 @@
<SidebarHelpCenterIcon :is-small="isSmall" />
<SidebarBottomPanelToggleButton :is-small="isSmall" />
<SidebarShortcutsToggleButton :is-small="isSmall" />
<SidebarSettingsButton :is-small="isSmall" />
</div>
</div>
</nav>
@@ -56,6 +57,7 @@ import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'
import ComfyMenuButton from '@/components/sidebar/ComfyMenuButton.vue'
import SidebarBottomPanelToggleButton from '@/components/sidebar/SidebarBottomPanelToggleButton.vue'
import SidebarSettingsButton from '@/components/sidebar/SidebarSettingsButton.vue'
import SidebarShortcutsToggleButton from '@/components/sidebar/SidebarShortcutsToggleButton.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'

Some files were not shown because too many files have changed in this diff Show More