Compare commits

...

100 Commits

Author SHA1 Message Date
Johnpaul Chiwetelu
890ab2019f feat: add Update ComfyUI option to Help Center for non-desktop environments (#7578)
## Summary

- Adds "Update ComfyUI" menu item to Help Center for portable/localhost
environments
- Wires existing `/v2/manager/queue/update_comfyui` endpoint to the
frontend
- Only visible in non-desktop, non-cloud distributions (where Electron
update mechanism isn't available)

## Changes

1. **Service layer**: Added `updateComfyUI()` method to
`comfyManagerService.ts`
2. **UI**: Added menu item with download icon to
`HelpCenterMenuContent.vue`
3. **i18n**: Added translation key for the new menu item

## Context

The new Manager UI (v4) lost the ability to update ComfyUI core in
non-desktop environments. This restores that functionality by
integrating the existing manager endpoint into the Help Center menu.

## Test plan

- [ ] Verify menu item appears in portable/localhost environments
- [ ] Verify menu item does NOT appear in desktop (Electron)
environments
- [ ] Verify menu item does NOT appear in cloud environments
- [ ] Test clicking the menu item triggers update and reboot

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7578-feat-add-Update-ComfyUI-option-to-Help-Center-for-non-desktop-environments-2cc6d73d3650811e9e4fe55515f50333)
by [Unito](https://www.unito.io)
2025-12-17 21:54:05 +01:00
Rizumu Ayaka
3e111bd75c fix: expand assets dropdown body to show entire "no results placeholder" (#7586)
Currently, when the assets dropdown has no assets available, the "No
Results Placeholder" text is not fully visible because the dropdown body
doesn't expand to accommodate the full message.

Make the body of the assets dropdown expand to show the entire "No
Results Placeholder" message when there are no assets present.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7586-fix-expand-assets-dropdown-body-to-show-entire-no-results-placeholder-2cc6d73d365081f4a142e927621f936d)
by [Unito](https://www.unito.io)
2025-12-17 13:53:43 -07:00
Christian Byrne
fa4daed163 Update CONTRIBUTING.md (#7588)
## Summary

Removes some outdated and unnecessary sections, updates some others.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7588-Update-CONTRIBUTING-md-2cc6d73d365081858102dab4d7f24867)
by [Unito](https://www.unito.io)

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-12-17 13:52:49 -07:00
Alexander Brown
e1814447f7 Devex: Convert .env_example sections to comment toggles (#7587)
## Summary

Set to true, but commented out so that a developer can just toggle
comment on that line to enable the behavior described.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7587-Devex-Convert-env_example-sections-to-comment-toggles-2cc6d73d365081db9907f107ee1b77e7)
by [Unito](https://www.unito.io)
2025-12-17 11:31:43 -08:00
Johnpaul Chiwetelu
e21f43f398 [feat] Replace NodeOptions with PrimeVue ContextMenu (#7114)
## Summary
- Add `NodeContextMenu.vue` using PrimeVue ContextMenu component with
native submenu support
- Rename `SubmenuPopover.vue` to `ColorPickerMenu.vue` (specialized for
color picker)
- Delete old components: `NodeOptions.vue`, `MenuOptionItem.vue`,
`useSubmenuPositioning.ts`
- Wire up context menu converter in `useMoreOptionsMenu.ts`
- Update tests to use hover instead of click for submenus

## Dependencies
**This PR depends on #7113** - the context menu converter infrastructure
PR. It should be merged after that PR.

## Benefits
- Native PrimeVue submenu support with proper keyboard navigation
- Constrained menu dimensions with overflow scrolling (max-h-[80vh])
- Cleaner component architecture with ~280 fewer lines of code
- Better separation: ColorPickerMenu handles only the custom color
picker UI

## Test plan
- [x] Typecheck passes
- [x] Lint passes
- [x] Knip passes
- [ ] Browser tests for submenu interactions pass
- [ ] Manual testing of node context menu

## Screenshots
(Menu UI should look the same, with improved submenu behavior)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7114-feat-Replace-NodeOptions-with-PrimeVue-ContextMenu-2be6d73d365081fda576fd691175eacf)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2025-12-17 06:47:28 +01:00
Alexander Brown
8d7dd9ed67 Component: Button migration 1: TextButton (#7537)
## Summary

Setup the variants and migrate existing uses of
TextButton/TextIconButton/IconButton to a single Button component.

Still a work in progress.

## Changes

- **What**: Add a new Button
- **What**: Migrate old buttons
- **What**: Delete old buttons
- **Dependencies**: CVA, upgrade Storybook

## Review Focus

<!-- Critical design decisions or edge cases that need attention -->

<!-- If this PR fixes an issue, uncomment and update the line below -->
<!-- Fixes #ISSUE_NUMBER -->

## Screenshots (if applicable)

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7537-WIP-Component-Button-migration-2cb6d73d36508156a81bfc7bbddb36e9)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2025-12-16 20:38:24 -08:00
AustinMroz
ab76d02823 Fix doubled control application (#7550)
With reactivity fixed, control widgets would apply twice. This is fixed
by using the litegraph implementation.

Also adds control widget support for combos

Followup to #7539.

Known Issue:
- Primitive node do not have litegraph callbacks properly setup. As a
result, they will display an updated value when modified by control
widgets. Fixing this will requires a larger, separate PR

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7550-Fix-doubled-control-application-2cb6d73d365081739a2fc40fdfb3630e)
by [Unito](https://www.unito.io)
2025-12-16 18:42:02 -08:00
Simula_r
fa37112caf feat(cloud): yearly pricing (#7572)
## Summary

Add support for yearly/annual billing. Implement new Figma design. Add
new yearly tier params to api.

## Changes

- **What**: Mostly PricingTable.vue,
SubscriptionRequiredDialogContent.vue,
- **Breaking**: <!-- Any breaking changes (if none, remove this line)
-->
- **Dependencies**: <!-- New dependencies (if none, remove this line)
-->

## Screenshots (if applicable)


https://github.com/user-attachments/assets/06545dca-95a4-43ce-a128-2e45bb44f132

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7572-feat-yearly-pricing-2cb6d73d365081c68802f2beb47a312e)
by [Unito](https://www.unito.io)
2025-12-16 18:20:11 -08:00
Alexander Brown
ed7dce84e0 🤖 Add code review and function declaration/expression preference to AGENTS.md (#7577)
## Summary

From the Google Engineering Practices docs.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7577-Add-code-review-and-function-declaration-expression-preference-to-AGENTS-md-2cc6d73d3650816ca402f6ed48002f5d)
by [Unito](https://www.unito.io)
2025-12-16 17:00:02 -08:00
Comfy Org PR Bot
02d3b38a26 1.36.3 (#7575)
Patch version increment to 1.36.3

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7575-1-36-3-2cc6d73d365081728802e01290ee5e1f)
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-12-16 17:30:54 -07:00
Terry Jia
84561a1eb2 feat: upgrade Vite from v5 to v7 (#7566)
## Summary

Update vite to version 7, this is prerequisite to support sparkjs
https://sparkjs.dev/ for 3DGS file.

Currently, vite 5 has issue to load the file in spark, as working with
developer from World lab https://www.worldlabs.ai/, we found it is bug
on vite 5, we should upgrade vite to latest one.

see https://github.com/Comfy-Org/ComfyUI_frontend/issues/4061

also discussed with @christian-byrne and got approval for this change

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7566-feat-upgrade-Vite-from-v5-to-v7-2cb6d73d365081f7bdb0d7425d8b869e)
by [Unito](https://www.unito.io)
2025-12-16 17:46:21 -05:00
Benjamin Lu
e1294d66cc Hide queue overlay header menu on cloud (#7571)
Hide the QueueOverlayHeader more-options (…) menu when running in Cloud
distribution

This is being hidden instead of fixed because it deletes all user assets
on cloud, will be touched by /jobs, replaced by the second iteration of
design changes, along with changes to asset deletion happening soon. It
would be too risky to have a proper fix for it in the cloud deploy
today.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7571-Hide-queue-overlay-header-menu-on-cloud-2cb6d73d3650815a8faac8a8f0de91f0)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2025-12-16 13:37:52 -08:00
AustinMroz
0ad5509037 Fix selecting loras on cloud (#7560)
Models must be updated by emit.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7560-Fix-selecting-loras-on-cloud-2cb6d73d365081d48d3de032317bcb71)
by [Unito](https://www.unito.io)
2025-12-16 19:51:32 +00:00
Kelly Yang
97386b0a14 Fix: the wrong selection under the hand mode (#7541)
## Summary

Fixed an inconsistency where nodes were resizable in Hand mode. Resizing
is now restricted to Selection mode only tomatch standard LiteGraph
behavior (Hand mode should only be for panning).

## Changes

What: Disabled node resizing logic when the canvas is set to Hand mode.

## Review Focus

Try toggling between Selection and Hand modes. Verify that resize
handles are unresponsive in Hand mode but work
normally in Selection mode.

## Screenshots

Before


https://github.com/user-attachments/assets/2707cdd9-93f5-4820-9282-893081778dff


After


https://github.com/user-attachments/assets/4d4ee027-b74a-481a-8b2a-97c58799534a

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7541-Fix-the-wrong-selection-under-the-hand-mode-2cb6d73d3650812b950ef385809130bb)
by [Unito](https://www.unito.io)
2025-12-16 14:50:24 -05:00
Alexander Brown
d448421263 Fix: Restore assets API short-circuit in WidgetSelectDropdown (#7563)
## Summary

Removed in #6985, but breaks the dropdown loading on Cloud.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7563-Fix-Restore-assets-API-short-circuit-in-WidgetSelectDropdown-2cb6d73d365081778a8dc65418d9f6be)
by [Unito](https://www.unito.io)
2025-12-16 19:34:55 +00:00
Christian Byrne
6391bd89bf feat: auto-focus the searchbox in asset dropdowns (#7554)
## Summary

On the assets dropdown in Vue Nodes loader nodes (e.g., Load Checkpoint,
Load Video, Load Image), focus the search box first.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7554-feat-auto-focus-the-searchbox-in-asset-dropdowns-2cb6d73d3650812bbe3eef406864e034)
by [Unito](https://www.unito.io)
2025-12-16 13:39:42 -05:00
Christian Byrne
786c3b8c12 cleanup: remove now-unused stripe pricing table remnants (#7557)
Removes vestigial remnants of the Stripe pricing table integration,
which was replaced in
https://github.com/Comfy-Org/ComfyUI_frontend/pull/7359. The feature
flag is now obsolete, but was forgotten about in that PR. This PR cleans
everything up fully.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7557-cleanup-remove-now-unused-stripe-pricing-table-remnants-2cb6d73d3650812aaa5efe91f766fb20)
by [Unito](https://www.unito.io)
2025-12-16 13:39:18 -05:00
Christian Byrne
a64597b4f8 feat: improve vue node video preview loading and a11y (#7558)
## Summary

Applies appropriate logic from below PRs (which affected image
outputs/previews) to video previews

- https://github.com/Comfy-Org/ComfyUI_frontend/pull/7268: don't port
the 250ms logic for videos, as it is not as relevant - videos typically
take longer than 250ms and most browsers have a built-in loading state
that will be displayed between component mount and video onloaded that
we don't want to flash. Use the native video loaded event instead
- https://github.com/Comfy-Org/ComfyUI_frontend/pull/7252: apply to
videos 1-for-1

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7558-feat-improve-vue-node-video-preview-loading-and-a11y-2cb6d73d365081eab4dcfeb1e62c553b)
by [Unito](https://www.unito.io)
2025-12-16 13:38:41 -05:00
Alexander Brown
89e67b1558 Deps: Update Storybook to v10 (#7559)
## Summary

Update Storybook and the related dependencies to v10.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7559-Deps-Update-Storybook-to-v10-2cb6d73d365081a1ab64e835b84b2a0e)
by [Unito](https://www.unito.io)
2025-12-16 17:41:34 +00:00
Christian Byrne
480711deb7 i18n: add missing translation keys (#7436)
## Summary

Adds i18n values for these missing keys:
6a73288ce3/src/components/dialog/content/MissingCoreNodesMessage.vue (L16-L29)

Fixes warning in console.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7436-i18n-add-missing-translation-keys-2c86d73d365081e49460dab121b86c2f)
by [Unito](https://www.unito.io)
2025-12-16 08:17:34 -08:00
Benjamin Lu
93195d3274 feat(server-config): restart required toast (#7479)
## Summary

Show a warning toast when leaving Server Config with pending changes,
reminding users they must restart to apply changes.

## Changes

- **What**: Add a `onBeforeUnmount` toast in `ServerConfigPanel` when
`modifiedConfigs` is non-empty and the user didn’t click Restart; add
i18n strings.

## Review Focus

- Confirm the toast timing/conditions are correct (only fires on leaving
the panel; suppressed when Restart is clicked).

> [!NOTE]
> This is a stacked PR. (main <=
https://github.com/Comfy-Org/ComfyUI_frontend/pull/7478 <=
https://github.com/Comfy-Org/ComfyUI_frontend/pull/7479)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7479-feat-server-config-restart-required-toast-2ca6d73d3650811f85f7f0c52c4cf8f0)
by [Unito](https://www.unito.io)
2025-12-16 06:57:11 -07:00
Alexander Piskun
5d1bf6dfb3 fix(api-nodes-pricing): adjust prices for Tripo3D (#6828) 2025-12-16 09:04:20 +02:00
AustinMroz
3ee6d53423 Make node inputs reactive in vue (#7546)
Piecemeal fix pulled out from #7095

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7546-Make-node-inputs-reactive-in-vue-2cb6d73d36508189a88bf35e5747b870)
by [Unito](https://www.unito.io)
2025-12-15 20:50:47 -08:00
AustinMroz
4e257bedca Fix widget reactivity (#7539)
Closes #7095

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7539-Fix-widget-reactivity-2cb6d73d3650816d8977ebda35ab88b9)
by [Unito](https://www.unito.io)
2025-12-15 20:11:05 -08:00
AustinMroz
108cfaaa4b Support display of multitype slots (#7457)
Example with forcibly modified types for testing
<img width="736" height="425" alt="image"
src="https://github.com/user-attachments/assets/e885a7d0-5946-41be-b9b4-b9b195f50c92"
/>

Vue mode doesn't currently seem to display optional inputs, but the SVGs
here include support for being made hollow with `--shape: url(#hollow)`
<img width="765" height="360" alt="image"
src="https://github.com/user-attachments/assets/0ea57179-99a4-4001-aa18-856e172287c0"
/>


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7457-Support-display-of-multitype-slots-2c86d73d3650818594afd988e73827e3)
by [Unito](https://www.unito.io)
2025-12-15 21:25:58 -05:00
AustinMroz
5d745c952a Feat: Fixed option for control after/before generate (#7517)
Followup to #7510.
- Makes `Fixed` a full option with description and swaps to radio
buttons
- Sorts the options to be the same order as litegraph
- Removes the "Edit control settings" button

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/e0e4acda-ba02-4a25-aeca-dd7f1adea0fb"
/>| <img width="360" alt="after"
src="https://github.com/user-attachments/assets/5c4c0fbf-a949-4ce1-83e9-5acdeac3b81a"
/>|

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7517-Austin-vue-fixed-2ca6d73d3650817ca845fb948bee73ac)
by [Unito](https://www.unito.io)
2025-12-15 16:31:31 -08:00
Comfy Org PR Bot
fddd703c4e 1.36.2 (#7533)
Patch version increment to 1.36.2

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7533-1-36-2-2cb6d73d3650815b80dbe07d0c9ea1d1)
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-12-15 17:24:32 -07:00
Terry Jia
dec929909b live selection (#7465)
## Summary

Add real-time selection feedback during marquee drag, matching the
behavior users expect from other applications.

## Changes

- Nodes and groups are now selected/deselected instantly as the
selection rectangle moves
- Supports all modifier keys (Shift to add, Alt to subtract) during drag
- Added Comfy.Graph.LiveSelection setting (off by default)

## Rationale

This interaction pattern is standard across virtually all design and
productivity software:
- Operating Systems: Windows Explorer, macOS Finder, and Linux file
managers all show live selection feedback when dragging
- Design Tools: Figma, Sketch, Adobe Illustrator, Photoshop, and Blender
use real-time selection
- IDEs: VS Code, JetBrains IDEs show live selection in file explorers
- Node Editors: Unreal Engine Blueprints, Unity Shader Graph, and
Houdini all support live selection

## Screenshots

https://github.com/user-attachments/assets/8b0c2217-47f9-4422-9cab-cb39e145310c

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7465-live-selection-2c96d73d36508133a4a6f917955d55b3)
by [Unito](https://www.unito.io)
2025-12-15 19:22:39 -05:00
Alexander Brown
18ce8e940a Fix: Add slot for footer (used by Assets Sidebar) (#7532)
## Summary

It contains the selected items controls.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7532-Fix-Add-slot-for-footer-used-by-Assets-Sidebar-2ca6d73d36508138b044da226dd24cea)
by [Unito](https://www.unito.io)
2025-12-15 23:59:03 +00:00
Terry Jia
38d01548d3 fix: prevent middle mouse button from triggering node resize in vueNodes mode (#7511)
## Summary
Only left mouse button (button === 0) should trigger resize, matching
litegraph behavior.

fix https://github.com/Comfy-Org/ComfyUI_frontend/issues/7476

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7511-fix-prevent-middle-mouse-button-from-triggering-node-resize-in-vueNodes-mode-2ca6d73d3650817b9553f4abbdde3784)
by [Unito](https://www.unito.io)
2025-12-15 15:32:49 -05:00
Alexander Brown
79ddff692e Fix: Extra Scrollbars in Media Assets Sidebar (#7508)
## Summary

The Divider was throwing off the sizing assumptions for the sidebar
tab's width and the interaction between the Sidebar Tab's container
height and the ScrollPanel's and VirtualGrid's sizing calculations.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7508-Fix-Extra-Scrollbars-in-Media-Assets-Sidebar-2ca6d73d365081dd8475e209d9b062c0)
by [Unito](https://www.unito.io)
2025-12-15 15:21:48 -05:00
Terry Jia
fb944fef56 refactor: standardize z-index Tailwind classes (#7509)
## Summary

Replace arbitrary z-index values (z-[n]) with standard Tailwind
utilities.

fix https://github.com/Comfy-Org/ComfyUI_frontend/issues/7499

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7509-refactor-standardize-z-index-Tailwind-classes-2ca6d73d365081339e66f0c8e6fcc20b)
by [Unito](https://www.unito.io)
2025-12-15 15:09:11 -05:00
Christian Byrne
38eaf4b30e ci: add AI agent prompt to backport conflict comments (#7367)
## Summary
- When backports fail due to merge conflicts, the comment now includes a
copyable prompt for AI coding assistants
- Styled similar to CodeRabbit's "Prompt for AI Agents" feature
- Prompt includes all necessary context: PR URL, merge commit, target
branch, conflict files, resolution guidelines

## Example Output

When a backport fails due to conflicts, the workflow now posts a cleaner
comment with an AI agent prompt:

---

### ⚠️ Backport to `core/1.33` failed

**Reason:** Merge conflicts detected during cherry-pick of `5233749`

<details>
<summary>📄 Conflicting files</summary>

- src/scripts/app.ts

</details>

<details>
<summary>🤖 Prompt for AI Agents</summary>

```
Backport PR #7166 (https://github.com/Comfy-Org/ComfyUI_frontend/pull/7166) to core/1.33. Cherry-pick merge commit 5233749fe3 onto new branch backport-7166-to-core-1.33 from origin/core/1.33. Resolve conflicts in: src/scripts/app.ts. For test snapshots (browser_tests/**/*-snapshots/), accept PR version if changed in original PR, else keep target. For package.json versions, keep target branch. For pnpm-lock.yaml, regenerate with pnpm install. Ask user for non-obvious conflicts. Create PR titled "[backport core/1.33] <original title>" with label "backport". See .github/workflows/pr-backport.yaml for workflow details.
```

</details>

---

The "Prompt for AI Agents" section can be copied directly into Claude
Code, Cursor, or other AI coding assistants to resolve the conflicts
automatically.


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7367-ci-add-AI-agent-prompt-to-backport-conflict-comments-2c66d73d365081e1a8fbcfdc48ea8777)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-15 12:45:00 -07:00
Rizumu Ayaka
f065654a1a fix: collapsed nodes getting extra height based on contents (#7490)
Test Workflow: Flux Schnell workflow (from templates)

When nodes are collapsed, they are showing additional height. The amount
of extra height appears to correlate with the contents of the node.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7490-fix-collapsed-nodes-getting-extra-height-based-on-contents-2ca6d73d365081af8476d16bbd7da3c3)
by [Unito](https://www.unito.io)
2025-12-15 11:18:19 -08:00
AustinMroz
42a292932e Support fixed seed in vue (#7510)
A small change pulled out of #7095 to make disabling the current control
option swap to 'fixed' instead of doing nothing.

Resolves #7468

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7510-Support-fixed-seed-in-vue-2ca6d73d365081b0a723ebc97b921305)
by [Unito](https://www.unito.io)
2025-12-15 14:15:12 -05:00
Terry Jia
4ec4da785b fix: render selection rectangle in DOM layer to appear above DOM widgets (#7474)
## Summary
Selection box was being drawn on canvas which appeared below DOM widgets
like images and textareas.

Now rendered via SelectionRectangle.vue with high z-index to ensure
visibility during drag selection.

## Screenshots (if applicable)
before
<img width="1268" height="1258" alt="image"
src="https://github.com/user-attachments/assets/7cb1271c-9ce6-4fac-83a9-ac783a309d97"
/>

after
<img width="1509" height="1129" alt="image"
src="https://github.com/user-attachments/assets/55dd698f-1213-4e60-ae46-9ed292ecd70c"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7474-fix-render-selection-rectangle-in-DOM-layer-to-appear-above-DOM-widgets-2c96d73d36508142bc2ac0d0943043c5)
by [Unito](https://www.unito.io)
2025-12-15 13:41:10 -05:00
Benjamin Lu
abf966ab83 Topbar: add Custom Nodes Manager button (#7400)
Adds a desktop-only "Custom Nodes Manager" topbar button (Lucide puzzle)
in its own bordered island left of the actionbar. Button opens the
manager via useManagerState().openManager().

- New i18n key: menu.customNodesManager

<img width="318" height="147" alt="image"
src="https://github.com/user-attachments/assets/e36c5c7f-80d1-454c-87de-e0daa822fad1"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7400-Topbar-add-Custom-Nodes-Manager-button-2c76d73d3650810b9122da61a3c4be39)
by [Unito](https://www.unito.io)
2025-12-15 09:35:38 -08:00
Christian Byrne
a89fa5a784 fix: "convert to subgraph" not shown in context menu if subgraph inside the selection context (#7470)
Fixes https://github.com/Comfy-Org/ComfyUI_frontend/issues/7453.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7470-fix-convert-to-subgraph-not-shown-in-context-menu-if-subgraph-inside-the-selection-con-2c96d73d36508146a475e8d39c64183c)
by [Unito](https://www.unito.io)
2025-12-15 16:39:18 +01:00
Johnpaul Chiwetelu
c414635ead [feat] Add context menu converter infrastructure (#7113)
## Summary
- Add `contextMenuConverter.ts` with utilities for converting LiteGraph
context menu items to Vue menu format
- Improve `contextMenuCompat.ts` with set-based diffing for more
reliable legacy extension detection
- Extend `MenuOption`/`SubMenuOption` types with `source`, `disabled`,
`isColorPicker`, and `category` type fields
- Add unit tests for converter functions

## Context
This is foundational work for migrating the node context menu from a
custom Popover-based component to PrimeVue ContextMenu.

The converter provides:
- Menu ordering and section grouping (core items first, then extensions)
- Deduplication with preference for Vue-native items over LiteGraph
items
- Extension categorization with labeled section
- Support for disabled states and color picker submenus

## Test plan
- [x] Unit tests pass for `buildStructuredMenu` (9 tests)
- [x] Unit tests pass for `convertContextMenuToOptions` (7 tests)
- [x] Typecheck passes
- [x] Lint passes
- [x] Knip passes (no unused exports)

## Related
This is PR 1 of 2 for the node context menu migration. PR 2 will wire up
the UI component.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7113-feat-Add-context-menu-converter-infrastructure-2be6d73d3650816ca6c9d2cf50f10159)
by [Unito](https://www.unito.io)
2025-12-14 21:01:12 -07:00
Terry Jia
e96593fe4c fix: prevent unrelated groups from moving when dragging nodes in vueNodes mode (#7473)
## Summary

Previously, when dragging a node that was not part of the selection, any
selected groups would still move along with it. This fix ensures groups
only move when the dragged node is actually part of the selection.

## Screenshots (if applicable)
before


https://github.com/user-attachments/assets/ff9a18c2-59b2-4bbd-81b4-7a6ecb35e659


after


https://github.com/user-attachments/assets/019a6cc6-b1e2-41d1-bfec-d6af7ae84091

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7473-fix-prevent-unrelated-groups-from-moving-when-dragging-nodes-in-vueNodes-mode-2c96d73d365081a194a6fef57f9c1108)
by [Unito](https://www.unito.io)
2025-12-14 22:13:00 -05:00
Benjamin Lu
93178c80ba feat(server-config): add legacy manager UI toggle (#7478)
Adds a Desktop (Electron) Server-Config setting for
`--enable-manager-legacy-ui` so users can opt into ComfyUI-Manager’s
legacy UI.

- Adds `enable-manager-legacy-ui` to `SERVER_CONFIG_ITEMS`
- Adds EN i18n label + tooltip

Note: this PR only adds the setting/flag wiring; it does not change
restart behavior in Desktop.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7478-feat-server-config-add-legacy-manager-UI-toggle-2ca6d73d365081a79bb2c376506f5346)
by [Unito](https://www.unito.io)


> [!NOTE]
> This is a stacked PR. (main <=
https://github.com/Comfy-Org/ComfyUI_frontend/pull/7478 <=
https://github.com/Comfy-Org/ComfyUI_frontend/pull/7479)
2025-12-14 18:40:25 -08:00
Christian Byrne
585d46d4fb fix: inner groups being moved double when moving outer group (in vue mode) (#7447)
## Summary

Fixes issue when dragging a group that had inner groups when in vue
mode.

When dragging the outer group in Vue mode:

1. getAllNestedItems(selected) returns ALL items: outer group + inner
groups + nodes
2. moveChildNodesInGroupVueMode loops through all items
3. For outer group G1: calls G1.move(delta, true) then
moveGroupChildren(G1, ...)
4. moveGroupChildren calls G2.move(delta) (no skipChildren) - this moves
G2 AND G2's children!
5. Then the loop reaches G2: calls G2.move(delta, true) - moves G2 again
6. Plus moveGroupChildren(G2, ...) processes G2's children again

This PR fixes it by adding `skipChildren=true` to the `move` call.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7447-fix-inner-groups-being-moved-double-when-moving-outer-group-in-vue-mode-2c86d73d365081ce97abec682f2a8518)
by [Unito](https://www.unito.io)
2025-12-14 18:37:29 -08:00
Comfy Org PR Bot
d70039103c 1.36.1 (#7477)
Patch version increment to 1.36.1

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7477-1-36-1-2ca6d73d3650812d84e6d6b0b079ec7d)
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-12-14 18:35:26 -08:00
AustinMroz
3a091277d0 Nesting support for autogrow (#7275)
- Modifies autogrow inputs to be named by key
- Allows autogrow inputs to be added after initialization.
  - Such as when added by another dynamic combo
- Groups dynamic input information under a single comfyDynamic property
which is opaque to Litegraph

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7275-Nesting-support-for-autogrow-2c46d73d36508171893ec43275f5b644)
by [Unito](https://www.unito.io)
2025-12-14 02:29:34 -08:00
Christian Byrne
209903e1f1 remove contentype badge from media assets card (#7440)
## Summary

Match Figma by removing contenttype assets badge, which doesn't really
serve a good purpose.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7440-remove-contentype-badge-from-media-assets-card-2c86d73d3650818e8d14ed4573d28725)
by [Unito](https://www.unito.io)
2025-12-14 02:26:19 -08:00
Christian Byrne
9ca58ce525 docs: add ADR on importmap removal decision (#7466)
## Summary

Adds an ADR detailing the context and rationale behind removing
importmap and explains how extension developers may go about resolving
any issues caused thereafter. See
https://github.com/Comfy-Org/ComfyUI_frontend/issues/7267#issuecomment-3650045669
for more details.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7466-docs-add-ADR-on-importmap-removal-decision-2c96d73d3650817599a7e8baad539a94)
by [Unito](https://www.unito.io)
2025-12-13 21:15:43 -07:00
Comfy Org PR Bot
c0d3fb312f 1.36.0 (#7467)
Minor version increment to 1.36.0

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7467-1-36-0-2c96d73d365081babc9fc0e3eeab858b)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-12-13 20:37:04 -07:00
Christian Byrne
eb04dc20f3 fix: staging badge shown in cloud settings panel (#7444)
## Summary

Fixes issue where settings panel on cloud was showing a "Staging" badge
because the badge's logic was being dictated by the buildtime config.
Adds the same pattern used everywhere else where, if cloud distribution,
use runtime config; otherwise, fallback to buildtime config. Allows
cloud to switch between staging and prod at runtime while local switches
at buildtime.

After fix:

<img width="2062" height="811" alt="image"
src="https://github.com/user-attachments/assets/e53547c5-3dcc-42ea-9e75-602fe7f855a2"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7444-fix-staging-badge-shown-in-cloud-settings-panel-2c86d73d36508154a788e8e2a6dd025a)
by [Unito](https://www.unito.io)
2025-12-13 18:41:17 -07:00
Christian Byrne
194fbdf520 remove user.css on cloud to prevent failed requests on startup (#7442)
## Summary

Removes the user.css put at top of the index.html when building for
cloud.

On local, now compiles to this (pictured):
<img width="1909" height="184" alt="image"
src="https://github.com/user-attachments/assets/be03beea-35e9-47d6-a293-08f2971b04be"
/>

Formatted, that looks like:

```html
<!doctype html>
<html lang="en">
   <head>
      <link rel="stylesheet" href="user.css">
      <link rel="stylesheet" href="api/userdata/user.css">
      <meta charset="UTF-8">
      <title>ComfyUI</title>
      <meta name="viewport" content="width=device-width,initial-scale=1,user-scalable=no">
      <link rel="stylesheet" href="materialdesignicons.min.css"/>
      <meta name="mobile-web-app-capable" content="yes">
      <meta name="apple-mobile-web-app-status-bar-style" content="black">
      <link rel="manifest" href="./assets/manifest-CebUEmtR.json">
      <script type="module" crossorigin src="./assets/index-DuwpHar_.js"></script>
      <link rel="modulepreload" crossorigin href="./assets/vendor-other--dOoND1c.js">
      <link rel="modulepreload" crossorigin href="./assets/vendor-primevue-BPXiTI_h.js">
      <link rel="modulepreload" crossorigin href="./assets/vendor-vue-RrbnUvXR.js">
      <link rel="modulepreload" crossorigin href="./assets/vendor-xterm-BZLod3g9.js">
      <link rel="modulepreload" crossorigin href="./assets/vendor-three-aR6ntw5X.js">
      <link rel="modulepreload" crossorigin href="./assets/vendor-tiptap-BVGjFCxT.js">
      <link rel="stylesheet" crossorigin href="./assets/vendor-other-DODGPXtn.css">
      <link rel="stylesheet" crossorigin href="./assets/vendor-xterm-BKlWQB97.css">
      <link rel="stylesheet" crossorigin href="./assets/index-CX9dQXxD.css">
   </head>
   <body class="litegraph grid">
      <div id="vue-app"></div>
   </body>
</html>
```

On cloud, this:

<img width="1911" height="1106" alt="image"
src="https://github.com/user-attachments/assets/bbf6046b-e2fd-4e02-bb71-cba27f579271"
/>


## Context

On the cloud distribution, there are currently 401 errors appearing in
the console from requests attempting to load custom user stylesheets:

- `https://cloud.comfy.org/user.css` (returns 200)
- `https://cloud.comfy.org/api/userdata/user.css` (returns 401)

This is a feature inherited from local ComfyUI that allows users to add
custom stylesheets. The implementation naively requests the stylesheet
from the server, and if the user has added one, it gets loaded;
otherwise, the request fails.

This PR removes the custom stylesheet loading from the cloud
distribution by removing it from teh index.html and only re-injecting it
on non-cloud builds.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7442-remove-user-css-on-cloud-to-prevent-failed-requests-on-startup-2c86d73d3650813d82a0deb3b01cee74)
by [Unito](https://www.unito.io)
2025-12-13 18:38:04 -07:00
Comfy Org PR Bot
5f20d554f3 1.35.7 (#7464)
Patch version increment to 1.35.7

**Base branch:** `main`

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

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-12-13 17:42:37 -07:00
Christian Byrne
fd9747375d fix: refreshing assets causes entire panel to re-render (enter loading state) (#7449)
## Problem

The Media Assets panel's loading state is currently determined by the
loading state of the assets store (or something similar). When the store
is refetching and reconciling, it displays a loading spinner briefly on
the entire panel. This causes the following issues:

1. **Visual jarring**: The loading spinner creates an unpleasant visual
flash
2. **Unnecessary reflow**: All assets must re-render after the loading
state changes, causing layout reflow
3. **Performance degradation**: Re-rendering all items is
computationally expensive

## Expected Behavior

Items should be able to be inserted into the list without:

- Re-rendering any other items
- Showing a jarring loading flash
- Causing unnecessary reflow

The loading state of individual items should be decoupled from the
panel's overall loading state, allowing for incremental updates to the
list without affecting the entire panel's UI.


## After

(ignore random progress spinner, removed it after taking the video)


https://github.com/user-attachments/assets/95d7f111-e844-44e2-a0c6-6bcbc4a34797

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7449-fix-refreshing-assets-causes-entire-panel-to-re-render-enter-loading-state-2c86d73d365081be8206f9fdbbf66772)
by [Unito](https://www.unito.io)
2025-12-13 12:55:42 -08:00
Comfy Org PR Bot
5187a77234 1.35.6 (#7452)
Patch version increment to 1.35.6

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7452-1-35-6-2c86d73d36508158952bd1308e819410)
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-12-13 05:51:04 -07:00
AustinMroz
b22ba97a13 Support "control after generate" in vue (#6985)
Continuation of #6034 with
- Updated synchronization for seed
- Properly truncates the displayed widget value for the button
- Synchronizes control after generate state with litegraph and allows
for serialization

Several issues from original PR have not (yet) been addressed, but are
likely better moved to future PR
- fix step value being 10 (legacy system)
- ensure it works with COMBO (Fixed in #7095)
- ensure it works with FLOAT (Fixed in #7095)
- either implement or remove the config button functionality - think it
should open settings?

<img width="280" height="694" alt="image"
src="https://github.com/user-attachments/assets/f36f1cb0-237d-4bfc-bff1-e4976775cf98"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6985-Support-control-after-generate-in-vue-2b86d73d365081d8b01ce489d887ff00)
by [Unito](https://www.unito.io)

---------

Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
2025-12-13 05:23:56 -07:00
Alexander Piskun
aab9a30511 feat(api-nodes): add pricing badge for Kling-2.6 nodes (#7381) 2025-12-13 09:52:42 +02:00
Comfy Org PR Bot
b0f5a9ffe2 1.35.5 (#7433)
Patch version increment to 1.35.5

**Base branch:** `main`

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

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-12-12 23:56:34 -07:00
Christian Byrne
3f777fb54f test: add basic mobile baseline tests (#7415)
## Summary

Allows authors to visualize how their changes affect mobile view. Often
we add some fundamental change to the UI and forget to make it
responsive, this test helps keep track of that.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7415-test-add-basic-mobile-baseline-tests-2c76d73d3650810bb4bad5a1f6c7c53c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-12-12 23:39:17 -07:00
Christian Byrne
6a73288ce3 ci: make nightly release happen automatically every night (#7410)
## Summary

Makes the existing "Release: Version Bump" workflow run at 00:00 UTC
every day.

## Details

- concurrency keeps only one run active while manual dispatch remains
available for ad-hoc bumps.
- inputs are normalized inside the workflow so scheduled runs (which
lack `workflow_dispatch` inputs) safely fall back to `patch`/`main`, and
the version bump + PR formatting steps only use the optional
`pre_release` flag when it is provided
- each nightly invocation closes any lingering bot-authored
`version-bump-*` PRs/branches before creating a new patch PR, preventing
stale locale bumps from conflicting
- checkout now disables credential persistence and `pnpm/action-setup`
is pinned to a commit for supply-chain safety.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7410-ci-make-nightly-release-happen-automatically-every-night-2c76d73d3650813a9e02ee8878370929)
by [Unito](https://www.unito.io)
2025-12-12 22:05:49 -07:00
Christian Byrne
d21ea0f65b fix: loading api-format workflow that contains "parameters" string (#7411)
## Summary

This change extends
https://github.com/Comfy-Org/ComfyUI_frontend/pull/7154 by making sure
the `prompt` metadata tag is parsed before the legacy A1111 fallback
when files are dropped onto the canvas.

ComfyUI embeds two structured payloads into every first-class export
format we support (PNG, WEBP, WEBM, MP4/MOV/M4V, GLB, SVG, MP3,
OGG/FLAC, etc.): `workflow`, which is the full editor JSON with layout
state, and `prompt`, which is the API graph sent to `/prompt`.

During import we try format-specific decoders first and only as a last
resort look for an A1111 file by scanning text chunks for a `parameters`
entry. That compatibility path was always meant to be a best-effort
option, but when we refactored the loader it accidentally enforced the
order `workflow → parameters → prompt`. As soon as a dropped asset
contained a `parameters` chunk—something Image Saver’s “A1111
compatibility” mode always adds—the A1111 converter activated and
blocked the subsequent `prompt` loading logic.

PR #7154 already lifted `workflow` ahead of the fallback, yet any file
lacking the `workflow` chunk but holding both `prompt` and `parameters`
still regressed. Reordering to `workflow → prompt → parameters`
preserves the compatibility shim for genuine A1111 exports while
guaranteeing native Comfy metadata always wins, eliminating the entire
class of failures triggered merely by the presence of the word
`parameters` in an unrelated metadata chunk.

Fixes https://github.com/Comfy-Org/ComfyUI_frontend/issues/7096, fixes
https://github.com/Comfy-Org/ComfyUI_frontend/issues/6988

## Related 

(fixed by https://github.com/Comfy-Org/ComfyUI_frontend/pull/7154)

- https://github.com/Comfy-Org/ComfyUI_frontend/issues/6633
- https://github.com/Comfy-Org/ComfyUI_frontend/issues/6561

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-12-12 21:39:30 -07:00
Alexander Brown
7613e70f63 style-fix: Don't add body padding with no body. (#7424)
## Summary

Small fix for collapsed nodes.

## Screenshots (if applicable)

### Before

<img width="594" height="184" alt="image"
src="https://github.com/user-attachments/assets/1ea39a32-738d-4a1b-87ad-b73abf640b45"
/>

### After
<img width="635" height="206" alt="image"
src="https://github.com/user-attachments/assets/9050bf33-b37c-4ede-8e26-d88fef59bf4d"
/>


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7424-style-fix-Don-t-add-body-padding-with-no-body-2c86d73d3650817cb8f7ccf859e6ab3a)
by [Unito](https://www.unito.io)
2025-12-12 17:03:27 -08:00
Johnpaul Chiwetelu
56b67085d0 Fix snapshot updates commit stage (#7423)
This pull request updates the
`.github/workflows/pr-update-playwright-expectations.yaml` workflow to
improve how changed Playwright snapshot files are detected and handled,
ensuring that both tracked and untracked (new) files are included
throughout the process. The changes also add robustness to file
operations and improve the accuracy of change summaries.

**Improvements to snapshot detection and staging:**

* The workflow now detects both tracked and untracked (new) snapshot
files in `browser_tests/` when preparing changed files for staging,
ensuring that new snapshots are not missed.
* When copying changed files to the staging directory, the script now
skips files that no longer exist (e.g., deleted files), preventing
errors and unnecessary operations.

**Enhancements to change summary and commit logic:**

* The summary of changes now includes both tracked and untracked files
in `browser_tests/`, and the output is expanded to show up to 50 files
for better visibility.
* The logic for determining whether there are changes to commit now
checks for both tracked and untracked changes, ensuring commits are only
made when necessary.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7423-Fix-snapshot-updates-commit-stage-2c76d73d36508195914ec92f37937e67)
by [Unito](https://www.unito.io)
2025-12-12 17:21:37 -07:00
Christian Byrne
4a91330e30 fix: flaky legacy context menu e2e test (#7373)
## Summary

Fixes this test which became flaky after removing the timeout. Now
simply wait for the title text in the context menu, which has built-in
timeout, then take snapshot.

<img width="1515" height="1155" alt="image"
src="https://github.com/user-attachments/assets/f2986ed9-31c4-44a1-89bc-982d1c18109b"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7373-WIP-fix-legacy-context-menu-e2e-test-2c66d73d3650819eb9baceb5bdebfbe8)
by [Unito](https://www.unito.io)
2025-12-12 15:33:10 -08:00
Johnpaul Chiwetelu
a1a507ed09 fix: enhance snapshot update process to include untracked files (#7422)
This pull request improves the snapshot staging process in the
Playwright expectations update workflow. The main focus is to ensure
both modified and newly added snapshot files are correctly detected and
handled, and to avoid errors when files have been deleted.

**Snapshot file detection and handling improvements:**

* The workflow now detects both modified and untracked (new) snapshot
files by combining output from `git diff` and `git ls-files --others`,
ensuring all relevant snapshot changes are staged.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7422-fix-enhance-snapshot-update-process-to-include-untracked-files-2c76d73d365081cc8023d9ed29b8781f)
by [Unito](https://www.unito.io)
2025-12-13 00:04:32 +01:00
Christian Byrne
a7c694f248 fix: update pricing table link (#7402)
## Summary

Update to https://docs.comfy.org/tutorials/partner-nodes/pricing --
actual link to the pricing table (before was sending to just the Partner
Nodes docs).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7402-fix-update-pricing-table-link-2c76d73d365081f2b890ee6887c0314e)
by [Unito](https://www.unito.io)
2025-12-12 15:42:46 -07:00
Christian Byrne
0646bb368a perf: add gpu hint and transform settle to prevent rasterizing while zooming (scale transform) (#7417)
## Summary

Ensures the nodes get their own compositing layers during scale
transform (tracked via mouse wheel events), which prevents rasterization
during transform. Adds forced reflow at end of transform to ensure
layers are always at correct resolution (fixes blurriness and some
readability issues).

Videos show testing this branch first then testing main - doing layer
visualization, paint (include paint operations calculations and actual
raster) visualizations, and cpu usage monitoring.


https://github.com/user-attachments/assets/c5fab219-0b32-4822-9238-c4572f0d6a44



https://github.com/user-attachments/assets/7e172e8d-cc5b-4dcd-aa07-1dfc3eb65bac
2025-12-12 15:41:13 -07:00
Comfy Org PR Bot
a8ef7a602f 1.35.4 (#7420)
Patch version increment to 1.35.4

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7420-1-35-4-2c76d73d365081abbbc6f81b8788c5d5)
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-12-12 15:40:52 -07:00
Simula_r
9c157296be refactor: stop fighting the DOM (#7421)
## Summary

Remove keyDown provider on the LGraphNode, remove inject on widget.

## Changes

- **What**: LGraphNode.vue ImagePreview.vue
- **Breaking**: <!-- Any breaking changes (if none, remove this line)
-->
- **Dependencies**: <!-- New dependencies (if none, remove this line)
-->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7421-refactor-stop-fighting-the-DOM-2c76d73d365081e6b5e9c99a61bbd883)
by [Unito](https://www.unito.io)
2025-12-12 15:40:26 -07:00
Alexander Brown
3e97225ff6 Feat: Separate Subscription management and Upgrade options (#7419)
## Summary

Manage Subscription vs Upgrade Plan

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7419-Feat-Separate-Subscription-management-and-Upgrade-options-2c76d73d36508191a16dd3a25817826f)
by [Unito](https://www.unito.io)
2025-12-12 15:40:07 -07:00
Christian Byrne
1cdea3063d cleanup: remove duplicate browser_tests/browser_tests directory (merge conflict resolution error) (#7413)
Remove orphaned browser_tests/browser_tests/ directory containing 21
duplicate test snapshots. This was accidentally created in PR #6112
during merge. No test execution impact - all valid snapshots exist in
correct locations.

I checked the history of the files, no one had been correctly
touching/changing/using these files since they were added, so simply
removing them is all that's needed here.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7413-cleanup-remove-duplicate-browser_tests-browser_tests-directory-merge-conflict-resolutio-2c76d73d3650816088b3d24c86586084)
by [Unito](https://www.unito.io)
2025-12-12 13:52:27 -08:00
Christian Byrne
b5ab45673a make cloud onboarding survey disableable via runtime feature flag (#7407)
## Summary

The survey is causing some friction. It incurs about 5-10% dropoff.
Although that number is actually somewhat slow, the information has
mostly served its purpose for now. We can toggle it freely once this PR
is merged.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7407-make-cloud-onboarding-survey-disableable-via-runtime-feature-flag-2c76d73d365081648195f322cb0d7a64)
by [Unito](https://www.unito.io)
2025-12-12 13:44:53 -08:00
Alexander Brown
7d326cbc14 Style: Thicker node status indicator (#7409)
## Summary

The outline is already thicker and this means we can use it as an
indicator without squishing the node internals.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7409-Style-Thicker-node-status-indicator-2c76d73d36508132a5defd3a8514013d)
by [Unito](https://www.unito.io)
2025-12-12 12:16:10 -08:00
Christian Byrne
6b1bd4be16 fix: ImagePreview i18n teardown (#7412)
Ensure ImagePreview component tests explicitly unmount their wrappers so
vue-i18n/Intlify watchers stop running before Vitest tears down
happy-dom’s window, eliminating the window is not defined failure noted
after merging #7142.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7412-fix-ImagePreview-i18n-teardown-2c76d73d36508122b94ac66864a0d3f2)
by [Unito](https://www.unito.io)
2025-12-12 03:26:23 -07:00
AustinMroz
57eee5c218 Implement a CustomCombo node (#7142)
Adds a frontend support for a "Custom Combo" node which contains a Combo
Widget, and a growing list of string inputs that determine the options
available to the combo.

![Custom
Combo](https://github.com/user-attachments/assets/3b5a1f68-ce2f-47f1-9ae4-dbb99f610d84)

By promoting the combo widget on this node, a list of options determined
by workflow builders can be exposed to end users.

CC: @Kosinkadink

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7142-Implement-a-CustomCombo-node-2bf6d73d36508161951df01009a7262f)
by [Unito](https://www.unito.io)
2025-12-12 02:23:44 -07:00
Christian Byrne
caca6c4163 fix: text-white usage causes video dimensions to be invisible on light theme (#7408)
## Summary

All other usages of `text-white` in the codebase require also changing
the bg color to be a semantic token. This is the only case where the bg
is theme-aware and the text is hardcoded.


| Before | After |
|
---------------------------------------------------------------------------------------------------------------------------------------------
|
---------------------------------------------------------------------------------------------------------------------------------------------
|
| <img width="1515" height="1155" alt="Screenshot from 2025-12-12
00-03-15"
src="https://github.com/user-attachments/assets/f15bfc48-ded6-4a20-b693-f8d2a2f4cc5b"
/> | <img width="1515" height="1155" alt="Screenshot from 2025-12-12
00-03-22"
src="https://github.com/user-attachments/assets/5dfd7345-0052-48ea-ad77-ecd7f3aa4b89"
/> |

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7408-fix-text-white-usage-causes-video-dimensions-to-be-invisible-on-light-theme-2c76d73d365081668eafef95639a42f9)
by [Unito](https://www.unito.io)
2025-12-12 00:17:32 -08:00
Christian Byrne
0385a7de9b style: fix typography in credits/account panel to be uniform (#7406)
## Summary

Updates typography on the "Manage Subscription" and "Add credits" button
to be uniform and match Figma.

After:

<img width="1515" height="1155" alt="Screenshot from 2025-12-11
23-29-36"
src="https://github.com/user-attachments/assets/a2e5c0bc-d478-45e4-a7f0-d409a233cc0b"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7406-style-fix-typography-in-credits-account-panel-to-be-uniform-2c76d73d365081b69b43dd2e6be50431)
by [Unito](https://www.unito.io)
2025-12-12 00:14:25 -08:00
Comfy Org PR Bot
e41c6934db 1.35.3 (#7405)
Patch version increment to 1.35.3

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7405-1-35-3-2c76d73d36508146b66bc18c512fd6ea)
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-12-12 00:48:32 -07:00
Christian Byrne
2a68dbff5a fix: correct grammar in pricing table description (#7403)
## Summary

Change "amount" to "number," as "video" is a countable noun.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7403-fix-correct-grammar-in-pricing-table-description-2c76d73d36508144828fd060b046c1a5)
by [Unito](https://www.unito.io)
2025-12-11 23:40:08 -08:00
Christian Byrne
2957d9897f fix: button text token on pricing table buttons (#7404)
Button text on middle button below was black before, here is after:

<img width="1703" height="1411" alt="image"
src="https://github.com/user-attachments/assets/dc55b4cf-ee86-49ee-842a-0bed84f78dee"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7404-fix-button-text-token-on-pricing-table-buttons-2c76d73d365081349c63d6a349dee6ed)
by [Unito](https://www.unito.io)
2025-12-11 23:38:52 -08:00
AustinMroz
f2a0e5102e Cleanup app.graph usage (#7399)
Prior to the release of subgraphs, there was a single graph accessed
through `app.graph`. Now that there's multiple graphs, there's a lot of
code that needs to be reviewed and potentially updated depending on if
it cares about nearby nodes, all nodes, or something else requiring
specific attention.

This was done by simply changing the type of `app.graph` to unknown so
the typechecker will complain about every place it's currently used.
References were then updated to `app.rootGraph` if the previous usage
was correct, or actually rewritten.

By not getting rid of `app.graph`, this change already ensures that
there's no loss of functionality for custom nodes, but the prior typing
of `app.graph` can always be restored if future dissuasion of
`app.graph` usage creates issues.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7399-Cleanup-app-graph-usage-2c76d73d365081178743dfdcf07f44d0)
by [Unito](https://www.unito.io)
2025-12-11 23:37:34 -07:00
Simula_r
88bdc605a7 fix: image preview a11y (#7252)
## Summary

Make image preview keyboard accessible, set the key listener on the node
itself for more robust and intuitive handling, also add better aria
labels.

Follow up PR: same on Video preview. 

## Changes

- **What**: LGraphNode.vue, ImagePreview.vue
- **Breaking**: <!-- Any breaking changes (if none, remove this line)
-->
- **Dependencies**: <!-- New dependencies (if none, remove this line)
-->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7252-fix-image-preview-a11y-2c46d73d3650815b9496f3d36a8942bf)
by [Unito](https://www.unito.io)
2025-12-11 23:31:36 -07:00
Simula_r
c1808db7c4 Fix/run button floating calc (#7340)
## Summary

Ensures the undocked ComfyRunButton resets to a valid position when the
app reopens after a browser resize. Since the component is dynamically
imported, we defer setInitialPosition until the suspense boundary has
been resolved rather than calling it on mount (when the import has not
been loaded / wxh are incorrect).

## Changes

- **What**:  ComfyActionbar.vue
- **Breaking**: <!-- Any breaking changes (if none, remove this line)
-->
- **Dependencies**: <!-- New dependencies (if none, remove this line)
-->

Fixes:
https://www.notion.so/comfy-org/Bug-Run-button-missing-after-undocking-moving-to-bottom-right-and-resizing-window-2c06d73d3650810f89e5eb5692f08b83?source=copy_link

## Screenshots (if applicable)

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7340-Fix-run-button-floating-calc-2c66d73d36508122b489c90c81c6129a)
by [Unito](https://www.unito.io)
2025-12-11 23:31:25 -07:00
Christian Byrne
514c437a38 ci: add shellcheck linter for ci scripts (#7331)
## Summary

Adds [shellcheck](https://www.shellcheck.net/) to the PR linting CI
steps -- when a PR has changed a `*.sh` file.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7331-ci-add-shellcheck-linter-for-ci-scripts-2c66d73d365081be889bf256cde92281)
by [Unito](https://www.unito.io)
2025-12-11 23:28:49 -07:00
Alexander Brown
18b133d22f Style: Larger Node Text, More Sidebar Alignment (#7223)
## Summary

See what it looks like. How it feels. What do you think?

- Also was able to unify down to a single SearchBox component.

## Changes

- Bigger widget / slot labels
- Smaller header text
- Unified Searchboxes across sidebar tabs

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7223-Style-prototype-with-larger-node-text-2c36d73d365081f8a371c86f178fa1ff)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-12-11 19:47:28 -08:00
Dr.Lt.Data
3e8a83547d feat: add live preview method setting for prompt execution (#7385)
## Summary

Add frontend setting to override live preview method per prompt
execution.

## Changes

- **What**: New setting `Comfy.Execution.PreviewMethod` allows users to
override preview method (default/none/auto/latent2rgb/taesd) from
frontend. Applied to Queue Prompt, Queue Front, Run Selected Nodes, and
Auto Queue.
- **Dependencies**: Requires backend support from
comfyanonymous/ComfyUI#11261

## Review Focus

- `'default'` option does not send `preview_method` to backend (uses
server CLI setting)
- Legacy UI intentionally not modified (deprecated, maintains backward
compatibility)
- `versionAdded: '1.35.3'` assigned tentatively; adjust as needed for
actual release version

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7385-feat-add-live-preview-method-setting-for-prompt-execution-2c66d73d365081759c9cebaec29f451c)
by [Unito](https://www.unito.io)
2025-12-11 22:45:57 -05:00
Terry Jia
91adcaf276 fix: work around Chrome GPU bug causing severe lag when dragging links (#7394)
## Summary

When Chrome is maximized with GPU acceleration and high DPR, calling
drawImage(canvas) + drawImage(img) in the same frame causes severe
performance degradation (FPS drops to 2-10, memory spikes ~18GB).

Defer image preview rendering using queueMicrotask to separate the two
drawImage calls into different tasks.

### Problem
Severe performance degradation in ComfyUI when dragging connection lines
in litegraph mode:
- FPS drops from 60 to 2-10
- Memory spikes from 36GB to 54GB (~18GB increase)
- CPU jumps from 2% to 15%
- Other Chrome tabs (e.g., YouTube) also stutter

### Environment
- Affected: Chrome with GPU acceleration, maximized/fullscreen window,
high DPR (1.75)
- Not affected: Firefox (WebRender), Chrome in windowed mode, Chrome
with GPU acceleration disabled

### Problem only occurs with:
- GPU acceleration enabled
- Chrome maximized/fullscreen
- An image loaded on canvas (e.g., LoadImage node with preview)

### Root cause: The bug is triggered when two drawImage() calls execute
in the same frame on the same canvas:
- ctx.drawImage(bgcanvas, ...) - copying background canvas to foreground
- ctx.drawImage(img, ...) - rendering image preview in node widget

## Screenshots (if applicable)
Before

https://github.com/user-attachments/assets/76005c10-3430-4d75-a7ed-58f61d18688c

After

https://github.com/user-attachments/assets/5a15b0f9-3935-4428-879b-e55390abff22

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7394-fix-work-around-Chrome-GPU-bug-causing-severe-lag-when-dragging-links-2c66d73d365081469d73d98bf1aa421a)
by [Unito](https://www.unito.io)
2025-12-11 20:38:00 -05:00
Alexander Brown
03e9dd4789 Feat: Remove the Nodes 2.0 Trial Banner (#7390)
## Summary

The option to try it out is still in the Menu if you're looking for it.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7390-Feat-Remove-the-Nodes-2-0-Trial-Banner-2c66d73d365081c3817ad5c89dd4029b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-12-11 17:13:26 -08:00
Johnpaul Chiwetelu
ac8c3847d2 chore: fix playwright expectations (#7395)
## Summary

<!-- One sentence describing what changed and why. -->

## Changes

- **What**: <!-- Core functionality added/modified -->
- **Breaking**: <!-- Any breaking changes (if none, remove this line)
-->
- **Dependencies**: <!-- New dependencies (if none, remove this line)
-->

## Review Focus

<!-- Critical design decisions or edge cases that need attention -->

<!-- If this PR fixes an issue, uncomment and update the line below -->
<!-- Fixes #ISSUE_NUMBER -->

## Screenshots (if applicable)

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7395-chore-fix-playwright-expectations-2c66d73d3650819d8913d80be55d7908)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-12-12 01:14:24 +01:00
Luke Mino-Altherr
c88fc99a86 fix: remove custom LoRA from subscription benefits display (#7396)
## Summary
Removes custom LoRA feature from subscription benefits display for
standard and founder tiers.

## Changes
- **What**: Removed `customLoRAs` benefit entry from `BENEFITS_BY_TIER`
for standard and founder tiers

## Review Focus
- Verify custom LoRA feature is completely removed from subscription UI

Related to #7391

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-11 15:56:51 -08:00
AustinMroz
3dd805a30e Unify zoom to fit implementation (#7393)
Unifys the zoom to fit code between litegraph and vue

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/62390297-d16d-4f0e-9330-add365222f4e"
/>| <img width="360" alt="after"
src="https://github.com/user-attachments/assets/d43ad869-58a6-4614-b7fd-9a60bc3d7bac"
/>|

See [this
comment](https://github.com/Comfy-Org/ComfyUI_frontend/issues/7195#issuecomment-3643714539)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7393-Unify-zoom-to-fit-implementation-2c66d73d36508198a757eedd2d7bd00b)
by [Unito](https://www.unito.io)
2025-12-11 15:28:36 -08:00
Johnpaul Chiwetelu
7c830a2f0b feat: bring node to front when clicking on any widget (#7202)
## Summary
- Adds a capture-phase pointerdown handler to NodeWidgets that calls
`bringNodeToFront` whenever any widget is clicked
- Improves UX by ensuring the interacted node is always visible on top,
without requiring the node itself to be selected

fix https://github.com/Comfy-Org/ComfyUI_frontend/issues/7131

## Before


https://github.com/user-attachments/assets/c2c2ff0e-6e5a-49f2-bf2e-333950559ada

## After


https://github.com/user-attachments/assets/fc3db735-20eb-40b5-9101-278badc4698e


## Test plan
- [ ] Click on any widget (button, dropdown, input, etc.) within a Vue
node
- [ ] Verify the node moves to the front (highest z-index) when the
widget is clicked
- [ ] Verify existing widget functionality is unaffected

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-12-12 00:15:09 +01:00
Luke Mino-Altherr
e7756eb6dd fix: remove custom LoRA feature from standard tier (#7391)
## Summary
Standard tier was incorrectly displaying custom LoRA as a benefit.
Refactored to use strongly-typed benefit configuration.

## Changes
- **What**: Created `BENEFITS_BY_TIER` configuration to explicitly
define tier benefits
- **Type Safety**: Added `TierKey` type and improved type constraints
throughout
- **Fix**: Excluded `customLoRAs` from standard tier (only
creator/pro/founder get this feature)

## Review Focus
Verify standard tier no longer shows custom LoRA feature in subscription
panel

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7391-fix-remove-custom-LoRA-feature-from-standard-tier-2c66d73d36508149ad6ff7bba6333109)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-11 14:34:01 -08:00
ric-yu
c83f3ff1a7 feat: Add Jobs API infrastructure (PR 1 of 3) (#7169)
## Summary

Add Jobs API infrastructure in preparation for migrating from legacy
`/history`, `/history_v2`, and `/queue` endpoints to the unified `/jobs`
API.

**This is PR 1 of 3** - Additive changes only, no breaking changes.

## Changes

- **What**:
- Add Zod schemas for runtime validation of Jobs API responses
(`JobListItem`, `JobDetail`)
- Add `fetchQueue`, `fetchHistory`, `fetchJobDetail` fetchers for
`/jobs` endpoint
- Add `extractWorkflow` utility for extracting workflow from nested job
detail response
- Add synthetic priority assignment for queue ordering (pending >
running > history)
  - Add comprehensive tests for all new fetchers

- **Non-breaking**: All changes are additive - existing code continues
to work

## Review Focus

1. **Zod schema flexibility**: Using `.passthrough()` to allow extra API
fields - ensures forward compatibility but less strict validation
2. **Priority computation**: Synthetic priority ensures display order:
pending (queued) → running → completed (history)
3. **Test coverage**: Verify tests adequately cover edge cases

## Files Added

- `src/platform/remote/comfyui/jobs/` - New Jobs API module
  - `types/jobTypes.ts` - Zod schemas and TypeScript types
  - `fetchers/fetchJobs.ts` - API fetchers with validation
  - `index.ts` - Barrel exports
-
`tests-ui/tests/platform/remote/comfyui/jobs/fetchers/fetchJobs.test.ts`
- Tests

## Next PRs

- **PR 2**: Migrate `getQueue()` and `getHistory()` to use Jobs API
- **PR 3**: Remove legacy history code and unused types

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7169-feat-Add-Jobs-API-infrastructure-PR-1-of-3-2bf6d73d3650812eae4ac0555a86969c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-11 14:17:36 -08:00
Alexander Brown
bf8d9de1c1 Fix: Flaky Playwright Tests: retry some assertions (#7389)
## Summary

Retries the widget value change check for up to 2 whole seconds.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7389-Fix-remoteWidgets-Playwright-test-add-retry-for-assertion-2c66d73d3650814e98b6fdfc83f6d3d6)
by [Unito](https://www.unito.io)
2025-12-11 14:02:23 -08:00
Johnpaul Chiwetelu
b9f75b6cc8 fix: improve type safety in type definitions (#7337)
## Summary

- Replace `any` types with proper TypeScript types in core type
definitions
- Add generics to `SettingParams`, `setting.get<T>()`, and
`setting.set<T>()` for type-safe setting access
- Add `NodeExecutionOutput` interface for `onExecuted` callback
- Change function parameters from `any` to `unknown` where appropriate

## Type Research

Performed GitHub code search across custom node repositories to
understand actual usage patterns:

**`onExecuted` output properties** (used in rgthree-comfy,
ComfyUI-KJNodes, ComfyUI-ExLlama-Nodes, comfy_mtb, etc.):
- `output.text` - string or string array for text display nodes
- `output.images`, `output.audio`, `output.video` - media outputs
- `output.ui.items` - complex debug/preview data with `input`, `text`,
`b64_images`

**`extensionManager.setting.get/set`** (used in ComfyUI-Crystools,
ComfyUI-Copilot, etc.):
- Returns various types (boolean, number, string, objects)
- Now uses generics: `setting.get<boolean>('MyExt.Setting')`

**`ComfyExtension` custom properties** (used in rgthree-comfy,
ComfyUI-Manager):
- `aboutPageBadges`, `commands`, custom methods
- Kept as `any` index signature since extensions add arbitrary
properties

## Changes

| File | Change |
|------|--------|
| `extensionTypes.ts` | Generic `setting.get<T>()` and
`setting.set<T>()`, typed Toast options |
| `litegraph-augmentation.d.ts` | `onExecuted(output:
NodeExecutionOutput)` |
| `metadataTypes.ts` | GLTF index signatures `any` → `unknown` |
| `apiSchema.ts` | New `NodeExecutionOutput` interface |
| `settings/types.ts` | `SettingOnChange<T>`, `SettingMigration<T>`,
`SettingParams<TValue>` |
| `nodeDefSchema.ts` | `validateComfyNodeDef(data: unknown)` |
| `workflowSchema.ts` | `isSubgraphDefinition(obj: unknown)` |
| `telemetry/types.ts` | `checkForCompletedTopup(events: AuditLog[])` |

## Test plan

- [x] `pnpm typecheck` passes
- [x] `pnpm test:unit` passes (3732 tests)
- [x] `pnpm lint` passes

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7337-fix-improve-type-safety-in-type-definitions-2c66d73d365081bdbc30e916cac607d6)
by [Unito](https://www.unito.io)
2025-12-11 22:10:01 +01:00
AustinMroz
3c8b7b015c Fix subgraphNode widget cloning with compressed target_slot (#7388)
## Cause
When graphs are actually exported, several layers of cleanup are
applied. Among these is link compression. Any widgets with inputs that
aren't used do not have inputs stored in the workflow. This was
implemented for backwards compatibility with the old "convert to input"
system for widgets. As part of this process, the target_slots on links
are rewritten such that they point to the index of the widget as if
unconnected widgets did not exist.

This "incorrect" state for links is only corrected AFTER a workflow has
loaded because the 'fix' method needs nodes to be initialized in order
to calculate the correct target_slot

This becomes a problem when subgraphs are introduced. SubgraphInputs
need to resolve a link to its target slot in order to construct a clone
of the linked widget DURING the loading process. Since this target slot
is not accurate, this can result in the cloned widget having the wrong
type.

For a minimal reproduction:
- Create a subgraph with an Empty Latent Image with batch_size linked to
the Subgraph Input
- Export the workflow
- On load, the batch_size has step and min attributes which incorrectly
correspond to width

## Fix
There's multiple possible ways to address this and input on direction is
appreciated
- Fix links before loading graph
  - Likely to break with any dynamic state
- Fix links, then load graph again
- Ugly, bad performance, dynamic state may require multiple passes to
correctly ripple
- In the Subgraph code, ignore target_slot and instead `.find()` input
with matching linkId (proposed)
- Promising, but means accepting that state is just wrong sometimes.
Another forever footgun.
- Entirely remove the input compression
- Some people may complain, and old workflows still need to be supported
- Only remove target_slot redirection inside subgraphs
- Creates ugly logical difference between what happens inside and
outside subgraphs.
  - Still leaves old workflows broken

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7388-Remove-target_slot-compression-from-subgraph-exports-2c66d73d3650815d8c96c5047958ab67)
by [Unito](https://www.unito.io)
2025-12-11 12:32:35 -08:00
Christian Byrne
97fa128999 fix: flaky e2e test for dropping assets on nodes (#7352)
Fix flaky "Can drag-and-drop animated webp image" test that was reading
the widget value before the upload completed, causing intermittent
failures where filenames appeared truncated. Added `waitForUpload`
option to `dragAndDropFile` helper that waits for the `/upload/`
response before returning. This is opt-in since not all drag-and-drop
operations trigger uploads (e.g., loading workflows from media files).
2025-12-11 11:48:02 -08:00
Christian Byrne
1e22c9067d fix: make flaky legacy prompt dialog test use locator rather than snapshot (#7371)
## Summary

Fix the flakiness of [this
test](https://fad8c753.comfyui-playwright-chromium.pages.dev/#?testId=967c1c643b6ca86a362c-8b516e2c224693bf7657)
by converting it from using snapshots to just normal locators.

The LiteGraph prompt that opens when click canvas widgets
(number/string) is still the raw DOM dialog created by
`LGraphCanvas.prototype.prompt`. That implementation wires its "click
outside to close" handler inside a `setTimeout` and ignores outside
clicks for ~256 ms after the dialog appears. It also never updates Vue
state or exposes a ready attribute/event we can observe from Playwright.

Because the UI offers no deterministic signal, using a short intentional
wait that matches the real guard is reasonable. We assert the dialog
becomes visible, call `await comfyPage.delay(300)` (just longer than the
256 ms guard), and then click outside. Without this wait the closing
click fires before the handler exists, so the dialog remains visible and
the test flakes. Until the widget exposes a ready hook, this scoped
delay is the most reliable approach and stays within Playwright guidance
("only sleep when there is no observable condition to await").


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7371-fix-make-flaky-legacy-prompt-dialog-test-use-locator-rather-than-snapshot-2c66d73d36508125b388e68861d7cd28)
by [Unito](https://www.unito.io)
2025-12-11 11:47:39 -08:00
Christian Byrne
273e39bbd1 feat: improve search alg in templates (#7377)
## Summary

Improves search alg on templates, especially for some highly searchd
terms like "wan" or "3d."

<img width="3456" height="2166" alt="image"
src="https://github.com/user-attachments/assets/2138e5c4-3536-4d33-8cd3-a408aea1fcd8"
/>

<img width="3456" height="2166" alt="image"
src="https://github.com/user-attachments/assets/2c0ef2df-7a0d-465c-9063-f70d2a349400"
/>

<img width="3456" height="2166" alt="image"
src="https://github.com/user-attachments/assets/8be9f056-26af-48bd-8214-63b16be68c16"
/>


<img width="3456" height="2166" alt="image"
src="https://github.com/user-attachments/assets/9d6159ce-bbc4-4a40-9455-1972ddd6438a"
/>




[Context](https://comfy-organization.slack.com/archives/C07G75QB06Q/p1765398450984809)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7377-feat-improve-search-alg-in-templates-2c66d73d3650812996e5c8be53873e92)
by [Unito](https://www.unito.io)
2025-12-11 11:47:04 -08:00
Alexander Brown
ca5f24fcd9 Fix: revert st function change (#7387)
Update the 'st' function to use fallback message correctly.

## Summary

Will follow-up to figure out why some custom node descriptions trigger
an issue here.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7387-Fix-revert-st-function-change-2c66d73d3650816991a3ecc2a4740716)
by [Unito](https://www.unito.io)
2025-12-11 19:32:54 +00:00
Alexander Piskun
8ba8b21fa0 increase some API nodes pricing (#7156)
## Summary

Changes for Runway, Luma and Ideogram nodes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7156-increase-some-API-nodes-pricing-2bf6d73d3650818d96c7ce53b3d77ef1)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Luke Mino-Altherr <luke@comfy.org>
Co-authored-by: Hunter <huntcsg@users.noreply.github.com>
2025-12-11 14:03:09 -05:00
Luke Mino-Altherr
1522622427 fix: remove incorrect tooltip on remaining credit balance (#7383)
## Summary
Removed incorrect tooltip displayed on the remaining credit balance in
the subscription panel.

## Changes
- **What**: Removed unused `refreshTooltip` destructure and i18n
translation key
- **Breaking**: None

Fixes #6694

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7383-fix-remove-incorrect-tooltip-on-remaining-credit-balance-2c66d73d3650814eaee0f3c9006b7bd6)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-12-11 10:52:10 -08:00
312 changed files with 9404 additions and 7806 deletions

View File

@@ -14,25 +14,25 @@ DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8188
# Allow dev server access from remote IP addresses.
# If true, the vite dev server will listen on all addresses, including LAN
# and public addresses.
VITE_REMOTE_DEV=false
# VITE_REMOTE_DEV=true
# The directory containing the ComfyUI installation used to run Playwright tests.
# If you aren't using a separate install for testing, point this to your regular install.
TEST_COMFYUI_DIR=/home/ComfyUI
# Whether to enable minification of the frontend code.
ENABLE_MINIFY=true
# ENABLE_MINIFY=true
# Whether to disable proxying the `/templates` route. If true, allows you to
# serve templates from the ComfyUI_frontend/public/templates folder (for
# locally testing changes to templates). When false or nonexistent, the
# templates are served via the normal method from the server's python site
# packages.
DISABLE_TEMPLATES_PROXY=false
# DISABLE_TEMPLATES_PROXY=true
# If playwright tests are being run via vite dev server, Vue plugins will
# invalidate screenshots. When `true`, vite plugins will not be loaded.
DISABLE_VUE_PLUGINS=false
# DISABLE_VUE_PLUGINS=true
# Algolia credentials required for developing with the new custom node manager.
ALGOLIA_APP_ID=4E0RO38HS8
@@ -42,7 +42,3 @@ ALGOLIA_API_KEY=684d998c36b67a9a9fce8fc2d8860579
# SENTRY_AUTH_TOKEN=private-token # get from sentry
# SENTRY_ORG=comfy-org
# SENTRY_PROJECT=cloud-frontend-staging
# Stripe pricing table configuration (used by feature-flagged subscription tiers UI)
# VITE_STRIPE_PUBLISHABLE_KEY=pk_test_123
# VITE_STRIPE_PRICING_TABLE_ID=prctbl_123

View File

@@ -0,0 +1,26 @@
# Description: Runs shellcheck on tracked shell scripts when they change
name: "CI: Shell Validation"
on:
push:
branches:
- main
paths:
- '**/*.sh'
pull_request:
paths:
- '**/*.sh'
jobs:
shell-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Install shellcheck
run: |
sudo apt-get update
sudo apt-get install -y shellcheck
- name: Run shellcheck
run: bash ./scripts/cicd/check-shell.sh

View File

@@ -361,6 +361,42 @@ jobs:
if: steps.filter-targets.outputs.skip != 'true' && failure() && steps.backport.outputs.failed
env:
GH_TOKEN: ${{ github.token }}
BACKPORT_AGENT_PROMPT_TEMPLATE: |
Backport PR #${PR_NUMBER} (${PR_URL}) to ${target}.
Cherry-pick merge commit ${MERGE_COMMIT} onto new branch
${BACKPORT_BRANCH} from origin/${target}.
Resolve conflicts in: ${CONFLICTS_INLINE}.
For test snapshots (browser_tests/**/*-snapshots/), accept PR version if
changed in original PR, else keep target. For package.json versions, keep
target branch. For pnpm-lock.yaml, regenerate with pnpm install.
Ask user for non-obvious conflicts.
Create PR titled "[backport ${target}] <original title>" with label "backport".
See .github/workflows/pr-backport.yaml for workflow details.
COMMENT_BODY_TEMPLATE: |
### ⚠️ Backport to `${target}` failed
**Reason:** Merge conflicts detected during cherry-pick of `${MERGE_COMMIT_SHORT}`
<details>
<summary>📄 Conflicting files</summary>
```
${CONFLICTS_BLOCK}
```
</details>
<details>
<summary>🤖 Prompt for AI Agents</summary>
```
${AGENT_PROMPT}
```
</details>
---
cc @${PR_AUTHOR}
run: |
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
PR_DATA=$(gh pr view ${{ inputs.pr_number }} --json author,mergeCommit)
@@ -383,10 +419,27 @@ jobs:
gh pr comment "${PR_NUMBER}" --body "@${PR_AUTHOR} Commit \`${MERGE_COMMIT}\` already exists on branch \`${target}\`. No backport needed."
elif [ "${reason}" = "conflicts" ]; then
# Convert comma-separated conflicts back to newlines for display
CONFLICTS_LIST=$(echo "${conflicts}" | tr ',' '\n' | sed 's/^/- /')
CONFLICTS_INLINE=$(echo "${conflicts}" | tr ',' ' ')
SAFE_TARGET=$(echo "$target" | tr '/' '-')
BACKPORT_BRANCH="backport-${PR_NUMBER}-to-${SAFE_TARGET}"
PR_URL="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/pull/${PR_NUMBER}"
export PR_NUMBER PR_URL MERGE_COMMIT target BACKPORT_BRANCH CONFLICTS_INLINE
# envsubst is provided by gettext-base
if ! command -v envsubst >/dev/null 2>&1; then
sudo apt-get update && sudo apt-get install -y gettext-base
fi
AGENT_PROMPT=$(envsubst '${PR_NUMBER} ${PR_URL} ${target} ${MERGE_COMMIT} ${BACKPORT_BRANCH} ${CONFLICTS_INLINE}' <<<"$BACKPORT_AGENT_PROMPT_TEMPLATE")
# Use fenced code block for conflicts to handle special chars in filenames
CONFLICTS_BLOCK=$(echo "${conflicts}" | tr ',' '\n')
MERGE_COMMIT_SHORT="${MERGE_COMMIT:0:7}"
export target MERGE_COMMIT_SHORT CONFLICTS_BLOCK AGENT_PROMPT PR_AUTHOR
COMMENT_BODY=$(envsubst '${target} ${MERGE_COMMIT_SHORT} ${CONFLICTS_BLOCK} ${AGENT_PROMPT} ${PR_AUTHOR}' <<<"$COMMENT_BODY_TEMPLATE")
COMMENT_BODY="@${PR_AUTHOR} Backport to \`${target}\` failed: Merge conflicts detected."$'\n\n'"Please manually cherry-pick commit \`${MERGE_COMMIT}\` to the \`${target}\` branch."$'\n\n'"<details><summary>Conflicting files</summary>"$'\n\n'"${CONFLICTS_LIST}"$'\n\n'"</details>"
gh pr comment "${PR_NUMBER}" --body "${COMMENT_BODY}"
fi
done

View File

@@ -124,12 +124,16 @@ jobs:
- name: Stage changed snapshot files
id: changed-snapshots
run: |
set -euo pipefail
echo "=========================================="
echo "STAGING CHANGED SNAPSHOTS (Shard ${{ matrix.shardIndex }})"
echo "=========================================="
# Get list of changed snapshot files
changed_files=$(git diff --name-only browser_tests/ 2>/dev/null | grep -E '\-snapshots/' || echo "")
# Get list of changed snapshot files (including untracked/new files)
changed_files=$( (
git diff --name-only browser_tests/ 2>/dev/null || true
git ls-files --others --exclude-standard browser_tests/ 2>/dev/null || true
) | sort -u | grep -E '\-snapshots/' || true )
if [ -z "$changed_files" ]; then
echo "No snapshot changes in this shard"
@@ -151,6 +155,11 @@ jobs:
# Strip 'browser_tests/' prefix to avoid double nesting
echo "Copying changed files to staging directory..."
while IFS= read -r file; do
# Skip paths that no longer exist (e.g. deletions)
if [ ! -f "$file" ]; then
echo " → (skipped; not a file) $file"
continue
fi
# Remove 'browser_tests/' prefix
file_without_prefix="${file#browser_tests/}"
# Create parent directories
@@ -261,11 +270,19 @@ jobs:
echo "CHANGES SUMMARY"
echo "=========================================="
echo ""
echo "Changed files in browser_tests:"
git diff --name-only browser_tests/ | head -20 || echo "No changes"
echo ""
echo "Total changes:"
git diff --name-only browser_tests/ | wc -l || echo "0"
echo "Changed files in browser_tests (including untracked):"
CHANGES=$(git status --porcelain=v1 --untracked-files=all -- browser_tests/)
if [ -z "$CHANGES" ]; then
echo "No changes"
echo ""
echo "Total changes:"
echo "0"
else
echo "$CHANGES" | head -50
echo ""
echo "Total changes:"
echo "$CHANGES" | wc -l
fi
- name: Commit updated expectations
id: commit
@@ -273,7 +290,7 @@ jobs:
git config --global user.name 'github-actions'
git config --global user.email 'github-actions@github.com'
if git diff --quiet browser_tests/; then
if [ -z "$(git status --porcelain=v1 --untracked-files=all -- browser_tests/)" ]; then
echo "No changes to commit"
echo "has-changes=false" >> $GITHUB_OUTPUT
exit 0

View File

@@ -20,6 +20,13 @@ on:
required: true
default: 'main'
type: string
schedule:
# 00:00 UTC ≈ 4:00 PM PST / 5:00 PM PDT on the previous calendar day
- cron: '0 0 * * *'
concurrency:
group: release-version-bump
cancel-in-progress: true
jobs:
bump-version:
@@ -29,15 +36,99 @@ jobs:
pull-requests: write
steps:
- name: Prepare inputs
id: prepared-inputs
shell: bash
env:
RAW_VERSION_TYPE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.version_type || '' }}
RAW_PRE_RELEASE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.pre_release || '' }}
RAW_BRANCH: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.branch || '' }}
run: |
set -euo pipefail
VERSION_TYPE="$RAW_VERSION_TYPE"
PRE_RELEASE="$RAW_PRE_RELEASE"
TARGET_BRANCH="$RAW_BRANCH"
if [[ -z "$VERSION_TYPE" ]]; then
VERSION_TYPE='patch'
fi
if [[ -z "$TARGET_BRANCH" ]]; then
TARGET_BRANCH='main'
fi
{
echo "version_type=$VERSION_TYPE"
echo "pre_release=$PRE_RELEASE"
echo "branch=$TARGET_BRANCH"
} >> "$GITHUB_OUTPUT"
- name: Close stale nightly version bump PRs
if: github.event_name == 'schedule'
uses: actions/github-script@v7
with:
github-token: ${{ github.token }}
script: |
const prefix = 'version-bump-'
const closed = []
const prs = await github.paginate(github.rest.pulls.list, {
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
per_page: 100
})
for (const pr of prs) {
if (!pr.head?.ref?.startsWith(prefix)) {
continue
}
if (pr.user?.login !== 'github-actions[bot]') {
continue
}
// Only clean up stale nightly PRs targeting main.
// Adjust here if other target branches should be cleaned.
if (pr.base?.ref !== 'main') {
continue
}
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pr.number,
state: 'closed'
})
try {
await github.rest.git.deleteRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `heads/${pr.head.ref}`
})
} catch (error) {
if (![404, 422].includes(error.status)) {
throw error
}
}
closed.push(pr.number)
}
core.info(`Closed ${closed.length} stale PR(s).`)
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.branch }}
ref: ${{ steps.prepared-inputs.outputs.branch }}
fetch-depth: 0
persist-credentials: false
- name: Validate branch exists
env:
TARGET_BRANCH: ${{ steps.prepared-inputs.outputs.branch }}
run: |
BRANCH="${{ github.event.inputs.branch }}"
BRANCH="$TARGET_BRANCH"
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
echo "❌ Branch '$BRANCH' does not exist"
echo ""
@@ -51,7 +142,7 @@ jobs:
echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
with:
version: 10
@@ -62,16 +153,31 @@ jobs:
- name: Bump version
id: bump-version
env:
VERSION_TYPE: ${{ steps.prepared-inputs.outputs.version_type }}
PRE_RELEASE: ${{ steps.prepared-inputs.outputs.pre_release }}
run: |
pnpm version ${{ github.event.inputs.version_type }} --preid ${{ github.event.inputs.pre_release }} --no-git-tag-version
set -euo pipefail
if [[ -n "$PRE_RELEASE" && ! "$VERSION_TYPE" =~ ^pre(major|minor|patch)$ && "$VERSION_TYPE" != "prerelease" ]]; then
echo "❌ pre_release was provided but version_type='$VERSION_TYPE' does not support --preid"
exit 1
fi
if [[ -n "$PRE_RELEASE" ]]; then
pnpm version "$VERSION_TYPE" --preid "$PRE_RELEASE" --no-git-tag-version
else
pnpm version "$VERSION_TYPE" --no-git-tag-version
fi
NEW_VERSION=$(node -p "require('./package.json').version")
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "NEW_VERSION=$NEW_VERSION" >> "$GITHUB_OUTPUT"
- name: Format PR string
id: capitalised
env:
VERSION_TYPE: ${{ steps.prepared-inputs.outputs.version_type }}
run: |
CAPITALISED_TYPE=${{ github.event.inputs.version_type }}
echo "capitalised=${CAPITALISED_TYPE@u}" >> $GITHUB_OUTPUT
CAPITALISED_TYPE="$VERSION_TYPE"
echo "capitalised=${CAPITALISED_TYPE@u}" >> "$GITHUB_OUTPUT"
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
@@ -82,8 +188,8 @@ jobs:
body: |
${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }}
**Base branch:** `${{ github.event.inputs.branch }}`
**Base branch:** `${{ steps.prepared-inputs.outputs.branch }}`
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
base: ${{ github.event.inputs.branch }}
base: ${{ steps.prepared-inputs.outputs.branch }}
labels: |
Release

View File

@@ -9,9 +9,9 @@ import ConfirmationService from 'primevue/confirmationservice'
import ToastService from 'primevue/toastservice'
import Tooltip from 'primevue/tooltip'
import '@/assets/css/style.css'
import { i18n } from '@/i18n'
import '@/lib/litegraph/public/css/litegraph.css'
import '@/assets/css/style.css'
const ComfyUIPreset = definePreset(Aura, {
semantic: {
@@ -58,6 +58,7 @@ export const withTheme = (Story: StoryFn, context: StoryContext) => {
document.documentElement.classList.remove('dark-theme')
document.body.classList.remove('dark-theme')
}
document.body.classList.add('[&_*]:!font-inter')
return Story(context.args, context)
}

View File

@@ -150,7 +150,8 @@ The project uses **Nx** for build orchestration and task management
21. Minimize [nesting](https://wiki.c2.com/?ArrowAntiPattern), e.g. `if () { ... }` or `for () { ... }`
22. Avoid mutable state, prefer immutability and assignment at point of declaration
23. Favor pure functions (especially testable ones)
24. Watch out for [Code Smells](https://wiki.c2.com/?CodeSmell) and refactor to avoid them
24. Do not use function expressions if it's possible to use function declarations instead
25. Watch out for [Code Smells](https://wiki.c2.com/?CodeSmell) and refactor to avoid them
## Testing Guidelines
@@ -214,6 +215,29 @@ The project uses **Nx** for build orchestration and task management
- Thousands of users and extensions
- Prioritize clean interfaces that restrict extension access
### Code Review
In doing a code review, you should make sure that:
- The code is well-designed.
- The functionality is good for the users of the code.
- Any UI changes are sensible and look good.
- Any parallel programming is done safely.
- The code isnt more complex than it needs to be.
- The developer isnt implementing things they might need in the future but dont know they need now.
- Code has appropriate unit tests.
- Tests are well-designed.
- The developer used clear names for everything.
- Comments are clear and useful, and mostly explain why instead of what.
- Code is appropriately documented (generally in g3doc).
- The code conforms to our style guides.
#### [Complexity](https://google.github.io/eng-practices/review/reviewer/looking-for.html#complexity)
Is the CL more complex than it should be? Check this at every level of the CL—are individual lines too complex? Are functions too complex? Are classes too complex? “Too complex” usually means “cant be understood quickly by code readers.” It can also mean “developers are likely to introduce bugs when they try to call or modify this code.”
A particular type of complexity is over-engineering, where developers have made the code more generic than it needs to be, or added functionality that isnt presently needed by the system. Reviewers should be especially vigilant about over-engineering. Encourage developers to solve the problem they know needs to be solved now, not the problem that the developer speculates might need to be solved in the future. The future problem should be solved once it arrives and you can see its actual shape and requirements in the physical universe.
## Repository Navigation
- Check README files in key folders (tests-ui, browser_tests, composables, etc.)

View File

@@ -12,7 +12,7 @@ For first-time setup, use the Claude command:
This bootstraps the monorepo with dependencies, builds, tests, and dev server verification.
**Prerequisites:** Node.js >= 24, Git repository, available ports (5173, 6006)
**Prerequisites:** Node.js >= 24, Git repository, available ports for dev server, storybook, etc.
## Development Workflow

View File

@@ -17,17 +17,9 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
### Prerequisites & Technology Stack
- **Required Software**:
- Node.js (v18 or later to build; v24 for vite dev server) and pnpm
- Node.js (v24) and pnpm
- Git for version control
- A running ComfyUI backend instance
- **Tech Stack**:
- [Vue 3.5 Composition API](https://vuejs.org/) with [TypeScript](https://www.typescriptlang.org/)
- [Pinia](https://pinia.vuejs.org/) for state management
- [PrimeVue](https://primevue.org/) with [TailwindCSS](https://tailwindcss.com/) for UI
- litegraph.js (integrated in src/lib) for node editor
- [zod](https://zod.dev/) for schema validation
- [vue-i18n](https://github.com/intlify/vue-i18n) for internationalization
- A running ComfyUI backend instance (otherwise, you can use `pnpm dev:cloud`)
### Initial Setup
@@ -55,15 +47,18 @@ To launch ComfyUI and have it connect to your development server:
python main.py --port 8188
```
### Git pre-commit hooks
If you are on Mac or a low-spec machine, you can run the server in CPU mode
Run `pnpm prepare` to install Git pre-commit hooks. Currently, the pre-commit hook is used to auto-format code on commit.
```bash
python main.py --port 8188 --cpu
```
### Dev Server
- Start local ComfyUI backend at `localhost:8188`
- Run `pnpm dev` to start the dev server
- Run `pnpm dev:electron` to start the dev server with electron API mocked
- Run `pnpm dev:cloud` to start the dev server against the cloud backend (instead of local ComfyUI server)
#### Access dev server on touch devices
@@ -113,7 +108,7 @@ When you fix a bug that affects a version in feature freeze, we use an automated
1. Create your PR fixing the bug on `main` branch as usual
2. Before merging, add these labels to your PR:
- `needs-backport` - triggers the automated backport workflow
- `1.24` - targets the `core/1.24` release candidate branch
- `core/1.24` - targets the `core/1.24` release candidate branch
3. Merge your PR normally
4. The automated workflow will:
@@ -145,67 +140,6 @@ This project includes `.vscode/launch.json.default` and `.vscode/settings.json.d
We've also included a list of recommended extensions in `.vscode/extensions.json`. Your editor should detect this file and show a human friendly list in the Extensions panel, linking each entry to its marketplace page.
### MCP Integrations
#### Playwright Browser Automation
The Playwright MCP server enables LLM coding assistants (like Copilot, Claude Code, etc.) to control and inspect web browsers programmatically. This allows assistants to navigate websites, take screenshots, and interact with web pages on your behalf.
For ComfyUI_frontend development, you can ask coding assistants to use Playwright screenshots to continuously verify that your code changes produce the expected visual results.
##### Setup for Claude Code
After installing dependencies with `pnpm i`, the Playwright MCP server will be automatically available when you start Claude Code locally.
Here's how Claude Code can use the Playwright MCP server to inspect the interface of the local development server (assuming you're running the dev server at `localhost:5173`):
```
> navigate to localhost:5173 and take a screenshot.
tell me the items in the menubar
● I'll navigate to localhost:5173 and take a
screenshot to see the menubar items.
● playwright:playwright_navigate (MCP)(url: …
"http://local
host:5173")
⎿ Navigated to http://localhost:5173
● playwright:playwright_screenshot (MCP)(name: "lo…
alhost_5173
_menubar")
⎿ Screenshot saved to: ../../../Downloads/localho
st_5173_menubar-2025-05-30T17-23-00-942Z.png
⎿ Screenshot also stored in memory with name:
'localhost_5173_menubar'
● Read(../../../Downloads/localhost_5173_menubar-2…
25-05-30T17-23-00-942Z.png)
⎿ Read image (94.3KB)
● Based on the screenshot, I can see the menubar at
the top of the ComfyUI interface contains the
following items:
Main Menubar Items:
- Workflow - Menu for workflow-related actions
- Edit - Menu for editing operations
- Help - Menu for help and documentation
Additional UI Elements:
- ComfyUI Logo - On the far left
- Workflow Tab - Shows "Unsaved Workflow" with a
dropdown and close button
- Layout Controls - On the far right (grid view
and hamburger menu icons)
The interface shows a typical ComfyUI workflow
graph with nodes like "Load Checkpoint", "CLIP
Text Encode (Prompt)", "KSampler", and "Empty
Latent Image" connected with colored cables.
```
## Testing
### Unit Tests
@@ -215,7 +149,7 @@ Here's how Claude Code can use the Playwright MCP server to inspect the interfac
### Playwright Tests
Playwright tests verify the whole app. See [browser_tests/README.md](browser_tests/README.md) for details.
Playwright tests verify the whole app. See [browser_tests/README.md](browser_tests/README.md) for details. The snapshots are generated in the GH actions runner, not locally.
### Running All Tests
@@ -223,7 +157,6 @@ Before submitting a PR, ensure all tests pass:
```bash
pnpm test:unit
pnpm test:browser
pnpm typecheck
pnpm lint
pnpm format
@@ -232,23 +165,28 @@ pnpm format
## Code Style Guidelines
### TypeScript
- Use TypeScript for all new code
- Avoid `any` types - use proper type definitions
- Never use `@ts-expect-error` - fix the underlying type issue
### Vue 3 Patterns
- Use Composition API for all components
- Follow Vue 3.5+ patterns (props destructuring is reactive)
- Use `<script setup>` syntax
### Styling
- Use Tailwind CSS classes instead of custom CSS
- NEVER use `dark:` or `dark-theme:` tailwind variants. Instead use a semantic value from the `style.css` theme, e.g. `bg-node-component-surface`
- NEVER use `dark:` or `dark-theme:` tailwind variants. Instead use a semantic value from the [style.css](packages/design-system/src/css/style.css) like `bg-node-component-surface`
### Internationalization
- All user-facing strings must use vue-i18n
- Add translations to `src/locales/en/main.json`
- Add translations to [src/locales/en/main.json](src/locales/en/main.json)
- Use translation keys: `const { t } = useI18n(); t('key.path')`
- The corresponding values in other locales is generated automatically on releases, PR authors only need to edit [src/locales/en/main.json](src/locales/en/main.json)
## Icons
@@ -282,34 +220,12 @@ The original litegraph repository (https://github.com/Comfy-Org/litegraph.js) is
2. Run all tests and ensure they pass
3. Create a pull request with a clear title and description
4. Use conventional commit format for PR titles:
- `[feat]` for new features
- `[fix]` for bug fixes
- `[docs]` for documentation
- `[refactor]` for code refactoring
- `[test]` for test additions/changes
- `[chore]` for maintenance tasks
### PR Description Template
```
## Description
Brief description of the changes
## Type of Change
- [ ] Bug fix
- [ ] New feature
- [ ] Breaking change
- [ ] Documentation update
## Testing
- [ ] Unit tests pass
- [ ] Component tests pass
- [ ] Browser tests pass (if applicable)
- [ ] Manual testing completed
## Screenshots (if applicable)
Add screenshots for UI changes
```
- `feat:` for new features
- `fix:` for bug fixes
- `docs:` for documentation
- `refactor:` for code refactoring
- `test:` for test additions/changes
- `chore:` for maintenance tasks
### Review Process
@@ -325,4 +241,4 @@ If you have questions about contributing:
- Ask in our [Discord](https://discord.com/invite/comfyorg)
- Open a new issue for clarification
Thank you for contributing to ComfyUI Frontend!
Thank you for contributing to the ComfyUI Frontend!

View File

@@ -51,8 +51,6 @@ defineProps<{
canProceed: boolean
/** Whether the location step should be disabled */
disableLocationStep: boolean
/** Whether the migration step should be disabled */
disableMigrationStep: boolean
/** Whether the settings step should be disabled */
disableSettingsStep: boolean
}>()

View File

@@ -0,0 +1,92 @@
{
"id": "2ba0b800-2f13-4f21-b8d6-c6cdb0152cae",
"revision": 0,
"last_node_id": 17,
"last_link_id": 9,
"nodes": [
{
"id": 17,
"type": "VAEDecode",
"pos": [
318.8446183157076,
355.3961392345528
],
"size": [
225,
102
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": null
},
{
"name": "vae",
"type": "VAE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": null
}
],
"properties": {
"Node name for S&R": "VAEDecode"
},
"widgets_values": []
}
],
"links": [],
"groups": [
{
"id": 4,
"title": "Outer Group",
"bounding": [
-46.25245366331014,
-150.82497138023245,
1034.4034361963616,
1007.338460439933
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
},
{
"id": 3,
"title": "Inner Group",
"bounding": [
80.96059074101554,
28.123757436778178,
718.286373661183,
691.2397164539732
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
}
],
"config": {},
"extra": {
"ds": {
"scale": 0.7121393732101533,
"offset": [
289.18242848011835,
367.0747755524199
]
},
"frontendVersion": "1.35.5",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true,
"workflowRendererVersion": "Vue"
},
"version": 0.4
}

View File

@@ -0,0 +1,150 @@
{
"id": "e0cb1d7e-5437-4911-b574-c9603dfbeaee",
"revision": 0,
"last_node_id": 2,
"last_link_id": 0,
"nodes": [
{
"id": 2,
"type": "8bfe4227-f272-49e1-a892-0a972a86867c",
"pos": [
-317,
-336
],
"size": [
210,
58
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {
"proxyWidgets": [
[
"-1",
"batch_size"
]
]
},
"widgets_values": [
1
]
}
],
"links": [],
"groups": [],
"definitions": {
"subgraphs": [
{
"id": "8bfe4227-f272-49e1-a892-0a972a86867c",
"version": 1,
"state": {
"lastGroupId": 0,
"lastNodeId": 1,
"lastLinkId": 1,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "New Subgraph",
"inputNode": {
"id": -10,
"bounding": [
-562,
-358,
120,
60
]
},
"outputNode": {
"id": -20,
"bounding": [
-52,
-358,
120,
40
]
},
"inputs": [
{
"id": "b4a8bc2a-8e9f-41aa-938d-c567a11d2c00",
"name": "batch_size",
"type": "INT",
"linkIds": [
1
],
"pos": [
-462,
-338
]
}
],
"outputs": [],
"widgets": [],
"nodes": [
{
"id": 1,
"type": "EmptyLatentImage",
"pos": [
-382,
-376
],
"size": [
270,
106
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"localized_name": "batch_size",
"name": "batch_size",
"type": "INT",
"widget": {
"name": "batch_size"
},
"link": 1
}
],
"outputs": [
{
"localized_name": "LATENT",
"name": "LATENT",
"type": "LATENT",
"links": null
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [
512,
512,
1
]
}
],
"groups": [],
"links": [
{
"id": 1,
"origin_id": -10,
"origin_slot": 0,
"target_id": 1,
"target_slot": 0,
"type": "INT"
}
],
"extra": {}
}
]
},
"config": {},
"extra": {
"frontendVersion": "1.35.1"
},
"version": 0.4
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 422 B

View File

@@ -585,9 +585,15 @@ export class ComfyPage {
fileName?: string
url?: string
dropPosition?: Position
waitForUpload?: boolean
} = {}
) {
const { dropPosition = { x: 100, y: 100 }, fileName, url } = options
const {
dropPosition = { x: 100, y: 100 },
fileName,
url,
waitForUpload = false
} = options
if (!fileName && !url)
throw new Error('Must provide either fileName or url')
@@ -624,6 +630,14 @@ export class ComfyPage {
// Dropping a URL (e.g., dropping image across browser tabs in Firefox)
if (url) evaluateParams.url = url
// Set up response waiter for file uploads before triggering the drop
const uploadResponsePromise = waitForUpload
? this.page.waitForResponse(
(resp) => resp.url().includes('/upload/') && resp.status() === 200,
{ timeout: 10000 }
)
: null
// Execute the drag and drop in the browser
await this.page.evaluate(async (params) => {
const dataTransfer = new DataTransfer()
@@ -690,12 +704,17 @@ export class ComfyPage {
}
}, evaluateParams)
// Wait for file upload to complete
if (uploadResponsePromise) {
await uploadResponsePromise
}
await this.nextFrame()
}
async dragAndDropFile(
fileName: string,
options: { dropPosition?: Position } = {}
options: { dropPosition?: Position; waitForUpload?: boolean } = {}
) {
return this.dragAndDropExternalResource({ fileName, ...options })
}
@@ -1634,6 +1653,55 @@ export class ComfyPage {
}, focusMode)
await this.nextFrame()
}
/**
* Get the position of a group by title.
* @param title The title of the group to find
* @returns The group's canvas position
* @throws Error if group not found
*/
async getGroupPosition(title: string): Promise<Position> {
const pos = await this.page.evaluate((title) => {
const groups = window['app'].graph.groups
const group = groups.find((g: { title: string }) => g.title === title)
if (!group) return null
return { x: group.pos[0], y: group.pos[1] }
}, title)
if (!pos) throw new Error(`Group "${title}" not found`)
return pos
}
/**
* Drag a group by its title.
* @param options.name The title of the group to drag
* @param options.deltaX Horizontal drag distance in screen pixels
* @param options.deltaY Vertical drag distance in screen pixels
*/
async dragGroup(options: {
name: string
deltaX: number
deltaY: number
}): Promise<void> {
const { name, deltaX, deltaY } = options
const screenPos = await this.page.evaluate((title) => {
const app = window['app']
const groups = app.graph.groups
const group = groups.find((g: { title: string }) => g.title === title)
if (!group) return null
// Position in the title area of the group
const clientPos = app.canvasPosToClientPos([
group.pos[0] + 50,
group.pos[1] + 15
])
return { x: clientPos[0], y: clientPos[1] }
}, name)
if (!screenPos) throw new Error(`Group "${name}" not found`)
await this.dragAndDrop(screenPos, {
x: screenPos.x + deltaX,
y: screenPos.y + deltaY
})
}
}
export const testComfySnapToGridGridSize = 50

View File

@@ -160,7 +160,7 @@ export class VueNodeHelpers {
return {
input: widget.locator('input'),
incrementButton: widget.locator('button').first(),
decrementButton: widget.locator('button').last()
decrementButton: widget.locator('button').nth(1)
}
}
}

View File

@@ -1,4 +1,5 @@
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import path from 'path'
import type {
@@ -8,9 +9,20 @@ import type {
export class ComfyTemplates {
readonly content: Locator
readonly allTemplateCards: Locator
constructor(readonly page: Page) {
this.content = page.getByTestId('template-workflows-content')
this.allTemplateCards = page.locator('[data-testid^="template-workflow-"]')
}
async waitForMinimumCardCount(count: number) {
return await expect(async () => {
const cardCount = await this.allTemplateCards.count()
expect(cardCount).toBeGreaterThanOrEqual(count)
}).toPass({
timeout: 1_000
})
}
async loadTemplate(id: string) {

View File

@@ -77,8 +77,7 @@ test.describe('Background Image Upload', () => {
// Verify the URL input now has an API URL
const urlInput = backgroundImageSetting.locator('input[type="text"]')
const inputValue = await urlInput.inputValue()
expect(inputValue).toMatch(/^\/api\/view\?.*subfolder=backgrounds/)
await expect(urlInput).toHaveValue(/^\/api\/view\?.*subfolder=backgrounds/)
// Verify clear button is now enabled
const clearButton = backgroundImageSetting.locator('button:has(.pi-trash)')

View File

@@ -36,9 +36,10 @@ test.describe('Execute to selected output nodes', () => {
await output1.click('title')
await comfyPage.executeCommand('Comfy.QueueSelectedOutputNodes')
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
await expect(async () => {
expect(await (await input.getWidget(0)).getValue()).toBe('foo')
expect(await (await output1.getWidget(0)).getValue()).toBe('foo')
expect(await (await output2.getWidget(0)).getValue()).toBe('')
}).toPass({ timeout: 2_000 })
})
})

View File

@@ -306,14 +306,16 @@ test.describe('Node Interaction', () => {
await comfyPage.canvas.click({
position: numberWidgetPos
})
await expect(comfyPage.canvas).toHaveScreenshot('prompt-dialog-opened.png')
const legacyPrompt = comfyPage.page.locator('.graphdialog')
await expect(legacyPrompt).toBeVisible()
await comfyPage.delay(300)
await comfyPage.canvas.click({
position: {
x: 10,
y: 10
}
})
await expect(comfyPage.canvas).toHaveScreenshot('prompt-dialog-closed.png')
await expect(legacyPrompt).toBeHidden()
})
test('Can close prompt dialog with canvas click (text widget)', async ({
@@ -327,18 +329,16 @@ test.describe('Node Interaction', () => {
await comfyPage.canvas.click({
position: textWidgetPos
})
await expect(comfyPage.canvas).toHaveScreenshot(
'prompt-dialog-opened-text.png'
)
const legacyPrompt = comfyPage.page.locator('.graphdialog')
await expect(legacyPrompt).toBeVisible()
await comfyPage.delay(300)
await comfyPage.canvas.click({
position: {
x: 10,
y: 10
}
})
await expect(comfyPage.canvas).toHaveScreenshot(
'prompt-dialog-closed-text.png'
)
await expect(legacyPrompt).toBeHidden()
})
test('Can double click node title to edit', async ({ comfyPage }) => {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

@@ -12,6 +12,7 @@ test.describe('Load Workflow in Media', () => {
'edited_workflow.webp',
'no_workflow.webp',
'large_workflow.webp',
'workflow_prompt_parameters.png',
'workflow.webm',
// Skipped due to 3d widget unstable visual result.
// 3d widget shows grid after fully loaded.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 143 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

View File

@@ -0,0 +1,29 @@
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
import { expect } from '@playwright/test'
test.describe('Mobile Baseline Snapshots', () => {
test('@mobile empty canvas', async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.ConfirmClear', false)
await comfyPage.executeCommand('Comfy.ClearWorkflow')
await expect(async () => {
expect(await comfyPage.getGraphNodesCount()).toBe(0)
}).toPass({ timeout: 256 })
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('mobile-empty-canvas.png')
})
test('@mobile default workflow', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('default')
await expect(comfyPage.canvas).toHaveScreenshot(
'mobile-default-workflow.png'
)
})
test('@mobile settings dialog', async ({ comfyPage }) => {
await comfyPage.settingDialog.open()
await comfyPage.nextFrame()
await expect(comfyPage.settingDialog.root).toHaveScreenshot(
'mobile-settings-dialog.png'
)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -260,6 +260,12 @@ test.describe('Release context menu', () => {
test('Can trigger on link release', async ({ comfyPage }) => {
await comfyPage.disconnectEdge()
const contextMenu = comfyPage.page.locator('.litecontextmenu')
// Wait for context menu with correct title (slot name | slot type)
// The title shows the output slot name and type from the disconnected link
await expect(contextMenu.locator('.litemenu-title')).toContainText(
'CLIP | CLIP'
)
await comfyPage.page.mouse.move(10, 10)
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot(

View File

@@ -212,8 +212,12 @@ test.describe('Remote COMBO Widget', () => {
// Click on the canvas to trigger widget refresh
await comfyPage.page.mouse.click(400, 300)
const refreshedOptions = await getWidgetOptions(comfyPage, nodeName)
expect(refreshedOptions).not.toEqual(initialOptions)
await expect(async () => {
const refreshedOptions = await getWidgetOptions(comfyPage, nodeName)
expect(refreshedOptions).not.toEqual(initialOptions)
}).toPass({
timeout: 2_000
})
})
test('does not refresh when TTL is not set', async ({ comfyPage }) => {
@@ -321,8 +325,12 @@ test.describe('Remote COMBO Widget', () => {
await clickRefreshButton(comfyPage, nodeName)
// Verify the selected value of the widget is the first option in the refreshed list
const refreshedValue = await getWidgetValue(comfyPage, nodeName)
expect(refreshedValue).toEqual('new first option')
await expect(async () => {
const refreshedValue = await getWidgetValue(comfyPage, nodeName)
expect(refreshedValue).toEqual('new first option')
}).toPass({
timeout: 2_000
})
})
})

View File

@@ -85,7 +85,7 @@ test.describe('Selection Toolbox - More Options Submenus', () => {
const initialShape = await nodeRef.getProperty<number>('shape')
await openMoreOptions(comfyPage)
await comfyPage.page.getByText('Shape', { exact: true }).click()
await comfyPage.page.getByText('Shape', { exact: true }).hover()
await expect(comfyPage.page.getByText('Box', { exact: true })).toBeVisible({
timeout: 5000
})
@@ -136,13 +136,18 @@ test.describe('Selection Toolbox - More Options Submenus', () => {
comfyPage
}) => {
await openMoreOptions(comfyPage)
await expect(
comfyPage.page.getByText('Rename', { exact: true })
).toBeVisible({ timeout: 5000 })
const renameItem = comfyPage.page.getByText('Rename', { exact: true })
await expect(renameItem).toBeVisible({ timeout: 5000 })
// Wait for multiple frames to allow PrimeVue's outside click handler to initialize
for (let i = 0; i < 30; i++) {
await comfyPage.nextFrame()
}
await comfyPage.page
.locator('#graph-canvas')
.click({ position: { x: 0, y: 50 }, force: true })
await comfyPage.nextFrame()
await expect(
comfyPage.page.getByText('Rename', { exact: true })

View File

@@ -290,16 +290,20 @@ test.describe('Node library sidebar', () => {
await comfyPage.page.keyboard.insertText('bar')
await comfyPage.page.keyboard.press('Enter')
await comfyPage.nextFrame()
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['bar/'])
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization')
).toEqual({
'bar/': {
icon: 'pi-folder',
color: '#007bff'
}
await expect(async () => {
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.Bookmarks.V2')
).toEqual(['bar/'])
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization')
).toEqual({
'bar/': {
icon: 'pi-folder',
color: '#007bff'
}
})
}).toPass({
timeout: 2_000
})
})

View File

@@ -329,6 +329,15 @@ test.describe('Subgraph Operations', () => {
expect(newInputName).toBe(labelClickRenamedName)
expect(newInputName).not.toBe(initialInputLabel)
})
test('Can create widget from link with compressed target_slot', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('subgraphs/subgraph-compressed-target-slot')
const step = await comfyPage.page.evaluate(() => {
return window['app'].graph.nodes[0].widgets[0].options.step
})
expect(step).toBe(10)
})
})
test.describe('Subgraph Creation and Deletion', () => {

View File

@@ -188,22 +188,19 @@ test.describe('Templates', () => {
.locator('header')
.filter({ hasText: 'Templates' })
const cardCount = await comfyPage.page
.locator('[data-testid^="template-workflow-"]')
.count()
expect(cardCount).toBeGreaterThan(0)
await comfyPage.templates.waitForMinimumCardCount(1)
await expect(templateGrid).toBeVisible()
await expect(nav).toBeVisible() // Nav should be visible at desktop size
const mobileSize = { width: 640, height: 800 }
await comfyPage.page.setViewportSize(mobileSize)
expect(cardCount).toBeGreaterThan(0)
await comfyPage.templates.waitForMinimumCardCount(1)
await expect(templateGrid).toBeVisible()
await expect(nav).not.toBeVisible() // Nav should collapse at mobile size
const tabletSize = { width: 1024, height: 800 }
await comfyPage.page.setViewportSize(tabletSize)
expect(cardCount).toBeGreaterThan(0)
await comfyPage.templates.waitForMinimumCardCount(1)
await expect(templateGrid).toBeVisible()
await expect(nav).toBeVisible() // Nav should be visible at tablet size
})

View File

@@ -32,4 +32,42 @@ test.describe('Vue Node Groups', () => {
'vue-groups-fit-to-contents.png'
)
})
test('should move nested groups together when dragging outer group', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('groups/nested-groups-1-inner-node')
// Get initial positions with null guards
const outerInitial = await comfyPage.getGroupPosition('Outer Group')
const innerInitial = await comfyPage.getGroupPosition('Inner Group')
const initialOffsetX = innerInitial.x - outerInitial.x
const initialOffsetY = innerInitial.y - outerInitial.y
// Drag the outer group
const dragDelta = { x: 100, y: 80 }
await comfyPage.dragGroup({
name: 'Outer Group',
deltaX: dragDelta.x,
deltaY: dragDelta.y
})
// Use retrying assertion to wait for positions to update
await expect(async () => {
const outerFinal = await comfyPage.getGroupPosition('Outer Group')
const innerFinal = await comfyPage.getGroupPosition('Inner Group')
const finalOffsetX = innerFinal.x - outerFinal.x
const finalOffsetY = innerFinal.y - outerFinal.y
// Both groups should have moved
expect(outerFinal.x).not.toBe(outerInitial.x)
expect(innerFinal.x).not.toBe(innerInitial.x)
// The relative offset should be maintained (inner group moved with outer)
expect(finalOffsetX).toBeCloseTo(initialOffsetX, 0)
expect(finalOffsetY).toBeCloseTo(initialOffsetY, 0)
}).toPass({ timeout: 5000 })
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 60 KiB

View File

@@ -0,0 +1,144 @@
import {
comfyExpect as expect,
comfyPageFixture as test
} from '../../../../fixtures/ComfyPage'
import type { ComfyPage } from '../../../../fixtures/ComfyPage'
import { fitToViewInstant } from '../../../../helpers/fitToView'
test.describe('Vue Node Bring to Front', () => {
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Disabled')
await comfyPage.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.loadWorkflow('vueNodes/simple-triple')
await comfyPage.vueNodes.waitForNodes()
await fitToViewInstant(comfyPage)
})
/**
* Helper to get the z-index of a node by its title
*/
async function getNodeZIndex(
comfyPage: ComfyPage,
title: string
): Promise<number> {
const node = comfyPage.vueNodes.getNodeByTitle(title)
const style = await node.getAttribute('style')
if (!style) {
throw new Error(
`Node "${title}" has no style attribute (observed: ${style})`
)
}
const match = style.match(/z-index:\s*(\d+)/)
if (!match) {
throw new Error(
`Node "${title}" has no z-index in style (observed: "${style}")`
)
}
return parseInt(match[1], 10)
}
/**
* Helper to get the bounding box center of a node
*/
async function getNodeCenter(
comfyPage: ComfyPage,
title: string
): Promise<{ x: number; y: number }> {
const node = comfyPage.vueNodes.getNodeByTitle(title)
const box = await node.boundingBox()
if (!box) throw new Error(`Node "${title}" not found`)
return { x: box.x + box.width / 2, y: box.y + box.height / 2 }
}
test('should bring overlapped node to front when clicking on it', async ({
comfyPage
}) => {
// Get initial positions
const clipCenter = await getNodeCenter(comfyPage, 'CLIP Text Encode')
const ksamplerHeader = await comfyPage.page
.getByText('KSampler')
.boundingBox()
if (!ksamplerHeader) throw new Error('KSampler header not found')
// Drag KSampler on top of CLIP Text Encode
await comfyPage.dragAndDrop(
{ x: ksamplerHeader.x + 50, y: ksamplerHeader.y + 10 },
clipCenter
)
await comfyPage.nextFrame()
// Screenshot showing KSampler on top of CLIP
await expect(comfyPage.canvas).toHaveScreenshot(
'bring-to-front-overlapped-before.png'
)
// KSampler should be on top (higher z-index) after being dragged
const ksamplerZIndexBefore = await getNodeZIndex(comfyPage, 'KSampler')
const clipZIndexBefore = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
expect(ksamplerZIndexBefore).toBeGreaterThan(clipZIndexBefore)
// Click on CLIP Text Encode (underneath) - need to click on a visible part
// Since KSampler is on top, we click on the edge of CLIP that should still be visible
const clipNode = comfyPage.vueNodes.getNodeByTitle('CLIP Text Encode')
const clipBox = await clipNode.boundingBox()
if (!clipBox) throw new Error('CLIP node not found')
// Click on a visible edge of CLIP
await comfyPage.page.mouse.click(clipBox.x + 30, clipBox.y + 10)
await comfyPage.nextFrame()
// CLIP should now be on top - compare post-action z-indices
const clipZIndexAfter = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
const ksamplerZIndexAfter = await getNodeZIndex(comfyPage, 'KSampler')
expect(clipZIndexAfter).toBeGreaterThan(ksamplerZIndexAfter)
// Screenshot showing CLIP now on top
await expect(comfyPage.canvas).toHaveScreenshot(
'bring-to-front-overlapped-after.png'
)
})
test('should bring overlapped node to front when clicking on its widget', async ({
comfyPage
}) => {
// Get CLIP Text Encode position (it has a text widget)
const clipCenter = await getNodeCenter(comfyPage, 'CLIP Text Encode')
// Get VAE Decode position and drag it on top of CLIP
const vaeHeader = await comfyPage.page.getByText('VAE Decode').boundingBox()
if (!vaeHeader) throw new Error('VAE Decode header not found')
await comfyPage.dragAndDrop(
{ x: vaeHeader.x + 50, y: vaeHeader.y + 10 },
{ x: clipCenter.x - 50, y: clipCenter.y }
)
await comfyPage.nextFrame()
// VAE should be on top after drag
const vaeZIndexBefore = await getNodeZIndex(comfyPage, 'VAE Decode')
const clipZIndexBefore = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
expect(vaeZIndexBefore).toBeGreaterThan(clipZIndexBefore)
// Screenshot showing VAE on top
await expect(comfyPage.canvas).toHaveScreenshot(
'bring-to-front-widget-overlapped-before.png'
)
// Click on the text widget of CLIP Text Encode
const clipNode = comfyPage.vueNodes.getNodeByTitle('CLIP Text Encode')
const clipBox = await clipNode.boundingBox()
if (!clipBox) throw new Error('CLIP node not found')
await comfyPage.page.mouse.click(clipBox.x + 170, clipBox.y + 80)
await comfyPage.nextFrame()
// CLIP should now be on top - compare post-action z-indices
const clipZIndexAfter = await getNodeZIndex(comfyPage, 'CLIP Text Encode')
const vaeZIndexAfter = await getNodeZIndex(comfyPage, 'VAE Decode')
expect(clipZIndexAfter).toBeGreaterThan(vaeZIndexAfter)
// Screenshot showing CLIP now on top after widget click
await expect(comfyPage.canvas).toHaveScreenshot(
'bring-to-front-widget-overlapped-after.png'
)
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 145 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -15,7 +15,9 @@ test.describe('Vue Integer Widget', () => {
await comfyPage.loadWorkflow('vueNodes/linked-int-widget')
await comfyPage.vueNodes.waitForNodes()
const seedWidget = comfyPage.vueNodes.getWidgetByName('KSampler', 'seed')
const seedWidget = comfyPage.vueNodes
.getWidgetByName('KSampler', 'seed')
.first()
const controls = comfyPage.vueNodes.getInputNumberControls(seedWidget)
const initialValue = Number(await controls.input.inputValue())

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 84 KiB

View File

@@ -252,7 +252,8 @@ test.describe('Animated image widget', () => {
// Drag and drop image file onto the load animated webp node
await comfyPage.dragAndDropFile('animated_webp.webp', {
dropPosition: { x, y }
dropPosition: { x, y },
waitForUpload: true
})
// Expect the filename combo value to be updated

View File

@@ -0,0 +1,97 @@
# 5. Remove Import Map for Vue Extensions
Date: 2025-12-13
## Status
Accepted
## Context
ComfyUI frontend previously used a Vite plugin (`generateImportMapPlugin`) to inject an HTML import map exposing shared modules to extensions. This allowed Vue-based extensions to mark dependencies as external in their Vite configs:
```typescript
// Extension vite.config.ts (old pattern)
rollupOptions: {
external: ['vue', 'vue-i18n', 'pinia', /^primevue\/?.*/, ...]
}
```
The import map resolved bare specifiers like `import { ref } from 'vue'` at runtime by mapping them to pre-built ESM files served from `/assets/lib/`.
**Modules exposed via import map:**
- `vue` (vue.esm-browser.prod.js)
- `vue-i18n` (vue-i18n.esm-browser.prod.js)
- `primevue/*` (all PrimeVue components)
- `@primevue/themes/*`
- `@primevue/forms/*`
**Problems with import map approach:**
1. **Blocked tree shaking**: Vue and PrimeVue loaded as remote modules at runtime, preventing bundler optimizations. The entire Vue runtime was loaded even if only a few APIs were used.
2. **Poor code splitting**: PrimeVue's component library split into hundreds of small chunks, each requiring a separate network request on mount. This significantly impacted initial page load.
3. **Cold start performance**: Each externalized module required a separate HTTP request and browser module resolution step. This compounded on lower-end systems and slower networks.
4. **Version alignment complexity**: Extensions relied on the frontend's Vue version at runtime. Subtle version mismatches between build-time types and runtime code caused debugging difficulties.
5. **Incompatible with Cloud distribution**: The Cloud deployment model requires fully bundled, optimized assets. Import maps added a layer of indirection incompatible with our CDN and caching strategy.
## Decision
Remove the `generateImportMapPlugin` and require Vue-based extensions to bundle their own Vue instance.
**Implementation (PR #6899):**
- Deleted `build/plugins/generateImportMapPlugin.ts`
- Removed plugin configuration from `vite.config.mts`
- Removed `fast-glob` dependency used by the plugin
**Extension migration path:**
1. Remove `external: ['vue', ...]` from Vite rollup options
2. Vue and related dependencies will be bundled into the extension output
3. No code changes required in extension source files
The import map was already disabled for Cloud builds (PR #6559) before complete removal. Removal aligns all distribution channels on the same bundling strategy.
## Consequences
### Positive
- **Improved page load**: Full tree shaking and optimal code splitting now apply to Vue and PrimeVue
- **Faster development**: No import map generation step; simplified build pipeline
- **Better debugging**: Extension's bundled Vue matches build-time expectations exactly
- **Cloud compatibility**: All assets fully bundled and CDN-optimizable
- **Consistent behavior**: Same bundling strategy across desktop, localhost, and cloud distributions
- **Reduced network requests**: Fewer module fetches on initial page load
### Negative
- **Breaking change for existing extensions**: Extensions using `external: ['vue']` pattern fail with "Failed to resolve module specifier 'vue'" error
- **Larger extension bundles**: Each extension now includes its own Vue instance (~30KB gzipped)
- **Potential version fragmentation**: Different extensions may bundle different Vue versions (mitigated by Vue's stable API)
### Migration Impact
Extensions affected must update their build configuration. The migration is straightforward:
```diff
// vite.config.ts
rollupOptions: {
- external: ['vue', 'vue-i18n', 'primevue', ...]
}
```
Affected versions:
- **v1.32.x - v1.33.8**: Import map present, external pattern works
- **v1.33.9+**: Import map removed, bundling required
## Notes
- [ComfyUI_frontend_vue_basic](https://github.com/jtydhr88/ComfyUI_frontend_vue_basic) has been updated to demonstrate the new bundled pattern
- Issue #7267 documents the user-facing impact and migration discussion
- Future Extension API v2 (Issue #4668) may provide alternative mechanisms for shared dependencies

View File

@@ -14,6 +14,7 @@ An Architecture Decision Record captures an important architectural decision mad
| [0002](0002-monorepo-conversion.md) | Restructure as a Monorepo | Accepted | 2025-08-25 |
| [0003](0003-crdt-based-layout-system.md) | Centralized Layout Management with CRDT | Proposed | 2025-08-27 |
| [0004](0004-fork-primevue-ui-library.md) | Fork PrimeVue UI Library | Rejected | 2025-08-27 |
| [0005](0005-remove-importmap-for-vue-extensions.md) | Remove Import Map for Vue Extensions | Accepted | 2025-12-13 |
## Creating a New ADR

View File

@@ -5,7 +5,7 @@ import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescrip
import { importX } from 'eslint-plugin-import-x'
import oxlint from 'eslint-plugin-oxlint'
import eslintPluginPrettierRecommended from 'eslint-plugin-prettier/recommended'
import storybook from 'eslint-plugin-storybook'
import { configs as storybookConfigs } from 'eslint-plugin-storybook'
import unusedImports from 'eslint-plugin-unused-imports'
import pluginVue from 'eslint-plugin-vue'
import { defineConfig } from 'eslint/config'
@@ -109,7 +109,8 @@ export default defineConfig([
// Difference in typecheck on CI vs Local
pluginVue.configs['flat/recommended'],
eslintPluginPrettierRecommended,
storybook.configs['flat/recommended'],
// @ts-expect-error Type incompatibility between storybook plugin and ESLint config types
storybookConfigs['flat/recommended'],
// @ts-expect-error Type incompatibility between import-x plugin and ESLint config types
importX.flatConfigs.recommended,
// @ts-expect-error Type incompatibility between import-x plugin and ESLint config types

2
global.d.ts vendored
View File

@@ -13,8 +13,6 @@ interface Window {
max_upload_size?: number
comfy_api_base_url?: string
comfy_platform_base_url?: string
stripe_publishable_key?: string
stripe_pricing_table_id?: string
firebase_config?: {
apiKey: string
authDomain: string

View File

@@ -5,8 +5,6 @@
<title>ComfyUI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" type="text/css" href="materialdesignicons.min.css" />
<link rel="stylesheet" type="text/css" href="user.css" />
<link rel="stylesheet" type="text/css" href="api/userdata/user.css" />
<!-- Fullscreen mode on mobile browsers -->
<meta name="mobile-web-app-capable" content="yes">

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.35.2",
"version": "1.36.3",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -40,7 +40,7 @@
"preinstall": "pnpm dlx only-allow pnpm",
"prepare": "husky || true && git config blame.ignoreRevsFile .git-blame-ignore-revs || true",
"preview": "nx preview",
"storybook": "nx storybook -p 6006",
"storybook": "nx storybook",
"stylelint:fix": "stylelint --cache --fix '{apps,packages,src}/**/*.{css,vue}'",
"stylelint": "stylelint --cache '{apps,packages,src}/**/*.{css,vue}'",
"test:browser": "pnpm exec nx e2e",
@@ -161,6 +161,7 @@
"algoliasearch": "catalog:",
"axios": "catalog:",
"chart.js": "^4.5.0",
"cva": "catalog:",
"dompurify": "^3.2.5",
"dotenv": "catalog:",
"es-toolkit": "^1.39.9",
@@ -175,7 +176,7 @@
"pinia": "catalog:",
"primeicons": "catalog:",
"primevue": "catalog:",
"reka-ui": "^2.5.0",
"reka-ui": "catalog:",
"semver": "^7.7.2",
"three": "^0.170.0",
"tiptap-markdown": "^0.8.10",

View File

@@ -98,7 +98,6 @@
--color-bypass: #6a246a;
--color-error: #962a2a;
--color-comfy-menu-secondary: var(--comfy-menu-secondary-bg);
--color-interface-panel-job-progress-primary: var(--color-azure-300);
--color-interface-panel-job-progress-secondary: var(--color-alpha-azure-600-30);
@@ -236,7 +235,7 @@
--brand-yellow: var(--color-electric-400);
--brand-blue: var(--color-sapphire-700);
--secondary-background: var(--color-smoke-200);
--secondary-background-hover: var(--color-smoke-200);
--secondary-background-hover: var(--color-smoke-400);
--secondary-background-selected: var(--color-smoke-600);
--base-background: var(--color-white);
--primary-background: var(--color-azure-400);
@@ -438,7 +437,11 @@
--color-interface-button-hover-surface: var(
--interface-button-hover-surface
);
--color-comfy-input: var(--comfy-input-bg);
--color-comfy-input-foreground: var(--input-text);
--color-comfy-menu-bg: var(--comfy-menu-bg);
--color-comfy-menu-secondary: var(--comfy-menu-secondary-bg);
--color-interface-stroke: var(--interface-stroke);
--color-nav-background: var(--nav-background);
--color-node-border: var(--node-border);
@@ -1325,6 +1328,15 @@ audio.comfy-audio.empty-audio-widget {
font-size 0.1s ease;
}
/* Performance optimization during canvas interaction */
.transform-pane--interacting .lg-node * {
transition: none !important;
}
.transform-pane--interacting .lg-node {
will-change: transform;
}
/* ===================== Mask Editor Styles ===================== */
/* To be migrated to Tailwind later */
#maskEditor_brush {

View File

@@ -0,0 +1,19 @@
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="hollow">
<path
d="M -50 50
A 100 100, 0, 0, 1, 150 50
A 100 100, 0, 0, 1, -50 50
M 30 50
A 20 20, 0, 0, 0, 70 50
A 20 20, 0, 0, 0, 30 50"/>
</clipPath>
</defs>
<g clip-path="var(--shape)" stroke-width="4">
<path d="M 50 0 A 50 50, 0, 0, 1, 50 100" fill="var(--type1, red)"/>
<path d="M 50 100 A 50 50, 0, 0, 1, 50 0" fill="var(--type2, blue)"/>
<path d="M50 0L50 100" stroke="var(--inner-stroke, black)"/>
<path d="M50 2A48 48 0 0 1 50 98A48 48 0 0 1 50 2" fill="transparent" stroke="var(--outer-stroke, transparent)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 693 B

View File

@@ -0,0 +1,20 @@
<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<defs>
<clipPath id="hollow">
<path
d="M-50 50
A100 100 0 0 1 150 50
A100 100 0 0 1 -50 50
M30 50
A20 20 0 0 0 70 50
A20 20 0 0 0 30 50"/>
</clipPath>
</defs>
<g clip-path="var(--shape)" stroke-width="4">
<path d="M50 0A50 50 0 0 1 93 75L50 50" fill="var(--type1, red)"/>
<path d="M93 75A50 50 0 0 1 7 75L50 50" fill="var(--type2, blue)"/>
<path d="M7 75A50 50 0 0 1 50 0L50 50" fill="var(--type3, green)"/>
<path d="M50 50L50 0M50 50L93 75M50 50L7 75" stroke="var(--inner-stroke, black)"/>
<path d="M50 2A48 48 0 0 1 50 98A48 48 0 0 1 50 2" fill="transparent" stroke="var(--outer-stroke, transparent)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 763 B

3682
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,10 +11,10 @@ catalog:
'@iconify/tailwind': ^1.1.3
'@intlify/eslint-plugin-vue-i18n': ^4.1.0
'@lobehub/i18n-cli': ^1.25.1
'@nx/eslint': 21.4.1
'@nx/playwright': 21.4.1
'@nx/storybook': 21.4.1
'@nx/vite': 21.4.1
'@nx/eslint': 22.2.6
'@nx/playwright': 22.2.6
'@nx/storybook': 22.2.4
'@nx/vite': 22.2.6
'@pinia/testing': ^0.1.5
'@playwright/test': ^1.52.0
'@prettier/plugin-oxc': ^0.1.3
@@ -27,19 +27,19 @@ catalog:
'@primevue/themes': ^4.2.5
'@sentry/vite-plugin': ^4.6.0
'@sentry/vue': ^8.48.0
'@storybook/addon-docs': ^9.1.1
'@storybook/vue3': ^9.1.1
'@storybook/vue3-vite': ^9.1.1
'@storybook/addon-docs': ^10.1.9
'@storybook/vue3': ^10.1.9
'@storybook/vue3-vite': ^10.1.9
'@tailwindcss/vite': ^4.1.12
'@trivago/prettier-plugin-sort-imports': ^5.2.0
'@types/fs-extra': ^11.0.4
'@types/jsdom': ^21.1.7
'@types/node': ^20.14.8
'@types/node': ^20.19.0
'@types/semver': ^7.7.0
'@types/three': ^0.169.0
'@vitejs/plugin-vue': ^5.1.4
'@vitejs/plugin-vue': ^6.0.0
'@vitest/coverage-v8': ^3.2.4
'@vitest/ui': ^3.0.0
'@vitest/ui': ^3.2.0
'@vue/test-utils': ^2.4.6
'@vueuse/core': ^11.0.0
'@vueuse/integrations': ^13.9.0
@@ -47,6 +47,7 @@ catalog:
algoliasearch: ^5.21.0
axios: ^1.8.2
cross-env: ^10.1.0
cva: 1.0.0-beta.4
dotenv: ^16.4.5
eslint: ^9.39.1
eslint-config-prettier: ^10.1.8
@@ -54,7 +55,7 @@ catalog:
eslint-plugin-import-x: ^4.16.1
eslint-plugin-oxlint: 1.25.0
eslint-plugin-prettier: ^5.5.4
eslint-plugin-storybook: ^9.1.16
eslint-plugin-storybook: ^10.1.9
eslint-plugin-unused-imports: ^4.3.0
eslint-plugin-vue: ^10.6.2
firebase: ^11.6.0
@@ -67,7 +68,7 @@ catalog:
lint-staged: ^15.5.2
markdown-table: ^3.0.4
mixpanel-browser: ^2.71.0
nx: 21.4.1
nx: 22.2.6
oxlint: ^1.32.0
oxlint-tsgolint: ^0.8.4
picocolors: ^1.1.1
@@ -77,8 +78,9 @@ catalog:
pretty-bytes: ^7.1.0
primeicons: ^7.0.0
primevue: ^4.2.5
reka-ui: ^2.5.0
rollup-plugin-visualizer: ^6.0.4
storybook: ^9.1.16
storybook: ^10.1.9
stylelint: ^16.26.1
tailwindcss: ^4.1.12
tailwindcss-primeui: ^0.6.1
@@ -90,10 +92,10 @@ catalog:
unplugin-icons: ^0.22.0
unplugin-typegpu: 0.8.0
unplugin-vue-components: ^0.28.0
vite: ^5.4.19
vite: ^7.0.0
vite-plugin-dts: ^4.5.4
vite-plugin-html: ^3.2.2
vite-plugin-vue-devtools: ^7.7.6
vite-plugin-vue-devtools: ^8.0.0
vitest: ^3.2.4
vue: ^3.5.13
vue-component-type-helpers: ^3.0.7

19
scripts/cicd/check-shell.sh Executable file
View File

@@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(git rev-parse --show-toplevel)"
cd "$ROOT_DIR"
if ! command -v shellcheck >/dev/null 2>&1; then
echo "Error: shellcheck is required but not installed" >&2
exit 127
fi
mapfile -t shell_files < <(git ls-files -- '*.sh')
if [[ ${#shell_files[@]} -eq 0 ]]; then
echo 'No shell scripts found.'
exit 0
fi
shellcheck --format=gcc "${shell_files[@]}"

View File

@@ -74,7 +74,7 @@ deploy_report() {
# Project name with dots converted to dashes for Cloudflare
sanitized_browser=$(echo "$browser" | sed 's/\./-/g')
sanitized_browser="${browser//./-}"
project="comfyui-playwright-${sanitized_browser}"
echo "Deploying $browser to project $project on branch $branch..." >&2
@@ -208,7 +208,7 @@ else
# Wait for all deployments to complete
for pid in $pids; do
wait $pid
wait "$pid"
done
# Collect URLs and counts in order
@@ -254,9 +254,9 @@ else
total_tests=0
# Parse counts and calculate totals
IFS='|'
set -- $all_counts
for counts_json; do
IFS='|' read -r -a counts_array <<< "$all_counts"
for counts_json in "${counts_array[@]}"; do
[ -z "$counts_json" ] && continue
if [ "$counts_json" != "{}" ] && [ -n "$counts_json" ]; then
# Parse JSON counts using simple grep/sed if jq is not available
if command -v jq > /dev/null 2>&1; then
@@ -324,13 +324,12 @@ $status_icon **$status_text**
# Add browser results with individual counts
i=0
IFS='|'
set -- $all_counts
for counts_json; do
# Get browser name
browser=$(echo "$BROWSERS" | cut -d' ' -f$((i + 1)))
# Get URL at position i
url=$(echo "$urls" | cut -d' ' -f$((i + 1)))
IFS=' ' read -r -a browser_array <<< "$BROWSERS"
IFS=' ' read -r -a url_array <<< "$urls"
for counts_json in "${counts_array[@]}"; do
[ -z "$counts_json" ] && { i=$((i + 1)); continue; }
browser="${browser_array[$i]:-}"
url="${url_array[$i]:-}"
if [ "$url" != "failed" ] && [ -n "$url" ]; then
# Parse individual browser counts
@@ -374,4 +373,4 @@ $status_icon **$status_text**
🎉 Click on the links above to view detailed test results for each browser configuration."
post_comment "$comment"
fi
fi

View File

@@ -264,7 +264,7 @@ if (!releaseInfo) {
}
// Output as JSON for GitHub Actions
// eslint-disable-next-line no-console
console.log(JSON.stringify(releaseInfo, null, 2))
export { resolveRelease }

View File

@@ -10,48 +10,66 @@
</div>
<div class="mx-1 flex flex-col items-end gap-1">
<div
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-interface-stroke px-2 shadow-interface"
>
<ActionBarButtons />
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div class="flex items-center gap-2">
<div
ref="legacyCommandsContainerRef"
class="[&:not(:has(*>*:not(:empty)))]:hidden"
></div>
<ComfyActionbar />
<IconButton
v-tooltip.bottom="queueHistoryTooltipConfig"
type="transparent"
size="sm"
class="relative mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
:aria-pressed="isQueueOverlayExpanded"
:aria-label="
t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
"
@click="toggleQueueOverlay"
v-if="managerState.shouldShowManagerButtons.value && isDesktop"
class="pointer-events-auto flex h-12 shrink-0 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
>
<i class="icon-[lucide--history] size-4" />
<span
v-if="queuedCount > 0"
class="absolute -top-1 -right-1 min-w-[16px] rounded-full bg-primary-background py-0.25 text-[10px] font-medium leading-[14px] text-white"
<IconButton
v-tooltip.bottom="customNodesManagerTooltipConfig"
type="transparent"
size="sm"
class="text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
:aria-label="t('menu.customNodesManager')"
@click="openCustomNodeManager"
>
{{ queuedCount }}
</span>
</IconButton>
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
<LoginButton v-else-if="isDesktop" />
<IconButton
v-if="!isRightSidePanelOpen"
v-tooltip.bottom="rightSidePanelTooltipConfig"
type="transparent"
size="sm"
class="mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
:aria-label="t('rightSidePanel.togglePanel')"
@click="rightSidePanelStore.togglePanel"
<i class="icon-[lucide--puzzle] size-4" />
</IconButton>
</div>
<div
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
>
<i class="icon-[lucide--panel-right] size-4" />
</IconButton>
<ActionBarButtons />
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
ref="legacyCommandsContainerRef"
class="[&:not(:has(*>*:not(:empty)))]:hidden"
></div>
<ComfyActionbar />
<IconButton
v-tooltip.bottom="queueHistoryTooltipConfig"
type="transparent"
size="sm"
class="relative mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
:aria-pressed="isQueueOverlayExpanded"
:aria-label="
t('sideToolbar.queueProgressOverlay.expandCollapsedQueue')
"
@click="toggleQueueOverlay"
>
<i class="icon-[lucide--history] size-4" />
<span
v-if="queuedCount > 0"
class="absolute -top-1 -right-1 min-w-[16px] rounded-full bg-primary-background py-0.25 text-[10px] font-medium leading-[14px] text-white"
>
{{ queuedCount }}
</span>
</IconButton>
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
<LoginButton v-else-if="isDesktop" />
<IconButton
v-if="!isRightSidePanelOpen"
v-tooltip.bottom="rightSidePanelTooltipConfig"
type="transparent"
size="sm"
class="mr-2 text-base-foreground transition-colors duration-200 ease-in-out bg-secondary-background hover:bg-secondary-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-primary-background"
:aria-label="t('rightSidePanel.togglePanel')"
@click="rightSidePanelStore.togglePanel"
>
<i class="icon-[lucide--panel-right] size-4" />
</IconButton>
</div>
</div>
<QueueProgressOverlay
v-model:expanded="isQueueOverlayExpanded"
@@ -74,18 +92,23 @@ import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import LoginButton from '@/components/topbar/LoginButton.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useErrorHandling } from '@/composables/useErrorHandling'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { app } from '@/scripts/app'
import { useQueueStore } from '@/stores/queueStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isElectron } from '@/utils/envUtil'
import { useManagerState } from '@/workbench/extensions/manager/composables/useManagerState'
import { ManagerTab } from '@/workbench/extensions/manager/types/comfyManagerTypes'
const workspaceStore = useWorkspaceStore()
const rightSidePanelStore = useRightSidePanelStore()
const managerState = useManagerState()
const { isLoggedIn } = useCurrentUser()
const isDesktop = isElectron()
const { t } = useI18n()
const { toastErrorHandler } = useErrorHandling()
const isQueueOverlayExpanded = ref(false)
const queueStore = useQueueStore()
const isTopMenuHovered = ref(false)
@@ -93,6 +116,9 @@ const queuedCount = computed(() => queueStore.pendingTasks.length)
const queueHistoryTooltipConfig = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))
)
const customNodesManagerTooltipConfig = computed(() =>
buildTooltipConfig(t('menu.customNodesManager'))
)
// Right side panel toggle
const { isOpen: isRightSidePanelOpen } = storeToRefs(rightSidePanelStore)
@@ -112,10 +138,20 @@ onMounted(() => {
const toggleQueueOverlay = () => {
isQueueOverlayExpanded.value = !isQueueOverlayExpanded.value
}
</script>
<style scoped>
.actionbar-container {
background-color: var(--comfy-menu-bg);
const openCustomNodeManager = async () => {
try {
await managerState.openManager({
initialTab: ManagerTab.All,
showToastOnLegacyError: false
})
} catch (error) {
try {
toastErrorHandler(error)
} catch (toastError) {
console.error(error)
console.error(toastError)
}
}
}
</style>
</script>

View File

@@ -28,8 +28,9 @@
)
"
/>
<ComfyRunButton />
<Suspense @resolve="comfyRunButtonResolved">
<ComfyRunButton />
</Suspense>
<IconButton
v-tooltip.bottom="cancelJobTooltipConfig"
type="transparent"
@@ -56,7 +57,7 @@ import {
import { clamp } from 'es-toolkit/compat'
import { storeToRefs } from 'pinia'
import Panel from 'primevue/panel'
import { computed, nextTick, onMounted, ref, watch } from 'vue'
import { computed, nextTick, ref, watch } from 'vue'
import IconButton from '@/components/button/IconButton.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
@@ -139,7 +140,14 @@ const setInitialPosition = () => {
}
}
}
onMounted(setInitialPosition)
//The ComfyRunButton is a dynamic import. Which means it will not be loaded onMount in this component.
//So we must use suspense resolve to ensure that is has loaded and updated the DOM before calling setInitialPosition()
async function comfyRunButtonResolved() {
await nextTick()
setInitialPosition()
}
watch(visible, async (newVisible) => {
if (newVisible) {
await nextTick(setInitialPosition)

View File

@@ -58,7 +58,7 @@ const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
const nodeDefStore = useNodeDefStore()
const hasMissingNodes = computed(() =>
graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName)
graphHasMissingNodes(app.rootGraph, nodeDefStore.nodeDefsByName)
)
const { t } = useI18n()

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