Compare commits

..

68 Commits

Author SHA1 Message Date
Terry Jia
2c33e21390 allow widget expand to fill available vertical space 2025-10-23 20:35:08 -04:00
AustinMroz
671e33cf09 Fix asset browser on subgraph nodes (#6240)
When a widget is linked to a subgraph, the subgraph creates a copy of
the widget. The callback used by the asset browser to update the widget
still refers to the widget that lives inside the subgraph, but at time
of execution, this is overwritten by the unchanged value of the copy.

This is fixed by instead updating the value of the caller. It's a little
hacky, and may need future review.

See also #6237

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6240-Fix-asset-browser-on-subgraph-nodes-2956d73d365081b49bd1cd1a7a254763)
by [Unito](https://www.unito.io)
2025-10-23 15:28:08 -07:00
Christian Byrne
68f98e2624 style: change gaps between floating elements from 2 (8px) to 1 (4px) (#6226)
## Summary

Design request

**Before**

<img width="2643" height="2074" alt="Screenshot from 2025-10-23
11-16-26"
src="https://github.com/user-attachments/assets/1c93879e-bfc4-4054-b0dd-ee0cd296a7ca"
/>

**After**

<img width="2643" height="2074" alt="Screenshot from 2025-10-23
11-16-13"
src="https://github.com/user-attachments/assets/b9327dcc-9b97-424a-8b05-4f5d62beeefa"
/>

## Related

- https://github.com/Comfy-Org/ComfyUI_frontend/pull/6120

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6226-style-change-gaps-between-floating-elements-from-2-8px-to-1-4px-2956d73d3650815b8640c5f3891fe494)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-10-23 14:01:21 -07:00
Christian Byrne
8120ed9dfa load assets browser before fetch completes and show loading state (#6189)
## Summary

Moves the fetch and post-fetch logic associated with the asset browser
into the component and shows a loading state while fetching.

To test, use this branch:
https://github.com/comfyanonymous/ComfyUI/pull/10045



https://github.com/user-attachments/assets/718974d5-efc7-46a0-bcd6-e82596d4c389

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6189-load-assets-browser-before-fetch-completes-and-show-loading-state-2946d73d365081879d1bd05d86e8c036)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2025-10-23 13:43:34 -07:00
Christian Byrne
89ff8255bd open markdown links in new window/tab (#6229)
## Summary

Changes links in markdown snippets (What's New popup, node info sidebar)
to open the link in a new tab/window rather than directly navigating and
potentially losing unsaved work.



https://github.com/user-attachments/assets/24331bba-e31a-484c-bc11-12cf61805c98



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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6229-open-markdown-links-in-new-window-tab-2956d73d365081edbb1efb21cd0e2ab2)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
2025-10-23 13:33:38 -07:00
Christian Byrne
f14a6beda5 allow Vue nodes to be resized from all 4 corners (#6187)
## Summary

Enables Vue nodes to resize from all four corners and consolidated the
interaction pipeline.

## Changes

- **What**: Added four-corner handles to `LGraphNode`, wired them
through the refactored `useNodeResize` composable, and centralized the
math/preset helpers under `interactions/resize/` with cleaner pure
functions and lint-compliant markup.

## Review Focus

Corner-to-corner resizing accuracy (position + size), pinned-node guard
preventing resize start, and snap-to-grid behavior at varied zoom
levels.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6187-allow-Vue-nodes-to-be-resized-from-all-4-corners-2936d73d365081c8bf14e944ab24c27f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2025-10-23 13:24:28 -07:00
Christian Byrne
aeabc24bf2 make support URL dynamic based on distribution (#6205)
## Summary

Add query param to indicate whether support ticket is from cloud or OSS.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6205-make-support-URL-dynamic-based-on-distribution-2946d73d365081868093c52981021189)
by [Unito](https://www.unito.io)

---------

Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2025-10-23 13:20:48 -07:00
Christian Byrne
56a6ad5660 fix: node hover previews overlapping with sidebar (#6232)
Fixes https://github.com/Comfy-Org/ComfyUI_frontend/issues/6130 -- node
previews are not positioned in correct location after
https://github.com/Comfy-Org/ComfyUI_frontend/pull/5980

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6232-fix-node-hover-previews-overlapping-with-sidebar-2956d73d365081b2a470f7fb399fda99)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2025-10-23 13:11:08 -07:00
Christian Byrne
4e5eba6c54 refactor: centralize all download utils across app and apply special cloud-specific behavior (#6188)
## Summary

Centralized all download functionalities across app. Then changed
downloadFile on the cloud distribution to stream assets via blob fetches
while desktop/local retains direct anchor downloads. This fixes issue
where trying to download cross-origin resources opens them in the
window, potentially losing the user's unsaved changes.

## Changes

- **What**: Moved `downloadBlob` into `downloadUtil`, routed all callers
(3D exporter, recording manager, node template export, workflow/palette
export, Litegraph save, ~~`useDownload` consumers~~) through shared
helpers, and changed `downloadFile` to `fetch` first when `isCloud` so
cross-origin URLs download reliably
- `useDownload` is the exception since we simply cannot do model
downloads through blob (forcing user to transfer the entire model data
twice is bad). Fortunately on cloud, the user doesn't need to download
models locally anyway.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6188-refactor-centralize-all-download-utils-across-app-and-apply-special-cloud-specific-behav-2946d73d365081de9f27f0994950511d)
by [Unito](https://www.unito.io)
2025-10-23 12:08:30 -07:00
Christian Byrne
647e62d4b7 [ci] run stylelint as part of CI tests (#6143)
## Summary

Final PR continuing from

- https://github.com/Comfy-Org/ComfyUI_frontend/pull/5926
- https://github.com/Comfy-Org/ComfyUI_frontend/pull/5940

actually run stylelint in lint workflows.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6143-ci-run-stylelint-as-part-of-CI-and-pre-commit-hook-2916d73d3650811891bcc9f94379cc5f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
2025-10-23 12:06:52 -07:00
Arjan Singh
4555656bf2 feat(AssetCard): remove model size (#6227)
## Summary

Remove model file sizes.

It was confusing some users who thought that was they were being asked
to download them locally.

## Changes

- Remove `formattedSize` from `AssetDisplayItem`.
- Remove associated code.
- 

## Screenshots

<img width="1299" height="512" alt="Screenshot 2025-10-23 at 11 22
06 AM"
src="https://github.com/user-attachments/assets/625b588b-a605-49dd-97a2-16bcd604aef2"
/>

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6227-feat-AssetCard-remove-model-size-2956d73d36508155930fdb4d2db2522a)
by [Unito](https://www.unito.io)
2025-10-23 18:48:30 +00:00
Alexander Brown
1e85902242 fix: Remove old readonly and memos from LGraphNodePreview, default to empty string for widget contents. (#6217)
## Summary

Fixes the prop validation errors when the previews rendered.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6217-fix-Remove-old-readonly-and-memos-from-LGraphNodePreview-default-to-empty-string-for-wi-2956d73d3650812da482cf1fab400dfa)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-10-23 11:22:36 -07:00
ComfyUI Wiki
c436552ade Remove unnecessary Template Library translations. (#6222)
All the template translations are now maintained in the
[template](https://github.com/Comfy-Org/workflow_templates) repo, so
these translations in the FE repo are no longer needed.

<img width="696" height="762" alt="image"
src="https://github.com/user-attachments/assets/e27148c0-737d-4b8c-8513-50504e6f929c"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6222-Remove-unnecessary-Template-Library-translations-2956d73d36508194aa08fa968d8d627f)
by [Unito](https://www.unito.io)
2025-10-23 08:06:25 -07:00
AustinMroz
449a51b82d Move toasts downwards so they don't overlap run button (#6216)
<img width="431" height="127" alt="image"
src="https://github.com/user-attachments/assets/76fece3a-c001-40c7-9036-e12900869e1d"
/>

Could potentially be improved to not use magic constants.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6216-Move-toasts-downwards-so-they-don-t-overlap-run-button-2956d73d365081d0b361fa396962c806)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-10-23 07:51:39 -07:00
Jin Yi
bd3f02065a [Style] Add custom scrollbar styling for SelectBox components (#5879) 2025-10-23 23:10:07 +09:00
Johnpaul Chiwetelu
fb66637765 Don't remove labels if initial jobs were skipped (#6218)
This pull request makes a small change to the
`.github/workflows/pr-update-playwright-expectations.yaml` file,
removing the unconditional `if: always()` condition from the
`merge-and-commit` job. This means the job will now only run if its
dependencies succeed, rather than always running regardless of previous
job outcomes.
2025-10-23 06:43:07 +01:00
Johnpaul Chiwetelu
97f7c2149a Shard Update Test Expectations PR (#6100)
This pull request significantly refactors the Playwright expectations
update workflow to improve reliability, efficiency, and maintainability.
The workflow is now split into three coordinated jobs—setup, sharded
snapshot updates, and merge/commit—enabling parallel test execution and
artifact management. Key improvements include sharding Playwright
snapshot updates, robust caching and artifact handling, and more
reliable PR context handling.

**Workflow Restructuring and Sharding:**

* The workflow is split into three jobs: `setup` (prepares environment
and caches it), `update-snapshots-sharded` (runs Playwright snapshot
updates in four parallel shards), and `merge-and-commit` (merges results
and commits updates). This enables faster, more reliable snapshot
updates.
[[1]](diffhunk://#diff-0289f4b5962314fa2d58937651c3d2a0f2c6f76e26c95d6a04d43c18b3449917L15-R15)
[[2]](diffhunk://#diff-0289f4b5962314fa2d58937651c3d2a0f2c6f76e26c95d6a04d43c18b3449917R27-R175)

**Caching and Artifact Management:**

* The setup job builds and caches the entire workspace, which is then
restored by each shard for consistent environments. Each shard uploads
its updated snapshots and test reports as artifacts, which are later
downloaded and merged in the final job.

**Improved PR Context Handling:**

* PR number, branch, and comment IDs are now reliably extracted and
passed between jobs using outputs, ensuring correct association with the
PR throughout the workflow (e.g., for commenting, reactions, and pushing
updates).
[[1]](diffhunk://#diff-0289f4b5962314fa2d58937651c3d2a0f2c6f76e26c95d6a04d43c18b3449917R27-R175)
[[2]](diffhunk://#diff-0289f4b5962314fa2d58937651c3d2a0f2c6f76e26c95d6a04d43c18b3449917L92-R199)

**Job and Step Renaming/Cleanup:**

* The main job is renamed from `test` to `setup`, and redundant or
unnecessary steps (such as the old branch SHA extraction) are removed
for clarity and maintainability.
[[1]](diffhunk://#diff-0289f4b5962314fa2d58937651c3d2a0f2c6f76e26c95d6a04d43c18b3449917L15-R15)
[[2]](diffhunk://#diff-0289f4b5962314fa2d58937651c3d2a0f2c6f76e26c95d6a04d43c18b3449917R27-R175)

**Comment and Label Automation Improvements:**

* Automated GitHub comment reactions and label removals now use the
correct PR context, ensuring that feedback and status updates are
reliably posted to the right place.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6100-Shard-Update-Test-Expectations-PR-28f6d73d36508109bcd8d382c942d44d)
by [Unito](https://www.unito.io)

---------

Co-authored-by: sno <snomiao@gmail.com>
2025-10-23 06:22:44 +01:00
AustinMroz
f63d0f3289 Fix type on LoadClip being marked as asset (#6207)
Previously, asset conversion was performed on any combo widget on a
valid node. As the `type` widget on LoadClip was also a combo widget, it
was being incorrectly converted.

This PR changes the isAssetBrowserEligible check to also verify the
widget name is correct.
<img width="694" height="174" alt="image"
src="https://github.com/user-attachments/assets/a8523ade-7f59-4480-b5e6-8782fd680106"
/>


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6207-Fix-type-on-LoadClip-being-marked-as-asset-2946d73d3650811690f8d3a221a5b76d)
by [Unito](https://www.unito.io)
2025-10-22 19:37:40 -07:00
Simula_r
e5a0466e40 feat: make login button show only on dekstop (#6213)
## Summary

Make login and current user button visible on desktop only. 

## Screenshots (if applicable)

<img width="519" height="162" alt="image"
src="https://github.com/user-attachments/assets/80d86145-2bb9-4396-a02d-dc577192cb17"
/>
<img width="482" height="130" alt="image"
src="https://github.com/user-attachments/assets/cd843611-ce23-4cd4-a864-78cac1c7101f"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6213-feat-make-login-button-show-only-on-dekstop-2956d73d365081eb93f1d5da92dc38ee)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-10-23 02:16:15 +00:00
Arjan Singh
c71ec35a5f refactor(vite.config): simplify cloud config (#6211)
## Summary

Simplify the `vite.config.mts` now that we know `addAuthHeaders` is not
necessary.

## Changes

- remove `addAuthHeaders`
- simplify cloud configuration objects accordingly.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6211-refactor-vite-config-simplify-cloud-config-2956d73d365081659cfade4e289a872f)
by [Unito](https://www.unito.io)
2025-10-22 18:48:12 -07:00
AustinMroz
7b213f135e Fix copying from multi-outputs with clipspace (#6210)
`copyToClipspace` sets the `paintedIndex` and `combinedIndex` regardless
of if any editing was actually performed on the image. Previously, they
were set to be simple increments above the currently selected image.

When attempting to copy from a node previewing multiple images, these
indexes may refer to images that already exist.
This is resolved by instead setting the indexes to be used for
paintedIndex and combinedIndex to always be beyond the range of current
images.
<img width="998" height="761" alt="image"
src="https://github.com/user-attachments/assets/dacd46b3-9029-45e3-a6b3-07e971125a14"
/>


Resolves #6202

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6210-Fix-copying-from-multi-outputs-with-clipspace-2956d73d365081c5a07be4ddb0b352a3)
by [Unito](https://www.unito.io)
2025-10-22 18:00:02 -07:00
Arjan Singh
8b7b580ed4 Cloud Auth Backport (#6195)
## Summary

Backports Firebase authentication with cloud environments.

Changes only work when developing for cloud environment locally.

## Changes

- Router guards to force unauthenticated users to sign in.
- Configure auth headers for REST and Websocket connections.
- Code implemented in a way that enables build tree-shaking based on
distribution
- Updates to build process to build cloud distribution and simplify
development workflow

## Review Focus

1. Idomatic Vue/codebase patterns.
2. Build logic (please double check that I integrated correctly with:
https://github.com/Comfy-Org/ComfyUI_frontend/blob/rh-test/vite.config.mts)

## Screenshots (if applicable)




https://github.com/user-attachments/assets/ee4ea3f7-afa6-4da0-ba43-d62ed8ba4e18





┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6195-Feat-cloud-auth-backport-2946d73d365081f395f5f2a89fb7d800)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
2025-10-23 00:06:37 +00:00
Comfy Org PR Bot
bfe53d7721 1.30.2 (#6171)
Patch version increment to 1.30.2

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6171-1-30-2-2926d73d36508194b993d488cf43de6d)
by [Unito](https://www.unito.io)

---------

Co-authored-by: simula-r <18093452+simula-r@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2025-10-22 15:42:41 -07:00
AustinMroz
6368647cde Fix empty padding on nodes with previews (#6208)
Followup to #6194. Fixes nodes with image previews having empty padding.
<img width="915" height="565" alt="image"
src="https://github.com/user-attachments/assets/852a5e95-10d5-4fde-a5f1-f2f72ae5ffb6"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6208-Fix-empty-padding-on-nodes-with-previews-2946d73d365081eca299e0cfae420be3)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-10-22 15:15:03 -07:00
sno
69d37e8949 feat: Add @prettier/plugin-oxc for faster formatting (#6088)
Note for reviewers: the code changes in src/* is because I've upgraded
prettier to latest.

the @prettier/plugin-oxc it self only improve performance and doesnt
affect format rules

## Summary

Integrates `@prettier/plugin-oxc` to improve Prettier performance by
~20%.

The oxc plugin provides a faster parser written in Rust, significantly
speeding up formatting operations across the codebase.

## Changes

- Added `@prettier/plugin-oxc` as dev dependency
- Updated `.prettierrc` to use oxc plugin alongside existing
sort-imports plugin
- Added `scripts/benchmark-prettier.js` to measure performance
improvements
- Updated `knip.config.ts` to ignore the oxc plugin
- Updated `eslint.config.ts` to ignore the benchmark script

## Benchmark Results

Ran 3 benchmarks comparing formatting performance on the entire
codebase:

**Without oxc:**
- Median: 32.76s
- Average: 32.89s
- Min: 32.49s
- Max: 33.43s

**With oxc:**
- Median: 26.13s
- Average: 26.35s
- Min: 25.24s
- Max: 27.69s

**Improvement: 20.26% faster (6.64s saved)**

## Testing

The benchmark script can be run with:
```bash
node scripts/benchmark-prettier.js
```

This will:
1. Test formatting performance without oxc plugin
2. Test formatting performance with oxc plugin
3. Display comparison results
4. Restore original configuration

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6088-feat-Add-prettier-plugin-oxc-for-faster-formatting-28e6d73d365081aabb24d3af98c11bb0)
by [Unito](https://www.unito.io)

---------

Co-authored-by: DrJKL <DrJKL0424@gmail.com>
Co-authored-by: GitHub Action <action@github.com>
2025-10-22 14:49:47 -07:00
Christian Byrne
dc5d41642d disable transform settling reflow when panning the graph (#6186)
## Summary

- disable pan tracking in `useTransformSettling` so we stop wiring
high-frequency pointer listeners during canvas drags
- the post-navigation-interaction forced reflow is only necessary when
zooming since it is for fixing pixel stretch that results from `scale`
(which doesn't happen during panning/`translate`)
- extend settle delay to 512ms to reduce unnecessary reflow while
preserving post-zoom pixel fix

After this PR, there should be 0 reflows when panning the graph.

First PR in series to address:

- https://github.com/Comfy-Org/ComfyUI_frontend/issues/6151

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6186-disable-transform-settling-reflow-when-panning-the-graph-2936d73d365081c2b357e3c72d711439)
by [Unito](https://www.unito.io)

---------

Co-authored-by: DrJKL <DrJKL@users.noreply.github.com>
2025-10-22 14:33:05 -07:00
AustinMroz
2f00893b27 Expand drop zone for docking run button (#6193)
Expand the drop zone for docking the run button to be a bit more
forgiving.
<img width="494" height="99" alt="image"
src="https://github.com/user-attachments/assets/97eb6948-211d-4ed6-b06c-d6fb57b45f0b"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6193-Expand-drop-zone-for-docking-run-button-2946d73d3650816d996fdc57022161db)
by [Unito](https://www.unito.io)
2025-10-22 14:05:53 -07:00
AustinMroz
a5c29a9826 Introduce grow-parent class for widgets (#6194)
Only some widgets actually want to grow. Flex makes this difficult. This
PR sets up a `widget-expands` class that widgets can use to indicate
that they want to dynamically grow and applies it to the textarea widget
<img width="709" height="860" alt="image"
src="https://github.com/user-attachments/assets/721d99e5-5939-4531-91b5-1cda69d4e8ed"
/>


There's a potential alternative avenue with using `flex-shrink` instead
of `flex-grow`, and using `min-content` for `calculateIntrinsicSize`,
but I've been poking around with that for a while with no success.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6194-Introduce-grow-parent-class-for-widgets-2946d73d3650812f9d03c305ab04e212)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-10-22 19:17:36 +00:00
Christian Byrne
b44a39569e style: remove pulsing animation on executing Vue nodes (#6206)
## Summary

Removes pulsing animation which wasn't part of the original design and
has performance overhead.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6206-style-remove-pulsing-animation-on-executing-Vue-nodes-2946d73d3650816ab877da8120ab4085)
by [Unito](https://www.unito.io)
2025-10-22 11:45:58 -07:00
sno
8e8a45c496 [fix] Update .gitignore to properly ignore Linux core dumps (#6201)
## Summary
Fixes the .gitignore pattern for Linux core dump files from `./core` to
`/core`.

## Problem
The pattern `./core` in .gitignore doesn't work as expected. Git
interprets the `./` prefix literally, looking for a path named `./core`
rather than matching `core` at the repository root.

## Solution
Change to `/core` which is the correct gitignore syntax to ignore
files/directories named `core` at the repository root only.

## Why This Matters
- Linux systems can generate core dump files named `core` when programs
crash
- These files shouldn't be tracked in version control
- The previous pattern wasn't actually ignoring these files

## Testing
The new pattern will properly ignore `core` files at the root while not
affecting subdirectories (e.g., `src/core/` would still be tracked).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6201-fix-Update-gitignore-to-properly-ignore-Linux-core-dumps-2946d73d365081059e57d9919d03a501)
by [Unito](https://www.unito.io)
2025-10-22 18:40:14 +09:00
sno
187f59eed3 [fix] Remove pnpm cache from release-version-bump workflow (#6199)
## Summary
- Fixed the "Post Setup Node.js" failure in the release-version-bump
workflow
- Removed unnecessary pnpm cache configuration that was causing
validation errors

fixes this JOB
- [Release: Version Bump · Comfy-Org/ComfyUI_frontend@2e8e136](
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/18695441150/job/53311521564
)
<img width="1361" height="229" alt="image"
src="https://github.com/user-attachments/assets/22f780f0-59b8-4e57-ad9b-540683289a10"
/>


## Problem
The workflow was failing with error: "Path(s) specified in the action
for caching do(es) not exist, hence no cache is being saved."

This occurred because `setup-node@v4` with `cache: 'pnpm'` expects the
pnpm store directory to exist, but the workflow never runs `pnpm
install`. The workflow only executes `pnpm version`, which doesn't
require dependencies to be installed.

## Solution
Removed the `cache: 'pnpm'` configuration from the Setup Node.js step
since:
1. The workflow doesn't install dependencies
2. The cache provides no benefit for this workflow
3. It was causing the post-setup cleanup step to fail

## Test Plan
- [ ] Verify workflow runs successfully without cache errors
- [ ] Confirm version bump functionality still works correctly

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6199-fix-Remove-pnpm-cache-from-release-version-bump-workflow-2946d73d3650813dae7cf987a800e28b)
by [Unito](https://www.unito.io)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-21 23:42:56 -07:00
Alexander Brown
9cd7d06a6d Fix: Make textarea fill the area available instead of being resizable. (#6190)
## Summary

Invert the sizing of textareas. They now grow based on the container
instead of being independently resizable.

## Review Focus

Tested the behavior in Note, Markdown Note, CLIP Text Encode, and
Subgrpahs with promoted mutliline text widgets.

Anything else that might break with this?

## Screenshots (if applicable)


https://github.com/user-attachments/assets/4e2da142-d0b7-4629-9814-b637566ac1d6


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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6190-Fix-Make-textarea-fill-the-area-available-instead-of-being-resizable-2946d73d3650818a9f77c619deb93d0b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-10-21 19:31:05 -07:00
Alexander Brown
4ad8ae2634 Fix: Make breadcrumbs non-draggable. (#6191)
## Summary

Prevents accidentally trying to load the current site as if it were a
workflow.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6191-Fix-Make-breadcrumbs-non-draggable-2946d73d365081bda833c7a413903e97)
by [Unito](https://www.unito.io)
2025-10-21 18:07:48 -07:00
Benjamin Lu
e4b52e5329 fix: surface knip hook output in vscode (#6183)
## Summary
- redirect pnpm knip output to stderr so VS Code surfaces failures

## Testing
<img width="1585" height="483" alt="image"
src="https://github.com/user-attachments/assets/d65af783-d168-45cf-b01e-2b727e429a91"
/>

before it would literally just say it failed and you'd have to manually
rerun pnpm knip in ur own terminal

------
https://chatgpt.com/codex/tasks/task_e_68f7d2f2f2548330a23ae74554f1a54a
2025-10-21 17:14:50 -07:00
Christian Byrne
2346ba1af0 fix Vue node number widget inc/dec buttons hover state style (#6121)
## Summary

Fix hover state on Vue node number widget incremenet/decrement buttons.
The problem was in `useNumberWidgetButtonPt.ts`, the button hover styles
were using `var(--color-node-component-surface-hovered)` which
references a Tailwind theme color created by the `@theme` inline
directive. This theme color doesn't properly inherit the `.dark-theme`
class overrides, so it was showing the light mode color (white) even in
dark mode.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6121-fix-Vue-node-number-widget-inc-dec-buttons-hover-state-style-2906d73d36508144b91aec3490e32d28)
by [Unito](https://www.unito.io)

---------

Co-authored-by: DrJKL <DrJKL0424@gmail.com>
2025-10-21 23:58:04 +00:00
Christian Byrne
45cefda6e1 [style] unify Vue widget/slot label colors (#6149)
## Summary

Change labels on all widgets and slots to the same value which matches
design spec.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6149-style-unify-Vue-widget-slot-label-colors-2916d73d3650810a98f3ee75e0b22da0)
by [Unito](https://www.unito.io)

---------

Co-authored-by: DrJKL <DrJKL0424@gmail.com>
2025-10-21 16:50:36 -07:00
AustinMroz
cc73c42f76 Fix circular dependency in setting registration (#6184)
`Comfy.Canvas.NavigationMode` and `Comfy.Canvas.LeftMouseClickBehavior`
introduce a circular dependency where setting the value of one will set
the value of the other.

This is solved by having `NavigationMode` skip changing other settings
when `oldValue` is undefined.
- Note that `oldValue` is only undefined during initial load. When a
user changes the value for the first time, oldValue will be the default
value.

In the unlikely event desync occurs (a user manually editing the backing
json?), the registration of the subsequent `LeftMouseClickBehavior` will
still correct `NavigationMode` back to custom

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6184-Fix-circular-dependency-in-setting-registration-2936d73d365081809aa5d8bff0bf2333)
by [Unito](https://www.unito.io)
2025-10-21 14:08:36 -07:00
AustinMroz
f8490d0939 Fix links to wrong slots in vue mode (#6181)
Error is divided into 2 parts
- A widget lacking slotMetadata would create a slot overwriting the
index 0 slot
- Slot data wasn't reactive. Any dynamic widgets would not have
slotMetadata
<img width="1065" height="436" alt="image"
src="https://github.com/user-attachments/assets/c83b04fb-3b3a-4abb-8b68-99b305336348"
/>

See #5705
- Describes incorrect links internal to subgraphs. Likely a different,
already solved issue

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6181-Fix-links-to-wrong-slots-in-vue-mode-2936d73d36508136ad43d8c818bf9fba)
by [Unito](https://www.unito.io)
2025-10-21 12:36:08 -07:00
filtered
2e8e1366bd Release desktop-ui v0.0.3 (#6182)
## What's Changed

### 🐛 Bug Fixes
- Remove broken installer terminal button (#6180)

### 🔧 Maintenance
- Remove redundant npm pack step from desktop-ui publish workflow
(#6176)
2025-10-21 11:41:01 -07:00
filtered
6ef5c2602f Remove broken installer terminal button (#6180)
## Summary

Removes broken terminal toggle button that causes UX issues during
desktop installation.

## Changes

- **What**: Removes "Show Terminal" button from ServerStartView during
install flow
- **Breaking**: None (removes broken functionality)

## Review Focus

Cherry-picked from feb3c078f (v1.27.9). The button causes broken UX when
clicked during install. The working "Show Logs" button remains and takes
users directly to the log directory. This patch was written directly for
the release, in a rush to fix issues. The fix was not PR'd into main as
well (due to timing), so the original issue has now resurfaced.
2025-10-21 11:23:21 -07:00
filtered
9cf3a318eb Remove redundant npm pack step from desktop-ui publish workflow (#6176)
## Summary

Removes duplicate tarball creation from desktop-ui publish workflow -
`pnpm publish` handles this internally.

## Changes

- **What**: Removes `npm pack` step and GitHub Actions artifact upload
- **Breaking**: None - workflow behavior unchanged, publish still works
identically

## Review Focus

The `npm pack` + artifact upload was creating a duplicate of what `pnpm
publish` generates and uploads to npm anyway. Verified
`publish-frontend-types.yaml` follows this same pattern (no pack step,
direct publish).
2025-10-21 10:39:18 -07:00
filtered
668e95501a Prepare desktop-ui 0.0.2 release (#6179)
## Summary

Bumps desktop-ui version for release.

## Changes

- **What**: Version bump from 0.0.1 to 0.0.2
2025-10-21 10:37:28 -07:00
filtered
d8860c87e8 Remove 'Desktop' suffix from desktop app title (#6177)
## Summary

Removes "Desktop" suffix from the desktop app window title.

## Changes

- **What**: Changes window title from "ComfyUI Desktop" to "ComfyUI"

## Review Focus

Fixes title regression introduced during desktop UI separation.
2025-10-21 10:34:20 -07:00
filtered
6f8789b9aa Fix asset path resolution in desktop GPU picker (#6178)
## Summary

Fixes regression where desktop UI GPU picker images failed to load due
to incorrect absolute path resolution.

## Changes

- **What**: Converts absolute image paths to relative paths with `./`
prefix in GpuPicker component
- **Breaking**: None

## Review Focus

ESLint rule incorrectly flagged relative paths as errors, leading to use
of absolute paths that don't resolve correctly in desktop app context.

The change is just adding `.` to the start of two lines. ESLint rules
reorganised the rest.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6178-Fix-asset-path-resolution-in-desktop-GPU-picker-2936d73d3650814e9d0df9faf8e28733)
by [Unito](https://www.unito.io)
2025-10-21 10:30:52 -07:00
AustinMroz
9ae66c778d Fix nodeDef resolution for virtual nodes. (#6175)
<img width="697" height="250" alt="image"
src="https://github.com/user-attachments/assets/71fe7d9b-0cd6-43c6-b0d5-7dcb64d385a6"
/>

Virtual nodes (like primitives) don't have a nodeData. As a result, the
existing call to attempt lookup from a node instance fails. This is
fixed by adding `node.type` as a fallback

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6175-Fix-nodeDef-resolution-for-virtual-nodes-2936d73d365081b0abfcfe8532a50f8e)
by [Unito](https://www.unito.io)
2025-10-20 20:22:30 -07:00
sno
80013bcd5c [bugfix] Fix i18n linting errors (#6170)
## Summary
- Fix i18n linting errors by adding missing locale keys to
`src/locales/en/main.json`
- Update all affected components to use `$t()` for internationalization

## Changes
Added the following locale keys:
- `comfyOrgLogoAlt`: "ComfyOrg Logo"
- `comfy`: "Comfy"
- `pressKeysForNewBinding`: "Press keys for new binding"
- `defaultBanner`: "default banner"
- `enableOrDisablePack`: "Enable or disable pack"
- `openManager`: "Open Manager"
- `graphNavigation`: "Graph navigation"

Updated components to use i18n keys:
- `ComfyOrgHeader.vue`
- `KeybindingPanel.vue`
- `PackBanner.vue`
- `PackIcon.vue`
- `PackEnableToggle.vue`
- `LoadWorkflowWarning.vue`
- `SubgraphBreadcrumb.vue`
- `SignInContent.vue`

## Test plan
- [x] Run `pnpm lint` - all i18n linting errors resolved
- [x] Pre-commit hooks pass

Aim to make #5625 CI/CD pass.

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6170-bugfix-Fix-i18n-linting-errors-2926d73d365081c3b7fbcbbf4a8e03d6)
by [Unito](https://www.unito.io)

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-20 13:11:22 -07:00
Copilot
aa943ac565 CI: Remove .cache caching from GitHub Actions workflows (#6097)
## Overview

Removes **all `actions/cache` steps** from GitHub Actions workflows
after empirical testing showed that they actually **slow down CI/CD by
11%** rather than speeding it up.

## Context

As discussed in #5988, the codebase has evolved with components moving
into the `/packages` directory structure. The review comment suggested
removing the entire `actions/cache` step rather than just the `.cache`
path to properly evaluate performance impact.

## Performance Benchmark Results

Empirical testing on this PR (commits 38695ae0b vs ab16635c5) revealed
that **removing cache steps improves CI performance across all
workflows**:

| Workflow | WITHOUT Cache | WITH Cache | Improvement |
|----------|---------------|------------|-------------|
| **CI: Lint Format** | 208s (3m 28s) | 226s (3m 46s) | **-18s (-8.7%)**
 |
| **CI: Tests Unit** | 160s (2m 40s) | 177s (2m 57s) | **-17s (-10.6%)**
 |
| **CI: Tests Storybook** | 65s (1m 5s) | 78s (1m 18s) | **-13s
(-20.0%)**  |
| **Total Pipeline** | **433s (7m 13s)** | **481s (8m 1s)** | **-48s
(-11.1%)**  |

### Why is caching slower?

1. **Cache overhead exceeds benefits**: Time spent saving/restoring
cache > time saved from cached content
2. **Complex cache key computation**: Hash calculations for file
patterns add processing time
3. **Network I/O cost**: Each cache step adds network round-trips
4. **Tools already optimize incrementally**: ESLint, Vitest, Prettier
handle their own incremental checks efficiently

## Changes

Removed the entire `actions/cache` step from 8 workflow files:

- `ci-lint-format.yaml` - Removed tool outputs cache (.eslintcache,
.prettierCache, .knip-cache, tsconfig.tsbuildinfo)
- `ci-tests-storybook.yaml` - Removed storybook-static and
tsconfig.tsbuildinfo cache (both jobs)
- `ci-tests-unit.yaml` - Removed coverage and .vitest-cache
- `api-update-electron-api-types.yaml` - Removed tsconfig.tsbuildinfo
cache
- `api-update-manager-api-types.yaml` - Removed tool cache and
ComfyUI-Manager repo cache
- `api-update-registry-api-types.yaml` - Removed tool cache and
comfy-api repo cache
- `release-draft-create.yaml` - Removed tsconfig.tsbuildinfo cache
- `release-pypi-dev.yaml` - Removed dist and tsconfig.tsbuildinfo cache

**What remains cached:**
-  pnpm packages via `cache: 'pnpm'` in setup-node actions (the most
valuable cache)
-  Tool-specific incremental caches generated fresh each run
-  Docker layer caching (where applicable)

## Testing

-  Empirical performance testing completed (see benchmark results
above)
-  All cache steps removed successfully
-  No structural changes to workflow logic
-  pnpm package caching remains active

## Conclusion

The benchmark data clearly shows that removing `actions/cache` steps
results in **faster, simpler CI workflows**. The overhead of cache
management exceeds any benefit, especially with pnpm package caching
already handling the most time-consuming dependency installations.

**Recommendation:  Proceed with this change**

## Test Methodology

1. **WITHOUT cache** (commit
[38695ae0b](https://github.com/Comfy-Org/ComfyUI_frontend/commit/38695ae0b)):
Removed all `actions/cache` steps → [Workflow
run](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/18654024806)
2. **WITH cache** (commit
[ab16635c5](https://github.com/Comfy-Org/ComfyUI_frontend/commit/ab16635c5)):
Temporarily restored all `actions/cache` steps → [Workflow
run](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/18654143363)
3. **Final state** (commit
[3ce876f87](https://github.com/Comfy-Org/ComfyUI_frontend/commit/3ce876f87)):
Restored no-cache version (current)

Fixes #5988

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: snomiao <7323030+snomiao@users.noreply.github.com>
Co-authored-by: snomiao <snomiao@gmail.com>
Co-authored-by: Claude <noreply@anthropic.com>
2025-10-20 11:15:56 -07:00
AustinMroz
8eac19d06e Support cross domain/application copy/paste (#6087)
![AnimateDiff_00001](https://github.com/user-attachments/assets/8ae88dc5-bba8-40c0-9cc2-5e81f579761d)


Browsers place very heavy restrictions on what can be copied and pasted.
See:
- https://alexharri.com/blog/clipboard
- https://www.w3.org/TR/clipboard-apis/#mandatory-data-types-x

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6087-Experimental-cross-domain-application-copy-paste-28e6d73d36508154a0a8deeb392f43a4)
by [Unito](https://www.unito.io)
2025-10-20 10:03:15 -07:00
AustinMroz
55d2b300a6 Fix link resolution of virtual nodes (#6135)
Some virtual nodes (like get/set nodes) perform link redirection at
prompt resolution. The prior implementation incorrectly tried to return
the source of the virtual link after resolution, but this causes things
to break when the source of the virtual link is a subgraph IO.

Instead, this PR changes the code section to restart resolution from the
destination of the virtual link so that the existing subgraph boundary
resolution code is applied.

Also fix a bug with reconnection of complex/any types on
conversion to subgraph.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6135-Fix-link-resolution-of-virtual-nodes-2916d73d36508183b891ef6eb39bad4c)
by [Unito](https://www.unito.io)
2025-10-20 10:02:41 -07:00
Jin Yi
32ed446285 [bugfix] Fix enable pack functionality to use proper API endpoint (#6157)
## Summary
- Fixed issue where enabling a disabled pack incorrectly triggered
installation instead of using the dedicated enable endpoint
- Added proper `enablePack` method in the manager service layer
- Updated store and component to use the correct API call

## Problem
When users toggled a disabled pack to enable it via `PackEnableToggle`
component, the system was incorrectly calling the install endpoint with
full installation parameters instead of the simpler enable endpoint that
only requires the pack ID.

## Solution
1. **Added dedicated `enablePack` method in `comfyManagerService.ts`**:
   - Uses the `'enable'` task kind with `EnablePackParams`
   - Only requires `cnr_id` parameter (simpler than install)

2. **Updated `comfyManagerStore.ts`**:
   - Created proper `enablePack` function that queues an enable task
- Removed the incorrect aliasing where `enablePack` was pointing to
`installPack`

3. **Simplified `PackEnableToggle.vue`**:
   - Now calls `enablePack` with only required parameters (id, version)
   - Removed unnecessary installation-specific parameters

## Test plan
- [x] Enable a disabled pack and verify it uses the enable endpoint (not
install)
- [x] Confirm the pack is properly enabled after the operation
- [x] Check that the task queue shows "Enabling" message (not
"Installing")
- [x] Verify existing install/uninstall functionality still works

Fixes #6154

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6157-bugfix-Fix-enable-pack-functionality-to-use-proper-API-endpoint-2926d73d3650819fa4caf1b848f99735)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-19 23:55:21 -07:00
Christian Byrne
4066fbd2d7 disable instant queue mode on cloud (#6141)
## Summary

Remove the _Instant_ mode from the queue mode options if the
distribution target is cloud.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6141-disable-instant-queue-mode-on-cloud-2916d73d36508197920fc8e462f0be9f)
by [Unito](https://www.unito.io)
2025-10-19 23:34:44 -07:00
Christian Byrne
26f587c956 [auth] add service worker on cloud distribution to attach auth header to browser native /view requests (#6139)
## Summary

Added Service Worker to inject Firebase auth headers into browser-native
`/api/view` requests (img, video, audio tags) for cloud distribution.

## Changes

- **What**: Implemented [Service
Worker](https://developer.mozilla.org/en-US/docs/Web/API/Service_Worker_API)
to intercept and authenticate media requests that cannot natively send
custom headers
- **Dependencies**: None (uses native Service Worker API)

## Implementation Details

**Tree-shaking**: Uses compile-time `isCloud` constant - completely
removed from localhost/desktop builds (verified via bundle analysis).
Verify yourself by building the app and `grep -r
"registerAuthServiceWorker\|setupAuth" dist/`
**Caching**: 50-minute auth header cache with automatic invalidation on
login/logout to prevent redundant token fetches.

**Message Flow**:
```mermaid
sequenceDiagram
    participant IMG as Browser
    participant SW as Service Worker
    participant MT as Main Thread
    participant FB as Firebase Auth

    IMG->>SW: GET /api/view/image.png
    SW->>SW: Check cache (50min TTL)
    alt Cache miss
        SW->>MT: REQUEST_AUTH_HEADER
        MT->>FB: getAuthHeader()
        FB-->>MT: Bearer token
        MT-->>SW: AUTH_HEADER_RESPONSE
        SW->>SW: Cache token
    end
    SW->>IMG: Fetch with Authorization header

    Note over SW,MT: On login/logout: INVALIDATE_AUTH_HEADER
```

## Review Focus

- **Same-origin mode**: Service Worker uses `mode: 'same-origin'` to
allow custom headers (browser-native requests default to `no-cors` which
strips headers)
- **Request deduplication**: Prevents concurrent auth header requests
from timing out
- **Build verification**: Confirm `register-*.js` absent in localhost
builds, present (~3.2KB) in cloud builds

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6139-auth-add-service-worker-on-cloud-distribution-to-attach-auth-header-to-browser-native--2916d73d3650812698dccd07d943ab3c)
by [Unito](https://www.unito.io)
2025-10-19 22:51:37 -07:00
Christian Byrne
7ad1112535 add telemetry provider for cloud distribution (#6154)
## Summary

This code is entirely excluded from open-source, local, and desktop
builds. During minification and dead-code elimination, the Mixpanel
library is fully tree-shaken -- meaning no telemetry code is ever
included or downloaded in those builds. Even the inline callsites are
removed during the build (because `isCloud` becomes false and the entire
block becomes dead code and is removed). The code not only has no
effect, is not even distributed in the first place. We’ve gone to great
lengths to ensure this behavior.

Verification proof:


https://github.com/user-attachments/assets/b66c35f7-e233-447f-93da-4d70c433908d

Telemetry is *enabled only in the ComfyUI Cloud environment*. Its goal
is to help us understand and improve onboarding and new-user adoption.
ComfyUI aims to be accessible to everyone, but we know the learning
curve can be steep. Anonymous usage insights will help us identify where
users struggle and guide us toward making the experience more intuitive
and welcoming.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6154-add-telemetry-provider-for-cloud-distribution-2926d73d3650813cb9ccfb3a2733848b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-19 19:47:35 -07:00
Christian Byrne
522656a2dc [style] remove hover effect on Vue node socket labels (#6150)
## Summary

Align with the design by removing hover state. Hover state should not be
applied if clicking doesn't actually do anything.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6150-style-remove-hover-effect-on-Vue-node-socket-labels-2916d73d365081158edef8065edc42e8)
by [Unito](https://www.unito.io)
2025-10-19 14:29:03 -07:00
Christian Byrne
15d223ef9b [style] adjust Vue widget hover state (#6146)
## Summary

Change to match the design (hover changes background color slightly and
doesn't affect outline/border).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6146-style-adjust-Vue-widget-hover-state-2916d73d365081a19297e77208dc9613)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-10-19 12:55:29 -07:00
Christian Byrne
c8146ffc64 Revert "fix dragging video/image components on Vue nodes triggers node drag (#5922)" (#6148)
## Summary

This PR reverts #5922 which fixed pointer capture behavior on video and
image preview components to prevent unintended node dragging.

## Changes

- Removes `data-capture-node="true"` attribute from `VideoPreview.vue`
and `ImagePreview.vue` components
- Removes pointer event delegation logic from
`useNodePointerInteractions.ts` composable
- Restores previous drag behavior where dragging on preview components
triggers node drag

## Reason for Revert

This changes the behavior from original Litegraph and is generally
annoying. Users would rather be able to drag the node than be able to
drag an image/video out from a node.

Reverts #5922

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6148-Revert-fix-dragging-video-image-components-on-Vue-nodes-triggers-node-drag-5922-2916d73d365081398bb5c20384e26bb8)
by [Unito](https://www.unito.io)
2025-10-19 12:28:40 -07:00
Christian Byrne
c263f6da25 [style] prevent Vue node selection outline being obscured by image output (#6061)
## Summary

Adds `px-2` on image to prevent this issue (below) - I think there's a
better solution but I'm not really sure what it is. We use outline for
selection state and it's somewhat complex how our ring/border/outline
with many different node states and interactions works right now. It
will take some CSS skill to allow the images to be totally flush.

<img width="720" height="715" alt="image"
src="https://github.com/user-attachments/assets/0283e036-7a31-45ef-b5cc-af3ac73171c9"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6061-style-prevent-Vue-node-selection-outline-being-obscured-by-image-output-28c6d73d365081d59b34d8f91252de92)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-10-19 11:52:14 -07:00
Christian Byrne
1f5191847a make "require subscription" toggleable in build (#6144)
## Summary

Adds build time feature flags system starting with a flag that indicates
whether subscription is required to use the app. This is only used on
cloud.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6144-make-require-subscription-toggleable-in-build-2916d73d3650813bb140c5e96bcce1ce)
by [Unito](https://www.unito.io)
2025-10-19 10:30:14 -07:00
sno
fc69924c4a [feat] implement dynamic imports for locale code splitting (#6076)
## Summary
- Implement dynamic imports for internationalization (i18n) locale files
to reduce initial bundle size
- Only load English locale eagerly as default/fallback, load other
locales on-demand
- Apply code splitting to both main ComfyUI frontend and desktop-ui
applications

## Technical Details
- **Before**: All locale files (main.json, nodeDefs.json, commands.json,
settings.json) for all 9 languages were bundled in the initial
JavaScript bundle
- **After**: Only English locale files are included in initial bundle,
other locales are loaded dynamically when needed
- Implemented `loadLocale()` function that uses dynamic imports with
`Promise.all()` for efficient parallel loading
- Added locale tracking with `loadedLocales` Set to prevent duplicate
loading
- Updated both `src/i18n.ts` and `apps/desktop-ui/src/i18n.ts` with
consistent implementation

## Bundle Size Impact
This change significantly reduces the initial bundle size by removing ~8
languages worth of JSON locale data from the main bundle. Locale files
are now loaded on-demand only when users switch languages.

## Implementation
- Uses dynamic imports: `import('./locales/[locale]/[file].json')`
- Maintains backward compatibility with existing locale switching
mechanism
- Graceful error handling for unsupported locales
- No breaking changes to the public API

## Test plan
- [x] Verify initial load only includes English locale
- [x] Test dynamic locale loading when switching languages in settings
- [x] Confirm fallback behavior for unsupported locales
- [x] Validate both web and desktop-ui applications work correctly

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6076-feat-implement-dynamic-imports-for-locale-code-splitting-28d6d73d36508189ae0ef060804a5cee)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-10-18 23:05:26 -07:00
Christian Byrne
5b5151f41f [perf] manually chunk vendored code (#6137)
## Summary

Added a `manualChunks` strategy in `vite.config.mts` that splits
primevue, tiptap, chart.js, three/@xterm, core Vue/Pinia, and the
remaining dependencies into dedicated vendor bundles. This reduces the
main application chunk size and allows browsers to cache heavy
third-party code across releases, improving load times when those
libraries stay unchanged.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6137-perf-manually-chunk-vendored-code-2916d73d36508140a44ec0de228ef9cc)
by [Unito](https://www.unito.io)
2025-10-18 22:49:11 -07:00
Christian Byrne
2018f1e671 [ci] drop console statements (except warn and error) when building app (#6123)
## Summary

Marks all the console methods besides `warn` and `error` as pure
functions so they can be dropped during DCE in build pipeline. It's
simpler to use `drop` but that would remove errors/warnings.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6123-ci-drop-console-statements-except-warn-and-error-when-building-app-2906d73d365081f28303c0d784352b12)
by [Unito](https://www.unito.io)
2025-10-18 22:43:38 -07:00
Christian Byrne
8822f186e0 [style] adjust appearance of "delete account" component to be text rather than button (#6126)
Adjusts style of "Delete Account" button.

**Before**:

<img width="731" height="925" alt="Screenshot from 2025-10-18 04-22-24"
src="https://github.com/user-attachments/assets/497de4ca-9359-41d8-b944-0d69835f43b1"
/>

**After**:

<img width="731" height="925" alt="Screenshot from 2025-10-18 04-22-14"
src="https://github.com/user-attachments/assets/f42a21fb-7d4d-43f5-b27b-1f85e4470dbd"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6126-style-adjust-appearance-of-delete-account-component-to-be-text-rather-than-button-2906d73d365081eab48ece171fa4fd8a)
by [Unito](https://www.unito.io)
2025-10-18 22:37:06 -07:00
Christian Byrne
0ff1837ced [ci] allow setting GENERATE_SOURCEMAP as env var (#6134)
## Summary

Introduces a `GENERATE_SOURCEMAP` environment flag in `vite.config.mts`
that defaults to enabled (`true` unless set to `'false'`). This keeps
source maps on by default, while allowing opt-out for lean production
artifacts.

Allows the choice to be made as part of the distribution pipeline and
changed for different targets.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6134-ci-allow-setting-GENERATE_SOURCEMAP-as-env-var-2916d73d3650815d91b9eff12b6e55fd)
by [Unito](https://www.unito.io)
2025-10-18 20:23:27 -07:00
Terry Jia
7e1e8e3b65 subscription page (#6064)
Summary

Implements cloud subscription management UI and flow for ComfyUI Cloud
users.

  Core Features:
- Subscription Status Tracking: Global reactive state management for
subscription status across all components
  using shared subscriptionStatus ref
- Subscribe to Run Button: Replaces the Run button in the actionbar with
a "Subscribe to Run" button for users
  without active subscriptions
- Subscription Required Dialog: Modal dialog with subscription benefits,
pricing, and checkout flow with video
  background
- Subscription Settings Panel: New settings panel showing subscription
status, renewal date, and quick access to
  billing management
- Auto-detection & Polling: Automatically polls subscription status
after checkout completion and syncs state
  across the application


https://github.com/user-attachments/assets/f41b8e6a-5845-48a7-8169-3a6fc0d2e5c8



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6064-subscription-page-28d6d73d36508135a2a0fe7c94b40852)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2025-10-18 20:21:30 -07:00
Christian Byrne
d83e34d0fc [ci] collapsible sections in ci size report comments (#6118)
## Summary

Adjust size reporting scripts to aggregate baseline/current metrics,
render collapsible GitHub-friendly tables using `<details>`, and
expanded bundle categorisation heuristics

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6118-ci-collapsible-sections-in-ci-size-report-comments-2906d73d365081d0b302c09fa1b98ccb)
by [Unito](https://www.unito.io)
2025-10-18 20:15:51 -07:00
Christian Byrne
c6b528b8be [ci] allow manual workflow dispatch to do version bumping on core branches (rather than just on main) (#6117)
## Summary

Added configurable base branch selection to version bump workflows,
enabling patch releases from `core/*` branches via GitHub Actions UI.

## Changes

- **What**: Extended [workflow_dispatch
inputs](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_dispatch)
with `branch` parameter for both main frontend and desktop-ui version
bump workflows
- **Validation**: Added branch existence check that lists available
`core/*` branches on error
- **Workflow modifications**:
- `release-version-bump.yaml`: Checkout and create PRs targeting
user-specified branch
- `version-bump-desktop-ui.yaml`: Same behavior for desktop-ui releases

## Review Focus

Branch validation logic correctly handles both local (`refs/heads/`) and
remote (`refs/remotes/origin/`) refs. Default value preserves backward
compatibility for release sheriffs unfamiliar with new feature.

## Use Case

Previously, patch releases from `core/1.29` required manual version
bumping. Now maintainers can trigger from Actions UI with dropdown
selections.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6117-ci-allow-manual-workflow-dispatch-to-do-version-bumping-on-core-branches-rather-than-j-2906d73d365081cba3aff46471206a9e)
by [Unito](https://www.unito.io)
2025-10-18 13:04:49 -07:00
Christian Byrne
37d2f25d2f [style] ensure all floating menus have same gap from screen edges (#6120)
## Summary

Ensures all the floating shell app elements (graph canvas menu, minmap,
floating sidebar) all have .5rem gap between edge and viewport. Was not
proper distance for graph canvas menu and minimap due to splitter
overlay (parent) itself having 8px gap from viewport.

**After**:

<img width="605" height="535" alt="Screenshot from 2025-10-18 01-34-51"
src="https://github.com/user-attachments/assets/215efd5c-541f-441a-8b0d-1eb1f6fce672"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6120-style-ensure-all-floating-menus-have-same-gap-from-screen-edges-2906d73d365081598ae4d16f2380baf8)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2025-10-18 05:07:06 -07:00
Christian Byrne
3dcabaeb0a Mark failing vue nodes bypass test as fixme temporarily (#6127)
## Summary

Mark this test as `fixme` while I fix it. Don't want to disrupt CI
signal for other repo maintainers - the test has already been broken for
nearly 2 days now which is not acceptable.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6127-Mark-failing-vue-nodes-bypass-test-as-fixme-temporarily-2906d73d36508186a1def1d2dc7f85a5)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2025-10-18 05:06:24 -07:00
207 changed files with 5310 additions and 4562 deletions

View File

@@ -5,6 +5,10 @@ PLAYWRIGHT_TEST_URL=http://localhost:5173
# Proxy target of the local development server
# Note: localhost:8188 does not work.
# Cloud auto-detection: Setting this to any *.comfy.org URL automatically enables
# cloud mode (DISTRIBUTION=cloud) without needing to set DISTRIBUTION separately.
# Examples: https://testcloud.comfy.org/, https://stagingcloud.comfy.org/,
# https://pr-123.testenvs.comfy.org/, https://cloud.comfy.org/
DEV_SERVER_COMFYUI_URL=http://127.0.0.1:8188
# Allow dev server access from remote IP addresses.

View File

@@ -26,15 +26,6 @@ jobs:
node-version: lts/*
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
key: electron-types-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
electron-types-tools-cache-${{ runner.os }}-
- name: Update electron types
run: pnpm install --workspace-root @comfyorg/comfyui-electron-types@latest

View File

@@ -31,26 +31,9 @@ jobs:
node-version: lts/*
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
key: update-manager-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
update-manager-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Cache ComfyUI-Manager repository
uses: actions/cache@v4
with:
path: ComfyUI-Manager
key: comfyui-manager-repo-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
comfyui-manager-repo-${{ runner.os }}-
- name: Checkout ComfyUI-Manager repository
uses: actions/checkout@v5
with:

View File

@@ -30,26 +30,9 @@ jobs:
node-version: lts/*
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
key: update-registry-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
update-registry-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Cache comfy-api repository
uses: actions/cache@v4
with:
path: comfy-api
key: comfy-api-repo-${{ runner.os }}-${{ github.run_id }}
restore-keys: |
comfy-api-repo-${{ runner.os }}-
- name: Checkout comfy-api repository
uses: actions/checkout@v5
with:

View File

@@ -33,27 +33,15 @@ jobs:
node-version: 'lts/*'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
.eslintcache
tsconfig.tsbuildinfo
.prettierCache
.knip-cache
key: lint-format-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js,mts}', '*.config.*', '.eslintrc.*', '.prettierrc.*', 'tsconfig.json') }}
restore-keys: |
lint-format-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
lint-format-cache-${{ runner.os }}-
ci-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Run ESLint with auto-fix
run: pnpm lint:fix
- name: Run Stylelint with auto-fix
run: pnpm stylelint:fix
- name: Run Prettier with auto-format
run: pnpm format
@@ -78,6 +66,7 @@ jobs:
- name: Final validation
run: |
pnpm lint
pnpm stylelint
pnpm format:check
pnpm knip

View File

@@ -50,19 +50,6 @@ jobs:
node-version: '20'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
storybook-static
tsconfig.tsbuildinfo
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
restore-keys: |
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
storybook-cache-${{ runner.os }}-
storybook-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile
@@ -115,19 +102,6 @@ jobs:
node-version: '20'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
storybook-static
tsconfig.tsbuildinfo
key: storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', '*.config.*', '.storybook/**/*') }}
restore-keys: |
storybook-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
storybook-cache-${{ runner.os }}-
storybook-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile

View File

@@ -29,19 +29,6 @@ jobs:
node-version: "lts/*"
cache: "pnpm"
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
coverage
.vitest-cache
key: vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-${{ hashFiles('src/**/*.{ts,vue,js}', 'vitest.config.*', 'tsconfig.json') }}
restore-keys: |
vitest-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}-
vitest-cache-${{ runner.os }}-
test-tools-cache-${{ runner.os }}-
- name: Install dependencies
run: pnpm install --frozen-lockfile

View File

@@ -12,11 +12,11 @@ concurrency:
cancel-in-progress: true
jobs:
test:
setup:
runs-on: ubuntu-latest
if: >
( github.event_name == 'pull_request' && github.event.label.name == 'New Browser Test Expectations' ) ||
( github.event.issue.pull_request &&
( github.event.issue.pull_request &&
github.event_name == 'issue_comment' &&
(
github.event.comment.author_association == 'OWNER' ||
@@ -24,12 +24,25 @@ jobs:
github.event.comment.author_association == 'COLLABORATOR'
) &&
startsWith(github.event.comment.body, '/update-playwright') )
outputs:
cache-key: ${{ steps.cache-key.outputs.key }}
pr-number: ${{ steps.pr-info.outputs.pr-number }}
branch: ${{ steps.pr-info.outputs.branch }}
comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
steps:
- name: Get PR info
id: pr-info
run: |
echo "pr-number=${{ github.event.number || github.event.issue.number }}" >> $GITHUB_OUTPUT
echo "branch=$(gh pr view ${{ github.event.number || github.event.issue.number }} --repo ${{ github.repository }} --json headRefName --jq '.headRefName')" >> $GITHUB_OUTPUT
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Find Update Comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
id: "find-update-comment"
with:
issue-number: ${{ github.event.number || github.event.issue.number }}
issue-number: ${{ steps.pr-info.outputs.pr-number }}
comment-author: "github-actions[bot]"
body-includes: "Updating Playwright Expectations"
@@ -37,49 +50,176 @@ jobs:
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
with:
comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
issue-number: ${{ github.event.number || github.event.issue.number }}
issue-number: ${{ steps.pr-info.outputs.pr-number }}
body: |
Updating Playwright Expectations
edit-mode: replace
reactions: eyes
- name: Get Branch SHA
id: "get-branch"
run: echo ::set-output name=branch::$(gh pr view $PR_NO --repo $REPO --json headRefName --jq '.headRefName')
env:
REPO: ${{ github.repository }}
PR_NO: ${{ github.event.number || github.event.issue.number }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Initial Checkout
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ steps.get-branch.outputs.branch }}
- name: Setup Frontend
ref: ${{ steps.pr-info.outputs.branch }}
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Setup ComfyUI Server
# Save expensive build artifacts (Python env, built frontend, node_modules)
# Source code will be checked out fresh in sharded jobs
- name: Generate cache key
id: cache-key
run: echo "key=$(date +%s)" >> $GITHUB_OUTPUT
- name: Save cache
uses: actions/cache/save@5a3ec84eff668545956fd18022155c47e93e2684
with:
path: |
ComfyUI
dist
key: comfyui-setup-${{ steps.cache-key.outputs.key }}
# Sharded snapshot updates
update-snapshots-sharded:
needs: setup
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
steps:
# Checkout source code fresh (not cached)
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ needs.setup.outputs.branch }}
# Restore expensive build artifacts from setup job
- name: Restore cached artifacts
uses: actions/cache/restore@5a3ec84eff668545956fd18022155c47e93e2684
with:
fail-on-cache-miss: true
path: |
ComfyUI
dist
key: comfyui-setup-${{ needs.setup.outputs.cache-key }}
- name: Setup ComfyUI server (from cache)
uses: ./.github/actions/setup-comfyui-server
with:
launch_server: true
- name: Setup nodejs, pnpm, reuse built frontend
uses: ./.github/actions/setup-frontend
- name: Setup Playwright
uses: ./.github/actions/setup-playwright
- name: Run Playwright tests and update snapshots
# Run sharded tests with snapshot updates
- name: Update snapshots (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
id: playwright-tests
run: pnpm exec playwright test --update-snapshots
run: pnpm exec playwright test --update-snapshots --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
continue-on-error: true
- uses: actions/upload-artifact@v4
# Upload updated snapshots from this shard
- name: Upload updated snapshots
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report
name: snapshots-shard-${{ matrix.shardIndex }}
path: browser_tests/**/*-snapshots/
retention-days: 1
- name: Upload test report
uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-shard-${{ matrix.shardIndex }}
path: ./playwright-report/
retention-days: 30
# Merge snapshots and commit
merge-and-commit:
needs: [setup, update-snapshots-sharded]
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ needs.setup.outputs.branch }}
# Download all snapshot artifacts from shards
- name: Download all snapshots
uses: actions/download-artifact@v4
with:
pattern: snapshots-shard-*
path: ./downloaded-snapshots
merge-multiple: false
# Merge snapshots from all shards into browser_tests directory
- name: Merge snapshots
run: |
set -euo pipefail
echo "Merging snapshots from all shards..."
# Verify target directory exists
if [ ! -d "browser_tests" ]; then
echo "::error::Target directory 'browser_tests' does not exist"
exit 1
fi
# List all downloaded artifacts for debugging
echo "Available shard artifacts:"
ls -la ./downloaded-snapshots/ || echo "No downloaded-snapshots directory found"
merged_count=0
skipped_count=0
for shard_dir in ./downloaded-snapshots/snapshots-shard-*; do
echo "Processing: $shard_dir"
# Check if glob matched any directories
if [ ! -e "$shard_dir" ]; then
echo "::warning::No shard directories found matching pattern"
break
fi
if [ ! -d "$shard_dir" ]; then
echo "::warning::Expected directory but found file: $shard_dir"
skipped_count=$((skipped_count + 1))
continue
fi
# Check if directory has content (empty is OK - shard might have no changes)
if [ -z "$(ls -A "$shard_dir" 2>/dev/null)" ]; then
echo "Shard directory $shard_dir is empty (no snapshots updated in this shard)"
skipped_count=$((skipped_count + 1))
continue
fi
echo "Merging snapshots from $shard_dir..."
# The artifact path 'browser_tests/**/*-snapshots/' uploads relative to browser_tests/
# So downloaded artifacts contain 'tests/' directly, not 'browser_tests/tests/'
# We need to merge the artifact contents into browser_tests/
if ! rsync -a "$shard_dir/" browser_tests/; then
echo "::error::Failed to merge snapshots from $shard_dir/ to browser_tests/"
echo "Contents of $shard_dir:"
find "$shard_dir" -type f | head -20
exit 1
fi
merged_count=$((merged_count + 1))
echo "Successfully merged shard $merged_count"
done
echo "Snapshot merge complete. Merged: $merged_count shard(s), Skipped: $skipped_count shard(s)"
- name: Debugging info
run: |
echo "PR: ${{ github.event.issue.number }}"
echo "Branch: ${{ steps.get-branch.outputs.branch }}"
echo "PR: ${{ needs.setup.outputs.pr-number }}"
echo "Branch: ${{ needs.setup.outputs.branch }}"
git status
- name: Commit updated expectations
run: |
git config --global user.name 'github-actions'
@@ -89,20 +229,20 @@ jobs:
echo "No changes to commit"
else
git commit -m "[automated] Update test expectations"
git push origin ${{ steps.get-branch.outputs.branch }}
git push origin ${{ needs.setup.outputs.branch }}
fi
- name: Add Done Reaction
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
if: github.event_name == 'issue_comment'
with:
comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
issue-number: ${{ github.event.number || github.event.issue.number }}
comment-id: ${{ needs.setup.outputs.comment-id }}
issue-number: ${{ needs.setup.outputs.pr-number }}
reactions: +1
reactions-edit-mode: replace
- name: Remove New Browser Test Expectations label
if: always() && github.event_name == 'pull_request'
run: gh pr edit ${{ github.event.pull_request.number }} --remove-label "New Browser Test Expectations"
run: gh pr edit ${{ needs.setup.outputs.pr-number }} --remove-label "New Browser Test Expectations"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -161,20 +161,6 @@ jobs:
echo "publish_dir=$PUBLISH_DIR" >> "$GITHUB_OUTPUT"
echo "name=$NAME" >> "$GITHUB_OUTPUT"
- name: Pack (preview only)
shell: bash
working-directory: ${{ steps.pkg.outputs.publish_dir }}
run: |
set -euo pipefail
npm pack --json | tee pack-result.json
- name: Upload package tarball artifact
uses: actions/upload-artifact@v4
with:
name: desktop-ui-npm-tarball-${{ inputs.version }}
path: ${{ steps.pkg.outputs.publish_dir }}/*.tgz
if-no-files-found: error
- name: Check if version already on npm
id: check_npm
env:

View File

@@ -28,16 +28,6 @@ jobs:
node-version: 'lts/*'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
tsconfig.tsbuildinfo
key: release-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
release-tools-cache-${{ runner.os }}-
- name: Get current version
id: current_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT

View File

@@ -25,17 +25,6 @@ jobs:
node-version: 'lts/*'
cache: 'pnpm'
- name: Cache tool outputs
uses: actions/cache@v4
with:
path: |
.cache
dist
tsconfig.tsbuildinfo
key: dev-release-tools-cache-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
dev-release-tools-cache-${{ runner.os }}-
- name: Get current version
id: current_version
run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT

View File

@@ -15,6 +15,11 @@ on:
required: false
default: ''
type: string
branch:
description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
required: true
default: 'main'
type: string
jobs:
bump-version:
@@ -26,6 +31,24 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.branch }}
fetch-depth: 0
- name: Validate branch exists
run: |
BRANCH="${{ github.event.inputs.branch }}"
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
echo "❌ Branch '$BRANCH' does not exist"
echo ""
echo "Available core branches:"
git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
echo ""
echo "Main branch:"
echo " - main"
exit 1
fi
echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm
uses: pnpm/action-setup@v4
@@ -36,7 +59,6 @@ jobs:
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'pnpm'
- name: Bump version
id: bump-version
@@ -59,7 +81,9 @@ jobs:
title: ${{ steps.bump-version.outputs.NEW_VERSION }}
body: |
${{ steps.capitalised.outputs.capitalised }} version increment to ${{ steps.bump-version.outputs.NEW_VERSION }}
**Base branch:** `${{ github.event.inputs.branch }}`
branch: version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
base: main
base: ${{ github.event.inputs.branch }}
labels: |
Release

View File

@@ -14,6 +14,11 @@ on:
required: false
default: ''
type: string
branch:
description: 'Base branch to bump (e.g., main, core/1.29, core/1.30)'
required: true
default: 'main'
type: string
jobs:
bump-version-desktop-ui:
@@ -26,8 +31,25 @@ jobs:
- name: Checkout repository
uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.branch }}
fetch-depth: 0
persist-credentials: false
- name: Validate branch exists
run: |
BRANCH="${{ github.event.inputs.branch }}"
if ! git show-ref --verify --quiet "refs/heads/$BRANCH" && ! git show-ref --verify --quiet "refs/remotes/origin/$BRANCH"; then
echo "❌ Branch '$BRANCH' does not exist"
echo ""
echo "Available core branches:"
git branch -r | grep 'origin/core/' | sed 's/.*origin\// - /' || echo " (none found)"
echo ""
echo "Main branch:"
echo " - main"
exit 1
fi
echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
@@ -64,8 +86,10 @@ jobs:
title: desktop-ui ${{ steps.bump-version.outputs.NEW_VERSION }}
body: |
${{ steps.capitalised.outputs.capitalised }} version increment for @comfyorg/desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}
**Base branch:** `${{ github.event.inputs.branch }}`
branch: desktop-ui-version-bump-${{ steps.bump-version.outputs.NEW_VERSION }}
base: main
base: ${{ github.event.inputs.branch }}
labels: |
Release

2
.gitignore vendored
View File

@@ -78,7 +78,7 @@ templates_repo/
vite.config.mts.timestamp-*.mjs
# Linux core dumps
./core
/core
*storybook.log
storybook-static

View File

@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Run Knip with cache via package script
pnpm knip
pnpm knip 1>&2

View File

@@ -7,12 +7,5 @@
"importOrder": ["^@core/(.*)$", "<THIRD_PARTY_MODULES>", "^@/(.*)$", "^[./]"],
"importOrderSeparation": true,
"importOrderSortSpecifiers": true,
"overrides": [
{
"files": "*.{js,cjs,mjs,ts,cts,mts,tsx,vue}",
"options": {
"plugins": ["@trivago/prettier-plugin-sort-imports"]
}
}
]
"plugins": ["@prettier/plugin-oxc", "@trivago/prettier-plugin-sort-imports"]
}

View File

@@ -2,7 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>ComfyUI Desktop</title>
<title>ComfyUI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" />
</head>
<body>

View File

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

View File

@@ -1,17 +1,17 @@
<template>
<div
class="grid grid-rows-[1fr_auto_auto_1fr] w-full max-w-3xl mx-auto h-[40rem] select-none"
class="mx-auto grid h-[40rem] w-full max-w-3xl grid-rows-[1fr_auto_auto_1fr] select-none"
>
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
<h2 class="text-center font-inter text-3xl font-bold text-neutral-100">
{{ $t('install.gpuPicker.title') }}
</h2>
<!-- GPU Selection buttons - takes up remaining space and centers content -->
<div class="flex-1 flex gap-8 justify-center items-center">
<div class="flex flex-1 items-center justify-center gap-8">
<!-- Apple Metal / NVIDIA -->
<HardwareOption
v-if="platform === 'darwin'"
:image-path="'/assets/images/apple-mps-logo.png'"
:image-path="'./assets/images/apple-mps-logo.png'"
placeholder-text="Apple Metal"
subtitle="Apple Metal"
:value="'mps'"
@@ -21,7 +21,7 @@
/>
<HardwareOption
v-else
:image-path="'/assets/images/nvidia-logo-square.jpg'"
:image-path="'./assets/images/nvidia-logo-square.jpg'"
placeholder-text="NVIDIA"
:subtitle="$t('install.gpuPicker.nvidiaSubtitle')"
:value="'nvidia'"
@@ -47,17 +47,17 @@
/>
</div>
<div class="pt-12 px-24 h-16">
<div class="h-16 px-24 pt-12">
<div v-show="showRecommendedBadge" class="flex items-center gap-2">
<Tag
:value="$t('install.gpuPicker.recommended')"
class="bg-neutral-300 text-neutral-900 rounded-full text-sm font-bold px-2 py-[1px]"
class="rounded-full bg-neutral-300 px-2 py-[1px] text-sm font-bold text-neutral-900"
/>
<i class="icon-[lucide--badge-check] text-neutral-300 text-lg" />
<i class="icon-[lucide--badge-check] text-lg text-neutral-300" />
</div>
</div>
<div class="text-neutral-300 px-24">
<div class="px-24 text-neutral-300">
<p v-show="descriptionText" class="leading-relaxed">
{{ descriptionText }}
</p>

View File

@@ -1,67 +1,163 @@
import arCommands from '@frontend-locales/ar/commands.json' with { type: 'json' }
import ar from '@frontend-locales/ar/main.json' with { type: 'json' }
import arNodes from '@frontend-locales/ar/nodeDefs.json' with { type: 'json' }
import arSettings from '@frontend-locales/ar/settings.json' with { type: 'json' }
// Import only English locale eagerly as the default/fallback
// ESLint cannot statically resolve dynamic imports with path aliases (@frontend-locales/*),
// but these are properly configured in tsconfig.json and resolved by Vite at build time.
// eslint-disable-next-line import-x/no-unresolved
import enCommands from '@frontend-locales/en/commands.json' with { type: 'json' }
// eslint-disable-next-line import-x/no-unresolved
import en from '@frontend-locales/en/main.json' with { type: 'json' }
// eslint-disable-next-line import-x/no-unresolved
import enNodes from '@frontend-locales/en/nodeDefs.json' with { type: 'json' }
// eslint-disable-next-line import-x/no-unresolved
import enSettings from '@frontend-locales/en/settings.json' with { type: 'json' }
import esCommands from '@frontend-locales/es/commands.json' with { type: 'json' }
import es from '@frontend-locales/es/main.json' with { type: 'json' }
import esNodes from '@frontend-locales/es/nodeDefs.json' with { type: 'json' }
import esSettings from '@frontend-locales/es/settings.json' with { type: 'json' }
import frCommands from '@frontend-locales/fr/commands.json' with { type: 'json' }
import fr from '@frontend-locales/fr/main.json' with { type: 'json' }
import frNodes from '@frontend-locales/fr/nodeDefs.json' with { type: 'json' }
import frSettings from '@frontend-locales/fr/settings.json' with { type: 'json' }
import jaCommands from '@frontend-locales/ja/commands.json' with { type: 'json' }
import ja from '@frontend-locales/ja/main.json' with { type: 'json' }
import jaNodes from '@frontend-locales/ja/nodeDefs.json' with { type: 'json' }
import jaSettings from '@frontend-locales/ja/settings.json' with { type: 'json' }
import koCommands from '@frontend-locales/ko/commands.json' with { type: 'json' }
import ko from '@frontend-locales/ko/main.json' with { type: 'json' }
import koNodes from '@frontend-locales/ko/nodeDefs.json' with { type: 'json' }
import koSettings from '@frontend-locales/ko/settings.json' with { type: 'json' }
import ruCommands from '@frontend-locales/ru/commands.json' with { type: 'json' }
import ru from '@frontend-locales/ru/main.json' with { type: 'json' }
import ruNodes from '@frontend-locales/ru/nodeDefs.json' with { type: 'json' }
import ruSettings from '@frontend-locales/ru/settings.json' with { type: 'json' }
import trCommands from '@frontend-locales/tr/commands.json' with { type: 'json' }
import tr from '@frontend-locales/tr/main.json' with { type: 'json' }
import trNodes from '@frontend-locales/tr/nodeDefs.json' with { type: 'json' }
import trSettings from '@frontend-locales/tr/settings.json' with { type: 'json' }
import zhTWCommands from '@frontend-locales/zh-TW/commands.json' with { type: 'json' }
import zhTW from '@frontend-locales/zh-TW/main.json' with { type: 'json' }
import zhTWNodes from '@frontend-locales/zh-TW/nodeDefs.json' with { type: 'json' }
import zhTWSettings from '@frontend-locales/zh-TW/settings.json' with { type: 'json' }
import zhCommands from '@frontend-locales/zh/commands.json' with { type: 'json' }
import zh from '@frontend-locales/zh/main.json' with { type: 'json' }
import zhNodes from '@frontend-locales/zh/nodeDefs.json' with { type: 'json' }
import zhSettings from '@frontend-locales/zh/settings.json' with { type: 'json' }
import { createI18n } from 'vue-i18n'
function buildLocale<M, N, C, S>(main: M, nodes: N, commands: C, settings: S) {
function buildLocale<
M extends Record<string, unknown>,
N extends Record<string, unknown>,
C extends Record<string, unknown>,
S extends Record<string, unknown>
>(main: M, nodes: N, commands: C, settings: S) {
return {
...main,
nodeDefs: nodes,
commands: commands,
settings: settings
}
} as M & { nodeDefs: N; commands: C; settings: S }
}
const messages = {
en: buildLocale(en, enNodes, enCommands, enSettings),
zh: buildLocale(zh, zhNodes, zhCommands, zhSettings),
'zh-TW': buildLocale(zhTW, zhTWNodes, zhTWCommands, zhTWSettings),
ru: buildLocale(ru, ruNodes, ruCommands, ruSettings),
ja: buildLocale(ja, jaNodes, jaCommands, jaSettings),
ko: buildLocale(ko, koNodes, koCommands, koSettings),
fr: buildLocale(fr, frNodes, frCommands, frSettings),
es: buildLocale(es, esNodes, esCommands, esSettings),
ar: buildLocale(ar, arNodes, arCommands, arSettings),
tr: buildLocale(tr, trNodes, trCommands, trSettings)
// Locale loader map - dynamically import locales only when needed
// ESLint cannot statically resolve these dynamic imports, but they are valid at build time
/* eslint-disable import-x/no-unresolved */
const localeLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('@frontend-locales/ar/main.json'),
es: () => import('@frontend-locales/es/main.json'),
fr: () => import('@frontend-locales/fr/main.json'),
ja: () => import('@frontend-locales/ja/main.json'),
ko: () => import('@frontend-locales/ko/main.json'),
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')
}
const nodeDefsLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('@frontend-locales/ar/nodeDefs.json'),
es: () => import('@frontend-locales/es/nodeDefs.json'),
fr: () => import('@frontend-locales/fr/nodeDefs.json'),
ja: () => import('@frontend-locales/ja/nodeDefs.json'),
ko: () => import('@frontend-locales/ko/nodeDefs.json'),
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')
}
const commandsLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('@frontend-locales/ar/commands.json'),
es: () => import('@frontend-locales/es/commands.json'),
fr: () => import('@frontend-locales/fr/commands.json'),
ja: () => import('@frontend-locales/ja/commands.json'),
ko: () => import('@frontend-locales/ko/commands.json'),
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')
}
const settingsLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('@frontend-locales/ar/settings.json'),
es: () => import('@frontend-locales/es/settings.json'),
fr: () => import('@frontend-locales/fr/settings.json'),
ja: () => import('@frontend-locales/ja/settings.json'),
ko: () => import('@frontend-locales/ko/settings.json'),
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')
}
// Track which locales have been loaded
const loadedLocales = new Set<string>(['en'])
// Track locales currently being loaded to prevent race conditions
const loadingLocales = new Map<string, Promise<void>>()
/**
* Dynamically load a locale and its associated files (nodeDefs, commands, settings)
*/
export async function loadLocale(locale: string): Promise<void> {
if (loadedLocales.has(locale)) {
return
}
// If already loading, return the existing promise to prevent duplicate loads
const existingLoad = loadingLocales.get(locale)
if (existingLoad) {
return existingLoad
}
const loader = localeLoaders[locale]
const nodeDefsLoader = nodeDefsLoaders[locale]
const commandsLoader = commandsLoaders[locale]
const settingsLoader = settingsLoaders[locale]
if (!loader || !nodeDefsLoader || !commandsLoader || !settingsLoader) {
console.warn(`Locale "${locale}" is not supported`)
return
}
// Create and track the loading promise
const loadPromise = (async () => {
try {
const [main, nodes, commands, settings] = await Promise.all([
loader(),
nodeDefsLoader(),
commandsLoader(),
settingsLoader()
])
const messages = buildLocale(
main.default,
nodes.default,
commands.default,
settings.default
)
i18n.global.setLocaleMessage(locale, messages as LocaleMessages)
loadedLocales.add(locale)
} catch (error) {
console.error(`Failed to load locale "${locale}":`, error)
throw error
} finally {
// Clean up the loading promise once complete
loadingLocales.delete(locale)
}
})()
loadingLocales.set(locale, loadPromise)
return loadPromise
}
// Only include English in the initial bundle
const messages = {
en: buildLocale(en, enNodes, enCommands, enSettings)
}
// Type for locale messages - inferred from the English locale structure
type LocaleMessages = typeof messages.en
export const i18n = createI18n({
// Must set `false`, as Vue I18n Legacy API is for Vue 2
legacy: false,

View File

@@ -66,17 +66,6 @@
@click="troubleshoot"
/>
</div>
<div class="text-center">
<button
v-if="!terminalVisible"
class="text-sm text-neutral-500 hover:text-neutral-300 transition-colors flex items-center gap-2 mx-auto"
@click="terminalVisible = true"
>
<i class="pi pi-search"></i>
{{ $t('serverStart.showTerminal') }}
</button>
</div>
</div>
<!-- Terminal Output (positioned at bottom when manually toggled in error state) -->

View File

@@ -13,7 +13,7 @@ export class ComfyActionbar {
async isDocked() {
const className = await this.root.getAttribute('class')
return className?.includes('is-docked') ?? false
return className?.includes('static') ?? false
}
}

View File

@@ -301,7 +301,9 @@ test.describe('Settings', () => {
})
test.describe('Support', () => {
test('Should open external zendesk link', async ({ comfyPage }) => {
test('Should open external zendesk link with OSS tag', async ({
comfyPage
}) => {
await comfyPage.setSetting('Comfy.UseNewMenu', 'Top')
const pagePromise = comfyPage.page.context().waitForEvent('page')
await comfyPage.menu.topbar.triggerTopbarCommand(['Help', 'Support'])
@@ -309,6 +311,10 @@ test.describe('Support', () => {
await newPage.waitForLoadState('networkidle')
await expect(newPage).toHaveURL(/.*support\.comfy\.org.*/)
const url = new URL(newPage.url())
expect(url.searchParams.get('tf_42243568391700')).toBe('oss')
await newPage.close()
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 99 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: 105 KiB

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@@ -12,24 +12,26 @@ test.describe('Vue Node Bypass', () => {
await comfyPage.vueNodes.waitForNodes()
})
test('should allow toggling bypass on a selected node with hotkey', async ({
comfyPage
}) => {
await comfyPage.setup()
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
test.fixme(
'should allow toggling bypass on a selected node with hotkey',
async ({ comfyPage }) => {
await comfyPage.setup()
await comfyPage.page.getByText('Load Checkpoint').click()
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
const checkpointNode = comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await comfyPage.page.mouse.click(400, 300)
await comfyPage.page.waitForTimeout(128)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-bypassed-state.png'
)
const checkpointNode =
comfyPage.vueNodes.getNodeByTitle('Load Checkpoint')
await expect(checkpointNode).toHaveClass(BYPASS_CLASS)
await comfyPage.page.mouse.click(400, 300)
await comfyPage.page.waitForTimeout(128)
await expect(comfyPage.canvas).toHaveScreenshot(
'vue-node-bypassed-state.png'
)
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
})
await comfyPage.page.keyboard.press(BYPASS_HOTKEY)
await expect(checkpointNode).not.toHaveClass(BYPASS_CLASS)
}
)
test('should allow toggling bypass on multiple selected nodes with hotkey', async ({
comfyPage

Binary file not shown.

Before

Width:  |  Height:  |  Size: 96 KiB

After

Width:  |  Height:  |  Size: 96 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 94 KiB

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 97 KiB

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 82 KiB

View File

@@ -60,6 +60,7 @@ export default defineConfig([
'**/vite.config.*.timestamp*',
'**/vitest.config.*.timestamp*',
'packages/registry-types/src/comfyRegistryTypes.ts',
'public/auth-sw.js',
'src/extensions/core/*',
'src/scripts/*',
'src/types/generatedManagerTypes.ts',

6
global.d.ts vendored
View File

@@ -4,6 +4,12 @@ declare const __SENTRY_DSN__: string
declare const __ALGOLIA_APP_ID__: string
declare const __ALGOLIA_API_KEY__: string
declare const __USE_PROD_CONFIG__: boolean
declare const __MIXPANEL_TOKEN__: string
type BuildFeatureFlags = {
REQUIRE_SUBSCRIPTION: boolean
}
declare const __BUILD_FLAGS__: BuildFeatureFlags
interface Navigator {
/**

View File

@@ -12,6 +12,10 @@ const config: KnipConfig = {
],
project: ['**/*.{js,ts,vue}', '*.{js,ts,mts}']
},
'apps/desktop-ui': {
entry: ['src/main.ts', 'src/i18n.ts'],
project: ['src/**/*.{js,ts,vue}', '*.{js,ts,mts}']
},
'packages/tailwind-utils': {
project: ['src/**/*.{js,ts}']
},
@@ -30,16 +34,16 @@ const config: KnipConfig = {
'@primeuix/forms',
'@primeuix/styled',
'@primeuix/utils',
'@primevue/icons',
// Dev
'@trivago/prettier-plugin-sort-imports'
'@primevue/icons'
],
ignore: [
// Auto generated manager types
'src/workbench/extensions/manager/types/generatedManagerTypes.ts',
'packages/registry-types/src/comfyRegistryTypes.ts',
// Used by a custom node (that should move off of this)
'src/scripts/ui/components/splitButton.ts'
'src/scripts/ui/components/splitButton.ts',
// Service worker - registered at runtime via navigator.serviceWorker.register()
'public/auth-sw.js'
],
compilers: {
// https://github.com/webpro-nl/knip/issues/1008#issuecomment-3207756199

View File

@@ -8,8 +8,10 @@ export default {
}
function formatAndEslint(fileNames) {
// Convert absolute paths to relative paths for better ESLint resolution
const relativePaths = fileNames.map((f) => f.replace(process.cwd() + '/', ''))
return [
`pnpm exec eslint --cache --fix ${fileNames.join(' ')}`,
`pnpm exec prettier --cache --write ${fileNames.join(' ')}`
`pnpm exec eslint --cache --fix ${relativePaths.join(' ')}`,
`pnpm exec prettier --cache --write ${relativePaths.join(' ')}`
]
}

View File

@@ -1,7 +1,7 @@
{
"name": "@comfyorg/comfyui-frontend",
"private": true,
"version": "1.30.1",
"version": "1.30.2",
"type": "module",
"repository": "https://github.com/Comfy-Org/ComfyUI_frontend",
"homepage": "https://comfy.org",
@@ -55,6 +55,7 @@
"@nx/vite": "catalog:",
"@pinia/testing": "catalog:",
"@playwright/test": "catalog:",
"@prettier/plugin-oxc": "catalog:",
"@storybook/addon-docs": "catalog:",
"@storybook/vue3": "catalog:",
"@storybook/vue3-vite": "catalog:",
@@ -89,6 +90,7 @@
"knip": "catalog:",
"lint-staged": "catalog:",
"markdown-table": "catalog:",
"mixpanel-browser": "catalog:",
"nx": "catalog:",
"picocolors": "catalog:",
"postcss-html": "catalog:",

View File

@@ -200,7 +200,7 @@
--node-stroke-executing: var(--color-blue-100);
--text-secondary: var(--color-stone-100);
--text-primary: var(--color-charcoal-700);
--input-surface: rgba(0, 0, 0, 0.15);
--input-surface: rgb(0 0 0 / 0.15);
}
.dark-theme {
@@ -247,7 +247,7 @@
--node-stroke-executing: var(--color-blue-100);
--text-secondary: var(--color-slate-100);
--text-primary: var(--color-pure-white);
--input-surface: rgba(130, 130, 130, 0.1);
--input-surface: rgb(130 130 130 / 0.1);
}
@theme inline {
@@ -258,9 +258,15 @@
--color-button-surface: var(--button-surface);
--color-button-surface-contrast: var(--button-surface-contrast);
--color-dialog-surface: var(--dialog-surface);
--color-interface-menu-component-surface-hovered: var(--interface-menu-component-surface-hovered);
--color-interface-menu-component-surface-selected: var(--interface-menu-component-surface-selected);
--color-interface-menu-keybind-surface-default: var(--interface-menu-keybind-surface-default);
--color-interface-menu-component-surface-hovered: var(
--interface-menu-component-surface-hovered
);
--color-interface-menu-component-surface-selected: var(
--interface-menu-component-surface-selected
);
--color-interface-menu-keybind-surface-default: var(
--interface-menu-keybind-surface-default
);
--color-interface-panel-surface: var(--interface-panel-surface);
--color-interface-stroke: var(--interface-stroke);
--color-nav-background: var(--nav-background);
@@ -324,7 +330,42 @@
}
}
/* Everything below here to be cleaned up over time. */
/* ===================== Scrollbar Utilities (Tailwind) =====================
Usage: Add `scrollbar-custom` class to scrollable containers.
The scrollbar styling adapts to light/dark theme automatically.
============================================================================ */
@utility scrollbar-custom {
overflow-y: auto;
/* Firefox */
scrollbar-width: thin;
scrollbar-color: var(--dialog-surface) transparent;
/* WebKit */
&::-webkit-scrollbar {
width: 10px;
height: 10px;
background-color: transparent;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: var(--dialog-surface);
border-radius: 9999px;
border: 2px solid transparent;
}
&::-webkit-scrollbar-thumb:hover {
background: var(--dialog-surface);
}
&::-webkit-scrollbar-corner {
background: transparent;
}
}
/* =================== End Custom Scrollbar (cross-browser) =================== */
/* Everthing below here to be cleaned up over time. */
body {
width: 100vw;
@@ -1139,7 +1180,7 @@ audio.comfy-audio.empty-audio-widget {
}
.isLOD .lg-node-header {
border-radius: 0px;
border-radius: 0;
pointer-events: none;
}

246
pnpm-lock.yaml generated
View File

@@ -45,6 +45,9 @@ catalogs:
'@playwright/test':
specifier: ^1.52.0
version: 1.52.0
'@prettier/plugin-oxc':
specifier: ^0.0.4
version: 0.0.4
'@primeuix/forms':
specifier: 0.0.2
version: 0.0.2
@@ -186,6 +189,9 @@ catalogs:
markdown-table:
specifier: ^3.0.4
version: 3.0.4
mixpanel-browser:
specifier: ^2.71.0
version: 2.71.0
nx:
specifier: 21.4.1
version: 21.4.1
@@ -495,6 +501,9 @@ importers:
'@playwright/test':
specifier: 'catalog:'
version: 1.52.0
'@prettier/plugin-oxc':
specifier: 'catalog:'
version: 0.0.4
'@storybook/addon-docs':
specifier: 'catalog:'
version: 9.1.1(@types/react@19.1.9)(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)))
@@ -597,6 +606,9 @@ importers:
markdown-table:
specifier: 'catalog:'
version: 3.0.4
mixpanel-browser:
specifier: 'catalog:'
version: 2.71.0
nx:
specifier: 'catalog:'
version: 21.4.1
@@ -2201,6 +2213,21 @@ packages:
'@microsoft/tsdoc@0.15.1':
resolution: {integrity: sha512-4aErSrCR/On/e5G2hDP0wjooqDdauzEbIq8hIkIe5pXV0rtWJZvdCEKL0ykZxex+IxIwBp0eGeV48hQN07dXtw==}
'@mixpanel/rrdom@2.0.0-alpha.18.2':
resolution: {integrity: sha512-vX/tbnS14ZzzatC7vOyvAm9tOLU8tof0BuppBlphzEx1YHTSw8DQiAmyAc0AmXidchLV0W+cUHV/WsehPLh2hQ==}
'@mixpanel/rrweb-snapshot@2.0.0-alpha.18.2':
resolution: {integrity: sha512-2kSnjZZ3QZ9zOz/isOt8s54mXUUDgXk/u0eEi/rE0xBWDeuA0NHrBcqiMc+w4F/yWWUpo5F5zcuPeYpc6ufAsw==}
'@mixpanel/rrweb-types@2.0.0-alpha.18.2':
resolution: {integrity: sha512-ucIYe1mfJ2UksvXW+d3bOySTB2/0yUSqQJlUydvbBz6OO2Bhq3nJHyLXV9ExkgUMZm1ZyDcvvmNUd1+5tAXlpA==}
'@mixpanel/rrweb-utils@2.0.0-alpha.18.2':
resolution: {integrity: sha512-OomKIB6GTx5xvCLJ7iic2khT/t/tnCJUex13aEqsbSqIT/UzUUsqf+LTrgUK5ex+f6odmkCNjre2y5jvpNqn+g==}
'@mixpanel/rrweb@2.0.0-alpha.18.2':
resolution: {integrity: sha512-J3dVTEu6Z4p8di7y9KKvUooNuBjX97DdG6XGWoPEPi07A9512h9M8MEtvlY3mK0PGfuC0Mz5Pv/Ws6gjGYfKQg==}
'@napi-rs/wasm-runtime@0.2.12':
resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==}
@@ -2327,6 +2354,98 @@ packages:
'@one-ini/wasm@0.1.1':
resolution: {integrity: sha512-XuySG1E38YScSJoMlqovLru4KTUNSjgVTIjyh7qMX6aNN5HY5Ct5LhRJdxO79JtTzKfzV/bnWpz+zquYrISsvw==}
'@oxc-parser/binding-android-arm64@0.74.0':
resolution: {integrity: sha512-lgq8TJq22eyfojfa2jBFy2m66ckAo7iNRYDdyn9reXYA3I6Wx7tgGWVx1JAp1lO+aUiqdqP/uPlDaETL9tqRcg==}
engines: {node: '>=20.0.0'}
cpu: [arm64]
os: [android]
'@oxc-parser/binding-darwin-arm64@0.74.0':
resolution: {integrity: sha512-xbY/io/hkARggbpYEMFX6CwFzb7f4iS6WuBoBeZtdqRWfIEi7sm/uYWXfyVeB8uqOATvJ07WRFC2upI8PSI83g==}
engines: {node: '>=20.0.0'}
cpu: [arm64]
os: [darwin]
'@oxc-parser/binding-darwin-x64@0.74.0':
resolution: {integrity: sha512-FIj2gAGtFaW0Zk+TnGyenMUoRu1ju+kJ/h71D77xc1owOItbFZFGa+4WSVck1H8rTtceeJlK+kux+vCjGFCl9Q==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [darwin]
'@oxc-parser/binding-freebsd-x64@0.74.0':
resolution: {integrity: sha512-W1I+g5TJg0TRRMHgEWNWsTIfe782V3QuaPgZxnfPNmDMywYdtlzllzclBgaDq6qzvZCCQc/UhvNb37KWTCTj8A==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [freebsd]
'@oxc-parser/binding-linux-arm-gnueabihf@0.74.0':
resolution: {integrity: sha512-gxqkyRGApeVI8dgvJ19SYe59XASW3uVxF1YUgkE7peW/XIg5QRAOVTFKyTjI9acYuK1MF6OJHqx30cmxmZLtiQ==}
engines: {node: '>=20.0.0'}
cpu: [arm]
os: [linux]
'@oxc-parser/binding-linux-arm-musleabihf@0.74.0':
resolution: {integrity: sha512-jpnAUP4Fa93VdPPDzxxBguJmldj/Gpz7wTXKFzpAueqBMfZsy9KNC+0qT2uZ9HGUDMzNuKw0Se3bPCpL/gfD2Q==}
engines: {node: '>=20.0.0'}
cpu: [arm]
os: [linux]
'@oxc-parser/binding-linux-arm64-gnu@0.74.0':
resolution: {integrity: sha512-fcWyM7BNfCkHqIf3kll8fJctbR/PseL4RnS2isD9Y3FFBhp4efGAzhDaxIUK5GK7kIcFh1P+puIRig8WJ6IMVQ==}
engines: {node: '>=20.0.0'}
cpu: [arm64]
os: [linux]
'@oxc-parser/binding-linux-arm64-musl@0.74.0':
resolution: {integrity: sha512-AMY30z/C77HgiRRJX7YtVUaelKq1ex0aaj28XoJu4SCezdS8i0IftUNTtGS1UzGjGZB8zQz5SFwVy4dRu4GLwg==}
engines: {node: '>=20.0.0'}
cpu: [arm64]
os: [linux]
'@oxc-parser/binding-linux-riscv64-gnu@0.74.0':
resolution: {integrity: sha512-/RZAP24TgZo4vV/01TBlzRqs0R7E6xvatww4LnmZEBBulQBU/SkypDywfriFqWuFoa61WFXPV7sLcTjJGjim/w==}
engines: {node: '>=20.0.0'}
cpu: [riscv64]
os: [linux]
'@oxc-parser/binding-linux-s390x-gnu@0.74.0':
resolution: {integrity: sha512-620J1beNAlGSPBD+Msb3ptvrwxu04B8iULCH03zlf0JSLy/5sqlD6qBs0XUVkUJv1vbakUw1gfVnUQqv0UTuEg==}
engines: {node: '>=20.0.0'}
cpu: [s390x]
os: [linux]
'@oxc-parser/binding-linux-x64-gnu@0.74.0':
resolution: {integrity: sha512-WBFgQmGtFnPNzHyLKbC1wkYGaRIBxXGofO0+hz1xrrkPgbxbJS1Ukva1EB8sPaVBBQ52Bdc2GjLSp721NWRvww==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [linux]
'@oxc-parser/binding-linux-x64-musl@0.74.0':
resolution: {integrity: sha512-y4mapxi0RGqlp3t6Sm+knJlAEqdKDYrEue2LlXOka/F2i4sRN0XhEMPiSOB3ppHmvK4I2zY2XBYTsX1Fel0fAg==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [linux]
'@oxc-parser/binding-wasm32-wasi@0.74.0':
resolution: {integrity: sha512-yDS9bRDh5ymobiS2xBmjlrGdUuU61IZoJBaJC5fELdYT5LJNBXlbr3Yc6m2PWfRJwkH6Aq5fRvxAZ4wCbkGa8w==}
engines: {node: '>=14.0.0'}
cpu: [wasm32]
'@oxc-parser/binding-win32-arm64-msvc@0.74.0':
resolution: {integrity: sha512-XFWY52Rfb4N5wEbMCTSBMxRkDLGbAI9CBSL24BIDywwDJMl31gHEVlmHdCDRoXAmanCI6gwbXYTrWe0HvXJ7Aw==}
engines: {node: '>=20.0.0'}
cpu: [arm64]
os: [win32]
'@oxc-parser/binding-win32-x64-msvc@0.74.0':
resolution: {integrity: sha512-1D3x6iU2apLyfTQHygbdaNbX3nZaHu4yaXpD7ilYpoLo7f0MX0tUuoDrqJyJrVGqvyXgc0uz4yXz9tH9ZZhvvg==}
engines: {node: '>=20.0.0'}
cpu: [x64]
os: [win32]
'@oxc-project/types@0.74.0':
resolution: {integrity: sha512-KOw/RZrVlHGhCXh1RufBFF7Nuo7HdY5w1lRJukM/igIl6x9qtz8QycDvZdzb4qnHO7znrPyo2sJrFJK2eKHgfQ==}
'@oxc-resolver/binding-android-arm-eabi@11.6.1':
resolution: {integrity: sha512-Ma/kg29QJX1Jzelv0Q/j2iFuUad1WnjgPjpThvjqPjpOyLjCUaiFCCnshhmWjyS51Ki1Iol3fjf1qAzObf8GIA==}
cpu: [arm]
@@ -2460,6 +2579,10 @@ packages:
'@polka/url@1.0.0-next.29':
resolution: {integrity: sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==}
'@prettier/plugin-oxc@0.0.4':
resolution: {integrity: sha512-UGXe+g/rSRbglL0FOJiar+a+nUrst7KaFmsg05wYbKiInGWP6eAj/f8A2Uobgo5KxEtb2X10zeflNH6RK2xeIQ==}
engines: {node: '>=14'}
'@primeuix/forms@0.0.2':
resolution: {integrity: sha512-DpecPQd/Qf/kav4LKCaIeGuT3AkwhJzuHCkLANTVlN/zBvo8KIj3OZHsCkm0zlIMVVnaJdtx1ULNlRQdudef+A==}
engines: {node: '>=12.11.0'}
@@ -3020,6 +3143,9 @@ packages:
'@types/chai@5.2.2':
resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==}
'@types/css-font-loading-module@0.0.7':
resolution: {integrity: sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==}
'@types/debug@4.1.12':
resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==}
@@ -3518,6 +3644,9 @@ packages:
'@webgpu/types@0.1.51':
resolution: {integrity: sha512-ktR3u64NPjwIViNCck+z9QeyN0iPkQCUOQ07ZCV1RzlkfP+olLTeEZ95O1QHS+v4w9vJeY9xj/uJuSphsHy5rQ==}
'@xstate/fsm@1.6.5':
resolution: {integrity: sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==}
'@xterm/addon-fit@0.10.0':
resolution: {integrity: sha512-UFYkDm4HUahf2lnEyHvio51TNGiLK66mqP2JoATy7hRZeXaGMRDr00JiSF7m63vR5WKATF605yEggJKsw0JpMQ==}
peerDependencies:
@@ -3800,6 +3929,10 @@ packages:
balanced-match@2.0.0:
resolution: {integrity: sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==}
base64-arraybuffer@1.0.2:
resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==}
engines: {node: '>= 0.6.0'}
base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
@@ -5987,6 +6120,9 @@ packages:
mitt@3.0.1:
resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==}
mixpanel-browser@2.71.0:
resolution: {integrity: sha512-jKmDXe68/oQFgk/9ns9Z36bA0CJ31PH8Y77XTLLGfJvhsUPbvu+7Se9e281NejZF6+OMqx7cE+zFxToozYyNrA==}
mkdirp@3.0.1:
resolution: {integrity: sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==}
engines: {node: '>=10'}
@@ -6173,6 +6309,10 @@ packages:
resolution: {integrity: sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==}
engines: {node: '>= 0.4'}
oxc-parser@0.74.0:
resolution: {integrity: sha512-2tDN/ttU8WE6oFh8EzKNam7KE7ZXSG5uXmvX85iNzxdJfMssDWcj3gpYzZi1E04XuE7m3v1dVWl/8BE886vPGw==}
engines: {node: '>=20.0.0'}
oxc-resolver@11.6.1:
resolution: {integrity: sha512-WQgmxevT4cM5MZ9ioQnEwJiHpPzbvntV5nInGAKo9NQZzegcOonHvcVcnkYqld7bTG35UFHEKeF7VwwsmA3cZg==}
@@ -9499,6 +9639,29 @@ snapshots:
'@microsoft/tsdoc@0.15.1': {}
'@mixpanel/rrdom@2.0.0-alpha.18.2':
dependencies:
'@mixpanel/rrweb-snapshot': 2.0.0-alpha.18.2
'@mixpanel/rrweb-snapshot@2.0.0-alpha.18.2':
dependencies:
postcss: 8.5.6
'@mixpanel/rrweb-types@2.0.0-alpha.18.2': {}
'@mixpanel/rrweb-utils@2.0.0-alpha.18.2': {}
'@mixpanel/rrweb@2.0.0-alpha.18.2':
dependencies:
'@mixpanel/rrdom': 2.0.0-alpha.18.2
'@mixpanel/rrweb-snapshot': 2.0.0-alpha.18.2
'@mixpanel/rrweb-types': 2.0.0-alpha.18.2
'@mixpanel/rrweb-utils': 2.0.0-alpha.18.2
'@types/css-font-loading-module': 0.0.7
'@xstate/fsm': 1.6.5
base64-arraybuffer: 1.0.2
mitt: 3.0.1
'@napi-rs/wasm-runtime@0.2.12':
dependencies:
'@emnapi/core': 1.4.5
@@ -9739,6 +9902,55 @@ snapshots:
'@one-ini/wasm@0.1.1': {}
'@oxc-parser/binding-android-arm64@0.74.0':
optional: true
'@oxc-parser/binding-darwin-arm64@0.74.0':
optional: true
'@oxc-parser/binding-darwin-x64@0.74.0':
optional: true
'@oxc-parser/binding-freebsd-x64@0.74.0':
optional: true
'@oxc-parser/binding-linux-arm-gnueabihf@0.74.0':
optional: true
'@oxc-parser/binding-linux-arm-musleabihf@0.74.0':
optional: true
'@oxc-parser/binding-linux-arm64-gnu@0.74.0':
optional: true
'@oxc-parser/binding-linux-arm64-musl@0.74.0':
optional: true
'@oxc-parser/binding-linux-riscv64-gnu@0.74.0':
optional: true
'@oxc-parser/binding-linux-s390x-gnu@0.74.0':
optional: true
'@oxc-parser/binding-linux-x64-gnu@0.74.0':
optional: true
'@oxc-parser/binding-linux-x64-musl@0.74.0':
optional: true
'@oxc-parser/binding-wasm32-wasi@0.74.0':
dependencies:
'@napi-rs/wasm-runtime': 0.2.12
optional: true
'@oxc-parser/binding-win32-arm64-msvc@0.74.0':
optional: true
'@oxc-parser/binding-win32-x64-msvc@0.74.0':
optional: true
'@oxc-project/types@0.74.0': {}
'@oxc-resolver/binding-android-arm-eabi@11.6.1':
optional: true
@@ -9834,6 +10046,10 @@ snapshots:
'@polka/url@1.0.0-next.29': {}
'@prettier/plugin-oxc@0.0.4':
dependencies:
oxc-parser: 0.74.0
'@primeuix/forms@0.0.2':
dependencies:
'@primeuix/utils': 0.3.2
@@ -10387,6 +10603,8 @@ snapshots:
dependencies:
'@types/deep-eql': 4.0.2
'@types/css-font-loading-module@0.0.7': {}
'@types/debug@4.1.12':
dependencies:
'@types/ms': 2.1.0
@@ -10980,6 +11198,8 @@ snapshots:
'@webgpu/types@0.1.51': {}
'@xstate/fsm@1.6.5': {}
'@xterm/addon-fit@0.10.0(@xterm/xterm@5.5.0)':
dependencies:
'@xterm/xterm': 5.5.0
@@ -11296,6 +11516,8 @@ snapshots:
balanced-match@2.0.0: {}
base64-arraybuffer@1.0.2: {}
base64-js@1.5.1: {}
better-opn@3.0.2:
@@ -13864,6 +14086,10 @@ snapshots:
mitt@3.0.1: {}
mixpanel-browser@2.71.0:
dependencies:
'@mixpanel/rrweb': 2.0.0-alpha.18.2
mkdirp@3.0.1: {}
mlly@1.8.0:
@@ -14106,6 +14332,26 @@ snapshots:
safe-push-apply: 1.0.0
optional: true
oxc-parser@0.74.0:
dependencies:
'@oxc-project/types': 0.74.0
optionalDependencies:
'@oxc-parser/binding-android-arm64': 0.74.0
'@oxc-parser/binding-darwin-arm64': 0.74.0
'@oxc-parser/binding-darwin-x64': 0.74.0
'@oxc-parser/binding-freebsd-x64': 0.74.0
'@oxc-parser/binding-linux-arm-gnueabihf': 0.74.0
'@oxc-parser/binding-linux-arm-musleabihf': 0.74.0
'@oxc-parser/binding-linux-arm64-gnu': 0.74.0
'@oxc-parser/binding-linux-arm64-musl': 0.74.0
'@oxc-parser/binding-linux-riscv64-gnu': 0.74.0
'@oxc-parser/binding-linux-s390x-gnu': 0.74.0
'@oxc-parser/binding-linux-x64-gnu': 0.74.0
'@oxc-parser/binding-linux-x64-musl': 0.74.0
'@oxc-parser/binding-wasm32-wasi': 0.74.0
'@oxc-parser/binding-win32-arm64-msvc': 0.74.0
'@oxc-parser/binding-win32-x64-msvc': 0.74.0
oxc-resolver@11.6.1:
dependencies:
napi-postinstall: 0.3.3

View File

@@ -16,6 +16,7 @@ catalog:
'@nx/vite': 21.4.1
'@pinia/testing': ^0.1.5
'@playwright/test': ^1.52.0
'@prettier/plugin-oxc': ^0.0.4
'@primeuix/forms': 0.0.2
'@primeuix/styled': 0.3.2
'@primeuix/utils': ^0.3.2
@@ -98,6 +99,7 @@ catalog:
zod: ^3.23.8
zod-to-json-schema: ^3.24.1
zod-validation-error: ^3.3.0
mixpanel-browser: ^2.71.0
cleanupUnusedCatalogs: true

Binary file not shown.

147
public/auth-sw.js Normal file
View File

@@ -0,0 +1,147 @@
/**
* @fileoverview Authentication Service Worker
* Intercepts /api/view requests and adds Firebase authentication headers.
* Required for browser-native requests (img, video, audio) that cannot send custom headers.
*/
/**
* @typedef {Object} AuthHeader
* @property {string} Authorization - Bearer token for authentication
*/
/**
* @typedef {Object} CachedAuth
* @property {AuthHeader|null} header
* @property {number} expiresAt - Timestamp when cache expires
*/
const CACHE_TTL_MS = 50 * 60 * 1000 // 50 minutes (Firebase tokens expire in 1 hour)
/** @type {CachedAuth|null} */
let authCache = null
/** @type {Promise<AuthHeader|null>|null} */
let authRequestInFlight = null
self.addEventListener('message', (event) => {
if (event.data.type === 'INVALIDATE_AUTH_HEADER') {
authCache = null
authRequestInFlight = null
}
})
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url)
if (
!url.pathname.startsWith('/api/view') &&
!url.pathname.startsWith('/api/viewvideo')
) {
return
}
event.respondWith(
(async () => {
try {
const authHeader = await getAuthHeader()
if (!authHeader) {
return fetch(event.request)
}
const headers = new Headers(event.request.headers)
for (const [key, value] of Object.entries(authHeader)) {
headers.set(key, value)
}
return fetch(
new Request(event.request.url, {
method: event.request.method,
headers: headers,
mode: 'same-origin',
credentials: event.request.credentials,
cache: 'no-store',
redirect: event.request.redirect,
referrer: event.request.referrer,
integrity: event.request.integrity
})
)
} catch (error) {
console.error('[Auth SW] Request failed:', error)
return fetch(event.request)
}
})()
)
})
/**
* Gets auth header from cache or requests from main thread
* @returns {Promise<AuthHeader|null>}
*/
async function getAuthHeader() {
// Return cached value if valid
if (authCache && authCache.expiresAt > Date.now()) {
return authCache.header
}
// Clear expired cache
if (authCache) {
authCache = null
}
// Deduplicate concurrent requests
if (authRequestInFlight) {
return authRequestInFlight
}
authRequestInFlight = requestAuthHeaderFromMainThread()
const header = await authRequestInFlight
authRequestInFlight = null
// Cache the result
if (header) {
authCache = {
header,
expiresAt: Date.now() + CACHE_TTL_MS
}
}
return header
}
/**
* Requests auth header from main thread via MessageChannel
* @returns {Promise<AuthHeader|null>}
*/
async function requestAuthHeaderFromMainThread() {
const clients = await self.clients.matchAll()
if (clients.length === 0) {
return null
}
const messageChannel = new MessageChannel()
return new Promise((resolve) => {
let timeoutId
messageChannel.port1.onmessage = (event) => {
clearTimeout(timeoutId)
resolve(event.data.authHeader)
}
timeoutId = setTimeout(() => {
console.error(
'[Auth SW] Timeout waiting for auth header from main thread'
)
resolve(null)
}, 1000)
clients[0].postMessage({ type: 'REQUEST_AUTH_HEADER' }, [
messageChannel.port2
])
})
}
self.addEventListener('activate', (event) => {
event.waitUntil(self.clients.claim())
})

View File

@@ -18,44 +18,81 @@
export const BUNDLE_CATEGORIES = [
{
name: 'App Entry Points',
description: 'Main application bundles',
patterns: [/^index-.*\.js$/],
description: 'Main entry bundles and manifests',
patterns: [/^index-.*\.js$/i, /^manifest-.*\.js$/i],
order: 1
},
{
name: 'Core Views',
description: 'Major application views and screens',
patterns: [/GraphView-.*\.js$/, /UserSelectView-.*\.js$/],
name: 'Graph Workspace',
description: 'Graph editor runtime, canvas, workflow orchestration',
patterns: [
/Graph(View|State)?-.*\.js$/i,
/(Canvas|Workflow|History|NodeGraph|Compositor)-.*\.js$/i
],
order: 2
},
{
name: 'UI Panels',
description: 'Settings and configuration panels',
patterns: [/.*Panel-.*\.js$/],
name: 'Views & Navigation',
description: 'Top-level views, pages, and routed surfaces',
patterns: [/.*(View|Page|Layout|Screen|Route)-.*\.js$/i],
order: 3
},
{
name: 'UI Components',
description: 'Reusable UI components',
patterns: [/Avatar-.*\.js$/, /Badge-.*\.js$/],
name: 'Panels & Settings',
description: 'Configuration panels, inspectors, and settings screens',
patterns: [/.*(Panel|Settings|Config|Preferences|Manager)-.*\.js$/i],
order: 4
},
{
name: 'Services',
description: 'Business logic and services',
patterns: [/.*Service-.*\.js$/, /.*Store-.*\.js$/],
name: 'User & Accounts',
description: 'Authentication, profile, and account management bundles',
patterns: [
/.*((User(Panel|Select|Auth|Account|Profile|Settings|Preferences|Manager|List|Menu|Modal))|Account|Auth|Profile|Login|Signup|Password).*-.+\.js$/i
],
order: 5
},
{
name: 'Utilities',
description: 'Helper functions and utilities',
patterns: [/.*[Uu]til.*\.js$/],
name: 'Editors & Dialogs',
description: 'Modals, dialogs, drawers, and in-app editors',
patterns: [/.*(Modal|Dialog|Drawer|Editor)-.*\.js$/i],
order: 6
},
{
name: 'UI Components',
description: 'Reusable component library chunks',
patterns: [
/.*(Button|Avatar|Badge|Dropdown|Tabs|Table|List|Card|Form|Input|Toggle|Menu|Toolbar|Sidebar)-.*\.js$/i,
/.*\.vue_vue_type_script_setup_true_lang-.*\.js$/i
],
order: 7
},
{
name: 'Data & Services',
description: 'Stores, services, APIs, and repositories',
patterns: [/.*(Service|Store|Api|Repository)-.*\.js$/i],
order: 8
},
{
name: 'Utilities & Hooks',
description: 'Helpers, composables, and utility bundles',
patterns: [
/.*(Util|Utils|Helper|Composable|Hook)-.*\.js$/i,
/use[A-Z].*\.js$/
],
order: 9
},
{
name: 'Vendor & Third-Party',
description: 'External libraries and shared vendor chunks',
patterns: [
/^(chunk|vendor|prime|three|lodash|chart|firebase|yjs|axios|uuid)-.*\.js$/i
],
order: 10
},
{
name: 'Other',
description: 'Uncategorized bundles',
patterns: [/.*/], // Catch-all pattern
description: 'Bundles that do not match a named category',
patterns: [/.*/],
order: 99
}
]

View File

@@ -7,6 +7,13 @@ import prettyBytes from 'pretty-bytes'
import { getCategoryMetadata } from './bundle-categories.js'
/**
* @typedef {Object} SizeMetrics
* @property {number} size
* @property {number} gzip
* @property {number} brotli
*/
/**
* @typedef {Object} SizeResult
* @property {number} size
@@ -18,15 +25,52 @@ import { getCategoryMetadata } from './bundle-categories.js'
* @typedef {SizeResult & { file: string, category?: string }} BundleResult
*/
/**
* @typedef {'added' | 'removed' | 'increased' | 'decreased' | 'unchanged'} BundleStatus
*/
/**
* @typedef {Object} BundleDiff
* @property {string} fileName
* @property {BundleResult | undefined} curr
* @property {BundleResult | undefined} prev
* @property {SizeMetrics} diff
* @property {BundleStatus} status
*/
/**
* @typedef {Object} CountSummary
* @property {number} added
* @property {number} removed
* @property {number} increased
* @property {number} decreased
* @property {number} unchanged
*/
/**
* @typedef {Object} CategoryReport
* @property {string} name
* @property {string | undefined} description
* @property {number} order
* @property {{ current: SizeMetrics, baseline: SizeMetrics, diff: SizeMetrics }} metrics
* @property {CountSummary} counts
* @property {BundleDiff[]} bundles
*/
/**
* @typedef {Object} BundleReport
* @property {CategoryReport[]} categories
* @property {{ currentBundles: number, baselineBundles: number, metrics: { current: SizeMetrics, baseline: SizeMetrics, diff: SizeMetrics }, counts: CountSummary }} overall
* @property {boolean} hasBaseline
*/
const currDir = path.resolve('temp/size')
const prevDir = path.resolve('temp/size-prev')
let output = '## Bundle Size Report\n\n'
const sizeHeaders = ['Size', 'Gzip', 'Brotli']
run()
/**
* Main function to generate the size report
* Main entry for generating the size report
*/
async function run() {
if (!existsSync(currDir)) {
@@ -35,27 +79,41 @@ async function run() {
process.exit(1)
}
await renderFiles()
const report = await buildBundleReport()
const output = renderReport(report)
process.stdout.write(output)
}
/**
* Renders file sizes and diffs between current and previous versions
* Build bundle comparison data from current and baseline artifacts
* @returns {Promise<BundleReport>}
*/
async function renderFiles() {
async function buildBundleReport() {
/**
* @param {string[]} files
* @returns {string[]}
*/
const filterFiles = (files) => files.filter((file) => file.endsWith('.json'))
const curr = filterFiles(await readdir(currDir))
const prev = existsSync(prevDir) ? filterFiles(await readdir(prevDir)) : []
const fileList = new Set([...curr, ...prev])
const currFiles = filterFiles(await readdir(currDir))
const baselineFiles = existsSync(prevDir)
? filterFiles(await readdir(prevDir))
: []
const fileList = new Set([...currFiles, ...baselineFiles])
// Group bundles by category
/** @type {Map<string, Array<{fileName: string, curr: BundleResult | undefined, prev: BundleResult | undefined}>>} */
const bundlesByCategory = new Map()
/** @type {Map<string, CategoryReport>} */
const categories = new Map()
const overall = {
currentBundles: 0,
baselineBundles: 0,
metrics: {
current: createMetrics(),
baseline: createMetrics(),
diff: createMetrics()
},
counts: createCounts()
}
for (const file of fileList) {
const currPath = path.resolve(currDir, file)
@@ -63,100 +121,440 @@ async function renderFiles() {
const curr = await importJSON(currPath)
const prev = await importJSON(prevPath)
const fileName = curr?.file || prev?.file || ''
const category = curr?.category || prev?.category || 'Other'
const fileName = curr?.file || prev?.file
if (!fileName) continue
if (!bundlesByCategory.has(category)) {
bundlesByCategory.set(category, [])
const categoryName = curr?.category || prev?.category || 'Other'
const category = ensureCategoryEntry(categories, categoryName)
const currMetrics = toMetrics(curr)
const baselineMetrics = toMetrics(prev)
const diffMetrics = subtractMetrics(currMetrics, baselineMetrics)
const status = getStatus(curr, prev, diffMetrics.size)
if (curr) {
overall.currentBundles++
}
if (prev) {
overall.baselineBundles++
}
// @ts-expect-error - get is valid
bundlesByCategory.get(category).push({ fileName, curr, prev })
}
addMetrics(overall.metrics.current, currMetrics)
addMetrics(overall.metrics.baseline, baselineMetrics)
addMetrics(overall.metrics.diff, diffMetrics)
incrementStatus(overall.counts, status)
// Sort categories by their order
const sortedCategories = Array.from(bundlesByCategory.keys()).sort((a, b) => {
const metaA = getCategoryMetadata(a)
const metaB = getCategoryMetadata(b)
return (metaA?.order ?? 99) - (metaB?.order ?? 99)
})
addMetrics(category.metrics.current, currMetrics)
addMetrics(category.metrics.baseline, baselineMetrics)
addMetrics(category.metrics.diff, diffMetrics)
incrementStatus(category.counts, status)
let totalSize = 0
let totalCount = 0
// Render each category
for (const category of sortedCategories) {
const bundles = bundlesByCategory.get(category) || []
if (bundles.length === 0) continue
const categoryMeta = getCategoryMetadata(category)
output += `### ${category}\n\n`
if (categoryMeta?.description) {
output += `_${categoryMeta.description}_\n\n`
}
const rows = []
let categorySize = 0
for (const { fileName, curr, prev } of bundles) {
if (!curr) {
// File was deleted
rows.push([`~~${fileName}~~`])
} else {
rows.push([
fileName,
`${prettyBytes(curr.size)}${getDiff(curr.size, prev?.size)}`,
`${prettyBytes(curr.gzip)}${getDiff(curr.gzip, prev?.gzip)}`,
`${prettyBytes(curr.brotli)}${getDiff(curr.brotli, prev?.brotli)}`
])
categorySize += curr.size
totalSize += curr.size
totalCount++
}
}
// Sort rows by file name within category
rows.sort((a, b) => {
const fileA = a[0].replace(/~~/g, '')
const fileB = b[0].replace(/~~/g, '')
return fileA.localeCompare(fileB)
category.bundles.push({
fileName,
curr,
prev,
diff: diffMetrics,
status
})
output += markdownTable([['File', ...sizeHeaders], ...rows])
output += `\n\n**Category Total:** ${prettyBytes(categorySize)}\n\n`
}
// Add overall summary
if (totalCount > 0) {
output += '---\n\n'
output += `**Overall Total Size:** ${prettyBytes(totalSize)}\n`
output += `**Total Bundle Count:** ${totalCount}\n`
const sortedCategories = Array.from(categories.values()).sort(
(a, b) => a.order - b.order
)
return {
categories: sortedCategories,
overall,
hasBaseline: baselineFiles.length > 0
}
}
/**
* Imports JSON data from a specified path
*
* Render the complete report in markdown
* @param {BundleReport} report
* @returns {string}
*/
function renderReport(report) {
const parts = ['## Bundle Size Report\n']
parts.push(renderSummary(report))
if (report.categories.length > 0) {
const glance = renderCategoryGlance(report)
if (glance) {
parts.push('\n' + glance)
}
parts.push('\n' + renderCategoryDetails(report))
}
return (
parts
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trimEnd() + '\n'
)
}
/**
* Render overall summary bullets
* @param {BundleReport} report
* @returns {string}
*/
function renderSummary(report) {
const { overall, hasBaseline } = report
const lines = ['**Summary**']
const rawLineParts = [
`- Raw size: ${prettyBytes(overall.metrics.current.size)}`
]
if (hasBaseline) {
rawLineParts.push(`baseline ${prettyBytes(overall.metrics.baseline.size)}`)
rawLineParts.push(`${formatDiffIndicator(overall.metrics.diff.size)}`)
}
lines.push(rawLineParts.join(' '))
const gzipLineParts = [`- Gzip: ${prettyBytes(overall.metrics.current.gzip)}`]
if (hasBaseline) {
gzipLineParts.push(`baseline ${prettyBytes(overall.metrics.baseline.gzip)}`)
gzipLineParts.push(`${formatDiffIndicator(overall.metrics.diff.gzip)}`)
}
lines.push(gzipLineParts.join(' '))
const brotliLineParts = [
`- Brotli: ${prettyBytes(overall.metrics.current.brotli)}`
]
if (hasBaseline) {
brotliLineParts.push(
`baseline ${prettyBytes(overall.metrics.baseline.brotli)}`
)
brotliLineParts.push(
`${formatDiffIndicator(overall.metrics.diff.brotli)}`
)
}
lines.push(brotliLineParts.join(' '))
const bundleStats = [`${overall.currentBundles} current`]
if (hasBaseline) {
bundleStats.push(`${overall.baselineBundles} baseline`)
}
const statusParts = []
if (overall.counts.added) statusParts.push(`${overall.counts.added} added`)
if (overall.counts.removed)
statusParts.push(`${overall.counts.removed} removed`)
if (overall.counts.increased)
statusParts.push(`${overall.counts.increased} grew`)
if (overall.counts.decreased)
statusParts.push(`${overall.counts.decreased} shrank`)
let bundlesLine = `- Bundles: ${bundleStats.join(' • ')}`
if (statusParts.length > 0) {
bundlesLine += `${statusParts.join(' / ')}`
}
lines.push(bundlesLine)
if (!hasBaseline) {
lines.push(
'_Baseline artifact not found; showing current bundle sizes only._'
)
}
return lines.join('\n')
}
/**
* Render a compact category glance line
* @param {BundleReport} report
* @returns {string}
*/
function renderCategoryGlance(report) {
const { categories, hasBaseline } = report
const relevant = categories.filter(
(category) =>
category.metrics.current.size > 0 ||
(hasBaseline && category.metrics.baseline.size > 0)
)
if (relevant.length === 0) return ''
const sorted = relevant.slice().sort((a, b) => {
if (hasBaseline) {
return (
Math.abs(b.metrics.diff.size) - Math.abs(a.metrics.diff.size) ||
b.metrics.current.size - a.metrics.current.size
)
}
return b.metrics.current.size - a.metrics.current.size
})
const limit = 6
const trimmed = sorted.slice(0, limit)
const parts = trimmed.map((category) => {
const currentStr = prettyBytes(category.metrics.current.size)
if (hasBaseline) {
return `${category.name} ${formatDiffIndicator(category.metrics.diff.size)} (${currentStr})`
}
return `${category.name} ${currentStr}`
})
if (sorted.length > limit) {
parts.push(`+ ${sorted.length - limit} more`)
}
return `**Category Glance**\n${parts.join(' · ')}`
}
/**
* Render per-category detail tables wrapped in collapsible sections
* @param {BundleReport} report
* @returns {string}
*/
function renderCategoryDetails(report) {
const lines = ['<details>', '<summary>Per-category breakdown</summary>', '']
for (const category of report.categories) {
lines.push(renderCategoryBlock(category, report.hasBaseline))
}
lines.push('</details>')
return lines.join('\n')
}
/**
* Render a single category block with its table
* @param {CategoryReport} category
* @param {boolean} hasBaseline
* @returns {string}
*/
function renderCategoryBlock(category, hasBaseline) {
const lines = ['<details>']
const currentStr = prettyBytes(category.metrics.current.size)
const summaryParts = [`<summary>${category.name}${currentStr}`]
if (hasBaseline) {
summaryParts.push(
` (baseline ${prettyBytes(category.metrics.baseline.size)}) • ${formatDiffIndicator(category.metrics.diff.size)}`
)
}
summaryParts.push('</summary>')
lines.push(summaryParts.join(''))
if (category.description) {
lines.push(`_${category.description}_`)
}
if (category.bundles.length === 0) {
lines.push('No bundles matched this category.\n')
lines.push('</details>\n')
return lines.join('\n')
}
const headers = hasBaseline
? ['File', 'Before', 'After', 'Δ Raw', 'Δ Gzip', 'Δ Brotli']
: ['File', 'Size', 'Gzip', 'Brotli']
const rows = category.bundles
.slice()
.sort((a, b) => {
const diffMagnitude = Math.abs(b.diff.size) - Math.abs(a.diff.size)
if (diffMagnitude !== 0) return diffMagnitude
return a.fileName.localeCompare(b.fileName)
})
.map((bundle) => {
if (hasBaseline) {
return [
formatFileLabel(bundle),
formatSize(bundle.prev?.size),
formatSize(bundle.curr?.size),
formatDiffIndicator(bundle.diff.size),
formatDiffIndicator(bundle.diff.gzip),
formatDiffIndicator(bundle.diff.brotli)
]
}
return [
formatFileLabel(bundle),
formatSize(bundle.curr?.size),
formatSize(bundle.curr?.gzip),
formatSize(bundle.curr?.brotli)
]
})
lines.push(markdownTable([headers, ...rows]))
const statusParts = []
if (category.counts.added) statusParts.push(`${category.counts.added} added`)
if (category.counts.removed)
statusParts.push(`${category.counts.removed} removed`)
if (category.counts.increased)
statusParts.push(`${category.counts.increased} grew`)
if (category.counts.decreased)
statusParts.push(`${category.counts.decreased} shrank`)
if (statusParts.length > 0) {
lines.push(`\n_Status:_ ${statusParts.join(' / ')}`)
}
lines.push('</details>\n')
return lines.join('\n')
}
/**
* Ensure a category entry exists in the map
* @param {Map<string, CategoryReport>} categories
* @param {string} categoryName
* @returns {CategoryReport}
*/
function ensureCategoryEntry(categories, categoryName) {
if (!categories.has(categoryName)) {
const meta = getCategoryMetadata(categoryName)
categories.set(categoryName, {
name: categoryName,
description: meta?.description,
order: meta?.order ?? 99,
metrics: {
current: createMetrics(),
baseline: createMetrics(),
diff: createMetrics()
},
counts: createCounts(),
bundles: []
})
}
// @ts-expect-error - ensured by check above
return categories.get(categoryName)
}
/**
* Convert bundle result to metrics
* @param {BundleResult | undefined} bundle
* @returns {SizeMetrics}
*/
function toMetrics(bundle) {
if (!bundle) return createMetrics()
return {
size: bundle.size,
gzip: bundle.gzip,
brotli: bundle.brotli
}
}
/**
* Create an empty metrics object
* @returns {SizeMetrics}
*/
function createMetrics() {
return { size: 0, gzip: 0, brotli: 0 }
}
/**
* Add source metrics into target metrics
* @param {SizeMetrics} target
* @param {SizeMetrics} source
*/
function addMetrics(target, source) {
target.size += source.size
target.gzip += source.gzip
target.brotli += source.brotli
}
/**
* Subtract baseline metrics from current metrics
* @param {SizeMetrics} current
* @param {SizeMetrics} baseline
* @returns {SizeMetrics}
*/
function subtractMetrics(current, baseline) {
return {
size: current.size - baseline.size,
gzip: current.gzip - baseline.gzip,
brotli: current.brotli - baseline.brotli
}
}
/**
* Create an empty counts object
* @returns {CountSummary}
*/
function createCounts() {
return { added: 0, removed: 0, increased: 0, decreased: 0, unchanged: 0 }
}
/**
* Increment status counters
* @param {CountSummary} counts
* @param {BundleStatus} status
*/
function incrementStatus(counts, status) {
counts[status] += 1
}
/**
* Determine bundle status for reporting
* @param {BundleResult | undefined} curr
* @param {BundleResult | undefined} prev
* @param {number} sizeDiff
* @returns {BundleStatus}
*/
function getStatus(curr, prev, sizeDiff) {
if (curr && prev) {
if (sizeDiff > 0) return 'increased'
if (sizeDiff < 0) return 'decreased'
return 'unchanged'
}
if (curr && !prev) return 'added'
if (!curr && prev) return 'removed'
return 'unchanged'
}
/**
* Format file label with status hints
* @param {BundleDiff} bundle
* @returns {string}
*/
function formatFileLabel(bundle) {
if (bundle.status === 'added') {
return `**${bundle.fileName}** _(new)_`
}
if (bundle.status === 'removed') {
return `~~${bundle.fileName}~~ _(removed)_`
}
return bundle.fileName
}
/**
* Format size for table output
* @param {number | undefined} value
* @returns {string}
*/
function formatSize(value) {
if (value === undefined) return '—'
return prettyBytes(value)
}
/**
* Format a diff with an indicator emoji
* @param {number} diff
* @returns {string}
*/
function formatDiffIndicator(diff) {
if (diff > 0) {
return `:red_circle: +${prettyBytes(diff)}`
}
if (diff < 0) {
return `:green_circle: -${prettyBytes(Math.abs(diff))}`
}
return ':white_circle: 0 B'
}
/**
* Import JSON data if it exists
* @template T
* @param {string} filePath - Path to the JSON file
* @returns {Promise<T | undefined>} The JSON content or undefined if the file does not exist
* @param {string} filePath
* @returns {Promise<T | undefined>}
*/
async function importJSON(filePath) {
if (!existsSync(filePath)) return undefined
return (await import(filePath, { with: { type: 'json' } })).default
}
/**
* Calculates the difference between the current and previous sizes
*
* @param {number} curr - The current size
* @param {number} [prev] - The previous size
* @returns {string} The difference in pretty format
*/
function getDiff(curr, prev) {
if (prev === undefined) return ''
const diff = curr - prev
if (diff === 0) return ''
const sign = diff > 0 ? '+' : ''
return ` (**${sign}${prettyBytes(diff)}**)`
}

View File

@@ -1,29 +1,64 @@
/**
* Utility functions for downloading files
*/
import { isCloud } from '@/platform/distribution/types'
// Constants
const DEFAULT_DOWNLOAD_FILENAME = 'download.png'
/**
* Trigger a download by creating a temporary anchor element
* @param href - The URL or blob URL to download
* @param filename - The filename to suggest to the browser
*/
function triggerLinkDownload(href: string, filename: string): void {
const link = document.createElement('a')
link.href = href
link.download = filename
link.style.display = 'none'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
}
/**
* Download a file from a URL by creating a temporary anchor element
* @param url - The URL of the file to download (must be a valid URL string)
* @param filename - Optional filename override (will use URL filename or default if not provided)
* @throws {Error} If the URL is invalid or empty
*/
export const downloadFile = (url: string, filename?: string): void => {
export function downloadFile(url: string, filename?: string): void {
if (!url || typeof url !== 'string' || url.trim().length === 0) {
throw new Error('Invalid URL provided for download')
}
const link = document.createElement('a')
link.href = url
link.download =
const inferredFilename =
filename || extractFilenameFromUrl(url) || DEFAULT_DOWNLOAD_FILENAME
// Trigger download
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
if (isCloud) {
// Assets from cross-origin (e.g., GCS) cannot be downloaded this way
void downloadViaBlobFetch(url, inferredFilename).catch((error) => {
console.error('Failed to download file', error)
})
return
}
triggerLinkDownload(url, inferredFilename)
}
/**
* Download a Blob by creating a temporary object URL and anchor element
* @param filename - The filename to suggest to the browser
* @param blob - The Blob to download
*/
export function downloadBlob(filename: string, blob: Blob): void {
const url = URL.createObjectURL(blob)
triggerLinkDownload(url, filename)
// Revoke on the next microtask to give the browser time to start the download
queueMicrotask(() => URL.revokeObjectURL(url))
}
/**
@@ -39,3 +74,15 @@ const extractFilenameFromUrl = (url: string): string | null => {
return null
}
}
const downloadViaBlobFetch = async (
href: string,
filename: string
): Promise<void> => {
const response = await fetch(href)
if (!response.ok) {
throw new Error(`Failed to fetch ${href}: ${response.status}`)
}
const blob = await response.blob()
downloadBlob(filename, blob)
}

View File

@@ -42,7 +42,7 @@
<slot name="topmenu" :sidebar-panel-visible="sidebarPanelVisible" />
<Splitter
class="splitter-overlay splitter-overlay-bottom mr-2 mb-2 ml-2 flex-1"
class="splitter-overlay splitter-overlay-bottom mr-1 mb-1 ml-1 flex-1"
layout="vertical"
:pt:gutter="
'rounded-tl-lg rounded-tr-lg ' +

View File

@@ -1,11 +1,11 @@
<template>
<div v-if="!workspaceStore.focusMode" class="ml-2 flex pt-2">
<div v-if="!workspaceStore.focusMode" class="ml-2 flex pt-1">
<div class="min-w-0 flex-1">
<SubgraphBreadcrumb />
</div>
<div
class="actionbar-container pointer-events-auto mx-2 flex h-12 items-center rounded-lg px-2 shadow-md"
class="actionbar-container pointer-events-auto mx-1 flex h-12 items-center rounded-lg px-2 shadow-md"
>
<!-- Support for legacy topbar elements attached by custom scripts, hidden if no elements present -->
<div
@@ -13,8 +13,10 @@
class="[&:not(:has(*>*:not(:empty)))]:hidden"
></div>
<ComfyActionbar />
<LoginButton v-if="!isLoggedIn" />
<CurrentUserButton v-else class="shrink-0" />
<template v-if="isDesktop">
<LoginButton v-if="!isLoggedIn" />
<CurrentUserButton v-else class="shrink-0" />
</template>
</div>
</div>
</template>
@@ -29,9 +31,11 @@ import LoginButton from '@/components/topbar/LoginButton.vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { app } from '@/scripts/app'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isElectron } from '@/utils/envUtil'
const workspaceStore = useWorkspaceStore()
const { isLoggedIn } = useCurrentUser()
const isDesktop = isElectron()
// Maintain support for legacy topbar elements attached by custom scripts
const legacyCommandsContainerRef = ref<HTMLElement>()

View File

@@ -2,10 +2,7 @@
<div class="flex h-full items-center">
<div
v-if="isDragging && !isDocked"
class="actionbar-drop-zone m-1.5 flex items-center justify-center self-stretch rounded-md"
:class="{
'drop-zone-active': isMouseOverDropZone
}"
:class="actionbarClass"
@mouseenter="onMouseEnterDropZone"
@mouseleave="onMouseLeaveDropZone"
>
@@ -13,18 +10,15 @@
</div>
<Panel
class="actionbar"
class="pointer-events-auto z-1000"
:style="style"
:class="{
fixed: !isDocked,
'is-dragging': isDragging,
'is-docked static mr-2 border-none bg-transparent p-0': isDocked
:class="panelClass"
:pt="{
header: { class: 'hidden' },
content: { class: isDocked ? 'p-0' : 'p-1' }
}"
>
<div
ref="panelRef"
class="actionbar-content flex items-center select-none"
>
<div ref="panelRef" class="flex items-center select-none">
<span
ref="dragHandleRef"
:class="
@@ -34,7 +28,8 @@
)
"
/>
<ComfyQueueButton />
<ComfyRunButton />
</div>
</Panel>
</div>
@@ -55,7 +50,7 @@ import { t } from '@/i18n'
import { useSettingStore } from '@/platform/settings/settingStore'
import { cn } from '@/utils/tailwindUtil'
import ComfyQueueButton from './ComfyQueueButton.vue'
import ComfyRunButton from './ComfyRunButton'
const settingsStore = useSettingStore()
@@ -250,45 +245,20 @@ watch(isDragging, (dragging) => {
isMouseOverDropZone.value = false
}
})
const actionbarClass = computed(() =>
cn(
'w-[265px] 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',
isMouseOverDropZone.value &&
'border-[3px] opacity-100 scale-105 shadow-[0_0_20px] shadow-blue-500'
)
)
const panelClass = computed(() =>
cn(
'actionbar pointer-events-auto z1000',
isDragging.value && 'select-none pointer-events-none',
isDocked.value ? 'p-0 static mr-2 border-none bg-transparent' : 'fixed'
)
)
</script>
<style scoped>
@reference '../../assets/css/style.css';
.actionbar {
pointer-events: all;
z-index: 1000;
}
.actionbar-drop-zone {
width: 265px;
border: 2px dashed var(--p-primary-color);
opacity: 0.8;
}
.actionbar-drop-zone.drop-zone-active {
background: var(--p-highlight-background-focus);
border-color: var(--p-primary-color);
border-width: 3px;
box-shadow: 0 0 20px var(--p-primary-color);
opacity: 1;
transform: scale(1.05);
}
.actionbar.is-dragging {
user-select: none;
pointer-events: none;
}
:deep(.p-panel-content) {
@apply p-1;
}
.is-docked :deep(.p-panel-content) {
@apply p-0;
}
:deep(.p-panel-header) {
display: none;
}
</style>

View File

@@ -0,0 +1,19 @@
<template>
<component
:is="currentButton"
:key="isActiveSubscription ? 'queue' : 'subscribe'"
/>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import ComfyQueueButton from '@/components/actionbar/ComfyRunButton/ComfyQueueButton.vue'
import SubscribeToRunButton from '@/platform/cloud/subscription/components/SubscribeToRun.vue'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
const { isActiveSubscription } = useSubscription()
const currentButton = computed(() =>
isActiveSubscription.value ? ComfyQueueButton : SubscribeToRunButton
)
</script>

View File

@@ -8,7 +8,7 @@
showDelay: 600
}"
class="comfyui-queue-button"
:label="activeQueueModeMenuItem.label"
:label="String(activeQueueModeMenuItem?.label ?? '')"
severity="primary"
size="small"
:model="queueModeMenuItems"
@@ -33,7 +33,7 @@
value: item.tooltip,
showDelay: 600
}"
:label="String(item.label)"
:label="String(item.label ?? '')"
:icon="item.icon"
:severity="item.key === queueMode ? 'primary' : 'secondary'"
size="small"
@@ -82,10 +82,13 @@
import { storeToRefs } from 'pinia'
import Button from 'primevue/button'
import ButtonGroup from 'primevue/buttongroup'
import type { MenuItem } from 'primevue/menuitem'
import SplitButton from 'primevue/splitbutton'
import { computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { isCloud } from '@/platform/distribution/types'
import { useTelemetry } from '@/platform/telemetry'
import { useCommandStore } from '@/stores/commandStore'
import {
useQueuePendingTaskCountStore,
@@ -93,43 +96,52 @@ import {
} from '@/stores/queueStore'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import BatchCountEdit from './BatchCountEdit.vue'
import BatchCountEdit from '../BatchCountEdit.vue'
const workspaceStore = useWorkspaceStore()
const queueCountStore = storeToRefs(useQueuePendingTaskCountStore())
const { mode: queueMode } = storeToRefs(useQueueSettingsStore())
const { t } = useI18n()
const queueModeMenuItemLookup = computed(() => ({
disabled: {
key: 'disabled',
label: t('menu.run'),
tooltip: t('menu.disabledTooltip'),
command: () => {
queueMode.value = 'disabled'
}
},
instant: {
key: 'instant',
label: `${t('menu.run')} (${t('menu.instant')})`,
tooltip: t('menu.instantTooltip'),
command: () => {
queueMode.value = 'instant'
}
},
change: {
key: 'change',
label: `${t('menu.run')} (${t('menu.onChange')})`,
tooltip: t('menu.onChangeTooltip'),
command: () => {
queueMode.value = 'change'
const queueModeMenuItemLookup = computed(() => {
const items: Record<string, MenuItem> = {
disabled: {
key: 'disabled',
label: t('menu.run'),
tooltip: t('menu.disabledTooltip'),
command: () => {
queueMode.value = 'disabled'
}
},
change: {
key: 'change',
label: `${t('menu.run')} (${t('menu.onChange')})`,
tooltip: t('menu.onChangeTooltip'),
command: () => {
queueMode.value = 'change'
}
}
}
}))
if (!isCloud) {
items.instant = {
key: 'instant',
label: `${t('menu.run')} (${t('menu.instant')})`,
tooltip: t('menu.instantTooltip'),
command: () => {
queueMode.value = 'instant'
}
}
}
return items
})
const activeQueueModeMenuItem = computed(
() => queueModeMenuItemLookup.value[queueMode.value]
)
const activeQueueModeMenuItem = computed(() => {
// Fallback to disabled mode if current mode is not available (e.g., instant mode in cloud)
return (
queueModeMenuItemLookup.value[queueMode.value] ||
queueModeMenuItemLookup.value.disabled
)
})
const queueModeMenuItems = computed(() =>
Object.values(queueModeMenuItemLookup.value)
)
@@ -141,10 +153,15 @@ const hasPendingTasks = computed(
const commandStore = useCommandStore()
const queuePrompt = async (e: Event) => {
const commandId =
'shiftKey' in e && e.shiftKey
? 'Comfy.QueuePromptFront'
: 'Comfy.QueuePrompt'
const isShiftPressed = 'shiftKey' in e && e.shiftKey
const commandId = isShiftPressed
? 'Comfy.QueuePromptFront'
: 'Comfy.QueuePrompt'
if (isCloud) {
useTelemetry()?.trackRunButton({ subscribe_to_run: false })
}
await commandStore.execute(commandId)
}
</script>

View File

@@ -0,0 +1,7 @@
import { defineAsyncComponent } from 'vue'
import { isCloud } from '@/platform/distribution/types'
export default isCloud && __BUILD_FLAGS__.REQUIRE_SUBSCRIPTION
? defineAsyncComponent(() => import('./CloudRunButtonWrapper.vue'))
: defineAsyncComponent(() => import('./ComfyQueueButton.vue'))

View File

@@ -18,7 +18,7 @@
class="w-fit rounded-lg p-0"
:model="items"
:pt="{ item: { class: 'pointer-events-auto' } }"
aria-label="Graph navigation"
:aria-label="$t('g.graphNavigation')"
>
<template #item="{ item }">
<SubgraphBreadcrumbItem

View File

@@ -5,6 +5,7 @@
value: item.label,
showDelay: 512
}"
draggable="false"
href="#"
class="p-breadcrumb-item-link h-12 cursor-pointer px-2"
:class="{

View File

@@ -46,7 +46,12 @@
: $t('manager.installAllMissingNodes')
"
/>
<Button label="Open Manager" size="small" outlined @click="openManager" />
<Button
:label="$t('g.openManager')"
size="small"
outlined
@click="openManager"
/>
</div>
</template>

View File

@@ -89,7 +89,7 @@
<img
src="/assets/images/comfy-logo-mono.svg"
class="mr-2 h-5 w-5"
alt="Comfy"
:alt="$t('g.comfy')"
/>
{{ t('auth.login.useApiKey') }}
</Button>

View File

@@ -89,7 +89,7 @@
ref="keybindingInput"
class="mb-2 text-center"
:model-value="newBindingKeyCombo?.toString() ?? ''"
placeholder="Press keys for new binding"
:placeholder="$t('g.pressKeysForNewBinding')"
autocomplete="off"
fluid
@keydown.stop.prevent="captureKeybinding"

View File

@@ -67,10 +67,10 @@
/>
<Button
v-if="!isApiKeyLogin"
class="w-32"
class="w-fit"
variant="text"
severity="danger"
:label="$t('auth.deleteAccount.deleteAccount')"
icon="pi pi-trash"
@click="handleDeleteAccount"
/>
</div>

View File

@@ -3,7 +3,7 @@
<div class="px-2 py-4">
<img
src="/assets/images/comfy-logo-single.svg"
alt="ComfyOrg Logo"
:alt="$t('g.comfyOrgLogoAlt')"
width="32"
height="32"
/>

View File

@@ -420,9 +420,7 @@ onMounted(async () => {
throw error
}
}
CORE_SETTINGS.forEach((setting) => {
settingStore.addSetting(setting)
})
CORE_SETTINGS.forEach(settingStore.addSetting)
await newUserService().initializeIfNewUser(settingStore)

View File

@@ -10,7 +10,7 @@
></div>
<ButtonGroup
class="absolute right-2 bottom-2 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
class="absolute right-0 bottom-0 z-[1200] flex-row gap-1 border-[1px] border-node-border bg-interface-panel-surface p-2"
:style="stringifiedMinimapStyles.buttonGroupStyles"
@wheel="canvasInteractions.handleWheel"
>

View File

@@ -1,7 +1,7 @@
<template>
<div
v-if="visible"
class="absolute right-2 bottom-[66px] z-1300 flex w-[250px] justify-center border-0! bg-inherit!"
class="absolute right-0 bottom-[62px] z-1300 flex w-[250px] justify-center border-0! bg-inherit!"
>
<div
class="w-4/5 rounded-lg border border-node-border bg-interface-panel-surface p-2 text-text-primary shadow-lg select-none"

View File

@@ -243,7 +243,7 @@ const pt = computed(() => ({
},
listContainer: () => ({
style: { maxHeight: listMaxHeight },
class: 'overflow-y-auto scrollbar-hide'
class: 'scrollbar-custom'
}),
list: {
class: 'flex flex-col gap-0 p-0 m-0 list-none border-none text-sm'

View File

@@ -159,7 +159,7 @@ const pt = computed(() => ({
},
listContainer: () => ({
style: `max-height: ${listMaxHeight}`,
class: 'overflow-y-auto scrollbar-hide'
class: 'scrollbar-custom'
}),
list: {
class:

View File

@@ -180,7 +180,7 @@ onMounted(() => {
* but need to reference sidebar dimensions for proper positioning.
*/
:root {
--sidebar-padding: 8px;
--sidebar-padding: 4px;
--sidebar-icon-size: 1rem;
--sidebar-default-floating-width: 56px;

View File

@@ -59,7 +59,7 @@
<teleport v-if="isHovered" to="#node-library-node-preview-container">
<div class="node-lib-node-preview" :style="nodePreviewStyle">
<NodePreview ref="previewRef" :node-def="nodeDef" />
<NodePreview :node-def="nodeDef" />
</div>
</teleport>
</div>
@@ -132,11 +132,12 @@ function deleteBlueprint() {
void useSubgraphStore().deleteBlueprint(props.node.data.name)
}
const previewRef = ref<InstanceType<typeof NodePreview> | null>(null)
const nodePreviewStyle = ref<CSSProperties>({
position: 'absolute',
position: 'fixed',
top: '0px',
left: '0px'
left: '0px',
pointerEvents: 'none',
zIndex: 1001
})
const handleNodeHover = async () => {
@@ -144,19 +145,15 @@ const handleNodeHover = async () => {
if (!hoverTarget) return
const targetRect = hoverTarget.getBoundingClientRect()
const margin = 40
const previewHeight = previewRef.value?.$el.offsetHeight || 0
const availableSpaceBelow = window.innerHeight - targetRect.bottom
nodePreviewStyle.value.top =
previewHeight > availableSpaceBelow
? `${Math.max(0, targetRect.top - (previewHeight - availableSpaceBelow) - 20)}px`
: `${targetRect.top - 40}px`
if (sidebarLocation.value === 'left') {
nodePreviewStyle.value.left = `${targetRect.right}px`
} else {
nodePreviewStyle.value.left = `${targetRect.left - 400}px`
}
nodePreviewStyle.value.top = `${targetRect.top}px`
nodePreviewStyle.value.left =
sidebarLocation.value === 'left'
? `${targetRect.right + margin}px`
: `${targetRect.left - margin}px`
nodePreviewStyle.value.transform =
sidebarLocation.value === 'right' ? 'translateX(-100%)' : undefined
}
const container = ref<HTMLElement | null>(null)

View File

@@ -64,7 +64,7 @@ function updateToastPosition() {
styleElement.textContent = `
.p-toast.p-component.p-toast-top-right {
top: ${rect.top + 20}px !important;
top: ${rect.top + 100}px !important;
right: ${window.innerWidth - (rect.left + rect.width) + 20}px !important;
}
`

View File

@@ -1,12 +1,19 @@
<template>
<div class="flex items-center gap-2 bg-comfy-menu-secondary px-3">
<div
class="flex items-center gap-2 bg-comfy-menu-secondary"
:class="[{ 'flex-row-reverse': reverseOrder }, noPadding ? '' : 'px-3']"
>
<div
v-if="badge.label"
class="rounded-full bg-white px-1.5 py-0.5 text-xxxs font-semibold text-black"
:class="labelClass"
>
{{ badge.label }}
</div>
<div class="font-inter text-sm font-extrabold text-slate-100">
<div
class="font-inter text-sm font-extrabold text-slate-100"
:class="textClass"
>
{{ badge.text }}
</div>
</div>
@@ -14,7 +21,19 @@
<script setup lang="ts">
import type { TopbarBadge } from '@/types/comfy'
defineProps<{
badge: TopbarBadge
}>()
withDefaults(
defineProps<{
badge: TopbarBadge
reverseOrder?: boolean
noPadding?: boolean
labelClass?: string
textClass?: string
}>(),
{
reverseOrder: false,
noPadding: false,
labelClass: '',
textClass: ''
}
)
</script>

View File

@@ -4,6 +4,10 @@
v-for="badge in topbarBadgeStore.badges"
:key="badge.text"
:badge
:reverse-order="reverseOrder"
:no-padding="noPadding"
:label-class="labelClass"
:text-class="textClass"
/>
</div>
</template>
@@ -13,5 +17,20 @@ import { useTopbarBadgeStore } from '@/stores/topbarBadgeStore'
import TopbarBadge from './TopbarBadge.vue'
withDefaults(
defineProps<{
reverseOrder?: boolean
noPadding?: boolean
labelClass?: string
textClass?: string
}>(),
{
reverseOrder: false,
noPadding: false,
labelClass: '',
textClass: ''
}
)
const topbarBadgeStore = useTopbarBadgeStore()
</script>

View File

@@ -1,10 +1,12 @@
import { FirebaseError } from 'firebase/app'
import { AuthErrorCodes } from 'firebase/auth'
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useErrorHandling } from '@/composables/useErrorHandling'
import type { ErrorRecoveryStrategy } from '@/composables/useErrorHandling'
import { t } from '@/i18n'
import { isCloud } from '@/platform/distribution/types'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useDialogService } from '@/services/dialogService'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
@@ -54,6 +56,16 @@ export const useFirebaseAuthActions = () => {
detail: t('auth.signOut.successDetail'),
life: 5000
})
if (isCloud) {
try {
const router = useRouter()
await router.push({ name: 'cloud-login' })
} catch (error) {
// needed for local development until we bring in cloud login pages.
window.location.reload()
}
}
}, reportError)
const sendPasswordReset = wrapWithErrorHandlingAsync(
@@ -208,6 +220,7 @@ export const useFirebaseAuthActions = () => {
signUpWithEmail,
updatePassword,
deleteAccount,
accessError
accessError,
reportError
}
}

View File

@@ -126,14 +126,6 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
// Extract safe widget data
const slotMetadata = new Map<string, WidgetSlotMetadata>()
node.inputs?.forEach((input, index) => {
if (!input?.widget?.name) return
slotMetadata.set(input.widget.name, {
index,
linked: input.link != null
})
})
const reactiveWidgets = shallowReactive<IBaseWidget[]>(node.widgets ?? [])
Object.defineProperty(node, 'widgets', {
get() {
@@ -144,8 +136,15 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
}
})
const safeWidgets = reactiveComputed<SafeWidgetData[]>(
() =>
const safeWidgets = reactiveComputed<SafeWidgetData[]>(() => {
node.inputs?.forEach((input, index) => {
if (!input?.widget?.name) return
slotMetadata.set(input.widget.name, {
index,
linked: input.link != null
})
})
return (
node.widgets?.map((widget) => {
try {
// TODO: Use widget.getReactiveData() once TypeScript types are updated
@@ -183,7 +182,8 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
}
}
}) ?? []
)
)
})
const nodeType =
node.type ||

View File

@@ -1,6 +1,12 @@
import { useEventListener } from '@vueuse/core'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers'
const clipboardHTMLWrapper = [
'<meta charset="utf-8"><div><span data-metadata="',
'"></span></div><span style="white-space:pre-wrap;">Text</span>'
]
/**
* Adds a handler on copy that serializes selected nodes to JSON
@@ -9,28 +15,19 @@ export const useCopy = () => {
const canvasStore = useCanvasStore()
useEventListener(document, 'copy', (e) => {
if (!(e.target instanceof Element)) {
return
}
if (
(e.target instanceof HTMLTextAreaElement &&
e.target.type === 'textarea') ||
(e.target instanceof HTMLInputElement && e.target.type === 'text')
) {
if (shouldIgnoreCopyPaste(e.target)) {
// Default system copy
return
}
const isTargetInGraph =
e.target.classList.contains('litegraph') ||
e.target.classList.contains('graph-canvas-container') ||
e.target.id === 'graph-canvas'
// copy nodes and clear clipboard
const canvas = canvasStore.canvas
if (isTargetInGraph && canvas?.selectedItems) {
canvas.copyToClipboard()
if (canvas?.selectedItems) {
const serializedData = canvas.copyToClipboard()
// clearData doesn't remove images from clipboard
e.clipboardData?.setData('text', ' ')
e.clipboardData?.setData(
'text/html',
clipboardHTMLWrapper.join(btoa(serializedData))
)
e.preventDefault()
e.stopImmediatePropagation()
return false

View File

@@ -21,7 +21,10 @@ import {
import type { Point } from '@/lib/litegraph/src/litegraph'
import { useAssetBrowserDialog } from '@/platform/assets/composables/useAssetBrowserDialog'
import { createModelNodeFromAsset } from '@/platform/assets/utils/createModelNodeFromAsset'
import { isCloud } from '@/platform/distribution/types'
import { useSettingStore } from '@/platform/settings/settingStore'
import { SUPPORT_URL } from '@/platform/support/config'
import { useTelemetry } from '@/platform/telemetry'
import { useToastStore } from '@/platform/updates/common/toastStore'
import { useWorkflowService } from '@/platform/workflow/core/services/workflowService'
import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore'
@@ -451,6 +454,11 @@ export function useCoreCommands(): ComfyCommand[] {
category: 'essentials' as const,
function: async () => {
const batchCount = useQueueSettingsStore().batchCount
if (isCloud) {
useTelemetry()?.trackWorkflowExecution()
}
await app.queuePrompt(0, batchCount)
}
},
@@ -462,6 +470,11 @@ export function useCoreCommands(): ComfyCommand[] {
category: 'essentials' as const,
function: async () => {
const batchCount = useQueueSettingsStore().batchCount
if (isCloud) {
useTelemetry()?.trackWorkflowExecution()
}
await app.queuePrompt(-1, batchCount)
}
},
@@ -763,7 +776,7 @@ export function useCoreCommands(): ComfyCommand[] {
label: 'Contact Support',
versionAdded: '1.17.8',
function: () => {
window.open('https://support.comfy.org/', '_blank')
window.open(SUPPORT_URL, '_blank')
}
},
{

View File

@@ -7,6 +7,22 @@ import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { app } from '@/scripts/app'
import { useWorkspaceStore } from '@/stores/workspaceStore'
import { isAudioNode, isImageNode, isVideoNode } from '@/utils/litegraphUtil'
import { shouldIgnoreCopyPaste } from '@/workbench/eventHelpers'
function pasteClipboardItems(data: DataTransfer): boolean {
const rawData = data.getData('text/html')
const match = rawData.match(/data-metadata="([A-Za-z0-9+/=]+)"/)?.[1]
if (!match) return false
try {
useCanvasStore()
.getCanvas()
._deserializeItems(JSON.parse(atob(match)), {})
return true
} catch (err) {
console.error(err)
}
return false
}
/**
* Adds a handler on paste that extracts and loads images or workflows from pasted JSON data
@@ -38,15 +54,10 @@ export const usePaste = () => {
}
useEventListener(document, 'paste', async (e) => {
const isTargetInGraph =
e.target instanceof Element &&
(e.target.classList.contains('litegraph') ||
e.target.classList.contains('graph-canvas-container') ||
e.target.id === 'graph-canvas')
// If the target is not in the graph, we don't want to handle the paste event
if (!isTargetInGraph) return
if (shouldIgnoreCopyPaste(e.target)) {
// Default system copy
return
}
// ctrl+shift+v is used to paste nodes with connections
// this is handled by litegraph
if (workspaceStore.shiftDown) return
@@ -109,6 +120,7 @@ export const usePaste = () => {
return
}
}
if (pasteClipboardItems(data)) return
// No image found. Look for node data
data = data.getData('text/plain')

View File

@@ -0,0 +1 @@
export const MONTHLY_SUBSCRIPTION_PRICE = 20

View File

@@ -0,0 +1,24 @@
import { watch } from 'vue'
import { useCurrentUser } from '@/composables/auth/useCurrentUser'
import { useSubscription } from '@/platform/cloud/subscription/composables/useSubscription'
import { useExtensionService } from '@/services/extensionService'
useExtensionService().registerExtension({
name: 'Comfy.CloudSubscription',
setup: async () => {
const { isLoggedIn } = useCurrentUser()
const { requireActiveSubscription } = useSubscription()
const checkSubscriptionStatus = () => {
if (!isLoggedIn.value) return
void requireActiveSubscription()
}
watch(() => isLoggedIn.value, checkSubscriptionStatus, {
immediate: true
})
}
})

View File

@@ -26,4 +26,8 @@ import './widgetInputs'
if (isCloud) {
import('./cloudBadge')
if (__BUILD_FLAGS__.REQUIRE_SUBSCRIPTION) {
import('./cloudSubscription')
}
}

View File

@@ -3,6 +3,7 @@ import { GLTFExporter } from 'three/examples/jsm/exporters/GLTFExporter'
import { OBJExporter } from 'three/examples/jsm/exporters/OBJExporter'
import { STLExporter } from 'three/examples/jsm/exporters/STLExporter'
import { downloadBlob } from '@/base/common/downloadUtil'
import { t } from '@/i18n'
import { useToastStore } from '@/platform/updates/common/toastStore'
@@ -38,13 +39,7 @@ export class ModelExporter {
try {
const response = await fetch(url)
const blob = await response.blob()
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = desiredFilename
link.click()
URL.revokeObjectURL(link.href)
downloadBlob(desiredFilename, blob)
} catch (error) {
console.error('Error downloading from URL:', error)
useToastStore().addAlert(t('toastMessages.failedToDownloadFile'))
@@ -152,19 +147,11 @@ export class ModelExporter {
private static saveArrayBuffer(buffer: ArrayBuffer, filename: string): void {
const blob = new Blob([buffer], { type: 'application/octet-stream' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
link.click()
URL.revokeObjectURL(link.href)
downloadBlob(filename, blob)
}
private static saveString(text: string, filename: string): void {
const blob = new Blob([text], { type: 'text/plain' })
const link = document.createElement('a')
link.href = URL.createObjectURL(blob)
link.download = filename
link.click()
URL.revokeObjectURL(link.href)
downloadBlob(filename, blob)
}
}

View File

@@ -1,5 +1,7 @@
import * as THREE from 'three'
import { downloadBlob } from '@/base/common/downloadUtil'
import { type EventManagerInterface } from './interfaces'
export class RecordingManager {
@@ -149,17 +151,7 @@ export class RecordingManager {
try {
const blob = new Blob(this.recordedChunks, { type: 'video/webm' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
document.body.appendChild(a)
a.style.display = 'none'
a.href = url
a.download = filename
a.click()
window.URL.revokeObjectURL(url)
document.body.removeChild(a)
downloadBlob(filename, blob)
this.eventManager.emitEvent('recordingExported', null)
} catch (error) {

View File

@@ -1,3 +1,4 @@
import { downloadBlob } from '@/base/common/downloadUtil'
import { t } from '@/i18n'
import { LGraphCanvas } from '@/lib/litegraph/src/litegraph'
import { useToastStore } from '@/platform/updates/common/toastStore'
@@ -145,18 +146,7 @@ class ManageTemplates extends ComfyDialog {
const json = JSON.stringify({ templates: this.templates }, null, 2) // convert the data to a JSON string
const blob = new Blob([json], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = $el('a', {
href: url,
download: 'node_templates.json',
style: { display: 'none' },
parent: document.body
})
a.click()
setTimeout(function () {
a.remove()
window.URL.revokeObjectURL(url)
}, 0)
downloadBlob('node_templates.json', blob)
}
override show() {
@@ -298,19 +288,9 @@ class ManageTemplates extends ComfyDialog {
const blob = new Blob([json], {
type: 'application/json'
})
const url = URL.createObjectURL(blob)
const a = $el('a', {
href: url,
// @ts-expect-error fixme ts strict error
download: (nameInput.value || t.name) + '.json',
style: { display: 'none' },
parent: document.body
})
a.click()
setTimeout(function () {
a.remove()
window.URL.revokeObjectURL(url)
}, 0)
// @ts-expect-error fixme ts strict error
const name = (nameInput.value || t.name) + '.json'
downloadBlob(name, blob)
}
}),
$el('button', {

View File

@@ -1,68 +1,159 @@
import { createI18n } from 'vue-i18n'
import arCommands from './locales/ar/commands.json' with { type: 'json' }
import ar from './locales/ar/main.json' with { type: 'json' }
import arNodes from './locales/ar/nodeDefs.json' with { type: 'json' }
import arSettings from './locales/ar/settings.json' with { type: 'json' }
// ESLint cannot statically resolve dynamic imports with relative paths in template strings,
// but these are valid ES module imports that Vite processes correctly at build time.
// Import only English locale eagerly as the default/fallback
import enCommands from './locales/en/commands.json' with { type: 'json' }
import en from './locales/en/main.json' with { type: 'json' }
import enNodes from './locales/en/nodeDefs.json' with { type: 'json' }
import enSettings from './locales/en/settings.json' with { type: 'json' }
import esCommands from './locales/es/commands.json' with { type: 'json' }
import es from './locales/es/main.json' with { type: 'json' }
import esNodes from './locales/es/nodeDefs.json' with { type: 'json' }
import esSettings from './locales/es/settings.json' with { type: 'json' }
import frCommands from './locales/fr/commands.json' with { type: 'json' }
import fr from './locales/fr/main.json' with { type: 'json' }
import frNodes from './locales/fr/nodeDefs.json' with { type: 'json' }
import frSettings from './locales/fr/settings.json' with { type: 'json' }
import jaCommands from './locales/ja/commands.json' with { type: 'json' }
import ja from './locales/ja/main.json' with { type: 'json' }
import jaNodes from './locales/ja/nodeDefs.json' with { type: 'json' }
import jaSettings from './locales/ja/settings.json' with { type: 'json' }
import koCommands from './locales/ko/commands.json' with { type: 'json' }
import ko from './locales/ko/main.json' with { type: 'json' }
import koNodes from './locales/ko/nodeDefs.json' with { type: 'json' }
import koSettings from './locales/ko/settings.json' with { type: 'json' }
import ruCommands from './locales/ru/commands.json' with { type: 'json' }
import ru from './locales/ru/main.json' with { type: 'json' }
import ruNodes from './locales/ru/nodeDefs.json' with { type: 'json' }
import ruSettings from './locales/ru/settings.json' with { type: 'json' }
import trCommands from './locales/tr/commands.json' with { type: 'json' }
import tr from './locales/tr/main.json' with { type: 'json' }
import trNodes from './locales/tr/nodeDefs.json' with { type: 'json' }
import trSettings from './locales/tr/settings.json' with { type: 'json' }
import zhTWCommands from './locales/zh-TW/commands.json' with { type: 'json' }
import zhTW from './locales/zh-TW/main.json' with { type: 'json' }
import zhTWNodes from './locales/zh-TW/nodeDefs.json' with { type: 'json' }
import zhTWSettings from './locales/zh-TW/settings.json' with { type: 'json' }
import zhCommands from './locales/zh/commands.json' with { type: 'json' }
import zh from './locales/zh/main.json' with { type: 'json' }
import zhNodes from './locales/zh/nodeDefs.json' with { type: 'json' }
import zhSettings from './locales/zh/settings.json' with { type: 'json' }
function buildLocale<M, N, C, S>(main: M, nodes: N, commands: C, settings: S) {
function buildLocale<
M extends Record<string, unknown>,
N extends Record<string, unknown>,
C extends Record<string, unknown>,
S extends Record<string, unknown>
>(main: M, nodes: N, commands: C, settings: S) {
return {
...main,
nodeDefs: nodes,
commands: commands,
settings: settings
}
} as M & { nodeDefs: N; commands: C; settings: S }
}
const messages = {
en: buildLocale(en, enNodes, enCommands, enSettings),
zh: buildLocale(zh, zhNodes, zhCommands, zhSettings),
'zh-TW': buildLocale(zhTW, zhTWNodes, zhTWCommands, zhTWSettings),
ru: buildLocale(ru, ruNodes, ruCommands, ruSettings),
ja: buildLocale(ja, jaNodes, jaCommands, jaSettings),
ko: buildLocale(ko, koNodes, koCommands, koSettings),
fr: buildLocale(fr, frNodes, frCommands, frSettings),
es: buildLocale(es, esNodes, esCommands, esSettings),
ar: buildLocale(ar, arNodes, arCommands, arSettings),
tr: buildLocale(tr, trNodes, trCommands, trSettings)
// Locale loader map - dynamically import locales only when needed
const localeLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('./locales/ar/main.json'),
es: () => import('./locales/es/main.json'),
fr: () => import('./locales/fr/main.json'),
ja: () => import('./locales/ja/main.json'),
ko: () => import('./locales/ko/main.json'),
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')
}
const nodeDefsLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('./locales/ar/nodeDefs.json'),
es: () => import('./locales/es/nodeDefs.json'),
fr: () => import('./locales/fr/nodeDefs.json'),
ja: () => import('./locales/ja/nodeDefs.json'),
ko: () => import('./locales/ko/nodeDefs.json'),
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')
}
const commandsLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('./locales/ar/commands.json'),
es: () => import('./locales/es/commands.json'),
fr: () => import('./locales/fr/commands.json'),
ja: () => import('./locales/ja/commands.json'),
ko: () => import('./locales/ko/commands.json'),
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')
}
const settingsLoaders: Record<
string,
() => Promise<{ default: Record<string, unknown> }>
> = {
ar: () => import('./locales/ar/settings.json'),
es: () => import('./locales/es/settings.json'),
fr: () => import('./locales/fr/settings.json'),
ja: () => import('./locales/ja/settings.json'),
ko: () => import('./locales/ko/settings.json'),
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')
}
// Track which locales have been loaded
const loadedLocales = new Set<string>(['en'])
// Track locales currently being loaded to prevent race conditions
const loadingLocales = new Map<string, Promise<void>>()
/**
* Dynamically load a locale and its associated files (nodeDefs, commands, settings)
*/
export async function loadLocale(locale: string): Promise<void> {
if (loadedLocales.has(locale)) {
return
}
// If already loading, return the existing promise to prevent duplicate loads
const existingLoad = loadingLocales.get(locale)
if (existingLoad) {
return existingLoad
}
const loader = localeLoaders[locale]
const nodeDefsLoader = nodeDefsLoaders[locale]
const commandsLoader = commandsLoaders[locale]
const settingsLoader = settingsLoaders[locale]
if (!loader || !nodeDefsLoader || !commandsLoader || !settingsLoader) {
console.warn(`Locale "${locale}" is not supported`)
return
}
// Create and track the loading promise
const loadPromise = (async () => {
try {
const [main, nodes, commands, settings] = await Promise.all([
loader(),
nodeDefsLoader(),
commandsLoader(),
settingsLoader()
])
const messages = buildLocale(
main.default,
nodes.default,
commands.default,
settings.default
)
i18n.global.setLocaleMessage(locale, messages as LocaleMessages)
loadedLocales.add(locale)
} catch (error) {
console.error(`Failed to load locale "${locale}":`, error)
throw error
} finally {
// Clean up the loading promise once complete
loadingLocales.delete(locale)
}
})()
loadingLocales.set(locale, loadPromise)
return loadPromise
}
// Only include English in the initial bundle
const messages = {
en: buildLocale(en, enNodes, enCommands, enSettings)
}
// Type for locale messages - inferred from the English locale structure
type LocaleMessages = typeof messages.en
export const i18n = createI18n({
// Must set `false`, as Vue I18n Legacy API is for Vue 2
legacy: false,

View File

@@ -1665,7 +1665,7 @@ export class LGraph
continue
}
const input = subgraphNode.findInputSlotByType(link.type, true, true)
const input = subgraphNode.inputs[i - 1]
outputNode.connectSlots(output, subgraphNode, input, link.parentId)
}

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