Compare commits

..

76 Commits

Author SHA1 Message Date
Benjamin Lu
ee2e1a909e fix(tests): align LGraphNode mock with Pinia 3 2025-12-30 16:21:51 -08:00
Benjamin Lu
4fced8a3f8 pnpm install 2025-12-30 15:36:23 -08:00
Benjamin Lu
a1f798c6c2 Merge remote-tracking branch 'origin/main' into bl/amd-gpu-picker 2025-12-30 15:33:28 -08:00
Benjamin Lu
6e3723d814 minor text change 2025-12-30 15:12:59 -08:00
Benjamin Lu
ebf11faeea Switch to new desktop types packages 2025-12-30 15:09:26 -08:00
Benjamin Lu
68d1d21865 Use useI18n in media asset actions (#7744)
Use useI18n for media asset action translations and fix missing
translation string.

## What changed
- Switch the media asset actions composable to use `useI18n()` instead
of the global `t`.
- Use the existing `mediaAsset.selection.downloadsStarted` key for the
single-download toast to avoid missing strings.

## Why
- Aligns translation usage with the composition API and avoids
referencing a non-existent key.
- Reuses existing translation keys without adding new strings.

## Evidence
- Tests: `pnpm lint:fix`, `pnpm typecheck`, `pnpm knip`

## References
- N/A

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7744-Use-useI18n-in-media-asset-actions-2d26d73d365081f79cd1c5af105fc59b)
by [Unito](https://www.unito.io)
2025-12-30 12:55:48 -08:00
Christian Byrne
53b1dd282c disable workflow validation warnings by default (#7795)
# Background

Currently, validation warnings about zod schema violations are shown to
all users when loading workflows. These warnings appear in a dialog that
users must dismiss, and they reappear every time the workflow is
reloaded.

## User Pain Points

- Many users (especially from the Chinese community) are asking how to
disable these alerts
- The zod schema information is too technical for end users
- Users don't understand what action they should take when seeing the
warning
- The warnings cannot be permanently dismissed - they reappear every
time
- The warnings don't actually prevent workflow execution

## Problem Statement

The validation warning just means the serialized workflow doesn't
conform to the official schema. Sometimes that makes the workflow
unusable; sometimes it still runs fine. While these checks have
historically surfaced real issues and are important for maintaining
static types internally, the current UX isn't helpful to regular users.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7795-disable-workflow-validation-warnings-by-default-2d86d73d36508102a0f7c46d43189e38)
by [Unito](https://www.unito.io)
2025-12-30 13:47:30 -07:00
Benjamin Lu
b1d94b6619 Group NVIDIA and AMD after darwin 2025-12-30 12:31:11 -08:00
Benjamin Lu
62c7ad8482 Add AMD logo and text 2025-12-30 12:26:34 -08:00
Terry Jia
f5e51d0339 fix: PrimitiveNode combo widget value not persisting in vueNodes mode (#7782)
## Summary
- Add missing v-model binding to WidgetWithControl in WidgetSelect.vue
- Trigger callback after setting widget value in PrimitiveNode to update
linked widgets

The combo widget value was lost because PrimitiveNode creates combo
widgets with controlWidget (via addValueControlWidgets), causing it to
use the WidgetWithControl branch which was missing v-model binding.

fix https://github.com/Comfy-Org/ComfyUI_frontend/issues/7694 and
https://github.com/Comfy-Org/ComfyUI_frontend/issues/7696

## Screenshots
before

https://github.com/user-attachments/assets/0a0dc668-eb71-4072-b5f7-0c20483fc1ff

after

https://github.com/user-attachments/assets/dfd2274c-8ebf-4105-821a-946063d87e9a
2025-12-30 14:12:29 -05:00
Terry Jia
27caaa38f9 fix: restore mask editor compatibility with Impact-Pack plugin (#7762)
## Summary

- Skip widget filenames starting with '$' (internal reference format)
- Add deprecated ComfyApp.open_maskeditor for plugin compatibility
- Register MaskEditor button in ClipspaceDialog

fix https://github.com/Comfy-Org/ComfyUI_frontend/issues/7665 and 
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/1165
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/1158
https://github.com/ltdrdata/ComfyUI-Impact-Pack/issues/1157

## Screenshots (if applicable)
before


https://github.com/user-attachments/assets/356dd920-8b64-40f4-8839-90b955ffafc3

after


https://github.com/user-attachments/assets/dff9abcd-530a-4995-bb4b-51f408d4eca9

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7762-fix-restore-mask-editor-compatibility-with-Impact-Pack-plugin-2d46d73d36508199bdaecbd5ba838483)
by [Unito](https://www.unito.io)
2025-12-29 22:21:21 -07:00
Benjamin Lu
4df0ad3dc7 Add AMD as an install option + lint fixes 2025-12-29 20:22:32 -08:00
Johnpaul Chiwetelu
3372f455ca CI: Use custom container for E2E tests (#7625)
## Summary
Use a pre-built container image with all dependencies for Playwright E2E
tests, eliminating ~130s setup time per shard.

## Changes
- Use `ghcr.io/comfy-org/comfyui-ci-container:0.0.8` container for test
jobs
- Container includes: Playwright browsers, Node.js, pnpm, Python,
ComfyUI backend (v0.5.1), all Python deps
- Simplified setup: just copy devtools and start server (no cloning, no
pip install, no browser setup)
- Add `version-bump*` to `branches-ignore` to skip E2E tests for version
bump PRs
- Replace cache with artifacts (cache doesn't work inside containers)

## Benefits
- ~130s faster per shard (no ComfyUI clone, no pip install, no browser
download)
- Consistent environment across all test jobs
- Simpler workflow configuration

## Container Image
Repository: https://github.com/comfy-org/comfyui-ci-container
Image: `ghcr.io/comfy-org/comfyui-ci-container:0.0.8`

## Test plan
- [x] Verify CI workflow runs with container
- [x] Verify Playwright tests pass
- [x] Verify snapshot updates work correctly

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-12-30 04:58:25 +01:00
Alexander Brown
3ae2b52649 Chore: Upgrade Vitest to v4 (#7797)
## Summary

https://vitest.dev/guide/migration.html#vitest-4

## Changes

- **What**: Update Vitest and some associated dependencies
- **What**: Fix issue with our existing mocks and mock types

## Review Focus

Double check the test updates. I tried to keep the changes minimal.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7797-Chore-Upgrade-Vitest-to-v4-2d96d73d3650810cbe3ac42d7bd6585a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2025-12-29 19:24:35 -08:00
Comfy Org PR Bot
91ed58acc9 1.37.0 (#7794)
Minor version increment to 1.37.0

**Base branch:** `main`

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

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-12-29 17:38:38 -07:00
Alexander Brown
7b68b19f11 Component: The Rest of the PrimeVue buttons (#7649)
## Summary

Automated initial change, cleaned up manually.

Please check the screenshot changes.

Includes a11y updates to icon buttons.

Doesn't hit the buttons in Desktop.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7649-WIP-Component-The-Rest-of-the-PrimeVue-buttons-2ce6d73d365081d68e06f200f1321267)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-12-29 15:03:34 -08:00
Godwin Iheuwa
ea96c71818 fix(queue): Cancel button now works for pending jobs (#7788)
## Summary

Fixes the cancel button on queue job items to properly handle pending
(queued) jobs.

## Problem

The cancel button was calling `api.interrupt()` for all jobs, but
interrupt only affects running/initializing jobs. For pending jobs, it
silently fails with log message: `"Prompt ... is not currently running,
skipping interrupt"`.

The "Cancel job" option in the context menu worked correctly because it
checks the job state first.

Reported in #7758.

## Changes

Update `onCancelItem` in `QueueProgressOverlay.vue` to mirror the
behavior of `cancelJob()` in `useJobMenu.ts`:

- Check `item.state` before deciding which API to call
- Call `api.interrupt(promptId)` for `running` or `initialization`
states
- Call `api.deleteItem('queue', promptId)` for `pending` state
- Refresh queue state after cancel action with `queueStore.update()`

## Testing

- All 3816 unit tests pass
- Type check passes
- Lint passes (prettier, oxlint, eslint)

## Steps to Reproduce (before fix)

1. Queue more than 1 job
2. Open job history
3. Click "Cancel" button on any "in queue" job
4. Observe nothing happens (job remains in queue)

After this fix, clicking Cancel on a pending job will remove it from the
queue.

Fixes #7758

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7788-fix-queue-Cancel-button-now-works-for-pending-jobs-2d86d73d365081b3957fdf1d5d677809)
by [Unito](https://www.unito.io)

---------

Co-authored-by: RUiNtheExtinct <deepkarma001@gmail.com>
2025-12-29 11:48:59 -08:00
Csongor Czezar
a87bd0eb37 feat: position properties panel opposite to sidebar (#7647)
## Problem

When sidebar is positioned on the right, the properties panel also
appears on the right, causing both panels to compete for space and
creating a poor layout.

## Solution

Properties panel now dynamically positions itself opposite to the
sidebar:
- Sidebar left → Properties panel right (default)
- Sidebar right → Properties panel left

## Changes

- Modified `LiteGraphCanvasSplitterOverlay.vue` to conditionally render
properties panel based on sidebar location
- Updated splitter refresh key to recalculate layout when sidebar
position changes
- Added dynamic close button icon in `RightSidePanel.vue` that points in
the correct direction

## Testing

- Created E2E tests to verify positioning behavior
- Manually verified visual behavior in browser

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7647-feat-position-properties-panel-opposite-to-sidebar-2ce6d73d365081049683e74c8d03dbdd)
by [Unito](https://www.unito.io)
2025-12-29 10:58:38 -08:00
Christian Byrne
33d8cb7069 test: deflake templates locale coverage (#7705)
## Summary
Ensure the templates locale Playwright test validates localized UI text
instead of waiting on a flaky network request.

## Changes
- **What**: Update `Templates >> Uses proper locale files for templates`
to assert on French strings rendered in the dialog and confirm English
fallback is absent

## Review Focus
- Confirm the chosen French strings always appear when the localized
bundle loads so the test meaningfully covers the regression

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7705-test-deflake-templates-locale-coverage-2d16d73d365081ffbf9adc1623a36733)
by [Unito](https://www.unito.io)
2025-12-27 18:40:00 -07:00
Comfy Org PR Bot
52bb58d307 1.36.12 (#7763)
Patch version increment to 1.36.12

**Base branch:** `main`

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-12-27 17:05:27 -08:00
Terry Jia
59af15961f feat: add ImageCompare node (#7538)
## Summary

add ImageCompare node, which is high demand among custom nodes, such as
rgthree, we should support as core node

Need BE change https://github.com/comfyanonymous/ComfyUI/pull/11343

## Screenshots (if applicable)



https://github.com/user-attachments/assets/a37bdcd0-de59-4bdd-bfc7-1adbe92f5298

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7538-feat-add-ImageCompare-node-2cb6d73d36508163a7d5f4807aece01a)
by [Unito](https://www.unito.io)
2025-12-26 13:27:44 -07:00
Tommie
533295ab76 Fix component widget state on graph change (#7648)
## Summary

Component widgets (e.g. Load3D) in the root graph stay inactive after
leaving a subgraph.

## Changes

-  Adds component widget class to the active widget filter.

## Screenshots

|Before|After|
|-|-|

|![ComponentBefore](https://github.com/user-attachments/assets/a931bcb7-bc46-424d-8a80-d7b3e6acba5e)|![ComponentAfter](https://github.com/user-attachments/assets/8673f777-c69c-4bce-9df0-ada28bf67b8d)|

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7648-Fix-component-widget-state-on-graph-change-2ce6d73d365081aabff5d6c07430da3d)
by [Unito](https://www.unito.io)
2025-12-26 11:09:12 -07:00
Jin Yi
39815b5a66 fix: import fail info warning icon (#7753)
# Fix Import Failed Warning Icon

## Problem Description

Warning icons were not displayed when import failed errors occurred in
installed packages.

## Root Cause

Conflict detection logic mismatch between `PackCardFooter` and
`PackEnableToggle`:

- **PackCardFooter**: Uses `checkNodeCompatibility()`
- System compatibility check **before** installation (OS, accelerator,
version, etc.)
  - Does not include import failed information

- **PackEnableToggle**: Uses `getConflictsForPackageByID()`  
- Actual conflict data **after** installation (including import failed)
  - But was dependent on parent component's `hasConflict` prop

## Changes Made

### 1. PackEnableToggle.vue
```diff
- <div v-if="hasConflict">
+ <div v-if="packageConflict?.has_conflict">
```
- Removed `hasConflict` prop dependency
- Changed to use only internal store data (`packageConflict`)

### 2. PackCardFooter.vue  
```diff
- <PackEnableToggle :has-conflict="hasConflicts" :node-pack="nodePack" />
+ <PackEnableToggle :node-pack="nodePack" />
```
- Removed unnecessary `has-conflict` prop passing

## Result

-  Warning icon properly displays for installed packages with import
failed errors
-  Conflict modal works correctly when clicked
-  Each component uses appropriate conflict detection logic


[after.webm](https://github.com/user-attachments/assets/80576018-0a5b-4e32-9df6-686be3774313)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7753-fix-import-fail-info-warning-icon-2d36d73d365081518fbeedf539a19040)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-12-26 11:08:00 -07:00
DorotaL
27a479f9c4 [i18n] update zh (#7761)
## Summary

Update zh, supplemented the untranslated content.
Only locale changes.

## Changes

zh locales

## Review Focus

no sure this needs a review

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7761-i18n-update-zh-2d46d73d365081c59c27f0b6ba257526)
by [Unito](https://www.unito.io)
2025-12-26 11:06:45 -07:00
Comfy Org PR Bot
f855deb4b1 1.36.11 (#7760)
Patch version increment to 1.36.11

**Base branch:** `main`

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

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-12-24 16:39:49 -08:00
Alexander Piskun
723bbb98eb add prices for Kling Motion Control node (#7756)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7756-add-prices-for-Kling-Motion-Control-node-2d36d73d3650814193f3c84f25624518)
by [Unito](https://www.unito.io)
2025-12-24 19:37:54 +02:00
AustinMroz
9fc6a5a27d Workaround for reload causing node spread (#7751)
#7435 introduced a tricky regression which will cause extremely small
levels of zoom with nodes spread far apart when in vue mode. I am able
to consistently reproduce this behaviour by
- Being in vue mode
- Swapping to a different tab so that ComfyUI is in the background
- Making a pointless line change to frontend code so that vite forces a
reload
- Waiting ~1 minute to ensure the reload completes
- Swapping back to the ComfyUI tab

From testing, if a reload occurs while the tab is backgrounded, the
canvas has an uninitialized size of 300x150. This PR proposes falling
back to a more sane default width and height of 1920x1080 if it is
detected that the canvas element is unitialized.

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/8e19fc98-7187-4008-98cc-fb5ea3bcdce2"/>
| <img width="360" alt="after"
src="https://github.com/user-attachments/assets/add88614-3451-44df-ae9a-b0b867486459"
/>|

This appears to have consistently good results, but second opinions or
further testing would be appreciated. A more reasonable option (like
skipping this automatic fitView if the canvas has uninitialized size) is
likely to be safer, even if it results in a return of edge cases
resulting in a graph having no nodes in view after load.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7751-Workaround-for-reload-causing-node-spread-2d36d73d365081b9ae74d5f0e6f436f5)
by [Unito](https://www.unito.io)
2025-12-23 23:29:36 -08:00
AustinMroz
ab16c153c7 Fix slot renaming in vue (#7748)
Adds an additional check for `slotData.label` so that renamed slots
properly display in vue mode

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7748-Fix-slot-renaming-in-vue-2d36d73d365081dc8247f2d9e9e06a7a)
by [Unito](https://www.unito.io)
2025-12-23 20:51:20 -07:00
Comfy Org PR Bot
08895767a9 1.36.10 (#7747)
Patch version increment to 1.36.10

**Base branch:** `main`

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

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-12-23 17:46:03 -07:00
AustinMroz
f9b58904d9 Ensure widgets always get a single callback (#7579)
The other side of reactivity. Ensure that vue mode always registers a
callback on litegraph nodes and never registers more than one.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7579-Ensure-widgets-always-get-a-single-callback-2cc6d73d365081e2a488c38ae394efc0)
by [Unito](https://www.unito.io)
2025-12-23 15:48:01 -08:00
Terry Jia
25b9c51237 fix: correct Vue node selection box detection accuracy (#7741)
## Summary
Remove double-counting of NODE_TITLE_HEIGHT in layout sync. The title
height was being added both in useLayoutSync and in LiteGraph's
measure() function, causing boundingRect to be 30px taller than actual
DOM.

This caused selection box to detect nodes ~30px below their actual
bottom.

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

## Screenshots (if applicable)
before


https://github.com/user-attachments/assets/babf0b9d-174e-4ec3-a3bb-18e4f9d88d09

after


https://github.com/user-attachments/assets/4f9989a5-4d3c-4303-bd1b-70ca70cb8f15

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7741-fix-correct-Vue-node-selection-box-detection-accuracy-2d26d73d3650817cbe80f98a3bf1abf1)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-12-23 17:36:06 -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
366 changed files with 9183 additions and 6722 deletions

View File

@@ -0,0 +1,23 @@
name: Start ComfyUI Server
description: 'Start ComfyUI server in a container environment (assumes ComfyUI is pre-installed)'
inputs:
front_end_root:
description: 'Path to frontend dist directory'
required: false
default: '$GITHUB_WORKSPACE/dist'
timeout:
description: 'Timeout in seconds for server startup'
required: false
default: '600'
runs:
using: 'composite'
steps:
- name: Copy devtools and start server
shell: bash
run: |
set -euo pipefail
cp -r ./tools/devtools/* /ComfyUI/custom_nodes/ComfyUI_devtools/
cd /ComfyUI && python3 main.py --cpu --multi-user --front-end-root "${{ inputs.front_end_root }}" &
wait-for-it --service 127.0.0.1:8188 -t ${{ inputs.timeout }}

View File

@@ -15,65 +15,56 @@ concurrency:
jobs:
setup:
runs-on: ubuntu-latest
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
# Setup Test Environment, build frontend but do not start server yet
- name: Setup ComfyUI server
uses: ./.github/actions/setup-comfyui-server
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Setup Playwright
uses: ./.github/actions/setup-playwright # Setup Playwright and cache browsers
# Save the entire workspace as cache for later test jobs to restore
- name: Generate cache key
id: cache-key
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
- name: Save cache
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
# Upload only built dist/ (containerized test jobs will pnpm install without cache)
- name: Upload built frontend
uses: actions/upload-artifact@v4
with:
path: .
key: comfyui-setup-${{ steps.cache-key.outputs.key }}
name: frontend-dist
path: dist/
retention-days: 1
# Sharded chromium tests
playwright-tests-chromium-sharded:
needs: setup
runs-on: ubuntu-latest
timeout-minutes: 60
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.8
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: read
packages: read
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4, 5, 6, 7, 8]
shardTotal: [8]
steps:
# download built frontend repo from setup job
- name: Wait for cache propagation
run: sleep 10
- name: Restore cached setup
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
- name: Checkout repository
uses: actions/checkout@v5
- name: Download built frontend
uses: actions/download-artifact@v4
with:
fail-on-cache-miss: true
path: .
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
name: frontend-dist
path: dist/
# Setup Test Environment for this runner, start server, use cached built frontend ./dist from 'setup' job
- name: Setup ComfyUI server
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
- name: Setup nodejs, pnpm, reuse built frontend
uses: ./.github/actions/setup-frontend
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Start ComfyUI server
uses: ./.github/actions/start-comfyui-server
# Run sharded tests and upload sharded reports
- name: Install frontend deps
run: pnpm install --frozen-lockfile
# Run sharded tests (browsers pre-installed in container)
- name: Run Playwright tests (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
id: playwright
run: pnpm exec playwright test --project=chromium --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --reporter=blob
@@ -93,39 +84,37 @@ jobs:
timeout-minutes: 15
needs: setup
runs-on: ubuntu-latest
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.8
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: read
packages: read
strategy:
fail-fast: false
matrix:
browser: [chromium-2x, chromium-0.5x, mobile-chrome]
steps:
# download built frontend repo from setup job
- name: Wait for cache propagation
run: sleep 10
- name: Restore cached setup
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
- name: Checkout repository
uses: actions/checkout@v5
- name: Download built frontend
uses: actions/download-artifact@v4
with:
fail-on-cache-miss: true
path: .
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
name: frontend-dist
path: dist/
# Setup Test Environment for this runner, start server, use cached built frontend ./dist from 'setup' job
- name: Setup ComfyUI server
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
- name: Setup nodejs, pnpm, reuse built frontend
uses: ./.github/actions/setup-frontend
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Start ComfyUI server
uses: ./.github/actions/start-comfyui-server
# Run tests and upload reports
- name: Install frontend deps
run: pnpm install --frozen-lockfile
# Run tests (browsers pre-installed in container)
- name: Run Playwright tests (${{ matrix.browser }})
id: playwright
run: |
# Run tests with blob reporter first
pnpm exec playwright test --project=${{ matrix.browser }} --reporter=blob
run: pnpm exec playwright test --project=${{ matrix.browser }} --reporter=blob
env:
PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report
@@ -146,7 +135,7 @@ jobs:
path: ./playwright-report/
retention-days: 30
# Merge sharded test reports
# Merge sharded test reports (no container needed - only runs CLI)
merge-reports:
needs: [playwright-tests-chromium-sharded]
runs-on: ubuntu-latest
@@ -155,11 +144,9 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v5
# Setup Test Environment, we only need playwright to merge reports
# Setup pnpm/node to run playwright merge-reports (no browsers needed)
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Download blob reports
uses: actions/download-artifact@v4

View File

@@ -25,7 +25,6 @@ jobs:
) &&
startsWith(github.event.comment.body, '/update-playwright') )
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
pr-number: ${{ steps.pr-info.outputs.pr-number }}
branch: ${{ steps.pr-info.outputs.branch }}
comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
@@ -64,70 +63,63 @@ jobs:
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
# Save expensive build artifacts (Python env, built frontend, node_modules)
# Source code will be checked out fresh in sharded jobs
- name: Generate cache key
id: cache-key
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
- name: Save cache
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
# Upload built dist/ (containerized test jobs will pnpm install without cache)
- name: Upload built frontend
uses: actions/upload-artifact@v4
with:
path: |
ComfyUI
dist
key: comfyui-setup-${{ steps.cache-key.outputs.key }}
name: frontend-dist
path: dist/
retention-days: 1
# Sharded snapshot updates
update-snapshots-sharded:
needs: setup
runs-on: ubuntu-latest
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.8
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: read
packages: read
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
# Checkout source code fresh (not cached)
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ needs.setup.outputs.branch }}
# Restore expensive build artifacts from setup job
- name: Restore cached artifacts
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
- name: Download built frontend
uses: actions/download-artifact@v4
with:
fail-on-cache-miss: true
path: |
ComfyUI
dist
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
name: frontend-dist
path: dist/
- name: Setup ComfyUI server (from cache)
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
- name: Start ComfyUI server
uses: ./.github/actions/start-comfyui-server
- name: Setup nodejs, pnpm, reuse built frontend
uses: ./.github/actions/setup-frontend
- name: Install frontend deps
run: pnpm install --frozen-lockfile
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
# Run sharded tests with snapshot updates
# Run sharded tests with snapshot updates (browsers pre-installed in container)
- name: Update snapshots (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
id: playwright-tests
run: pnpm exec playwright test --update-snapshots --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
continue-on-error: true
# Identify and stage only changed snapshot files
- name: Stage changed snapshot files
id: changed-snapshots
shell: bash
run: |
set -euo pipefail
echo "=========================================="
echo "STAGING CHANGED SNAPSHOTS (Shard ${{ matrix.shardIndex }})"
echo "=========================================="
# Configure git safe.directory for container environment
git config --global --add safe.directory "$(pwd)"
# Get list of changed snapshot files (including untracked/new files)
changed_files=$( (
@@ -136,43 +128,25 @@ jobs:
) | sort -u | grep -E '\-snapshots/' || true )
if [ -z "$changed_files" ]; then
echo "No snapshot changes in this shard"
echo "No snapshot changes in shard ${{ matrix.shardIndex }}"
echo "has-changes=false" >> $GITHUB_OUTPUT
exit 0
fi
echo "✓ Found changed files:"
echo "$changed_files"
file_count=$(echo "$changed_files" | wc -l)
echo "Count: $file_count"
echo "Shard ${{ matrix.shardIndex }}: $file_count changed snapshot(s):"
echo "$changed_files"
echo "has-changes=true" >> $GITHUB_OUTPUT
echo ""
# Create staging directory
# Copy changed files to staging directory
mkdir -p /tmp/changed_snapshots_shard
# Copy only changed files, preserving directory structure
# Strip 'browser_tests/' prefix to avoid double nesting
echo "Copying changed files to staging directory..."
while IFS= read -r file; do
# Skip paths that no longer exist (e.g. deletions)
if [ ! -f "$file" ]; then
echo " → (skipped; not a file) $file"
continue
fi
# Remove 'browser_tests/' prefix
[ -f "$file" ] || continue
file_without_prefix="${file#browser_tests/}"
# Create parent directories
mkdir -p "/tmp/changed_snapshots_shard/$(dirname "$file_without_prefix")"
# Copy file
cp "$file" "/tmp/changed_snapshots_shard/$file_without_prefix"
echo " → $file_without_prefix"
done <<< "$changed_files"
echo ""
echo "Staged files for upload:"
find /tmp/changed_snapshots_shard -type f
# Upload ONLY the changed files from this shard
- name: Upload changed snapshots
uses: actions/upload-artifact@v4
@@ -213,9 +187,15 @@ jobs:
echo "=========================================="
echo "DOWNLOADED SNAPSHOT FILES"
echo "=========================================="
find ./downloaded-snapshots -type f
echo ""
echo "Total files: $(find ./downloaded-snapshots -type f | wc -l)"
if [ -d "./downloaded-snapshots" ]; then
find ./downloaded-snapshots -type f
echo ""
echo "Total files: $(find ./downloaded-snapshots -type f | wc -l)"
else
echo "No snapshot artifacts downloaded (no changes in any shard)"
echo ""
echo "Total files: 0"
fi
# Merge only the changed files into browser_tests
- name: Merge changed snapshots
@@ -226,6 +206,16 @@ jobs:
echo "MERGING CHANGED SNAPSHOTS"
echo "=========================================="
# Check if any artifacts were downloaded
if [ ! -d "./downloaded-snapshots" ]; then
echo "No snapshot artifacts to merge"
echo "=========================================="
echo "MERGE COMPLETE"
echo "=========================================="
echo "Shards merged: 0"
exit 0
fi
# Verify target directory exists
if [ ! -d "browser_tests" ]; then
echo "::error::Target directory 'browser_tests' does not exist"

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.

View File

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

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

@@ -181,6 +181,10 @@ pnpm format
- 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](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

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

View File

@@ -11,29 +11,32 @@
<!-- Apple Metal / NVIDIA -->
<HardwareOption
v-if="platform === 'darwin'"
:image-path="'./assets/images/apple-mps-logo.png'"
image-path="./assets/images/apple-mps-logo.png"
placeholder-text="Apple Metal"
subtitle="Apple Metal"
:value="'mps'"
:selected="selected === 'mps'"
:recommended="true"
@click="pickGpu('mps')"
/>
<HardwareOption
v-else
:image-path="'./assets/images/nvidia-logo-square.jpg'"
placeholder-text="NVIDIA"
:subtitle="$t('install.gpuPicker.nvidiaSubtitle')"
:value="'nvidia'"
:selected="selected === 'nvidia'"
:recommended="true"
@click="pickGpu('nvidia')"
/>
<template v-else>
<HardwareOption
image-path="./assets/images/nvidia-logo-square.jpg"
placeholder-text="NVIDIA"
:subtitle="$t('install.gpuPicker.nvidiaSubtitle')"
:selected="selected === 'nvidia'"
@click="pickGpu('nvidia')"
/>
<HardwareOption
image-path="./assets/images/amd-rocm-logo.png"
placeholder-text="AMD"
:subtitle="$t('install.gpuPicker.amdSubtitle')"
:selected="selected === 'amd'"
@click="pickGpu('amd')"
/>
</template>
<!-- CPU -->
<HardwareOption
placeholder-text="CPU"
:subtitle="$t('install.gpuPicker.cpuSubtitle')"
:value="'cpu'"
:selected="selected === 'cpu'"
@click="pickGpu('cpu')"
/>
@@ -41,7 +44,6 @@
<HardwareOption
placeholder-text="Manual Install"
:subtitle="$t('install.gpuPicker.manualSubtitle')"
:value="'unsupported'"
:selected="selected === 'unsupported'"
@click="pickGpu('unsupported')"
/>
@@ -81,13 +83,15 @@ const selected = defineModel<TorchDeviceType | null>('device', {
const electron = electronAPI()
const platform = electron.getPlatform()
const showRecommendedBadge = computed(
() => selected.value === 'mps' || selected.value === 'nvidia'
const recommendedDevices: TorchDeviceType[] = ['mps', 'nvidia', 'amd']
const showRecommendedBadge = computed(() =>
selected.value ? recommendedDevices.includes(selected.value) : false
)
const descriptionKeys = {
mps: 'appleMetal',
nvidia: 'nvidia',
amd: 'amd',
cpu: 'cpu',
unsupported: 'manual'
} as const
@@ -97,7 +101,7 @@ const descriptionText = computed(() => {
return st(`install.gpuPicker.${key}Description`, '')
})
const pickGpu = (value: TorchDeviceType) => {
function pickGpu(value: TorchDeviceType) {
selected.value = value
}
</script>

View File

@@ -29,7 +29,6 @@ export const AppleMetalSelected: Story = {
imagePath: '/assets/images/apple-mps-logo.png',
placeholderText: 'Apple Metal',
subtitle: 'Apple Metal',
value: 'mps',
selected: true
}
}
@@ -39,7 +38,6 @@ export const AppleMetalUnselected: Story = {
imagePath: '/assets/images/apple-mps-logo.png',
placeholderText: 'Apple Metal',
subtitle: 'Apple Metal',
value: 'mps',
selected: false
}
}
@@ -48,7 +46,6 @@ export const CPUOption: Story = {
args: {
placeholderText: 'CPU',
subtitle: 'Subtitle',
value: 'cpu',
selected: false
}
}
@@ -57,7 +54,6 @@ export const ManualInstall: Story = {
args: {
placeholderText: 'Manual Install',
subtitle: 'Subtitle',
value: 'unsupported',
selected: false
}
}
@@ -67,7 +63,6 @@ export const NvidiaSelected: Story = {
imagePath: '/assets/images/nvidia-logo-square.jpg',
placeholderText: 'NVIDIA',
subtitle: 'NVIDIA',
value: 'nvidia',
selected: true
}
}

View File

@@ -36,17 +36,13 @@
</template>
<script setup lang="ts">
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
import { cn } from '@/utils/tailwindUtil'
interface Props {
imagePath?: string
placeholderText: string
subtitle?: string
value: TorchDeviceType
selected?: boolean
recommended?: boolean
}
defineProps<Props>()

View File

@@ -104,8 +104,8 @@
</template>
<script setup lang="ts">
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
import { TorchMirrorUrl } from '@comfyorg/comfyui-electron-types'
import type { TorchDeviceType } from '@comfyorg/comfyui-electron-types'
import { isInChina } from '@comfyorg/shared-frontend-utils/networkUtil'
import Accordion from 'primevue/accordion'
import AccordionContent from 'primevue/accordioncontent'
@@ -155,7 +155,7 @@ const activeAccordionIndex = ref<string[] | undefined>(undefined)
const electron = electronAPI()
// Mirror configuration logic
const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
function getTorchMirrorItem(device: TorchDeviceType): UVMirror {
const settingId = 'Comfy-Desktop.UV.TorchInstallMirror'
switch (device) {
case 'mps':
@@ -170,6 +170,7 @@ const getTorchMirrorItem = (device: TorchDeviceType): UVMirror => {
mirror: TorchMirrorUrl.Cuda,
fallbackMirror: TorchMirrorUrl.Cuda
}
case 'amd':
case 'cpu':
default:
return {

View File

@@ -63,7 +63,6 @@ const taskStore = useMaintenanceTaskStore()
defineProps<{
displayAsList: string
filter: MaintenanceFilter
isRefreshing: boolean
}>()
const executeTask = async (task: MaintenanceTask) => {

View File

@@ -143,6 +143,8 @@ const goToPreviousStep = () => {
const electron = electronAPI()
const router = useRouter()
const install = async () => {
if (!device.value) return
const options: InstallOptions = {
installPath: installPath.value,
autoUpdate: autoUpdate.value,
@@ -152,7 +154,6 @@ const install = async () => {
pythonMirror: pythonMirror.value,
pypiMirror: pypiMirror.value,
torchMirror: torchMirror.value,
// @ts-expect-error fixme ts strict error
device: device.value
}
electron.installComfyUI(options)
@@ -166,7 +167,11 @@ onMounted(async () => {
if (!electron) return
const detectedGpu = await electron.Config.getDetectedGpu()
if (detectedGpu === 'mps' || detectedGpu === 'nvidia') {
if (
detectedGpu === 'mps' ||
detectedGpu === 'nvidia' ||
detectedGpu === 'amd'
) {
device.value = detectedGpu
}

View File

@@ -74,7 +74,6 @@
class="border-neutral-700 border-solid border-x-0 border-y"
:filter
:display-as-list
:is-refreshing
/>
<!-- Actions -->

View File

@@ -110,16 +110,18 @@ type KeysOfType<T, Match> = {
}[keyof T]
class ConfirmDialog {
private readonly root: Locator
public readonly delete: Locator
public readonly overwrite: Locator
public readonly reject: Locator
public readonly confirm: Locator
constructor(public readonly page: Page) {
this.delete = page.locator('button.p-button[aria-label="Delete"]')
this.overwrite = page.locator('button.p-button[aria-label="Overwrite"]')
this.reject = page.locator('button.p-button[aria-label="Cancel"]')
this.confirm = page.locator('button.p-button[aria-label="Confirm"]')
this.root = page.getByRole('dialog')
this.delete = this.root.getByRole('button', { name: 'Delete' })
this.overwrite = this.root.getByRole('button', { name: 'Overwrite' })
this.reject = this.root.getByRole('button', { name: 'Cancel' })
this.confirm = this.root.getByRole('button', { name: 'Confirm' })
}
async click(locator: KeysOfType<ConfirmDialog, Locator>) {

View File

@@ -30,7 +30,7 @@ export class ComfyNodeSearchFilterSelectionPanel {
async addFilter(filterValue: string, filterType: string) {
await this.selectFilterType(filterType)
await this.selectFilterValue(filterValue)
await this.page.locator('.p-button-label:has-text("Add")').click()
await this.page.locator('button:has-text("Add")').click()
}
}

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[]> {

View File

@@ -85,11 +85,11 @@ test.describe('Missing models warning', () => {
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
const downloadButton = missingModelsWarning.getByLabel('Download')
const downloadButton = missingModelsWarning.getByText('Download')
await expect(downloadButton).toBeVisible()
// Check that the copy URL button is also visible for Desktop environment
const copyUrlButton = missingModelsWarning.getByLabel('Copy URL')
const copyUrlButton = missingModelsWarning.getByText('Copy URL')
await expect(copyUrlButton).toBeVisible()
})
@@ -102,11 +102,11 @@ test.describe('Missing models warning', () => {
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
const downloadButton = missingModelsWarning.getByLabel('Download')
const downloadButton = missingModelsWarning.getByText('Download')
await expect(downloadButton).toBeVisible()
// Check that the copy URL button is also visible for Desktop environment
const copyUrlButton = missingModelsWarning.getByLabel('Copy URL')
const copyUrlButton = missingModelsWarning.getByText('Copy URL')
await expect(copyUrlButton).toBeVisible()
})
@@ -176,7 +176,7 @@ test.describe('Missing models warning', () => {
const missingModelsWarning = comfyPage.page.locator('.comfy-missing-models')
await expect(missingModelsWarning).toBeVisible()
const downloadButton = comfyPage.page.getByLabel('Download')
const downloadButton = comfyPage.page.getByText('Download')
await expect(downloadButton).toBeVisible()
const downloadPromise = comfyPage.page.waitForEvent('download')
await downloadButton.click()
@@ -290,7 +290,7 @@ test.describe('Settings', () => {
// Save keybinding
const saveButton = comfyPage.page
.getByLabel('New Blank Workflow')
.getByLabel('Save')
.getByText('Save')
await saveButton.click()
const request = await requestPromise

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.4 KiB

After

Width:  |  Height:  |  Size: 8.7 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

@@ -123,8 +123,7 @@ test.describe('Node Help', () => {
await expect(helpPage).toContainText('KSampler')
// Click the back button - use a more specific selector
const backButton = comfyPage.page.locator('button:has(.pi-arrow-left)')
await expect(backButton).toBeVisible()
const backButton = helpPage.getByRole('button', { name: /back/i })
await backButton.click()
// Verify that we're back to the node library view
@@ -553,12 +552,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(

View File

@@ -0,0 +1,86 @@
import { expect } from '@playwright/test'
import { comfyPageFixture as test } from '../../fixtures/ComfyPage'
test.describe('Properties panel position', () => {
test.beforeEach(async ({ comfyPage }) => {
// Open a sidebar tab to ensure sidebar is visible
await comfyPage.menu.nodeLibraryTab.open()
await comfyPage.actionbar.propertiesButton.click()
})
test('positions on the right when sidebar is on the left', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Sidebar.Location', 'left')
await comfyPage.nextFrame()
const propertiesPanel = comfyPage.page.getByTestId('properties-panel')
const sidebar = comfyPage.page.locator('.side-bar-panel').first()
await expect(propertiesPanel).toBeVisible()
await expect(sidebar).toBeVisible()
const propsBoundingBox = await propertiesPanel.boundingBox()
const sidebarBoundingBox = await sidebar.boundingBox()
expect(propsBoundingBox).not.toBeNull()
expect(sidebarBoundingBox).not.toBeNull()
// Properties panel should be to the right of the sidebar
expect(propsBoundingBox!.x).toBeGreaterThan(
sidebarBoundingBox!.x + sidebarBoundingBox!.width
)
})
test('positions on the left when sidebar is on the right', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.Sidebar.Location', 'right')
await comfyPage.nextFrame()
const propertiesPanel = comfyPage.page.getByTestId('properties-panel')
const sidebar = comfyPage.page.locator('.side-bar-panel').first()
await expect(propertiesPanel).toBeVisible()
await expect(sidebar).toBeVisible()
const propsBoundingBox = await propertiesPanel.boundingBox()
const sidebarBoundingBox = await sidebar.boundingBox()
expect(propsBoundingBox).not.toBeNull()
expect(sidebarBoundingBox).not.toBeNull()
// Properties panel should be to the left of the sidebar
expect(propsBoundingBox!.x + propsBoundingBox!.width).toBeLessThan(
sidebarBoundingBox!.x
)
})
test('close button icon updates based on sidebar location', async ({
comfyPage
}) => {
const propertiesPanel = comfyPage.page.getByTestId('properties-panel')
// When sidebar is on the left, panel is on the right
await comfyPage.setSetting('Comfy.Sidebar.Location', 'left')
await comfyPage.nextFrame()
await expect(propertiesPanel).toBeVisible()
const closeButtonLeft = propertiesPanel
.locator('button[aria-pressed]')
.locator('i')
await expect(closeButtonLeft).toBeVisible()
await expect(closeButtonLeft).toHaveClass(/lucide--panel-right/)
// When sidebar is on the right, panel is on the left
await comfyPage.setSetting('Comfy.Sidebar.Location', 'right')
await comfyPage.nextFrame()
const closeButtonRight = propertiesPanel
.locator('button[aria-pressed]')
.locator('i')
await expect(closeButtonRight).toBeVisible()
await expect(closeButtonRight).toHaveClass(/lucide--panel-left/)
})
})

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -100,7 +100,7 @@ test.describe('Node library sidebar', () => {
const tab = comfyPage.menu.nodeLibraryTab
await tab.getFolder('foo').click({ button: 'right' })
await comfyPage.page.getByLabel('New Folder').click()
await comfyPage.page.getByRole('menuitem', { name: 'New Folder' }).click()
const textInput = comfyPage.page.locator('.editable-text input')
await textInput.waitFor({ state: 'visible' })
await textInput.fill('bar')
@@ -203,7 +203,7 @@ test.describe('Node library sidebar', () => {
await comfyPage.page
.locator('.color-field .p-selectbutton > *:nth-child(2)')
.click()
await comfyPage.page.getByLabel('Confirm').click()
await comfyPage.page.getByRole('button', { name: 'Confirm' }).click()
await comfyPage.nextFrame()
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization')
@@ -223,7 +223,7 @@ test.describe('Node library sidebar', () => {
await comfyPage.page
.locator('.icon-field .p-selectbutton > *:nth-child(2)')
.click()
await comfyPage.page.getByLabel('Confirm').click()
await comfyPage.page.getByRole('button', { name: 'Confirm' }).click()
await comfyPage.nextFrame()
expect(
await comfyPage.getSetting('Comfy.NodeLibrary.BookmarksCustomization')
@@ -261,7 +261,7 @@ test.describe('Node library sidebar', () => {
await comfyPage.page
.locator('.icon-field .p-selectbutton > *:nth-child(2)')
.click()
await comfyPage.page.getByLabel('Confirm').click()
await comfyPage.page.getByRole('button', { name: 'Confirm' }).click()
await comfyPage.nextFrame()
// Verify the color selection is saved

View File

@@ -109,22 +109,27 @@ 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'
)
await comfyPage.executeCommand('Comfy.BrowseTemplates')
const request = await requestPromise
const dialog = comfyPage.page.getByRole('dialog').filter({
has: comfyPage.page.getByRole('heading', { name: 'Modèles', exact: true })
})
await expect(dialog).toBeVisible()
// Verify French index was requested
expect(request.url()).toContain('templates/index.fr.json')
// Validate that French-localized strings from the templates index are rendered
await expect(
dialog.getByRole('heading', { name: 'Modèles', exact: true })
).toBeVisible()
await expect(
dialog.getByRole('button', { name: 'Tous les modèles', exact: true })
).toBeVisible()
await expect(comfyPage.templates.content).toBeVisible()
// Ensure the English fallback copy is not shown anywhere
await expect(
comfyPage.page.getByText('All Templates', { exact: true })
).toHaveCount(0)
})
test('Falls back to English templates when locale file not found', async ({

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

After

Width:  |  Height:  |  Size: 114 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: 25 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: 61 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: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 62 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: 58 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: 108 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: 108 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 91 KiB

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

@@ -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,7 +4,10 @@ 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'
// 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'
@@ -108,7 +111,8 @@ export default defineConfig([
tseslintConfigs.recommended,
// Difference in typecheck on CI vs Local
pluginVue.configs['flat/recommended'],
eslintPluginPrettierRecommended,
// 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

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.36.3",
"version": "1.37.0",
"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}'",
@@ -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",

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;

938
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,10 @@
packages:
- apps/**
- packages/**
catalog:
'@alloc/quick-lru': ^5.2.0
'@comfyorg/comfyui-electron-types': 0.5.5
'@comfyorg/comfyui-electron-types': 0.6.2
'@eslint/js': ^9.39.1
'@iconify-json/lucide': ^1.1.178
'@iconify/json': ^2.2.380
@@ -11,8 +15,8 @@ catalog:
'@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
'@pinia/testing': ^1.0.3
'@playwright/test': ^1.57.0
'@prettier/plugin-oxc': ^0.1.3
'@primeuix/forms': 0.0.2
'@primeuix/styled': 0.3.2
@@ -22,7 +26,8 @@ catalog:
'@primevue/icons': 4.2.5
'@primevue/themes': ^4.2.5
'@sentry/vite-plugin': ^4.6.0
'@sentry/vue': ^8.48.0
'@sentry/vue': ^10.32.1
'@sparkjsdev/spark': ^0.1.10
'@storybook/addon-docs': ^10.1.9
'@storybook/vue3': ^10.1.9
'@storybook/vue3-vite': ^10.1.9
@@ -34,8 +39,8 @@ catalog:
'@types/semver': ^7.7.0
'@types/three': ^0.169.0
'@vitejs/plugin-vue': ^6.0.0
'@vitest/coverage-v8': ^3.2.4
'@vitest/ui': ^3.2.0
'@vitest/coverage-v8': ^4.0.16
'@vitest/ui': ^4.0.16
'@vue/test-utils': ^2.4.6
'@vueuse/core': ^11.0.0
'@vueuse/integrations': ^13.9.0
@@ -50,16 +55,15 @@ catalog:
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: ^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
globals: ^16.5.0
happy-dom: ^20.0.11
husky: ^9.1.7
jiti: 2.6.1
jsdom: ^26.1.0
jsdom: ^27.4.0
knip: ^5.75.1
lint-staged: ^16.2.7
markdown-table: ^3.0.4
@@ -68,7 +72,7 @@ catalog:
oxlint: ^1.33.0
oxlint-tsgolint: ^0.9.1
picocolors: ^1.1.1
pinia: ^2.1.7
pinia: ^3.0.4
postcss-html: ^1.8.0
prettier: ^3.7.4
pretty-bytes: ^7.1.0
@@ -92,24 +96,27 @@ catalog:
vite-plugin-dts: ^4.5.4
vite-plugin-html: ^3.2.2
vite-plugin-vue-devtools: ^8.0.0
vitest: ^3.2.4
vitest: ^4.0.16
vue: ^3.5.13
vue-component-type-helpers: ^3.0.7
vue-component-type-helpers: ^3.2.1
vue-eslint-parser: ^10.2.0
vue-i18n: ^9.14.3
vue-router: ^4.4.3
vue-tsc: ^3.1.8
vue-tsc: ^3.2.1
vuefire: ^3.2.1
yjs: ^13.6.27
zod: ^3.23.8
zod-to-json-schema: ^3.24.1
zod-validation-error: ^3.3.0
cleanupUnusedCatalogs: true
ignoredBuiltDependencies:
- '@firebase/util'
- protobufjs
- unrs-resolver
- vue-demi
onlyBuiltDependencies:
- '@playwright/browser-chromium'
- '@playwright/browser-firefox'
@@ -119,8 +126,6 @@ onlyBuiltDependencies:
- esbuild
- nx
- oxc-resolver
overrides:
'@types/eslint': '-'
packages:
- apps/**
- packages/**

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -22,29 +22,38 @@
state-storage="local"
@resizestart="onResizestart"
>
<!-- First panel: sidebar when left, properties when right -->
<SplitterPanel
v-if="sidebarLocation === 'left' && !focusMode"
:class="
cn(
'side-bar-panel bg-comfy-menu-bg pointer-events-auto',
sidebarPanelVisible && 'min-w-78'
)
v-if="
!focusMode && (sidebarLocation === 'left' || rightSidePanelVisible)
"
:min-size="10"
:class="
sidebarLocation === 'left'
? cn(
'side-bar-panel bg-comfy-menu-bg pointer-events-auto',
sidebarPanelVisible && 'min-w-78'
)
: 'bg-comfy-menu-bg pointer-events-auto'
"
:min-size="sidebarLocation === 'left' ? 10 : 15"
:size="20"
:style="{
display:
sidebarPanelVisible && sidebarLocation === 'left'
? 'flex'
: 'none'
}"
:style="firstPanelStyle"
:role="sidebarLocation === 'left' ? 'complementary' : undefined"
:aria-label="
sidebarLocation === 'left' ? t('sideToolbar.sidebar') : undefined
"
>
<slot
v-if="sidebarPanelVisible && sidebarLocation === 'left'"
v-if="sidebarLocation === 'left' && sidebarPanelVisible"
name="side-bar-panel"
/>
<slot
v-else-if="sidebarLocation === 'right'"
name="right-side-panel"
/>
</SplitterPanel>
<!-- Main panel (always present) -->
<SplitterPanel :size="80" class="flex flex-col">
<slot name="topmenu" :sidebar-panel-visible />
@@ -73,38 +82,33 @@
</Splitter>
</SplitterPanel>
<!-- Last panel: properties when left, sidebar when right -->
<SplitterPanel
v-if="sidebarLocation === 'right' && !focusMode"
:class="
cn(
'side-bar-panel pointer-events-auto',
sidebarPanelVisible && 'min-w-78'
)
v-if="
!focusMode && (sidebarLocation === 'right' || rightSidePanelVisible)
"
:min-size="10"
:class="
sidebarLocation === 'right'
? cn(
'side-bar-panel bg-comfy-menu-bg pointer-events-auto',
sidebarPanelVisible && 'min-w-78'
)
: 'bg-comfy-menu-bg pointer-events-auto'
"
:min-size="sidebarLocation === 'right' ? 10 : 15"
:size="20"
:style="{
display:
sidebarPanelVisible && sidebarLocation === 'right'
? 'flex'
: 'none'
}"
:style="lastPanelStyle"
:role="sidebarLocation === 'right' ? 'complementary' : undefined"
:aria-label="
sidebarLocation === 'right' ? t('sideToolbar.sidebar') : undefined
"
>
<slot v-if="sidebarLocation === 'left'" name="right-side-panel" />
<slot
v-if="sidebarPanelVisible && sidebarLocation === 'right'"
v-else-if="sidebarLocation === 'right' && sidebarPanelVisible"
name="side-bar-panel"
/>
</SplitterPanel>
<!-- Right Side Panel - independent of sidebar -->
<SplitterPanel
v-if="rightSidePanelVisible && !focusMode"
class="bg-comfy-menu-bg pointer-events-auto"
:min-size="15"
:size="20"
>
<slot name="right-side-panel" />
</SplitterPanel>
</Splitter>
</div>
</div>
@@ -117,6 +121,7 @@ import Splitter from 'primevue/splitter'
import type { SplitterResizeStartEvent } from 'primevue/splitter'
import SplitterPanel from 'primevue/splitterpanel'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
@@ -128,6 +133,7 @@ const workspaceStore = useWorkspaceStore()
const settingStore = useSettingStore()
const rightSidePanelStore = useRightSidePanelStore()
const sidebarTabStore = useSidebarTabStore()
const { t } = useI18n()
const sidebarLocation = computed<'left' | 'right'>(() =>
settingStore.get('Comfy.Sidebar.Location')
)
@@ -159,12 +165,25 @@ function onResizestart({ originalEvent: event }: SplitterResizeStartEvent) {
}
/*
* Force refresh the splitter when right panel visibility changes to recalculate the width
* Force refresh the splitter when right panel visibility or sidebar location changes
* to recalculate the width and panel order
*/
const splitterRefreshKey = computed(() => {
return rightSidePanelVisible.value
? 'main-splitter-with-right-panel'
: 'main-splitter'
return `main-splitter${rightSidePanelVisible.value ? '-with-right-panel' : ''}-${sidebarLocation.value}`
})
const firstPanelStyle = computed(() => {
if (sidebarLocation.value === 'left') {
return { display: sidebarPanelVisible.value ? 'flex' : 'none' }
}
return undefined
})
const lastPanelStyle = computed(() => {
if (sidebarLocation.value === 'right') {
return { display: sidebarPanelVisible.value ? 'flex' : 'none' }
}
return undefined
})
</script>

View File

@@ -5,23 +5,23 @@
>
<Button
v-tooltip="{ value: $t('menu.showMenu'), showDelay: 300 }"
icon="pi pi-bars"
severity="secondary"
text
size="large"
variant="muted-textonly"
size="lg"
:aria-label="$t('menu.showMenu')"
aria-live="assertive"
@click="exitFocusMode"
@contextmenu="showNativeSystemMenu"
/>
>
<i class="pi pi-bars" />
</Button>
<div class="window-actions-spacer" />
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { watchEffect } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import { app } from '@/scripts/app'
import { useWorkspaceStore } from '@/stores/workspaceStore'

View File

@@ -12,7 +12,7 @@
<div class="mx-1 flex flex-col items-end gap-1">
<div class="flex items-center gap-2">
<div
v-if="managerState.shouldShowManagerButtons.value && isDesktop"
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"
>
<Button

View File

@@ -22,12 +22,13 @@
value: item.tooltip,
showDelay: 600
}"
:label="String(item.label ?? '')"
:icon="item.icon"
:severity="item.key === queueMode ? 'primary' : 'secondary'"
size="small"
text
/>
:variant="item.key === queueMode ? 'primary' : 'secondary'"
size="sm"
class="w-full justify-start"
>
<i v-if="item.icon" :class="item.icon" />
{{ String(item.label ?? '') }}
</Button>
</template>
</SplitButton>
<BatchCountEdit />
@@ -36,12 +37,12 @@
<script setup lang="ts">
import { storeToRefs } from 'pinia'
import Button from 'primevue/button'
import type { MenuItem } from 'primevue/menuitem'
import SplitButton from 'primevue/splitbutton'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { app } from '@/scripts/app'

View File

@@ -46,21 +46,22 @@
<div class="flex items-center gap-2">
<Button
v-if="isShortcutsTabActive"
:label="$t('shortcuts.manageShortcuts')"
icon="pi pi-cog"
severity="secondary"
size="small"
text
variant="muted-textonly"
size="sm"
@click="openKeybindingSettings"
/>
>
<i class="pi pi-cog" />
{{ $t('shortcuts.manageShortcuts') }}
</Button>
<Button
class="justify-self-end"
icon="pi pi-times"
severity="secondary"
size="small"
text
variant="muted-textonly"
size="sm"
:aria-label="t('g.close')"
@click="closeBottomPanel"
/>
>
<i class="pi pi-times" />
</Button>
</div>
</div>
</TabList>
@@ -79,7 +80,6 @@
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Tab from 'primevue/tab'
import type { TabPassThroughMethodOptions } from 'primevue/tab'
import TabList from 'primevue/tablist'
@@ -88,6 +88,7 @@ import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import ExtensionSlot from '@/components/common/ExtensionSlot.vue'
import Button from '@/components/ui/button/Button.vue'
import { useDialogService } from '@/services/dialogService'
import { useBottomPanelStore } from '@/stores/workspace/bottomPanelStore'
import type { BottomPanelExtension } from '@/types/extensionTypes'

View File

@@ -11,9 +11,8 @@
value: tooltipText,
showDelay: 300
}"
icon="pi pi-copy"
severity="secondary"
size="small"
variant="secondary"
size="sm"
:class="
cn('absolute top-2 right-8 transition-opacity', {
'opacity-0 pointer-events-none select-none': !isHovered
@@ -21,18 +20,20 @@
"
:aria-label="tooltipText"
@click="handleCopy"
/>
>
<i class="pi pi-copy" />
</Button>
</div>
</template>
<script setup lang="ts">
import { useElementHover, useEventListener } from '@vueuse/core'
import type { IDisposable } from '@xterm/xterm'
import Button from 'primevue/button'
import type { Ref } from 'vue'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { useTerminal } from '@/composables/bottomPanelTabs/useTerminal'
import { electronAPI, isElectron } from '@/utils/envUtil'
import { cn } from '@/utils/tailwindUtil'

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

@@ -7,20 +7,24 @@
/>
<Button
v-tooltip="$t('g.upload')"
:icon="isUploading ? 'pi pi-spin pi-spinner' : 'pi pi-upload'"
size="small"
variant="secondary"
size="sm"
:aria-label="$t('g.upload')"
:disabled="isUploading"
@click="triggerFileInput"
/>
>
<i :class="isUploading ? 'pi pi-spin pi-spinner' : 'pi pi-upload'" />
</Button>
<Button
v-tooltip="$t('g.clear')"
outlined
icon="pi pi-trash"
severity="danger"
size="small"
variant="destructive"
size="sm"
:aria-label="$t('g.clear')"
:disabled="!modelValue"
@click="clearImage"
/>
>
<i class="pi pi-trash" />
</Button>
<input
ref="fileInput"
type="file"
@@ -32,10 +36,10 @@
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import InputText from 'primevue/inputtext'
import { ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { api } from '@/scripts/api'

View File

@@ -27,24 +27,19 @@
</div>
</div>
<template #footer>
<Button
:label="$t('g.reset')"
icon="pi pi-refresh"
class="p-button-text"
@click="resetCustomization"
/>
<Button
:label="$t('g.confirm')"
icon="pi pi-check"
autofocus
@click="confirmCustomization"
/>
<Button variant="textonly" @click="resetCustomization">
<i class="pi pi-refresh" />
{{ $t('g.reset') }}
</Button>
<Button autofocus @click="confirmCustomization">
<i class="pi pi-check" />
{{ $t('g.confirm') }}
</Button>
</template>
</Dialog>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Dialog from 'primevue/dialog'
import Divider from 'primevue/divider'
import SelectButton from 'primevue/selectbutton'
@@ -52,6 +47,7 @@ import { computed, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import ColorCustomizationSelector from '@/components/common/ColorCustomizationSelector.vue'
import Button from '@/components/ui/button/Button.vue'
import { useNodeBookmarkStore } from '@/stores/nodeBookmarkStore'
const { t } = useI18n()

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

@@ -16,20 +16,22 @@
<Button
v-if="status === null || status === 'error'"
class="file-action-button"
:label="$t('g.download') + ' (' + fileSize + ')'"
size="small"
outlined
variant="secondary"
size="sm"
:disabled="!!props.error"
icon="pi pi-download"
@click="triggerDownload"
/>
>
<i class="pi pi-download" />
{{ $t('g.download') + ' (' + fileSize + ')' }}
</Button>
<Button
v-if="(status === null || status === 'error') && !!props.url"
:label="$t('g.copyURL')"
size="small"
outlined
variant="secondary"
size="sm"
@click="copyURL"
/>
>
{{ $t('g.copyURL') }}
</Button>
</div>
</div>
<div
@@ -49,44 +51,48 @@
v-if="status === 'in_progress'"
v-tooltip.top="t('electronFileDownload.pause')"
class="file-action-button"
size="small"
outlined
variant="secondary"
size="sm"
:disabled="!!props.error"
icon="pi pi-pause-circle"
@click="triggerPauseDownload"
/>
>
<i class="pi pi-pause-circle" />
</Button>
<Button
v-if="status === 'paused'"
v-tooltip.top="t('electronFileDownload.resume')"
class="file-action-button"
size="small"
outlined
variant="secondary"
size="sm"
:aria-label="t('electronFileDownload.resume')"
:disabled="!!props.error"
icon="pi pi-play-circle"
@click="triggerResumeDownload"
/>
>
<i class="pi pi-play-circle" />
</Button>
<Button
v-tooltip.top="t('electronFileDownload.cancel')"
class="file-action-button"
size="small"
outlined
variant="destructive"
size="sm"
:aria-label="t('electronFileDownload.cancel')"
:disabled="!!props.error"
icon="pi pi-times-circle"
severity="danger"
@click="triggerCancelDownload"
/>
>
<i class="pi pi-times-circle" />
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import ProgressBar from 'primevue/progressbar'
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { useDownload } from '@/composables/useDownload'
import { useElectronDownloadStore } from '@/stores/electronDownloadStore'

View File

@@ -22,31 +22,27 @@
</div>
<div>
<Button
:label="$t('g.download') + ' (' + fileSize + ')'"
size="small"
outlined
variant="secondary"
:disabled="!!props.error"
:title="props.url"
@click="download.triggerBrowserDownload"
/>
>
{{ $t('g.download') + ' (' + fileSize + ')' }}
</Button>
</div>
<div>
<Button
:label="$t('g.copyURL')"
size="small"
outlined
:disabled="!!props.error"
@click="copyURL"
/>
<Button variant="secondary" :disabled="!!props.error" @click="copyURL">
{{ $t('g.copyURL') }}
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Message from 'primevue/message'
import { computed } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { useDownload } from '@/composables/useDownload'
import { formatSize } from '@/utils/formatUtil'

View File

@@ -14,21 +14,20 @@
</div>
<div class="flex flex-col gap-2">
<Button
icon="pi pi-upload"
:label="$t('g.upload')"
size="small"
@click="triggerFileInput"
/>
<Button size="sm" @click="triggerFileInput">
<i class="pi pi-upload" />
{{ $t('g.upload') }}
</Button>
<Button
v-if="modelValue"
class="w-full"
outlined
icon="pi pi-trash"
severity="danger"
size="small"
variant="destructive"
size="sm"
:aria-label="$t('g.delete')"
@click="clearImage"
/>
>
<i class="pi pi-trash" />
</Button>
</div>
</div>
<input
@@ -42,9 +41,10 @@
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
defineProps<{
modelValue: string
}>()

View File

@@ -10,10 +10,11 @@
</p>
<Button
v-if="buttonLabel"
:label="buttonLabel"
class="p-button-text"
variant="textonly"
@click="$emit('action')"
/>
>
{{ buttonLabel }}
</Button>
</div>
</template>
</Card>
@@ -21,9 +22,10 @@
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Card from 'primevue/card'
import Button from '@/components/ui/button/Button.vue'
const props = defineProps<{
class?: string
icon?: string

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>
@@ -382,7 +383,6 @@ import ProgressSpinner from 'primevue/progressspinner'
import { computed, onBeforeUnmount, onMounted, provide, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
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'

View File

@@ -11,24 +11,25 @@
<ApiNodesList :node-names="apiNodeNames" />
<div class="flex items-center justify-between">
<Button :label="t('g.learnMore')" link @click="handleLearnMoreClick" />
<Button variant="textonly" @click="handleLearnMoreClick">
{{ t('g.learnMore') }}
</Button>
<div class="flex gap-2">
<Button
:label="t('g.cancel')"
outlined
severity="secondary"
@click="onCancel?.()"
/>
<Button :label="t('g.login')" @click="onLogin?.()" />
<Button variant="secondary" @click="onCancel?.()">
{{ t('g.cancel') }}
</Button>
<Button @click="onLogin?.()">
{{ t('g.login') }}
</Button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { useExternalLink } from '@/composables/useExternalLink'
const { t } = useI18n()

View File

@@ -31,69 +31,64 @@
}}</label>
</div>
<Button
:label="$t('g.cancel')"
icon="pi pi-undo"
severity="secondary"
autofocus
@click="onCancel"
/>
<Button
v-if="type === 'default'"
:label="$t('g.confirm')"
severity="primary"
icon="pi pi-check"
@click="onConfirm"
/>
<Button variant="secondary" autofocus @click="onCancel">
<i class="pi pi-undo" />
{{ $t('g.cancel') }}
</Button>
<Button v-if="type === 'default'" variant="primary" @click="onConfirm">
<i class="pi pi-check" />
{{ $t('g.confirm') }}
</Button>
<Button
v-else-if="type === 'delete'"
:label="$t('g.delete')"
severity="danger"
icon="pi pi-trash"
variant="destructive"
@click="onConfirm"
/>
>
<i class="pi pi-trash" />
{{ $t('g.delete') }}
</Button>
<Button
v-else-if="type === 'overwrite' || type === 'overwriteBlueprint'"
:label="$t('g.overwrite')"
severity="warn"
icon="pi pi-save"
variant="destructive"
@click="onConfirm"
/>
>
<i class="pi pi-save" />
{{ $t('g.overwrite') }}
</Button>
<template v-else-if="type === 'dirtyClose'">
<Button
:label="$t('g.no')"
severity="secondary"
icon="pi pi-times"
@click="onDeny"
/>
<Button :label="$t('g.save')" icon="pi pi-save" @click="onConfirm" />
<Button variant="secondary" @click="onDeny">
<i class="pi pi-times" />
{{ $t('g.no') }}
</Button>
<Button @click="onConfirm">
<i class="pi pi-save" />
{{ $t('g.save') }}
</Button>
</template>
<Button
v-else-if="type === 'reinstall'"
:label="$t('desktopMenu.reinstall')"
severity="warn"
icon="pi pi-eraser"
variant="destructive"
@click="onConfirm"
/>
>
<i class="pi pi-eraser" />
{{ $t('desktopMenu.reinstall') }}
</Button>
<!-- Invalid - just show a close button. -->
<Button
v-else
:label="$t('g.close')"
severity="primary"
icon="pi pi-times"
@click="onCancel"
/>
<Button v-else variant="primary" @click="onCancel">
<i class="pi pi-times" />
{{ $t('g.close') }}
</Button>
</div>
</section>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Checkbox from 'primevue/checkbox'
import Message from 'primevue/message'
import { ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { ConfirmationDialogType } from '@/services/dialogService'
import { useDialogStore } from '@/stores/dialogStore'

View File

@@ -14,18 +14,16 @@
</template>
<div class="flex justify-center gap-2">
<Button v-show="!reportOpen" variant="textonly" @click="showReport">
{{ $t('g.showReport') }}
</Button>
<Button
v-show="!reportOpen"
text
:label="$t('g.showReport')"
@click="showReport"
/>
<Button
v-show="!reportOpen"
text
:label="$t('issueReport.helpFix')"
variant="textonly"
@click="showContactSupport"
/>
>
{{ $t('issueReport.helpFix') }}
</Button>
</div>
<template v-if="reportOpen">
<Divider />
@@ -40,18 +38,15 @@
:repo-owner="repoOwner"
:repo-name="repoName"
/>
<Button
v-if="reportOpen"
:label="$t('g.copyToClipboard')"
icon="pi pi-copy"
@click="copyReportToClipboard"
/>
<Button v-if="reportOpen" @click="copyReportToClipboard">
<i class="pi pi-copy" />
{{ $t('g.copyToClipboard') }}
</Button>
</div>
</div>
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import ScrollPanel from 'primevue/scrollpanel'
import { useToast } from 'primevue/usetoast'
@@ -60,6 +55,7 @@ import { useI18n } from 'vue-i18n'
import NoResultsPlaceholder from '@/components/common/NoResultsPlaceholder.vue'
import FindIssueButton from '@/components/dialog/content/error/FindIssueButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { useCopyToClipboard } from '@/composables/useCopyToClipboard'
import { useTelemetry } from '@/platform/telemetry'
import { api } from '@/scripts/api'

View File

@@ -4,20 +4,17 @@
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>
<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>
@@ -50,7 +47,6 @@
import { computed, nextTick, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import IconTextButton from '@/components/button/IconTextButton.vue'
import Button from '@/components/ui/button/Button.vue'
import { isCloud } from '@/platform/distribution/types'
import { useToastStore } from '@/platform/updates/common/toastStore'

View File

@@ -18,11 +18,11 @@
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import FloatLabel from 'primevue/floatlabel'
import InputText from 'primevue/inputtext'
import { ref } from 'vue'
import Button from '@/components/ui/button/Button.vue'
import { useDialogStore } from '@/stores/dialogStore'
const props = defineProps<{

View File

@@ -51,8 +51,7 @@
<Button
type="button"
class="h-10"
severity="secondary"
outlined
variant="secondary"
@click="signInWithGoogle"
>
<i class="pi pi-google mr-2"></i>
@@ -66,8 +65,7 @@
<Button
type="button"
class="h-10"
severity="secondary"
outlined
variant="secondary"
@click="signInWithGithub"
>
<i class="pi pi-github mr-2"></i>
@@ -82,8 +80,7 @@
<Button
type="button"
class="h-10"
severity="secondary"
outlined
variant="secondary"
@click="showApiKeyForm = true"
>
<img
@@ -142,12 +139,12 @@
</template>
<script setup lang="ts">
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import Message from 'primevue/message'
import { computed, onMounted, onUnmounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { getComfyPlatformBaseUrl } from '@/config/comfyApi'
import {

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">
@@ -59,98 +58,40 @@
<Button
:disabled="!selectedCredits || loading"
:loading="loading"
severity="primary"
:label="$t('credits.topUp.buy')"
:class="['w-full', { 'opacity-30': !selectedCredits || loading }]"
:pt="{ label: { class: 'text-primary-foreground' } }"
variant="primary"
:class="cn('w-full', (!selectedCredits || loading) && 'opacity-30')"
@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>
>
{{ $t('credits.topUp.buy') }}
</Button>
</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 Button from '@/components/ui/button/Button.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 { cn } from '@/utils/tailwindUtil'
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 +143,4 @@ const handleBuy = async () => {
loading.value = false
}
}
const handleSeeDetails = async () => {
await authActions.accessBillingPortal()
}
</script>

View File

@@ -7,12 +7,9 @@
<PasswordFields />
<!-- Submit Button -->
<Button
type="submit"
:label="$t('userSettings.updatePassword')"
class="mt-4 h-10 font-medium"
:loading="loading"
/>
<Button type="submit" class="mt-4 h-10 font-medium" :loading="loading">
{{ $t('userSettings.updatePassword') }}
</Button>
</Form>
</template>
@@ -20,10 +17,10 @@
import type { FormSubmitEvent } from '@primevue/forms'
import { Form } from '@primevue/forms'
import { zodResolver } from '@primevue/forms/resolvers/zod'
import Button from 'primevue/button'
import { ref } from 'vue'
import PasswordFields from '@/components/dialog/content/signin/PasswordFields.vue'
import Button from '@/components/ui/button/Button.vue'
import { useFirebaseAuthActions } from '@/composables/auth/useFirebaseAuthActions'
import { updatePasswordSchema } from '@/schemas/signInSchema'

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>

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