Compare commits

..

84 Commits

Author SHA1 Message Date
GitHub Action
5a71450b85 [automated] Apply ESLint and Prettier fixes 2025-12-01 22:10:56 +00:00
coderabbitai[bot]
6031e6ccf2 📝 Add docstrings to drjkl/byom-2
Docstrings generation was requested by @guill.

* https://github.com/Comfy-Org/ComfyUI_frontend/pull/6969#issuecomment-3583609253

The following files were modified:

* `src/components/dialog/confirm/confirmDialog.ts`
* `src/composables/useFeatureFlags.ts`
* `src/platform/assets/services/assetService.ts`
* `src/services/dialogService.ts`
* `src/stores/dialogStore.ts`
2025-12-01 22:08:29 +00:00
DrJKL
4417b0d907 Update flag name 2025-12-01 13:40:51 -08:00
DrJKL
ff988cd6f1 cleanup: More consistent asset name 2025-12-01 13:39:14 -08:00
DrJKL
38cc93b8f6 Grammar is hard 2025-12-01 13:35:24 -08:00
DrJKL
23d8ccd394 UX: Gate the buttons behind a feature flag 2025-12-01 13:31:42 -08:00
DrJKL
23d01f0b34 UX: Handle failed update 2025-12-01 13:24:24 -08:00
DrJKL
64a7955eca UX: Add progress and confirmation to deletion 2025-12-01 13:09:08 -08:00
DrJKL
a5bd37ef56 Properly resolve MaybeRef 2025-12-01 12:00:00 -08:00
Alexander Brown
ca539a6fd0 Update src/platform/assets/components/AssetBadgeGroup.vue
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2025-12-01 11:50:44 -08:00
DrJKL
585e47f2df We should remove that reexport and standardize the import 2025-12-01 11:48:17 -08:00
DrJKL
aa13e4c385 i18n: internationalize deletion confirmation 2025-12-01 11:48:17 -08:00
DrJKL
57ed0afde6 nits: Typo and unused ref 2025-12-01 11:48:17 -08:00
DrJKL
3a18d27b9d WIP: Real API calls 2025-12-01 11:48:16 -08:00
DrJKL
f9e8dfdbe7 style: Hide details button for now 2025-12-01 11:48:16 -08:00
DrJKL
ed68b085cc style: Make sure extra properties hug the bottom of the card. 2025-12-01 11:48:15 -08:00
DrJKL
bb9475fed6 Cleanup: Remove dark-theme variants in buttonTypes. Really need to migrate this to CVA and setup the right types. 2025-12-01 11:48:14 -08:00
DrJKL
ba43494d32 WIP: Deleting assets, UI only, also not wired up 2025-12-01 11:48:14 -08:00
DrJKL
df82698f1e type: Add component prop typesafety to dialogs 2025-12-01 11:48:13 -08:00
DrJKL
d4b993b16d style: Fix card padding 2025-12-01 11:48:13 -08:00
DrJKL
04ab767649 WIP: Renaming Assets, UI, not hooked up to backend. 2025-12-01 11:48:12 -08:00
DrJKL
0b7d0f1d35 style: non-functional buttons and menu 2025-12-01 11:48:11 -08:00
DrJKL
0dfe36f1f8 style: Align card styles with Designs 2025-12-01 11:48:08 -08:00
Alexander Piskun
2c437acff6 feat(api-nodes-pricing): add prices for Kling O1 video model (#7077)
## Summary

Add price badges for the upcoming API nodes.

## Screenshots (if applicable)

<img width="1242" height="1094" alt="Screenshot From 2025-12-01
14-03-22"
src="https://github.com/user-attachments/assets/8f7909ea-f629-4dde-a075-ca31a9fff2ac"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7077-feat-api-nodes-pricing-add-prices-for-Kling-O1-video-model-2bc6d73d36508152843ff96196317ff8)
by [Unito](https://www.unito.io)
2025-12-01 11:02:34 -08:00
Comfy Org PR Bot
b97b21add0 1.34.3 (#7074)
Patch version increment to 1.34.3

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7074-1-34-3-2bc6d73d365081e9be00e6c1438142fd)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-11-30 23:48:38 -07:00
Christian Byrne
dfe098897a [fix] Prevent drag activation during Vue node resize (#7064)
## Summary
- Add isResizingVueNodes flag to layoutStore
- Set flag in useNodeResize during resize operation
- Check flag in useNodePointerInteractions to skip drag activation

Fixes a bug where resizing a Vue node after selecting it would
incorrectly trigger drag logic, causing the node to teleport.


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

---------

Co-authored-by: DrJKL <DrJKL0424@gmail.com>
2025-11-30 20:23:30 -08:00
Christian Byrne
d76c59cb14 [refactor] Simplify Vue node resize to bottom-right corner only (#7063)
## Summary

Temporarily simplifies the resize logic to only work on bottom right
corner and eliminates edge cases where corner resizing caused position
drift issues.

- Remove multi-corner resize handles in favor of bottom-right only
- Delete resizeMath.ts and its tests (no longer needed)
- Simplify useNodeResize to only handle bottom-right resize
- Remove position tracking from resize callback

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-11-30 18:27:37 -08:00
Christian Byrne
c298d8a870 [fix] Weekly release workflow adjustments (#7060)
Follow-up adjustments to the weekly ComfyUI release automation workflow.

## Changes

1. **Rename workflow to follow conventions**
   - File: `weekly-comfyui-release.yaml` → `release-weekly-comfyui.yaml`
   - Name: "Weekly ComfyUI Release" → "Release: Weekly ComfyUI"
   - Matches pattern of other `release-*` workflows

2. **Sync fork with upstream before creating PR**
   - Fetches latest upstream/master before making changes
   - Ensures PR only shows requirements.txt diff, not stale fork commits
- Does not modify fork's master branch (only pushes automation branch)

## Testing

After merge, can test via manual workflow dispatch in Actions tab.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7060-fix-Weekly-release-workflow-adjustments-2bb6d73d365081008436d1b9e5f7dd65)
by [Unito](https://www.unito.io)
2025-11-30 18:27:04 -08:00
niknah
b50b34ac95 Expose LGraphNode.getSlotPosition (#7042)
Before node v2, you could use getInputPos, getOutputPos. In node v2,
that is not possible.

## Summary

Want to get the position of the output / inputs on the graph like before
node v2.

## Changes

- **What**: <!-- Core functionality added/modified --> Added
LGraphNode.getSlotPosition

## Review Focus


## Screenshots (if applicable)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7042-Expose-LGraphNode-getSlotPosition-2ba6d73d36508195a0fed2d1fe2b64f6)
by [Unito](https://www.unito.io)
2025-11-30 12:55:16 -07:00
Christian Byrne
7a6cc39c39 [feat] Add weekly ComfyUI release automation (#6877)
Adds scheduled workflow to bump ComfyUI frontend RC releases every
Monday at noon PST.

## Implementation

- **Resolver script** (`scripts/cicd/resolve-comfyui-release.ts`):
Checks ComfyUI `requirements.txt` and determines next minor version,
compares branch commits to latest patch tag
- **Workflow** (`.github/workflows/weekly-comfyui-release.yaml`): 
  - Scheduled for Monday 20:00 UTC (noon PST)
  - Manual dispatch supported for testing/off-cycle runs
- Three jobs: resolve version → trigger release if needed → create
ComfyUI PR
- Reuses existing `release-version-bump.yaml` workflow
- Creates draft PR from fork to `comfyanonymous/ComfyUI` with updated
`requirements.txt`
- Includes error handling and validation for all steps
- Force pushes to same branch weekly to maintain single open PR

## Testing

- Resolver script tested locally: correctly identified v1.28.8 → v1.29.4
with release needed
- yamllint passes
- knip passes with `gh` binary added to ignore list

## Follow-up

Can test via workflow_dispatch once merged, or in this PR if we enable
Actions on branch.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6877-feat-Add-weekly-ComfyUI-release-automation-2b46d73d36508154aa05c783c6942d9a)
by [Unito](https://www.unito.io)
2025-11-30 12:45:47 -07:00
Christian Byrne
2b751200be test: add setting to ignore version compatibility toast warnings in e2e tests (#7004)
There's a warning toast shown if the frontend is considered out-of-date
(relative to the version in the requirements.txt of
comfyanonymous/ComfyUI). As a result, e2e tests run on older release
branches (e.g., when backporting or hotfixing) can sometimes trigger the
warning which obviously causes visual regression tests to fail. This PR
adds a hidden setting to disable the warning and sets it to `true` in
the e2e test fixtures.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7004-test-add-setting-to-ignore-version-compatibility-toast-warnings-in-e2e-tests-2b86d73d3650812d9e07f54a0c86b996)
by [Unito](https://www.unito.io)
2025-11-29 19:56:19 -07:00
Alexander Brown
5b03d3fcbc Minor/Lint: Fix two warnings (#7045)
## Summary

Unnecessary import of a compiler macro and importing the default instead
of the named export.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7045-Minor-Lint-Fix-two-warnings-2bb6d73d365081758369d14bafcd7aa0)
by [Unito](https://www.unito.io)
2025-11-29 19:15:06 -07:00
Christian Byrne
1caf3fdb0c remove temporary markdown file (#7048)
Deletes a file accidentally committed when rebasing and doing `git add
.`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7048-remove-temporary-markdown-file-2bb6d73d36508139b95ef3c7e4ab0798)
by [Unito](https://www.unito.io)
2025-11-29 19:13:37 -07:00
Christian Byrne
f39faf6b8e mark vue nodes menu toggle with beta tag (#7047)
## Summary

Adds a "Beta" tag next to the Nodes 2.0 menu item toggle to better
indicate to users, especially new users, that the feature is in beta.

<img width="752" height="918" alt="Selection_2477"
src="https://github.com/user-attachments/assets/cf341b2b-1af6-4259-8449-bf157989b7b3"
/>

<img width="433" height="880" alt="Selection_2476"
src="https://github.com/user-attachments/assets/a0f658bf-1904-4d79-9ca7-c81c458adc54"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7047-mark-vue-nodes-menu-toggle-with-beta-tag-2bb6d73d3650815e8a9aceaa9250c02e)
by [Unito](https://www.unito.io)
2025-11-29 18:33:57 -07:00
Christian Byrne
3ee921119d fix: loader node widget value shows placeholder instead of filename on cloud (#7005)
Fixes issue on cloud where the Loader node dropdowns showed placeholder
values (after refresh) in the widget UI despite a value being loaded.

Cloud loader dropdowns hydrate via `useAssetWidgetData(nodeType)`, so
`dropdownItems` stays empty until the Asset API returns friendly
filenames. Meanwhile `modelValue` already holds the saved asset and the
watcher only tracks `modelValue`:
df653d6ce1/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue (L215-L226)

This watcher runs before assets load, fails to find a match, clears
`selectedSet`, and the placeholder persists:


df653d6ce1/src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue (L222-L224)

Once the assets API resolves, `dropdownItems` recomputes but nothing
resyncs because the watcher never sees that change. Desktop doesn’t hit
this because it still reads from `widget.options.values` immediately.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7005-fix-loader-node-widget-value-shows-placeholder-instead-of-filename-on-cloud-2b86d73d36508125a16de5c9e366b014)
by [Unito](https://www.unito.io)
2025-11-29 18:13:32 -07:00
Christian Byrne
f61bfe666e style: move border and shadow style to outer container to fix topbar badges shadow/border (#7044)
## Summary

Moves the shadow and border from workflow tabs to outer container so
that there is no longer an inconsistency with the badges and topbar
bottom border and shadows.

## Changes

| Before | After |
|
---------------------------------------------------------------------------------------------------------------------------------------------
|
---------------------------------------------------------------------------------------------------------------------------------------------
|
| <img width="1522" height="598" alt="Selection_2473"
src="https://github.com/user-attachments/assets/92aa602c-a018-4b3d-8606-9c00552ffc20"
/> | <img width="1522" height="598" alt="Selection_2472"
src="https://github.com/user-attachments/assets/13c75da3-a938-442a-9bf6-9c30d822a610"
/> |
| <img width="1522" height="598" alt="Selection_2474"
src="https://github.com/user-attachments/assets/e84efd2a-6ae1-44a6-9901-0f902e942b64"
/> | <img width="1522" height="598" alt="Selection_2475"
src="https://github.com/user-attachments/assets/66d4852e-eed7-49f9-8f0f-9e8bbcdcb0b8"
/> |

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7044-style-move-border-and-shadow-style-to-outer-container-to-fix-topbar-badges-shadow-border-2ba6d73d365081b991a3f981db87f434)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-11-29 17:52:03 -07:00
Christian Byrne
d8795ec997 fix: actionbar can get stuck behind floating menu and not be moved with drag handle (#7034)
Fixes issue where actionbar (and therefore "Run" button) can get stuck
behind the floating menus and not be physically moved (especially on
mobile) leading to an inability to press "Run" button.

`z1010` and `z1000` do not seem to be actual classes in our system.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7034-fix-actionbar-can-get-stuck-behind-floating-menu-and-not-be-moved-with-drag-handle-2b96d73d365081fc87d1ec4446a50e43)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2025-11-30 00:22:08 +00:00
Alexander Brown
d3aa8dfc88 A11y/style: Make properties panel use theme colors. Not comprehensive. (#7036)
## Summary

Addressing the theme aspect of #6976 

## Changes

- **What**: Not comprehensive, but swaps many of the colors in the
litegraph panel styles to reference theme tokens.

## Screenshots (if applicable)
<img width="730" height="637" alt="image"
src="https://github.com/user-attachments/assets/1f8b0c0f-f957-487e-96b5-324ed2a6a717"
/>
<img width="721" height="719" alt="image"
src="https://github.com/user-attachments/assets/37f098a3-39f6-4cae-8bcb-601121d106f5"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7036-A11y-style-Make-properties-panel-use-theme-colors-Not-comprehensive-2ba6d73d365081038481c66be68a599a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-11-29 16:06:29 -08:00
Christian Byrne
2cce0fe611 style: make feedback button icon-only after mobile breakpoint (#7043)
## Summary

On mobile breakpoint, change the "Feedback" button to only show an icon,
preserving crucial space on the breadcrumb axis.

## Changes

| Before | After |
|
---------------------------------------------------------------------------------------------------------------------------------------------
|
---------------------------------------------------------------------------------------------------------------------------------------------
|
| <img width="732" height="1458" alt="Selection_2471"
src="https://github.com/user-attachments/assets/8491a755-5817-4021-a2bd-8dff09a8345f"
/> | <img width="732" height="1458" alt="Selection_2470"
src="https://github.com/user-attachments/assets/6229badb-46c4-4617-9984-5636ad4b803c"
/> |

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7043-style-make-feedback-button-icon-only-after-mobile-breakpoint-2ba6d73d365081489d9adc89eb5ec42b)
by [Unito](https://www.unito.io)
2025-11-29 15:23:48 -07:00
Johnpaul Chiwetelu
b9cb335255 [fix] Add fraction digits props to number input widget (#7033)
Add min-fraction-digits and max-fraction-digits props to InputNumber
component to allow typing decimal values. Without these props, users
could not manually type values like "0.8" even though the increment
buttons worked correctly.

This aligns the input-only variant with the slider variant which
already had these props configured.

fix https://github.com/Comfy-Org/ComfyUI_frontend/issues/7016
## Screenshots (if applicable)



https://github.com/user-attachments/assets/fa205023-13c4-45ac-874b-b241808cf56d



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7033-fix-Add-fraction-digits-props-to-number-input-widget-2b96d73d36508124ac65e466802bbb0c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-11-29 15:10:04 -07:00
Christian Byrne
2f89eb070c when on cloud, link to cloud version of "getting started" page for "docs" button (#7041)
Changes docs button in bottom left help center to open
https://docs.comfy.org/get_started/cloud when on cloud (as opposed to
https://docs.comfy.org/).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7041-when-on-cloud-link-to-cloud-version-of-getting-started-page-for-docs-button-2ba6d73d365081b79bcaf91cfeb3dffd)
by [Unito](https://www.unito.io)
2025-11-29 13:11:53 -07:00
Benjamin Lu
d94e0720f3 Handle HTML fallbacks for node help and skip blueprint docs (#7021)
explicitly prevents subgraphs from making an api call since they don't
have docs, this was previously reliant on a non-ok resolution

also doesn't try returning anything that has contenttype of text/html to
prevent the markdown renderer from crashing

## Summary
- short-circuit blueprint/subgraph nodes in help: skip doc fetch and
return the node description, avoiding SPA fallback responses
- guard node help fetch against HTML/SPA fallbacks using content-type
checks; treat them as missing and trigger the existing description
fallback
- keep base URL logic unchanged for non-blueprint nodes

## Testing
- pnpm typecheck
- pnpm lint:fix
- pnpm test:unit
2025-11-28 10:09:09 -08:00
Alexander Piskun
fe47a487bb feat(api-nodes-pricing): add prices for ByteDance seedance-1-0-pro-fast model (#7026)
## Summary

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7026-feat-api-nodes-pricing-add-prices-for-ByteDance-seedance-1-0-pro-fast-model-2b96d73d365081eeb7f7de1dc892389b)
by [Unito](https://www.unito.io)
2025-11-28 10:38:58 -07:00
Benjamin Lu
63f68543e4 [feat] Show "Finished in" duration for completed jobs in cloud (#6895)
## Summary
In cloud distribution, completed jobs now show "Finished in Xh Ym Zs" as
the primary text instead of the filename.

- Uses `formatDuration` to display time as `1h 30m 45s`, `30m 45s`, or
`45s`
- Gated with `isCloud` - non-cloud continues to show filename
- Added i18n key `queue.completedIn` for localization

Filename is not fetchable right now in cloud. This is what design wanted
as the alternative.

<img width="679" height="1097" alt="image"
src="https://github.com/user-attachments/assets/291deb42-77d8-4de9-b4f8-ee65f3c25011"
/>

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-27 16:41:45 -07:00
Johnpaul Chiwetelu
9806ba807a [fix] Re-encode cloud-subscription video to VP9 for Safari compatibility (#7006)
The original video was encoded with AV1 codec which Safari does not
support, causing "Plug-in handled load" errors. Re-encoded to VP9
which is supported across all major browsers including Safari 14.1+.

 Summary

- Re-encode cloud-subscription.webm from AV1 to VP9 codec for Safari
browser compatibility

  Problem

Safari does not support AV1 codec in webm containers, causing the cloud
subscription video to
  fail with "Plug-in handled load" error.

  Solution

Re-encoded the video using VP9 codec (libvpx-vp9) which is supported by
all major browsers
  including Safari 14.1+.

  Test plan

  - Verify video plays correctly in Safari
  - Verify video plays correctly in Chrome/Firefox/Edge


[cloud-subscription.webm](https://github.com/user-attachments/assets/ab2dcaa1-f3b3-47c9-84d0-9fc8ea8c6f17)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7006-fix-Re-encode-cloud-subscription-video-to-VP9-for-Safari-compatibility-2b86d73d365081ad9262f97fc1ebb5ee)
by [Unito](https://www.unito.io)
2025-11-27 16:41:00 -07:00
Simula_r
7433f470fc Fix/vue nodes banner text (#7007)
## Summary

ensure banner text is white

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7007-Fix-vue-nodes-banner-text-2b86d73d365081e1ab60c7013cc9d37c)
by [Unito](https://www.unito.io)
2025-11-27 16:40:27 -07:00
AustinMroz
490bb22bd3 Remove app.graph usage from widgetInput code (#7008)
Resolves #7000

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7008-Remove-app-graph-usage-from-widgetInput-code-2b86d73d3650819eb1bcc98130f18cbe)
by [Unito](https://www.unito.io)
2025-11-27 16:39:53 -07:00
Comfy Org PR Bot
895775c319 1.34.2 (#7009)
Patch version increment to 1.34.2

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7009-1-34-2-2b86d73d36508119ba75d4b9834d8786)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-11-27 16:38:31 -07:00
JonatanAtila
e248ecfa4e feat(i18n): add Portuguese (Brazil) locale (pt-BR) (#6943)
Finalmente o idioma em Portugues do Brasil verá a luz do dia (se tudo
correr bem).

What has been done:

- Added pt-BR to .i18nrc.cjs and settings
- Included loaders in src/i18n.ts and apps/desktop-ui/src/i18n.ts
- Now Portuguese (BR) is displayed in the language selector
- Created empty main.json, commands.json, settings.json and
nodeDefs.json files to be populated by CI

- Checklist: the language appears in the dropdown list, selection occurs
without errors, the fallback to English, in case technical terms have no
translation, is working correctly.

- I will maintain the pt-br translation and review for as long as
necessary.

---------

Co-authored-by: Comfy Contributor <dev@example.com>
2025-11-27 15:46:27 -07:00
Kelly Yang
923695ffde fix(ui): update button style for mask editor (#6991)
## Summary

Updated the styling of the `resetToDefault` button to match the visual
design and UX behavior (hover states, dimensions) of other buttons in
the `TopBarHeader`

## Changes

Applied the standard top bar button CSS classes to the resetToDefault
button to ensure visual consistency. @jtydhr88


## Review Focus

Please verify that the button alignment and hover effects now match the
adjacent UI elements.

## Screenshots
<img width="2746" height="2300" alt="ScreenShot_2025-11-27_010713_634"
src="https://github.com/user-attachments/assets/758e108f-7c38-4b51-a236-fee2e049186e"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6991-fix-ui-update-button-style-for-mask-editor-2b86d73d3650813ea3cbd28203239cd8)
by [Unito](https://www.unito.io)
2025-11-27 15:43:55 -07:00
Comfy Org PR Bot
df653d6ce1 1.34.1 (#6986)
Patch version increment to 1.34.1

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6986-1-34-1-2b86d73d365081f0b515e6392ac90be8)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-11-27 13:07:03 -07:00
Alexander Piskun
28dc9314d2 feat(api-nodes-pricing): add prices for Kling-v2-5-turbo for node KlingStartEndFrame (#6996)
## Summary

Pricing for new model for this node

## Screenshots (if applicable)

<img width="1174" height="422" alt="Screenshot From 2025-11-27 14-44-38"
src="https://github.com/user-attachments/assets/78edb4d2-3aef-427c-98d5-ea9b6b18b203"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6996-feat-api-nodes-pricing-add-prices-for-Kling-v2-5-turbo-for-node-KlingStartEndFrame-2b86d73d36508171ba2cd186248228b7)
by [Unito](https://www.unito.io)
2025-11-27 13:06:44 -07:00
Johnpaul Chiwetelu
3acf9aae0a fix: add filter for combo widgets (#6999)
This pull request introduces a minor enhancement to the
`WidgetSelectDefault.vue` component. The change conditionally enables
the filtering feature in the select dropdown when there are more than
four options, improving usability for larger lists.


<img width="932" height="622" alt="Screenshot 2025-11-27 160239"
src="https://github.com/user-attachments/assets/76f6080f-13c7-4fa9-a813-2799af15363e"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6999-fix-add-filter-for-combo-widgets-2b86d73d365081f994e5caf78e64a196)
by [Unito](https://www.unito.io)
2025-11-27 13:06:22 -07:00
Alexander Brown
a055241e2e Change node-component-header-surface color variable (#6990)
## Summary

Light mode node header color update.
2025-11-27 16:15:59 +01:00
Jin Yi
9d131a4267 [feat] Add right-click context menu to MediaAssetCard (#6844)
## Summary
- Add right-click context menu functionality to MediaAssetCard
- Separate context menu into its own component
(MediaAssetContextMenu.vue)
- Ensure only one context menu is visible at a time within the same tab

## Changes
- Add `MediaAssetContextMenu.vue` - new component for context menu
- Update `MediaAssetCard.vue` - show context menu on right-click and
more button click
- Delete `MediaAssetMoreMenu.vue` - consolidated into context menu
- Delete `MediaAssetButtonDivider.vue` - unused
- Update `AssetsSidebarTab.vue` - add context menu state management
- Refactor `useMediaAssetActions.ts`

## Screenshot

[screen-capture.webm](https://github.com/user-attachments/assets/6fe414ef-b134-4fbe-98aa-6437bb354b41)

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

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-11-26 23:02:32 -07:00
Christian Byrne
c57ceaf826 fix: add missing translations (#6970)
## Summary

Adds translations for things identified as "always English" during QA
testing.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6970-fix-add-missing-translations-2b86d73d365081ffa58ccef156d28028)
by [Unito](https://www.unito.io)
2025-11-26 18:25:49 -07:00
Christian Byrne
29dbfa3f60 fix: Vue Node <-> Litegraph node height offset normalization (#6966)
## Summary

Changes the layout store to treat node sizes as body-only measurements
while LiteGraph continues to reason about full heights. DOM-driven
updates are tagged with `LayoutSource.DOM`, which lets the store strip
the title height exactly once before persisting. That classification (a
new mutation source - `LayoutSource.DOM`) is accurate because those
mutations are triggered by the browser’s layout engine via
ResizeObserver, rather than by direct calls into the layout APIs (e.g.,
`moveNodeTo`, `useNodeDrag`). So all sources are:

- `LayoutSource.DOM`: browser layout/ResizeObserver measurements that
include the title bar
- `LayoutSource.Vue`: direct Vue-driven mutations routed through the
layout store
- `LayoutSource.Canvas`: legacy LiteGraph/canvas updates that will be
phased out over time
- `LayoutSource.External`: for multiplayer or syncing with a when going
online after making changes offline (in teams/workspace)

When layout state flows back into LiteGraph we add the title height just
in time for `liteNode.setSize`, so LiteGraph’s rendering stays
unchanged. This makes Vue node resizing and workflow persistence
deterministic - multiline widgets hold their dimensions across reloads
because every path that crosses the layout/LiteGraph boundary performs
the same normalization.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6966-normalize-height-at-sites-2b76d73d365081b6bcb4f4ce6a27663a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-11-26 17:37:24 -07:00
Christian Byrne
83f04490ba fix: don't use registry when only checking for presence of missing nodes (#6965)
Changes the client-side validation of whether a graph has missing nodes
to use a simpler check (is the node name in the node defs from
`/object_info`), rather than using the Comfy Node Registry API
(api.comfy.org). The Registry API is still needed to get full
metadata/info about missing nodes, but that can be deferred until the
user actually needs that info.

This also fixes an issue where a node you coded yourself locally which
is neither a core node nor a node in the registry would be considered
"missing."

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6965-fix-don-t-use-registry-when-only-checking-for-presence-of-missing-nodes-2b76d73d365081e7b8fbcb3f2a1c4502)
by [Unito](https://www.unito.io)
2025-11-26 17:21:16 -07:00
Christian Byrne
c5fe617347 feat: open template via URL in linear mode (#6945)
## Summary

Adds `mode` as a valid url query param when opening template from URL.



https://github.com/user-attachments/assets/8e7efb1c-d842-4953-822a-f3cea7ddecb6

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6945-feat-open-template-via-URL-in-linear-mode-2b76d73d365081d8ad4af3d49a68a4ff)
by [Unito](https://www.unito.io)
2025-11-26 16:44:28 -07:00
Christian Byrne
8b2c1fc45d allow telemetry events to be disabled by name in feature flags (#6946)
## Summary

Adds ability to toggle events on/off by name from dynamic feature flags.
Also makes some events disabled by default (not being used for any
analysis and taking too much of event quota currently).

Note: telemetry is only enabled on cloud - this doesn't affect local
users in any way.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6946-allow-telemetry-events-to-be-disabled-by-name-in-feature-flags-2b76d73d365081ea8676cfbb8217e640)
by [Unito](https://www.unito.io)
2025-11-26 14:20:21 -07:00
Alexander Brown
df66a96976 Feat: Double Click on Image Assets to open the lightbox (#6955)
## Summary

Make it easier to open the lightbox preview.

## Review Focus

The first click still selects the image. I liked the suggestion to
require shift or ctrl to initiate selection and have click go back to
the previous behavior.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6955-Feat-Double-Click-on-Image-Assets-to-open-the-lightbox-2b76d73d365081f0bf0de847add5c820)
by [Unito](https://www.unito.io)
2025-11-26 11:52:54 -08:00
Alexander Brown
96d12330bb Fix: Toolbox position desync (#6962)
## Summary

Toggle the toolbox position check based on visibility, not just when
selection changes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6962-Fix-Toolbox-position-desync-2b76d73d3650818da327c7adee6c1098)
by [Unito](https://www.unito.io)
2025-11-26 19:43:58 +00:00
Simula_r
4b87b1fdc5 fix: remove LOD from vue nodes (#6950)
## Summary

Refactor to remove LOD from vue nodes. Also, hide Litegraph LOD settings
in vue nodes to prevent confusion / stale setting. Keep litegraph LOD
the exact same.

## Changes

- **What**: settings, all LOD related code in vue nodes

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

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-11-26 12:26:05 -07:00
Comfy Org PR Bot
08b256c29d 1.34.0 (#6961)
Minor version increment to 1.34.0

**Base branch:** `main`

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

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2025-11-26 12:24:25 -07:00
Alexander Brown
e6332046b0 BYOM: Model Import Wizard (#6949)
## Summary

Design alignment for the model import wizard.

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

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

Add mobile breakpoint to vue nodes banner

## Changes

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

## Screenshots (if applicable)

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

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

**Base branch:** `main`

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

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

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

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

## Screenshots (if applicable)

OSS version stays the same:

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

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

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

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

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

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

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

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

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

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

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

## Design Spec

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

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

---------

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

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

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

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

## Changes

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

## Review Focus

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

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

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

Less padding, looks nice.

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

---------

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

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

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

---------

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

## Summary

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

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

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

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


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

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

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

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-11-24 12:23:29 -08:00
191 changed files with 6179 additions and 4511 deletions

View File

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

View File

@@ -236,8 +236,8 @@ jobs:
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
else
PR_TITLE="${{ github.event.pull_request.title }}"
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
PR_TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH")
MERGE_COMMIT=$(jq -r '.pull_request.merge_commit_sha' "$GITHUB_EVENT_PATH")
fi
for target in ${{ steps.filter-targets.outputs.pending-targets }}; do
@@ -326,8 +326,8 @@ jobs:
PR_TITLE=$(echo "$PR_DATA" | jq -r '.title')
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
else
PR_TITLE="${{ github.event.pull_request.title }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
PR_TITLE=$(jq -r '.pull_request.title' "$GITHUB_EVENT_PATH")
PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
fi
for backport in ${{ steps.backport.outputs.success }}; do
@@ -364,9 +364,9 @@ jobs:
PR_AUTHOR=$(echo "$PR_DATA" | jq -r '.author.login')
MERGE_COMMIT=$(echo "$PR_DATA" | jq -r '.mergeCommit.oid')
else
PR_NUMBER="${{ github.event.pull_request.number }}"
PR_AUTHOR="${{ github.event.pull_request.user.login }}"
MERGE_COMMIT="${{ github.event.pull_request.merge_commit_sha }}"
PR_NUMBER=$(jq -r '.pull_request.number' "$GITHUB_EVENT_PATH")
PR_AUTHOR=$(jq -r '.pull_request.user.login' "$GITHUB_EVENT_PATH")
MERGE_COMMIT=$(jq -r '.pull_request.merge_commit_sha' "$GITHUB_EVENT_PATH")
fi
for failure in ${{ steps.backport.outputs.failed }}; do

View File

@@ -0,0 +1,292 @@
# Automated weekly workflow to bump ComfyUI frontend RC releases
name: "Release: Weekly ComfyUI"
on:
# Schedule for Monday at 12:00 PM PST (20:00 UTC)
schedule:
- cron: '0 20 * * 1'
# Allow manual triggering
workflow_dispatch:
inputs:
comfyui_fork:
description: 'ComfyUI fork to use for PR (e.g., Comfy-Org/ComfyUI)'
required: false
default: 'Comfy-Org/ComfyUI'
type: string
jobs:
resolve-version:
runs-on: ubuntu-latest
outputs:
current_version: ${{ steps.resolve.outputs.current_version }}
target_version: ${{ steps.resolve.outputs.target_version }}
target_minor: ${{ steps.resolve.outputs.target_minor }}
target_branch: ${{ steps.resolve.outputs.target_branch }}
needs_release: ${{ steps.resolve.outputs.needs_release }}
diff_url: ${{ steps.resolve.outputs.diff_url }}
latest_patch_tag: ${{ steps.resolve.outputs.latest_patch_tag }}
steps:
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v5
with:
fetch-depth: 0
path: frontend
- name: Checkout ComfyUI (sparse)
uses: actions/checkout@v5
with:
repository: comfyanonymous/ComfyUI
sparse-checkout: |
requirements.txt
path: comfyui
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
working-directory: frontend
run: pnpm install --frozen-lockfile
- name: Resolve release information
id: resolve
working-directory: frontend
run: |
set -euo pipefail
# Run the resolver script
if ! RESULT=$(tsx scripts/cicd/resolve-comfyui-release.ts ../comfyui .); then
echo "Failed to resolve release information"
exit 1
fi
echo "Resolver output:"
echo "$RESULT"
# Validate JSON output
if ! echo "$RESULT" | jq empty 2>/dev/null; then
echo "Invalid JSON output from resolver"
exit 1
fi
# Parse JSON output and set outputs
echo "current_version=$(echo "$RESULT" | jq -r '.current_version')" >> $GITHUB_OUTPUT
echo "target_version=$(echo "$RESULT" | jq -r '.target_version')" >> $GITHUB_OUTPUT
echo "target_minor=$(echo "$RESULT" | jq -r '.target_minor')" >> $GITHUB_OUTPUT
echo "target_branch=$(echo "$RESULT" | jq -r '.target_branch')" >> $GITHUB_OUTPUT
echo "needs_release=$(echo "$RESULT" | jq -r '.needs_release')" >> $GITHUB_OUTPUT
echo "diff_url=$(echo "$RESULT" | jq -r '.diff_url')" >> $GITHUB_OUTPUT
echo "latest_patch_tag=$(echo "$RESULT" | jq -r '.latest_patch_tag')" >> $GITHUB_OUTPUT
- name: Summary
run: |
echo "## Release Information" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Current version: ${{ steps.resolve.outputs.current_version }}" >> $GITHUB_STEP_SUMMARY
echo "- Target version: ${{ steps.resolve.outputs.target_version }}" >> $GITHUB_STEP_SUMMARY
echo "- Target branch: ${{ steps.resolve.outputs.target_branch }}" >> $GITHUB_STEP_SUMMARY
echo "- Needs release: ${{ steps.resolve.outputs.needs_release }}" >> $GITHUB_STEP_SUMMARY
echo "- Diff: [${{ steps.resolve.outputs.current_version }}...${{ steps.resolve.outputs.target_version }}](${{ steps.resolve.outputs.diff_url }})" >> $GITHUB_STEP_SUMMARY
trigger-release-if-needed:
needs: resolve-version
if: needs.resolve-version.outputs.needs_release == 'true'
runs-on: ubuntu-latest
steps:
- name: Trigger release workflow
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
run: |
set -euo pipefail
echo "Triggering release workflow for branch ${{ needs.resolve-version.outputs.target_branch }}"
# Trigger the release-version-bump workflow
if ! gh workflow run release-version-bump.yaml \
--repo Comfy-Org/ComfyUI_frontend \
--ref main \
--field version_type=patch \
--field branch=${{ needs.resolve-version.outputs.target_branch }}; then
echo "Failed to trigger release workflow"
exit 1
fi
echo "Release workflow triggered successfully for ${{ needs.resolve-version.outputs.target_branch }}"
- name: Summary
run: |
echo "## Release Workflow Triggered" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Branch: ${{ needs.resolve-version.outputs.target_branch }}" >> $GITHUB_STEP_SUMMARY
echo "- Target version: ${{ needs.resolve-version.outputs.target_version }}" >> $GITHUB_STEP_SUMMARY
echo "- [View workflow runs](https://github.com/Comfy-Org/ComfyUI_frontend/actions/workflows/release-version-bump.yaml)" >> $GITHUB_STEP_SUMMARY
create-comfyui-pr:
needs: [resolve-version, trigger-release-if-needed]
if: always() && needs.resolve-version.result == 'success'
runs-on: ubuntu-latest
steps:
- name: Checkout ComfyUI fork
uses: actions/checkout@v5
with:
repository: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }}
token: ${{ secrets.PR_GH_TOKEN }}
path: comfyui
- name: Sync with upstream
working-directory: comfyui
run: |
set -euo pipefail
# Fetch latest upstream to base our branch on fresh code
# Note: This only affects the local checkout, NOT the fork's master branch
# We only push the automation branch, leaving the fork's master untouched
echo "Fetching upstream master..."
if ! git fetch https://github.com/comfyanonymous/ComfyUI.git master; then
echo "Failed to fetch upstream master"
exit 1
fi
echo "Checking out upstream master..."
if ! git checkout FETCH_HEAD; then
echo "Failed to checkout FETCH_HEAD"
exit 1
fi
echo "Successfully synced with upstream master"
- name: Update requirements.txt
working-directory: comfyui
run: |
set -euo pipefail
TARGET_VERSION="${{ needs.resolve-version.outputs.target_version }}"
echo "Updating comfyui-frontend-package to ${TARGET_VERSION}"
# Update the comfyui-frontend-package version (POSIX-compatible)
sed -i.bak "s/comfyui-frontend-package==[0-9.][0-9.]*/comfyui-frontend-package==${TARGET_VERSION}/" requirements.txt
rm requirements.txt.bak
# Verify the change was made
if ! grep -q "comfyui-frontend-package==${TARGET_VERSION}" requirements.txt; then
echo "Failed to update requirements.txt"
exit 1
fi
echo "Updated requirements.txt:"
grep comfyui-frontend-package requirements.txt
- name: Build PR description
id: pr-body
run: |
BODY=$(cat <<'EOF'
Bumps frontend to ${{ needs.resolve-version.outputs.target_version }}
Test quickly:
```bash
python main.py --front-end-version Comfy-Org/ComfyUI_frontend@${{ needs.resolve-version.outputs.target_version }}
```
- Diff: [v${{ needs.resolve-version.outputs.current_version }}...v${{ needs.resolve-version.outputs.target_version }}](${{ needs.resolve-version.outputs.diff_url }})
- PyPI: https://pypi.org/project/comfyui-frontend-package/${{ needs.resolve-version.outputs.target_version }}/
- npm: https://www.npmjs.com/package/@comfyorg/comfyui-frontend-types/v/${{ needs.resolve-version.outputs.target_version }}
EOF
)
# Add release PR note if release was triggered
if [ "${{ needs.resolve-version.outputs.needs_release }}" = "true" ]; then
RELEASE_NOTE="⚠️ **Release PR must be merged first** - check [release workflow runs](https://github.com/Comfy-Org/ComfyUI_frontend/actions/workflows/release-version-bump.yaml)"
BODY=$''"${RELEASE_NOTE}"$'\n\n'"${BODY}"
fi
# Save to file for later use
printf '%s\n' "$BODY" > pr-body.txt
cat pr-body.txt
- name: Create PR to ComfyUI
working-directory: comfyui
env:
GH_TOKEN: ${{ secrets.PR_GH_TOKEN }}
COMFYUI_FORK: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }}
run: |
set -euo pipefail
# Extract fork owner from repository name
FORK_OWNER=$(echo "$COMFYUI_FORK" | cut -d'/' -f1)
echo "Creating PR from ${COMFYUI_FORK} to comfyanonymous/ComfyUI"
# Configure git
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
# Create/update branch (reuse same branch name each week)
BRANCH="automation/comfyui-frontend-bump"
git checkout -B "$BRANCH"
git add requirements.txt
if ! git diff --cached --quiet; then
git commit -m "Bump comfyui-frontend-package to ${{ needs.resolve-version.outputs.target_version }}"
else
echo "No changes to commit"
exit 0
fi
# Force push to fork (overwrites previous week's branch)
# Note: This intentionally destroys branch history to maintain a single PR
# Any review comments or manual commits will need to be re-applied
if ! git push -f origin "$BRANCH"; then
echo "Failed to push branch to fork"
exit 1
fi
# Create draft PR from fork to upstream
PR_BODY=$(cat ../pr-body.txt)
# Try to create PR, ignore error if it already exists
if ! gh pr create \
--repo comfyanonymous/ComfyUI \
--head "${FORK_OWNER}:${BRANCH}" \
--base master \
--title "Bump comfyui-frontend-package to ${{ needs.resolve-version.outputs.target_version }}" \
--body "$PR_BODY" \
--draft 2>&1; then
# Check if PR already exists
set +e
EXISTING_PR=$(gh pr list --repo comfyanonymous/ComfyUI --head "${FORK_OWNER}:${BRANCH}" --json number --jq '.[0].number' 2>&1)
PR_LIST_EXIT=$?
set -e
if [ $PR_LIST_EXIT -ne 0 ]; then
echo "Failed to check for existing PR: $EXISTING_PR"
exit 1
fi
if [ -n "$EXISTING_PR" ] && [ "$EXISTING_PR" != "null" ]; then
echo "PR already exists (#${EXISTING_PR}), updating branch will update the PR"
else
echo "Failed to create PR and no existing PR found"
exit 1
fi
fi
- name: Summary
run: |
echo "## ComfyUI PR Created" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Draft PR created in comfyanonymous/ComfyUI" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### PR Body:" >> $GITHUB_STEP_SUMMARY
cat pr-body.txt >> $GITHUB_STEP_SUMMARY

View File

@@ -9,7 +9,7 @@ module.exports = defineConfig({
entry: 'src/locales/en',
entryLocale: 'en',
output: 'src/locales',
outputLocales: ['zh', 'zh-TW', 'ru', 'ja', 'ko', 'fr', 'es', 'ar', 'tr'],
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.
'latent' is the short form of 'latent space'.
'mask' is in the context of image processing.

View File

@@ -38,6 +38,7 @@
"typescript/no-redundant-type-constituents": "off",
"typescript/restrict-template-expressions": "off",
"typescript/unbound-method": "off",
"typescript/no-floating-promises": "error"
"typescript/no-floating-promises": "error",
"vue/no-import-compiler-macros": "error"
}
}

View File

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

View File

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

View File

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

View File

@@ -59,7 +59,8 @@ const LOCALES = [
['fr', 'Français'],
['es', 'Español'],
['ar', 'عربي'],
['tr', 'Türkçe']
['tr', 'Türkçe'],
['pt-BR', 'Português (BR)']
] as const satisfies ReadonlyArray<[string, string]>
type SupportedLocale = (typeof LOCALES)[number][0]

View File

@@ -40,7 +40,8 @@ const localeLoaders: Record<
ru: () => import('@frontend-locales/ru/main.json'),
tr: () => import('@frontend-locales/tr/main.json'),
zh: () => import('@frontend-locales/zh/main.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/main.json')
'zh-TW': () => import('@frontend-locales/zh-TW/main.json'),
'pt-BR': () => import('@frontend-locales/pt-BR/main.json')
}
const nodeDefsLoaders: Record<
@@ -55,7 +56,8 @@ const nodeDefsLoaders: Record<
ru: () => import('@frontend-locales/ru/nodeDefs.json'),
tr: () => import('@frontend-locales/tr/nodeDefs.json'),
zh: () => import('@frontend-locales/zh/nodeDefs.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/nodeDefs.json')
'zh-TW': () => import('@frontend-locales/zh-TW/nodeDefs.json'),
'pt-BR': () => import('@frontend-locales/pt-BR/nodeDefs.json')
}
const commandsLoaders: Record<
@@ -70,7 +72,8 @@ const commandsLoaders: Record<
ru: () => import('@frontend-locales/ru/commands.json'),
tr: () => import('@frontend-locales/tr/commands.json'),
zh: () => import('@frontend-locales/zh/commands.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/commands.json')
'zh-TW': () => import('@frontend-locales/zh-TW/commands.json'),
'pt-BR': () => import('@frontend-locales/pt-BR/commands.json')
}
const settingsLoaders: Record<
@@ -85,7 +88,8 @@ const settingsLoaders: Record<
ru: () => import('@frontend-locales/ru/settings.json'),
tr: () => import('@frontend-locales/tr/settings.json'),
zh: () => import('@frontend-locales/zh/settings.json'),
'zh-TW': () => import('@frontend-locales/zh-TW/settings.json')
'zh-TW': () => import('@frontend-locales/zh-TW/settings.json'),
'pt-BR': () => import('@frontend-locales/pt-BR/settings.json')
}
// Track which locales have been loaded

View File

@@ -1651,7 +1651,10 @@ export const comfyPageFixture = base.extend<{
// Set tutorial completed to true to avoid loading the tutorial workflow.
'Comfy.TutorialCompleted': true,
'Comfy.SnapToGrid.GridSize': testComfySnapToGridGridSize,
'Comfy.VueNodes.AutoScaleLayout': false
'Comfy.VueNodes.AutoScaleLayout': false,
// Disable toast warning about version compatibility, as they may or
// may not appear - depending on upstream ComfyUI dependencies
'Comfy.VersionCompatibility.DisableWarnings': true
})
} catch (e) {
console.error(e)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 103 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 106 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 109 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 38 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 142 KiB

View File

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 102 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 81 KiB

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,7 @@ const config: KnipConfig = {
project: ['src/**/*.{js,ts}']
}
},
ignoreBinaries: ['python3'],
ignoreBinaries: ['python3', 'gh'],
ignoreDependencies: [
// Weird importmap things
'@iconify/json',

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.33.8",
"version": "1.34.3",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -164,7 +164,6 @@
"es-toolkit": "^1.39.9",
"extendable-media-recorder": "^9.2.27",
"extendable-media-recorder-wav-encoder": "^7.0.129",
"fast-glob": "^3.3.3",
"firebase": "catalog:",
"fuse.js": "^7.0.0",
"glob": "^11.0.3",

View File

@@ -194,7 +194,7 @@
--node-component-executing: var(--color-blue-500);
--node-component-header: var(--fg-color);
--node-component-header-icon: var(--color-ash-800);
--node-component-header-surface: var(--color-white);
--node-component-header-surface: var(--color-smoke-400);
--node-component-outline: var(--color-black);
--node-component-ring: rgb(from var(--color-smoke-500) r g b / 50%);
--node-component-slot-dot-outline-opacity-mult: 1;
@@ -1190,24 +1190,19 @@ dialog::backdrop {
.litegraph.litecontextmenu,
.litegraph.litecontextmenu.dark {
z-index: 9999 !important;
background-color: var(--comfy-menu-bg) !important;
background-color: var(--comfy-menu-bg);
}
.litegraph.litecontextmenu
.litemenu-entry:hover:not(.disabled):not(.separator) {
background-color: var(--comfy-menu-hover-bg, var(--border-color)) !important;
color: var(--fg-color);
}
.litegraph.litecontextmenu .litemenu-entry.submenu,
.litegraph.litecontextmenu.dark .litemenu-entry.submenu {
background-color: var(--comfy-menu-bg) !important;
color: var(--input-text);
background-color: var(--comfy-menu-bg);
color: var(--fg-color);
}
.litegraph.litecontextmenu input {
background-color: var(--comfy-input-bg) !important;
color: var(--input-text) !important;
background-color: var(--comfy-input-bg);
color: var(--input-text);
}
.comfy-context-menu-filter {
@@ -1248,14 +1243,14 @@ dialog::backdrop {
.litegraph.litesearchbox {
z-index: 9999 !important;
background-color: var(--comfy-menu-bg) !important;
background-color: var(--comfy-menu-bg);
overflow: hidden;
display: block;
}
.litegraph.litesearchbox input,
.litegraph.litesearchbox select {
background-color: var(--comfy-input-bg) !important;
background-color: var(--comfy-input-bg);
color: var(--input-text);
}
@@ -1329,57 +1324,6 @@ audio.comfy-audio.empty-audio-widget {
will-change: transform;
}
/* START LOD specific styles */
/* LOD styles - Custom CSS avoids 100+ Tailwind selectors that would slow style recalculation when .isLOD toggles */
.isLOD .lg-node {
box-shadow: none;
filter: none;
backdrop-filter: none;
text-shadow: none;
mask-image: none;
clip-path: none;
background-image: none;
text-rendering: optimizeSpeed;
border-radius: 0;
contain: layout style;
transition: none;
}
.isLOD .lg-node-header {
border-radius: 0;
pointer-events: none;
}
.isLOD .lg-node-widgets {
pointer-events: none;
}
.lod-toggle {
visibility: visible;
}
.isLOD .lod-toggle {
visibility: hidden;
}
.lod-fallback {
display: none;
}
.isLOD .lod-fallback {
display: block;
}
.isLOD .image-preview img {
image-rendering: pixelated;
}
.isLOD .slot-dot {
border-radius: 0;
}
/* END LOD specific styles */
/* ===================== Mask Editor Styles ===================== */
/* To be migrated to Tailwind later */
#maskEditor_brush {

View File

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

17
pnpm-lock.yaml generated
View File

@@ -15,15 +15,9 @@ catalogs:
'@eslint/js':
specifier: ^9.35.0
version: 9.35.0
'@iconify-json/lucide':
specifier: ^1.1.178
version: 1.2.66
'@iconify/json':
specifier: ^2.2.380
version: 2.2.380
'@iconify/tailwind':
specifier: ^1.1.3
version: 1.2.0
'@intlify/eslint-plugin-vue-i18n':
specifier: ^4.1.0
version: 4.1.0
@@ -431,9 +425,6 @@ importers:
extendable-media-recorder-wav-encoder:
specifier: ^7.0.129
version: 7.0.129
fast-glob:
specifier: ^3.3.3
version: 3.3.3
firebase:
specifier: 'catalog:'
version: 11.6.0
@@ -7877,8 +7868,8 @@ packages:
vue-component-type-helpers@3.1.1:
resolution: {integrity: sha512-B0kHv7qX6E7+kdc5nsaqjdGZ1KwNKSUQDWGy7XkTYT7wFsOpkEyaJ1Vq79TjwrrtuLRgizrTV7PPuC4rRQo+vw==}
vue-component-type-helpers@3.1.4:
resolution: {integrity: sha512-Uws7Ew1OzTTqHW8ZVl/qLl/HB+jf08M0NdFONbVWAx0N4gMLK8yfZDgeB77hDnBmaigWWEn5qP8T9BG59jIeyQ==}
vue-component-type-helpers@3.1.5:
resolution: {integrity: sha512-7V3yJuNWW7/1jxCcI1CswnpDsvs02Qcx/N43LkV+ZqhLj2PKj50slUflHAroNkN4UWiYfzMUUUXiNuv9khmSpQ==}
vue-demi@0.14.10:
resolution: {integrity: sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==}
@@ -10681,7 +10672,7 @@ snapshots:
storybook: 9.1.6(@testing-library/dom@10.4.1)(prettier@3.6.2)(vite@5.4.19(@types/node@20.14.10)(lightningcss@1.30.1)(terser@5.39.2))
type-fest: 2.19.0
vue: 3.5.13(typescript@5.9.2)
vue-component-type-helpers: 3.1.4
vue-component-type-helpers: 3.1.5
'@swc/helpers@0.5.17':
dependencies:
@@ -16448,7 +16439,7 @@ snapshots:
vue-component-type-helpers@3.1.1: {}
vue-component-type-helpers@3.1.4: {}
vue-component-type-helpers@3.1.5: {}
vue-demi@0.14.10(vue@3.5.13(typescript@5.9.2)):
dependencies:

View File

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

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 1.5 MiB

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,254 @@
#!/usr/bin/env tsx
import { execSync } from 'child_process'
import fs from 'fs'
import path from 'path'
interface ReleaseInfo {
current_version: string
target_minor: number
target_version: string
target_branch: string
needs_release: boolean
latest_patch_tag: string | null
branch_head_sha: string | null
tag_commit_sha: string | null
diff_url: string
release_pr_url: string | null
}
/**
* Execute a command and return stdout
*/
function exec(command: string, cwd?: string): string {
try {
return execSync(command, {
cwd,
encoding: 'utf-8',
stdio: ['pipe', 'pipe', 'pipe']
}).trim()
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const cwdInfo = cwd ? ` in directory: ${cwd}` : ''
console.error(
`Command failed: ${command}${cwdInfo}\nError: ${errorMessage}`
)
return ''
}
}
/**
* Parse version from requirements.txt
* Handles formats: comfyui-frontend-package==1.2.3, comfyui-frontend-package>=1.2.3, etc.
*/
function parseRequirementsVersion(requirementsPath: string): string | null {
if (!fs.existsSync(requirementsPath)) {
console.error(`Requirements file not found: ${requirementsPath}`)
return null
}
const content = fs.readFileSync(requirementsPath, 'utf-8')
const match = content.match(
/comfyui-frontend-package\s*(?:==|>=|<=|~=|>|<)\s*([0-9]+\.[0-9]+\.[0-9]+)/
)
if (!match) {
console.error(
'Could not find comfyui-frontend-package version in requirements.txt'
)
return null
}
return match[1]
}
/**
* Validate semantic version string
*/
function isValidSemver(version: string): boolean {
if (!version || typeof version !== 'string') {
return false
}
const parts = version.split('.')
if (parts.length !== 3) {
return false
}
return parts.every((part) => {
const num = Number(part)
return Number.isFinite(num) && num >= 0 && String(num) === part
})
}
/**
* Get the latest patch tag for a given minor version
*/
function getLatestPatchTag(repoPath: string, minor: number): string | null {
// Fetch all tags
exec('git fetch --tags', repoPath)
// Use git's native version sorting to get the latest tag
const latestTag = exec(
`git tag -l 'v1.${minor}.*' --sort=-version:refname | head -n 1`,
repoPath
)
if (!latestTag) {
return null
}
// Validate the tag is a valid semver (vX.Y.Z format)
const validTagRegex = /^v\d+\.\d+\.\d+$/
if (!validTagRegex.test(latestTag)) {
console.error(
`Latest tag for minor version ${minor} is not valid semver: ${latestTag}`
)
return null
}
return latestTag
}
/**
* Resolve the ComfyUI release information
*/
function resolveRelease(
comfyuiRepoPath: string,
frontendRepoPath: string
): ReleaseInfo | null {
// Parse current version from ComfyUI requirements.txt
const requirementsPath = path.join(comfyuiRepoPath, 'requirements.txt')
const currentVersion = parseRequirementsVersion(requirementsPath)
if (!currentVersion) {
return null
}
// Validate version format
if (!isValidSemver(currentVersion)) {
console.error(
`Invalid semantic version format: ${currentVersion}. Expected format: X.Y.Z`
)
return null
}
const [major, currentMinor, patch] = currentVersion.split('.').map(Number)
// Calculate target minor version (next minor)
const targetMinor = currentMinor + 1
const targetBranch = `core/1.${targetMinor}`
// Check if target branch exists in frontend repo
exec('git fetch origin', frontendRepoPath)
const branchExists = exec(
`git rev-parse --verify origin/${targetBranch}`,
frontendRepoPath
)
if (!branchExists) {
console.error(
`Target branch ${targetBranch} does not exist in frontend repo`
)
return null
}
// Get latest patch tag for target minor
const latestPatchTag = getLatestPatchTag(frontendRepoPath, targetMinor)
let needsRelease = false
let branchHeadSha: string | null = null
let tagCommitSha: string | null = null
let targetVersion = currentVersion
if (latestPatchTag) {
// Get commit SHA for the tag
tagCommitSha = exec(`git rev-list -n 1 ${latestPatchTag}`, frontendRepoPath)
// Get commit SHA for branch head
branchHeadSha = exec(
`git rev-parse origin/${targetBranch}`,
frontendRepoPath
)
// Check if there are commits between tag and branch head
const commitsBetween = exec(
`git rev-list ${latestPatchTag}..origin/${targetBranch} --count`,
frontendRepoPath
)
const commitCount = parseInt(commitsBetween, 10)
needsRelease = !isNaN(commitCount) && commitCount > 0
// Parse existing patch number and increment if needed
const tagVersion = latestPatchTag.replace('v', '')
// Validate tag version format
if (!isValidSemver(tagVersion)) {
console.error(
`Invalid tag version format: ${tagVersion}. Expected format: X.Y.Z`
)
return null
}
const [, , existingPatch] = tagVersion.split('.').map(Number)
// Validate existingPatch is a valid number
if (!Number.isFinite(existingPatch) || existingPatch < 0) {
console.error(`Invalid patch number in tag: ${existingPatch}`)
return null
}
if (needsRelease) {
targetVersion = `1.${targetMinor}.${existingPatch + 1}`
} else {
targetVersion = tagVersion
}
} else {
// No tags exist for this minor version, need to create v1.{targetMinor}.0
needsRelease = true
targetVersion = `1.${targetMinor}.0`
branchHeadSha = exec(
`git rev-parse origin/${targetBranch}`,
frontendRepoPath
)
}
const diffUrl = `https://github.com/Comfy-Org/ComfyUI_frontend/compare/v${currentVersion}...v${targetVersion}`
return {
current_version: currentVersion,
target_minor: targetMinor,
target_version: targetVersion,
target_branch: targetBranch,
needs_release: needsRelease,
latest_patch_tag: latestPatchTag,
branch_head_sha: branchHeadSha,
tag_commit_sha: tagCommitSha,
diff_url: diffUrl,
release_pr_url: null // Will be populated by workflow if release is triggered
}
}
// Main execution
const comfyuiRepoPath = process.argv[2]
const frontendRepoPath = process.argv[3] || process.cwd()
if (!comfyuiRepoPath) {
console.error(
'Usage: resolve-comfyui-release.ts <comfyui-repo-path> [frontend-repo-path]'
)
process.exit(1)
}
const releaseInfo = resolveRelease(comfyuiRepoPath, frontendRepoPath)
if (!releaseInfo) {
console.error('Failed to resolve release information')
process.exit(1)
}
// Output as JSON for GitHub Actions
// eslint-disable-next-line no-console
console.log(JSON.stringify(releaseInfo, null, 2))
export { resolveRelease }

View File

@@ -1,10 +1,15 @@
<template>
<div v-if="!workspaceStore.focusMode" class="ml-1 flex gap-x-0.5 pt-1">
<div
v-if="!workspaceStore.focusMode"
class="ml-1 flex gap-x-0.5 pt-1"
@mouseenter="isTopMenuHovered = true"
@mouseleave="isTopMenuHovered = false"
>
<div class="min-w-0 flex-1">
<SubgraphBreadcrumb />
</div>
<div ref="queueAreaRef" class="mx-1 flex flex-col items-end gap-1">
<div class="mx-1 flex flex-col items-end gap-1">
<div
class="actionbar-container pointer-events-auto flex h-12 items-center rounded-lg border border-interface-stroke px-2 shadow-interface"
>
@@ -42,14 +47,13 @@
</div>
<QueueProgressOverlay
v-model:expanded="isQueueOverlayExpanded"
:external-hovered="isQueueAreaHovered"
:menu-hovered="isTopMenuHovered"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { useElementBounding, useMouse } from '@vueuse/core'
import { computed, onMounted, ref } from 'vue'
import { useI18n } from 'vue-i18n'
@@ -72,21 +76,8 @@ const { isLoggedIn } = useCurrentUser()
const isDesktop = isElectron()
const { t } = useI18n()
const isQueueOverlayExpanded = ref(false)
// Track hover over the rectangular bounding box of the queue area
// Using mouse position + element bounds instead of mouseenter/mouseleave
// because QueueProgressOverlay has pointer-events-none on its wrapper
const queueAreaRef = ref<HTMLElement | null>(null)
const { x: mouseX, y: mouseY } = useMouse()
const { left, top, right, bottom } = useElementBounding(queueAreaRef)
const isQueueAreaHovered = computed(
() =>
mouseX.value >= left.value &&
mouseX.value <= right.value &&
mouseY.value >= top.value &&
mouseY.value <= bottom.value
)
const queueStore = useQueueStore()
const isTopMenuHovered = ref(false)
const queuedCount = computed(() => queueStore.pendingTasks.length)
const queueHistoryTooltipConfig = computed(() =>
buildTooltipConfig(t('sideToolbar.queueProgressOverlay.viewJobHistory'))

View File

@@ -10,7 +10,7 @@
</div>
<Panel
class="pointer-events-auto z-1010"
class="pointer-events-auto"
:style="style"
:class="panelClass"
:pt="{
@@ -66,12 +66,7 @@ const storedPosition = useLocalStorage('Comfy.MenuPosition.Floating', {
x: 0,
y: 0
})
const {
x,
y,
style: style,
isDragging
} = useDraggable(panelRef, {
const { x, y, style, isDragging } = useDraggable(panelRef, {
initialValue: { x: 0, y: 0 },
handle: dragHandleRef,
containerElement: document.body,
@@ -257,7 +252,7 @@ watch(isDragging, (dragging) => {
})
const actionbarClass = computed(() =>
cn(
'w-[265px] border-dashed border-blue-500 opacity-80',
'w-[200px] border-dashed border-blue-500 opacity-80',
'm-1.5 flex items-center justify-center self-stretch',
'rounded-md before:w-50 before:-ml-50 before:h-full',
'pointer-events-auto',
@@ -267,7 +262,7 @@ const actionbarClass = computed(() =>
)
const panelClass = computed(() =>
cn(
'actionbar pointer-events-auto z1000',
'actionbar pointer-events-auto z-1300',
isDragging.value && 'select-none pointer-events-none',
isDocked.value
? 'p-0 static mr-2 border-none bg-transparent'

View File

@@ -44,17 +44,22 @@ import { useI18n } from 'vue-i18n'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { app } from '@/scripts/app'
import { useCommandStore } from '@/stores/commandStore'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { useQueueSettingsStore } from '@/stores/queueStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes'
import { graphHasMissingNodes } from '@/workbench/extensions/manager/utils/graphHasMissingNodes'
import BatchCountEdit from '../BatchCountEdit.vue'
const workspaceStore = useWorkspaceStore()
const { mode: queueMode, batchCount } = storeToRefs(useQueueSettingsStore())
const { hasMissingNodes } = useMissingNodes()
const nodeDefStore = useNodeDefStore()
const hasMissingNodes = computed(() =>
graphHasMissingNodes(app.graph, nodeDefStore.nodeDefsByName)
)
const { t } = useI18n()
const queueModeMenuItemLookup = computed(() => {

View File

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

View File

@@ -1,17 +1,15 @@
<template>
<div :class="iconGroupClasses">
<div
:class="
cn(
'flex justify-center items-center shrink-0 outline-hidden border-none p-0 rounded-lg bg-secondary-background shadow-sm transition-all duration-200 cursor-pointer'
)
"
>
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { cn } from '@/utils/tailwindUtil'
const iconGroupClasses = cn(
'flex justify-center items-center shrink-0',
'outline-hidden border-none p-0 rounded-lg',
'bg-secondary-background shadow-sm',
'transition-all duration-200',
'cursor-pointer'
)
</script>

View File

@@ -1,6 +1,6 @@
<template>
<div class="relative inline-flex items-center">
<IconButton :size="size" :type="type" @click="toggle">
<IconButton :size="size" :type="type" @click="popover?.toggle">
<i v-if="!isVertical" class="icon-[lucide--ellipsis] text-sm" />
<i v-else class="icon-[lucide--more-vertical] text-sm" />
</IconButton>
@@ -25,8 +25,18 @@
)
}
}"
@show="$emit('menuOpened')"
@hide="$emit('menuClosed')"
@show="
() => {
isOpen = true
$emit('menuOpened')
}
"
@hide="
() => {
isOpen = false
$emit('menuClosed')
}
"
>
<div class="flex min-w-40 flex-col gap-2 p-2">
<slot :close="hide" />
@@ -48,8 +58,6 @@ interface MoreButtonProps extends BaseButtonProps {
isVertical?: boolean
}
const popover = ref<InstanceType<typeof Popover>>()
const {
size = 'md',
type = 'secondary',
@@ -61,15 +69,15 @@ defineEmits<{
menuClosed: []
}>()
const toggle = (event: Event) => {
popover.value?.toggle(event)
}
const isOpen = ref(false)
const popover = ref<InstanceType<typeof Popover>>()
const hide = () => {
function hide() {
popover.value?.hide()
}
defineExpose({
hide
hide,
isOpen
})
</script>

View File

@@ -18,8 +18,8 @@
...inputAttrs
}
}"
@keyup.enter="blurInputElement"
@keyup.escape="cancelEditing"
@keyup.enter.capture.stop="blurInputElement"
@keyup.escape.stop="cancelEditing"
@click.stop
@pointerdown.stop.capture
@pointermove.stop.capture
@@ -38,7 +38,7 @@ const {
} = defineProps<{
modelValue: string
isEditing?: boolean
inputAttrs?: Record<string, any>
inputAttrs?: Record<string, string>
}>()
const emit = defineEmits(['update:modelValue', 'edit', 'cancel'])

View File

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

View File

@@ -14,6 +14,7 @@
<component
:is="item.headerComponent"
v-if="item.headerComponent"
v-bind="item.headerProps"
:id="item.key"
/>
<h3 v-else :id="item.key">

View File

@@ -0,0 +1,19 @@
<template>
<div
class="flex flex-col px-4 py-2 text-sm text-muted-foreground border-t border-border-default"
>
<p v-if="promptTextReal">
{{ promptTextReal }}
</p>
</div>
</template>
<script setup lang="ts">
import { computed, toValue } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
const { promptText } = defineProps<{
promptText?: MaybeRefOrGetter<string>
}>()
const promptTextReal = computed(() => toValue(promptText))
</script>

View File

@@ -0,0 +1,43 @@
<template>
<section class="w-full flex gap-2 justify-end px-2 pb-2">
<TextButton
:label="cancelTextX"
:disabled
type="transparent"
autofocus
@click="$emit('cancel')"
/>
<TextButton
:label="confirmTextX"
:disabled
type="transparent"
:class="confirmClass"
@click="$emit('confirm')"
/>
</section>
</template>
<script setup lang="ts">
import { computed, toValue } from 'vue'
import type { MaybeRefOrGetter } from 'vue'
import { useI18n } from 'vue-i18n'
import TextButton from '@/components/button/TextButton.vue'
const { t } = useI18n()
const { cancelText, confirmText, confirmClass, optionsDisabled } = defineProps<{
cancelText?: string
confirmText?: string
confirmClass?: string
optionsDisabled?: MaybeRefOrGetter<boolean>
}>()
defineEmits<{
cancel: []
confirm: []
}>()
const confirmTextX = computed(() => confirmText || t('g.confirm'))
const cancelTextX = computed(() => cancelText || t('g.cancel'))
const disabled = computed(() => toValue(optionsDisabled))
</script>

View File

@@ -0,0 +1,12 @@
<template>
<div
class="flex items-center gap-2 p-4 font-bold text-sm text-base-foreground font-inter"
>
<span v-if="title" class="flex-auto">{{ title }}</span>
</div>
</template>
<script setup lang="ts">
defineProps<{
title?: string
}>()
</script>

View File

@@ -0,0 +1,39 @@
import ConfirmBody from '@/components/dialog/confirm/ConfirmBody.vue'
import ConfirmFooter from '@/components/dialog/confirm/ConfirmFooter.vue'
import ConfirmHeader from '@/components/dialog/confirm/ConfirmHeader.vue'
import { useDialogStore } from '@/stores/dialogStore'
import type { ComponentAttrs } from 'vue-component-type-helpers'
interface ConfirmDialogOptions {
headerProps?: ComponentAttrs<typeof ConfirmHeader>
props?: ComponentAttrs<typeof ConfirmBody>
footerProps?: ComponentAttrs<typeof ConfirmFooter>
}
/**
* Open a confirmation dialog composed of the standard confirm header, body, and footer.
*
* Forwards any provided `headerProps`, `props`, and `footerProps` to the corresponding components.
*
* @param options - Optional configuration with `headerProps`, `props`, and `footerProps` to customize the header, body, and footer components
* @returns A dialog handle representing the shown confirmation dialog
*/
export function showConfirmDialog(options: ConfirmDialogOptions = {}) {
const dialogStore = useDialogStore()
const { headerProps, props, footerProps } = options
return dialogStore.showDialog({
headerComponent: ConfirmHeader,
component: ConfirmBody,
footerComponent: ConfirmFooter,
headerProps,
props,
footerProps,
dialogComponentProps: {
pt: {
header: 'py-0! px-0!',
content: 'p-0!',
footer: 'p-0!'
}
}
})
}

View File

@@ -14,7 +14,9 @@
v-if="isNativeWindow() && workflowTabsPosition !== 'Topbar'"
class="app-drag fixed top-0 left-0 z-10 h-[var(--comfy-topbar-height)] w-full"
/>
<div class="flex h-full items-center">
<div
class="flex h-full items-center border-b border-interface-stroke bg-comfy-menu-bg shadow-interface"
>
<WorkflowTabs />
<TopbarBadges />
</div>

View File

@@ -280,7 +280,8 @@ const menuItems = computed<MenuItem[]>(() => {
label: t('helpCenter.docs'),
action: () => {
trackResourceClick('docs', true)
openExternalLink(buildDocsUrl('/', { includeLocale: true }))
const path = isCloud ? '/get_started/cloud' : '/'
openExternalLink(buildDocsUrl(path, { includeLocale: true }))
emit('close')
}
},

View File

@@ -152,7 +152,7 @@ const {
popoverMaxWidth?: string
}>()
const selectedItem = defineModel<string | null>({ required: true })
const selectedItem = defineModel<string | undefined>({ required: true })
const { t } = useI18n()

View File

@@ -6,10 +6,7 @@
{{ t('maskEditor.brushSettings') }}
</h3>
<button
class="w-45 h-7.5 border-none bg-black/20 border border-[var(--border-color)] text-[var(--input-text)] font-sans text-[15px] pointer-events-auto transition-colors duration-100 hover:bg-[var(--p-overlaybadge-outline-color)] hover:border-none"
@click="resetToDefault"
>
<button :class="textButtonClass" @click="resetToDefault">
{{ t('maskEditor.resetToDefault') }}
</button>
@@ -80,7 +77,7 @@
/>
<SliderControl
label="Stepsize"
:label="$t('maskEditor.stepSize')"
:min="1"
:max="100"
:step="1"
@@ -99,6 +96,9 @@ import SliderControl from './controls/SliderControl.vue'
const store = useMaskEditorStore()
const textButtonClass =
'h-7.5 w-32 rounded-[10px] border border-[var(--p-form-field-border-color)] text-[var(--input-text)] font-sans pointer-events-auto transition-colors duration-100 bg-[var(--comfy-menu-bg)] hover:bg-secondary-background-hover'
const setBrushShape = (shape: BrushShape) => {
store.brushSettings.type = shape
}

View File

@@ -25,7 +25,11 @@
class="inline-block h-6 w-6 overflow-hidden rounded-[6px] border-0 bg-secondary-background"
:style="{ marginLeft: idx === 0 ? '0' : '-12px' }"
>
<img :src="url" alt="preview" class="h-full w-full object-cover" />
<img
:src="url"
:alt="$t('sideToolbar.queueProgressOverlay.preview')"
class="h-full w-full object-cover"
/>
</span>
</span>

View File

@@ -6,8 +6,8 @@
<div
class="pointer-events-auto flex w-[350px] min-w-[310px] max-h-[60vh] flex-col overflow-hidden rounded-lg border font-inter transition-colors duration-200 ease-in-out"
:class="containerClass"
@mouseenter="isHoveredInternal = true"
@mouseleave="isHoveredInternal = false"
@mouseenter="isHovered = true"
@mouseleave="isHovered = false"
>
<!-- Expanded state -->
<QueueOverlayExpanded
@@ -85,11 +85,15 @@ import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore'
type OverlayState = 'hidden' | 'empty' | 'active' | 'expanded'
const props = defineProps<{
expanded?: boolean
/** External hover state from parent container */
externalHovered?: boolean
}>()
const props = withDefaults(
defineProps<{
expanded?: boolean
menuHovered?: boolean
}>(),
{
menuHovered: false
}
)
const emit = defineEmits<{
(e: 'update:expanded', value: boolean): void
@@ -111,10 +115,8 @@ const {
totalProgressStyle,
currentNodeProgressStyle
} = useQueueProgress()
const isHoveredInternal = ref(false)
const isHovered = computed(
() => isHoveredInternal.value || props.externalHovered
)
const isHovered = ref(false)
const isOverlayHovered = computed(() => isHovered.value || props.menuHovered)
const internalExpanded = ref(false)
const isExpanded = computed({
get: () =>
@@ -147,7 +149,7 @@ const showBackground = computed(
() =>
overlayState.value === 'expanded' ||
overlayState.value === 'empty' ||
(overlayState.value === 'active' && isHovered.value)
(overlayState.value === 'active' && isOverlayHovered.value)
)
const isVisible = computed(() => overlayState.value !== 'hidden')
@@ -161,7 +163,7 @@ const containerClass = computed(() =>
const bottomRowClass = computed(
() =>
`flex items-center justify-end gap-4 transition-opacity duration-200 ease-in-out ${
overlayState.value === 'active' && isHovered.value
overlayState.value === 'active' && isOverlayHovered.value
? 'opacity-100 pointer-events-auto'
: 'opacity-0 pointer-events-none'
}`

View File

@@ -84,13 +84,7 @@
/>
<i
v-else
:class="
cn(
iconClass,
'size-4',
props.state === 'pending' && 'animate-spin'
)
"
:class="cn(iconClass, 'size-4', shouldSpin && 'animate-spin')"
/>
</div>
</div>
@@ -349,6 +343,13 @@ const iconClass = computed(() => {
return iconForJobState(props.state)
})
const shouldSpin = computed(
() =>
props.state === 'pending' &&
iconClass.value === iconForJobState('pending') &&
!props.iconImageUrl
)
const computedShowClear = computed(() => {
if (props.showClear !== undefined) return props.showClear
return props.state !== 'completed'

View File

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

View File

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

View File

@@ -0,0 +1,39 @@
<template>
<SidebarIcon
:label="$t('g.settings')"
:tooltip="tooltipText"
@click="showSettingsDialog"
>
<template #icon>
<i class="icon-[lucide--settings]" />
</template>
</SidebarIcon>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { useTelemetry } from '@/platform/telemetry'
import { useCommandStore } from '@/stores/commandStore'
import SidebarIcon from './SidebarIcon.vue'
const { t } = useI18n()
const { getCommand, formatKeySequence } = useCommandStore()
const command = getCommand('Comfy.ShowSettingsDialog')
const tooltipText = computed(
() => `${t('g.settings')} (${formatKeySequence(command)})`
)
/**
* Toggle keyboard shortcuts panel and track UI button click.
*/
const showSettingsDialog = () => {
command.function()
useTelemetry()?.trackUiButtonClicked({
button_id: 'sidebar_settings_button_clicked'
})
}
</script>

View File

@@ -85,13 +85,12 @@
:show-output-count="shouldShowOutputCount(item)"
:output-count="getOutputCount(item)"
:show-delete-button="shouldShowDeleteButton"
:open-popover-id="openPopoverId"
:open-context-menu-id="openContextMenuId"
@click="handleAssetSelect(item)"
@zoom="handleZoomClick(item)"
@output-count-click="enterFolderView(item)"
@asset-deleted="refreshAssets"
@popover-opened="openPopoverId = item.id"
@popover-closed="openPopoverId = null"
@context-menu-opened="openContextMenuId = item.id"
/>
</template>
</VirtualGrid>
@@ -113,7 +112,7 @@
count: totalOutputCount
})
"
type="transparent"
type="secondary"
:class="isCompact ? 'text-left' : ''"
@click="handleDeselectAll"
/>
@@ -202,8 +201,8 @@ const folderPromptId = ref<string | null>(null)
const folderExecutionTime = ref<number | undefined>(undefined)
const isInFolderView = computed(() => folderPromptId.value !== null)
// Track which asset's popover is open (for single-instance popover management)
const openPopoverId = ref<string | null>(null)
// Track which asset's context menu is open (for single-instance context menu management)
const openContextMenuId = ref<string | null>(null)
// Determine if delete button should be shown
// Hide delete button when in input tab and not in cloud (OSS mode - files are from local folders)

View File

@@ -4,7 +4,7 @@
v-for="(button, index) in actionBarButtonStore.buttons"
:key="index"
v-tooltip.bottom="button.tooltip"
:label="button.label"
:label="isMobile ? undefined : button.label"
:aria-label="button.tooltip || button.label"
:class="button.class"
text
@@ -21,9 +21,13 @@
</template>
<script lang="ts" setup>
import { breakpointsTailwind, useBreakpoints } from '@vueuse/core'
import Button from 'primevue/button'
import { useActionBarButtonStore } from '@/stores/actionBarButtonStore'
const actionBarButtonStore = useActionBarButtonStore()
const breakpoints = useBreakpoints(breakpointsTailwind)
const isMobile = breakpoints.smaller('sm')
</script>

View File

@@ -3,9 +3,12 @@
v-if="showVueNodesBanner"
class="pointer-events-auto relative w-full h-10 bg-gradient-to-r from-blue-600 to-blue-700 flex items-center justify-center px-4"
>
<div class="flex items-center">
<div class="flex items-center text-sm text-white">
<i class="icon-[lucide--rocket]"></i>
<span class="pl-2 text-sm">{{ $t('vueNodesBanner.message') }}</span>
<span class="pl-2">{{ $t('vueNodesBanner.title') }}</span>
<span class="pl-1.5 hidden md:inline">{{
$t('vueNodesBanner.desc')
}}</span>
<Button
class="cursor-pointer bg-transparent rounded h-7 px-3 border border-white text-white ml-4 text-xs"
@click="handleTryItOut"
@@ -14,7 +17,7 @@
</Button>
</div>
<Button
class="cursor-pointer bg-transparent border-0 outline-0 grid place-items-center absolute right-4"
class="cursor-pointer bg-transparent border-0 outline-0 grid place-items-center absolute right-4 text-white"
unstyled
@click="handleDismiss"
>
@@ -63,7 +66,7 @@ const handleTryItOut = async (): Promise<void> => {
try {
await settingStore.set('Comfy.VueNodes.Enabled', true)
} catch (error) {
console.error('Failed to enable Vue nodes:', error)
console.error('Failed to enable Nodes 2.0:', error)
} finally {
handleDismiss()
}

View File

@@ -1,7 +1,7 @@
<template>
<div
ref="containerRef"
class="workflow-tabs-container flex h-full max-w-full flex-auto flex-row overflow-hidden border-b border-[var(--interface-stroke)] shadow-interface"
class="workflow-tabs-container flex h-full max-w-full flex-auto flex-row overflow-hidden"
:class="{ 'workflow-tabs-container-desktop': isDesktop }"
>
<Button

View File

@@ -30,7 +30,7 @@
<template #default="{ close }">
<IconTextButton
type="secondary"
label="Settings"
:label="$t('g.settings')"
@click="
() => {
close()
@@ -43,7 +43,7 @@
</IconTextButton>
<IconTextButton
type="primary"
label="Profile"
:label="$t('g.profile')"
@click="
() => {
close()
@@ -65,7 +65,7 @@
v-model="selectedFrameworks"
v-model:search-query="searchText"
class="w-[250px]"
label="Select Frameworks"
:label="$t('assetBrowser.selectFrameworks')"
:options="frameworkOptions"
:show-search-box="true"
:show-selected-count="true"
@@ -73,12 +73,12 @@
/>
<MultiSelect
v-model="selectedProjects"
label="Select Projects"
:label="$t('assetBrowser.selectProjects')"
:options="projectOptions"
/>
<SingleSelect
v-model="selectedSort"
label="Sorting Type"
:label="$t('assetBrowser.sortingType')"
:options="sortOptions"
class="w-[135px]"
>

View File

@@ -1,5 +1,5 @@
import { useElementBounding, useRafFn } from '@vueuse/core'
import { computed, onUnmounted, ref, watch } from 'vue'
import { computed, onUnmounted, ref, watch, watchEffect } from 'vue'
import type { Ref } from 'vue'
import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems'
@@ -157,6 +157,14 @@ export function useSelectionToolboxPosition(
// Sync with canvas transform
const { resume: startSync, pause: stopSync } = useRafFn(updateTransform)
watchEffect(() => {
if (visible.value) {
startSync()
} else {
stopSync()
}
})
// Watch for selection changes
watch(
() => canvasStore.getCanvas().state.selectionChanged,
@@ -173,11 +181,6 @@ export function useSelectionToolboxPosition(
}
updateSelectionBounds()
canvasStore.getCanvas().state.selectionChanged = false
if (visible.value) {
startSync()
} else {
stopSync()
}
}
},
{ immediate: true }

View File

@@ -3,7 +3,6 @@ import { shallowRef, watch } from 'vue'
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
import type { GraphNodeManager } from '@/composables/graph/useGraphNodeManager'
import { useRenderModeSetting } from '@/composables/settings/useRenderModeSetting'
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
import { useVueNodesMigrationDismissed } from '@/composables/useVueNodesMigrationDismissed'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
@@ -11,6 +10,7 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync'
import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil'
import { ensureCorrectLayoutScale } from '@/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale'
import { app as comfyApp } from '@/scripts/app'
import { useToastStore } from '@/platform/updates/common/toastStore'
@@ -26,11 +26,6 @@ function useVueNodeLifecycleIndividual() {
let hasShownMigrationToast = false
useRenderModeSetting(
{ setting: 'LiteGraph.Canvas.MinFontSizeForLOD', vue: 0, litegraph: 8 },
shouldRenderVueNodes
)
const initializeNodeManager = () => {
// Use canvas graph if available (handles subgraph contexts), fallback to app graph
const activeGraph = comfyApp.canvas?.graph
@@ -44,7 +39,10 @@ function useVueNodeLifecycleIndividual() {
const nodes = activeGraph._nodes.map((node: LGraphNode) => ({
id: node.id.toString(),
pos: [node.pos[0], node.pos[1]] as [number, number],
size: [node.size[0], node.size[1]] as [number, number]
size: [node.size[0], removeNodeTitleHeight(node.size[1])] as [
number,
number
]
}))
layoutStore.initializeFromLiteGraph(nodes)

View File

@@ -1,4 +1,4 @@
import tgpu from 'typegpu'
import { tgpu } from 'typegpu'
import * as d from 'typegpu/data'
import { BrushUniforms } from './gpuSchema'

View File

@@ -12,7 +12,7 @@ import type { Brush, Point } from '@/extensions/core/maskeditor/types'
import { useMaskEditorStore } from '@/stores/maskEditorStore'
import { useCoordinateTransform } from './useCoordinateTransform'
import { resampleSegment } from './splineUtils'
import TGPU from 'typegpu'
import { tgpu } from 'typegpu'
import { GPUBrushRenderer } from './gpu/GPUBrushRenderer'
import { StrokeProcessor } from './StrokeProcessor'
import { getEffectiveBrushSize, getEffectiveHardness } from './brushUtils'
@@ -276,7 +276,7 @@ export function useBrushDrawing(initialSettings?: {
}
try {
const root = await TGPU.init()
const root = await tgpu.init()
store.tgpuRoot = root
device = root.device
console.warn('✅ TypeGPU initialized! Root:', root)

View File

@@ -49,6 +49,21 @@ const calculateRunwayDurationPrice = (node: LGraphNode): string => {
return `$${cost}/Run`
}
const makeOmniProDurationCalculator =
(pricePerSecond: number): PricingFunction =>
(node: LGraphNode): string => {
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration'
) as IComboWidget
if (!durationWidget) return `$${pricePerSecond.toFixed(3)}/second`
const seconds = parseFloat(String(durationWidget.value))
if (!Number.isFinite(seconds)) return `$${pricePerSecond.toFixed(3)}/second`
const cost = pricePerSecond * seconds
return `$${cost.toFixed(2)}/Run`
}
const pixversePricingCalculator = (node: LGraphNode): string => {
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration_seconds'
@@ -131,6 +146,11 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
'720p': [0.51, 0.56],
'1080p': [1.18, 1.22]
},
'seedance-1-0-pro-fast': {
'480p': [0.09, 0.1],
'720p': [0.21, 0.23],
'1080p': [0.47, 0.49]
},
'seedance-1-0-lite': {
'480p': [0.17, 0.18],
'720p': [0.37, 0.41],
@@ -138,11 +158,13 @@ const byteDanceVideoPricingCalculator = (node: LGraphNode): string => {
}
}
const modelKey = model.includes('seedance-1-0-pro')
? 'seedance-1-0-pro'
: model.includes('seedance-1-0-lite')
? 'seedance-1-0-lite'
: ''
const modelKey = model.includes('seedance-1-0-pro-fast')
? 'seedance-1-0-pro-fast'
: model.includes('seedance-1-0-pro')
? 'seedance-1-0-pro'
: model.includes('seedance-1-0-lite')
? 'seedance-1-0-lite'
: ''
const resKey = resolution.includes('1080')
? '1080p'
@@ -303,6 +325,46 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
FluxProKontextMaxNode: {
displayPrice: '$0.08/Run'
},
Flux2ProImageNode: {
displayPrice: (node: LGraphNode): string => {
const widthW = node.widgets?.find(
(w) => w.name === 'width'
) as IComboWidget
const heightW = node.widgets?.find(
(w) => w.name === 'height'
) as IComboWidget
const w = Number(widthW?.value)
const h = Number(heightW?.value)
if (!Number.isFinite(w) || !Number.isFinite(h) || w <= 0 || h <= 0) {
// global min/max for this node given schema bounds (1MP..4MP output)
return '$0.03$0.15/Run'
}
// Is the 'images' input connected?
const imagesInput = node.inputs?.find(
(i) => i.name === 'images'
) as INodeInputSlot
const hasRefs =
typeof imagesInput?.link !== 'undefined' && imagesInput.link != null
// Output cost: ceil((w*h)/MP); first MP $0.03, each additional $0.015
const MP = 1024 * 1024
const outMP = Math.max(1, Math.floor((w * h + MP - 1) / MP))
const outputCost = 0.03 + 0.015 * Math.max(outMP - 1, 0)
if (hasRefs) {
// Unknown ref count/size on the frontend:
// min extra is $0.015, max extra is $0.120 (8 MP cap / 8 refs)
const minTotal = outputCost + 0.015
const maxTotal = outputCost + 0.12
return `~$${parseFloat(minTotal.toFixed(3))}$${parseFloat(maxTotal.toFixed(3))}/Run`
}
// Precise text-to-image price
return `$${parseFloat(outputCost.toFixed(3))}/Run`
}
},
OpenAIVideoSora2: {
displayPrice: sora2PricingCalculator
},
@@ -583,7 +645,12 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
const modeValue = String(modeWidget.value)
// Same pricing matrix as KlingTextToVideoNode
if (modeValue.includes('v2-1')) {
if (modeValue.includes('v2-5-turbo')) {
if (modeValue.includes('10')) {
return '$0.70/Run'
}
return '$0.35/Run' // 5s default
} else if (modeValue.includes('v2-1')) {
if (modeValue.includes('10s')) {
return '$0.98/Run' // pro, 10s
}
@@ -659,6 +726,21 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
KlingVirtualTryOnNode: {
displayPrice: '$0.07/Run'
},
KlingOmniProTextToVideoNode: {
displayPrice: makeOmniProDurationCalculator(0.112)
},
KlingOmniProFirstLastFrameNode: {
displayPrice: makeOmniProDurationCalculator(0.112)
},
KlingOmniProImageToVideoNode: {
displayPrice: makeOmniProDurationCalculator(0.112)
},
KlingOmniProVideoToVideoNode: {
displayPrice: makeOmniProDurationCalculator(0.168)
},
KlingOmniProEditVideoNode: {
displayPrice: '$0.168/second'
},
LumaImageToVideoNode: {
displayPrice: (node: LGraphNode): string => {
// Same pricing as LumaVideoNode per CSV
@@ -1197,6 +1279,40 @@ const apiNodeCosts: Record<string, { displayPrice: string | PricingFunction }> =
return '$0.80-3.20/Run'
}
},
Veo3FirstLastFrameNode: {
displayPrice: (node: LGraphNode): string => {
const modelWidget = node.widgets?.find(
(w) => w.name === 'model'
) as IComboWidget
const generateAudioWidget = node.widgets?.find(
(w) => w.name === 'generate_audio'
) as IComboWidget
const durationWidget = node.widgets?.find(
(w) => w.name === 'duration'
) as IComboWidget
if (!modelWidget || !generateAudioWidget || !durationWidget) {
return '$0.40-3.20/Run (varies with model & audio generation)'
}
const model = String(modelWidget.value)
const generateAudio =
String(generateAudioWidget.value).toLowerCase() === 'true'
const seconds = parseFloat(String(durationWidget.value))
let pricePerSecond: number | null = null
if (model.includes('veo-3.1-fast-generate')) {
pricePerSecond = generateAudio ? 0.15 : 0.1
} else if (model.includes('veo-3.1-generate')) {
pricePerSecond = generateAudio ? 0.4 : 0.2
}
if (pricePerSecond === null) {
return '$0.40-3.20/Run'
}
const cost = pricePerSecond * seconds
return `$${cost.toFixed(2)}/Run`
}
},
LumaImageNode: {
displayPrice: (node: LGraphNode): string => {
const modelWidget = node.widgets?.find(
@@ -1799,6 +1915,10 @@ export const useNodePricing = () => {
KlingDualCharacterVideoEffectNode: ['mode', 'model_name', 'duration'],
KlingSingleImageVideoEffectNode: ['effect_scene'],
KlingStartEndFrameNode: ['mode', 'model_name', 'duration'],
KlingOmniProTextToVideoNode: ['duration'],
KlingOmniProFirstLastFrameNode: ['duration'],
KlingOmniProImageToVideoNode: ['duration'],
KlingOmniProVideoToVideoNode: ['duration'],
MinimaxHailuoVideoNode: ['resolution', 'duration'],
OpenAIDalle3: ['size', 'quality'],
OpenAIDalle2: ['size', 'n'],
@@ -1809,8 +1929,10 @@ export const useNodePricing = () => {
IdeogramV3: ['rendering_speed', 'num_images', 'character_image'],
FluxProKontextProNode: [],
FluxProKontextMaxNode: [],
Flux2ProImageNode: ['width', 'height', 'images'],
VeoVideoGenerationNode: ['duration_seconds'],
Veo3VideoGenerationNode: ['model', 'generate_audio'],
Veo3FirstLastFrameNode: ['model', 'generate_audio', 'duration'],
LumaVideoNode: ['model', 'resolution', 'duration'],
LumaImageToVideoNode: ['model', 'resolution', 'duration'],
LumaImageNode: ['model', 'aspect_ratio'],

View File

@@ -3,6 +3,7 @@ import { useI18n } from 'vue-i18n'
import { useQueueProgress } from '@/composables/queue/useQueueProgress'
import { st } from '@/i18n'
import { isCloud } from '@/platform/distribution/types'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useQueueStore } from '@/stores/queueStore'
@@ -96,6 +97,7 @@ export function useJobList() {
const executionStore = useExecutionStore()
const workflowStore = useWorkflowStore()
const seenPendingIds = ref<Set<string>>(new Set())
const recentlyAddedPendingIds = ref<Set<string>>(new Set())
const addedHintTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
@@ -126,23 +128,27 @@ export function useJobList() {
.filter((id): id is string => !!id),
(pendingIds) => {
const pendingSet = new Set(pendingIds)
const next = new Set(recentlyAddedPendingIds.value)
const nextAdded = new Set(recentlyAddedPendingIds.value)
const nextSeen = new Set(seenPendingIds.value)
pendingIds.forEach((id) => {
if (!next.has(id)) {
next.add(id)
if (!nextSeen.has(id)) {
nextSeen.add(id)
nextAdded.add(id)
scheduleAddedHintExpiry(id)
}
})
for (const id of Array.from(next)) {
for (const id of Array.from(nextSeen)) {
if (!pendingSet.has(id)) {
next.delete(id)
nextSeen.delete(id)
nextAdded.delete(id)
clearAddedHintTimeout(id)
}
}
recentlyAddedPendingIds.value = next
recentlyAddedPendingIds.value = nextAdded
seenPendingIds.value = nextSeen
},
{ immediate: true }
)
@@ -157,6 +163,7 @@ export function useJobList() {
onUnmounted(() => {
addedHintTimeouts.forEach((timeoutId) => clearTimeout(timeoutId))
addedHintTimeouts.clear()
seenPendingIds.value = new Set<string>()
recentlyAddedPendingIds.value = new Set<string>()
})
@@ -257,7 +264,8 @@ export function useJobList() {
totalPercent: isActive ? totalPercent.value : undefined,
currentNodePercent: isActive ? currentNodePercent.value : undefined,
currentNodeName: isActive ? currentNodeName.value : undefined,
showAddedHint
showAddedHint,
isCloud
})
return {

View File

@@ -1,42 +0,0 @@
import type { ComputedRef } from 'vue'
import { ref, watch } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
import type { Settings } from '@/schemas/apiSchema'
interface RenderModeSettingConfig<TSettingKey extends keyof Settings> {
setting: TSettingKey
vue: Settings[TSettingKey]
litegraph: Settings[TSettingKey]
}
export function useRenderModeSetting<TSettingKey extends keyof Settings>(
config: RenderModeSettingConfig<TSettingKey>,
isVueMode: ComputedRef<boolean>
) {
const settingStore = useSettingStore()
const vueValue = ref(config.vue)
const litegraphValue = ref(config.litegraph)
const lastWasVue = ref<boolean | null>(null)
const load = async (vue: boolean) => {
if (lastWasVue.value === vue) return
if (lastWasVue.value !== null) {
const currentValue = settingStore.get(config.setting)
if (lastWasVue.value) {
vueValue.value = currentValue
} else {
litegraphValue.value = currentValue
}
}
await settingStore.set(
config.setting,
vue ? vueValue.value : litegraphValue.value
)
lastWasVue.value = vue
}
watch(isVueMode, load, { immediate: true })
}

View File

@@ -330,7 +330,7 @@ export function useCoreCommands(): ComfyCommand[] {
label: () =>
`Experimental: ${
useSettingStore().get('Comfy.VueNodes.Enabled') ? 'Disable' : 'Enable'
} Vue Nodes`,
} Nodes 2.0`,
function: async () => {
const settingStore = useSettingStore()
const current = settingStore.get('Comfy.VueNodes.Enabled') ?? false

View File

@@ -9,11 +9,20 @@ export enum ServerFeatureFlag {
SUPPORTS_PREVIEW_METADATA = 'supports_preview_metadata',
MAX_UPLOAD_SIZE = 'max_upload_size',
MANAGER_SUPPORTS_V4 = 'extension.manager.supports_v4',
MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled'
MODEL_UPLOAD_BUTTON_ENABLED = 'model_upload_button_enabled',
ASSET_UPDATE_OPTIONS_ENABLED = 'asset_update_options_enabled'
}
/**
* Composable for reactive access to server-side feature flags
* Provides reactive accessors for server-side feature flags.
*
* Exposes a readonly `flags` object containing convenience getters for known server feature keys
* and a `featureFlag` helper that returns a computed value for an arbitrary feature path,
* optionally using a supplied default when the feature is not present.
*
* @returns An object with:
* - `flags`: a readonly reactive object with predefined getters for common server feature flags
* - `featureFlag`: a generic function `(featurePath: string, defaultValue?) => ComputedRef<T>` that yields a computed feature value
*/
export function useFeatureFlags() {
const flags = reactive({
@@ -31,6 +40,12 @@ export function useFeatureFlags() {
ServerFeatureFlag.MODEL_UPLOAD_BUTTON_ENABLED,
false
)
},
get assetUpdateOptionsEnabled() {
return api.getServerFeature(
ServerFeatureFlag.ASSET_UPDATE_OPTIONS_ENABLED,
false
)
}
})

View File

@@ -7,9 +7,9 @@ import type {
INodeInputSlot,
INodeOutputSlot,
ISlotType,
LLink,
Point
LLink
} from '@/lib/litegraph/src/litegraph'
import { NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
import type { CanvasPointerEvent } from '@/lib/litegraph/src/types/events'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import type { InputSpec } from '@/schemas/nodeDefSchema'
@@ -37,15 +37,15 @@ export class PrimitiveNode extends LGraphNode {
}
override applyToGraph(extraLinks: LLink[] = []) {
if (!this.outputs[0].links?.length) return
if (!this.outputs[0].links?.length || !this.graph) return
const links = [
...this.outputs[0].links.map((l) => app.graph.links[l]),
...this.outputs[0].links.map((l) => this.graph!.links[l]),
...extraLinks
]
let v = this.widgets?.[0].value
if (v && this.properties[replacePropertyName]) {
v = applyTextReplacements(app.graph, v as string)
v = applyTextReplacements(this.graph, v as string)
}
// For each output link copy our value over the original widget value
@@ -331,13 +331,13 @@ export class PrimitiveNode extends LGraphNode {
const config1 = (output.widget?.[GET_CONFIG] as () => InputSpec)?.()
if (!config1) return
const isNumber = config1[0] === 'INT' || config1[0] === 'FLOAT'
if (!isNumber) return
if (!isNumber || !this.graph) return
for (const linkId of links) {
const link = app.graph.links[linkId]
const link = this.graph.links[linkId]
if (!link) continue // Can be null when removing a node
const theirNode = app.graph.getNodeById(link.target_id)
const theirNode = this.graph.getNodeById(link.target_id)
if (!theirNode) continue
const theirInput = theirNode.inputs[link.target_slot]
@@ -441,10 +441,7 @@ function getWidgetType(config: InputSpec) {
return { type }
}
export function setWidgetConfig(
slot: INodeInputSlot | INodeOutputSlot,
config?: InputSpec
) {
export function setWidgetConfig(slot: INodeInputSlot, config?: InputSpec) {
if (!slot.widget) return
if (config) {
slot.widget[GET_CONFIG] = () => config
@@ -452,19 +449,18 @@ export function setWidgetConfig(
delete slot.widget
}
if ('link' in slot) {
const link = app.graph.links[slot.link ?? -1]
if (link) {
const originNode = app.graph.getNodeById(link.origin_id)
if (originNode && isPrimitiveNode(originNode)) {
if (config) {
originNode.recreateWidget()
} else if (!app.configuringGraph) {
originNode.disconnectOutput(0)
originNode.onLastDisconnect()
}
}
}
if (!(slot instanceof NodeSlot)) return
const graph = slot.node.graph
if (!graph) return
const link = graph.links[slot.link ?? -1]
if (!link) return
const originNode = graph.getNodeById(link.origin_id)
if (!originNode || !isPrimitiveNode(originNode)) return
if (config) {
originNode.recreateWidget()
} else if (!app.configuringGraph) {
originNode.disconnectOutput(0)
originNode.onLastDisconnect()
}
}
@@ -555,15 +551,6 @@ app.registerExtension({
}
)
function isNodeAtPos(pos: Point) {
for (const n of app.graph.nodes) {
if (n.pos[0] === pos[0] && n.pos[1] === pos[1]) {
return true
}
}
return false
}
// Double click a widget input to automatically attach a primitive
const origOnInputDblClick = nodeType.prototype.onInputDblClick
nodeType.prototype.onInputDblClick = function (
@@ -589,18 +576,18 @@ app.registerExtension({
// Create a primitive node
const node = LiteGraph.createNode('PrimitiveNode')
if (!node) return r
const graph = app.canvas.graph
if (!node || !graph) return r
this.graph?.add(node)
graph?.add(node)
// Calculate a position that won't directly overlap another node
const pos: [number, number] = [
this.pos[0] - node.size[0] - 30,
this.pos[1]
]
while (isNodeAtPos(pos)) {
while (graph.getNodeOnPos(pos[0], pos[1], graph.nodes))
pos[1] += LiteGraph.NODE_TITLE_HEIGHT
}
node.pos = pos
node.connect(0, this, slot)

View File

@@ -36,7 +36,8 @@ const localeLoaders: Record<
ru: () => import('./locales/ru/main.json'),
tr: () => import('./locales/tr/main.json'),
zh: () => import('./locales/zh/main.json'),
'zh-TW': () => import('./locales/zh-TW/main.json')
'zh-TW': () => import('./locales/zh-TW/main.json'),
'pt-BR': () => import('./locales/pt-BR/main.json')
}
const nodeDefsLoaders: Record<
@@ -51,7 +52,8 @@ const nodeDefsLoaders: Record<
ru: () => import('./locales/ru/nodeDefs.json'),
tr: () => import('./locales/tr/nodeDefs.json'),
zh: () => import('./locales/zh/nodeDefs.json'),
'zh-TW': () => import('./locales/zh-TW/nodeDefs.json')
'zh-TW': () => import('./locales/zh-TW/nodeDefs.json'),
'pt-BR': () => import('./locales/pt-BR/nodeDefs.json')
}
const commandsLoaders: Record<
@@ -66,7 +68,8 @@ const commandsLoaders: Record<
ru: () => import('./locales/ru/commands.json'),
tr: () => import('./locales/tr/commands.json'),
zh: () => import('./locales/zh/commands.json'),
'zh-TW': () => import('./locales/zh-TW/commands.json')
'zh-TW': () => import('./locales/zh-TW/commands.json'),
'pt-BR': () => import('./locales/pt-BR/commands.json')
}
const settingsLoaders: Record<
@@ -81,7 +84,8 @@ const settingsLoaders: Record<
ru: () => import('./locales/ru/settings.json'),
tr: () => import('./locales/tr/settings.json'),
zh: () => import('./locales/zh/settings.json'),
'zh-TW': () => import('./locales/zh-TW/settings.json')
'zh-TW': () => import('./locales/zh-TW/settings.json'),
'pt-BR': () => import('./locales/pt-BR/settings.json')
}
// Track which locales have been loaded
@@ -159,6 +163,7 @@ export const i18n = createI18n({
legacy: false,
locale: navigator.language.split('-')[0] || 'en',
fallbackLocale: 'en',
escapeParameter: true,
messages,
// Ignore warnings for locale options as each option is in its own language.
// e.g. "English", "中文", "Русский", "日本語", "한국어", "Français", "Español"

View File

@@ -21,8 +21,8 @@
min-width: 100px;
color: #aaf;
padding: 0;
box-shadow: 0 0 10px black !important;
background-color: #2e2e2e !important;
box-shadow: 0 0 10px black;
background-color: #2e2e2e;
z-index: 10;
max-height: -webkit-fill-available;
overflow-y: auto;
@@ -36,10 +36,6 @@
}
}
.litegraph.litecontextmenu.dark {
background-color: #000 !important;
}
.litegraph.litecontextmenu .litemenu-title img {
margin-top: 2px;
margin-left: 2px;
@@ -51,14 +47,6 @@
padding: 2px;
}
.litegraph.litecontextmenu .litemenu-entry.submenu {
background-color: #2e2e2e !important;
}
.litegraph.litecontextmenu.dark .litemenu-entry.submenu {
background-color: #000 !important;
}
.litegraph .litemenubar ul {
font-family: Tahoma, sans-serif;
margin: 0;
@@ -132,14 +120,13 @@
.litegraph .litemenu-entry.separator {
display: block;
border-top: 1px solid #333;
border-bottom: 1px solid #666;
border-top: 1px solid var(--border-default);
width: 100%;
height: 0px;
margin: 3px 0 2px 0;
background-color: transparent;
padding: 0 !important;
cursor: default !important;
padding: 0;
cursor: default;
}
.litegraph .litemenu-entry.has_submenu {
@@ -155,8 +142,8 @@
}
.litegraph .litemenu-entry:hover:not(.disabled):not(.separator) {
background-color: #444 !important;
color: #eee;
background-color: var(--palette-interface-panel-hover-surface);
color: var(--content-hover-fg);
transition: all 0.2s;
}
@@ -259,7 +246,8 @@
margin-top: -150px;
margin-left: -200px;
background-color: #2a2a2a;
color: var(--base-foreground);
background-color: var(--comfy-menu-bg);
min-width: 400px;
min-height: 200px;
@@ -299,8 +287,7 @@
}
.litegraph .dialog .dialog-header {
color: #aaa;
border-bottom: 1px solid #161616;
border-bottom: 1px solid var(--border-default);
}
.litegraph .dialog .dialog-header {
@@ -310,11 +297,12 @@
height: 50px;
padding: 10px;
margin: 0;
border-top: 1px solid #1a1a1a;
border-top: 1px solid var(--border-default);
}
.litegraph .dialog .dialog-header .dialog-title {
font: 20px "Arial";
font: 1rem;
font-family: Inter, Arial, sans-serif;
margin: 4px;
padding: 4px 10px;
display: inline-block;
@@ -326,7 +314,7 @@
width: 100%;
min-height: 100px;
display: inline-block;
color: #aaa;
/* color: #aaa; */
/*background-color: black;*/
overflow: auto;
}
@@ -362,8 +350,7 @@
display: block;
width: calc(100% - 4px);
height: 1px;
border-top: 1px solid #000;
border-bottom: 1px solid #333;
border-top: 1px solid var(--border-default);
margin: 10px 2px;
padding: 0;
}
@@ -373,12 +360,8 @@
padding: 4px;
}
.litegraph .dialog .property:hover {
background: #545454;
}
.litegraph .dialog .property_name {
color: #737373;
color: var(--muted-foreground);
display: inline-block;
text-align: left;
vertical-align: top;
@@ -395,8 +378,8 @@
.litegraph .dialog .property_value {
display: inline-block;
text-align: right;
color: #aaa;
background-color: #1a1a1a;
color: var(--input-text);
background-color: var(--component-node-widget-background);
/*width: calc( 100% - 122px );*/
max-width: calc(100% - 162px);
min-width: 200px;
@@ -432,18 +415,18 @@
border-radius: 4px;
padding: 4px 20px;
margin-left: 0px;
background-color: #060606;
color: #8e8e8e;
background-color: var(--secondary-background);
color: var(--base-foreground);
}
.litegraph .dialog .btn:hover {
background-color: #111;
color: #fff;
background-color: var(--secondary-background-hover);
color: var(--base-foreground);
}
.litegraph .dialog .btn.delete:hover {
background-color: #f33;
color: black;
background-color: var(--color-danger-100);
color: var(--base-foreground);
}
.litegraph .bullet_icon {
@@ -497,11 +480,11 @@
.graphmenu-entry.danger,
.litemenu-entry.danger {
color: var(--error-text) !important;
color: var(--error-text);
}
.litegraph .litemenu-entry.danger:hover:not(.disabled) {
color: var(--error-text) !important;
color: var(--error-text);
opacity: 0.8;
}
@@ -518,8 +501,7 @@
}
.graphmenu-entry.separator {
background-color: #111;
border-bottom: 1px solid #666;
background-color: var(--border-default);
height: 1px;
width: calc(100% - 20px);
-moz-width: calc(100% - 20px);
@@ -551,7 +533,7 @@
min-height: 2em;
background-color: #333;
font-size: 1.2em;
box-shadow: 0 0 10px black !important;
box-shadow: 0 0 10px black;
z-index: 10;
}

View File

@@ -7,6 +7,7 @@ import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculatio
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { LayoutSource } from '@/renderer/core/layout/types'
import { removeNodeTitleHeight } from '@/renderer/core/layout/utils/nodeSizeUtil'
import { CanvasPointer } from './CanvasPointer'
import type { ContextMenu } from './ContextMenu'
@@ -4043,16 +4044,25 @@ export class LGraphCanvas
// TODO: Report failures, i.e. `failedNodes`
const newPositions = created.map((node) => ({
nodeId: String(node.id),
bounds: {
x: node.pos[0],
y: node.pos[1],
width: node.size?.[0] ?? 100,
height: node.size?.[1] ?? 200
}
}))
const newPositions = created
.filter((item): item is LGraphNode => item instanceof LGraphNode)
.map((node) => {
const fullHeight = node.size?.[1] ?? 200
const layoutHeight = LiteGraph.vueNodesMode
? removeNodeTitleHeight(fullHeight)
: fullHeight
return {
nodeId: String(node.id),
bounds: {
x: node.pos[0],
y: node.pos[1],
width: node.size?.[0] ?? 100,
height: layoutHeight
}
}
})
if (newPositions.length) layoutStore.setSource(LayoutSource.Canvas)
layoutStore.batchUpdateNodeBounds(newPositions)
this.selectItems(created)

View File

@@ -2,7 +2,8 @@ import { LGraphNodeProperties } from '@/lib/litegraph/src/LGraphNodeProperties'
import {
calculateInputSlotPos,
calculateInputSlotPosFromSlot,
calculateOutputSlotPos
calculateOutputSlotPos,
getSlotPosition
} from '@/renderer/core/canvas/litegraph/slotCalculations'
import type { SlotPositionContext } from '@/renderer/core/canvas/litegraph/slotCalculations'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
@@ -3354,6 +3355,16 @@ export class LGraphNode
)
}
/**
* Get slot position using layout tree if available, fallback to node's position * Unified implementation used by both LitegraphLinkAdapter and useLinkLayoutSync
* @param slotIndex The slot index
* @param isInput Whether this is an input slot
* @returns Position of the slot center in graph coordinates
*/
getSlotPosition(slotIndex: number, isInput: boolean): Point {
return getSlotPosition(this, slotIndex, isInput)
}
/** @inheritdoc */
snapToGrid(snapTo: number): boolean {
return this.pinned ? false : snapPoint(this.pos, snapTo)

View File

@@ -32,6 +32,7 @@ export interface IWidgetOptions<TValues = unknown[]> {
/** Optional function to format values for display (e.g., hash → human-readable name) */
getOptionLabel?: (value?: string | null) => string
callback?: IWidget['callback']
iconClass?: string
}
interface IWidgetSliderOptions extends IWidgetOptions<number[]> {

View File

@@ -237,7 +237,7 @@
"label": "Sign Out"
},
"Experimental_ToggleVueNodes": {
"label": "Experimental: Enable Vue Nodes"
"label": "Experimental: Enable Nodes 2.0"
},
"Workspace_CloseWorkflow": {
"label": "Close Current Workflow"

View File

@@ -105,6 +105,7 @@
"dropYourFileOr": "Drop your file or",
"back": "Back",
"next": "Next",
"submit": "Submit",
"install": "Install",
"installing": "Installing",
"overwrite": "Overwrite",
@@ -123,6 +124,7 @@
"searchKeybindings": "Search Keybindings",
"searchExtensions": "Search Extensions",
"search": "Search",
"searchPlaceholder": "Search...",
"noResultsFound": "No Results Found",
"searchFailedMessage": "We couldn't find any settings matching your search. Try adjusting your search terms.",
"noTasksFound": "No Tasks Found",
@@ -130,6 +132,11 @@
"newFolder": "New Folder",
"enableAll": "Enable All",
"disableAll": "Disable All",
"enableSelected": "Enable Selected",
"disableSelected": "Disable Selected",
"disableThirdParty": "Disable Third-Party",
"core": "Core",
"custom": "Custom",
"command": "Command",
"keybinding": "Keybinding",
"upload": "Upload",
@@ -234,6 +241,7 @@
"frameNodes": "Frame Nodes",
"listening": "Listening...",
"ready": "Ready",
"playPause": "Play/Pause",
"playRecording": "Play Recording",
"playing": "Playing",
"stopPlayback": "Stop Playback",
@@ -242,7 +250,9 @@
"halfSpeed": "0.5x",
"1x": "1x",
"2x": "2x",
"beta": "BETA"
"beta": "BETA",
"profile": "Profile",
"noItems": "No items"
},
"manager": {
"title": "Custom Nodes Manager",
@@ -689,6 +699,7 @@
"currentNode": "Current node:",
"viewAllJobs": "View all jobs",
"running": "running",
"preview": "Preview",
"interruptAll": "Interrupt all running jobs",
"moreOptions": "More options",
"showAssets": "Show assets",
@@ -930,6 +941,7 @@
"thickness": "Thickness",
"opacity": "Opacity",
"hardness": "Hardness",
"stepSize": "Step Size",
"smoothingPrecision": "Smoothing Precision",
"resetToDefault": "Reset to Default",
"paintBucketSettings": "Paint Bucket Settings",
@@ -978,6 +990,7 @@
"initializingAlmostReady": "Initializing - Almost ready",
"inQueue": "In queue...",
"jobAddedToQueue": "Job added to queue",
"completedIn": "Finished in {duration}",
"jobMenu": {
"openAsWorkflowNewTab": "Open as workflow in new tab",
"openWorkflowNewTab": "Open workflow in new tab",
@@ -1107,7 +1120,9 @@
"Undo": "Undo",
"Open Sign In Dialog": "Open Sign In Dialog",
"Sign Out": "Sign Out",
"Experimental: Enable Vue Nodes": "Experimental: Enable Vue Nodes",
"Experimental: Enable Vue Nodes": "Experimental: Enable Nodes 2.0",
"Experimental: Enable Nodes 2.0": "Experimental: Enable Nodes 2.0",
"Experimental: Disable Nodes 2.0": "Experimental: Disable Nodes 2.0",
"Close Current Workflow": "Close Current Workflow",
"Next Opened Workflow": "Next Opened Workflow",
"Previous Opened Workflow": "Previous Opened Workflow",
@@ -1183,10 +1198,10 @@
"API Nodes": "API Nodes",
"Notification Preferences": "Notification Preferences",
"3DViewer": "3DViewer",
"Vue Nodes": "Vue Nodes",
"Canvas Navigation": "Canvas Navigation",
"PlanCredits": "Plan & Credits",
"VueNodes": "Vue Nodes",
"Vue Nodes": "Nodes 2.0",
"VueNodes": "Nodes 2.0",
"Nodes 2_0": "Nodes 2.0"
},
"serverConfigItems": {
@@ -1833,6 +1848,7 @@
"title": "Subscription",
"titleUnsubscribed": "Subscribe to Comfy Cloud",
"comfyCloud": "Comfy Cloud",
"comfyCloudLogo": "Comfy Cloud Logo",
"beta": "BETA",
"perMonth": "USD / month",
"renewsDate": "Renews {date}",
@@ -2068,6 +2084,7 @@
"cloudSurvey_steps_making": "What do you plan on making?",
"assetBrowser": {
"assets": "Assets",
"assetCollection": "Asset collection",
"checkpoints": "Checkpoints",
"browseAssets": "Browse Assets",
"noAssetsFound": "No assets found",
@@ -2076,17 +2093,16 @@
"connectionError": "Please check your connection and try again",
"failedToCreateNode": "Failed to create node. Please try again or check console for details.",
"noModelsInFolder": "No {type} available in this folder",
"searchAssetsPlaceholder": "Type to search...",
"uploadModel": "Import model",
"uploadModelFromCivitai": "Import a model from Civitai",
"uploadModelFailedToRetrieveMetadata": "Failed to retrieve metadata. Please check the link and try again.",
"onlyCivitaiUrlsSupported": "Only Civitai URLs are supported",
"uploadModelDescription1": "Paste a Civitai model download link to add it to your library.",
"uploadModelDescription2": "Only links from https://civitai.com are supported at the moment",
"uploadModelDescription3": "Max file size: 1 GB",
"civitaiLinkLabel": "Civitai model download link",
"uploadModelDescription2": "Only links from <a href=\"https://civitai.com\" target=\"_blank\" class=\"text-muted-foreground\">https://civitai.com</a> are supported at the moment",
"uploadModelDescription3": "Max file size: <strong>1 GB</strong>",
"civitaiLinkLabel": "Civitai model <span class=\"font-bold italic\">download</span> link",
"civitaiLinkPlaceholder": "Paste link here",
"civitaiLinkExample": "Example: https://civitai.com/api/download/models/833921?type=Model&format=SafeTensor",
"civitaiLinkExample": "<strong>Example:</strong> <a href=\"https://civitai.com/api/download/models/833921?type=Model&format=SafeTensor\" target=\"_blank\" class=\"text-muted-foreground\">https://civitai.com/api/download/models/833921?type=Model&format=SafeTensor</a>",
"confirmModelDetails": "Confirm Model Details",
"fileName": "File Name",
"fileSize": "File Size",
@@ -2118,6 +2134,9 @@
"sortZA": "Z-A",
"sortRecent": "Recent",
"sortPopular": "Popular",
"selectFrameworks": "Select Frameworks",
"selectProjects": "Select Projects",
"sortingType": "Sorting Type",
"errorFileTooLarge": "File exceeds the maximum allowed size limit",
"errorFormatNotAllowed": "Only SafeTensor format is allowed",
"errorUnsafePickleScan": "CivitAI detected potentially unsafe code in this file",
@@ -2132,6 +2151,13 @@
"media": {
"threeDModelPlaceholder": "3D Model",
"audioPlaceholder": "Audio"
},
"deletion": {
"header": "Delete this model?",
"body": "This model will be permanently removed from your library.",
"inProgress": "Deleting {assetName}...",
"complete": "{assetName} has been deleted.",
"failed": "{assetName} could not be deleted."
}
},
"mediaAsset": {
@@ -2143,9 +2169,15 @@
"deletingImportedFilesCloudOnly": "Deleting imported files is only supported in cloud version",
"failedToDeleteAsset": "Failed to delete asset",
"actions": {
"inspect": "Inspect",
"inspect": "Inspect asset",
"more": "More options",
"seeMoreOutputs": "See more outputs"
"seeMoreOutputs": "See more outputs",
"addToWorkflow": "Add to current workflow",
"download": "Download",
"openWorkflow": "Open as workflow in new tab",
"exportWorkflow": "Export workflow",
"copyJobId": "Copy job ID",
"delete": "Delete"
},
"jobIdToast": {
"jobIdCopied": "Job ID copied to clipboard",
@@ -2189,7 +2221,8 @@
}
},
"vueNodesBanner": {
"message": "Introducing Nodes 2.0 More flexible workflows, powerful new widgets, built for extensibility",
"title": "Introducing Nodes 2.0",
"desc": " More flexible workflows, powerful new widgets, built for extensibility",
"tryItOut": "Try it out"
},
"vueNodesMigration": {
@@ -2218,4 +2251,4 @@
"replacementInstruction": "Install these nodes to run this workflow, or replace them with installed alternatives. Missing nodes are highlighted in red on the canvas."
}
}
}
}

View File

@@ -336,7 +336,7 @@
},
"Comfy_VueNodes_AutoScaleLayout": {
"name": "Auto-scale layout (Nodes 2.0)",
"tooltip": "Automatically scale node positions when switching to Vue rendering to prevent overlap"
"tooltip": "Automatically scale node positions when switching to Nodes 2.0 rendering to prevent overlap"
},
"Comfy_VueNodes_Enabled": {
"name": "Modern Node Design (Nodes 2.0)",

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