Compare commits

...

106 Commits

Author SHA1 Message Date
github-actions
b0e7883f25 [automated] Update test expectations 2025-12-23 22:46:53 +00:00
Terry Jia
2d7123fc6b fix: sync node.imgs on image load for VueNodes context menu 2025-12-23 15:49:54 -05:00
Comfy Org PR Bot
12dc32b019 1.36.9 (#7735)
Patch version increment to 1.36.9

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7735-1-36-9-2d26d73d36508175a1b6f191f59cdbf8)
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-22 17:13:29 -08:00
Luke Mino-Altherr
47884c623e [feat] Add HuggingFace model import support (#7540)
## Summary
Adds HuggingFace as a model import source alongside CivitAI, with
improved UX for model type selection and UTF-8 filename support.

## Changes
- **Import Sources**: Implemented extensible import source handler
pattern supporting both CivitAI and HuggingFace
- **UTF-8 Support**: Decode URL-encoded filenames to properly display
international characters (e.g., Chinese)
- **UX**: Sort model types alphabetically for easier selection
- **Feature Flag**: Added `huggingfaceModelImportEnabled` flag for
gradual rollout
- **i18n**: Use proper template parameters for localized error messages

## Technical Details
- Created `ImportSourceHandler` interface for extensibility
- Refactored existing CivitAI logic into handler pattern
- Added URL validation per source
- Filename decoding handles malformed URLs gracefully

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7540-feat-Add-HuggingFace-model-import-support-2cb6d73d3650818f966cca89244e8c36)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
2025-12-22 11:34:49 -08:00
Hunter
176c8e110b feat: pass target tier to billing portal for subscription updates (#7692)
## Summary

Pass target tier to billing portal API for deep linking to Stripe's
subscription update confirmation screen when user has an active
subscription.

## Changes

- **What**: When a user with an active subscription clicks a tier in
PricingTable, pass the target tier (including billing cycle) to
`accessBillingPortal` which sends it as `target_tier` in the request
body. This enables the backend to create a Stripe billing portal deep
link directly to the subscription update confirmation screen.
- **Dependencies**: Requires comfy-api PR for `POST /customers/billing`
`target_tier` support

## Review Focus

- PricingTable now differentiates between new subscriptions (checkout
flow) and existing subscriptions (billing portal with deep link)
- Type derivation uses `Parameters<typeof
authStore.accessBillingPortal>[0]` to avoid duplicating the tier union
(matches codebase pattern)
- Registry types manually updated to include `target_tier` field (will
be regenerated when API is deployed)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7692-feat-pass-target-tier-to-billing-portal-for-subscription-updates-2d06d73d365081b38fe4c81e95dce58c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
2025-12-22 11:43:44 -07:00
Christian Byrne
959c1990b5 update partner nodes from USD to comfy credits (#7451)
## Summary

Show in comfy credits

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7451-WIP-update-partner-nodes-from-USD-to-comfy-credits-2c86d73d3650818f9b2deda45e4968e1)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: bigcat88 <bigcat88@icloud.com>
2025-12-22 19:59:35 +02:00
Christian Byrne
e2c8478025 fix: show yearly labels in subscription panel for annual subscribers (#7706)
## Summary

Updates SubscriptionPanel to display yearly-appropriate labels for
annual subscribers:

- "Credits remaining this year" instead of "this month"
- "Yearly credits" instead of "Monthly credits" in the "Your plan
includes" section

## Changes

- Added `creditsRemainingThisYear` i18n key
- Added `creditsRemainingLabel` computed that switches based on
`isYearlySubscription`
- Updated `tierBenefits` to use `yearlyCreditsLabel` for annual
subscribers

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7706-fix-show-yearly-labels-in-subscription-panel-for-annual-subscribers-2d16d73d365081488552c2c0b03d862e)
by [Unito](https://www.unito.io)
2025-12-22 08:19:17 -07:00
Christian Byrne
0762985ca7 cleanup: remove the legacy mask editor and all related code (#7370)
## Summary

Removes the legacy mask editor. May also want to remove the "Beta" tags
on all the current mask editor components/mentions in the app now as
well.

## Context

Telemetry data shows zero users using the legacy mask editor. It has
been considerable time since we switched to the new one, and there
really is no reason to use the legacy version given how lacking it is.

In https://github.com/Comfy-Org/ComfyUI_frontend/pull/7332 (v1.35.2 -
Dec 11, 2025), we added a final warning that the legacy mask editor is
being removed.

On 1.36, this PR can be merged, as more than enough warning will have
been given.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7370-cleanup-remove-the-legacy-mask-editor-and-all-related-code-2c66d73d365081d58fbed0f3c84bcb0d)
by [Unito](https://www.unito.io)
2025-12-22 07:17:31 -07:00
Simula_r
9514e5d36c Fix(cloud)/pricing annual misc (#7701)
## Summary

Fix: PricingTable showed "Current Plan" on the wrong billing cycle
(e.g., showing it on Yearly when subscribed to Monthly) because we
weren't checking subscription_duration. Now we check for ANNUAL |
MONTHLY match.

Fix: Subscribed users were being sent to billing portal instead of
checkout. Now routes to checkout.

Improved: Types now use openapi.yml as source of truth. Tier names in
user popover and subscription panels now reflect the billing cycle
(YEARLY/MONTHLY).

Recommended to merge this before
https://github.com/Comfy-Org/ComfyUI_frontend/pull/7692

---------

Co-authored-by: bymyself <cbyrne@comfy.org>
2025-12-22 05:01:32 -07:00
Comfy Org PR Bot
970861e677 [chore] Update Comfy Registry API types from comfy-api@ade7a7d (#7702)
## Automated API Type Update

This PR updates the Comfy Registry API types from the latest comfy-api
OpenAPI specification.

- API commit: ade7a7d
- Generated on: 2025-12-22T11:30:15Z

These types are automatically generated using openapi-typescript.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7702-chore-Update-Comfy-Registry-API-types-from-comfy-api-ade7a7d-2d16d73d3650814b9b9ad0793ea44926)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-12-22 04:48:58 -07:00
Benjamin Lu
216c8694c7 Desktop: wire release toast Update to updater (#7549)
Desktop builds should not send users to git-update docs from the release
notification toast; route the toast “Update” action into the Desktop
updater flow.

On Electron, the release toast “Update” button executes
`Comfy-Desktop.CheckForUpdates`; failures surface via
`useErrorHandling`.

Fixes #7543

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7549-Desktop-wire-release-toast-Update-to-updater-2cb6d73d3650815aadbccc15c724815d)
by [Unito](https://www.unito.io)
2025-12-22 04:30:08 -07:00
Benjamin Lu
d253f67919 refactor node help watch source (#7660)
Extract the node help watch source into a computed value.

## What changed
- Move the watch predicate in `NodeHelpPage.vue` into a named
`activeHelpDef` computed and pass it to `whenever`
- Keep behavior identical while making the watch source easier to read
and reference

## Why
- Motivation: a review comment requested separating the predicate from
the watcher for readability and idiomatic usage
- Why this approach: a local computed is the smallest change that
preserves behavior and matches the requested structure
- Tradeoffs / limitations: adds a couple of lines and a computed without
changing runtime behavior

## Evidence
- Tests: Not run (existing unit tests for help sync live in
`tests-ui/tests/components/sidebar/nodeLibrary/NodeHelpPage.test.ts`)

## References
- Review comment:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/7105#discussion_r2636631268
(request to extract the watcher predicate into a computed)
- Related PR: https://github.com/Comfy-Org/ComfyUI_frontend/pull/7105
(original help sync change)

---------

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2025-12-22 03:12:11 -07:00
Comfy Org PR Bot
082426f084 docs: Weekly Documentation Update (#6369)
# Documentation Accuracy Review - No Changes Required

## Summary

After conducting a comprehensive fact-check of all documentation across
the ComfyUI Frontend repository, I found that **the documentation is
100% accurate** and up-to-date with the current codebase. Only one minor
correction was needed.

•  All Claude commands documented correctly
•  Package scripts accurately referenced  
•  API examples verified against implementation
•  Extension APIs match current interface
•  Port numbers and URLs correct
• 🔧 Fixed single incorrect test script reference

## Changes Made

### Minor Corrections
- **tests-ui/README.md**: Fixed watch mode command from non-existent
`pnpm test:unit:dev` to correct `pnpm test:unit -- --watch`

## Review Notes

### Documentation Files Verified
- **Core Documentation**: CLAUDE.md, README.md, CONTRIBUTING.md (
accurate)
- **Command Documentation**: All .claude/commands/*.md files (
accurate)
- **Technical Documentation**: docs/ directory including ADRs, settings,
features ( accurate)
- **Development Guides**: Testing, extensions, litegraph API docs (
accurate)
- **Package Configuration**: All scripts in package.json match
documented commands ( accurate)

### API Verification
- **Extension Manager API**: Verified dialog.prompt(), dialog.confirm(),
toast.addAlert() examples against implementation ( accurate)
- **Settings API**: Confirmed extensionManager.setting.get/set methods
exist ( accurate)
- **Development Scripts**: All pnpm commands referenced in docs exist in
package.json ( accurate)

### Infrastructure Checks
- **Port Configuration**: localhost:5173 references accurate for Vite
dev server ( correct)
- **Package Manager**: Consistent use of pnpm throughout documentation
( accurate)
- **Node.js Version**: Node 24 requirement properly documented (
accurate)
- **Setup Process**: /setup_repo command implementation matches
documentation ( accurate)

The ComfyUI Frontend documentation is exceptionally well-maintained with
accurate references to current implementation, proper API examples, and
up-to-date development workflows.

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-12-22 03:06:33 -07:00
Comfy Org PR Bot
08c43f6028 1.36.8 (#7699)
Patch version increment to 1.36.8

**Base branch:** `main`

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

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-12-22 02:37:47 -07:00
Alexander Brown
6b62d9cff9 🤖 Agent file location rules (and CSS prohibition) (#7691)
Add rule for agents to use the gitignored temp directory for
plans/scripts
Add rule to avoid `!important`
Add more rules around temporary files

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7691-Agent-file-location-rules-and-CSS-prohibition-2d06d73d365081a6b3a2f619cd67cb91)
by [Unito](https://www.unito.io)

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-12-20 22:03:10 -08:00
Comfy Org PR Bot
916d90bb51 1.36.7 (#7689)
Patch version increment to 1.36.7

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7689-1-36-7-2d06d73d365081389182ca06b37add7a)
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-20 16:37:33 -08:00
AustinMroz
17be3b9770 Add support for NO_TITLE in vue, disabling border (#7589)
When `node.title_mode` is set to `TitleMode.NO_TITLE` the node header is
not displayed in vue mode.
| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/0e64c3df-8bcb-496f-a53c-618fdca79610"/>
| <img width="360" alt="after"
src="https://github.com/user-attachments/assets/34ea3a28-cc2e-4316-a154-40f54bdf8e60"
/>|

When a node has specified both `NO_TITLE` and a transparent background,
node borders are also disabled in vue mode.
| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/e52cf371-ba7e-401c-b9e5-b53607c26778"/>
| <img width="360" alt="after"
src="https://github.com/user-attachments/assets/979a4ba4-cf6d-49b3-ae97-6e1d62f487cc"
/>|

Known issues:
- `NODE_TITLE_HEIGHT` strikes again.
<img width="254" height="64" alt="image"
src="https://github.com/user-attachments/assets/526b1e2c-66dd-4c5d-9954-8c997a0ab5b8"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7589-Add-support-for-NO_TITLE-in-vue-disabling-border-2cc6d73d36508182834bc78ea8dffa27)
by [Unito](https://www.unito.io)
2025-12-20 14:32:07 -07:00
Hunter
e5edbf91eb feat: support effective_balance_micros for user balance display (#7658)
## Summary

Add support for the new `effective_balance_micros` field to show users
their effective balance accounting for pending charges.

## Changes

- **What**: Update balance display components to use
`effective_balance_micros` (with fallback to `amount_micros` for
backwards compatibility)
- **Types**: Add `pending_charges_micros` and `effective_balance_micros`
to `GetCustomerBalance` response type in registry-types

## Review Focus

- The fallback pattern ensures backwards compatibility if the API
doesn't return the new field
- The `effective_balance_micros` can be negative when pending charges
exceed the available balance

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7658-feat-support-effective_balance_micros-for-user-balance-display-2cf6d73d36508193a5a7e999f3185078)
by [Unito](https://www.unito.io)
2025-12-20 14:30:16 -07:00
AustinMroz
0977e6e751 Fix buttons displayed behind images in litegraph (#7627)
#7394 caused a regression with preview image buttons showing behind the
image in litegraph. This is fixed by also deferring button draws

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/4c3b02e0-4951-403d-98b8-b01a01512d21"/>
| <img width="360" alt="after"
src="https://github.com/user-attachments/assets/7a4fb8e4-8caa-47f3-939d-8d8ddc0e71b4"
/>|

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-12-20 14:14:58 -07:00
Terry Jia
3c4b99ed84 3dgs & ply support (#7602)
## Summary

integrated sparkjs https://sparkjs.dev/, built by [world labs
](https://www.worldlabs.ai/) to support 3dgs.

- Add 3D Gaussian Splatting (3DGS) support using @sparkjsdev/spark
library
- Add PLY file format support with multiple rendering engines
- Support new file formats: `.ply`, `.spz`, `.splat`, `.ksplat`
- Add PLY Engine setting with three options: `threejs` (mesh), `fastply`
(optimized ASCII point clouds), `sparkjs` (3DGS)
- Add `FastPLYLoader` for 4-5x faster ASCII PLY parsing
- Add `original(Advanced)` material mode for point cloud rendering with
THREE.Points

3dgs generated by https://marble.worldlabs.ai/

test ply file from:
1. made by https://github.com/PozzettiAndrea/ComfyUI-DepthAnythingV3
2. threejs offically repo

## Screenshots


https://github.com/user-attachments/assets/44e64d3e-b58d-4341-9a70-a9aa64801220



https://github.com/user-attachments/assets/76b0dfba-0c12-4f64-91cb-bfc5d672294d



https://github.com/user-attachments/assets/2a8bfe81-1fb2-44c4-8787-dff325369c61



https://github.com/user-attachments/assets/e4beecee-d7a2-40c9-97f7-79b09c60312d

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7602-3dgs-ply-support-2cd6d73d3650814098fcea86cfaf747d)
by [Unito](https://www.unito.io)
2025-12-20 14:04:16 -07:00
Alexander Brown
212d19e2fa Decrease timeout for chromium sharded tests (#7664)
## Summary

Reduce the timeout per shard from 6 hours to 1 hour.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7664-Decrease-timeout-for-chromium-sharded-tests-2cf6d73d36508164a60be3072daa9629)
by [Unito](https://www.unito.io)
2025-12-20 01:26:26 -08:00
Comfy Org PR Bot
4bce7fbcde [chore] Update Comfy Registry API types from comfy-api@8034f18 (#7659)
## Automated API Type Update

This PR updates the Comfy Registry API types from the latest comfy-api
OpenAPI specification.

- API commit: 8034f18
- Generated on: 2025-12-20T04:03:57Z

These types are automatically generated using openapi-typescript.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7659-chore-Update-Comfy-Registry-API-types-from-comfy-api-8034f18-2cf6d73d3650817ba6c8faaa538fbc4d)
by [Unito](https://www.unito.io)

Co-authored-by: huntcsg <6245448+huntcsg@users.noreply.github.com>
2025-12-19 21:11:36 -08:00
Benjamin Lu
f6bc10bb9d Sync node help with selection and add watcher tests (#7105)
## Summary
- add a watcher to sync the node help panel with the currently selected
node
- add unit coverage for help auto-switching and guard cases

## Testing
- pnpm typecheck
- pnpm lint:fix
- pnpm exec vitest
tests-ui/tests/composables/graph/useSelectionState.test.ts

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7105-Sync-node-help-with-selection-and-add-watcher-tests-2bd6d73d36508140b5acd3f3c65c5680)
by [Unito](https://www.unito.io)
2025-12-19 20:51:10 -08:00
AustinMroz
68ccd683ad Do not delay fit to view on graph restore (#7645)
Fixes a bug where swapping to a different workflow from the inside of a
subgraph would cause nodes to be in an incorrect position after swapping
back. in vue mode

Prior to an unknown-but-recent PR, all nodes would would stack on the
origin. This PR instead solves the remaining issue where having
`ComfyEnableWorkflowViewRestore` would cause incorrect node positions.

This is done by not delaying the fitView by a frame (which causes it to
occur after the graph is no longer in the configuring state). In order
to accomplish this, the code in LGraphNode has been updated to allow
measuring node bounds without requiring a ctx argument. This arg is only
used to ensure sufficient width for a node's title and is irrelevant
when loading an existing graph.

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/7f73817b-36e9-4400-8342-9e660cb36628"/>
| <img width="360" alt="after"
src="https://github.com/user-attachments/assets/c7ab4b99-2797-4276-9703-58d489cc3eaf"
/>|

See also #7591, which solves similar issues, but does not resolve this
bug.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7645-Do-not-delay-fit-to-view-on-graph-restore-2ce6d73d36508153972cc7b5948ce375)
by [Unito](https://www.unito.io)
2025-12-19 20:03:52 -08:00
Benjamin Lu
88df6627f0 docs: require design approval for notable UI changes (#7629)
Clarify design-team approval requirement for notable UI changes.

- Add a Design Team Approval section in CONTRIBUTING that states notable
UI changes must be requested by or approved by the design team and
applies to OSS contributors.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7629-docs-require-design-approval-for-notable-UI-changes-2ce6d73d36508151a894d683dda85c2e)
by [Unito](https://www.unito.io)
2025-12-19 19:10:26 -07:00
Simula_r
ccb73186fb refactor: start on removing FF for subscription tiers (#7596)
## Summary

Refactor: remove FF for subscription tier, remove legacy code for non
subscription tier logic.
 
## Review Focus

Preexisting cloud functionality impact.

<!-- 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-7596-refactor-start-on-removing-FF-for-subscription-tiers-2cc6d73d3650816bac3aef893e4f37cd)
by [Unito](https://www.unito.io)
2025-12-19 18:52:37 -07:00
Benjamin Lu
c0c284977d Prevent sidebar tool buttons from flashing during collapse (#7652)
Prevent sidebar tool buttons from flashing during collapse.

## What changed
- Clip the sidebar tool-buttons container during hover collapse so tab
labels don't render outside the header.
- Keep the existing width/opacity transition so the title still reclaims
space.

## Why
- Motivation: the hover-out transition shrinks the tool-buttons wrapper
to `w-0` while the tabs keep their intrinsic width, causing a brief
clipped flash on the right edge of the sidebar.
- Why this approach: clipping during the transition is the smallest
change that fixes the visual artifact without altering layout timing or
hover behavior.
- Tradeoffs / limitations: no functional change; the buttons are only
clipped while collapsing.

## Evidence
- Issues: n/a
- Tests: Not run (lint/typecheck only; not evidence)
- Screenshots/video:


https://github.com/user-attachments/assets/3af4d735-6330-4521-b4cf-45eb4b09f9ba

## References
- Related PRs: n/a
2025-12-19 16:55:03 -08:00
Kelly Yang
08faa69256 fix: 3d resize vertically (#7621)
## Summary

Fix an issue #7620 where 3D nodes could be resized vertically only in
the expanding direction but could not be shrunk.


## Changes
- **What**:  
- Allow the 3D node content container to shrink by removing `h-full` and
adding `min-h-0 flex-1`.
- Prevent the internal canvas from reasserting its previous height
during parent resize by avoiding `h-full` and constraining it with
`max-h-full`.
- **Dependencies**:  
  - None

## Review Focus

- Verify that vertical resize works in both directions for 3D nodes.
- Ensure the canvas still resizes correctly with the node and that
three.js rendering is unaffected.
- Confirm no regression in pointer events, overlays, or resize handles.

## Screenshots 
before


https://github.com/user-attachments/assets/6b9e3bb0-98eb-4b06-adb3-b3aab60a7f86


after


https://github.com/user-attachments/assets/76d1a962-6cdc-4ca4-941e-7a3874629b29

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7621-fix-3d-resize-vertically-2cd6d73d365081efb281e8a40c87bf9e)
by [Unito](https://www.unito.io)
2025-12-19 19:52:35 -05:00
Comfy Org PR Bot
08a3c767ac 1.36.6 (#7651)
Patch version increment to 1.36.6

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7651-1-36-6-2cf6d73d36508165b199d08f0d0a5379)
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-19 17:20:14 -07:00
Johnpaul Chiwetelu
552452622d fix: context menu not opening when right-clicking different Vue nodes (#7644)
## Summary
- Fixed issue where right-clicking on one Vue node then another would
close the context menu instead of repositioning it
- Added `showNodeOptions` function that always shows the menu at the new
position (used for contextmenu events)
- Kept `toggleNodeOptions` for the "More Options" button where toggle
behavior is expected

## Test plan
- [ ] Right-click on a Vue node to open context menu
- [ ] Right-click on a different Vue node - menu should immediately show
for the new node
- [ ] Click "More Options" button when menu is open - should close the
menu
- [ ] Click "More Options" button when menu is closed - should open the
menu



https://github.com/user-attachments/assets/bb31c2e4-12b4-4786-96ac-23b1e2b4daa0



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7644-fix-context-menu-not-opening-when-right-clicking-different-Vue-nodes-2ce6d73d36508145bc30fe6947b6808a)
by [Unito](https://www.unito.io)
2025-12-19 22:36:05 +01:00
Alexander Brown
01a7c6ee54 Test: Fix templates Spec, scroll card into view (#7643)
## Summary

- Scroll the target card into view before clicking
- Hopefully stabilize the locale check by enqueuing the check earlier in
the process

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7643-Test-Fix-templates-Spec-scroll-card-into-view-2ce6d73d36508175a009f81502f0fe16)
by [Unito](https://www.unito.io)
2025-12-19 13:12:37 -08:00
Terry Jia
ac1b551b76 eslint issue on windows (#7634)
## Summary
fix eslint issue on windows

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7634-eslint-issue-on-windows-2ce6d73d365081f785def469045f57bd)
by [Unito](https://www.unito.io)

---------

Co-authored-by: DrJKL <DrJKL0424@gmail.com>
2025-12-19 12:06:11 -08:00
Alexander Brown
6d57b4def5 Fix: Minimap rendering (#7639)
## Summary

Restores the refs, but in a way that vue-tsc understands.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7639-Fix-Minimap-rendering-2ce6d73d3650817eb323ce7e2022ab74)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com>
2025-12-19 11:52:13 -08:00
Alexander Piskun
744d37cc3c add pricing badge for Flux2Max node (#7641)
## Summary

Node uses the same pricing method as Flux2Pro, but at a doubled rate.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7641-add-pricing-badge-for-Flux2Max-node-2ce6d73d365081e785b3cf562f289470)
by [Unito](https://www.unito.io)
2025-12-19 19:32:42 +02:00
Terry Jia
ed7ec2af0f improve logic of 3d scene size (#7619)
## Summary

improve 3d scene size logic, for preview3d, it should not have target
size

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7619-improve-logic-of-3d-scene-size-2cd6d73d36508101b9f5dcef9bbe1314)
by [Unito](https://www.unito.io)
2025-12-19 05:56:19 -05:00
Johnpaul Chiwetelu
b598ce2c0f fix: node preview Vue mode sizing in manager panel (#7611)
## Summary
- Add `lg-node-preview` class to LGraphNodePreview for CSS targeting
- Override absolute positioning in NodesTabPanel to make Vue mode
previews fit within the container
- Apply zoom scaling (0.5) to fit node previews in the manager info
panel

## Test plan
- [ ] Open manager panel and select a node pack with nodes
- [ ] Verify node previews display correctly with Vue mode enabled
- [ ] Verify previews fit within the panel bounds without overflow

## Before


https://github.com/user-attachments/assets/8cd3a201-600d-4f31-9b79-4a480a07d998



## After



https://github.com/user-attachments/assets/b88ee7f2-5e6d-4913-b5a6-fa5fbe3b4dde





┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7611-fix-node-preview-Vue-mode-sizing-in-manager-panel-2cd6d73d36508141843cea046f104746)
by [Unito](https://www.unito.io)
2025-12-19 08:32:58 +01:00
AustinMroz
2724840fea Fix animated webp test and remove screenshot (#7632)
Remove screenshot expectation and restore test.

We care that it's displaying an image not what frame of the image is
being displayed.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7632-Fix-animated-webp-test-and-remove-screenshot-2ce6d73d365081a59d38dc9d82fa1a51)
by [Unito](https://www.unito.io)
2025-12-18 21:17:38 -08:00
Johnpaul Chiwetelu
7724558e7b fix(manager): refactor PackTryUpdateButton to use Button component (#7638)
## Summary
- Refactors `PackTryUpdateButton` to use standard `Button` component
instead of deprecated `IconTextButton`
- Fixes broken import in `InfoPanelMultiItem.vue` (IconTextButton no
longer exists)
- Follows same pattern as `PackUninstallButton` and `PackInstallButton`

## Test plan
- [ ] Verify "Try Update" button appears and functions correctly for
nightly packs in the manager info panel
- [ ] Verify multi-select update button works in InfoPanelMultiItem
- [ ] Verify DotSpinner shows during update operation

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7638-fix-manager-refactor-PackTryUpdateButton-to-use-Button-component-2ce6d73d3650816da271c2c0f442d59e)
by [Unito](https://www.unito.io)
2025-12-19 06:08:35 +01:00
Johnpaul Chiwetelu
14fdbdf793 fix: prevent custom context menu when editing text (#7633)
## Summary
- Stop contextmenu event propagation in EditableText component
- Allows browser's native context menu (copy/paste) when renaming nodes

## Test plan
- [ ] Double-click a node title to enter edit mode
- [ ] Select text and right-click
- [ ] Verify browser's native context menu appears (not the node context
menu)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7633-fix-prevent-custom-context-menu-when-editing-text-2ce6d73d365081e38461d080abe12b32)
by [Unito](https://www.unito.io)
2025-12-18 21:48:12 -07:00
Johnpaul Chiwetelu
8d37e48849 feat(manager): add Try Update button for nightly packs (#7610)
## Summary
- Add "Try Update" button in InfoPanel for installed nightly packs
- Allows users to pull latest changes from repository for nightly
versions
- Nightly updates cannot be auto-detected (git hashes vs semver), so
users trigger manually

## Test plan
- [x] Install a nightly version of any pack
- [x] Select the pack in Manager
- [x] Verify "Try Update" button appears in InfoPanel
- [x] Click "Try Update" and verify update request is sent




https://github.com/user-attachments/assets/87443eb2-adc6-4d1c-afc9-171d301cca92


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7610-feat-manager-add-Try-Update-button-for-nightly-packs-2cd6d73d36508167adcbc07c6a07938c)
by [Unito](https://www.unito.io)
2025-12-18 21:45:24 -07:00
Simula_r
0a7515b757 Fix(cloud)/subscription panel (#7628)
## Summary

Fix subscription panel to use new shared consts for pricing info and
misc plan related items.

## Changes

- **What**: SubscriptionPanel.vue, /en/main.json
- **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-7628-Fix-cloud-subscription-panel-2ce6d73d36508119846dd537b37a0d59)
by [Unito](https://www.unito.io)
2025-12-18 18:24:31 -08:00
Comfy Org PR Bot
9a35fa97a5 1.36.5 (#7626)
Patch version increment to 1.36.5

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7626-1-36-5-2ce6d73d36508117a1affc38a9293468)
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-18 18:14:37 -07:00
Benjamin Lu
f1b8c2246e Topbar: remove isDesktop gate for Custom Nodes Manager (#7606)
Removes the `isDesktop` (Electron) gate for the Custom Nodes Manager
topbar button so it shows whenever the manager is enabled.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7606-Topbar-remove-isDesktop-gate-for-Custom-Nodes-Manager-2cd6d73d36508153a804cc3603d8406c)
by [Unito](https://www.unito.io)
2025-12-18 17:08:54 -08:00
Alexander Brown
2c26fbb550 Component: Button Migration 3: IconTextButton (#7603)
## Summary

Replace all the `IconTextButton`s with `Button`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7603-WIP-Component-Button-Migraion-3-IconTextButton-2cd6d73d365081b7b742fa2172dc2ba8)
by [Unito](https://www.unito.io)
2025-12-18 16:09:56 -08:00
Alexander Brown
6244cf1008 Tests: Golden Updates (#7624)
## Summary

...

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7624-Tests-Golden-Updates-2cd6d73d3650812eb92ece7c78d29c1b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-12-18 15:01:01 -08:00
Alexander Brown
2044d1430c Deps: Update Playwright (#7623)
## Summary

Let's see if this lets it run.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7623-Deps-Update-Playwright-2cd6d73d36508155a156dec2ea7aaec1)
by [Unito](https://www.unito.io)
2025-12-18 14:29:28 -08:00
Comfy Org PR Bot
7e011da830 1.36.4 (#7599)
Patch version increment to 1.36.4

**Base branch:** `main`

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

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-12-18 14:14:08 -07:00
Christian Byrne
fa1719fece fix: pricing table links to wrong page in docs (p2) (#7434)
## Summary

Continuation of updating pricing table link (p1 was
https://github.com/Comfy-Org/ComfyUI_frontend/pull/7402). Create a
constant since it's now used in multiple locations.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7434-fix-pricing-table-links-to-wrong-page-in-docs-p2-2c86d73d36508199b320d49faa6a0c73)
by [Unito](https://www.unito.io)
2025-12-17 19:56:35 -07:00
Christian Byrne
6dd688e680 docs: update docs and workflows for change to 2-week release cycle (from 1-week) (#7564)
## Summary

Updates all documentation regarding the release cycle to reflect new
2-week cycles.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7564-docs-update-docs-and-workflows-for-change-to-2-week-release-cycle-from-1-week-2cb6d73d36508198a35ed9a3825704e5)
by [Unito](https://www.unito.io)
2025-12-17 19:53:14 -07:00
AustinMroz
97c7aef72d Update Search Box IO filters to support multitype (#7542)
It doesn't feel like this further hurts the lackluster responsiveness of
the searchbox, but second opinions would be appreciated.

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/fb4b81f7-6eac-45bd-9bc8-17aebf739f0c"/>|
<img width="360" alt="after"
src="https://github.com/user-attachments/assets/7844cab4-0f73-4a3f-beb0-850efc09497a"
/>|

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7542-Update-Search-Box-IO-filters-to-support-multitype-2cb6d73d365081ccbeabf1a891351996)
by [Unito](https://www.unito.io)
2025-12-17 19:52:14 -07:00
Rizumu Ayaka
80335ac936 fix: right side panel visual indicators don't update for color picker… (#7489)
**Important code was mistakenly omitted in #7137, which caused this bug.
(@DrJKL )**

The color picker and node state components in the right side properties
panel have a visual indicator bug. While the components function
correctly and successfully change the node values, the visual state
indicators fail to update to reflect the current selection.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7489-fix-right-side-panel-visual-indicators-don-t-update-for-color-picker-2ca6d73d365081489ccdcac69186892f)
by [Unito](https://www.unito.io)
2025-12-17 19:49:45 -07:00
AustinMroz
6396eb6fa3 Fix promoted assets not being assets in vue (#7576)
- Fixes asset widgets which have been promoted failing to display as
asset widgets and having red names in vue mode.
- Fixes promoted subgraph widgets failing to resolve inputSpec for use
in vue mode.

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/6c2d2763-6ac3-4769-82c5-b1ab1cc5e945"/>
| <img width="360" alt="after"
src="https://github.com/user-attachments/assets/742e218b-ec42-411a-b5a2-021820031e2a"
/>|

I'm not excited that this creates further bloat of SimplifiedWidget.

Known issue
- Similar to #7550, subgraph widgets will have an incorrect callback and
will fail to update value on a fresh reload. This can be "fixed" (made
worse) by entering and exiting the subgraph. Since this creates a
'leaked' widget callback which will then be called.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7576-Fix-promoted-assets-not-being-assets-in-vue-2cc6d73d3650814b8734f69b225b0228)
by [Unito](https://www.unito.io)
2025-12-17 19:48:32 -07:00
Alexander Brown
fba580dc7d Component: Button Migration 2: IconButton (#7598)
## Summary

Still a work in progress. Buttons with just icons are already in the
stories for button.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7598-WIP-Component-Button-Migration-2-IconButton-2cc6d73d365081c09143c63464ac60b7)
by [Unito](https://www.unito.io)
2025-12-17 18:11:43 -08:00
Christian Byrne
1d014c0dbe feat: when restored position has no nodes in viewport, automatically fit to view (#7435)
## Summary

Sometimes the saved position is super far away from any of the nodes,
which causes general confusion. This PR changes the `loadGraphData`
logic to fit-to-view in those scenarios.

Fixes https://github.com/Comfy-Org/ComfyUI_frontend/issues/7425

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7435-feat-when-restored-position-has-no-nodes-in-viewport-automatically-fit-to-view-2c86d73d36508119bf2ed9d361ec868f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-12-17 15:38:05 -08:00
Alexander Brown
db929220af Chore: Update several Develeoper Dependencies (#7590)
## Summary

Hopefully this will stabilize the precommit and prepush behavior for
developers using Windows and speed up the runtime of a few scripts for
everyone else.

Includes fixes for unused refs that were caught by the updated vue-tsc.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7590-Chore-Update-several-Develeoper-Dependencies-2cc6d73d365081fdb27cd00e53b169d5)
by [Unito](https://www.unito.io)
2025-12-17 15:24:07 -08:00
Simula_r
ed04215d40 feat: add pricing table to user popover (#7583)
## Summary

Add ability to show pricing table from user popover. Misc style changes
and logic.

## Changes

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

## Screenshots (if applicable)

<img width="481" height="672" alt="image"
src="https://github.com/user-attachments/assets/3dd55bc4-b49e-4b4e-920e-c97cabaa448a"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7583-feat-add-pricing-table-to-user-popover-2cc6d73d365081e1bbf1f2b351e53289)
by [Unito](https://www.unito.io)
2025-12-17 15:07:04 -07:00
Simula_r
a78e8c587f Refactor(cloud)/yearly credits monthly (#7584)
## Summary

Add yearly total credits vs monthly. Also pulled out numerical values
from the main.json to avoid translation issues and used n() for better
currency support on prices.

## Changes

- **What**: PricingTable.vue, /en/main.json
- **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)

<img width="2321" height="1538" alt="image"
src="https://github.com/user-attachments/assets/8c7b3eed-bfd8-4188-914f-3bfa5397a84f"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7584-Refactor-cloud-yearly-credits-monthly-2cc6d73d365081b28afbec7f9d22546f)
by [Unito](https://www.unito.io)
2025-12-17 15:06:14 -07:00
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
318 changed files with 12199 additions and 12266 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

@@ -45,6 +45,7 @@ jobs:
playwright-tests-chromium-sharded:
needs: setup
runs-on: ubuntu-latest
timeout-minutes: 60
permissions:
contents: read
strategy:

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

@@ -1,12 +1,12 @@
# Automated weekly workflow to bump ComfyUI frontend RC releases
name: "Release: Weekly ComfyUI"
# Automated bi-weekly workflow to bump ComfyUI frontend RC releases
name: "Release: Bi-weekly ComfyUI"
on:
# Schedule for Monday at 12:00 PM PST (20:00 UTC)
schedule:
- cron: '0 20 * * 1'
# Allow manual triggering
# Allow manual triggering (bypasses bi-weekly check)
workflow_dispatch:
inputs:
comfyui_fork:
@@ -16,7 +16,39 @@ on:
type: string
jobs:
check-release-week:
runs-on: ubuntu-latest
outputs:
is_release_week: ${{ steps.check.outputs.is_release_week }}
steps:
- name: Check if release week
id: check
run: |
# Anchor date: first bi-weekly release Monday
ANCHOR="2025-12-22"
ANCHOR_EPOCH=$(date -d "$ANCHOR" +%s)
NOW_EPOCH=$(date +%s)
WEEKS_SINCE=$(( (NOW_EPOCH - ANCHOR_EPOCH) / 604800 ))
if [ $((WEEKS_SINCE % 2)) -eq 0 ]; then
echo "Release week (week $WEEKS_SINCE since anchor $ANCHOR)"
echo "is_release_week=true" >> $GITHUB_OUTPUT
else
echo "Not a release week (week $WEEKS_SINCE since anchor $ANCHOR)"
echo "is_release_week=false" >> $GITHUB_OUTPUT
fi
- name: Summary
run: |
echo "## Bi-weekly Check" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Is release week: ${{ steps.check.outputs.is_release_week }}" >> $GITHUB_STEP_SUMMARY
echo "- Manual trigger: ${{ github.event_name == 'workflow_dispatch' }}" >> $GITHUB_STEP_SUMMARY
resolve-version:
needs: check-release-week
if: needs.check-release-week.outputs.is_release_week == 'true' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
outputs:
current_version: ${{ steps.resolve.outputs.current_version }}
@@ -131,8 +163,8 @@ jobs:
echo "- [View workflow runs](https://github.com/Comfy-Org/ComfyUI_frontend/actions/workflows/release-version-bump.yaml)" >> $GITHUB_STEP_SUMMARY
create-comfyui-pr:
needs: [resolve-version, trigger-release-if-needed]
if: always() && needs.resolve-version.result == 'success'
needs: [check-release-week, resolve-version, trigger-release-if-needed]
if: always() && needs.resolve-version.result == 'success' && (needs.check-release-week.outputs.is_release_week == 'true' || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
steps:
@@ -231,7 +263,7 @@ jobs:
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Create/update branch (reuse same branch name each week)
# Create/update branch (reuse same branch name each release cycle)
BRANCH="automation/comfyui-frontend-bump"
git checkout -B "$BRANCH"
git add requirements.txt
@@ -243,7 +275,7 @@ jobs:
exit 0
fi
# Force push to fork (overwrites previous week's branch)
# Force push to fork (overwrites previous release cycle's branch)
# Note: This intentionally destroys branch history to maintain a single PR
# Any review comments or manual commits will need to be re-applied
if ! git push -f origin "$BRANCH"; then

View File

@@ -10,7 +10,7 @@ module.exports = defineConfig({
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr', 'pt-BR'],
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream.
reference: `Special names to keep untranslated: flux, photomaker, clip, vae, cfg, stable audio, stable cascade, stable zero, controlnet, lora, HiDream, Civitai, Hugging Face.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.

1
.npmrc
View File

@@ -1,2 +1,3 @@
ignore-workspace-root-check=true
catalog-mode=prefer
public-hoist-pattern[]=@parcel/watcher

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.)
@@ -242,3 +266,16 @@ When referencing Comfy-Org repos:
- Always use `import { cn } from '@/utils/tailwindUtil'`
- e.g. `<div :class="cn('text-node-component-header-icon', hasError && 'text-danger')" />`
- Use `cn()` inline in the template when feasible instead of creating a `computed` to hold the value
- NEVER use `!important` or the `!` important prefix for tailwind classes
- Find existing `!important` classes that are interfering with the styling and propose corrections of those instead.
## Agent-only rules
Rules for agent-based coding tasks.
### Temporary Files
- Put planning documents under `/temp/plans/`
- Put scripts used under `/temp/scripts/`
- Put summaries of work performed under `/temp/summaries/`
- Put TODOs and status updates under `/temp/in_progress/`

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

@@ -46,7 +46,6 @@
# Mask Editor
/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp
/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp
/src/extensions/core/maskEditorOld.ts @trsommer @brucew4yn3rp
# 3D
/src/extensions/core/load3d.ts @jtydhr88

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,32 @@ 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`
## Design Team Approval (Required for Notable UI Changes)
Changes that materially affect the default UI must be approved or requested by our design team before they can be merged. This is generally a blocking requirement and applies to internal contributors and OSS contributors alike.
### 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 +224,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 +245,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

@@ -33,11 +33,11 @@
The project follows a structured release process for each minor version, consisting of three distinct phases:
1. **Development Phase** - 1 week
1. **Development Phase** - 2 weeks
- Active development of new features
- Code changes merged to the development branch
2. **Feature Freeze** - 1 week
2. **Feature Freeze** - 2 weeks
- No new features accepted
- Only bug fixes are cherry-picked to the release branch
- Testing and stabilization of the codebase
@@ -56,16 +56,16 @@ To use the latest nightly release, add the following command line argument to yo
```
## Overlapping Release Cycles
The development of successive minor versions overlaps. For example, while version 1.1 is in feature freeze, development for version 1.2 begins simultaneously.
The development of successive minor versions overlaps. For example, while version 1.1 is in feature freeze, development for version 1.2 begins simultaneously. Each feature has approximately 4 weeks from merge to ComfyUI stable release (2 weeks on main, 2 weeks frozen on RC).
### Example Release Cycle
| Week | Date Range | Version 1.1 | Version 1.2 | Version 1.3 | Patch Releases |
|------|------------|-------------|-------------|-------------|----------------|
| 1 | Mar 1-7 | Development | - | - | - |
| 2 | Mar 8-14 | Feature Freeze | Development | - | 1.1.0 through 1.1.6 (daily) |
| 3 | Mar 15-21 | Released | Feature Freeze | Development | 1.1.7 through 1.1.13 (daily)<br>1.2.0 through 1.2.6 (daily) |
| 4 | Mar 22-28 | - | Released | Feature Freeze | 1.2.7 through 1.2.13 (daily)<br>1.3.0 through 1.3.6 (daily) |
| 1-2 | Mar 1-14 | Development | - | - | - |
| 3-4 | Mar 15-28 | Feature Freeze | Development | - | 1.1.0 through 1.1.13 (daily) |
| 5-6 | Mar 29-Apr 11 | Released | Feature Freeze | Development | 1.1.14+ (daily)<br>1.2.0 through 1.2.13 (daily) |
| 7-8 | Apr 12-25 | - | Released | Feature Freeze | 1.2.14+ (daily)<br>1.3.0 through 1.3.13 (daily) |
## Release Summary

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,412 @@
{
"id": "2ba0b800-2f13-4f21-b8d6-c6cdb0152cae",
"revision": 0,
"last_node_id": 16,
"last_link_id": 9,
"nodes": [
{
"id": 4,
"type": "CheckpointLoaderSimple",
"pos": [
60,
200
],
"size": [
315,
98
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"slot_index": 0,
"links": [
1
]
},
{
"name": "CLIP",
"type": "CLIP",
"slot_index": 1,
"links": [
3,
5
]
},
{
"name": "VAE",
"type": "VAE",
"slot_index": 2,
"links": [
8
]
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple",
"cnr_id": "comfy-core",
"ver": "0.3.65",
"models": [
{
"name": "v1-5-pruned-emaonly-fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly-fp16.safetensors?download=true",
"directory": "checkpoints"
}
]
},
"widgets_values": [
"v1-5-pruned-emaonly-fp16.safetensors"
]
},
{
"id": 3,
"type": "KSampler",
"pos": [
870,
170
],
"size": [
315,
474
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": 1
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 4
},
{
"name": "negative",
"type": "CONDITIONING",
"link": 6
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
7
]
}
],
"properties": {
"Node name for S&R": "KSampler",
"cnr_id": "comfy-core",
"ver": "0.3.65"
},
"widgets_values": [
685468484323813,
"randomize",
20,
8,
"euler",
"normal",
1
]
},
{
"id": 8,
"type": "VAEDecode",
"pos": [
975,
700
],
"size": [
210,
46
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "samples",
"type": "LATENT",
"link": 7
},
{
"name": "vae",
"type": "VAE",
"link": 8
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": []
}
],
"properties": {
"Node name for S&R": "VAEDecode",
"cnr_id": "comfy-core",
"ver": "0.3.65"
},
"widgets_values": []
},
{
"id": 7,
"type": "CLIPTextEncode",
"pos": [
410,
410
],
"size": [
425.27801513671875,
180.6060791015625
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 5
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
6
]
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode",
"cnr_id": "comfy-core",
"ver": "0.3.65"
},
"widgets_values": [
"text, watermark"
],
"color": "#223",
"bgcolor": "#335"
},
{
"id": 5,
"type": "EmptyLatentImage",
"pos": [
520,
690
],
"size": [
315,
106
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"slot_index": 0,
"links": [
2
]
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage",
"cnr_id": "comfy-core",
"ver": "0.3.65"
},
"widgets_values": [
512,
512,
1
]
},
{
"id": 6,
"type": "CLIPTextEncode",
"pos": [
411.21649169921875,
203.68695068359375
],
"size": [
422.84503173828125,
164.31304931640625
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "clip",
"type": "CLIP",
"link": 3
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"slot_index": 0,
"links": [
4
]
}
],
"properties": {
"Node name for S&R": "CLIPTextEncode",
"cnr_id": "comfy-core",
"ver": "0.3.65"
},
"widgets_values": [
"beautiful scenery nature glass bottle landscape, purple galaxy bottle,"
],
"color": "#232",
"bgcolor": "#353"
}
],
"links": [
[
1,
4,
0,
3,
0,
"MODEL"
],
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
]
],
"groups": [
{
"id": 1,
"title": "Step 1 - Load model",
"bounding": [
50,
130,
335,
181.60000610351562
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
},
{
"id": 2,
"title": "Step 3 - Image size",
"bounding": [
510,
620,
335,
189.60000610351562
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
},
{
"id": 3,
"title": "Step 2 - Prompt",
"bounding": [
400,
130,
445.27801513671875,
467.2060852050781
],
"color": "#3f789e",
"font_size": 24,
"flags": {}
}
],
"config": {},
"extra": {
"ds": {
"scale": 0.44218252181616574,
"offset": [
-666.5670907104311,
-2227.894644048147
]
},
"frontendVersion": "1.35.3",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true,
"workflowRendererVersion": "LG"
},
"version": 0.4
}

View File

@@ -1653,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

@@ -26,10 +26,9 @@ export class ComfyTemplates {
}
async loadTemplate(id: string) {
await this.content
.getByTestId(`template-workflow-${id}`)
.getByRole('img')
.click()
const templateCard = this.content.getByTestId(`template-workflow-${id}`)
await templateCard.scrollIntoViewIfNeeded()
await templateCard.getByRole('img').click()
}
async getAllTemplates(): Promise<TemplateInfo[]> {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 21 KiB

View File

@@ -553,12 +553,6 @@ This is English documentation.
)
await selectNodeWithPan(comfyPage, checkpointNodes[0])
// Click help button again
const helpButton2 = comfyPage.page.locator(
'.selection-toolbox button[data-testid="info-button"]'
)
await helpButton2.click()
// Content should update
await expect(helpPage).toContainText('Checkpoint Loader Help')
await expect(helpPage).toContainText(

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 104 KiB

After

Width:  |  Height:  |  Size: 97 KiB

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

@@ -109,14 +109,14 @@ test.describe('Templates', () => {
})
test('Uses proper locale files for templates', async ({ comfyPage }) => {
// Set locale to French before opening templates
await comfyPage.setSetting('Comfy.Locale', 'fr')
// Load the templates dialog and wait for the French index file request
const requestPromise = comfyPage.page.waitForRequest(
'**/templates/index.fr.json'
)
// Set locale to French before opening templates
await comfyPage.setSetting('Comfy.Locale', 'fr')
await comfyPage.executeCommand('Comfy.BrowseTemplates')
const request = await requestPromise

View File

@@ -0,0 +1,20 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../fixtures/ComfyPage'
test.describe('Viewport', () => {
test('Fits view to nodes when saved viewport position is offscreen', async ({
comfyPage
}) => {
await comfyPage.loadWorkflow('viewport/default-viewport-saved-offscreen')
// Wait a few frames for rendering to stabilize
for (let i = 0; i < 5; i++) {
await comfyPage.nextFrame()
}
await expect(comfyPage.canvas).toHaveScreenshot(
'viewport-fits-when-saved-offscreen.png'
)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

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: 118 KiB

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 27 KiB

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 101 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 84 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

@@ -205,6 +205,32 @@ test.describe('Image widget', () => {
const filename = await fileComboWidget.getValue()
expect(filename).toBe('image32x32.webp')
})
test('Displays buttons when viewing single image of batch', async ({
comfyPage
}) => {
const [x, y] = await comfyPage.page.evaluate(() => {
const src =
"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='768' height='512' viewBox='0 0 1 1'%3E%3Crect width='1' height='1' stroke='black'/%3E%3C/svg%3E"
const image1 = new Image()
image1.src = src
const image2 = new Image()
image2.src = src
const targetNode = graph.nodes[6]
targetNode.imgs = [image1, image2]
targetNode.imageIndex = 1
app.canvas.setDirty(true)
const x = targetNode.pos[0] + targetNode.size[0] - 41
const y = targetNode.pos[1] + targetNode.widgets.at(-1).last_y + 30
return app.canvasPosToClientPos([x, y])
})
const clip = { x, y, width: 35, height: 35 }
await expect(comfyPage.page).toHaveScreenshot(
'image_preview_close_button.png',
{ clip }
)
})
})
test.describe('Animated image widget', () => {
@@ -262,13 +288,7 @@ test.describe('Animated image widget', () => {
expect(filename).toContain('animated_webp.webp')
})
// FIXME: This test keeps flip-flopping because it relies on animated webp timing,
// which is inherently unreliable in CI environments. The test asset is an animated
// webp with 2 frames, and the test depends on animation frame timing to verify that
// animated webp images are properly displayed (as opposed to being treated as static webp).
// While the underlying functionality works (animated webp are correctly distinguished
// from static webp), the test is flaky due to timing dependencies with webp animation frames.
test.fixme('Can preview saved animated webp image', async ({ comfyPage }) => {
test('Can preview saved animated webp image', async ({ comfyPage }) => {
await comfyPage.loadWorkflow('widgets/save_animated_webp')
// Get position of the load animated webp node
@@ -295,18 +315,13 @@ test.describe('Animated image widget', () => {
([loadId, saveId]) => {
// Set the output of the SaveAnimatedWEBP node to equal the loader node's image
window['app'].nodeOutputs[saveId] = window['app'].nodeOutputs[loadId]
app.canvas.setDirty(true)
},
[loadAnimatedWebpNode.id, saveAnimatedWebpNode.id]
)
await comfyPage.nextFrame()
// Move mouse and click on canvas to trigger render
await comfyPage.page.mouse.click(64, 64)
// Expect the SaveAnimatedWEBP node to have an output preview
await expect(comfyPage.canvas).toHaveScreenshot(
'animated_image_preview_saved_webp.png'
)
await expect(
comfyPage.page.locator('.dom-widget').locator('img')
).toHaveCount(2)
})
})

Binary file not shown.

After

Width:  |  Height:  |  Size: 423 B

View File

@@ -9,11 +9,7 @@ interface ShimResult {
const SKIP_WARNING_FILES = new Set(['scripts/app', 'scripts/api'])
/** Files that will be removed in v1.34 */
const DEPRECATED_FILES = [
'scripts/ui',
'extensions/core/maskEditorOld',
'extensions/core/groupNode'
] as const
const DEPRECATED_FILES = ['scripts/ui', 'extensions/core/groupNode'] as const
function getWarningMessage(
fileKey: string,

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

@@ -41,7 +41,6 @@ The following table lists ALL core extensions in the system as of 2025-01-30:
| groupOptions.ts | Handles group node configuration options | Graph |
| index.ts | Main extension registration and coordination | Core |
| load3d.ts | Supports 3D model loading and visualization | 3D |
| maskEditorOld.ts | Legacy mask editor implementation | Image |
| maskeditor.ts | Implements the mask editor for image masking operations | Image |
| nodeTemplates.ts | Provides node template functionality | Templates |
| noteNode.ts | Adds note nodes for documentation within workflows | Graph |
@@ -178,4 +177,4 @@ For more detailed information about ComfyUI's extension system, refer to the off
- [JavaScript Settings](https://docs.comfy.org/custom-nodes/js/javascript_settings)
- [JavaScript Examples](https://docs.comfy.org/custom-nodes/js/javascript_examples)
Also, check the main [README.md](https://github.com/Comfy-Org/ComfyUI_frontend#developer-apis) section on Developer APIs for the latest information on extension APIs and features.
Also, check the main [README.md](https://github.com/Comfy-Org/ComfyUI_frontend#developer-apis) section on Developer APIs for the latest information on extension APIs and features.

View File

@@ -4,8 +4,11 @@ import pluginI18n from '@intlify/eslint-plugin-vue-i18n'
import { createTypeScriptImportResolver } from 'eslint-import-resolver-typescript'
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'
// WORKAROUND: eslint-plugin-prettier causes segfault on Node.js 24 + Windows
// See: https://github.com/nodejs/node/issues/58690
// Prettier is still run separately in lint-staged, so this is safe to disable
import eslintConfigPrettier from 'eslint-config-prettier'
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'
@@ -108,8 +111,10 @@ export default defineConfig([
tseslintConfigs.recommended,
// Difference in typecheck on CI vs Local
pluginVue.configs['flat/recommended'],
eslintPluginPrettierRecommended,
storybook.configs['flat/recommended'],
// Use eslint-config-prettier instead of eslint-plugin-prettier to avoid Node 24 segfault
eslintConfigPrettier,
// @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.7",
"version": "1.36.9",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -19,6 +19,7 @@
"dev:cloud": "cross-env DEV_SERVER_COMFYUI_URL='https://testcloud.comfy.org/' nx serve",
"dev:desktop": "nx dev @comfyorg/desktop-ui",
"dev:electron": "nx serve --config vite.electron.config.mts",
"dev:no-vue": "cross-env DISABLE_VUE_PLUGINS=true nx serve",
"dev": "nx serve",
"devtools:pycheck": "python3 -m compileall -q tools/devtools",
"format:check:no-cache": "prettier --check './**/*.{js,ts,tsx,vue,mts}'",
@@ -40,7 +41,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",
@@ -84,7 +85,6 @@
"eslint-import-resolver-typescript": "catalog:",
"eslint-plugin-import-x": "catalog:",
"eslint-plugin-oxlint": "catalog:",
"eslint-plugin-prettier": "catalog:",
"eslint-plugin-storybook": "catalog:",
"eslint-plugin-unused-imports": "catalog:",
"eslint-plugin-vue": "catalog:",
@@ -146,6 +146,7 @@
"@primevue/icons": "catalog:",
"@primevue/themes": "catalog:",
"@sentry/vue": "catalog:",
"@sparkjsdev/spark": "catalog:",
"@tiptap/core": "^2.10.4",
"@tiptap/extension-link": "^2.10.4",
"@tiptap/extension-table": "^2.10.4",
@@ -161,6 +162,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 +177,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

@@ -235,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);

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

View File

@@ -217,6 +217,28 @@ export interface paths {
patch?: never;
trace?: never;
};
"/admin/verify-api-key": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Verify a ComfyUI API key and return customer details
* @description Validates a ComfyUI API key and returns the associated customer information.
* This endpoint is used by cloud.comfy.org to authenticate users via API keys
* instead of Firebase tokens.
*/
post: operations["VerifyApiKey"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/admin/customers/{customer_id}/cloud-subscription-status": {
parameters: {
query?: never;
@@ -2154,6 +2176,26 @@ export interface paths {
patch?: never;
trace?: never;
};
"/proxy/bfl/flux-2-max/generate": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
/**
* Proxy request to BFL Flux 2 Max for image generation
* @description Forwards image generation requests to BFL's Flux 2 Max API and returns the results. Supports image-to-image generation with up to 8 input images.
*/
post: operations["bflFlux2MaxGenerate"];
delete?: never;
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/proxy/bfl/flux-pro-1.0-expand/generate": {
parameters: {
query?: never;
@@ -3911,6 +3953,11 @@ export interface components {
* @enum {string}
*/
SubscriptionTier: "STANDARD" | "CREATOR" | "PRO" | "FOUNDERS_EDITION";
/**
* @description The subscription billing duration
* @enum {string}
*/
SubscriptionDuration: "MONTHLY" | "ANNUAL";
FeaturesResponse: {
/**
* @description The conversion rate for partner nodes
@@ -4757,13 +4804,13 @@ export interface components {
* @default kling-v1
* @enum {string}
*/
KlingTextToVideoModelName: "kling-v1" | "kling-v1-5" | "kling-v1-6" | "kling-v2-master" | "kling-v2-1-master" | "kling-v2-5-turbo";
KlingTextToVideoModelName: "kling-v1" | "kling-v1-5" | "kling-v1-6" | "kling-v2-master" | "kling-v2-1-master" | "kling-v2-5-turbo" | "kling-v2-6";
/**
* @description Model Name
* @default kling-v2-master
* @enum {string}
*/
KlingVideoGenModelName: "kling-v1" | "kling-v1-5" | "kling-v1-6" | "kling-v2-master" | "kling-v2-1" | "kling-v2-1-master" | "kling-v2-5-turbo";
KlingVideoGenModelName: "kling-v1" | "kling-v1-5" | "kling-v1-6" | "kling-v2-master" | "kling-v2-1" | "kling-v2-1-master" | "kling-v2-5-turbo" | "kling-v2-6";
/**
* @description Video generation mode. std: Standard Mode, which is cost-effective. pro: Professional Mode, generates videos with longer duration but higher quality output.
* @default std
@@ -4908,6 +4955,12 @@ export interface components {
camera_control?: components["schemas"]["KlingCameraControl"];
aspect_ratio?: components["schemas"]["KlingVideoGenAspectRatio"];
duration?: components["schemas"]["KlingVideoGenDuration"];
/**
* @description Whether to generate sound simultaneously when generating videos. Only V2.6 and subsequent versions of the model support this parameter.
* @default off
* @enum {string}
*/
sound: "on" | "off";
/**
* Format: uri
* @description The callback notification address
@@ -4970,6 +5023,12 @@ export interface components {
camera_control?: components["schemas"]["KlingCameraControl"];
aspect_ratio?: components["schemas"]["KlingVideoGenAspectRatio"];
duration?: components["schemas"]["KlingVideoGenDuration"];
/**
* @description Whether to generate sound simultaneously when generating videos. Only V2.6 and subsequent versions of the model support this parameter.
* @default off
* @enum {string}
*/
sound: "on" | "off";
/**
* Format: uri
* @description The callback notification address. Server will notify when the task status changes.
@@ -5759,7 +5818,7 @@ export interface components {
width: number;
/**
* @description Height of the image.
* @default 768
* @default 1024
*/
height: number;
/** @description Seed for reproducibility. */
@@ -5775,6 +5834,11 @@ export interface components {
* @enum {string}
*/
output_format: "jpeg" | "png";
/**
* @description Moderation tolerance level (Flux 2 Max only).
* @default 2
*/
safety_tolerance: number;
};
/** FluxProFillInputs */
BFLFluxProFillInputs: {
@@ -6973,6 +7037,10 @@ export interface components {
image_tokens?: number;
};
output_tokens?: number;
output_tokens_details?: {
text_tokens?: number;
image_tokens?: number;
};
total_tokens?: number;
};
};
@@ -10356,40 +10424,76 @@ export interface components {
* @description The ID of the model to call
* @enum {string}
*/
model: "wan2.5-t2v-preview" | "wan2.5-i2v-preview";
model: "wan2.5-t2v-preview" | "wan2.5-i2v-preview" | "wan2.6-t2v" | "wan2.6-i2v";
/** @description Enter basic information, such as prompt words, etc. */
input: {
/** @description Text prompt words. Support Chinese and English, length not exceeding 800 characters */
/**
* @description Text prompt words. Support Chinese and English, length not exceeding 800 characters.
* For wan2.6-r2v with multiple reference videos, use 'character1', 'character2', etc. to refer to subjects
* in the order of reference videos. Example: "Character1 sings on the roadside, Character2 dances beside it"
*/
prompt: string;
/** @description Reverse prompt words are used to describe content that you do not want to see in the video screen */
negative_prompt?: string;
/** @description Audio file download URL. Supported formats: mp3 and wav. */
/** @description Audio file download URL. Supported formats: mp3 and wav. Cannot be used with reference_video_urls. */
audio_url?: string;
/** @description First frame image URL or Base64 encoded data. Required for I2V models. Image formats: JPEG, JPG, PNG, BMP, WEBP. Resolution: 360-2000 pixels. File size: max 10MB. */
img_url?: string;
/** @description Video effect template name. Optional. Currently supported: squish, flying, carousel. When used, prompt parameter is ignored. */
template?: string;
/**
* @description Reference video URLs for wan2.6-r2v model only. Array of 1-3 video URLs.
* Input restrictions:
* - Format: mp4, mov
* - Quantity: 1-3 videos
* - Single video length: 2-30 seconds
* - Single file size: max 30MB
* - Cannot be used with audio_url
* Reference duration: Single video max 5s, two videos max 2.5s each, three videos proportionally less.
* Billing: Based on actual reference duration used.
*/
reference_video_urls?: string[];
};
/** @description Video processing parameters */
parameters?: {
/** @description Used to specify the video resolution in the format of 宽*高. Supported resolutions vary by model (for T2V models) */
/**
* @description Video resolution in format width*height. Supported resolutions vary by model:
* For wan2.5 T2V: 480P (480*832, 832*480, 624*624), 720P, 1080P sizes
* For wan2.6 T2V/R2V (no 480P):
* 720P: 1280*720, 720*1280, 960*960, 1088*832, 832*1088
* 1080P: 1920*1080, 1080*1920, 1440*1440, 1632*1248, 1248*1632
*/
size?: string;
/**
* @description Resolution level for I2V models. Supported values vary by model: 480P, 720P, 1080P
* @description Resolution level for I2V models. Supported values vary by model:
* - wan2.5-i2v-preview: 480P, 720P, 1080P
* - wan2.6-i2v: 720P, 1080P only (no 480P support)
* @enum {string}
*/
resolution?: "480P" | "720P" | "1080P";
/**
* @description The duration of the video generated, in seconds
* @description The duration of the video generated, in seconds:
* - wan2.5 models: 5 or 10 seconds
* - wan2.6-t2v, wan2.6-i2v: 5, 10, or 15 seconds
* - wan2.6-r2v: 5 or 10 seconds only (no 15s support)
* @default 5
* @enum {integer}
*/
duration?: 5 | 10;
duration?: 5 | 10 | 15;
/**
* @description Is it enabled prompt intelligent rewriting. Default is true
* @default true
*/
prompt_extend?: boolean;
/**
* @description Intelligent multi-lens control. Only active when prompt_extend is enabled.
* For wan2.6 models only.
* - multi: Intelligent disassembly into multiple lenses (default)
* - single: Single lens generation
* @default multi
* @enum {string}
*/
shot_type?: "multi" | "single";
/** @description Random number seed, used to control the randomness of the model generated content */
seed?: number;
/**
@@ -11806,6 +11910,8 @@ export interface operations {
"application/json": {
/** @description Optional URL to redirect the customer after they're done with the billing portal */
return_url?: string;
/** @description Optional target subscription tier. When provided, creates a deep link directly to the subscription update confirmation screen with this tier pre-selected. */
target_tier?: "standard" | "creator" | "pro" | "standard-yearly" | "creator-yearly" | "pro-yearly";
};
};
};
@@ -11902,8 +12008,8 @@ export interface operations {
query?: never;
header?: never;
path: {
/** @description The subscription tier (standard, creator, or pro) */
tier: "standard" | "creator" | "pro";
/** @description The subscription tier (standard, creator, or pro) with optional yearly billing (standard-yearly, creator-yearly, pro-yearly) */
tier: "standard" | "creator" | "pro" | "standard-yearly" | "creator-yearly" | "pro-yearly";
};
cookie?: never;
};
@@ -11969,6 +12075,7 @@ export interface operations {
/** @description The active subscription ID if one exists */
subscription_id?: string | null;
subscription_tier?: components["schemas"]["SubscriptionTier"] | null;
subscription_duration?: components["schemas"]["SubscriptionDuration"] | null;
/** @description Whether the customer has funds/credits available */
has_fund?: boolean;
/**
@@ -12002,6 +12109,72 @@ export interface operations {
};
};
};
VerifyApiKey: {
parameters: {
query?: never;
header: {
/** @description Admin API secret used to authorize this request */
"X-Comfy-Admin-Secret": string;
};
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": {
/** @description The ComfyUI API key to verify (e.g., comfy_xxx...) */
api_key: string;
};
};
};
responses: {
/** @description API key is valid */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": {
/** @description Whether the API key is valid */
valid: boolean;
/** @description The Firebase UID of the user */
firebase_uid: string;
/** @description The customer's email address */
email?: string;
/** @description The customer's name */
name?: string;
/** @description Whether the customer is an admin */
is_admin?: boolean;
};
};
};
/** @description Unauthorized or missing admin API secret */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description API key not found or invalid */
404: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
/** @description Internal server error */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
};
};
GetAdminCustomerCloudSubscriptionStatus: {
parameters: {
query?: never;
@@ -12029,6 +12202,7 @@ export interface operations {
/** @description The active subscription ID if one exists */
subscription_id?: string | null;
subscription_tier?: components["schemas"]["SubscriptionTier"] | null;
subscription_duration?: components["schemas"]["SubscriptionDuration"] | null;
/** @description Whether the customer has funds/credits available */
has_fund?: boolean;
/**
@@ -12146,6 +12320,16 @@ export interface operations {
* @description The remaining balance from cloud credits in microamount
*/
cloud_credit_balance_micros?: number;
/**
* Format: double
* @description The total amount of pending/unbilled charges from draft invoices in microamount. Only included when the show_negative_balances feature flag is enabled.
*/
pending_charges_micros?: number;
/**
* Format: double
* @description The effective balance (total balance minus pending charges). Can be negative if pending charges exceed the balance. Only included when the show_negative_balances feature flag is enabled.
*/
effective_balance_micros?: number;
/** @description The currency code (e.g., "usd") */
currency: string;
};
@@ -12212,6 +12396,16 @@ export interface operations {
* @description The remaining balance from cloud credits in microamount
*/
cloud_credit_balance_micros?: number;
/**
* Format: double
* @description The total amount of pending/unbilled charges from draft invoices in microamount. Only included when the show_negative_balances feature flag is enabled.
*/
pending_charges_micros?: number;
/**
* Format: double
* @description The effective balance (total balance minus pending charges). Can be negative if pending charges exceed the balance. Only included when the show_negative_balances feature flag is enabled.
*/
effective_balance_micros?: number;
/** @description The currency code (e.g., "usd") */
currency: string;
};
@@ -19417,6 +19611,89 @@ export interface operations {
};
};
};
bflFlux2MaxGenerate: {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
requestBody: {
content: {
"application/json": components["schemas"]["BFLFlux2ProGenerateRequest"];
};
};
responses: {
/** @description Successful response from BFL Flux 2 Max proxy */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["BFLFluxProGenerateResponse"];
};
};
/** @description Bad Request (invalid input to proxy) */
400: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
/** @description Unauthorized */
401: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Payment Required */
402: {
headers: {
[name: string]: unknown;
};
content?: never;
};
/** @description Rate limit exceeded (either from proxy or BFL) */
429: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
/** @description Internal Server Error (proxy or upstream issue) */
500: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
/** @description Bad Gateway (error communicating with BFL) */
502: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
/** @description Gateway Timeout (BFL took too long to respond) */
504: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["ErrorResponse"];
};
};
};
};
BFLExpand_v1_flux_pro_1_0_expand_post: {
parameters: {
query?: never;

View File

@@ -1,4 +1,5 @@
import clsx, { type ClassArray } from 'clsx'
import { clsx } from 'clsx'
import type { ClassArray } from 'clsx'
import { twMerge } from 'tailwind-merge'
export type { ClassValue } from 'clsx'

5159
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,12 +11,12 @@ 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
'@playwright/test': ^1.57.0
'@prettier/plugin-oxc': ^0.1.3
'@primeuix/forms': 0.0.2
'@primeuix/styled': 0.3.2
@@ -27,19 +27,20 @@ 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
'@sparkjsdev/spark': ^0.1.10
'@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': ^24.1.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,29 +48,29 @@ 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
eslint-import-resolver-typescript: ^4.4.4
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
globals: ^15.9.0
happy-dom: ^15.11.0
husky: ^9.0.11
jiti: 2.4.2
husky: ^9.1.7
jiti: 2.6.1
jsdom: ^26.1.0
knip: ^5.62.0
lint-staged: ^15.5.2
knip: ^5.75.1
lint-staged: ^16.2.7
markdown-table: ^3.0.4
mixpanel-browser: ^2.71.0
nx: 21.4.1
oxlint: ^1.32.0
oxlint-tsgolint: ^0.8.4
nx: 22.2.6
oxlint: ^1.33.0
oxlint-tsgolint: ^0.9.1
picocolors: ^1.1.1
pinia: ^2.1.7
postcss-html: ^1.8.0
@@ -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
@@ -87,13 +89,13 @@ catalog:
typegpu: ^0.8.2
typescript: ^5.9.3
typescript-eslint: ^8.49.0
unplugin-icons: ^0.22.0
unplugin-icons: ^22.5.0
unplugin-typegpu: 0.8.0
unplugin-vue-components: ^0.28.0
vite: ^5.4.19
unplugin-vue-components: ^30.0.0
vite: ^7.3.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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -10,48 +10,63 @@
</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"
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"
<Button
v-tooltip.bottom="customNodesManagerTooltipConfig"
variant="secondary"
size="icon"
: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" />
</Button>
</div>
<div
class="actionbar-container pointer-events-auto flex gap-2 h-12 items-center rounded-lg border border-interface-stroke bg-comfy-menu-bg px-2 shadow-interface"
>
<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 />
<Button
v-tooltip.bottom="queueHistoryTooltipConfig"
type="destructive"
size="icon"
: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>
</Button>
<CurrentUserButton v-if="isLoggedIn" class="shrink-0" />
<LoginButton v-else-if="isDesktop" />
<Button
v-if="!isRightSidePanelOpen"
v-tooltip.bottom="rightSidePanelTooltipConfig"
type="secondary"
size="icon"
:aria-label="t('rightSidePanel.togglePanel')"
@click="rightSidePanelStore.togglePanel"
>
<i class="icon-[lucide--panel-right] size-4" />
</Button>
</div>
</div>
<QueueProgressOverlay
v-model:expanded="isQueueOverlayExpanded"
@@ -68,24 +83,29 @@ import { useI18n } from 'vue-i18n'
import ComfyActionbar from '@/components/actionbar/ComfyActionbar.vue'
import SubgraphBreadcrumb from '@/components/breadcrumb/SubgraphBreadcrumb.vue'
import IconButton from '@/components/button/IconButton.vue'
import QueueProgressOverlay from '@/components/queue/QueueProgressOverlay.vue'
import ActionBarButtons from '@/components/topbar/ActionBarButtons.vue'
import CurrentUserButton from '@/components/topbar/CurrentUserButton.vue'
import LoginButton from '@/components/topbar/LoginButton.vue'
import Button from '@/components/ui/button/Button.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 +113,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 +135,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

@@ -18,12 +18,12 @@
content: { class: isDocked ? 'p-0' : 'p-1' }
}"
>
<div ref="panelRef" class="flex items-center select-none">
<div ref="panelRef" class="flex items-center select-none gap-2">
<span
ref="dragHandleRef"
:class="
cn(
'drag-handle cursor-grab w-3 h-max mr-2',
'drag-handle cursor-grab w-3 h-max',
isDragging && 'cursor-grabbing'
)
"
@@ -31,17 +31,16 @@
<Suspense @resolve="comfyRunButtonResolved">
<ComfyRunButton />
</Suspense>
<IconButton
<Button
v-tooltip.bottom="cancelJobTooltipConfig"
type="transparent"
size="sm"
class="ml-2 bg-destructive-background text-base-foreground transition-colors duration-200 ease-in-out hover:bg-destructive-background-hover focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-destructive-background"
variant="destructive"
size="icon"
:disabled="isExecutionIdle"
:aria-label="t('menu.interrupt')"
@click="cancelCurrentJob"
>
<i class="icon-[lucide--x] size-4" />
</IconButton>
</Button>
</div>
</Panel>
</div>
@@ -58,10 +57,10 @@ import { clamp } from 'es-toolkit/compat'
import { storeToRefs } from 'pinia'
import Panel from 'primevue/panel'
import { computed, nextTick, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { buildTooltipConfig } from '@/composables/useTooltipConfig'
import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useTelemetry } from '@/platform/telemetry'
import { useCommandStore } from '@/stores/commandStore'
@@ -72,6 +71,7 @@ import ComfyRunButton from './ComfyRunButton'
const settingsStore = useSettingStore()
const commandStore = useCommandStore()
const { t } = useI18n()
const { isIdle: isExecutionIdle } = storeToRefs(useExecutionStore())
const position = computed(() => settingsStore.get('Comfy.UseNewMenu'))
@@ -301,7 +301,7 @@ const panelClass = computed(() =>
'actionbar pointer-events-auto z-1300',
isDragging.value && 'select-none pointer-events-none',
isDocked.value
? 'p-0 static mr-2 border-none bg-transparent'
? 'p-0 static border-none bg-transparent'
: 'fixed shadow-interface'
)
)

View File

@@ -1,152 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import IconButton from './IconButton.vue'
const meta: Meta<typeof IconButton> = {
title: 'Components/Button/IconButton',
component: IconButton,
tags: ['autodocs'],
argTypes: {
size: {
control: { type: 'select' },
options: ['sm', 'md']
},
type: {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent']
},
border: {
control: 'boolean',
description: 'Toggle border attribute'
},
disabled: {
control: 'boolean',
description: 'Toggle disable status'
},
onClick: { action: 'clicked' }
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
render: (args) => ({
components: { IconButton },
setup() {
return { args }
},
template: `
<IconButton v-bind="args">
<i class="icon-[lucide--trophy] size-4" />
</IconButton>
`
}),
args: {
type: 'primary',
size: 'md'
}
}
export const Secondary: Story = {
render: (args) => ({
components: { IconButton },
setup() {
return { args }
},
template: `
<IconButton v-bind="args">
<i class="icon-[lucide--settings] size-4" />
</IconButton>
`
}),
args: {
type: 'secondary',
size: 'md'
}
}
export const Transparent: Story = {
render: (args) => ({
components: { IconButton },
setup() {
return { args }
},
template: `
<IconButton v-bind="args">
<i class="icon-[lucide--x] size-4" />
</IconButton>
`
}),
args: {
type: 'transparent',
size: 'md'
}
}
export const Small: Story = {
render: (args) => ({
components: { IconButton },
setup() {
return { args }
},
template: `
<IconButton v-bind="args">
<i class="icon-[lucide--bell] size-3" />
</IconButton>
`
}),
args: {
type: 'secondary',
size: 'sm'
}
}
export const AllVariants: Story = {
render: () => ({
components: { IconButton },
template: `
<div class="flex flex-col gap-4">
<div class="flex gap-2 items-center">
<IconButton type="primary" size="sm" @click="() => {}">
<i class="icon-[lucide--trophy] size-3" />
</IconButton>
<IconButton type="primary" size="md" @click="() => {}">
<i class="icon-[lucide--trophy] size-4" />
</IconButton>
</div>
<div class="flex gap-2 items-center">
<IconButton type="secondary" size="sm" @click="() => {}">
<i class="icon-[lucide--settings] size-3" />
</IconButton>
<IconButton type="secondary" size="md" @click="() => {}">
<i class="icon-[lucide--settings] size-4" />
</IconButton>
</div>
<div class="flex gap-2 items-center">
<IconButton type="transparent" size="sm" @click="() => {}">
<i class="icon-[lucide--x] size-3" />
</IconButton>
<IconButton type="transparent" size="md" @click="() => {}">
<i class="icon-[lucide--x] size-4" />
</IconButton>
</div>
<div class="flex gap-2 items-center">
<IconButton type="primary" size="md" @click="() => {}">
<i class="icon-[lucide--bell] size-4" />
</IconButton>
<IconButton type="secondary" size="md" @click="() => {}">
<i class="icon-[lucide--heart] size-4" />
</IconButton>
<IconButton type="transparent" size="md" @click="() => {}">
<i class="icon-[lucide--download] size-4" />
</IconButton>
</div>
</div>
`
}),
parameters: {
controls: { disable: true },
actions: { disable: true }
}
}

View File

@@ -1,52 +0,0 @@
<template>
<Button
v-bind="$attrs"
unstyled
:class="buttonStyle"
:disabled="disabled"
@click="onClick"
>
<slot></slot>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getBorderButtonTypeClasses,
getButtonTypeClasses,
getIconButtonSizeClasses
} from '@/types/buttonTypes'
import { cn } from '@/utils/tailwindUtil'
interface IconButtonProps extends BaseButtonProps {
onClick?: (event: MouseEvent) => void
}
defineOptions({
inheritAttrs: false
})
const {
size = 'md',
type = 'secondary',
border = false,
disabled = false,
class: className,
onClick
} = defineProps<IconButtonProps>()
const buttonStyle = computed(() => {
const baseClasses = `${getBaseButtonClasses()} p-0`
const sizeClasses = getIconButtonSizeClasses(size)
const typeClasses = border
? getBorderButtonTypeClasses(type)
: getButtonTypeClasses(type)
return cn(baseClasses, sizeClasses, typeClasses, className)
})
</script>

View File

@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import IconButton from './IconButton.vue'
import Button from '@/components/ui/button/Button.vue'
import IconGroup from './IconGroup.vue'
const meta: Meta<typeof IconGroup> = {
@@ -16,18 +16,18 @@ type Story = StoryObj<typeof IconGroup>
export const Basic: Story = {
render: () => ({
components: { IconGroup, IconButton },
components: { IconGroup, Button },
template: `
<IconGroup>
<IconButton @click="console.log('Hello World!!')">
<Button size="icon" @click="console.log('Hello World!!')">
<i class="icon-[lucide--heart] size-4" />
</IconButton>
<IconButton @click="console.log('Hello World!!')">
</Button>
<Button size="icon" @click="console.log('Hello World!!')">
<i class="icon-[lucide--download] size-4" />
</IconButton>
<IconButton @click="console.log('Hello World!!')">
</Button>
<Button size="icon" @click="console.log('Hello World!!')">
<i class="icon-[lucide--external-link] size-4" />
</IconButton>
</Button>
</IconGroup>
`
})

View File

@@ -1,213 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import IconTextButton from './IconTextButton.vue'
const meta: Meta<typeof IconTextButton> = {
title: 'Components/Button/IconTextButton',
component: IconTextButton,
tags: ['autodocs'],
argTypes: {
label: {
control: 'text'
},
size: {
control: { type: 'select' },
options: ['sm', 'md']
},
type: {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent']
},
border: {
control: 'boolean',
description: 'Toggle border attribute'
},
disabled: {
control: 'boolean',
description: 'Toggle disable status'
},
iconPosition: {
control: { type: 'select' },
options: ['left', 'right']
},
onClick: { action: 'clicked' }
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
render: (args) => ({
components: { IconTextButton },
setup() {
return { args }
},
template: `
<IconTextButton v-bind="args">
<template #icon>
<i class="icon-[lucide--package] size-4" />
</template>
</IconTextButton>
`
}),
args: {
label: 'Deploy',
type: 'primary',
size: 'md'
}
}
export const Secondary: Story = {
render: (args) => ({
components: { IconTextButton },
setup() {
return { args }
},
template: `
<IconTextButton v-bind="args">
<template #icon>
<i class="icon-[lucide--settings] size-4" />
</template>
</IconTextButton>
`
}),
args: {
label: 'Settings',
type: 'secondary',
size: 'md'
}
}
export const Transparent: Story = {
render: (args) => ({
components: { IconTextButton },
setup() {
return { args }
},
template: `
<IconTextButton v-bind="args">
<template #icon>
<i class="icon-[lucide--x] size-4" />
</template>
</IconTextButton>
`
}),
args: {
label: 'Cancel',
type: 'transparent',
size: 'md'
}
}
export const WithIconRight: Story = {
render: (args) => ({
components: { IconTextButton },
setup() {
return { args }
},
template: `
<IconTextButton v-bind="args">
<template #icon>
<i class="icon-[lucide--chevron-right] size-4" />
</template>
</IconTextButton>
`
}),
args: {
label: 'Next',
type: 'primary',
size: 'md',
iconPosition: 'right'
}
}
export const Small: Story = {
render: (args) => ({
components: { IconTextButton },
setup() {
return { args }
},
template: `
<IconTextButton v-bind="args">
<template #icon>
<i class="icon-[lucide--save] size-3" />
</template>
</IconTextButton>
`
}),
args: {
label: 'Save',
type: 'primary',
size: 'sm'
}
}
export const AllVariants: Story = {
render: () => ({
components: {
IconTextButton
},
template: `
<div class="flex flex-col gap-4">
<div class="flex gap-2 items-center">
<IconTextButton label="Download" type="primary" size="sm" @click="() => {}">
<template #icon>
<i class="icon-[lucide--download] size-3" />
</template>
</IconTextButton>
<IconTextButton label="Download" type="primary" size="md" @click="() => {}">
<template #icon>
<i class="icon-[lucide--download] size-4" />
</template>
</IconTextButton>
</div>
<div class="flex gap-2 items-center">
<IconTextButton label="Settings" type="secondary" size="sm" @click="() => {}">
<template #icon>
<i class="icon-[lucide--settings] size-3" />
</template>
</IconTextButton>
<IconTextButton label="Settings" type="secondary" size="md" @click="() => {}">
<template #icon>
<i class="icon-[lucide--settings] size-4" />
</template>
</IconTextButton>
</div>
<div class="flex gap-2 items-center">
<IconTextButton label="Delete" type="transparent" size="sm" @click="() => {}">
<template #icon>
<i class="icon-[lucide--trash-2] size-3" />
</template>
</IconTextButton>
<IconTextButton label="Delete" type="transparent" size="md" @click="() => {}">
<template #icon>
<i class="icon-[lucide--trash-2] size-4" />
</template>
</IconTextButton>
</div>
<div class="flex gap-2 items-center">
<IconTextButton label="Next" type="primary" size="md" iconPosition="right" @click="() => {}">
<template #icon>
<i class="icon-[lucide--chevron-right] size-4" />
</template>
</IconTextButton>
<IconTextButton label="Previous" type="secondary" size="md" @click="() => {}">
<template #icon>
<i class="icon-[lucide--chevron-left] size-4" />
</template>
</IconTextButton>
<IconTextButton label="Save File" type="primary" size="md" @click="() => {}">
<template #icon>
<i class="icon-[lucide--save] size-4" />
</template>
</IconTextButton>
</div>
</div>
`
}),
parameters: {
controls: { disable: true },
actions: { disable: true }
}
}

View File

@@ -1,58 +0,0 @@
<template>
<Button
v-bind="$attrs"
unstyled
:class="buttonStyle"
:disabled="disabled"
@click="onClick"
>
<slot v-if="iconPosition !== 'right'" name="icon"></slot>
<span>{{ label }}</span>
<slot v-if="iconPosition === 'right'" name="icon"></slot>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getBorderButtonTypeClasses,
getButtonSizeClasses,
getButtonTypeClasses
} from '@/types/buttonTypes'
import { cn } from '@/utils/tailwindUtil'
defineOptions({
inheritAttrs: false
})
interface IconTextButtonProps extends BaseButtonProps {
iconPosition?: 'left' | 'right'
label: string
onClick?: () => void
}
const {
size = 'md',
type = 'primary',
border = false,
disabled = false,
class: className,
iconPosition = 'left',
label,
onClick
} = defineProps<IconTextButtonProps>()
const buttonStyle = computed(() => {
const baseClasses = `${getBaseButtonClasses()} justify-start gap-2`
const sizeClasses = getButtonSizeClasses(size)
const typeClasses = border
? getBorderButtonTypeClasses(type)
: getButtonTypeClasses(type)
return cn(baseClasses, sizeClasses, typeClasses, className)
})
</script>

View File

@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import IconTextButton from './IconTextButton.vue'
import Button from '@/components/ui/button/Button.vue'
import MoreButton from './MoreButton.vue'
const meta: Meta<typeof MoreButton> = {
@@ -17,30 +17,26 @@ type Story = StoryObj<typeof MoreButton>
export const Basic: Story = {
render: () => ({
components: { MoreButton, IconTextButton },
components: { MoreButton, Button },
template: `
<div style="height: 200px; display: flex; align-items: center; justify-content: center;">
<MoreButton>
<template #default="{ close }">
<IconTextButton
type="transparent"
label="Settings"
<Button
variant="textonly"
@click="() => { close() }"
>
<template #icon>
<i class="icon-[lucide--download] size-4" />
</template>
</IconTextButton>
<i class="icon-[lucide--download] size-4" />
<span>Settings</span>
</Button>
<IconTextButton
type="transparent"
label="Profile"
<Button
variant="textonly"
@click="() => { close() }"
>
<template #icon>
<i class="icon-[lucide--scroll-text] size-4" />
</template>
</IconTextButton>
<i class="icon-[lucide--scroll-text] size-4" />
<span>Profile</span>
</Button>
</template>
</MoreButton>
</div>

View File

@@ -1,9 +1,17 @@
<template>
<div class="relative inline-flex items-center">
<IconButton :size="size" :type="type" @click="popover?.toggle">
<i v-if="!isVertical" class="icon-[lucide--ellipsis] text-sm" />
<i v-else class="icon-[lucide--more-vertical] text-sm" />
</IconButton>
<Button size="icon" variant="secondary" @click="popover?.toggle">
<i
:class="
cn(
!isVertical
? 'icon-[lucide--ellipsis]'
: 'icon-[lucide--more-vertical]',
'text-sm'
)
"
/>
</Button>
<Popover
ref="popover"
@@ -49,20 +57,14 @@
import Popover from 'primevue/popover'
import { ref } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import Button from '@/components/ui/button/Button.vue'
import { cn } from '@/utils/tailwindUtil'
import IconButton from './IconButton.vue'
interface MoreButtonProps extends BaseButtonProps {
interface MoreButtonProps {
isVertical?: boolean
}
const {
size = 'md',
type = 'secondary',
isVertical = false
} = defineProps<MoreButtonProps>()
const { isVertical = false } = defineProps<MoreButtonProps>()
defineEmits<{
menuOpened: []

View File

@@ -1,91 +0,0 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import TextButton from './TextButton.vue'
const meta: Meta<typeof TextButton> = {
title: 'Components/Button/TextButton',
component: TextButton,
tags: ['autodocs'],
argTypes: {
label: {
control: 'text',
defaultValue: 'Click me'
},
size: {
control: { type: 'select' },
options: ['sm', 'md'],
defaultValue: 'md'
},
border: {
control: 'boolean',
description: 'Toggle border attribute'
},
disabled: {
control: 'boolean',
description: 'Toggle disable status'
},
type: {
control: { type: 'select' },
options: ['primary', 'secondary', 'transparent'],
defaultValue: 'primary'
},
onClick: { action: 'clicked' }
}
}
export default meta
type Story = StoryObj<typeof meta>
export const Primary: Story = {
args: {
label: 'Primary Button',
type: 'primary',
size: 'md'
}
}
export const Secondary: Story = {
args: {
label: 'Secondary Button',
type: 'secondary',
size: 'md'
}
}
export const Transparent: Story = {
args: {
label: 'Transparent Button',
type: 'transparent',
size: 'md'
}
}
export const Small: Story = {
args: {
label: 'Small Button',
type: 'primary',
size: 'sm'
}
}
export const AllVariants: Story = {
render: () => ({
components: { TextButton },
template: `
<div class="flex flex-col gap-4">
<div class="flex gap-2 items-center">
<TextButton label="Primary Small" type="primary" size="sm" @click="() => {}" />
<TextButton label="Primary Medium" type="primary" size="md" @click="() => {}" />
</div>
<div class="flex gap-2 items-center">
<TextButton label="Secondary Small" type="secondary" size="sm" @click="() => {}" />
<TextButton label="Secondary Medium" type="secondary" size="md" @click="() => {}" />
</div>
<div class="flex gap-2 items-center">
<TextButton label="Transparent Small" type="transparent" size="sm" @click="() => {}" />
<TextButton label="Transparent Medium" type="transparent" size="md" @click="() => {}" />
</div>
</div>
`
})
}

View File

@@ -1,54 +0,0 @@
<template>
<Button
v-bind="$attrs"
unstyled
:class="buttonStyle"
:disabled="disabled"
@click="onClick"
>
<span>{{ label }}</span>
</Button>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { computed } from 'vue'
import type { BaseButtonProps } from '@/types/buttonTypes'
import {
getBaseButtonClasses,
getBorderButtonTypeClasses,
getButtonSizeClasses,
getButtonTypeClasses
} from '@/types/buttonTypes'
import { cn } from '@/utils/tailwindUtil'
interface TextButtonProps extends BaseButtonProps {
label: string
onClick: () => void
}
defineOptions({
inheritAttrs: false
})
const {
size = 'md',
type = 'primary',
border = false,
disabled = false,
class: className,
label,
onClick
} = defineProps<TextButtonProps>()
const buttonStyle = computed(() => {
const baseClasses = getBaseButtonClasses()
const sizeClasses = getButtonSizeClasses(size)
const typeClasses = border
? getBorderButtonTypeClasses(type)
: getButtonTypeClasses(type)
return cn(baseClasses, sizeClasses, typeClasses, className)
})
</script>

View File

@@ -1,7 +1,7 @@
import type { Meta, StoryObj } from '@storybook/vue3-vite'
import { ref } from 'vue'
import IconButton from '../button/IconButton.vue'
import Button from '@/components/ui/button/Button.vue'
import SquareChip from '../chip/SquareChip.vue'
import CardBottom from './CardBottom.vue'
import CardContainer from './CardContainer.vue'
@@ -173,7 +173,7 @@ const createCardTemplate = (args: CardStoryArgs) => ({
CardBottom,
CardTitle,
CardDescription,
IconButton,
Button,
SquareChip
},
setup() {
@@ -222,19 +222,19 @@ const createCardTemplate = (args: CardStoryArgs) => ({
</template>
<template v-if="args.showTopRight" #top-right>
<IconButton
<Button
class="!bg-white/90 !text-neutral-900"
@click="() => console.log('Info clicked')"
>
<i class="icon-[lucide--info] size-4" />
</IconButton>
<IconButton
</Button>
<Button
class="!bg-white/90"
:class="favorited ? '!text-red-500' : '!text-neutral-900'"
@click="toggleFavorite"
>
<i class="icon-[lucide--heart] size-4" :class="favorited ? 'fill-current' : ''" />
</IconButton>
</Button>
</template>
<template v-if="args.showBottomLeft" #bottom-left>

View File

@@ -21,6 +21,7 @@
@keyup.enter.capture.stop="blurInputElement"
@keyup.escape.stop="cancelEditing"
@click.stop
@contextmenu.stop
@pointerdown.stop.capture
@pointermove.stop.capture
/>

View File

@@ -12,7 +12,6 @@
/>
<img
v-if="cachedSrc"
ref="imageRef"
:src="cachedSrc"
:alt="alt"
draggable="false"
@@ -61,7 +60,6 @@ const {
}>()
const containerRef = ref<HTMLElement | null>(null)
const imageRef = ref<HTMLImageElement | null>(null)
const isIntersecting = ref(false)
const isImageLoaded = ref(false)
const hasError = ref(false)

View File

@@ -17,22 +17,25 @@
class="absolute inset-0 size-full pl-11 border-none outline-none bg-transparent text-sm"
:aria-label="placeholder"
/>
<IconButton
<Button
v-if="filterIcon"
class="p-inputicon filter-button absolute right-0 inset-y-0 h-full m-0 p-0"
:icon="filterIcon"
severity="contrast"
size="icon"
variant="textonly"
class="filter-button absolute right-0 inset-y-0 m-0 p-0"
@click="$emit('showFilter', $event)"
/>
>
<i :class="filterIcon" />
</Button>
<InputIcon v-if="!modelValue" :class="icon" />
<Button
v-if="modelValue"
class="p-inputicon clear-button"
icon="pi pi-times"
text
severity="contrast"
class="clear-button absolute left-0"
variant="textonly"
size="icon"
@click="modelValue = ''"
/>
>
<i class="icon-[lucide--x] size-4" />
</Button>
</div>
<div v-if="filters?.length" class="search-filters flex flex-wrap gap-2 pt-2">
<SearchFilterChip
@@ -49,12 +52,12 @@
<script setup lang="ts" generic="TFilter extends SearchFilter">
import { cn } from '@comfyorg/tailwind-utils'
import { watchDebounced } from '@vueuse/core'
import Button from 'primevue/button'
import InputIcon from 'primevue/inputicon'
import InputText from 'primevue/inputtext'
import { computed, ref } from 'vue'
import IconButton from '../button/IconButton.vue'
import Button from '@/components/ui/button/Button.vue'
import type { SearchFilter } from './SearchFilterChip.vue'
import SearchFilterChip from './SearchFilterChip.vue'
@@ -125,8 +128,4 @@ const wrapperStyle = computed(() => {
:deep(.p-inputtext) {
--p-form-field-padding-x: 0.625rem;
}
.p-button.p-inputicon {
@apply p-0 w-auto border-none;
}
</style>

View File

@@ -0,0 +1,134 @@
import { mount } from '@vue/test-utils'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { createI18n } from 'vue-i18n'
import enMessages from '@/locales/en/main.json' with { type: 'json' }
import UserCredit from './UserCredit.vue'
vi.mock('firebase/app', () => ({
initializeApp: vi.fn(),
getApp: vi.fn()
}))
vi.mock('firebase/auth', () => ({
getAuth: vi.fn(),
setPersistence: vi.fn(),
browserLocalPersistence: {},
onAuthStateChanged: vi.fn(),
signInWithEmailAndPassword: vi.fn(),
signOut: vi.fn()
}))
vi.mock('pinia')
const mockBalance = vi.hoisted(() => ({
value: {
amount_micros: 100_000,
effective_balance_micros: 100_000,
currency: 'usd'
}
}))
const mockIsFetchingBalance = vi.hoisted(() => ({ value: false }))
vi.mock('@/stores/firebaseAuthStore', () => ({
useFirebaseAuthStore: vi.fn(() => ({
balance: mockBalance.value,
isFetchingBalance: mockIsFetchingBalance.value
}))
}))
describe('UserCredit', () => {
beforeEach(() => {
vi.clearAllMocks()
mockBalance.value = {
amount_micros: 100_000,
effective_balance_micros: 100_000,
currency: 'usd'
}
mockIsFetchingBalance.value = false
})
const mountComponent = (props = {}) => {
const i18n = createI18n({
legacy: false,
locale: 'en',
messages: { en: enMessages }
})
return mount(UserCredit, {
props,
global: {
plugins: [i18n],
stubs: {
Skeleton: true,
Tag: true
}
}
})
}
describe('effective_balance_micros handling', () => {
it('uses effective_balance_micros when present (positive balance)', () => {
mockBalance.value = {
amount_micros: 200_000,
effective_balance_micros: 150_000,
currency: 'usd'
}
const wrapper = mountComponent()
expect(wrapper.text()).toContain('Credits')
})
it('uses effective_balance_micros when zero', () => {
mockBalance.value = {
amount_micros: 100_000,
effective_balance_micros: 0,
currency: 'usd'
}
const wrapper = mountComponent()
expect(wrapper.text()).toContain('0')
})
it('uses effective_balance_micros when negative', () => {
mockBalance.value = {
amount_micros: 0,
effective_balance_micros: -50_000,
currency: 'usd'
}
const wrapper = mountComponent()
expect(wrapper.text()).toContain('-')
})
it('falls back to amount_micros when effective_balance_micros is missing', () => {
mockBalance.value = {
amount_micros: 100_000,
currency: 'usd'
} as typeof mockBalance.value
const wrapper = mountComponent()
expect(wrapper.text()).toContain('Credits')
})
it('falls back to 0 when both effective_balance_micros and amount_micros are missing', () => {
mockBalance.value = {
currency: 'usd'
} as typeof mockBalance.value
const wrapper = mountComponent()
expect(wrapper.text()).toContain('0')
})
})
describe('loading state', () => {
it('shows skeleton when loading', () => {
mockIsFetchingBalance.value = true
const wrapper = mountComponent()
expect(wrapper.findComponent({ name: 'Skeleton' }).exists()).toBe(true)
})
})
})

View File

@@ -14,13 +14,7 @@
class="p-1 text-amber-400"
>
<template #icon>
<i
:class="
flags.subscriptionTiersEnabled
? 'icon-[lucide--component]'
: 'pi pi-dollar'
"
/>
<i class="icon-[lucide--component]" />
</template>
</Tag>
<div :class="textClass">
@@ -36,7 +30,6 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { formatCreditsFromCents } from '@/base/credits/comfyCredits'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const { textClass, showCreditsOnly } = defineProps<{
@@ -45,13 +38,14 @@ const { textClass, showCreditsOnly } = defineProps<{
}>()
const authStore = useFirebaseAuthStore()
const { flags } = useFeatureFlags()
const balanceLoading = computed(() => authStore.isFetchingBalance)
const { t, locale } = useI18n()
const formattedBalance = computed(() => {
// Backend returns cents despite the *_micros naming convention.
const cents = authStore.balance?.amount_micros ?? 0
const cents =
authStore.balance?.effective_balance_micros ??
authStore.balance?.amount_micros ??
0
const amount = formatCreditsFromCents({
cents,
locale: locale.value
@@ -60,8 +54,10 @@ const formattedBalance = computed(() => {
})
const formattedCreditsOnly = computed(() => {
// Backend returns cents despite the *_micros naming convention.
const cents = authStore.balance?.amount_micros ?? 0
const cents =
authStore.balance?.effective_balance_micros ??
authStore.balance?.amount_micros ??
0
const amount = formatCreditsFromCents({
cents,
locale: locale.value,

View File

@@ -22,16 +22,17 @@
<template #header-right-area>
<div class="flex gap-2">
<IconTextButton
<Button
v-if="filteredCount !== totalCount"
type="secondary"
:label="$t('templateWorkflows.resetFilters', 'Clear Filters')"
variant="secondary"
size="lg"
@click="resetFilters"
>
<template #icon>
<i class="icon-[lucide--filter-x]" />
</template>
</IconTextButton>
<i class="icon-[lucide--filter-x]" />
<span>{{
$t('templateWorkflows.resetFilters', 'Clear Filters')
}}</span>
</Button>
</div>
</template>
@@ -301,16 +302,16 @@
v-if="template.tutorialUrl"
class="flex flex-col-reverse justify-center"
>
<IconButton
<Button
v-if="hoveredTemplate === template.name"
v-tooltip.bottom="$t('g.seeTutorial')"
v-bind="$attrs"
type="primary"
size="sm"
variant="inverted"
size="icon"
@click.stop="openTutorial(template)"
>
<i class="icon-[lucide--info] size-4" />
</IconButton>
</Button>
</div>
</div>
</div>
@@ -382,8 +383,6 @@ import ProgressSpinner from 'primevue/progressspinner'
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconButton from '@/components/button/IconButton.vue'
import IconTextButton from '@/components/button/IconTextButton.vue'
import CardBottom from '@/components/card/CardBottom.vue'
import CardContainer from '@/components/card/CardContainer.vue'
import CardTop from '@/components/card/CardTop.vue'
@@ -395,6 +394,7 @@ import AudioThumbnail from '@/components/templates/thumbnails/AudioThumbnail.vue
import CompareSliderThumbnail from '@/components/templates/thumbnails/CompareSliderThumbnail.vue'
import DefaultThumbnail from '@/components/templates/thumbnails/DefaultThumbnail.vue'
import HoverDissolveThumbnail from '@/components/templates/thumbnails/HoverDissolveThumbnail.vue'
import Button from '@/components/ui/button/Button.vue'
import BaseModalLayout from '@/components/widget/layout/BaseModalLayout.vue'
import LeftSidePanel from '@/components/widget/panel/LeftSidePanel.vue'
import { useIntersectionObserver } from '@/composables/useIntersectionObserver'

View File

@@ -1,19 +1,16 @@
<template>
<section class="w-full flex gap-2 justify-end px-2 pb-2">
<TextButton
:label="cancelTextX"
<Button :disabled variant="textonly" autofocus @click="$emit('cancel')">
{{ cancelTextX }}
</Button>
<Button
:disabled
type="transparent"
autofocus
@click="$emit('cancel')"
/>
<TextButton
:label="confirmTextX"
:disabled
type="transparent"
variant="textonly"
:class="confirmClass"
@click="$emit('confirm')"
/>
>
{{ confirmTextX }}
</Button>
</section>
</template>
<script setup lang="ts">
@@ -21,7 +18,7 @@ import { computed, toValue } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
import { useI18n } from 'vue-i18n'
import TextButton from '@/components/button/TextButton.vue'
import Button from '@/components/ui/button/Button.vue'
const { t } = useI18n()

View File

@@ -4,36 +4,27 @@
v-if="isCloud"
class="flex w-full items-center justify-between gap-2 py-2 px-4"
>
<IconTextButton
:label="$t('missingNodes.cloud.learnMore')"
type="transparent"
<Button
variant="textonly"
size="sm"
icon-position="left"
as="a"
href="https://www.comfy.org/cloud"
target="_blank"
rel="noopener noreferrer"
>
<template #icon>
<i class="icon-[lucide--info]"></i>
</template>
</IconTextButton>
<TextButton
:label="$t('missingNodes.cloud.gotIt')"
type="secondary"
size="md"
@click="handleGotItClick"
/>
<i class="icon-[lucide--info]"></i>
<span>{{ $t('missingNodes.cloud.learnMore') }}</span>
</Button>
<Button variant="secondary" size="md" @click="handleGotItClick">{{
$t('missingNodes.cloud.gotIt')
}}</Button>
</div>
<!-- OSS mode: Open Manager + Install All buttons -->
<div v-else-if="showManagerButtons" class="flex justify-end gap-1 py-2 px-4">
<TextButton
:label="$t('g.openManager')"
type="transparent"
size="sm"
@click="openManager"
/>
<Button variant="textonly" size="sm" @click="openManager">{{
$t('g.openManager')
}}</Button>
<PackInstallButton
v-if="showInstallAllButton"
type="secondary"
@@ -56,8 +47,7 @@
import { computed, nextTick, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import TextButton from '@/components/button/TextButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { isCloud } from '@/platform/distribution/types'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useDialogStore } from '@/stores/dialogStore'

View File

@@ -1,6 +1,5 @@
<template>
<!-- New Credits Design (default) -->
<div v-if="useNewDesign" class="flex w-112 flex-col gap-8 p-8">
<div class="flex w-112 flex-col gap-8 p-8">
<!-- Header -->
<div class="flex flex-col gap-4">
<h1 class="text-2xl font-semibold text-base-foreground m-0">
@@ -66,91 +65,32 @@
@click="handleBuy"
/>
</div>
<!-- Legacy Design -->
<div v-else class="flex w-96 flex-col gap-10 p-2">
<div v-if="isInsufficientCredits" class="flex flex-col gap-4">
<h1 class="my-0 text-2xl leading-normal font-medium">
{{ $t('credits.topUp.insufficientTitle') }}
</h1>
<p class="my-0 text-base">
{{ $t('credits.topUp.insufficientMessage') }}
</p>
</div>
<!-- Balance Section -->
<div class="flex items-center justify-between">
<div class="flex w-full flex-col gap-2">
<div class="text-base text-muted">
{{ $t('credits.yourCreditBalance') }}
</div>
<div class="flex w-full items-center justify-between">
<UserCredit text-class="text-2xl" />
<Button
outlined
severity="secondary"
:label="$t('credits.topUp.seeDetails')"
icon="pi pi-arrow-up-right"
@click="handleSeeDetails"
/>
</div>
</div>
</div>
<!-- Amount Input Section -->
<div class="flex flex-col gap-2">
<span class="text-sm text-muted"
>{{ $t('credits.topUp.quickPurchase') }}:</span
>
<div class="grid grid-cols-[2fr_1fr] gap-2">
<LegacyCreditTopUpOption
v-for="amount in amountOptions"
:key="amount"
:amount="amount"
:preselected="amount === preselectedAmountOption"
/>
<LegacyCreditTopUpOption :amount="100" :preselected="false" editable />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useToast } from 'primevue/usetoast'
import { computed, ref } from 'vue'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { creditsToUsd } from '@/base/credits/comfyCredits'
import UserCredit from '@/components/common/UserCredit.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useFeatureFlags } from '@/composables/useFeatureFlags'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { useTelemetry } from '@/platform/telemetry'
import CreditTopUpOption from './credit/CreditTopUpOption.vue'
import LegacyCreditTopUpOption from './credit/LegacyCreditTopUpOption.vue'
interface CreditOption {
credits: number
description: string
}
const {
isInsufficientCredits = false,
amountOptions = [5, 10, 20, 50],
preselectedAmountOption = 10
} = defineProps<{
const { isInsufficientCredits = false } = defineProps<{
isInsufficientCredits?: boolean
amountOptions?: number[]
preselectedAmountOption?: number
}>()
const { flags } = useFeatureFlags()
const { formattedRenewalDate } = useSubscription()
// Use feature flag to determine design - defaults to true (new design)
const useNewDesign = computed(() => flags.subscriptionTiersEnabled)
const { t } = useI18n()
const authActions = useFirebaseAuthActions()
@@ -202,8 +142,4 @@ const handleBuy = async () => {
loading.value = false
}
}
const handleSeeDetails = async () => {
await authActions.accessBillingPortal()
}
</script>

View File

@@ -1,119 +0,0 @@
<template>
<div class="flex items-center gap-2">
<Tag
severity="secondary"
icon="pi pi-wallet"
rounded
class="p-1 text-amber-400"
/>
<div v-if="editable" class="flex items-center gap-2">
<InputNumber
v-model="customAmount"
:min="1"
:max="1000"
:step="1"
show-buttons
:allow-empty="false"
:highlight-on-focus="true"
prefix="$"
pt:pc-input-text:root="w-28"
@blur="
(e: InputNumberBlurEvent) =>
(customAmount = clampUsd(Number(e.value)))
"
@input="
(e: InputNumberInputEvent) =>
(customAmount = clampUsd(Number(e.value)))
"
/>
<span class="text-xs text-muted">{{ formattedCredits }}</span>
</div>
<div v-else class="flex flex-col leading-tight">
<span class="text-xl font-semibold">{{ formattedCredits }}</span>
<span class="text-xs text-muted">{{ formattedUsd }}</span>
</div>
</div>
<ProgressSpinner v-if="loading" class="h-8 w-8" />
<Button
v-else
:severity="preselected ? 'primary' : 'secondary'"
:outlined="!preselected"
:label="$t('credits.topUp.buyNow')"
@click="handleBuyNow"
/>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import InputNumber from 'primevue/inputnumber'
import type {
InputNumberBlurEvent,
InputNumberInputEvent
} from 'primevue/inputnumber'
import ProgressSpinner from 'primevue/progressspinner'
import Tag from 'primevue/tag'
import { computed, onBeforeUnmount, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import {
clampUsd,
formatCreditsFromUsd,
formatUsd
} from '@/base/credits/comfyCredits'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { useTelemetry } from '@/platform/telemetry'
const authActions = useFirebaseAuthActions()
const telemetry = useTelemetry()
const {
amount,
preselected,
editable = false
} = defineProps<{
amount: number
preselected: boolean
editable?: boolean
}>()
const customAmount = ref(amount)
const didClickBuyNow = ref(false)
const loading = ref(false)
const { t, locale } = useI18n()
const displayUsdAmount = computed(() =>
editable ? clampUsd(Number(customAmount.value)) : clampUsd(amount)
)
const formattedCredits = computed(
() =>
`${formatCreditsFromUsd({
usd: displayUsdAmount.value,
locale: locale.value
})} ${t('credits.credits')}`
)
const formattedUsd = computed(
() => `$${formatUsd({ value: displayUsdAmount.value, locale: locale.value })}`
)
const handleBuyNow = async () => {
const creditAmount = displayUsdAmount.value
telemetry?.trackApiCreditTopupButtonPurchaseClicked(creditAmount)
loading.value = true
try {
await authActions.purchaseCredits(creditAmount)
didClickBuyNow.value = true
} finally {
loading.value = false
}
}
onBeforeUnmount(() => {
if (didClickBuyNow.value) {
// If clicked buy now, then returned back to the dialog and closed, fetch the balance
void authActions.fetchBalance()
}
})
</script>

View File

@@ -139,7 +139,7 @@ interface CreditHistoryItemData {
isPositive: boolean
}
const { buildDocsUrl } = useExternalLink()
const { buildDocsUrl, docsPaths } = useExternalLink()
const dialogService = useDialogService()
const authStore = useFirebaseAuthStore()
const authActions = useFirebaseAuthActions()
@@ -194,9 +194,7 @@ const handleFaqClick = () => {
const handleOpenPartnerNodesInfo = () => {
window.open(
buildDocsUrl('/tutorials/api-nodes/overview#api-nodes', {
includeLocale: true
}),
buildDocsUrl(docsPaths.partnerNodesPricing, { includeLocale: true }),
'_blank'
)
}

View File

@@ -15,9 +15,7 @@
<script setup lang="ts">
import Tag from 'primevue/tag'
// Global variable from vite build defined in global.d.ts
// eslint-disable-next-line no-undef
const isStaging = !__USE_PROD_CONFIG__
import { isStaging } from '@/config/staging'
</script>
<style scoped>

View File

@@ -76,8 +76,8 @@
/>
</TransformPane>
<!-- Selection rectangle overlay for Vue nodes mode -->
<SelectionRectangle v-if="shouldRenderVueNodes && comfyAppReady" />
<!-- Selection rectangle overlay - rendered in DOM layer to appear above DOM widgets -->
<SelectionRectangle v-if="comfyAppReady" />
<NodeTooltip v-if="tooltipEnabled" />
<NodeSearchboxPopover ref="nodeSearchboxPopoverRef" />
@@ -87,7 +87,6 @@
<template v-if="comfyAppReady">
<TitleEditor />
<SelectionToolbox v-if="selectionToolboxEnabled" />
<NodeOptions />
<!-- Render legacy DOM widgets only when Vue nodes are disabled -->
<DomWidgets v-if="!shouldRenderVueNodes" />
</template>
@@ -115,7 +114,6 @@ import GraphCanvasMenu from '@/components/graph/GraphCanvasMenu.vue'
import NodeTooltip from '@/components/graph/NodeTooltip.vue'
import SelectionToolbox from '@/components/graph/SelectionToolbox.vue'
import TitleEditor from '@/components/graph/TitleEditor.vue'
import NodeOptions from '@/components/graph/selectionToolbox/NodeOptions.vue'
import NodePropertiesPanel from '@/components/rightSidePanel/RightSidePanel.vue'
import NodeSearchboxPopover from '@/components/searchbox/NodeSearchBoxPopover.vue'
import SideToolbar from '@/components/sidebar/SideToolbar.vue'

View File

@@ -10,7 +10,7 @@
></div>
<ButtonGroup
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-interface-stroke bg-comfy-menu-bg p-2"
class="absolute right-0 bottom-0 z-1200 flex-row gap-1 border-[1px] border-interface-stroke bg-comfy-menu-bg p-2"
:style="{
...stringifiedMinimapStyles.buttonGroupStyles
}"

View File

@@ -0,0 +1,272 @@
<template>
<ContextMenu
ref="contextMenu"
:model="menuItems"
class="max-h-[80vh] md:max-h-none overflow-y-auto md:overflow-y-visible"
@show="onMenuShow"
@hide="onMenuHide"
>
<template #item="{ item, props, hasSubmenu }">
<a
v-bind="props.action"
class="flex items-center gap-2 px-3 py-1.5"
@click="item.isColorSubmenu ? showColorPopover($event) : undefined"
>
<i v-if="item.icon" :class="[item.icon, 'size-4']" />
<span class="flex-1">{{ item.label }}</span>
<span
v-if="item.shortcut"
class="flex h-3.5 min-w-3.5 items-center justify-center rounded bg-interface-menu-keybind-surface-default px-1 py-0 text-xs"
>
{{ item.shortcut }}
</span>
<i
v-if="hasSubmenu || item.isColorSubmenu"
class="icon-[lucide--chevron-right] size-4 opacity-60"
/>
</a>
</template>
</ContextMenu>
<!-- Color picker menu (custom with color circles) -->
<ColorPickerMenu
v-if="colorOption"
ref="colorPickerMenu"
key="color-picker-menu"
:option="colorOption"
@submenu-click="handleColorSelect"
/>
</template>
<script setup lang="ts">
import { useElementBounding, useEventListener, useRafFn } from '@vueuse/core'
import ContextMenu from 'primevue/contextmenu'
import type { MenuItem } from 'primevue/menuitem'
import { computed, onMounted, onUnmounted, ref, watchEffect } from 'vue'
import {
registerNodeOptionsInstance,
useMoreOptionsMenu
} from '@/composables/graph/useMoreOptionsMenu'
import type {
MenuOption,
SubMenuOption
} from '@/composables/graph/useMoreOptionsMenu'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import ColorPickerMenu from './selectionToolbox/ColorPickerMenu.vue'
interface ExtendedMenuItem extends MenuItem {
isColorSubmenu?: boolean
shortcut?: string
originalOption?: MenuOption
}
const contextMenu = ref<InstanceType<typeof ContextMenu>>()
const colorPickerMenu = ref<InstanceType<typeof ColorPickerMenu>>()
const isOpen = ref(false)
const { menuOptions, bump } = useMoreOptionsMenu()
const canvasStore = useCanvasStore()
// World position (canvas coordinates) where menu was opened
const worldPosition = ref({ x: 0, y: 0 })
// Get canvas bounding rect reactively
const lgCanvas = canvasStore.getCanvas()
const { left: canvasLeft, top: canvasTop } = useElementBounding(lgCanvas.canvas)
// Track last canvas transform to detect actual changes
let lastScale = 0
let lastOffsetX = 0
let lastOffsetY = 0
// Update menu position based on canvas transform
const updateMenuPosition = () => {
if (!isOpen.value) return
const menuInstance = contextMenu.value as unknown as {
container?: HTMLElement
}
const menuEl = menuInstance?.container
if (!menuEl) return
const { scale, offset } = lgCanvas.ds
// Only update if canvas transform actually changed
if (
scale === lastScale &&
offset[0] === lastOffsetX &&
offset[1] === lastOffsetY
) {
return
}
lastScale = scale
lastOffsetX = offset[0]
lastOffsetY = offset[1]
// Convert world position to screen position
const screenX = (worldPosition.value.x + offset[0]) * scale + canvasLeft.value
const screenY = (worldPosition.value.y + offset[1]) * scale + canvasTop.value
// Update menu position
menuEl.style.left = `${screenX}px`
menuEl.style.top = `${screenY}px`
}
// Sync with canvas transform using requestAnimationFrame
const { resume: startSync, pause: stopSync } = useRafFn(updateMenuPosition, {
immediate: false
})
// Start/stop syncing based on menu visibility
watchEffect(() => {
if (isOpen.value) {
startSync()
} else {
stopSync()
}
})
// Close on touch outside to handle mobile devices where click might be swallowed
useEventListener(
window,
'touchstart',
(event: TouchEvent) => {
if (!isOpen.value || !contextMenu.value) return
const target = event.target as Node
const contextMenuInstance = contextMenu.value as unknown as {
container?: HTMLElement
$el?: HTMLElement
}
const menuEl = contextMenuInstance.container || contextMenuInstance.$el
if (menuEl && !menuEl.contains(target)) {
hide()
}
},
{ passive: true }
)
// Find color picker option
const colorOption = computed(() =>
menuOptions.value.find((opt) => opt.isColorPicker)
)
// Check if option is the color picker
function isColorOption(option: MenuOption): boolean {
return Boolean(option.isColorPicker)
}
// Convert MenuOption to PrimeVue MenuItem
function convertToMenuItem(option: MenuOption): ExtendedMenuItem {
if (option.type === 'divider') return { separator: true }
const isColor = isColorOption(option)
const item: ExtendedMenuItem = {
label: option.label,
icon: option.icon,
disabled: option.disabled,
shortcut: option.shortcut,
isColorSubmenu: isColor,
originalOption: option
}
// Native submenus for non-color options
if (option.hasSubmenu && option.submenu && !isColor) {
item.items = option.submenu.map((sub) => ({
label: sub.label,
icon: sub.icon,
disabled: sub.disabled,
command: () => {
sub.action()
hide()
}
}))
}
// Regular action items
if (!option.hasSubmenu && option.action) {
item.command = () => {
option.action?.()
hide()
}
}
return item
}
// Build menu items
const menuItems = computed<ExtendedMenuItem[]>(() =>
menuOptions.value.map(convertToMenuItem)
)
// Show context menu
function show(event: MouseEvent) {
bump()
// Convert screen position to world coordinates
// Screen position relative to canvas = event position - canvas offset
const screenX = event.clientX - canvasLeft.value
const screenY = event.clientY - canvasTop.value
// Convert to world coordinates using canvas transform
const { scale, offset } = lgCanvas.ds
worldPosition.value = {
x: screenX / scale - offset[0],
y: screenY / scale - offset[1]
}
isOpen.value = true
contextMenu.value?.show(event)
}
// Hide context menu
function hide() {
contextMenu.value?.hide()
}
function toggle(event: Event) {
if (isOpen.value) {
hide()
} else {
show(event as MouseEvent)
}
}
defineExpose({ toggle, hide, isOpen, show })
function showColorPopover(event: MouseEvent) {
event.stopPropagation()
event.preventDefault()
const target = Array.from((event.currentTarget as HTMLElement).children).find(
(el) => el.classList.contains('icon-[lucide--chevron-right]')
) as HTMLElement
colorPickerMenu.value?.toggle(event, target)
}
// Handle color selection
function handleColorSelect(subOption: SubMenuOption) {
subOption.action()
hide()
}
function onMenuShow() {
isOpen.value = true
}
function onMenuHide() {
isOpen.value = false
}
onMounted(() => {
registerNodeOptionsInstance({ toggle, show, hide, isOpen })
})
onUnmounted(() => {
registerNodeOptionsInstance(null)
})
</script>

View File

@@ -1,7 +1,7 @@
<template>
<div
v-if="isVisible"
class="pointer-events-none absolute border border-blue-400 bg-blue-500/20"
class="pointer-events-none absolute z-9999 border border-blue-400 bg-blue-500/20"
:style="rectangleStyle"
/>
</template>

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