Compare commits

..

153 Commits

Author SHA1 Message Date
CodeRabbit Fixer
eb8521d16f fix: Unify UserAvatar compact class to size-full to prevent width shift when toggling workspaces (#9689)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 03:21:19 +01:00
AustinMroz
af5a72021b Use preview downscaling in fewer places (#9678)
Thumbnail downscaling is currently being used in more places than it
should be.
- Nodes which display images will display incorrect resolution
indicators
<img width="255" height="372" alt="image"
src="https://github.com/user-attachments/assets/674790b6-04c8-4db0-84c2-2fa2dbaf123d"
/> <img width="255" height="372" alt="image"
src="https://github.com/user-attachments/assets/1dbe751b-7462-4408-9236-9446b005f5fc"
/>

This is particularly confusing with output nodes, which claim the output
is not of the intended resolution
- The "Download Image" and "Open Image" context menu actions will
incorrectly download the downscaled thumbnail.
- The assets panel will incorrectly display the thumbnail resolution as
the resolution of the output
- The lightbox (zoom) of an image will incorrectly display a downscaled
thumbnail.

This PR is a quick workaround to staunch the major problems
- Nodes always display full previews.
- Resolution downscaling is applied on the assert card, not on the
assetItem itself
- Due to implementation, this means that asset cards will still
incorrectly show the resolution of the thumbnail instead of the size of
the full image.

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-03-09 16:03:32 -07:00
Hunter
4e5bb3e540 fix: dispatch cloud build on synchronize for preview-labeled PRs (#9636)
## Summary

Cloud build dispatch was only triggering on the `labeled` event, not on
subsequent pushes to PRs that already had a preview label.

## Changes

- **What**: Add `synchronize` to `pull_request` event types and update
the `if` condition to support all three preview labels (`preview`,
`preview-cpu`, `preview-gpu`). For `labeled` events, check the added
label name; for `synchronize` events, check existing PR labels.

## Review Focus

The `if` condition now branches on `github.event.action` to use the
correct label-checking mechanism for each event type.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9636-fix-dispatch-cloud-build-on-synchronize-for-preview-labeled-PRs-31e6d73d3650814e9069e37d6199ffc9)
by [Unito](https://www.unito.io)
2026-03-09 15:55:53 -07:00
AustinMroz
2ccfb822b4 Restore hiding of linked inputs in app mode (#9671)
As a temporary fix for widgets being incorrectly hidden, #9669 allowed
all disabled widgets to be displayed.

This PR provides a more robust implementation to derive whether the
widget, as would be displayed from the root graph, is disabled.

Potential regression:
- Drag drop handlers are applied on node, not widgets. A subgraph
containing a "Load Image" node, does not allow dragging and dropping an
image onto the subgraphNode in order to load it. Because app mode
widgets would display from the original owning node prior to this PR,
these drag/drop handlers would apply. Placing "Load Image" nodes. I
believe this change makes behavior more consistent, but it warrants
consideration.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9671-Restore-hiding-of-linked-inputs-in-app-mode-31e6d73d365081688e37fbb931f3af68)
by [Unito](https://www.unito.io)
2026-03-09 13:18:05 -07:00
jaeone94
370003da94 fix: add isGraphReady guard to prevent premature graph access error logs (#9672)
## Summary
Adds `isGraphReady` getter to `ComfyApp` and uses it in
`executionErrorStore` guards to prevent false 'ComfyApp graph accessed
before initialization' error logs during early store evaluation.

## Changes
- **What**: Added `isGraphReady` boolean getter to `ComfyApp` that
safely checks graph initialization without triggering the `rootGraph`
getter's error log. Updated 5 guard sites in `executionErrorStore` to
use `app.isGraphReady` instead of `app.rootGraph`.
- **Why**: The `rootGraph` getter logs an error when accessed before
initialization. Computed properties and watch callbacks in
`executionErrorStore` are evaluated early (before graph init), causing
false error noise in the console.

## Review Focus
- `isGraphReady` is intentionally minimal — just
`!!this.rootGraphInternal` — to avoid duplicating the error-logging
behavior of `rootGraph`
- The `watch(lastNodeErrors, ...)` callback now checks `isGraphReady` at
the top and early-returns, consistent with the computed property pattern

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9672-fix-add-isGraphReady-guard-to-prevent-premature-graph-access-error-logs-31e6d73d365081be8e1fc77114ce9382)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-03-09 13:15:04 -07:00
Alexander Brown
3b5af4960f fix: show load widget inputs in media dropdown (#9670)
Main targeted, built on
https://github.com/Comfy-Org/ComfyUI_frontend/pull/9551

## Summary

Fix Load Image/Load Video input dropdown tabs not showing available
input assets in Vue node select dropdown.

## Changes

- **What**: Keep combo widget `options` object identity while exposing
dynamic `values` for cloud/remote combos.
- **What**: Remove temporary debug logging and restore clearer dropdown
filter branching.
- **What**: Remove stale `searcher`/`updateKey` prop plumbing in
dropdown menu/actions and update related tests.

## Review Focus

Verify `Load Image` / `Load Video` Inputs tab behavior and confirm
cloud/remote combo option values still update correctly.

Relates to #9551

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9670-fix-show-load-widget-inputs-in-media-dropdown-31e6d73d36508148b845e18268a60c2a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: Amp <amp@ampcode.com>
2026-03-09 12:49:47 -07:00
Christian Byrne
46895ee1a9 docs: add release process guide (#9548)
Adds a concise guide to `docs/release-process.md` explaining how the
release workflows interact, with focus on the version semantics that
differ between minor and patch bumps.

Key sections:
- How minor bumps freeze the previous minor into `core/` and `cloud/`
branches
- How patch bumps on `main` vs `core/X.Y` differ (published vs draft
releases)
- Why unreleased commits are dual-homed when a minor bump happens
- Summary table, backporting, publishing, and bi-weekly automation

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9548-docs-add-release-process-guide-31d6d73d365081f2bdaace48a7cb81ae)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-03-09 12:38:40 -07:00
AustinMroz
7f0472fde4 Always use interior nodeId for app mode (#9669)
App mode stores the state of selected widgets as a tuple of `[NodeId,
WidgetName]`. With recent subgraph changes, for a given node,
`widget.name` will no longer uniquely resolve to a single widget.

- From both Vue and Litegraph, selecting an input for display in App
mode will now resolve the NodeId of the node which owns the widget
instead of the selected node.
- When displaying selections in litegraph, if the NodeId does not exist
in the current graph, instead of resolving the actual node the rootGraph
is searched for any subgraphNode which contains a view matching the
`[NodeId, WidgetName]` pair.
- When displaying widgets in App mode, the widget is always set as being
a view of the real widget (This means that they will not display a
purple promotion border.

Known Issue:
- These same subgraph changes made it so that a widget can be linked
without being disabled. This PR makes it so widgets which have been
linked instead display normally under the assumption that they are
incorrectly marked as disabled. As disabled widgets can not be selected
as inputs, this should handle normal usage fine, but a better solution
is being investigated

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9669-Always-use-interior-nodeId-for-app-mode-31e6d73d365081f8a918d0e43cb659ee)
by [Unito](https://www.unito.io)
2026-03-09 11:36:33 -07:00
Alexander Brown
24ac6388d7 style: Update share icon to be a send icon instead (#9667)
## Summary

It's a plane!

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9667-style-Update-share-icon-to-be-a-send-icon-instead-31e6d73d365081919013ea1521d26f2c)
by [Unito](https://www.unito.io)
2026-03-09 17:00:53 +00:00
Alexander Brown
6b6049e48e fix: Add a tooltip to account for assets with really long names. (#9665)
## Summary

Simple tooltip, default show delay.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9665-fix-Add-a-tooltip-to-account-for-assets-with-really-long-names-31e6d73d3650811f9057d1ec41e761b6)
by [Unito](https://www.unito.io)
2026-03-09 09:52:28 -07:00
AustinMroz
592f992d1d Even further app fixes (#9617)
- Allow dragging zoom pane with middle click
- Prevent selection of canvasOnly widgets
- These widgets would not display in app mode, so allow selection would
only cause confusion.
- Support displaying the error dialogue in app mode
- Add a somewhat involved mobile app mode error indication system
<img width="300" alt="image"
src="https://github.com/user-attachments/assets/d8793bbd-fff5-4b2a-a316-6ff154bae2c4"
/> <img width="300" alt="image"
src="https://github.com/user-attachments/assets/cb88b0f6-f7e5-409e-ae43-f1348f946b19"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9617-Even-further-app-fixes-31d6d73d365081c891dfdfe3477cfd61)
by [Unito](https://www.unito.io)
2026-03-09 09:35:31 -07:00
jaeone94
76fd80aa98 fix: hide empty actionbar container and relocate error border to floating actionbar (#9657)
## Summary
When the actionbar is floating and has no docked buttons, the container
is now hidden (zero-width, transparent border) to avoid showing an empty
rounded box. Additionally, the error/destructive border is now applied
to the floating actionbar panel itself (via `ComfyActionbar`) instead of
the container, so it appears in the correct location when floating.

## Changes
- **TopMenuSection**: Added `hasDockedButtons` and
`isActionbarContainerEmpty` computed properties to detect when the
docked container has no visible buttons; `actionbarContainerClass`
computed hides the container by collapsing it when empty and floating,
while preserving the legacy drop zone via `:has(.border-dashed)` CSS
selector
- **TopMenuSection**: Error border
(`border-destructive-background-hover`) is now only applied to the
docked container when the actionbar is **not** floating
- **ComfyActionbar**: Accepts new `hasAnyError` prop and applies the
error border to the floating panel's `panelClass` when floating

## Review Focus
- The `has-[.border-dashed]` CSS selector restores the container visuals
when a legacy drag-target element is present inside it — verify this
works as expected
- Error border placement: docked mode shows border on container,
floating mode shows border on the fixed panel

## Screenshots


https://github.com/user-attachments/assets/75caabac-e391-4bfd-b4dc-62d564e55d37
2026-03-09 21:24:00 +09:00
Hunter
63c36d3f2f feat: display original asset names instead of hashes in assets panel (#9626)
## Problem
Output assets in the assets panel show content hashes (e.g.,
`a1b2c3d4.png`) instead of display names (e.g., `ComfyUI_00001_.png`).

## Root Cause
Cloud inference replaces `filename` with the content hash in the output
transform pipeline. The hashed filename gets stored in the jobs table's
`preview_output` JSONB. The frontend uses this hash as the display name.

## Solution
- Add `display_name` field to `AssetItem` schema and `ResultItemImpl`
- Backend (cloud PR) joins job→assets table to resolve the original name
and injects `display_name` into job responses
- Frontend prefers `display_name` over `name` **only for display text
and download filenames**
- `asset.name` remains unchanged (the hash) for URLs, drag-to-canvas,
export filters, and output key dedup

## Backwards Compatible
- OSS: `display_name` is undefined, falls back to `asset.name` (which is
already the real filename in OSS)
- Cloud pre-deploy: `display_name` absent from API, falls back
gracefully
- Old jobs with no assets: `display_name` not injected, no change

## Cloud PR
https://github.com/Comfy-Org/cloud/pull/2747



https://github.com/user-attachments/assets/8a4c9cac-4ade-4ea2-9a70-9af240a56602
2026-03-09 01:06:28 -04:00
pythongosssss
892a9cf2c5 fix: prevent showing outputs in app mode when no output nodes configured (#9625)
## Summary

After a user runs the workflow once in graph mode, switching to app mode
with no app built, incorrectly showed the app mode outputs view instead
of the intro screen

## Changes

- **What**: don't try and select outputs if no outputs & filter out all
outputs when nothing chosen
2026-03-08 17:36:15 -07:00
Comfy Org PR Bot
308c22efc6 1.42.2 (#9629)
Patch version increment to 1.42.2

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9629-1-42-2-31e6d73d365081faa106d97ae431e2e6)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2026-03-08 17:24:52 -07:00
Hunter
5728d240da fix: restore backend outputs_count for asset sidebar multi-output badge (#9627)
## Summary

Fix regression from PR #9535 where the multi-output count badge stopped
appearing in the asset sidebar.

## Root Cause

PR #9535 changed `outputCount` in `mapHistoryToAssets` from
`job.outputs_count` (backend-provided total) to
`task.previewableOutputs.length`. However, `TaskItemImpl` constructed
from a job listing only has the single `preview_output`, so
`previewableOutputs.length` is always **1** — the multi-output badge
never appears.

## Fix

Use the backend-provided `outputs_count` (via `task.outputsCount`) with
fallback to `task.previewableOutputs.length` when unavailable. This
restores the correct count while preserving the fallback for jobs that
don't have `outputs_count` from the server.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9627-fix-restore-backend-outputs_count-for-asset-sidebar-multi-output-badge-31d6d73d36508160b93fd03af4a01aa3)
by [Unito](https://www.unito.io)
2026-03-08 13:17:22 -07:00
Kelly Yang
acf2f4280c fix(maskeditor): make brush size slider logarithmic (#8097) (#9534)
## Summary
fix #8097.

This PR shifts the Mask Editor Brush Size slider from a linear scale to
a logarithmic (exponential) scale. Previously, the linear 1-250 range
heavily clumped the usable, small "fine-detail" brush sizes (e.g., 1px
to 20px) into the very first 10% of the slider, making it extremely
difficult to select precise sizes with the mouse.

This update borrows UX paradigms from other standard image editors like
Photoshop and GIMP, which map their scale entry widgets on an
exponential curve.

## GIMP Source
By inspecting the official **GIMP** source code under
`libgimpwidgets/gimpscaleentry.c`, we can see this exact mathematical
relationship being utilized when the logarithmic property is marked TRUE
on a brush radius adjustment widget:

```
// Mapping visual slider to internal value
value = gtk_adjustment_get_lower(...) + exp(t);
// Mapping internal value to visual slider
t = log (value - gtk_adjustment_get_lower(...) + 0.1);
```


https://github.com/user-attachments/assets/6d59ff12-f623-42cc-a52b-84147e9bb90b

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9534-fix-maskeditor-make-brush-size-slider-logarithmic-8097-31c6d73d365081118508e8363e0c5312)
by [Unito](https://www.unito.io)
2026-03-08 09:11:19 -07:00
Comfy Org PR Bot
7ad6994d01 1.42.1 (#9546)
Patch version increment to 1.42.1

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9546-1-42-1-31d6d73d365081a781fdebfef024a7cd)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-03-07 18:28:06 -08:00
Christian Byrne
2829f78579 fix: use previewable output count for asset sidebar badge (#9535)
## Summary

Fix asset sidebar badge showing inflated output count by using
previewable outputs length instead of raw server count.

## Changes

- **What**: Changed `outputCount` in `mapHistoryToAssets` from
`job.outputs_count` (includes all output types: text, JSON, custom data)
to `task.previewableOutputs.length` (only image, video, audio, 3D). The
badge now matches what users actually see in the expanded view.

## Review Focus

One-line change. The `task.previewableOutputs` array is already computed
on the line immediately below and used for `allOutputs`, so this
introduces no new computation.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9535-fix-use-previewable-output-count-for-asset-sidebar-badge-31c6d73d365081c49161caec64cf3921)
by [Unito](https://www.unito.io)
2026-03-07 18:03:21 -08:00
pythongosssss
c4156d7059 feat/fix: App mode further updates (#9545)
## Summary

Additional updates

## Changes

- **What**: 
- Share widget rename functionality with properties panel implementation
- Add hammer icon to builder mode tabs
- Change (!) to (i) on app builder info sections

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9545-feat-fix-App-mode-further-updates-31c6d73d36508104aaa9c5f1e6205a0b)
by [Unito](https://www.unito.io)
2026-03-07 16:03:55 -08:00
Christian Byrne
725a0a2b89 fix: remove timeouts from error toasts so they persist until dismissed (#9543)
## Summary

Remove `life` (timeout) property from all error-severity toast calls so
they persist until manually dismissed, preventing users from missing
important error messages.

## Changes

- **What**: Removed `life` property from 86 error toast calls across 46
files. Error toasts now use PrimeVue's default behavior (no
auto-dismiss). Non-error toasts (success, warn, info) are unchanged.
- Also fixed a pre-existing lint issue in `TaskListPanel.vue` (`import {
t } from '@/i18n'` → `useI18n()`)

## Review Focus

- One conditional toast in `useMediaAssetActions.ts` intentionally keeps
`life` because its severity alternates between `warn` and `error`

Fixes
https://www.notion.so/comfy-org/Implement-Remove-timeouts-for-all-error-toasts-31b6d73d365081cead54fddc77ae7c3d

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9543-fix-remove-timeouts-from-error-toasts-so-they-persist-until-dismissed-31c6d73d365081fa8d30f6366e9bfe38)
by [Unito](https://www.unito.io)
2026-03-07 15:08:13 -08:00
Alexander Brown
8a5bcde168 fix: prevent non-widget inputs on nested subgraphs from appearing as button widgets (#9542)
## Summary

Fix non-widget inputs on nested subgraphs appearing twice — once as
slots and once as unresolved button widgets.

## Changes

- **What**: Add `getTargetWidget()` guard in the `isSubgraphNode()`
branch of `resolveSubgraphInputTarget`, matching the existing check for
non-subgraph nodes. Non-widget inputs (e.g. AUDIO, IMAGE) now return
`undefined` instead of a bogus promotion entry.

## Review Focus

`resolveSubgraphInputTarget` had an asymmetry: the non-subgraph branch
checked `getTargetWidget()` before returning, but the `isSubgraphNode()`
branch returned unconditionally for every input. For nested subgraphs
where non-widget slots are linked through to inner SubgraphNode inputs,
this created `PromotedWidgetView` entries that failed `resolveDeepest()`
(falling back to `type: 'button'`), while the inputs also rendered as
normal slot circles since `input.widget` was never set by
`_resolveInputWidget` (which correctly skipped them).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9542-fix-prevent-non-widget-inputs-on-nested-subgraphs-from-appearing-as-button-widgets-31c6d73d3650816387c3f97f0385e762)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-03-07 22:58:59 +00:00
AustinMroz
83ffaf30c8 Yet further app fixes (#9523)
- Prevent selection of note nodes
- Prevents selection  of nodes with errors
- A bit broader than the reported "can select missing nodes". A node
with an error is a node that can not execute and thus can not be used in
an app.
- Updates the typeform survey
- Add a collapsible list of all api nodes(/prices) contained in an app.
  - Needs to be prettied up for mobile still.
<img width="322" height="751" alt="image"
src="https://github.com/user-attachments/assets/ebfeeada-9b80-488e-88d6-feaa8bd53629"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9523-Yet-further-app-fixes-31c6d73d365081de9150fbf2d3ec54dd)
by [Unito](https://www.unito.io)
2026-03-07 14:22:15 -08:00
Christian Byrne
2875f897dc fix: remove workspace switching confirmation dialog (#9250)
## Summary

Remove the workspace switching confirmation dialog since switching
workspaces no longer discards unsaved changes.

## Changes

- **What**: Remove `hasUnsavedChanges` check, `dialogService.confirm`
call, and unused imports (`useI18n`, `useWorkflowStore`,
`useDialogService`) from `useWorkspaceSwitch`. Rename
`switchWithConfirmation` to `switchWorkspace`. Update callers
(`WorkspaceSwitcherPopover.vue`, `InviteAcceptedToast.vue`). Remove
`workspace.unsavedChanges` i18n entries from all 12 locale files.
Simplify tests to cover core switching behavior only.

## Review Focus

The confirmation dialog was showing inaccurate information (warning
about discarding unsaved changes when that no longer happens). This is a
pure removal with no new behavior.

<!-- Pipeline-Ticket: COM-15441 -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9250-fix-remove-workspace-switching-confirmation-dialog-3136d73d365081d3b959da22e8f151d1)
by [Unito](https://www.unito.io)
2026-03-07 14:19:05 -08:00
pythongosssss
ec129de63d fix: Prevent corruption of workflow data due to checkState during graph loading (#9531)
## Summary

During workflow loading, the workflow data & active workflow object can
be out of sync, meaning any checkState calls will overwrite data into
the wrong workflow.

Recreation steps:
* Open 2-3 workflows
* Enter builder mode > select step
* Select some different inputs on each
* Quickly tap the shift key (this triggers checkState) while switching
tabs
* After a while, you'll see the wrong inputs on the workflows

Alternatively, register an extension that guarantees to call checkState
during the bad phase, run this in browser devtools and switch tabs:
```
window.app.registerExtension({
  name: 'bad',
  async afterConfigureGraph() {
    window.app.extensionManager.workflow.activeWorkflow.changeTracker.checkState()
  }
})
```

## Changes

- **What**: 
- Add loading graph flag
- Prevent checkState calls while loading
- Prevent app mode data sync while loading

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9531-fix-Prevent-corruption-of-workflow-data-due-to-checkState-during-graph-loading-31c6d73d365081e2ab91d9145bf1d025)
by [Unito](https://www.unito.io)
2026-03-07 12:44:12 -08:00
pythongosssss
1687ca93b3 feat: Integrated tab UI updates (#8516)
## Summary
Next iteration of the integrated tab/top menu

## Changes
- **What**:  
- make integrated default, rename old to legacy
- move feedback to integrated
- fix user icon shapes
- remove comfy cloud text in top bar, move to canvas stats
- add chevron to C logo menu
- move help back to sidebar
   - remove now unused help top positioning code

## Screenshots (if applicable)
<img width="428" height="148" alt="image"
src="https://github.com/user-attachments/assets/725025b7-4982-4f61-be11-8aabb0a1faff"
/>
<img width="264" height="187" alt="image"
src="https://github.com/user-attachments/assets/91fa5e92-df08-4467-9bc5-50a614d9b8aa"
/>
<img width="1169" height="220" alt="image"
src="https://github.com/user-attachments/assets/68c81bea-0cff-48df-8303-a6231a1d2fc4"
/>
<img width="242" height="207" alt="image"
src="https://github.com/user-attachments/assets/5a10f40e-83ae-44c3-9434-3dbe87ba30e2"
/>
<img width="302" height="222" alt="image"
src="https://github.com/user-attachments/assets/27fcc638-5fff-4302-9a1f-066227aafd86"
/>

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-03-07 11:20:01 -08:00
pythongosssss
5bb742ac3a feat/fix: App mode QA fixes (#9530)
## Summary

Additional updates for app mode

## Changes

- **What**:
- Reposition toolbar button in app mode so it doesnt jump between modes
- Move node name to under title on input/output selection
- Change delete asset button text
- Change exit builder button icon

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9530-feat-fix-App-mode-QA-fixes-31c6d73d365081738b5cc5beaf2cbd41)
by [Unito](https://www.unito.io)
2026-03-07 18:54:13 +00:00
Kelly Yang
ca2d61f393 use getLoad3dAsync (#9520)
## Summary

Applying `useLoad3dService().getLoad3dAsync` to fix the broken 3D viewer
scene.

Fix https://github.com/Comfy-Org/ComfyUI_frontend/issues/9495

## Screenshots 

before

<img width="3456" height="2168" alt="665d1b83bf8b23a9ddde1411a34c8df9"
src="https://github.com/user-attachments/assets/6cb190f4-ef13-4fd3-a0c5-2360f056da55"
/>


after

<img width="5120" height="2638" alt="image"
src="https://github.com/user-attachments/assets/154b1a98-bd71-41e2-839d-f0f1f7e5e72e"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9520-use-getLoad3dAsync-31c6d73d365081cf8c24cd89b545ccb4)
by [Unito](https://www.unito.io)
2026-03-07 09:22:35 -08:00
Benjamin Lu
750a2d23e0 chore: standardize on Node 24 (#9521)
## Summary

Standardize the repo's Node contract on 24 while centralizing workflow
resolution through `.nvmrc` so local setup, CI, and package metadata
stay aligned from one version file.

## Changes

- **What**: Add `package.json` `engines.node = 24.x`, switch every
`actions/setup-node` workflow in the repo to `node-version-file:
'.nvmrc'`, and update contributor and Playwright docs to point to
`.nvmrc` as the Node source of truth.

## Review Focus

The workflow behavior should be unchanged apart from sourcing the Node
version from `.nvmrc` instead of repeating literals like `20`, `22`,
`24.x`, or `lts/*`. GitHub's formatter also moved the new `engines`
block to the package metadata section near the end of `package.json`.

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-03-07 09:06:10 +00:00
Comfy Org PR Bot
6d90bf3537 1.42.0 (#9522)
Minor version increment to 1.42.0

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9522-1-42-0-31c6d73d3650816faa33df8b4dde26fd)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-03-06 23:11:18 -08:00
Benjamin Lu
1ada6dbfc6 fix: align run controls with queue modal design (#9134)
## Summary
- move queue batch controls to the left of the run button
- align run control styling to the Figma queue modal spec using PrimeVue
PT/Tailwind (secondary background on batch + dropdown, primary run
button)
- normalize control heights to match actionbar buttons and tighten
dropdown hit area
- update run typography/spacing and replace all three chevrons (dropdown
+ batch up/down) with the requested SVG

Design:
https://www.figma.com/design/LVilZgHGk5RwWOkVN6yCEK/Queue-Progress-Modal?node-id=3845-23904&m=dev

<img width="303" height="122" alt="image"
src="https://github.com/user-attachments/assets/4ed80ee7-3ceb-4512-96ce-f55ec6da835e"
/>


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9134-fix-align-run-controls-with-queue-modal-design-3106d73d36508160afcedbcfe4b98291)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-03-06 20:10:59 -08:00
Hunter
f02adf84eb feat: dispatch cloud build when preview label is added to PR (#9518)
## Summary

Dispatch a `frontend-asset-build` event to the cloud repo when the
`preview` label is added to a PR, so cloud can build preview assets.

## Changes

- **What**: Extended `cloud-dispatch-build.yaml` to trigger on
`pull_request` `labeled` events filtered to the `preview` label. The
payload sends the PR head SHA and branch.

## Review Focus

- The `pull_request` trigger gives a read-only `GITHUB_TOKEN`, but the
dispatch step uses `CLOUD_DISPATCH_TOKEN` so this is fine.
- Fork PRs are blocked by the existing `github.repository` guard.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9518-feat-dispatch-cloud-build-when-preview-label-is-added-to-PR-31c6d73d365081a8aab6f585960977f6)
by [Unito](https://www.unito.io)
2026-03-07 03:57:08 +00:00
pythongosssss
1058b7d12d feat/fix: App mode QA feedback 2 (#9511)
## Summary

Additional fixes and updates based on testing

## Changes

- **What**: 
- add warning to welcome screen & when sharing an app that has had all
outputs removed
- fix target workflow when changing mode via tab right click menu
- change build app text to be conditional "edit" vs "build" depending on
if an app is already defined
- update empty apps sidebar tab button text to make it clearer
- remove templates button from app mode (we will reintroduce this once
we have app templates)
- add "exit to graph" after applying default mode of node graph
- update cancel button to remove item from queue if it hasn't started
yet
- improve scoping of jobs/outputs to the current workflow [not perfect
but should be much improved]
- close sidebar tabs on entering app mode
- change tooltip to be under the workflow menu rather than covering the
button

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9511-feat-fix-App-mode-QA-feedback-2-31b6d73d365081d59bbbc13111100d46)
by [Unito](https://www.unito.io)
2026-03-06 18:57:03 -08:00
jaeone94
8bfd93963f [style] Update error/subgraph node footer design with layered overlay approach (#9360)
## Summary

Refactors the error and subgraph node footer UI by extracting a
dedicated `NodeFooter` component and replacing the CSS `outline`
approach with a layered border overlay for selection/executing state
indicators.

## Changes

- **What**: Extracted `NodeFooter.vue` from `LGraphNode.vue` to
encapsulate the footer tab logic (subgraph enter, error, advanced
inputs). Replaced CSS `outline` with an absolutely-positioned border
overlay div for selection and executing state. Added a separate root
border overlay div for the node body border. Removed unused
`isTransparent` function from `colorUtil.ts`.
- **Dependencies**: None

## Review Focus

- The layered overlay approach (`absolute -inset-[3px] border-3`) for
selection/executing outlines vs the previous `outline-3` approach —
ensures the outline renders outside the node bounds correctly including
the footer area
- `NodeFooter` handles 4 cases: subgraph+error (dual tabs), error only,
subgraph only, advanced inputs — verify edge cases render correctly
- Resize handle bottom offset adjustments for nodes with footers
(`hasFooter`)

## Screenshots
<img width="1142" height="603" alt="image"
src="https://github.com/user-attachments/assets/e0d401f0-8516-4f5f-ab77-48a79530f4bd"
/>
<img width="1175" height="577" alt="image"
src="https://github.com/user-attachments/assets/bcf08fff-728a-491c-add9-5b96d2f3bfce"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9360-style-Update-error-subgraph-node-footer-design-with-layered-overlay-approach-3186d73d365081b2ac31f166f4d1944a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-03-06 17:51:08 -08:00
Benjamin Lu
3366079f59 test: disable missing model warnings in browser tests (#9513)
Disable missing model warnings in browser tests by default.

Browser tests run without model files on disk, so workflows that embed
model metadata can render differently in CI than the test actually
intends to cover. The viewport screenshot golden had started depending
on the missing-model popup even though the test is only about restoring
an offscreen viewport.

Set `Comfy.Workflow.ShowMissingModelsWarning` to `false` in the shared
Playwright fixture, keep the missing-model dialog coverage by explicitly
enabling the setting in the dialog tests, and update the viewport
screenshot expectation to the no-popup rendering.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9513-test-disable-missing-model-warnings-in-browser-tests-31b6d73d365081d1908bfe11ec0c3bc2)
by [Unito](https://www.unito.io)
2026-03-06 17:37:50 -08:00
Johnpaul Chiwetelu
c4dabb8f98 refactor: extract input widget resolution from SubgraphNode configure (#9383)
## Summary

Extract the inner link-resolution loop from
`_internalConfigureAfterSlots` into a private `_resolveInputWidget`
method to reduce cognitive complexity below the sonarjs threshold of 15.

## Changes

- **What**: Extract nested loop body (lines 654-689) into
`_resolveInputWidget` private method in `SubgraphNode.ts`
- Pure refactoring with no behavioral changes

## Review Focus

Straightforward extract-method refactoring. The new method contains the
exact same logic that was previously inline.

Fixes #9297

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9383-refactor-extract-input-widget-resolution-from-SubgraphNode-configure-3196d73d365081ba9124cfd0d312fcb0)
by [Unito](https://www.unito.io)
2026-03-06 23:24:49 +01:00
Alexander Brown
0b73285ca1 fix: extract and harden subgraph node ID deduplication (#9510)
## Summary

Extract and harden subgraph node ID deduplication to prevent widget
store key collisions when multiple subgraph copies share identical node
IDs.

## Changes

- **What**: Extract `deduplicateSubgraphNodeIds` from `LGraph.ts` into
`utils/subgraphDeduplication.ts`, decomposed into focused helpers
(`remapNodeIds`, `findNextAvailableId`, `patchSerialisedLinks`,
`patchPromotedWidgets`, `patchProxyWidgets`). Clone inputs internally so
caller data is never mutated. Add safety limit on ID search to prevent
unbounded loops. Add `console.warn` on remapped IDs matching existing
`ensureGlobalIdUniqueness` behavior. Add test fixture and 5 behavioral
tests covering ID remapping, link patching, promoted widget patching,
proxyWidget patching, and no-op when IDs are unique.

## Review Focus

- The cloning strategy in `deduplicateSubgraphNodeIds` — it
`structuredClone`s subgraphs and rootNodes, returning the clones. The
caller uses `effectiveNodesData` to thread the patched root nodes
through to node creation.
- The `MAX_NODE_ID` safety limit (100M) — is this a reasonable ceiling?

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9510-fix-extract-and-harden-subgraph-node-ID-deduplication-31b6d73d365081f48c7de75e2bfc48b3)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-03-06 21:56:56 +00:00
AustinMroz
7a01be388f More app fixes (#9432)
- Increased the z-index on app mode outputs so that they display above a
zoomed image
- The "view job" button on the job queued toast in mobile app mode will
take you to outputs instead of assets
- Image previews now have a minimum zoom of ~20% and a maximum zoom of
~50x
- The enter panel in linear mode now has a minimum size of ~1/5th screen
size
- In arrange mode, dragging to rearrange inputs will no longer cause a
horizontal scrollbar to appear.
- Videos will now display the first frame instead of a generic video
icon
- Muted/Bypassed nodes can no longer be selected as inputs/outputs, or
be displayed when in app mode.
- Linked input can no longer be selected or displayed
- Adds a share workflow button in app mode and wires up the existing
context menu

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9432-More-app-fixes-31a6d73d365081509cd0ea74bfdc9b95)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-03-06 13:41:52 -08:00
pythongosssss
3ddff9f7b6 feat: Update workflow menu to allow quick toggling modes (#9436)
## Summary

Adds a quick toggle mode button to the workflow menu for users to easier
discover & change modes

## Changes

- **What**: 
- remove specific app mode rendering
- increase spacing around breadcrumbs menu
- add current mode text to menu
- add base button variant

## Screenshots (if applicable)

<img width="258" height="137" alt="image"
src="https://github.com/user-attachments/assets/2ed7b276-c52c-44cd-b107-399f769574af"
/>
<img width="233" height="172" alt="image"
src="https://github.com/user-attachments/assets/2639d30c-2150-4434-a86b-732649c4b142"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9436-feat-Update-workflow-menu-to-allow-quick-toggling-modes-31a6d73d365081b589eee0e03cd6f1de)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-03-06 20:03:02 +00:00
pythongosssss
4ff14b5eb9 feat/fix: App mode QA updates (#9439)
## Summary

Various fixes from app mode QA

## Changes

- **What**: 
- fix: prevent inserting nodes from workflow/apps sidebar tabs
- fix: hide json extension in workflow tab
- fix: hide apps nav button in apps tab when already in apps mode
- fix: center text on arrange page
- fix: prevent IoItems from "jumping" due to stale transform after drag
and drop op
- fix: refactor side panels and add custom stable pixel based sizing
- fix: make outputs/inputs lists in app builder scrollable
- fix: fix rerun not working correctly

- feat: add text to interrupt button
- feat: add enter app mode button to builder toolbar
- feat: add tooltip to download button on linear view
- feat: show last output of workflow in arrange tab if available
- feat: show download count in download all button, hide if only 1 asset
to download

## Review Focus

- Rerun - I am not sure why it was triggering widget actions, removing
it seemed like the correct fix
- useStablePrimeVueSplitter - this is a workaround for the fact it uses
percent sizing, I also tried switching to reka-ui splitters, but they
also only support % sizing in our version [pixel based looks to have
been added in a newer version, will log an issue to upgrade & replace
splitters with this]


## Screenshots (if applicable)

<img width="1314" height="1129" alt="image"
src="https://github.com/user-attachments/assets/c430f9d6-7c29-4853-803e-5b6fe7086fca"
/>
<img width="511" height="283" alt="image"
src="https://github.com/user-attachments/assets/b7e594d4-70a1-41e3-8ba1-78512f2a5c8b"
/>
<img width="254" height="232" alt="image"
src="https://github.com/user-attachments/assets/1d146399-39ea-4b0e-928c-340b74957535"
/>
<img width="487" height="198" alt="image"
src="https://github.com/user-attachments/assets/e2ba7f5d-8ff5-47f4-9526-61ebb99514b8"
/>
<img width="378" height="647" alt="image"
src="https://github.com/user-attachments/assets/a47a3054-9320-4327-bdc0-b0a16e19f83d"
/>
<img width="1016" height="476" alt="image"
src="https://github.com/user-attachments/assets/479ae50e-d380-4d56-a5c9-5df142b14ed0"
/>


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9439-feat-fix-App-mode-QA-updates-31a6d73d365081b38337d63207b88817)
by [Unito](https://www.unito.io)
2026-03-06 20:02:19 +00:00
pythongosssss
bae1081a08 fix: update loadWorkflowInMedia test to only assert upload request URL (#9488)
## Summary

Fixes flakey test to only assert that the upload request is made with
the correct URL

## Changes

- **What**
- Replace waitForResponse with waitForRequest for the no_workflow.webp
upload test to only assert the request is initiated with the correct URL
- Move request listener setup before the drag-drop action to avoid race
conditions
- Remove screenshot assertion for the upload case since the upload may
not complete before the screenshot is taken

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9488-fix-update-loadWorkflowInMedia-test-to-only-assert-upload-request-URL-31b6d73d365081f69a9aeb1095da7d60)
by [Unito](https://www.unito.io)
2026-03-06 11:38:53 -08:00
AustinMroz
55b8236c8d Fix localization on share and hide entry (#9395)
A placeholder share entry was added in #9368, but the localization for
this share label was then removed in #9361.

This localization is re-added in a location that is less likely to be
overwritten and the menu item is set to hidden. I'll manually connect it
to the workflow sharing feature flag in a followup PR after that has
been merged.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9395-Fix-localization-on-share-and-hide-entry-3196d73d36508146a343f625a5327bdd)
by [Unito](https://www.unito.io)
2026-03-06 09:35:18 -08:00
Johnpaul Chiwetelu
5e17bbbf85 feat: expose litegraph internal keybindings (#9459)
## Summary

Migrate hardcoded litegraph canvas keybindings (Ctrl+A/C/V, Delete,
Backspace) into the customizable keybinding system so users can remap
them via Settings > Keybindings.

## Changes

- **What**: Register Ctrl+A (SelectAll), Ctrl+C (CopySelected), Ctrl+V
(PasteFromClipboard), Ctrl+Shift+V (PasteFromClipboardWithConnect),
Delete/Backspace (DeleteSelectedItems) as core keybindings in
`defaults.ts`. Add new `PasteFromClipboardWithConnect` command. Remove
hardcoded handling from litegraph `processKey()`, the `app.ts` Ctrl+C/V
monkey-patch, and the `keybindingService` canvas forwarding logic.

Fixes #1082
Fixes #2015

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9459-feat-expose-litegraph-internal-keybindings-31b6d73d3650819a8499fd96c8a6678f)
by [Unito](https://www.unito.io)
2026-03-06 18:30:35 +01:00
Johnpaul Chiwetelu
7cb07f9b2d fix: standardize i18n pluralization to two-part English format (#9384)
## Summary

Standardize 5 English pluralization strings from incorrect 3-part format
to proper 2-part `"singular | plural"` format.

## Changes

- **What**: Convert `nodesCount`, `asset`, `errorCount`,
`downloadsFailed`, and `exportFailed` i18n keys from redundant 3-part
pluralization (zero/one/many) to standard 2-part English format
(singular/plural)

## Review Focus

The 3-part format (`a | b | a`) was redundant for English since the
first and third parts were identical. vue-i18n only needs 2 parts for
English pluralization.

Fixes #9277

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9384-fix-standardize-i18n-pluralization-to-two-part-English-format-3196d73d365081cf97c4e7cfa310ce8e)
by [Unito](https://www.unito.io)
2026-03-06 14:53:13 +01:00
Christian Byrne
a0abe3e36f chore: add deprecation warning for legacy queue/history menu (#9460)
## Summary

Add console.warn deprecation notice when the legacy ComfyList
queue/history menu is instantiated.

## Changes

- **What**: Log a deprecation warning in the `ComfyList` constructor
telling users the legacy menu is deprecated, may break, and won't
receive support. Includes instructions to switch via Settings → "Use new
menu" → "Top".

## Review Focus

Wording of the user-facing console warning.

Fixes #8100 (Phase 1)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9460-chore-add-deprecation-warning-for-legacy-queue-history-menu-31b6d73d365081ffa041cad33e8cd9a7)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-03-06 00:32:47 -08:00
Jin Yi
96fd25de5c feat: add Logo C fill and Comfy wave loading indicator components (#9433)
## Summary

Add SVG-based brand loading indicators (LogoCFillLoader,
LogoComfyWaveLoader) and use the wave loader as the app loading screen.

## Changes

- **What**: New `LogoCFillLoader` (bottom-to-top fill, plays once) and
`LogoComfyWaveLoader` (wave water-fill animation) components with
`size`, `color`, `bordered`, and `disableAnimation` props. Move all
loaders from `components/common/` to `components/loader/`. Use
`LogoComfyWaveLoader` in `App.vue` and `WorkspaceAuthGate.vue`. Render
loader above BlockUI overlay (z-1200) to prevent dim wash-out.
- **Dependencies**: None

## Review Focus

- SVG mask-based animation approach using `currentColor` for flexible
theming
- z-index layering: loader at z-1200 renders above PrimeVue BlockUI's
z-1100 modal overlay
- `disableAnimation` prop used in WorkspaceAuthGate to show static logo
outline during auth loading

## Screenshots (if applicable)

[loading_record.webm](https://github.com/user-attachments/assets/b34f7296-9904-4a42-9273-a7d5fda49d15)

Storybook stories added for both components under `Components/Loader/`.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9433-feat-add-Logo-C-fill-and-Comfy-wave-loading-indicator-components-31a6d73d3650811cacfdcf867b1f835f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-03-06 00:32:20 -08:00
Christian Byrne
e2cb3560cc test: fix flaky no_workflow.webp screenshot test (#9458)
## Summary

Fix flaky `no_workflow.webp` screenshot test by waiting for async upload
and `/view` response before asserting.

## Changes

- **What**: In `loadWorkflowInMedia.spec.ts`, added `waitForUpload:
true` for `no_workflow.webp` and a `waitForResponse` call for the
`/view` endpoint. This ensures the error toast (from the 500 response)
is consistently visible before the screenshot assertion.

## Review Focus

The fix is scoped to `no_workflow.webp` only (via a `filesWithUpload`
Set) since it's the only test file that triggers an upload + `/view`
call. Other media files embed workflows and don't hit this path.

Fixes #9450

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9458-test-fix-flaky-no_workflow-webp-screenshot-test-31b6d73d365081b88deaee91769baec1)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-03-05 18:27:43 -08:00
Alexander Brown
9ae85068eb feat: Transparent background for the Image and Video Previews (#9455)
## Summary

Less jarring appearance, especially with different aspect ratios or
Alpha channels.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9455-feat-Transparent-background-for-the-Image-and-Video-Previews-31b6d73d3650819eaa82def10e66da21)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-03-05 18:06:53 -08:00
Comfy Org PR Bot
23bb5f2afa 1.41.13 (#9452)
Patch version increment to 1.41.13

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9452-1-41-13-31b6d73d3650819db118e6455c555bce)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-03-05 18:05:01 -08:00
Christian Byrne
ef4e4a69d5 fix: enable enforce-consistent-class-order tailwind lint rule (#9428)
## Summary

Enable `better-tailwindcss/enforce-consistent-class-order` lint rule and
auto-fix all 1027 violations across 263 files. Stacked on #9427.

## Changes

- **What**: Sort Tailwind classes into consistent order via `eslint
--fix`
- Enable `enforce-consistent-class-order` as `'error'` in eslint config
- Purely cosmetic reordering — no behavioral or visual changes

## Review Focus

Mechanical auto-fix PR — all changes are class reordering only. This is
the largest diff but lowest risk since it changes no class names, only
their order.

**Stack:** #9417#9427 → **this PR**

Fixes #9300 (partial — 3 of 3 rules)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9428-fix-enable-enforce-consistent-class-order-tailwind-lint-rule-31a6d73d3650811c9065f5178ba3e724)
by [Unito](https://www.unito.io)
2026-03-05 17:24:34 -08:00
Dante
60267fc64c refactor: unify image classification and fix cloud preview param handling (#9408)
## Summary

Addresses review feedback from PR #9298 and resolves the divergence
between `ResultItemImpl.isImage` and `appendCloudResParam`'s image
classification.

### Changes

- **Unify suffix-based classification**: Replace narrow
`isImageBySuffix` (gif/webp only), `isVideoBySuffix` (webm/mp4), and
`isAudioBySuffix` with `getMediaTypeFromFilename()` from
shared-frontend-utils, using the same `IMAGE_EXTENSIONS` set (png, jpg,
jpeg, gif, webp, bmp, avif, tif, tiff) that `appendCloudResParam` uses
- **imageCompare.ts**: Pass `record.filename` to `appendCloudResParam`
(was called without filename, bypassing image-extension guard)
- **imagePreviewStore.ts**: Use per-image `image.filename` instead of
first image's filename for all images in batch
- **LinearControls.vue**: Use `resultItem.filename` (already a string)
instead of `String(filename)` which converts undefined to `"undefined"`

### Related review comments

- [imageCompare.ts — missing
filename](https://github.com/Comfy-Org/ComfyUI_frontend/pull/9298#discussion_r2886137498)
- [imagePreviewStore.ts — per-image
filename](https://github.com/Comfy-Org/ComfyUI_frontend/pull/9298#discussion_r2886138718)
- [LinearControls.vue —
String(filename)](https://github.com/Comfy-Org/ComfyUI_frontend/pull/9298#discussion_r2886140159)
- [queueStore.ts — diverging image
classification](https://github.com/Comfy-Org/ComfyUI_frontend/pull/9298#discussion_r2886142886)

## Test plan

- [x] 66 unit tests pass (queueStore + cloudPreviewUtil)
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes

- Fixes #9386

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-05 17:20:18 -08:00
Christian Byrne
47f2b63628 fix: prevent persistent loading state when cycling batches with identical URLs (#8999)
## Summary

Fix persistent loading/skeleton state when cycling through batch images
or videos that share the same URL (common on Cloud).

## Changes

- **What**: In `setCurrentIndex()` for both `ImagePreview.vue` and
`VideoPreview.vue`, only start the loader when the target URL differs
from the current URL. When batch items share the same URL, the browser
doesn't fire a new `load`/`loadeddata` event since `src` didn't change,
so the loader was never dismissed.
- Also fixes `VideoPreview.vue` navigation dots using hardcoded
`bg-white` instead of semantic `bg-base-foreground` tokens.

## Review Focus

This bug has regressed 3+ times (PRs #6521, #7094, #8366). The
regression tests specifically target the root cause — cycling through
identical URLs — to prevent future reintroduction.

Fixes
https://www.notion.so/comfy-org/Bug-Cycling-through-image-batches-results-in-persistent-loading-state-on-Cloud-30c6d73d3650816e9738d5dbea52c47d

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8999-fix-prevent-persistent-loading-state-when-cycling-batches-with-identical-URLs-30d6d73d36508180831edbaf8ad8ad48)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com>
2026-03-05 17:14:40 -08:00
Christian Byrne
1221756e05 fix: enable enforce-canonical-classes tailwind lint rule (#9427)
## Summary

Enable `better-tailwindcss/enforce-canonical-classes` lint rule and
auto-fix all 611 violations across 173 files. Stacked on #9417.

## Changes

- **What**: Simplify Tailwind classes to canonical forms via `eslint
--fix`:
  - `h-X w-X` → `size-X`
  - `overflow-x-hidden overflow-y-hidden` → `overflow-hidden`
  - and other canonical simplifications
- Enable `enforce-canonical-classes` as `'error'` in eslint config

## Review Focus

Mechanical auto-fix PR — all changes produced by `eslint --fix`. No
visual or behavioral changes; canonical forms are functionally
identical.

**Stack:** #9417 → **this PR** → PR 3 (class order)

Fixes #9300 (partial — 2 of 3 rules)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9427-fix-enable-enforce-canonical-classes-tailwind-lint-rule-31a6d73d365081a49340d7d4640ede45)
by [Unito](https://www.unito.io)
2026-03-05 17:07:46 -08:00
Alexander Brown
1bac5d9bdd feat: workflow sharing and ComfyHub publish flow (#8951)
## Summary

Add workflow sharing by URL and a multi-step ComfyHub publish wizard,
gated by feature flags and an optional profile gate.

## Changes

- **What**: Share dialog with URL generation and asset warnings;
ComfyHub publish wizard (Describe → Examples → Finish) with thumbnail
upload and tags; profile gate flow; shared workflow URL loader with
confirmation dialog
- **Dependencies**: None (new `sharing/` module under
`src/platform/workflow/`)

## Review Focus

- Three new feature flags: `workflow_sharing_enabled`,
`comfyhub_upload_enabled`, `comfyhub_profile_gate_enabled`
- Share service API contract and stale-share detection
(`workflowShareService.ts`)
- Publish wizard and profile gate state management
- Shared workflow URL loading and query-param preservation

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8951-feat-share-workflow-by-URL-30b6d73d3650813ebbfafdad775bfb33)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-03-05 16:33:06 -08:00
Robin Huang
6c2680f0ba feat: Add PostHog telemetry provider (#9409)
Add PostHog as a telemetry provider for cloud builds so custom events
can be correlated with session recordings. Follows the same pattern as
MixpanelTelemetryProvider with dynamic import, event queuing, and
disabled events from remote config. Tree-shaken away in OSS builds.

The posthog-js package uses Apache-2.0 (verified from its LICENSE file)
but declares it as "SEE LICENSE IN LICENSE" in package.json, which
  the license checker can't parse.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9409-feat-Add-PostHog-telemetry-provider-31a6d73d3650818b8e86c772c6551099)
by [Unito](https://www.unito.io)
2026-03-05 16:19:35 -08:00
Dante
5843dced84 feat: add Storybook stories for WidgetInputText and WidgetTextarea (#9398)
Add Storybook stories for WidgetInputText and WidgetTextarea, aligned
with the Figma Design System spec.

Task: COM-15821

## Summary

Add comprehensive Storybook stories for text widget components and
implement missing Figma design system variants for WidgetInputText.

## Changes

- **WidgetInputText component enhancements**:
- Add `size` prop (`medium` | `large`) matching Figma size variants
(32px / 40px)
- Add `invalid` prop with destructive border style per Figma Invalid
state
- Add `loading` prop showing spinning loader icon per Figma Status state
  - Add hover background (`bg-component-node-widget-background-hovered`)
  - Fix `readonly` not being applied from `widget.options.read_only`
- **WidgetTextarea component fixes**:
  - Show copy button on hover for all states (not just read-only)
  - Apply `text-component-node-foreground` token to copy icon
  - Add hover background to wrapper
- **Storybook stories**:
- WidgetInputText: Default, Disabled, Invalid, Status, WithPlaceholder,
WithLabel stories
- WidgetTextarea: Default, Disabled, HiddenLabel, WithPlaceholder
stories
  - Interactive controls for size, readOnly, disabled, invalid, loading

## Review Focus

- Figma alignment: size/invalid/loading/status variants for
WidgetInputText
- Copy icon color token (`text-component-node-foreground`) for
light/dark theme support
- `layoutWidget` computed pattern to merge `borderStyle` with invalid
state

## Screenshots (if applicable)

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 08:42:53 +09:00
Christian Byrne
b2915ed42a refactor: extract shared createMockWidget factory for widget component tests (#9423)
## Summary

Extract a shared `createMockWidget` test factory to eliminate duplicated
`SimplifiedWidget` object construction across 13 widget component test
files.

## Changes

- **What**: Add `widgetTestUtils.ts` with a generic
`createMockWidget<T>` factory providing sensible defaults (`name`,
`type`, `options`). Refactor 13 test files to delegate to it via thin
local wrappers that supply component-specific defaults (combo values,
slider ranges, etc.).

## Review Focus

- The shared factory only covers `SimplifiedWidget`-based tests. Three
files using different base types (`NodeWidgets.test.ts`,
`useRemoteWidget.test.ts`, `useComboWidget.test.ts`) are intentionally
excluded.
- `mountComponent` helpers remain per-file since plugin/component setups
vary too much to share.

Fixes #5554

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9423-refactor-extract-shared-createMockWidget-factory-for-widget-component-tests-31a6d73d36508159b65ee0e7b49212c3)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
2026-03-05 15:39:41 -08:00
Christian Byrne
df69d6b5d4 feat: add Amp code review checks (#9445)
## Summary

Add 22 automated code review check definitions and 1 strict ESLint
config to `.agents/checks/` for Amp-powered code review.

## Changes

- **What**: 23 files in `.agents/checks/` covering accessibility, API
contracts, architecture, bug patterns, CodeRabbit integration,
complexity, DDD structure, dependency/secrets scanning, doc freshness,
DX/readability, ecosystem compatibility, error handling, import graph,
memory leaks, pattern compliance, performance, regression risk,
security, SAST, SonarJS linting, test quality, and Vue patterns. Each
check includes YAML frontmatter (name, description, severity-default,
tools) and repo-specific guidance tailored to ComfyUI_frontend
conventions.

## Review Focus

- Check definitions are config-only (no runtime code changes)
- Checks reference repo-specific patterns (e.g., `useErrorHandling`
composable, `useToastStore`, `es-toolkit`, Tailwind 4, Vue Composition
API)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9445-feat-add-Amp-code-review-checks-31a6d73d3650817a8466fe2f4440a350)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-03-05 15:29:30 -08:00
Christian Byrne
bab1d34634 fix: stabilize flaky screenshot tests that rely on search box timings (#9426)
## Summary

Fixes flaky screenshot test that was identified during the PR #9400
snapshot update, where baselines regenerated without any code changes
affecting them.

**Root cause:** `fillAndSelectFirstNode('KSampler')` selected `nth(0)`
from the autocomplete dropdown, but search result ordering is
non-deterministic when the search box opens via link release with filter
chips. Sometimes 'Preview Image' appeared as the first result instead of
'KSampler', causing a completely different node to be added.

**Fix:** Added an `exact: true` option to `fillAndSelectFirstNode` that
uses an `aria-label` selector to click the specific matching result
instead of blindly selecting the first item.

- Fixes #4658

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9426-fix-stabilize-flaky-screenshot-tests-for-search-results-and-image-preview-31a6d73d365081598167ce285416995c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-03-05 15:28:22 -08:00
Christian Byrne
e7588c33e1 refactor: rename imagePreviewStore to nodeOutputStore (#9416)
## Summary

Rename `imagePreviewStore.ts` → `nodeOutputStore.ts` to match the store
it houses (`useNodeOutputStore`, Pinia ID `nodeOutput`).

## Changes

- **What**: Rename file + test file, update all 21 import paths, mock
paths, and describe labels
- **Breaking**: None — exported symbol (`useNodeOutputStore`) and Pinia
store ID (`nodeOutput`) are unchanged

## Custom Node Ecosystem Audit

Searched the ComfyUI custom node ecosystem for `imagePreviewStore` and
`useNodeOutputStore`:
- **Not part of the public API** — neither filename nor export appear in
`comfyui_frontend_package` or `vite.types.config.mts`
- **1 external repo found:** `wallen0322/ComfyUI-AE-Animation` —
contains a full fork of the frontend source tree; it copies the file
internally and does not import from the published package. **No
breakage.**
- **No custom nodes import this store via the extension API.** This is a
safe internal-only rename.

## Review Focus

Pure mechanical rename — no logic changes. Verify no stale
`imagePreviewStore` references remain.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9416-refactor-rename-imagePreviewStore-to-nodeOutputStore-31a6d73d3650816086c5e62959861ddb)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-03-05 13:52:50 -08:00
Yourz
5c7851a727 fix: clean up essential node card and tab list styling (#9438)
## Summary

Remove unnecessary styling from EssentialNodeCard and
NodeLibrarySidebarTabV2 tab list.

## Changes

- **What**:
  - Remove `justify-between` from TabsList in NodeLibrarySidebarTabV2;
  - remove `border border-component-node-border` from EssentialNodeCard.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9438-fix-clean-up-essential-node-card-and-tab-list-styling-31a6d73d3650817facf1c648568ac6bd)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-03-05 12:28:25 -08:00
Alexander Brown
63399d6ec1 test: regenerate screenshot expectations (#9442)
The pixels fade, the tests turn red,
Old screenshots haunt us from the dead.
A branch is born, a label placed,
And golden images are replaced.

The shards spin up, four workers strong,
To right what rendering got wrong.
When CI turns green, the deed is done—
New expectations, freshly won.

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-03-05 19:59:44 +00:00
jaeone94
25d8384716 fix(ErrorNodeCard): show error message body in compact (single-node) mode (#9437)
## Summary
Remove the `!compact` condition that was preventing the error message
body from being displayed in compact (single-node error) mode.

## Changes
- **What**: Removed `&& !compact` guard from `v-if="error.message &&
!compact"` in `ErrorNodeCard.vue` so the error message body is always
shown when present, regardless of display mode

<img width="1209" height="605" alt="image"
src="https://github.com/user-attachments/assets/b720c9fa-2789-441d-aa4b-7850fe370436"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9437-fix-ErrorNodeCard-show-error-message-body-in-compact-single-node-mode-31a6d73d3650814683f7e6983534a4ad)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-03-05 05:03:59 -08:00
Christian Byrne
c817563cf0 feat: GLSLPreviewEngine + GLSL utility functions (#9200)
## Summary

Standalone WebGL2 rendering engine for client-side GLSL shader preview,
with utility functions that mirror the backend `nodes_glsl.py` detection
logic.

## Changes

- **What**: New `GLSLPreviewEngine` class (OffscreenCanvas + WebGL2),
`glslUtils.ts` (detectOutputCount, detectPassCount,
hasVersionDirective), and unit tests
- **GLSLPreviewEngine**: Fullscreen triangle via `gl_VertexID` (no VBO),
ping-pong FBOs for multi-pass rendering, MRT via `gl.drawBuffers()`,
blob output via `canvas.convertToBlob()`
- **glslUtils**: Pure functions ported from backend Python to
TypeScript, regex-based detection matching `_detect_output_count()` and
`_detect_pass_count()`

## Review Focus

- WebGL2 resource lifecycle (context loss, texture cleanup, FBO teardown
in `dispose()`)
- Ping-pong FBO logic for multi-pass shaders
- Engine tests are WebGL2-gated (`describe.skip` in happy-dom) — they
run in real browser environments

## Stacked PR

PR 2 of 3. Stacked on #9198 (fix: GLSLShader preview promotion).
PR 3: `feat/glsl-live-preview` (composable + LGraphNode.vue integration)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9200-feat-GLSLPreviewEngine-GLSL-utility-functions-3126d73d3650812fadc6df4a26387d0e)
by [Unito](https://www.unito.io)
2026-03-05 03:04:33 -08:00
pythongosssss
5376b7ed1e feat: App mode empty graph handling (#9393)
## Summary

Adds handling for entering app mode with an empty graph prompting the
user to load a template as a starting point

## Changes

- **What**: 
- app mode handle empty workflows, disable builder button, show
different message
- fix fitView when switching from app mode to graph

## Review Focus

Moving the fitView since the canvas is hidden in app mode until after
the workflow is loaded and the mode has been switched back to graph, I
don't see how this could cause any issues but worth a closer eye

## Screenshots (if applicable)

<img width="1057" height="916" alt="image"
src="https://github.com/user-attachments/assets/2ffe2b6d-9ce1-4218-828a-b7bc336c365a"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9393-feat-App-mode-empty-graph-handling-3196d73d3650812cab0ce878109ed5c9)
by [Unito](https://www.unito.io)
2026-03-05 02:27:05 -08:00
pythongosssss
e0089d93d0 feat: App mode progress updates (#9375)
## Summary

- move progress bar below preview thumbnail instead of overlaying it
- add interactive pending placeholder
- fix: scope in-progress items to active workflow in output history

## Screenshots (if applicable)

<img width="209" height="86" alt="image"
src="https://github.com/user-attachments/assets/46590fdc-3df9-4a40-8492-a54e63e3f44c"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9375-feat-App-mode-progress-updates-3196d73d3650817ea891c9e744893846)
by [Unito](https://www.unito.io)
2026-03-05 02:26:03 -08:00
jaeone94
5fc82823ef feat: add manager enable hint for OSS local users (#9377)
## Summary

When ComfyUI Manager is disabled, OSS local users see a hint in the
Missing Nodes panel explaining how to install and enable it.

## Changes

- **What**: Added an inline hint in `MissingNodeCard` that renders when
the user is on OSS (non-Cloud) and Manager is not active
(`showInfoButton` is false). The hint shows the pip install command and
the `--enable-manager` startup flag, formatted as inline `<code>`
snippets via `i18n-t` interpolation.

## Review Focus

- The `showManagerHint` computed is intentionally simple: `!isCloud &&
!props.showInfoButton`. `showInfoButton` is the existing signal for
whether Manager is available/enabled.
- Styling uses existing semantic tokens (`bg-comfy-menu-bg`,
`text-comfy-input-foreground`) to match the rest of the panel.

## Screenshot
<img width="642" height="452" alt="image"
src="https://github.com/user-attachments/assets/d08280d3-b4a0-4613-b092-1baa49f0b091"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9377-feat-add-manager-enable-hint-for-OSS-local-users-3196d73d365081a19037c8f55f11d1eb)
by [Unito](https://www.unito.io)
2026-03-05 00:20:25 -08:00
Jin Yi
b521e75c6a [feat] Add model metadata fetching with loading skeleton and gated repo support (#9415)
## Summary
- Fetch file sizes via HEAD requests (HuggingFace) and Civitai API with
caching and request deduplication
- Show skeleton loader while metadata is loading
- Display "Accept terms" link for gated HuggingFace models instead of
download button

## Changes
- **`missingModelsUtils.ts`**: Add `fetchModelMetadata` with Civitai API
support, HuggingFace gated repo detection, in-memory cache, and inflight
request deduplication
- **`MissingModelsContent.vue`**: Add Skeleton loading state, gated
model "Accept terms" link, extract `showSkeleton` helper
- **`missingModelsUtils.test.ts`**: Tests for HEAD/Civitai fetching,
gated repo detection, caching, and deduplication
- **`main.json`**: Add `acceptTerms` i18n key

## Related Issues
https://github.com/Comfy-Org/ComfyUI_frontend/issues/9410
https://github.com/Comfy-Org/ComfyUI_frontend/issues/9412


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9415-feat-Add-model-metadata-fetching-with-loading-skeleton-and-gated-repo-support-31a6d73d36508127859efa0b3847505e)
by [Unito](https://www.unito.io)
2026-03-04 23:59:17 -08:00
Christian Byrne
493b1e42aa fix: enable no-deprecated-classes tailwind lint rule (#9417)
## Summary

Enable `better-tailwindcss/no-deprecated-classes` lint rule and auto-fix
all 103 violations across 65 files. First PR in a stacked series for
#9300.

## Changes

- **What**: Replace deprecated Tailwind v3 classes with v4 equivalents:
  - `rounded` → `rounded-sm` (85)
  - `flex-shrink-0` → `shrink-0` (16)
  - `flex-grow` → `grow` (2)
- Enable `no-deprecated-classes` as `'error'` in eslint config
- Update one test asserting on `'rounded'` class string

## Review Focus

Mechanical auto-fix PR — all changes produced by `eslint --fix`. No
visual or behavioral changes (Tailwind v4 aliases these classes
identically).

Fixes #9300 (partial — 1 of 3 rules)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9417-fix-enable-no-deprecated-classes-tailwind-lint-rule-31a6d73d3650819eaef4cf8ad84fb186)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-03-05 07:41:23 +00:00
Yourz
7cd11f0da5 fix: clean up unused icons and add LoadImage lucide icon override (#9359)
## Summary

Remove 46 unused snake_case SVG icons from design-system and use lucide
icon for LoadImage.

## Changes

- **What**: Remove unreferenced snake_case SVG files, replace LoadImage
custom icon with `lucide--image-up`, add `preview-image.svg` (renamed
from `image-preview.svg` to match `kebabCase('PreviewImage')`), extract
`ESSENTIALS_ICON_OVERRIDES` to `essentialsNodes.ts`
- Remove `load-image` from safelist in `style.css`

<img width="307" height="701" alt="image"
src="https://github.com/user-attachments/assets/de5e1bde-03eb-415e-ac76-f2e653a5eeb2"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9359-fix-clean-up-unused-icons-and-add-LoadImage-lucide-icon-override-3186d73d36508159be05ce6f4145be56)
by [Unito](https://www.unito.io)
2026-03-05 15:31:41 +08:00
Jin Yi
706060a2bf [refactor] Replace PrimeVue ProgressSpinner with Lucide loader icon (#9372)
## Summary
- Replace PrimeVue `ProgressSpinner` with Lucide `loader-circle` icon in
App.vue and WorkspaceAuthGate.vue
- Use white color for loading spinner for better visibility on dark
backgrounds
- Remove `primevue/progressspinner` imports and update related test

## Changes
- **App.vue**: Replace `ProgressSpinner` with
`icon-[lucide--loader-circle]`
- **WorkspaceAuthGate.vue**: Same replacement
- **WorkspaceAuthGate.test.ts**: Remove ProgressSpinner mock, use
`.animate-spin` selector

## Review Focus
- Visual consistency of white spinner on dark background during initial
load

<img width="1596" height="1189" alt="스크린샷 2026-03-04 오후 6 28 27"
src="https://github.com/user-attachments/assets/d703db74-4123-4328-912a-45ac45cf6eeb"
/>
<img width="1680" height="1304" alt="스크린샷 2026-03-04 오후 6 28 24"
src="https://github.com/user-attachments/assets/8026d10a-7e06-4f95-849c-bc891756823c"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9372-refactor-Replace-PrimeVue-ProgressSpinner-with-Lucide-loader-icon-3196d73d3650815bb1d1d4554f7f744e)
by [Unito](https://www.unito.io)
2026-03-05 16:23:22 +09:00
Christian Byrne
c2fc0c0f03 fix: make HoneyToast responsive on small screens (#9429)
## Summary

HoneyToast overflows on screens narrower than 400px because the expanded
content uses a fixed `max(400px, 40vw)` width.

## Changes

- **What**: On small screens (`<640px`), the outer container uses
`inset-x-4` gutters with `w-auto` instead of `w-min`, and the expanded
content uses `w-full` instead of the fixed width. At `sm:` breakpoint
and above, the original centered `w-min` / `w-[max(400px,40vw)]`
behavior is preserved.

## Review Focus

The fix is mobile-first: small screens get fluid width, `sm:` restores
the original fixed behavior. No changes needed in the three consumer
components (ModelImportProgressDialog, AssetExportProgressDialog,
ManagerProgressToast).

<!-- Fixes
https://www.notion.so/comfy-org/Bug-Fix-HoneyToast-width-styling-for-responsiveness-on-all-screen-sizes-3136d73d3650814b97e2cc943a98bedb
-->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9429-fix-make-HoneyToast-responsive-on-small-screens-31a6d73d36508103ba02dcef2e5ff33b)
by [Unito](https://www.unito.io)
2026-03-04 22:42:11 -08:00
Christian Byrne
6689a1b14e fix: split perf report workflow for fork PR support (#9382)
## Summary

Perf report workflow fails on fork PRs because `GITHUB_TOKEN` is
read-only for forks, causing "Resource not accessible by integration" on
the PR comment step.

## Changes

- **What**: Split `ci-perf-report.yaml` into a data-collection workflow
+ a `workflow_run`-triggered reporter (`pr-perf-report.yaml`), matching
the existing `ci-size-data`/`pr-size-report` pattern. Added fork PR
permissions guidance to `.github/AGENTS.md`.
- **ci-perf-report.yaml**: Removed the `report` job and `pull-requests:
write` permission. Added PR metadata (number + base branch) artifact
upload.
- **pr-perf-report.yaml** (new): Triggered by `workflow_run` on the perf
workflow. Downloads metrics + metadata artifacts, generates report,
posts PR comment with write permissions from the default-branch context.

## Review Focus

- The two-workflow split follows the same pattern as `ci-size-data.yaml`
→ `pr-size-report.yaml`, which already works for fork PRs.
- The `workflow_run` trigger runs in the base repo context per [GitHub
Security Lab
guidance](https://securitylab.github.com/resources/github-actions-preventing-pwn-requests/),
so it safely has write permissions even for fork PRs.
- AGENTS.md guidance documents this pattern to prevent recurrence.

Fixes the failure seen in
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/22684230751/job/65763595989?pr=9380

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9382-fix-split-perf-report-workflow-for-fork-PR-support-3196d73d365081b29b35ed354e7789e2)
by [Unito](https://www.unito.io)
2026-03-04 22:09:31 -08:00
Christian Byrne
6c20d64fa9 fix: deterministic DOM widget clip-path for flaky screenshot test (#9400)
## Summary

Fix deterministic DOM widget clip-path rendering to resolve the flaky
"Can drag node" screenshot test.

## Root Cause

`useDomClipping.updateClipPath()` schedules clip-path calculation in a
`requestAnimationFrame`, but `DomWidget.vue`'s watcher reads
`clippingStyle.value` synchronously before the RAF fires. The stale
clip-path gets baked into `style.value` and never updated when the RAF
completes, causing the textarea DOM widget to non-deterministically
render in front of or behind the canvas-drawn node selection border.

## Fix

- Extract `composeStyle()` function and add a dedicated watcher on
`clippingStyle` that recomposes the final inline style whenever the
RAF-deferred clip-path updates
- Add `enableDomClipping` to the main watcher dependency array so
toggling the clipping setting immediately recomposes the style
- Add `moveMouseToEmptyArea()` call in the test as a secondary
stabilizer against hover highlight non-determinism
- Delete stale snapshot so CI regenerates it with correct clip-path
behavior

- Fixes #4658

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-03-04 21:44:20 -08:00
jaeone94
f00a65ced0 feat: add collapse/expand all toggle to right panel tabs (#9333)
## Summary
Adds a collapse/expand all toggle button to all parameter and error tabs
in the right side panel, letting users quickly collapse or expand all
accordion sections at once.

## Changes
- **New component**: `CollapseToggleButton.vue` — a reusable icon button
(list-collapse / list-tree icon) with tooltip, bound via `v-model`
- **Error tab**: Toggle collapses/expands all error groups; per-group
state is now managed through `isSectionCollapsed` /
`setSectionCollapsed` helpers
- **Nodes tab** (`TabNodes.vue`): Per-node `collapseMap`; toggle
overrides per-section state; defaults to collapsed (nodes tab default
was already collapsed)
- **Normal inputs tab** (`TabNormalInputs.vue`): Per-node `collapseMap`
+ `advancedCollapsed`; toggle covers both normal and advanced sections;
defaults to collapsed when multiple nodes selected
- **Subgraph inputs tab** (`TabSubgraphInputs.vue`): Toggle covers both
main and advanced inputs sections
- **Global parameters tab** (`TabGlobalParameters.vue`): Toggle bound to
single `SectionWidgets`
- **i18n**: Added `g.collapseAll` and `g.expandAll` keys

## Review Focus
- `isAllCollapsed` getter in each tab: reads from the same per-section
state so the toggle accurately reflects current state rather than being
independently tracked
- `TabNormalInputs`: multi-node selection default collapse behaviour is
preserved through `isSectionCollapsed` fallback logic

## Screenshots
<img width="778" height="643" alt="image"
src="https://github.com/user-attachments/assets/04d07f32-5135-47f9-b029-78ca78a996fb"
/>


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9333-feat-add-collapse-expand-all-toggle-to-right-panel-tabs-3176d73d36508123ba22d6e81983bb1b)
by [Unito](https://www.unito.io)
2026-03-04 21:41:15 -08:00
Jin Yi
d20cc82ef4 [feat] Add reusable SearchInput component (#9168)
## Summary
Add a reusable `SearchInput` component with theme-aware styling, clear
button, loading state, and configurable sizes.

## Changes
- **SearchInput.vue**: Composable search input wrapping Reka UI Combobox
with search/clear/loading icon states
- **searchInput.variants.ts**: CVA-based size variants (`sm`, `md`,
`lg`) using semantic theme tokens (`bg-secondary-background`,
`text-base-foreground`)
- **SearchInput.stories.ts**: Storybook coverage for all sizes, loading,
custom icon/placeholder, and background override

## Review Focus
- Clear button alignment with search icon (`left-3.5` for `icon-sm`
button vs `left-4` for `size-4` icon)
- Theme token choices for light/dark compatibility

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9168-feat-Add-reusable-SearchInput-component-3116d73d365081309290fe84a46852e4)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-03-05 14:26:00 +09:00
Johnpaul Chiwetelu
a2cb864828 chore: bump CI container to 0.0.13 (#9394)
Updates CI container from `0.0.12` to `0.0.13`

**Triggered by:** [Tag
0.0.13](https://github.com/Comfy-Org/comfyui-ci-container/tree/0.0.13)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9394-chore-bump-CI-container-to-0-0-13-3196d73d365081279c5bca477025cb5a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
2026-03-05 05:49:10 +01:00
Deep Mehta
3d99566840 feat: add model-to-node backlinks for upcoming custom nodes (#9411)
## Summary
- Add `quickRegister()` mappings for models being added in open cloud
PRs so the "Use" button works when those node packs ship

## Mappings added

| Cloud PR | Node Pack | Model Directories | Loader Node |
|----------|-----------|-------------------|-------------|
| [#2645](https://github.com/Comfy-Org/cloud/pull/2645) |
ComfyUI-HunyuanVideoWrapper | `LLM/llava-llama-3-8b-*` |
`DownloadAndLoadHyVideoTextEncoder` |
| [#2598](https://github.com/Comfy-Org/cloud/pull/2598) |
comfyui-cogvideoxwrapper | `CogVideo/GGUF`, `CogVideo/ControlNet` |
`DownloadAndLoadCogVideoGGUFModel`, `DownloadAndLoadCogVideoControlNet`
|
| [#2594](https://github.com/Comfy-Org/cloud/pull/2594) |
ComfyUI-DynamiCrafterWrapper | `checkpoints/dynamicrafter{,/controlnet}`
| `DownloadAndLoadDynamiCrafterModel`,
`DownloadAndLoadDynamiCrafterCNModel` |
| [#2537](https://github.com/Comfy-Org/cloud/pull/2537) |
ComfyUI_LayerStyle_Advance | `BEN`, `BiRefNet/pth`, `onnx/human-parts`,
`lama` | `LS_LoadBenModel`, `LS_LoadBiRefNetModel`,
`LS_HumanPartsUltra`, `LaMa` |

## Safe to merge before backend

Unknown node classes are silently skipped by `registerNodeProvider()` —
the mapping becomes a no-op until the node pack is deployed. Zero risk.

## Test plan
- [ ] Verify no runtime errors on load (unknown classes are skipped
gracefully)
- [ ] After backend PRs merge, verify "Use" button creates correct node
for each model type

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9411-feat-add-model-to-node-backlinks-for-upcoming-custom-nodes-31a6d73d3650811bb129e557450f263a)
by [Unito](https://www.unito.io)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 20:04:07 -08:00
Christian Byrne
6dc4a3ed1e refactor(changeTracker): use shared clone util and centralize nodeOutputs snapshot (#9387)
## Summary

Follow-up to #9380. Replaces local `clone()` with shared util and
centralizes output snapshotting.

## Changes

- **What**: Replaced local `JSON.parse(JSON.stringify)` clone in
`changeTracker.ts` with shared `clone()` from `@/scripts/utils` (prefers
`structuredClone` with JSON fallback). Added `snapshotOutputs()` to
`useNodeOutputStore` as symmetric counterpart to existing
`restoreOutputs()`, and wired `changeTracker.store()` to use it.
- **Breaking**: None

## Review Focus

Symmetry between `snapshotOutputs()` and `restoreOutputs()` in the node
output store.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9387-refactor-changeTracker-use-shared-clone-util-and-centralize-nodeOutputs-snapshot-3196d73d365081a289c3cb414f57929e)
by [Unito](https://www.unito.io)
2026-03-04 20:03:26 -08:00
Alexander Brown
5327fef29f fix: spin out workflow tab/load stability regressions (#9345)
## Summary

Spin out workflow tab/load stability fixes from the share-by-url branch
so they can merge independently and reduce regression risk.

## Changes

- **What**: Fixes duplicate tabs on repeated same-workflow loads by
making active-workflow reload idempotent in `afterLoadNewGraph`; fixes
tab flicker on save/rename by removing async detach/attach gaps in
`workflowStore`; hardens duplicate workflow path by loading before clone
and assigning a new workflow `id`.

## Review Focus

Please review the idempotency gate in `afterLoadNewGraph`
(`activeState.id === workflowData.id`) and the save/rename path update
sequencing in `workflowStore` to confirm behavior remains correct for
restoration and re-import flows.

## Screenshots (if applicable)

N/A (workflow logic and tests only)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9345-fix-spin-out-workflow-tab-load-stability-regressions-3186d73d365081fe922bdc61dcf8d8f8)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-03-04 17:35:18 -08:00
Comfy Org PR Bot
f5e1330228 1.41.12 (#9396)
Patch version increment to 1.41.12

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9396-1-41-12-31a6d73d36508145a967c7fcf5831532)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-03-04 17:19:30 -08:00
Christian Byrne
0196a1cf54 fix: hide all painter labels in compact mode consistently (#9397)
## Summary

Add missing `v-if="!compact"` guards to painter label divs so all labels
are hidden consistently in compact mode.

## Changes

- **What**: Added `v-if="!compact"` to the Color, Hardness, Width,
Height, and Background label divs in `WidgetPainter.vue`, matching the
existing guards on Tool and Size labels.

## Review Focus

Straightforward consistency fix — all 7 label divs now use the same
compact-mode guard.

Fixes #9235

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9397-fix-hide-all-painter-labels-in-compact-mode-consistently-31a6d73d3650811a9d3af8dd290e2bca)
by [Unito](https://www.unito.io)
2026-03-04 17:09:41 -08:00
Christian Byrne
80e31a27b1 refactor: remove redundant = false defaults on boolean props (#9155)
## Summary

Remove redundant `= false` defaults from destructured `defineProps` on
boolean props, since Vue's [Boolean
casting](https://vuejs.org/guide/components/props.html#boolean-casting)
already defaults absent boolean props to `false`.

## Changes

- **What**: Remove 10 unnecessary `= false` default values across 8
components

## Review Focus

Vue compiles `defineProps<{ myBool?: boolean }>()` to `{ myBool: { type:
Boolean, required: false } }`. Boolean casting means absent boolean
props are already `false` — the explicit `= false` in JS destructuring
defaults never triggers.

Follow-up to #9150.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9155-refactor-remove-redundant-false-defaults-on-boolean-props-3116d73d36508196af0bcba53257720a)
by [Unito](https://www.unito.io)
2026-03-04 16:59:05 -08:00
Deep Mehta
0f8473db35 feat: add model type mappings for cloud custom nodes (#9392)
## Summary

Adds model-to-node backlinks in `modelToNodeStore.ts` for all
cloud-deployed custom node models that were missing mappings. Without
these, clicking "Use" on a model in the model browser throws an error.

**17 new backlinks added** covering ~340 models across deployed node
packs:

| Category | Directories | Node | Models |
|----------|-------------|------|--------|
| Vision-Language | LLM/Qwen-VL/* (12 specific paths) | AILab_QwenVL /
AILab_QwenVL_PromptEnhancer | ~186 |
| TTS | qwen-tts/* | FB_Qwen3TTSVoiceClone | ~68 |
| Video | SEEDVR2, liveportrait/*, mimicmotion, rife | various | ~33 |
| Depth | depthanything3 | DownloadAndLoadDepthAnythingV3Model | 7 |
| Segmentation | face_parsing, sam3 | various | 4 |
| Diffusers | diffusers/* (Kolors) | DownloadAndLoadKolorsModel | 16 |
| Other | clip/*, dwpose, onnx, detection, UltraShape, sharp | various |
~26 |

**Key fix:** Replaced the top-level `LLM` fallback with specific
`LLM/Qwen-VL/*` paths. The old fallback incorrectly mapped `LLM/llava-*`
models to `AILab_QwenVL`.

Models without deployed node packs (llava/HyVideo, latentsync, sam3d,
sam3dbody, inpaint, vae_approx) are excluded — those are being removed
from `supported_models.json` in Comfy-Org/cloud#2652.

## Test plan
- [ ] Verify "Use" button works for QwenVL models in model browser
- [ ] Verify "Use" button works for TTS, video, depth, segmentation
models
- [ ] Verify no `No node provider registered for category` errors for
deployed models

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

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
2026-03-04 16:51:42 -08:00
Kelly Yang
120524faa1 feat(minimap): add node execution status visualization (#9187)
## Summary

Added visual indicators (colored borders) to the MiniMap to display the
real-time execution status (running, executed, or error) of nodes.

## Changes

- **What**: Added visual feedback to the MiniMap to show node execution
states (green for running/executed, red for errors) by integrating with
`useExecutionStore` and updating the canvas renderer.

## Review Focus

Confirmed that relying on the array `.includes()` check for
`executingNodeIds` in the data sources avoids unnecessary `Set`
allocations during frequent redraws.

## Screenshots 

<img width="540" height="446" alt="14949d48035db5c64cceb11f7f7f94a3"
src="https://github.com/user-attachments/assets/cac53a80-9882-43fd-a725-7003fe3fd21a"
/>

<img width="562" height="464" alt="7e922f54dea2cea4e6b66202d2ad0dd3"
src="https://github.com/user-attachments/assets/e178b981-3af0-417f-8e21-a706f192fabf"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9187-feat-minimap-add-node-execution-status-visualization-3126d73d3650816eb7b3ca415cf6a8f1)
by [Unito](https://www.unito.io)
2026-03-04 16:43:12 -08:00
Kelly Yang
fd9e774a29 feat(ui): add copy button to read-only textarea widget on hover (#9331)
## Summary

Added a `copy-to-clipboard` button that appears when hovering over
read-only textarea widgets to improve user experience.

## Changes

- **What**: Added a copy button utilizing `useCopyToClipboard` to
[WidgetTextarea.vue](cci:7://file:///Users/kelly/Documents/comfyui/ComfyUI_frontend/src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue:0:0-0:0)
that only displays when the widget is read-only and hovered.

## Screenshots 
<img width="670" height="498" alt="e30362fdc6792f3a955f3415f0f42afb"
src="https://github.com/user-attachments/assets/1b7ec5dc-3733-48b6-9708-6ae56926054a"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9331-feat-ui-add-copy-button-to-read-only-textarea-widget-on-hover-3176d73d36508159a339d567b5c33591)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Terry Jia <terryjia88@gmail.com>
Co-authored-by: Alexander Brown <DrJKL0424@gmail.com>
Co-authored-by: Dante <bunggl@naver.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-03-05 09:35:38 +09:00
Christian Byrne
7b316eb9a2 feat: add statistical significance to perf report with z-score thresholds (#9305)
## Summary

Replace fixed 10%/20% perf delta thresholds with dynamic σ-based
classification using z-scores, eliminating false alarms from naturally
noisy duration metrics (10-17% CV).

## Changes

- **What**:
- Run each perf test 3× (`--repeat-each=3`) and report the mean,
reducing single-run noise
- Download last 5 successful main branch perf artifacts to compute
historical μ/σ per metric
- Replace fixed threshold flags with z-score significance: `⚠️
regression` (z>2), ` neutral/improvement`, `🔇 noisy` (CV>50%)
  - Add collapsible historical variance table (μ, σ, CV) to PR comment
- Graceful cold start: falls back to simple delta table until ≥2
historical runs exist
- New `scripts/perf-stats.ts` module with `computeStats`, `zScore`,
`classifyChange`
  - 18 unit tests for stats functions

- **CI time impact**: ~3 min → ~5-6 min (repeat-each adds ~2 min,
historical download <10s)

## Review Focus

- The `gh api` call in the new "Download historical perf baselines"
step: it queries the last 5 successful push runs on the base branch. The
`gh` CLI is available natively on `ubuntu-latest` runners and
auto-authenticates with `GITHUB_TOKEN`.
- `getHistoricalStats` averages per-run measurements before computing
cross-run σ — this is intentional since historical artifacts may also
contain repeated measurements after this change lands.
- The `noisy` classification (CV>50%) suppresses metrics like `layouts`
that hover near 0 and have meaningless percentage swings.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9305-feat-add-statistical-significance-to-perf-report-with-z-score-thresholds-3156d73d3650818d9360eeafd9ae7dc1)
by [Unito](https://www.unito.io)
2026-03-04 16:16:53 -08:00
Christian Byrne
b8edb11ac1 feat: add eslint-plugin-better-tailwindcss for Tailwind v4 linting (#9245)
## Summary

Add `eslint-plugin-better-tailwindcss` to the ESLint toolchain for
Tailwind CSS v4 class linting.

## Changes

- **What**: Integrate `eslint-plugin-better-tailwindcss` (v4.3.1) with
the recommended config, pointed at the design-system CSS entry point for
v4 theme resolution. Five rules are enabled initially:
`enforce-canonical-classes`, `no-deprecated-classes`,
`no-conflicting-classes`, `no-duplicate-classes`,
`no-unnecessary-whitespace`. Three rules are disabled pending follow-up:
`no-unknown-classes` (needs PrimeIcon/custom class whitelisting),
`enforce-consistent-line-wrapping` (oxfmt conflict risk),
`enforce-consistent-class-order` (large batch change).
- **Dependencies**: `eslint-plugin-better-tailwindcss` ^4.3.1
- Fix conflicting `outline outline-1` classes in
`FormDropdownMenuActions.vue` (caught by the new
`no-conflicting-classes` rule).

## Review Focus

- Is the rule severity/enablement strategy appropriate for incremental
adoption?
- The 700 warnings (mostly `enforce-canonical-classes` and
`no-deprecated-classes`) are all auto-fixable via `eslint --fix` —
should we batch-fix them in this PR or a follow-up?

Fixes COM-15518

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9245-feat-add-eslint-plugin-better-tailwindcss-for-Tailwind-v4-linting-3136d73d365081df8a64dd55962d073f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-03-04 15:34:23 -08:00
AustinMroz
57a919fad2 Split selection into an inputs and outputs step (#9362)
When building an app, selecting inputs and selecting outputs are now 2
separate steps. This prevents confusion where clicking on the widget of
an output node will select that widget instead of the entire output.

<img width="1673" height="773" alt="image"
src="https://github.com/user-attachments/assets/e5994479-6fcf-4572-b58b-bf8cecfb7d55"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9362-Split-selection-into-an-inputs-and-outputs-step-3196d73d36508187b4a1e51c73f1c54c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-03-04 15:18:16 -08:00
Johnpaul Chiwetelu
316a05c77f fix: replace hardcoded styles with design tokens and cache StatusBadge variants (#9349)
## Summary

Replace hardcoded color and spacing values with semantic design tokens
and cache a computed variant class in StatusBadge.

## Changes

- **What**: Use Tailwind 4 CSS spacing variables in FormDropdownMenu
layout configs, replace zinc color utilities with semantic
`node-component-border` tokens in FormDropdownInput, wrap
`statusBadgeVariants()` in a `computed` for caching in StatusBadge.

## Review Focus

Straightforward token replacements and a computed caching change -- no
behavioral differences expected.

Fixes #9087
Fixes #9086
Fixes #7910

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9349-fix-replace-hardcoded-styles-with-design-tokens-and-cache-StatusBadge-variants-3186d73d36508185aae2e0753c9d1694)
by [Unito](https://www.unito.io)
2026-03-04 14:23:47 -08:00
Comfy Org PR Bot
4b70ca298a 1.41.11 (#9361)
Patch version increment to 1.41.11

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9361-1-41-11-3196d73d365081cc9b9ef730251b07b4)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-03-04 14:22:12 -08:00
Benjamin Lu
1cee6272c1 fix: add run progress toggle to job history menu (#9176)
Summary
- Add hidden setting `Comfy.Queue.ShowRunProgressBar` (default `true`).
- Add `Show run progress bar` toggle to the shared `...` job history
menu (`JobHistoryActionsMenu`), placed next to `Docked Job History`.
- Use that setting to control both the inline run progress bar and the
inline summary text under it.
- Keep queue button right-click context menu focused on queue actions.
- Add/update tests for the new toggle behavior and summary visibility.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9176-fix-add-run-progress-toggle-to-job-history-menu-3116d73d365081118202d8d67a857367)
by [Unito](https://www.unito.io)
2026-03-04 14:15:11 -08:00
Christian Byrne
bcc470642f fix: cache canvas cursor style to avoid redundant DOM writes (#9171)
## Summary

Cache `canvas.style.cursor` to avoid redundant DOM writes that dirty
Firefox's style tree.

## Changes

- **What**: Add `_lastCursor` field to
`LGraphCanvas._updateCursorStyle()` — only writes `canvas.style.cursor`
when the value changes. Eliminates ~347 redundant style mutations per
profiling session.

## Review Focus

- The fix is 2 lines (cache field + comparison). The unit test validates
the caching pattern without requiring full LGraphCanvas instantiation.
- This is one of several contributors to Firefox's cascading style
recalculation freeze. Each `canvas.style.cursor` write dirties the style
tree, which is flushed during the next paint in the canvas render loop.

## Stack

2 of 4 in Firefox perf fix stack. Depends on #9170.

<!-- Fixes #ISSUE_NUMBER -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9171-fix-cache-canvas-cursor-style-to-avoid-redundant-DOM-writes-3116d73d36508139827fe1d644fa1bd0)
by [Unito](https://www.unito.io)
2026-03-04 14:06:31 -08:00
Johnpaul Chiwetelu
df712953a3 [fix] Replace eval() with safe math expression parser (#9263)
## Summary
Replace `eval()` in `evaluateInput()` with a custom recursive descent
math parser, eliminating a security concern and enabling the `no-eval`
lint rule.

## Changes
- **New**: `mathParser.ts` — recursive descent parser for `+`, `-`, `*`,
`/`, `%`, `()`, decimals, unary operators. Zero new dependencies.
- **Modified**: `widget.ts` — replaced `eval()` call with
`evaluateMathExpression()`, use `isFinite()` instead of `isNaN()` to
reject `Infinity`
- **Modified**: `.oxlintrc.json` — `no-eval` rule changed from `"off"`
to `"error"`
- **Tests**: 59 parser tests + 23 integration tests covering complex
expressions, edge cases, and invalid input

## Review Feedback Addressed
- Renamed `unit()` → `primary()` for clarity
- Added modulo (`%`) operator support
- Normalized negative zero to positive zero
- Added depth limit (200) for nested parentheses
- Used `isFinite()` instead of `isNaN()` to reject
`Infinity`/`-Infinity`
- Added tests for edge-case number formats, unary-after-binary
operators, modulo, depth limits, scientific/hex notation, and `Infinity`

Fixes #8032
Fixes #9272
Fixes #9273
Fixes #9274
Fixes #9275

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9263-fix-Replace-eval-with-safe-math-expression-parser-3136d73d3650812f9f8dea21d1ea4f06)
by [Unito](https://www.unito.io)
2026-03-04 14:04:37 -08:00
Johnpaul Chiwetelu
82750d629d [refactor] Type createNode options parameter (#9262)
## Summary
Narrow `CreateNodeOptions` from `Partial<Omit<LGraphNode, ...>>`
(exposing hundreds of properties/methods) to an explicit interface
listing only creation-time properties.

## Changes
- Replace `Partial<Omit<LGraphNode, 'constructor' | 'inputs' |
'outputs'>>` with explicit `CreateNodeOptions` interface containing
only: `pos`, `size`, `properties`, `flags`, `mode`, `color`, `bgcolor`,
`boxcolor`, `title`, `shape`, `inputs`, `outputs`
- Rename local `CreateNodeOptions` in `createModelNodeFromAsset.ts` to
`ModelNodeCreateOptions` to avoid collision

## Ecosystem verification
GitHub code search across ~50 repos confirms only `pos` and `outputs`
are used externally. All covered by the narrowed interface.

Fixes #9276
Fixes #4740
2026-03-04 14:01:18 -08:00
jaeone94
9e2299ca65 feat(error-groups): sort execution error cards by node execution ID (#9334)
## Summary

Sort execution error cards within each error group by their node
execution ID in ascending numeric order, ensuring consistent and
predictable display order.

## Changes

- **What**: Added `compareExecutionId` utility to
`src/types/nodeIdentification.ts` that splits node IDs on `:` and
compares segments numerically left-to-right; applied it as a sort
comparator when building `ErrorGroup.cards` in `useErrorGroups.ts`

## Review Focus

- The comparison treats missing segments as `0`, so `"1"` sorts before
`"1:20"` (subgraph nodes follow their parent); confirm this ordering
matches user expectations
- All comparisons are purely numeric — non-numeric segment values would
sort as `NaN` (treated as `0`)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9334-feat-error-groups-sort-execution-error-cards-by-node-execution-ID-3176d73d365081e1b3e4e4fa8831fe16)
by [Unito](https://www.unito.io)
2026-03-04 13:59:58 -08:00
Christian Byrne
69076f35f8 test: add subgraph workflow round to performance tests (#9306)
## Summary

Add subgraph workflow performance tests to track style recalculations
and layout thrashing for nested subgraph workflows.

## Changes

- **What**: Add 3 new perf test cases (`subgraph-idle`,
`subgraph-mouse-sweep`, `subgraph-dom-widget-clipping`) that mirror the
existing default workflow tests but load the `subgraphs/nested-subgraph`
workflow. The existing perfReporter pipeline automatically picks up the
new measurements.

## Review Focus

The new tests are structurally identical to the existing 3 default
workflow tests — only the workflow loaded and measurement names differ.
No CI or config changes needed.

Fixes
https://www.notion.so/comfy-org/Implement-Add-subgraph-workflow-round-in-performance-testing-process-3156d73d365081d094efdee58215e15b

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9306-test-add-subgraph-workflow-round-to-performance-tests-3156d73d36508133b85cc53a748bc75f)
by [Unito](https://www.unito.io)
2026-03-04 13:38:48 -08:00
Kelly Yang
1e86e8c4d5 [Bug] Node preview images are lost when switching between multiple workflow tabs (#9380)
## Summary

When working with multiple workflow tabs, the internal preview (image
thumbnail) of nodes like Load Image disappears after navigating away
from and back to a tab. This affects all active tabs once the switch
occurs.

## Screenshot
before


https://github.com/user-attachments/assets/99466123-37db-406f-9e17-0a9ff22311c3



after




https://github.com/user-attachments/assets/bdad0ef1-72b7-46c7-aa61-0a557688e55e

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-03-04 20:58:57 +00:00
pythongosssss
31276ff2a6 feat: Show empty workflow dialog when entering app builder with no nodes (#9379)
## Summary

Prompts users to load a template or return to graph when entering
builder mode on an empty workflow

## Screenshots (if applicable)

<img width="627" height="275" alt="image"
src="https://github.com/user-attachments/assets/c1a35dc3-4e8f-4abd-95b9-2f92524e8ebf"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9379-feat-Show-empty-workflow-dialog-when-entering-app-builder-with-no-nodes-3196d73d36508123b643ec893cd86cac)
by [Unito](https://www.unito.io)
2026-03-04 12:15:56 -08:00
AustinMroz
f084a60708 Misc app mode fixes (#9368)
A working branch of smaller app mode fixes. Can be merged at any time
and I'll make a new branch.
- Selected inputs and outputs can now be re-ordered when clicking on
label text
- 3d outputs once again display correctly
- Some padding has been added to the side so that control buttons don't
overlap with the floating app sidebar controls
- A "Share" button placeholder has been added to the menu, but is
disabled
- Adds a workaround for canvas read_only state being disabled when
'space' is pressed.
  - This one is particularly hacky, and can be pulled out if problematic
- Fix download all only downloading the first output

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9368-Misc-app-mode-fixes-3196d73d365081eab02ad1e693784707)
by [Unito](https://www.unito.io)
2026-03-04 10:14:05 -08:00
pythongosssss
c759fe517f feat: Replace BuilderExitButton with new BuilderFooterToolbar (#9378)
## Summary

Makes it easier and more obvious for users to navigate between steps

## Changes

- **What**: 
- add back/next navigation to builder footer alongside exit button
- extract shared step logic into useBuilderSteps composable

## Screenshots (if applicable)

<img width="428" height="102" alt="image"
src="https://github.com/user-attachments/assets/91b33e8f-53ae-4895-a2eb-fb1316b2b367"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9378-feat-Replace-BuilderExitButton-with-new-BuilderFooterToolbar-3196d73d3650819392efc171cf277326)
by [Unito](https://www.unito.io)
2026-03-04 09:58:59 -08:00
pythongosssss
f4ed79b133 feat: Add apps sidebar tab (#9342)
## Summary

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

## Changes

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

## Review Focus

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

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

## Screenshots (if applicable)

<img width="383" height="359" alt="image"
src="https://github.com/user-attachments/assets/47905196-9db6-4a57-8cf7-384d4d37d000"
/>

<img width="335" height="281" alt="image"
src="https://github.com/user-attachments/assets/843068f3-e895-4781-bf5f-e0eb86d3387c"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9342-feat-Add-apps-sidebar-tab-3176d73d3650812b822fc9cc3f17322e)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-03-04 09:54:26 -08:00
pythongosssss
194218a9d6 fix: Prune invalid builder mappings on load (#9376)
## Summary

- extract resolveNode to reusable util
- remove mid builder pruning
- handle missing widgets with label

## Review Focus

`resolveNode` was simplified for subgraphs by calling getNodeById on
each of the subgraphs instead of searching their inner nodes manually.

## Screenshots (if applicable)

"Widget not visible"
<img width="657" height="822" alt="image"
src="https://github.com/user-attachments/assets/ab7d1e87-3210-4e54-876a-07881974b5c7"
/>
<img width="674" height="375" alt="image"
src="https://github.com/user-attachments/assets/c50ec871-d423-43d6-8e1e-7b1a362f621c"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9376-fix-Prune-invalid-builder-mappings-on-load-3196d73d3650811280c2d459ed0271af)
by [Unito](https://www.unito.io)
2026-03-04 09:52:14 -08:00
pythongosssss
3e59f8e932 feat: App builder confirmation dialog after setting default view mode (#9374)
## Summary

Adds an additional dialog after setting the default view of the workflow
to let users pick their next step

## Screenshots (if applicable)

<img width="479" height="332" alt="image"
src="https://github.com/user-attachments/assets/1ea40b10-d7d3-49ff-9ea2-27b9e907c923"
/>

<img width="478" height="343" alt="image"
src="https://github.com/user-attachments/assets/21674998-5ce2-496d-97e6-ef8f2f2d7dd7"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9374-feat-App-builder-confirmation-dialog-after-setting-default-view-mode-3196d73d36508192a45ee8ba0a7f74a6)
by [Unito](https://www.unito.io)

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-03-04 09:51:36 -08:00
Johnpaul Chiwetelu
9933d9bd11 fix: make queue button tooltip reflect current mode (#9350)
## Summary

Make queue button tooltip mode-aware so it shows the correct action text
based on whether QPOV2 is enabled.

## Changes

- **What**: Update `queueHistoryTooltipConfig` in `ComfyActionbar.vue`
to conditionally show "View Job History" (QPOV2 enabled) or
"Expand/Collapse Queue" (QPOV2 disabled) instead of always showing "View
Job History"

## Review Focus

Straightforward conditional using existing `isQueuePanelV2Enabled`
computed and existing i18n keys.

Fixes #9278

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9350-fix-make-queue-button-tooltip-reflect-current-mode-3186d73d36508122b198e5fbb0226221)
by [Unito](https://www.unito.io)
2026-03-03 22:27:36 -08:00
AustinMroz
fe8ab1d896 App mode mobile redesign (#9047)
Reworks the app mode display for mobile devices. Adds multiple bottom
tabs that can be swiped between.


![AnimateDiff_00005](https://github.com/user-attachments/assets/e1c928ff-dd52-4f4c-83a6-c351c4711e62)

To be handled in followup PRs
- Nicer error display
- Support for even smaller screens
- UX improvements for the 'Outputs' pane
  - Was postponed to minimize conflicts with non-mobile development.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9047-App-mode-mobile-redesign-30e6d73d365081388e4adea4df886522)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-03-03 14:18:19 -08:00
pythongosssss
68b16e3a3f feat: App mode saving rework (#9338)
## Summary

Change app mode changes to be written directly to the workflow on change
instead of requiring explicit save via builder.
Temporary: Adds `.app.json` file extension to app files for
identification since we don't currently have a way to identify them with
metadata
Removes app builder save dialog and replaces it with default mode
selection

## Changes

- **What**: 
- ensure all save locations handle app mode
- remove dirtyLinearData and flushing

- **Breaking**: 
- if people are relying on workflow names and are converting to/from app
mode in the same workflow, they will gain/lose the `.app` part of the
extension

## Screenshots (if applicable)

<img width="689" height="84" alt="image"
src="https://github.com/user-attachments/assets/335596ee-dce9-4e3a-a7b5-f0715c294e41"
/>

<img width="421" height="324" alt="image"
src="https://github.com/user-attachments/assets/ad3cd33c-e9f0-4c30-8874-d4507892fc6b"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9338-feat-App-mode-saving-rework-3176d73d3650813f9ae1f6c5a234da8c)
by [Unito](https://www.unito.io)
2026-03-03 11:35:36 -08:00
Alexander Brown
ab2aaa3852 refactor: use the gradient directly instead of with a custom utility. (#9327)
## Summary

Little simpler.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9327-refactor-use-the-gradient-directly-instead-of-with-a-custom-utility-3166d73d36508179876af0ef8cea35b7)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-03-03 09:25:04 -08:00
pythongosssss
be04046ec8 App builder item - cap max width and truncate (#9335)
## Summary

Currently they overflow, this adds truncation

## Screenshots (if applicable)

<img width="325" height="459" alt="image"
src="https://github.com/user-attachments/assets/92600c14-da31-4a96-af3c-aeac928243c4"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9335-App-builder-item-cap-max-width-and-truncate-3176d73d365081d1b461cf2758b04ec2)
by [Unito](https://www.unito.io)
2026-03-03 08:55:13 -08:00
pythongosssss
b18a0713db feat: App mode enter builder menu item (#9341)
## Summary

Adds enter builder menu item for easier access to app builder. 
Fixes issues with seen item tracking

## Changes

- **What**: 
- add enter builder menu item
- change non visible items to still be returned as part of the array, so
they are not incorrectly removed from the seen-items tracking
- split toggle-app-mode into two stable items

## Screenshots (if applicable)

<img width="309" height="526" alt="image"
src="https://github.com/user-attachments/assets/69affc2c-34ab-45eb-b47b-efacb8a20b99"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9341-feat-App-mode-enter-builder-menu-item-3176d73d365081a9a7e7cf1a1986354f)
by [Unito](https://www.unito.io)
2026-03-03 08:35:47 -08:00
pythongosssss
d360b2218f fix: App builder menu change "Save app" to just "Save" (#9356)
## Summary

Changes "Save app" to just "Save" as you are saving the whole workflow
as normal

## Screenshots (if applicable)

<img width="293" height="247" alt="image"
src="https://github.com/user-attachments/assets/c5aea772-12cf-4b0e-8336-8d72ef55be98"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9356-fix-App-builder-menu-change-Save-app-to-just-Save-3186d73d36508119a7c0e8e00155e0b0)
by [Unito](https://www.unito.io)
2026-03-03 08:25:48 -08:00
Terry Jia
613058e831 fix: propagate widget disabled state to Vue node components (#9321)
## Summary

Widgets with `widget.disabled = true` (e.g. display-only counters in
custom nodes) were editable in Vue node mode despite being correctly
greyed out in litegraph mode. The disabled state from the widget store
was not being merged into the options passed to Vue widget components.

## Screenshots (if applicable)
Before


https://github.com/user-attachments/assets/6957dd86-6eb9-4edb-93ee-50fc5aa5350f


After


https://github.com/user-attachments/assets/d954006f-d7e6-4e7c-9b3c-bcabed0e6260

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9321-fix-propagate-widget-disabled-state-to-Vue-node-components-3166d73d365081a7936aeabe81eb6e15)
by [Unito](https://www.unito.io)
2026-03-03 10:27:26 -05:00
Johnpaul Chiwetelu
16119dfcd2 fix: allow cursor positioning in painter opacity input (#9348) 2026-03-03 10:14:34 +01:00
Terry Jia
a6f1b1cf90 fix: sync subgraph name on double-click title rename (#9353)
## Summary
The Vue renderer's title editing path (NodeHeader →
useNodeEventHandlers) only updated node.title but not subgraph.name, so
the breadcrumb didn't reflect the new name when entering the subgraph.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9353-fix-sync-subgraph-name-on-double-click-title-rename-3186d73d365081e2bc54f19ecd421ac0)
by [Unito](https://www.unito.io)
2026-03-02 20:15:21 -08:00
Alexander Brown
c95d32249b fix: Custom Combo options display in Nodes 2.0 (#9324)
## Summary

Keep the value in the store instead of in the closure.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9324-fix-Custom-Combo-options-display-in-Nodes-2-0-3166d73d3650814db361c41ebdb1d222)
by [Unito](https://www.unito.io)
2026-03-02 19:23:01 -08:00
Dante
740df0470e feat: use cloud backend thumbnail resize for image previews (#9298)
## Summary

- In cloud mode, large generated images (4K, 8K+) cause browser freezing
when loaded at full resolution for preview display
- The cloud backend (ingest service) now supports a `res` query
parameter on `/api/view` that returns server-side resized JPEG (quality
80, max 512px) instead of redirecting to the full-size GCS original
- This PR adds `&res=512` to all image preview URLs in cloud mode,
reducing browser decode overhead from tens of MB to tens of KB
- Downloads still use the original resolution (no `res` param)
- No impact on localhost/desktop builds (`isCloud` compile-time
constant)

### without `?res`

302 -> png downloads
<img width="808" height="564" alt="스크린샷 2026-02-28 오후 6 53 03"
src="https://github.com/user-attachments/assets/7c1c62dd-0bc4-468d-9c74-7b98e892e126"
/>
<img width="323" height="137" alt="스크린샷 2026-02-28 오후 6 52 52"
src="https://github.com/user-attachments/assets/926aa0c4-856c-4057-96a0-d8fbd846762b"
/>

200 -> jpeg

### with `?res`
<img width="811" height="407" alt="스크린샷 2026-02-28 오후 6 51 55"
src="https://github.com/user-attachments/assets/d58d46ae-6749-4888-8bad-75344c4d868b"
/>


### Changes

- **New utility**: `getCloudResParam(filename?)` returns `&res=512` in
cloud mode for image files, empty string otherwise
- **Core stores**: `imagePreviewStore` appends `res` to node output
URLs; `queueStore.ResultItemImpl` gets a `previewUrl` getter (separates
preview from download URLs)
- **Applied to**: asset browser thumbnails, widget dropdown previews,
linear mode indicators, image compare node, background image upload

### Intentionally excluded

- Downloads (`getAssetUrl`) — need original resolution
- Mask editor — needs pixel-accurate data
- Audio/video/3D files — `res` only applies to raster images
- Execution-in-progress previews — use WebSocket blob URLs, not
`/api/view`

## Test plan

- [x] Unit tests for `getCloudResParam()` (5 tests: cloud/non-cloud,
image/non-image, undefined filename)
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes
- [x] All 5332 unit tests pass
- [x] Manual verification on cloud.comfy.org: `res=512` returns 200 with
resized JPEG; without `res` returns 302 redirect to GCS PNG original

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 02:56:06 +00:00
Terry Jia
dccf68ddb7 fix: improve painter cursor performance by bypassing Vue reactivity (#9339)
## Summary
Previously painter has node performance issue. 
Use direct DOM manipulation for cursor position updates instead of
reactive refs, and add will-change-transform for GPU layer promotion.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9339-fix-improve-painter-cursor-performance-by-bypassing-Vue-reactivity-3176d73d365081d88b23d26e774cebf5)
by [Unito](https://www.unito.io)
2026-03-02 21:18:48 -05:00
Terry Jia
117448fba4 fix: stop pointer events on audio widgets to prevent node drag (#9329)
## Summary

Audio player and record widgets were missing @pointerdown.stop, causing
node drag when interacting with the timeline or controls.

## Screenshots (if applicable)
before


https://github.com/user-attachments/assets/061a9ad2-0cc2-45f8-aea0-d45e3a2912b9


after


https://github.com/user-attachments/assets/a510c50a-65b8-4944-9480-b53cbe61c7da

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9329-fix-stop-pointer-events-on-audio-widgets-to-prevent-node-drag-3176d73d36508140b236c61e83954f5c)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-03-02 20:43:25 -05:00
Terry Jia
da77227cf2 fix: clear combo widget value when removing image preview (#9323)
## Summary
The X button on image preview in VueNodes mode only cleared the stored
outputs but left the combo widget value intact, causing the old image to
persist across workflow runs and page refreshes.

## Screenshots (if applicable)
Before

https://github.com/user-attachments/assets/e2146ed1-5d79-41d6-946c-b30667ffac6a

After


https://github.com/user-attachments/assets/359b81fa-acc9-4711-9cee-62c230086f0c

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9323-fix-clear-combo-widget-value-when-removing-image-preview-3166d73d3650816db867eba49b8aeb6c)
by [Unito](https://www.unito.io)
2026-03-02 20:26:44 -05:00
Comfy Org PR Bot
4868e6003f 1.41.10 (#9343)
Patch version increment to 1.41.10

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9343-1-41-10-3186d73d3650814d9077c68cc06131ea)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-03-02 16:25:51 -08:00
pythongosssss
9b05d7cbb7 App mode output history UX improvements (#9285)
## Summary
- replace reka ui list with normal elements due to rekas aggressive
autoscrolling and event blocking
- rework layout to fix in progress items outside scrollable area
- extract feedback component
- avoid scroll position changing when adding new items
- add left/right keyboard navigation

## Screenshots (if applicable)
Showing fixed active items at start
<img width="1292" height="101" alt="image"
src="https://github.com/user-attachments/assets/dcd3215c-ac09-4081-b483-8631d17ca6bf"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9285-App-mode-output-history-UX-improvements-3146d73d3650819a9f97edb41db975cc)
by [Unito](https://www.unito.io)
2026-03-02 14:46:45 -08:00
Terry Jia
74626d65d3 fix: use widget.options.hidden to hide painter widgets in Vue renderer (#9337)
## Summary

The Vue node renderer checks widget.options.hidden, not widget.hidden.
This was previously masked by the backend sending hidden: true via
extra_dict, but is now needed as the backend switches to io.Color.Input.

BE change is in https://github.com/Comfy-Org/ComfyUI/pull/12294

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9337-fix-use-widget-options-hidden-to-hide-painter-widgets-in-Vue-renderer-3176d73d3650815cad4beb8f9f35f7e6)
by [Unito](https://www.unito.io)
2026-03-02 14:29:21 -08:00
pythongosssss
31a4dce5d4 Add enterAppBuilder method for skipping arrange mode (#9310)
## Summary

When already in app mode and entering builder with outputs defined, skip
the select step and go straight to arrange

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9310-Add-enterAppBuilder-method-for-skipping-arrange-mode-3156d73d36508101903ff434a2a1ac08)
by [Unito](https://www.unito.io)
2026-03-02 11:10:48 -08:00
pythongosssss
0d7dc15916 App mode output feed to only show current session results for outputs defined in the app (#9307)
## Summary

Updates app mode to only show images from:
- the workflow that generated the image
- in the current session
- for the outputs selected in the builder

## Changes

- **What**: 
- adds new mapping of jobid -> workflow path [cant use id here as it is
not guaranteed unique], capped at 4k entries
- fix bug where executing a workflow then quickly switching tabs
associated incorrect workflow
- add missing output history tests

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9307-App-mode-output-feed-to-only-show-current-session-results-for-outputs-defined-in-the-app-3156d73d36508142b4bbca3f938fc5c2)
by [Unito](https://www.unito.io)
2026-03-02 11:10:20 -08:00
AustinMroz
1dd789fa54 Support selection of app inputs and outputs from vue mode (#9259)
- The input and output indicators are now plugged directly into the
`LGraphNode.vue` template. Care was taken to make implementation to have
low cost for performance and complexity when not in app mode setup.
- Context menu event handlers are added to each widget in vue mode
instead of resolving the target widget of an event
- Swap the nodeId passed by `useGraphNodeManager` to not include the
locator id. This id was never used and was incorrect since it didn't
resolve across nested subgraphs.
- Continued bug fixes for app mode as a whole.

Known issue: There is disparity of nodeId between litegraph (which
references the widget in the root graph) and vue (which promotes the
original widget). Efforts to reconcile are ongoing.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9259-Support-selection-app-inputs-and-outputs-from-vue-mode-3136d73d365081ae8e56e35bf6322409)
by [Unito](https://www.unito.io)

---------

Co-authored-by: pythongosssss <125205205+pythongosssss@users.noreply.github.com>
2026-03-02 09:49:21 -08:00
Comfy Org PR Bot
84d7aa0fd9 1.41.9 (#9312)
Patch version increment to 1.41.9

**Base branch:** `main`

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-03-01 20:19:10 -08:00
Christian Byrne
59c3215296 fix: skip CodeRabbit reviews on bot and release PRs (#9279)
## Problem

CodeRabbit is reviewing release and backport PRs created by bots (e.g.
[#9264](https://github.com/Comfy-Org/ComfyUI_frontend/pull/9264)),
leaving unnecessary review comments.

## Solution

Add ignore rules to `.coderabbit.yaml`:

- **`ignore_usernames`**: `comfy-pr-bot`, `github-actions` — skips
reviews on PRs authored by these bot accounts
- **`ignore_title_keywords`**: `[release]`, `[backport` — skips reviews
on release and backport PRs by title

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9279-fix-skip-CodeRabbit-reviews-on-bot-and-release-PRs-3146d73d3650814c9ebae0d08acbafd6)
by [Unito](https://www.unito.io)

---------

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-28 23:29:21 -08:00
Hunter
589f58f916 feat: add ever-present upgrade button for free-tier users (#9315)
## Summary

Add persistent upgrade CTAs for free-tier users: a topbar button and
"Upgrade to add credits" replacing "Add Credits" in popovers and
settings panels.

## Changes

- **What**:
- New `TopbarSubscribeButton` component in both GraphCanvas and
LinearView topbars, visible only to free-tier users
- Profile popover (legacy + workspace): free-tier users see "Upgrade to
add credits" instead of "Add Credits", linking directly to the pricing
table
- Manage Plan settings (legacy + workspace): same replacement —
free-tier users see "Upgrade to add credits" instead of "Add Credits"
- Paid-tier users retain the original "Add Credits" behavior in all
locations
  - All upgrade buttons go directly to the pricing table (one-step flow)

## Review Focus

- The `isFreeTier` conditional gating on the buttons — ensure free-tier
users see upgrade CTAs and paid users see normal Add Credits
- Layout in Manage Plan panels uses `flex flex-col gap-3` to stack the
upgrade button below the usage history link instead of side-by-side

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9315-feat-add-ever-present-upgrade-button-for-free-tier-users-3166d73d365081228cdfe6a67fec6aec)
by [Unito](https://www.unito.io)
2026-02-28 20:07:12 -08:00
Hunter
7c8a548798 feat: add cloud frontend build dispatch workflow (#9308)
## Summary

Adds `.github/workflows/cloud-dispatch-build.yaml` — fires a
`repository_dispatch` event (`frontend-asset-build`) to
`Comfy-Org/cloud` on push to `cloud/*` branches and `main`.

The cloud repo handles the actual build, GCS upload, and secret
management (Sentry, Algolia, GCS creds). This is fire-and-forget.

## Changes

- New workflow: `cloud-dispatch-build.yaml`
- Trigger: `push` to `cloud/*` and `main` only
- Payload: `ref` (commit SHA) + `branch` (branch name), built with `jq`
to prevent injection
- SHA-pinned `peter-evans/repository-dispatch@v4.0.1`
- Hardened: `permissions: {}`, fork guard (`if: github.repository ==
'Comfy-Org/ComfyUI_frontend'`), concurrency to avoid dispatch storms
- `cloud-deploy-frontend.yaml` left unchanged (still needed during
migration)

## Setup Required

A repository secret `CLOUD_DISPATCH_TOKEN` must be configured — see PR
description comments.

## Part of

Frontend separate deploy prep (Task 1.3)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9308-feat-add-cloud-frontend-build-dispatch-workflow-3156d73d36508164a515eb968f6c5d79)
by [Unito](https://www.unito.io)
2026-02-28 17:59:19 -05:00
Alexander Brown
dd1a1f77d6 fix: stabilize nested subgraph promoted widget resolution (#9282)
## Summary

Fix multiple issues with promoted widget resolution in nested subgraphs,
ensuring correct value propagation, slot matching, and rendering for
deeply nested promoted widgets.

## Changes

- **What**: Stabilize nested subgraph promoted widget resolution chain
- Use deep source keys for promoted widget values in Vue rendering mode
- Resolve effective widget options from the source widget instead of the
promoted view
  - Stabilize slot resolution for nested promoted widgets
  - Preserve combo value rendering for promoted subgraph widgets
- Prevent subgraph definition deletion while other nodes still reference
the same type
  - Clean up unused exported resolution types

## Review Focus

- `resolveConcretePromotedWidget.ts` — new recursive resolution logic
for deeply nested promoted widgets
- `useGraphNodeManager.ts` — option extraction now uses
`effectiveWidget` for promoted widgets
- `SubgraphNode.ts` — unpack no longer force-deletes definitions
referenced by other nodes

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9282-fix-stabilize-nested-subgraph-promoted-widget-resolution-3146d73d365081208a4fe931bb7569cf)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
2026-02-28 13:45:04 -08:00
pythongosssss
0ab3fdc2c9 Add indicator circle when new unseen menu items are available (#9220)
## Summary

Adds a little indicator circle when new workflow menu items are added
that the user has not seen

## Changes

- **What**: Adds a hidden setting to track menu items flagged as new
that have been seen

## Screenshots (if applicable)

<img width="164" height="120" alt="image"
src="https://github.com/user-attachments/assets/ac36673d-fbf1-42ff-9a9e-1371eb96115b"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9220-Add-indicator-circle-when-new-unseen-menu-items-are-available-3126d73d3650819cb8cde854d6b6510b)
by [Unito](https://www.unito.io)
2026-02-28 12:53:26 -08:00
Terry Jia
ec1977131d feat: wrap CURVE widget value with typed format (#9294)
## Summary
Send CURVE values as { __type: 'CURVE', value: [...] } instead of {
__value__: [...] } to avoid ambiguity with link detection and enable
external tools to identify the data type.

change requested by @guill

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9294-feat-wrap-CURVE-widget-value-with-typed-format-3156d73d365081bf8e5de59527e2d3ce)
by [Unito](https://www.unito.io)
2026-02-28 15:00:39 -05:00
Christian Byrne
3f497081ee feat: Node Library sidebar and V2 Search dialog UI/UX updates (#9085)
## Summary

Implement 11 Figma design discrepancies for the Node Library sidebar and
V2 Node Search dialog, aligning the UI with the [Toolbox Figma
design](https://www.figma.com/design/xMFxCziXJe6Denz4dpDGTq/Toolbox?node-id=2074-21394&m=dev).

## Changes

- **What**: Sidebar: reorder tabs (All/Essentials/Blueprints), rename
Custom→Blueprints, uppercase section headers, chevron-left of folder
icon, bookmark-on-hover for node rows, filter dropdown with checkbox
items, sort labels (Categorized/A-Z) with label-left/check-right layout,
hide section headers in A-Z mode. Search dialog: expand filter chips
from 3→6, add Recents and source categories to sidebar, remove "Filter
by" label. Pull foundation V2 components from merged PR #8548.
- **Dependencies**: Depends on #8987 (V2 Node Search) and #8548
(NodeLibrarySidebarTabV2)

## Review Focus

- Filter dropdown (`filterOptions`) is UI-scaffolded but not yet wired
to filtering logic (pending V2 integration)
- "Recents" category currently returns frequency-based results as
placeholder until a usage-tracking store is implemented
- Pre-existing type errors from V2 PR dependencies not in the base
commit (SearchBoxV2, usePerTabState, TextTicker, getProviderIcon,
getLinkTypeColor, SidebarContainerKey) are expected and will resolve
when rebased onto main after parent PRs land

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9085-feat-Node-Library-sidebar-and-V2-Search-dialog-Figma-design-improvements-30f6d73d36508175bf72d716f5904476)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Yourz <crazilou@vip.qq.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-28 22:34:27 +08:00
jaeone94
a0e518aa98 refactor(node-replacement): reorganize domain components and expand comprehensive test suite (#9301)
## Summary

Resolves six open issues by reorganizing node replacement components
into a domain-driven folder structure, refactoring event handling to
follow the emit pattern, and adding comprehensive test coverage across
all affected modules.

## Changes

- **What**:
- Moved `SwapNodeGroupRow.vue` and `SwapNodesCard.vue` from
`src/components/rightSidePanel/errors/` to
`src/platform/nodeReplacement/components/` (Issues #9255)
- Moved `useMissingNodeScan.ts` from `src/composables/` to
`src/platform/nodeReplacement/missingNodeScan.ts`, renamed to reflect it
is a plain function not a Vue composable (Issues #9254)
- Refactored `SwapNodeGroupRow.vue` to emit a `'replace'` event instead
of calling `useNodeReplacement()` and `useExecutionErrorStore()`
directly; replacement logic now handled in `TabErrors.vue` (Issue #9267)
- Added unit tests for `removeMissingNodesByType`
(`executionErrorStore.test.ts`), `scanMissingNodes`
(`missingNodeScan.test.ts`), and `swapNodeGroups` computed
(`swapNodeGroups.test.ts`, `useErrorGroups.test.ts`) (Issue #9270)
- Added placeholder detection tests covering unregistered-type detection
when `has_errors` is false, and exclusion of registered types
(`useNodeReplacement.test.ts`) (Issue #9271)
- Added component tests for `MissingNodeCard` and `MissingPackGroupRow`
covering rendering, expand/collapse, events, install states, and edge
cases (Issue #9231)
- Added component tests for `SwapNodeGroupRow` and `SwapNodesCard`
(Issues #9255, #9267)

## Additional Changes (Post-Review)

- **Edge case guard in placeholder detection**
(`useNodeReplacement.ts`): When `last_serialization.type` is absent (old
serialization format), the predicate falls back to `n.type`, which the
app may have already run through `sanitizeNodeName` — stripping HTML
special characters (`& < > " ' \` =`). In that case, a `Set.has()`
lookup against the original unsanitized type name would silently miss,
causing replacement to be skipped.

Fixed by including sanitized variants of each target type in the
`targetTypes` Set at construction time. For the overwhelmingly common
case (no special characters in type names), the Set deduplicates the
entries and runtime behavior is identical to before.

A regression test was added to cover the specific scenario:
`last_serialization.type` absent + live `n.type` already sanitized.

## Review Focus

- `TabErrors.vue`: confirm the new `@replace` event handler correctly
replaces nodes and removes them from missing nodes list (mirrors the old
inline logic in `SwapNodeGroupRow`)
- `missingNodeScan.ts`: filename/export name change from
`useMissingNodeScan` — verify all call sites updated via `app.ts`
- Test mocking strategy: module-level `vi.mock()` factories use closures
over `ref`/plain objects to allow per-test overrides without global
mutable state

- Fixes #9231
- Fixes #9254
- Fixes #9255
- Fixes #9267
- Fixes #9270
- Fixes #9271
2026-02-28 06:17:30 -08:00
jaeone94
45f112e226 fix: node replacement fails after execution and modal sync (#9269)
## Summary

Fixes two bugs in the node replacement flow: placeholder detection
failing after workflow execution or pack reinstallation, and missing UI
sync in the Errors Tab when replacements are applied from the modal
dialog.

## Changes

- **Placeholder detection**: Node placeholder detection now matches
against `targetTypes` (derived from the replaceable node list built at
workflow load time) instead of relying on `has_errors` flag or
`registered_node_types` lookup. This ensures replacement works reliably
after execution (where `has_errors` gets cleared) and after pack
reinstallation (where the type becomes registered).
- **Modal → Errors Tab sync**: Added
`executionErrorStore.removeMissingNodesByType()` call in
`MissingNodesContent.vue` after replacement, so the Errors Tab reflects
changes immediately without requiring a page reload.

## Review Focus

- `collectAllNodes` predicate change in `useNodeReplacement.ts`: now
uses `targetTypes.has(originalType)` to find nodes by their original
serialized type. This is independent of runtime state like `has_errors`
or `registered_node_types`.
- `executionErrorStore.removeMissingNodesByType` call timing in
`MissingNodesContent.vue` — runs synchronously after
`replaceNodesInPlace` resolves, before auto-close logic.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9269-fix-node-replacement-fails-after-execution-and-modal-sync-3146d73d365081218398c961639b450f)
by [Unito](https://www.unito.io)
2026-02-28 04:05:58 -08:00
Comfy Org PR Bot
b80eace639 1.41.8 (#9288)
Patch version increment to 1.41.8

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9288-1-41-8-3156d73d3650817ca737ced3e08d8c86)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-28 01:07:23 -08:00
Christian Byrne
8da07f2ce2 fix: pre-rasterize SubgraphNode SVG icon to bitmap canvas (#9172)
## Summary

Pre-rasterize the SubgraphNode SVG icon to a bitmap canvas to eliminate
Firefox's per-frame SVG style processing.

## Changes

- **What**: Add `getWorkflowBitmap()` that lazily rasterizes the
`data:image/svg+xml` workflow icon to an `HTMLCanvasElement` (16×16) on
first use. `SubgraphNode.drawTitleBox()` draws the cached bitmap instead
of the raw SVG.

## Review Focus

- Firefox re-processes SVG internal stylesheets (`stroke`,
`stroke-linecap`, `stroke-width`) every time `ctx.drawImage(svgImage)`
is called. Chrome caches the rasterization. This happens on every frame
for every visible SubgraphNode.
- Reporter confirmed strong subgraph correlation: "it may be happening
in the default workflow with subgraph" / "didn't seem to happen just
using manually wired up diffusion loader, clip, sampler, etc."
- Falls back to the raw SVG Image if not yet loaded or if
`getContext('2d')` returns null.

## Stack

3 of 4 in Firefox perf fix stack. Depends on #9170.

<!-- Fixes #ISSUE_NUMBER -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9172-fix-pre-rasterize-SubgraphNode-SVG-icon-to-bitmap-canvas-3116d73d365081babf17cf0848d37269)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-02-28 01:06:54 -08:00
AustinMroz
ea5ffcc66e Fix essentials nodes not being marked core (#9287)
In adding an essentials cateogory for nodes, #8987 introduced a
regression where core nodes which are also essential are marked as being
from a `nodes` custom node instead of being marked core. Since the
essentials designation should pre-empt core and custom nodes can choose
to mark themself as essential, the getter for `isCoreNode` is updated to
instead repeat the existing check for if a node is core.

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/f1b8bf80-d072-409a-a0f9-4837e1d11767"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/14ff525b-9833-4e73-888f-791aff6cf531"/>|

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9287-Fix-essentials-nodes-not-being-marked-core-3146d73d365081fca2a0f8bdc2baf01a)
by [Unito](https://www.unito.io)
2026-02-27 16:23:08 -08:00
pythongosssss
07dab97aed App builder exit updates (#9218)
## Summary

- remove exit builder button from right panel
- add builder exit button to bottom of canvas
- add builder menu with save & exit in top left

## Screenshots (if applicable)

<img width="1544" height="998" alt="image"
src="https://github.com/user-attachments/assets/f5deadc5-2bf5-4729-b644-2b6a815b9975"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9218-App-builder-exit-updates-3126d73d365081a0bf1adf92e1171060)
by [Unito](https://www.unito.io)
2026-02-27 13:55:05 -08:00
pythongosssss
f83daa6f3b App mode - discard slow preview messages to prevent overwriting output image (#9261)
## Summary

Prevent latent previews received after the job/node has already finished
processing overwriting the actual output display

## Changes

- **What**: 
- updates job preview store to also track which node the preview was for
- updates linear progress tracking to store executed nodes enabling
skipping previews of these

## Review Focus

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

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

## Screenshots (if applicable)

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9261-App-mode-discard-slow-preview-messages-to-prevent-overwriting-output-image-3136d73d3650817884c2ce2ff5993b9e)
by [Unito](https://www.unito.io)
2026-02-27 10:58:41 -08:00
pythongosssss
c090d189f0 Render app builder in arrange mode (#9260)
## Summary

Adds app builder in arrange/preview mode with re-orderable widgets,
maintaining size (as much as possible) between the select + preview
steps

## Changes

- **What**: 
- Extract sidebar size constants for sharing between canvas splitter +
app mode
- Add widget list using DraggableList and render inert WidgetItems

## Screenshots (if applicable)

<img width="1391" height="923" alt="image"
src="https://github.com/user-attachments/assets/3e17eafe-db1e-40a3-83b5-15a7d0672909"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9260-Render-app-builder-in-arrange-mode-3136d73d365081ef875acab683d01d9e)
by [Unito](https://www.unito.io)
2026-02-27 02:32:44 -08:00
Benjamin Lu
7901e14318 fix: make docked job history toggle persistence-safe (#9265)
## Summary
Follow-up to #9215 to keep Docked Job History toggle behavior
deterministic even when settings persistence fails.

## Changes
- Close the actions popover immediately when toggling Docked Job
History.
- Use settingStore.setMany(...) when switching from docked to floating
mode.
- Set sidebarTabStore.activeSidebarTabId = 'job-history' before
persisting when switching from floating to docked mode.
- Wrap persistence calls in try/catch without rollback so
locally-applied UI state remains deterministic.
- Expand QueueOverlayHeader tests to cover setMany, popover close
behavior, and persistence-failure paths.

## Testing
- pnpm test:unit -- src/components/queue/QueueOverlayHeader.test.ts
- pnpm typecheck
- pnpm exec eslint src/components/queue/JobHistoryActionsMenu.vue
src/components/queue/QueueOverlayHeader.test.ts
- pnpm lint (fails in this branch due pre-existing stylelint errors in
generated apps/desktop-ui/dist/**/*.css files, unrelated to this change)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9265-fix-make-docked-job-history-toggle-persistence-safe-3146d73d3650818f86c4dfdd57669abd)
by [Unito](https://www.unito.io)
2026-02-26 19:49:08 -08:00
Benjamin Lu
c8b1cd9dfb fix: remove beta labeling from comfy cloud badges (#9184)
Remove the BETA label from Comfy Cloud badges while keeping the `Comfy
Cloud` text.

This updates both paths that render Comfy Cloud badge content:
- `src/extensions/core/cloudBadges.ts` (topbar extension badge path)
- `src/components/topbar/CloudBadge.vue` (reusable cloud badge used in
subscription UI)

<img width="479" height="106" alt="image"
src="https://github.com/user-attachments/assets/a73e0607-e747-4335-b09e-cf45b5016ff5"
/>

Reasoning:

https://comfy-organization.slack.com/archives/C08V9NDB3B4/p1771981530055409?thread_ts=1771978875.127499&cid=C08V9NDB3B4

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9184-fix-remove-beta-labeling-from-comfy-cloud-badges-3126d73d365081e993aac651993010e7)
by [Unito](https://www.unito.io)
2026-02-26 19:48:55 -08:00
Comfy Org PR Bot
5e99faadfe 1.41.7 (#9264)
Patch version increment to 1.41.7

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9264-1-41-7-3146d73d36508108a0d1c3e418216cd0)
by [Unito](https://www.unito.io)

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-26 19:31:49 -08:00
Benjamin Lu
f495f07469 fix: move active jobs button into actionbar (#9211)
## Summary

Move the top menu `N active` queue button into `ComfyActionbar` so it
stays attached to the actionbar when docked, dragged, or floating.

## Changes

- Moved the `queue-overlay-toggle` button UI from `TopMenuSection.vue`
into `ComfyActionbar.vue`
- Moved queue button behavior and context menu handling (`toggle`,
right-click clear queue, active count badge/label) into
`ComfyActionbar.vue`
- Removed now-unused queue button state/handlers/imports from
`TopMenuSection.vue`

<img width="513" height="101" alt="image"
src="https://github.com/user-attachments/assets/6ed85237-4293-47a6-a0fb-258d0182889d"
/>

<img width="778" height="145" alt="image"
src="https://github.com/user-attachments/assets/6e5ef423-c5fc-471f-b9d0-a4bd8dc5d072"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9211-fix-move-active-jobs-button-into-actionbar-3126d73d365081ceb553c172db479e3b)
by [Unito](https://www.unito.io)
2026-02-26 19:00:10 -08:00
Christian Byrne
1054ba8949 fix: batch updateClipPath via requestAnimationFrame (#9173)
## Summary

Batch `getBoundingClientRect()` calls in `updateClipPath` via
`requestAnimationFrame` to avoid forced synchronous layout.

## Changes

- **What**: Wrap the layout-reading portion of `updateClipPath` in
`requestAnimationFrame()` with cancellation. Multiple rapid calls within
the same frame are coalesced into a single layout read. Eliminates
~1,053 forced synchronous layouts per profiling session.

## Review Focus

- `getBoundingClientRect()` forces synchronous layout. When interleaved
with style mutations (from PrimeVue `useStyle`, cursor writes, Vue VDOM
patching), this creates layout thrashing — especially in Firefox where
Stylo aggressively invalidates the entire style cache.
- The RAF wrapper coalesces all calls within a frame into one, reading
layout only once per frame. The `cancelAnimationFrame` ensures only the
latest parameters are used.
- `willChange: 'clip-path'` is included to hint the browser to optimize
clip-path animations.

## Stack

4 of 4 in Firefox perf fix stack. Depends on #9170.

<!-- Fixes #ISSUE_NUMBER -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9173-fix-batch-updateClipPath-via-requestAnimationFrame-3116d73d3650810392f7fba7ea5ceb6f)
by [Unito](https://www.unito.io)
2026-02-26 18:53:14 -08:00
Christian Byrne
0698ec23c0 feat: wire essentials_category for Essentials tab display (#9091)
## Summary

Wire `essentials_category` through from backend to the Essentials tab
UI. Creates a single source of truth for node categorization and
ordering.

### Changes

**New file — `src/constants/essentialsNodes.ts`:**
- Single source of truth: `ESSENTIALS_NODES` (ordered nodes per
category), `ESSENTIALS_CATEGORIES` (folder display order),
`ESSENTIALS_CATEGORY_MAP` (flat lookup), `TOOLKIT_NOVEL_NODE_NAMES`
(telemetry), `TOOLKIT_BLUEPRINT_MODULES`

**Refactored files:**
- `src/types/nodeSource.ts`: Removed inline `ESSENTIALS_CATEGORY_MOCK`,
imports `ESSENTIALS_CATEGORY_MAP` from centralized constants
- `src/services/nodeOrganizationService.ts`: Removed inline
`NODE_ORDER_BY_FOLDER`, imports `ESSENTIALS_NODES` and
`ESSENTIALS_CATEGORIES`
- `src/constants/toolkitNodes.ts`: Re-exports from `essentialsNodes.ts`
instead of maintaining a separate list

**Subgraph passthrough:**
- `src/stores/subgraphStore.ts`: Passes `essentials_category` from
`GlobalSubgraphData` and extracts it from `definitions.subgraphs[0]` as
fallback
- `src/platform/workflow/validation/schemas/workflowSchema.ts`: Added
`essentials_category` to `SubgraphDefinitionBase` and
`zSubgraphDefinition`

**Tests:**
- `src/constants/essentialsNodes.test.ts`: 6 tests validating no
duplicates, complete coverage, basics exclusion
- `src/stores/subgraphStore.test.ts`: 2 tests for essentials_category
passthrough

All 43 relevant tests pass. Typecheck, lint, format clean.

**Depends on:** Comfy-Org/ComfyUI#12573

Fixes COM-15221

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9091-feat-wire-essentials_category-for-Essentials-tab-display-30f6d73d3650814ab3d4c06b451c273b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-26 18:40:15 -08:00
Johnpaul Chiwetelu
54b710b239 [refactor] Rename queueIndex variables to reflect job.priority usage (#9258)
## Summary
Rename `lastHistoryQueueIndex` → `lastJobHistoryPriority` and
`currentQueueIndex` → `currentJobPriority` to reflect that these
variables now read `job.priority` directly.

## Changes
- **queueStore.ts**: `lastHistoryQueueIndex` → `lastJobHistoryPriority`
- **JobDetailsPopover.vue**: `currentQueueIndex` → `currentJobPriority`
- **queueStore.test.ts**: Updated references and test descriptions

Fixes #9246

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9258-refactor-Rename-queueIndex-variables-to-reflect-job-priority-usage-3136d73d36508126989dd464f7dad9a1)
by [Unito](https://www.unito.io)
2026-02-26 18:31:27 -08:00
jaeone94
1c3984a178 feat: add node replacement UI to Errors Tab (#9253)
## Summary

Adds a node replacement UI to the Errors Tab so users can swap missing
nodes with compatible alternatives directly from the error panel,
without opening a separate dialog.

## Changes

- **What**: New `SwapNodesCard` and `SwapNodeGroupRow` components render
swap groups in the Errors Tab; each group shows the missing node type,
its instances (with locate buttons), and a Replace button. Added
`useMissingNodeScan` composable to scan the graph for missing nodes and
populate `executionErrorStore`. Added `removeMissingNodesByType()` to
`executionErrorStore` so replaced nodes are pruned from the error list
reactively.

## Bug Fixes Found During Implementation

### Bug 1: Replaced nodes render as empty shells until page refresh

`replaceWithMapping()` directly mutates `_nodes[idx]`, bypassing the Vue
rendering pipeline entirely. Because the replacement node reuses the
same ID, `vueNodeData` retains the stale entry from the old placeholder
(`hasErrors: true`, empty widgets/inputs). `graph.setDirtyCanvas()` only
repaints the LiteGraph canvas and has no effect on Vue.

**Fix**: After `replaceWithMapping()`, manually call
`nodeGraph.onNodeAdded?.(newNode)` to trigger `handleNodeAdded` in
`useGraphNodeManager`, which runs `extractVueNodeData(newNode)` and
updates `vueNodeData` correctly. Also added a guard in `handleNodeAdded`
to skip `layoutStore.createNode()` when a layout for the same ID already
exists, preventing a duplicate `spatialIndex.insert()`.

### Bug 2: Missing node error list overwritten by incomplete server
response

Two compounding issues: (A) the server's `missing_node_type` error only
reports the *first* missing node — the old handler parsed this and
called `surfaceMissingNodes([singleNode])`, overwriting the full list
collected at load time. (B) `queuePrompt()` calls `clearAllErrors()`
before the API request; if the subsequent rescan used the stale
`has_errors` flag and found nothing, the missing nodes were permanently
lost.

**Fix**: Created `useMissingNodeScan.ts` which scans
`LiteGraph.registered_node_types` directly (not `has_errors`). The
`missing_node_type` catch block in `app.ts` now calls
`rescanAndSurfaceMissingNodes(this.rootGraph)` instead of parsing the
server's partial response.

## Review Focus

- `handleReplaceNode` removes the group from the store only when
`replaceNodesInPlace` returns at least one replaced node — should we
always clear, or only on full success?
- `useMissingNodeScan` re-scans on every execution-error change; confirm
no performance concerns for large graphs with many subgraphs.


## Screenshots 


https://github.com/user-attachments/assets/78310fc4-0424-4920-b369-cef60a123d50



https://github.com/user-attachments/assets/3d2fd5e1-5e85-4c20-86aa-8bf920e86987



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9253-feat-add-node-replacement-UI-to-Errors-Tab-3136d73d365081718d4ddfd628cb4449)
by [Unito](https://www.unito.io)
2026-02-26 17:37:48 -08:00
Benjamin Lu
367d96715b fix: open target panel when toggling Docked Job History (#9215)
## Summary
Make the Docked Job History toggle deterministic so it opens the
expected UI target in both directions.

## Changes
- Update `JobHistoryActionsMenu` toggle behavior:
- When currently docked (`Comfy.Queue.QPOV2=true`), disable docked mode
and explicitly open floating QPO (`Comfy.Queue.History.Expanded=true`)
- When currently floating (`Comfy.Queue.QPOV2=false`), enable docked
mode and open the `job-history` sidebar tab
- Add/adjust unit tests in `QueueOverlayHeader.test.ts` to verify both
toggle directions and target panel behavior

## Testing
- `pnpm exec eslint src/components/queue/JobHistoryActionsMenu.vue
src/components/queue/QueueOverlayHeader.test.ts`
- `pnpm typecheck`
- `pnpm test:unit -- src/components/queue/QueueOverlayHeader.test.ts`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9215-fix-open-target-panel-when-toggling-Docked-Job-History-3126d73d3650810eb409ff38e3a521f3)
by [Unito](https://www.unito.io)
2026-02-26 16:21:31 -08:00
Benjamin Lu
84fdf55902 fix: set queue job filter tabs to 32px (#9217)
## Summary
- Increase queue job filter tab button height from `sm` to `md` (`32px`
equivalent).
- Apply consistently to both floating Queue Progress Overlay and docked
Job History sidebar, since both use `JobFilterTabs`.

## Design
-
https://www.figma.com/board/R9eN9DHmDgX3qEJXsRKiRr/QA-Feedback--Alex-?node-id=273-111&t=OUCBdoZhwrOMsxXE-4

## Testing
- `pnpm typecheck`
- `pnpm lint` (passes with existing repo warnings only)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9217-fix-set-queue-job-filter-tabs-to-32px-3126d73d36508106a9c8e3786ab77aa5)
by [Unito](https://www.unito.io)
2026-02-26 16:09:26 -08:00
pythongosssss
9fb93a5b0a App mode - more updates & fixes (#9137)
## Summary

- fix sizing of sidebars in app mode
- update feedback button to match design
- update job queue notification
- clickable queue spinner item to allow clear queue
- refactor mode out of store to specific workflow instance
- support different saved vs active mode
- other styling/layout tweaks

## Changes

- **What**: Changes the store to a composable and moves the mode state
to the workflow.
- This enables switching between tabs and maintaining the mode they were
in

## Screenshots (if applicable)
<img width="1866" height="1455" alt="image"
src="https://github.com/user-attachments/assets/f9a8cd36-181f-4948-b48c-dd27bd9127cf"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9137-App-mode-more-updates-fixes-3106d73d365081a18ccff6ffe24fdec7)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-26 09:55:10 -08:00
Benjamin Lu
ac12a3d9b9 fix: preserve refill date slashes in subscription credits label (#9251)
### Motivation
- Subscription credit labels were rendering the refill date with
HTML-escaped separators (`&#x2F;`) because `vue-i18n` parameter escaping
was applied to the date interpolation.
- The goal is to render date-only parameters like `MM/DD/YY` with
literal slashes so the UI shows a human-readable date string.

### Description
- Disabled `vue-i18n` parameter escaping for the
`subscription.creditsRemainingThisMonth` and
`subscription.creditsRemainingThisYear` lookups in both subscription
panels by passing `{ escapeParameter: false }` to `t()` in
`SubscriptionPanelContentLegacy.vue` and
`SubscriptionPanelContentWorkspace.vue`.
- Adjusted the unit test i18n setup in `SubscriptionPanel.test.ts` to
include `escapeParameter: true` in the test `i18n` instance and updated
the test messages to use `Included (Refills {date})`.
- Added a regression unit test in `SubscriptionPanel.test.ts` asserting
the rendered label contains `Included (Refills 12/31/24)` and does not
contain the escaped entity `&#x2F;`.

### Testing
- Ran formatting with `pnpm format` which completed successfully.
- Ran lint via `pnpm lint` which passed with pre-existing warnings only
(no new errors).
- Ran type checking with `pnpm typecheck` (via `vue-tsc --noEmit`) which
completed successfully.
- Ran the modified unit tests with `pnpm vitest run
src/platform/cloud/subscription/components/SubscriptionPanel.test.ts`
and the test file passed (10 passed, 5 skipped).
- Attempted a Playwright-based visual capture of the running app but
Chromium crashed in this environment (SIGSEGV) before navigation, so no
screenshot was produced.

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_e_69a0175f58788330b2256329a500e14b)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9251-fix-preserve-refill-date-slashes-in-subscription-credits-label-3136d73d36508182b770f5719a52d189)
by [Unito](https://www.unito.io)
2026-02-26 09:37:03 -08:00
928 changed files with 39855 additions and 7455 deletions

View File

@@ -0,0 +1,28 @@
---
name: accessibility
description: Reviews UI code for WCAG 2.2 AA accessibility violations
severity-default: medium
tools: [Read, Grep]
---
You are an accessibility auditor reviewing a code diff for WCAG 2.2 AA compliance. Focus on UI changes that affect users with disabilities.
Check for:
1. **Missing form labels** - inputs, selects, textareas without associated `<label>` or `aria-label`/`aria-labelledby`
2. **Missing alt text** - images without `alt` attributes, or decorative images missing `alt=""`
3. **Keyboard navigation** - interactive elements not focusable, custom widgets missing keyboard handlers (Enter, Space, Escape, Arrow keys), focus traps without escape
4. **Focus management** - modals/dialogs that don't trap focus, dynamic content that doesn't move focus appropriately, removed elements without focus recovery
5. **ARIA misuse** - invalid `aria-*` attributes, roles without required children/properties, `aria-hidden` on focusable elements
6. **Color as sole indicator** - using color alone to convey meaning (errors, status) without text/icon alternative
7. **Touch targets** - interactive elements smaller than 24x24 CSS pixels (WCAG 2.2 SC 2.5.8)
8. **Screen reader support** - dynamic content changes without `aria-live` announcements, unlabeled icon buttons, links with only "click here"
9. **Heading hierarchy** - skipped heading levels (h1 → h3), missing page landmarks
Rules:
- Focus on NEW or CHANGED UI in the diff — do not audit the entire existing codebase
- Only flag issues in .vue, .tsx, .jsx, .html, or template-containing files
- Skip non-UI files entirely (stores, services, utils)
- Skip canvas-based content: the LiteGraph node editor renders on `<canvas>` elements, not DOM-based UI. WCAG rules don't fully apply to canvas rendering internals — only audit the DOM-based controls around it (toolbars, panels, dialogs)
- "Critical" for completely inaccessible interactive elements, "major" for missing labels/ARIA, "minor" for enhancement opportunities

View File

@@ -0,0 +1,35 @@
---
name: api-contract
description: Catches breaking changes to public interfaces, window-exposed APIs, event contracts, and exported symbols
severity-default: high
tools: [Grep, Read, glob]
---
You are an API contract reviewer. Your job is to catch breaking changes and contract violations in public-facing interfaces.
## What to Check
1. **Breaking changes to globally exposed APIs** — anything on `window` or other global objects that consumers depend on. Renamed properties, removed methods, changed signatures, changed return types.
2. **Event contract changes** — renamed events, changed event payloads, removed events that listeners may depend on.
3. **Changed function signatures** — parameters reordered, required params added, return type changed on exported functions.
4. **Removed or renamed exports** — any `export` that was previously available and is now gone or renamed without a re-export alias.
5. **REST API changes** — changed endpoints, added required fields, removed response fields, changed status codes.
6. **Type contract narrowing** — a function that used to accept `string | number` now only accepts `string`, or a return type that narrows unexpectedly.
7. **Default value changes** — changing defaults on optional parameters that consumers may rely on.
8. **Store/state shape changes** — renamed store properties, changed state structure that computed properties or watchers may depend on.
## How to Identify the Public API
- Check `package.json` for `"exports"` or `"main"` fields.
- **Window globals**: This repo exposes LiteGraph classes on `window` (e.g., `window['LiteGraph']`, `window['LGraphNode']`, `window['LGraphCanvas']`) and `window['__COMFYUI_FRONTEND_VERSION__']`. These are consumed by custom node extensions and must not be renamed or removed.
- **Extension hooks**: The `app` object and its extension registration system (`app.registerExtension`) is a public contract for third-party custom nodes. Changes to `ComfyApp`, `ComfyApi`, or the extension lifecycle are breaking changes.
- Check AGENTS.md for project-specific API surface definitions.
- Any exported symbol from common entry points (e.g., `src/types/index.ts`) should be treated as potentially public.
## Rules
- ONLY flag changes that break existing consumers.
- Do NOT flag additions (new methods, new exports, new endpoints).
- Do NOT flag internal/private API changes.
- Always check if a re-export or compatibility shim was added before flagging.
- Critical for removed/renamed globals, high for changed export signatures, medium for changed defaults.

View File

@@ -0,0 +1,27 @@
---
name: architecture-reviewer
description: Reviews code for architectural issues like over-engineering, SOLID violations, coupling, and API design
severity-default: medium
tools: [Grep, Read, glob]
---
You are a software architect reviewing a code diff. Focus on structural and design issues.
## What to Check
1. **Over-engineering** — abstractions for single-use cases, premature generalization, unnecessary indirection layers
2. **SOLID violations** — god classes, mixed responsibilities, rigid coupling, interface segregation issues
3. **Separation of concerns** — business logic in UI components, data access in controllers, mixed layers
4. **API design** — inconsistent interfaces, leaky abstractions, unclear contracts
5. **Coupling** — tight coupling between modules, circular dependencies, feature envy
6. **Consistency** — breaking established patterns without justification, inconsistent approaches to similar problems
7. **Dependency direction** — imports going the wrong way in the architecture, lower layers depending on higher
8. **Change amplification** — designs requiring changes in many places for simple feature additions
## Rules
- Focus on structural issues that affect maintainability and evolution.
- Do NOT report bugs, security, or performance issues (other checks handle those).
- Consider whether the code is proportional to the problem it solves.
- "Under-engineering" (missing useful abstractions) is as valid as over-engineering.
- Rate severity by impact on future maintainability.

View File

@@ -0,0 +1,34 @@
---
name: bug-hunter
description: Finds logic errors, off-by-ones, null safety issues, race conditions, and edge cases
severity-default: high
tools: [Read, Grep]
---
You are a bug hunter reviewing a code diff. Your ONLY job is to find bugs - logic errors that will cause incorrect behavior at runtime.
Focus areas:
1. **Off-by-one errors** in loops, slices, and indices
2. **Null/undefined dereferences** - any path where a value could be null but isn't checked
3. **Race conditions** - shared mutable state, async ordering assumptions
4. **Edge cases** - empty arrays, zero values, empty strings, boundary conditions
5. **Type coercion bugs** - loose equality, implicit conversions
6. **Error handling gaps** - unhandled promise rejections, swallowed errors
7. **State mutation bugs** - mutating props, shared references, stale closures
8. **Incorrect boolean logic** - flipped conditions, missing negation, wrong operator precedence
Rules:
- ONLY report actual bugs that will cause wrong behavior
- Do NOT report style issues, naming, or performance
- Do NOT report hypothetical bugs that require implausible inputs
- Each finding must explain the specific runtime failure scenario
## Repo-Specific Bug Patterns
- `z.any()` in Zod schemas disables validation and propagates `any` into TypeScript types — always flag
- Destructuring reactive objects (props, reactive()) without `toRefs()` loses reactivity — flag outside of `defineProps` destructuring
- `ComputedRef<T>` exposed via `defineExpose` or public API should be unwrapped first
- LiteGraph node operations: check for missing null guards on `node.graph` (can be null when node is removed)
- Watch/watchEffect without cleanup for side effects (timers, listeners) — leak on component unmount

View File

@@ -0,0 +1,50 @@
---
name: coderabbit
description: Runs CodeRabbit CLI for AST-aware code quality review
severity-default: medium
tools: [Bash, Read]
---
Run CodeRabbit CLI review on the current changes.
## Steps
1. Check if CodeRabbit CLI is installed:
```bash
which coderabbit
```
If not installed, skip this check and report:
"Skipped: CodeRabbit CLI not installed. Install and authenticate:
```
npm install -g coderabbit
coderabbit auth login
```
See https://docs.coderabbit.ai/guides/cli for setup."
2. Run review:
```bash
coderabbit --prompt-only --type uncommitted
```
If there are committed but unpushed changes, use `--type committed` instead.
3. Parse CodeRabbit's output. Each finding should include:
- File path and line number
- Severity mapped from CodeRabbit's own levels
- Category (logic, security, performance, style, test, architecture, dx)
- Description and suggested fix
## Rate Limiting
If a rate limit is hit, skip and note it. Prefer reading the current quota from CLI/API output rather than assuming a fixed reviews/hour limit.
## Error Handling
- Auth expired: skip and report "CodeRabbit auth expired, run: coderabbit auth login"
- CLI timeout (>120s): skip and note
- Parse error: return raw output with a warning

View File

@@ -0,0 +1,28 @@
---
name: complexity
description: Reviews code for excessive complexity and suggests refactoring opportunities
severity-default: medium
tools: [Grep, Read, glob]
---
You are a complexity and refactoring advisor reviewing a code diff. Focus on code that is unnecessarily complex and will be hard to maintain.
## What to Check
1. **High cyclomatic complexity** — functions with many branching paths (if/else chains, switch statements with >7 cases, nested ternaries). Threshold: complexity >10 is high severity, >15 is critical.
2. **Deep nesting** — code nested >4 levels deep (nested if/for/try blocks). Suggest guard clauses, early returns, or extraction.
3. **Oversized functions** — functions >50 lines that do multiple things. Suggest extraction of cohesive sub-functions.
4. **God classes/modules** — files >500 lines mixing multiple responsibilities. Suggest splitting by concern.
5. **Long parameter lists** — functions with >5 parameters. Suggest parameter objects or builder patterns.
6. **Complex boolean expressions** — conditions with >3 clauses that are hard to parse. Suggest extracting to named boolean variables.
7. **Feature envy** — methods that use data from another class more than their own, suggesting the method belongs elsewhere.
8. **Duplicate logic** — two or more code blocks in the diff doing essentially the same thing with minor variations.
9. **Unnecessary indirection** — wrapper functions that add no value, abstractions for single-use cases, premature generalization.
## Rules
- Only flag complexity in NEW or SIGNIFICANTLY CHANGED code.
- Do NOT suggest refactoring stable, well-tested code that happens to be complex.
- Do NOT flag complexity that is inherent to the problem domain (e.g., state machines, protocol handlers).
- Provide a concrete refactoring approach, not just "this is too complex".
- High severity for code that will likely cause bugs during future modifications, medium for readability improvements, low for optional simplifications.

View File

@@ -0,0 +1,86 @@
---
name: ddd-structure
description: Reviews whether new code is placed in the right domain/layer and follows domain-driven structure principles
severity-default: medium
tools: [Grep, Read, glob]
---
You are a domain-driven design reviewer. Your job is to check whether new or moved code is placed in the correct architectural layer and domain folder.
## Principles
1. **Domain over Technical Layer** — code should be organized by what it does (domain/feature), not by what it is (component/service/store). New files in flat technical folders like `src/components/`, `src/services/`, `src/stores/`, `src/utils/` are a smell if the repo already has domain folders.
2. **Cohesion** — files that change together should live together. A component, its store, its service, and its types for a single feature should be co-located.
3. **Import Direction** — lower layers must not import from higher layers. Check that imports flow in the allowed direction (see Layer Architecture below).
4. **Bounded Contexts** — each domain/feature should have clear boundaries. Cross-domain imports should go through public interfaces, not reach into internal files.
5. **Naming** — folders and files should reflect domain concepts, not technical roles. `workflowExecution.ts` > `service.ts`.
## Layer Architecture
This repo uses a VSCode-style layered architecture with strict unidirectional imports:
```
base → platform → workbench → renderer
```
| Layer | Purpose | Can Import From |
| ------------ | -------------------------------------- | ---------------------------------- |
| `base/` | Pure utilities, no framework deps | Nothing |
| `platform/` | Core domain services, business logic | `base/` |
| `workbench/` | Features, workspace orchestration | `base/`, `platform/` |
| `renderer/` | UI layer (Vue components, composables) | `base/`, `platform/`, `workbench/` |
### Import Direction Violations to Check
```bash
# platform must NOT import from workbench or renderer
grep -r "from '@/renderer'" src/platform/ --include="*.ts" --include="*.vue"
grep -r "from '@/workbench'" src/platform/ --include="*.ts" --include="*.vue"
# base must NOT import from platform, workbench, or renderer
grep -r "from '@/platform'" src/base/ --include="*.ts" --include="*.vue"
grep -r "from '@/workbench'" src/base/ --include="*.ts" --include="*.vue"
grep -r "from '@/renderer'" src/base/ --include="*.ts" --include="*.vue"
# workbench must NOT import from renderer
grep -r "from '@/renderer'" src/workbench/ --include="*.ts" --include="*.vue"
```
### Legacy Flat Folders
Flag NEW files added to these legacy flat folders (they should go in a domain folder under the appropriate layer instead):
- `src/components/` → should be in `src/renderer/` or `src/workbench/extensions/{feature}/components/`
- `src/stores/` → should be in `src/platform/{domain}/` or `src/workbench/extensions/{feature}/stores/`
- `src/services/` → should be in `src/platform/{domain}/`
- `src/composables/` → should be in `src/renderer/` or `src/platform/{domain}/ui/`
Do NOT flag modifications to existing files in legacy folders — only flag NEW files.
## How to Review
1. Look at the diff to see where new files are created or where code is added.
2. Check if the repo has an established domain folder structure (look for domain-organized directories like `src/platform/`, `src/workbench/`, `src/renderer/`, `src/base/`, or similar).
3. If domain folders exist but new code was placed in a flat technical folder, flag it.
4. Run import direction checks:
- Use `Grep` or `Read` to check if new imports violate layer boundaries.
- Flag any imports from a higher layer to a lower one using the rules above.
5. Check for new files in legacy flat folders and flag them per the Legacy Flat Folders section.
## Generic Checks (when no domain structure is detected)
- God files (>500 lines mixing concerns)
- Circular imports between modules
- Business logic in UI components
## Severity Guidelines
| Issue | Severity |
| ------------------------------------------------------------- | -------- |
| Import direction violation (lower layer imports higher layer) | high |
| New file in legacy flat folder when domain folders exist | medium |
| Business logic in UI component | medium |
| Missing domain boundary (cross-cutting import into internals) | low |
| Naming uses technical role instead of domain concept | low |

View File

@@ -0,0 +1,66 @@
---
name: dep-secrets-scan
description: Runs dependency vulnerability audit and secrets detection
severity-default: critical
tools: [Bash, Read]
---
Run dependency audit and secrets scan to detect known CVEs in dependencies and leaked secrets in code.
## Steps
1. Check which tools are available:
```bash
pnpm --version
gitleaks version
```
- If **neither** is installed, skip this check and report: "Skipped: neither pnpm nor gitleaks installed. Install pnpm: `npm i -g pnpm`. Install gitleaks: `brew install gitleaks` or see https://github.com/gitleaks/gitleaks#installing"
- If only one is available, run that one and note the other was skipped.
2. **Dependency audit** (if pnpm is available):
```bash
pnpm audit --json 2>/dev/null || true
```
Parse the JSON output. Map advisory severity:
- `critical` advisory → `critical`
- `high` advisory → `major`
- `moderate` advisory → `minor`
- `low` advisory → `nitpick`
Report each finding with: package name, version, advisory title, CVE, and suggested patched version.
3. **Secrets detection** (if gitleaks is available):
```bash
gitleaks detect --no-banner --report-format json --source . 2>/dev/null || true
```
Parse the JSON output. All secret findings are `critical` severity.
Report each finding with: file and line, rule description, and a redacted match. Always suggest removing the secret and rotating credentials.
## What This Catches
### Dependency Audit
- Known CVEs in direct and transitive dependencies
- Vulnerable packages from the npm advisory database
### Secrets Detection
- API keys and tokens in code
- AWS credentials, GCP service account keys
- Database connection strings with passwords
- Private keys and certificates
- Generic high-entropy secrets
## Error Handling
- If pnpm audit fails, log the error and continue with gitleaks.
- If gitleaks fails, log the error and continue with audit results.
- If JSON parsing fails for either tool, include raw output with a warning.
- If both tools produce no findings, report "No issues found."

View File

@@ -0,0 +1,40 @@
---
name: doc-freshness
description: Reviews whether code changes are reflected in documentation
severity-default: medium
tools: [Read, Grep]
---
You are a documentation freshness reviewer. Your job is to check whether code changes are properly reflected in documentation, and whether new features need documentation.
Check for:
1. **Stale README sections** - code changes that invalidate setup instructions, API examples, or architecture descriptions in README.md
2. **Outdated code comments** - comments referencing removed functions, old parameter names, previous behavior, or TODO items that are now done
3. **Missing JSDoc on public APIs** - exported functions, classes, or interfaces without JSDoc descriptions, especially those used by consumers of the library
4. **Changed behavior without changelog** - user-facing behavior changes that should be noted in a changelog or release notes
5. **Dead documentation links** - links in markdown files pointing to moved or deleted files
6. **Missing migration guidance** - breaking changes without upgrade instructions
Rules:
- Focus on documentation that needs to CHANGE due to the diff — don't audit all existing docs
- Do NOT flag missing comments on internal/private functions
- Do NOT flag missing changelog entries for purely internal refactors
- "Major" for stale docs that will mislead users, "minor" for missing JSDoc on public APIs, "nitpick" for minor doc improvements
## ComfyUI_frontend Documentation
This repository's public APIs are used by custom node and extension authors. Documentation lives at [docs.comfy.org](https://docs.comfy.org) (repo: Comfy-Org/docs).
For any NEW API, event, hook, or configuration that extensions or custom nodes can use:
- Flag with a suggestion to open a PR to Comfy-Org/docs to document the new API
- Example: "This new extension API should be documented at docs.comfy.org — consider opening a PR to Comfy-Org/docs"
For changes to existing extension-facing APIs:
- Check if the existing docs at docs.comfy.org may need updating
- Flag stale references in CONTRIBUTING.md or developer guides
Anything relevant to custom extension authors should trigger a documentation suggestion.

View File

@@ -0,0 +1,25 @@
---
name: dx-readability
description: Reviews code for developer experience issues including naming clarity, cognitive complexity, dead code, and confusing patterns
severity-default: low
tools: [Read, Grep]
---
You are a developer experience reviewer. Focus on code that will confuse the next developer who reads it.
Check for:
1. **Unclear naming** - variables/functions that don't communicate intent, abbreviations, misleading names
2. **Cognitive complexity** - deeply nested conditions, long functions doing multiple things, complex boolean expressions
3. **Dead code** - unreachable branches, unused variables, commented-out code, vestigial parameters
4. **Confusing patterns** - clever tricks over simple code, implicit behavior, action-at-a-distance, surprising side effects
5. **Missing context** - complex business logic without explaining why, non-obvious algorithms without comments
6. **Inconsistent abstractions** - mixing raw and wrapped APIs, different error handling styles in same module
7. **Implicit knowledge** - code that only works because of undocumented assumptions or conventions
Rules:
- Only flag things that would genuinely confuse a competent developer
- Do NOT flag established project conventions even if you'd prefer different ones
- "Minor" for things that slow comprehension, "nitpick" for pure style preferences
- Major is reserved for genuinely misleading code (names that lie, silent behavior changes)

View File

@@ -0,0 +1,58 @@
---
name: ecosystem-compat
description: Checks whether changes break exported symbols that downstream consumers may depend on
severity-default: high
tools: [Grep, Read, glob, mcp__comfy_codesearch__search_code]
---
Check whether this PR introduces breaking changes to exported symbols that downstream consumers may depend on.
## What to Check
- Renamed or removed exported functions/classes/types
- Changed function signatures (parameters added/removed/reordered)
- Changed return types
- Removed or renamed CSS classes used for selectors
- Changed event names or event payload shapes
- Changed global registrations or extension hooks
- Modified integration points with external systems
## Method
1. Read the diff and identify any changes to exported symbols.
2. For each potentially breaking change, try to determine if downstream consumers exist:
- If `mcp__comfy_codesearch__search_code` is available, search for usages of the changed symbol across downstream repositories.
- Otherwise, use `Grep` to search for usages within the current repository and note that external usage could not be verified.
3. If consumers are found using the changed API, report it as a finding.
## Severity Guidelines
| Ecosystem Usage | Severity | Guidance |
| --------------- | -------- | ------------------------------------------------------------ |
| 5+ consumers | critical | Must address before merge |
| 2-4 consumers | high | Should address or document |
| 1 consumer | medium | Note in PR, author decides |
| 0 consumers | low | Note potential risk only |
| Unknown usage | medium | Require explicit note that external usage was not verifiable |
## Suggestion Template
When a breaking change is found, suggest:
- Keeping the old export alongside the new one
- Adding a deprecation wrapper
- Explicitly noting this as a breaking change in the PR description so consumers can update
## ComfyUI Code Search MCP
This check works best with the ComfyUI code search MCP tool, which searches across all custom node repositories for usage of changed symbols.
If the `mcp__comfy_codesearch__search_code` tool is not available, install it:
```
amp mcp add comfy-codesearch https://comfy-codesearch.vercel.app/api/mcp
# OR for Claude Code:
claude mcp add -t http comfy-codesearch https://comfy-codesearch.vercel.app/api/mcp
```
Without this MCP, the check will fall back to searching within the current repository only, and cannot verify external ecosystem usage.

View File

@@ -0,0 +1,38 @@
---
name: error-handling
description: Reviews error handling patterns for empty catches, swallowed errors, missing async error handling, and generic error UX
severity-default: high
tools: [Read, Grep]
---
You are an error handling auditor reviewing a code diff. Focus exclusively on how errors are handled, propagated, and surfaced.
Check for:
1. **Empty catch blocks** - errors caught and silently swallowed with no logging or re-throw
2. **Generic catches** - catching all errors without distinguishing types, losing context
3. **Missing async error handling** - unhandled promise rejections, async functions without try/catch or .catch()
4. **Swallowed errors** - errors caught and replaced with a return value that hides the failure
5. **Missing error boundaries** - Vue/React component trees without error boundaries around risky subtrees
6. **No retry or fallback** - network calls, file I/O, or external service calls with no retry logic or graceful degradation
7. **Generic error UX** - user-facing code showing "Something went wrong" without actionable guidance or error codes
8. **Missing cleanup on error** - resources (connections, file handles, timers) not cleaned up in error paths
9. **Error propagation breaks** - catching errors mid-chain and not re-throwing, breaking caller's ability to handle
Rules:
- Focus on NEW or CHANGED error handling in the diff
- Do NOT flag existing error handling patterns in untouched code
- Do NOT suggest adding error handling to code that legitimately cannot fail (pure functions, type-safe internal calls)
- "Critical" for swallowed errors in data-mutation paths, "major" for missing error handling on external calls, "minor" for missing logging
## Repo-Specific Error Handling
- User-facing error messages must be actionable and friendly (per AGENTS.md)
- Use the shared `useErrorHandling` composable (`src/composables/useErrorHandling.ts`) for centralized error handling:
- `wrapWithErrorHandling` / `wrapWithErrorHandlingAsync` automatically catch errors and surface them as toast notifications via `useToastStore`
- `toastErrorHandler` can be used directly for custom error handling flows
- Supports `ErrorRecoveryStrategy` for retry/fallback patterns (e.g., reauthentication, network reconnect)
- API errors from `api.get()`/`api.post()` should be caught and surfaced to the user via `useToastStore` (`src/platform/updates/common/toastStore.ts`)
- Electron/desktop code paths: IPC errors should be caught and not crash the renderer process
- Workflow execution errors should be displayed in the UI status bar, not silently swallowed

View File

@@ -0,0 +1,60 @@
/**
* Strict ESLint config for the sonarjs-lint review check.
*
* Uses eslint-plugin-sonarjs to get SonarQube-grade analysis without a server.
* This config is NOT used for regular development linting — only for the
* code review checks' static analysis pass.
*
* Install: pnpm add -D eslint eslint-plugin-sonarjs
* Run: pnpm dlx eslint --no-config-lookup --config .agents/checks/eslint.strict.config.js --ext .ts,.js,.vue {files}
*/
import sonarjs from 'eslint-plugin-sonarjs'
export default [
sonarjs.configs.recommended,
{
plugins: {
sonarjs
},
rules: {
// Bug detection
'sonarjs/no-all-duplicated-branches': 'error',
'sonarjs/no-element-overwrite': 'error',
'sonarjs/no-identical-conditions': 'error',
'sonarjs/no-identical-expressions': 'error',
'sonarjs/no-one-iteration-loop': 'error',
'sonarjs/no-use-of-empty-return-value': 'error',
'sonarjs/no-collection-size-mischeck': 'error',
'sonarjs/no-duplicated-branches': 'error',
'sonarjs/no-identical-functions': 'error',
'sonarjs/no-redundant-jump': 'error',
'sonarjs/no-unused-collection': 'error',
'sonarjs/no-gratuitous-expressions': 'error',
// Code smell detection
'sonarjs/cognitive-complexity': ['error', 15],
'sonarjs/no-duplicate-string': ['error', { threshold: 3 }],
'sonarjs/no-redundant-boolean': 'error',
'sonarjs/no-small-switch': 'error',
'sonarjs/prefer-immediate-return': 'error',
'sonarjs/prefer-single-boolean-return': 'error',
'sonarjs/no-inverted-boolean-check': 'error',
'sonarjs/no-nested-template-literals': 'error'
},
languageOptions: {
ecmaVersion: 2024,
sourceType: 'module'
}
},
{
ignores: [
'**/node_modules/**',
'**/dist/**',
'**/build/**',
'**/*.config.*',
'**/*.test.*',
'**/*.spec.*'
]
}
]

View File

@@ -0,0 +1,72 @@
---
name: import-graph
description: Validates import rules, detects circular dependencies, and enforces layer boundaries using dependency-cruiser
severity-default: high
tools: [Bash, Read]
---
Run dependency-cruiser import graph analysis on changed files to detect circular dependencies, orphan modules, and import rule violations.
> **Note:** The circular dependency scan in step 4 targets `src/` specifically, since this is a frontend app with source code under `src/`.
## Steps
1. Check if dependency-cruiser is available:
```bash
pnpm dlx dependency-cruiser --version
```
If not available, skip this check and report: "Skipped: dependency-cruiser not available. Install with: `pnpm add -D dependency-cruiser`"
> **Install:** `pnpm add -D dependency-cruiser`
2. Identify changed directories from the diff.
3. Determine config to use:
- If `.dependency-cruiser.js` or `.dependency-cruiser.cjs` exists in the repo root, use it (dependency-cruiser auto-detects it). This config may enforce layer architecture rules (e.g., base → platform → workbench → renderer import direction):
```bash
pnpm dlx dependency-cruiser --output-type json <changed_directories> 2>/dev/null
```
- If no config exists, run with built-in defaults:
```bash
pnpm dlx dependency-cruiser --no-config --output-type json <changed_directories> 2>/dev/null
```
4. Also check for circular dependencies specifically across `src/`:
```bash
pnpm dlx dependency-cruiser --no-config --output-type json --do-not-follow "node_modules" --include-only "^src" src 2>/dev/null
```
Look for modules where `.circular == true` in the output.
5. Parse the JSON output. Each violation has:
- `rule.name`: the violated rule
- `rule.severity`: error, warn, info
- `from`: importing module
- `to`: imported module
6. Map violation severity:
- `error` → `major`
- `warn` → `minor`
- `info` → `nitpick`
- Circular dependencies → `major` (category: architecture)
- Orphan modules → `nitpick` (category: dx)
7. Report each violation with: the rule name, source and target modules, file path, and a suggestion (usually move the import or extract an interface).
## What It Catches
| Rule | What It Detects |
| ------------------------ | ---------------------------------------------------- |
| `no-circular` | Circular dependency chains (A → B → C → A) |
| `no-orphans` | Modules with no incoming or outgoing dependencies |
| `not-to-dev-dep` | Production code importing devDependencies |
| `no-duplicate-dep-types` | Same dependency in multiple sections of package.json |
| Custom layer rules | Import direction violations (e.g., base → platform) |
## Error Handling
- If pnpm dlx is not available, skip and report the error.
- If the config file fails to parse, fall back to `--no-config`.
- If there are more than 50 violations, report the first 20 and note the total count.
- If no violations are found, report "No issues found."

View File

@@ -0,0 +1,27 @@
---
name: memory-leak
description: Scans for memory leak patterns including event listeners without cleanup, timers not cleared, and unbounded caches
severity-default: high
tools: [Read, Grep]
---
You are a memory leak specialist reviewing a code diff. Focus exclusively on patterns that cause memory to grow unboundedly over time.
Check for:
1. **Event listeners without cleanup** - addEventListener without corresponding removeEventListener, especially in Vue onMounted without onBeforeUnmount cleanup
2. **Timers not cleared** - setInterval/setTimeout started in component lifecycle without clearInterval/clearTimeout on unmount
3. **Observer patterns without disconnect** - MutationObserver, IntersectionObserver, ResizeObserver created without .disconnect() on cleanup
4. **WebSocket/Worker connections** - opened connections never closed on component unmount or route change
5. **Unbounded caches** - Maps, Sets, or arrays that grow with usage but never evict entries, especially keyed by user input or dynamic IDs
6. **Stale closures holding references** - closures in event handlers or callbacks that capture large objects or DOM nodes and prevent garbage collection
7. **RequestAnimationFrame without cancel** - rAF loops started without cancelAnimationFrame on cleanup
8. **Vue-specific leaks** - watch/watchEffect without stop(), computed that captures reactive dependencies it shouldn't, provide/inject holding stale references
9. **Global state accumulation** - pushing to global arrays/maps without ever removing entries, console.log keeping object references in dev
Rules:
- Focus on NEW leak patterns introduced in the diff
- Do NOT flag existing cleanup patterns that are correct
- Every finding must explain the specific lifecycle scenario where the leak occurs (e.g., "when user navigates away from this view, the interval keeps running")
- "Critical" for leaks in hot paths or long-lived pages, "major" for component-level leaks, "minor" for dev-only or cold-path leaks

View File

@@ -0,0 +1,60 @@
---
name: pattern-compliance
description: Checks code against repository conventions from AGENTS.md and established patterns
severity-default: medium
tools: [Read, Grep]
---
Check code against repository conventions and framework patterns.
Steps:
1. Read AGENTS.md (and any directory-specific guidance files) for project-specific conventions
2. Read each changed file
3. Check against the conventions found in AGENTS.md and these standard patterns:
### TypeScript
- No `any` types or `as any` assertions
- No `@ts-ignore` without explanatory comment
- Separate type imports (`import type { ... }`)
- Use `import type { ... }` for type-only imports
- Explicit return types on exported functions
- Use `es-toolkit` for utility functions, NOT lodash. Flag any new `import ... from 'lodash'` or `import ... from 'lodash/*'`
- Never use `z.any()` in Zod schemas — use `z.unknown()` and narrow
### Vue (if applicable)
- Composition API with `<script setup lang="ts">`
- Reactive props destructuring (not `withDefaults` pattern)
- New components must use `<script setup lang="ts">` with reactive props destructuring (Vue 3.5 style): `const { color = 'blue' } = defineProps<Props>()`
- Separate type imports from value imports
- All user-facing strings must use `vue-i18n` (`$t()` in templates, `t()` in script). Flag hardcoded English strings in templates that should be translated. The locale file is `src/locales/en/main.json`
### Tailwind (if applicable)
- No `dark:` variants (use semantic theme tokens)
- Use `cn()` utility for conditional classes
- No `!important` in utility classes
- Tailwind 4: CSS variable references use parentheses syntax: `h-(--my-var)` NOT `h-[--my-var]`
- Use design tokens: `bg-secondary-background`, `text-muted-foreground`, `border-border-default`
- No `<style>` blocks in Vue SFCs — use inline Tailwind only
### Testing
- Behavioral tests, not change detectors
- No mock-heavy tests that don't test real behavior
- Test names describe behavior, not implementation
### General
- No commented-out code
- No `console.log` in production code (unless intentional logging)
- No hardcoded URLs, credentials, or environment-specific values
- Package manager is `pnpm`. Never use `npm`, `npx`, or `yarn`. Use `pnpm dlx` for one-off package execution
- Sanitize HTML with `DOMPurify.sanitize()`, never raw `innerHTML` or `v-html` without it
Rules:
- Only flag ACTUAL violations, not hypothetical ones
- AGENTS.md conventions take priority over default patterns if they conflict

View File

@@ -0,0 +1,35 @@
---
name: performance-profiler
description: Reviews code for performance issues including algorithmic complexity, unnecessary work, and bundle size impact
severity-default: medium
tools: [Read, Grep]
---
You are a performance engineer reviewing a code diff. Focus exclusively on performance issues.
Check for:
1. **Algorithmic complexity** - O(n²) or worse in loops, nested iterations over large collections
2. **Unnecessary re-computation** - repeated work in render cycles, missing memoization for expensive ops
3. **Memory leaks** - event listeners not cleaned up, growing caches without eviction, closures holding references
4. **N+1 queries** - database/API calls inside loops
5. **Bundle size** - large imports that could be tree-shaken, dynamic imports for heavy modules
6. **Rendering performance** - unnecessary re-renders, layout thrashing, expensive computed properties
7. **Data structures** - using arrays for lookups instead of maps/sets, unnecessary copying of large objects
8. **Async patterns** - sequential awaits that could be parallel, missing abort controllers
Rules:
- ONLY report actual performance issues, not premature optimization suggestions
- Distinguish between hot paths (major) and cold paths (minor)
- Include Big-O analysis when relevant
- Do NOT suggest micro-optimizations that a JIT compiler handles
- Quantify the impact when possible: "This is O(n²) where n = number of users"
## Repo-Specific Performance Concerns
- **LiteGraph canvas rendering** is the primary hot path. Operations inside `LGraphNode.onDrawForeground`, `onDrawBackground`, `processMouseMove` run every frame at 60fps. Any O(n) or worse operation here on the node/link collections is critical.
- **Node definition lookups** happen frequently — these should use Maps, not array.find()
- **Workflow serialization/deserialization** can involve large JSON objects (1000+ nodes). Watch for deep copies or unnecessary re-parsing.
- **Vue reactivity in canvas code** — reactive getters triggered during canvas render cause performance issues. Canvas-facing code should read raw values, not reactive refs.
- **Bundle size** — check for large imports that could be dynamically imported. The build uses Vite with `build:analyze` for bundle visualization.

View File

@@ -0,0 +1,44 @@
---
name: regression-risk
description: Detects potential regressions by analyzing git blame history of modified lines
severity-default: high
tools: [Bash, Read, Grep]
---
Perform regression risk analysis on the current changes using git blame.
## Method
1. Determine the base branch by examining git context (e.g., `git merge-base origin/main HEAD`, or check the PR's target branch). Never use `HEAD~1` as the base — it compares against the PR's own prior commit and causes false positives.
2. Get the PR's own commits: `git log --format=%H <base>..HEAD`
3. For each changed file, run: `git diff <base>...HEAD -- <file>`
4. Extract the modified line ranges from the diff (lines removed or changed in the base version).
5. For each modified line range, check git blame in the base version:
`git blame <base> -L <start>,<end> -- <file>`
6. Look for blame commits whose messages match bugfix patterns:
- Contains: fix, bug, patch, hotfix, revert, regression, CVE
- Ignore: "fix lint", "fix typo", "fix format", "fix style"
7. **Filter out false positives.** If the blamed commit SHA is in the PR's own commits, skip it.
8. For each verified bugfix line being modified, report as a finding.
## What to Report
For each finding, include:
- The file and line number
- The original bugfix commit (short SHA and subject)
- The date of the original fix
- A suggestion to verify the original bug scenario still works and to add a regression test if one doesn't exist
## Shallow Clone Limitations
When working with shallow clones, `git blame` may not have full history. If blame fails with "no such path in revision" or shows truncated history, report only findings where blame succeeds and note the limitation.
## Edge Cases
| Situation | Action |
| ------------------------ | -------------------------------- |
| Shallow clone (no blame) | Report what succeeds, note limit |
| Blame shows PR's own SHA | Skip finding (false positive) |
| File renamed | Try blame with `--follow` |
| Binary file | Skip file |

View File

@@ -0,0 +1,34 @@
---
name: security-auditor
description: Reviews code for security vulnerabilities aligned with OWASP Top 10
severity-default: critical
tools: [Read, Grep]
---
You are a security auditor reviewing a code diff. Focus exclusively on security vulnerabilities.
Check for:
1. **Injection** - SQL injection, command injection, template injection, XSS (stored/reflected/DOM)
2. **Authentication/Authorization** - auth bypass, privilege escalation, missing access checks
3. **Data exposure** - secrets in code, PII in logs, sensitive data in error messages, overly broad API responses
4. **Cryptography** - weak algorithms, hardcoded keys, predictable tokens, missing encryption
5. **Input validation** - missing sanitization, path traversal, SSRF, open redirects
6. **Dependency risks** - known vulnerable patterns, unsafe deserialization
7. **Configuration** - CORS misconfiguration, missing security headers, debug mode in production
8. **Race conditions with security impact** - TOCTOU, double-spend, auth state races
Rules:
- ONLY report security issues, not general bugs or style
- All findings must be severity "critical" or "major"
- Explain the attack vector: who can exploit this and how
- Do NOT report theoretical issues without a plausible attack scenario
- Reference OWASP category when applicable
## Repo-Specific Patterns
- HTML sanitization must use `DOMPurify.sanitize()` — flag any `v-html` or `innerHTML` without DOMPurify
- API calls should use `api.get(api.apiURL(...))` helpers, not raw `fetch('/api/...')` — direct URL construction can bypass auth
- Firebase/Sentry credentials are configured via environment — flag any hardcoded Firebase config objects
- Electron IPC: check for unsafe `ipcRenderer.send` patterns in desktop code paths

View File

@@ -0,0 +1,54 @@
---
name: semgrep-sast
description: Runs Semgrep SAST with auto-configured rules for JS/TS/Vue
severity-default: high
tools: [Bash, Read]
---
Run Semgrep static analysis on changed files to detect security vulnerabilities, dangerous patterns, and framework-specific issues.
## Steps
1. Check if semgrep is installed:
```bash
semgrep --version
```
If not installed, skip this check and report: "Skipped: semgrep not installed. Install with: `pip3 install semgrep`"
2. Identify changed files (`.ts`, `.js`, `.vue`) from the diff.
If none are found, skip and report: "Skipped: no changed JS/TS/Vue files."
3. Run semgrep against changed files:
```bash
semgrep --config=auto --json --quiet <changed_files>
```
4. Parse the JSON output (`.results[]` array). For each finding, map severity:
- Semgrep `ERROR` → `critical`
- Semgrep `WARNING` → `major`
- Semgrep `INFO` → `minor`
5. Report each finding with:
- The semgrep rule ID (`check_id`)
- File path and line number (`path`, `start.line`)
- The message from `extra.message`
- A fix suggestion from `extra.fix` if available, otherwise general remediation advice
## What Semgrep Catches
With `--config=auto`, Semgrep loads community-maintained rules for:
- **Security vulnerabilities:** injection, XSS, SSRF, path traversal, open redirect
- **Dangerous patterns:** eval(), innerHTML, dangerouslySetInnerHTML, exec()
- **Crypto issues:** weak hashing, hardcoded secrets, insecure random
- **Best practices:** missing security headers, unsafe deserialization
- **Framework-specific:** Express, React, Vue security patterns
## Error Handling
- If semgrep config download fails, skip and report the error.
- If semgrep fails to parse a specific file, skip that file and continue with others.
- If semgrep produces no findings, report "No issues found."

View File

@@ -0,0 +1,59 @@
---
name: sonarjs-lint
description: Runs SonarQube-grade static analysis using eslint-plugin-sonarjs
severity-default: high
tools: [Bash, Read]
---
Run eslint-plugin-sonarjs analysis on changed files to detect bugs, code smells, and security patterns without needing a SonarQube server.
## Steps
1. Check if eslint is available:
```bash
pnpm dlx eslint --version
```
If pnpm dlx or eslint is unavailable, skip this check and report: "Skipped: eslint not available. Ensure Node.js and pnpm dlx are installed."
2. Identify changed files (`.ts`, `.js`, `.vue`) from the diff.
3. Determine eslint config to use. This check uses a **strict sonarjs-specific config** (not the project's own eslint config, which is less strict):
- Look for the colocated strict config at `.agents/checks/eslint.strict.config.js`
- If found, run with `--config .agents/checks/eslint.strict.config.js`
- **Fallback:** if the strict config cannot be found or fails to load, skip this check and report: "Skipped: .agents/checks/eslint.strict.config.js missing; SonarJS rules require explicit config."
4. Run eslint against changed files:
```bash
# Use the strict config
pnpm dlx --yes --package eslint-plugin-sonarjs eslint --no-config-lookup --config .agents/checks/eslint.strict.config.js --format json <changed_files> 2>/dev/null || true
```
5. Parse the JSON array of file results. For each eslint message, map severity:
- `severity 2` (error) → `major`
- `severity 1` (warning) → `minor`
6. Categorize findings by rule ID:
- Rule IDs starting with `sonarjs/no-` → category: `logic`
- Rule IDs containing `cognitive-complexity` → category: `dx`
- Other sonarjs rules → category: `style`
7. Report each finding with:
- The rule ID
- File path and line number
- The message from eslint
- A fix suggestion based on the rule
## What This Catches
- **Bug detection:** duplicated branches, element overwrite, identical conditions/expressions, one-iteration loops, empty return values
- **Code smells:** cognitive complexity (threshold: 15), duplicate strings, redundant booleans, small switches
- **Security patterns:** via sonarjs recommended ruleset
## Error Handling
- If eslint fails to parse a Vue file, skip that file and continue with others.
- If the plugin fails to install, skip and report the error.
- If eslint produces no output or errors, report "No issues found."

View File

@@ -0,0 +1,37 @@
---
name: test-quality
description: Reviews test code for quality issues and coverage gaps
severity-default: medium
tools: [Read, Grep]
---
You are a test quality reviewer. Evaluate the tests included with (or missing from) this code change.
Check for:
1. **Missing tests** - new behavior without test coverage, modified logic without updated tests
2. **Change-detector tests** - tests that assert implementation details instead of behavior (testing that a function was called, not what it produces)
3. **Mock-heavy tests** - tests with so many mocks they don't test real behavior
4. **Snapshot abuse** - large snapshots that no one reviews, snapshots of implementation details
5. **Fragile assertions** - tests that break on unrelated changes, order-dependent tests
6. **Missing edge cases** - happy path only, no empty/null/error scenarios tested
7. **Test readability** - unclear test names, complex setup that obscures intent, shared mutable state between tests
8. **Test isolation** - tests depending on execution order, shared state, external services without mocking
Rules:
- Focus on test quality and coverage gaps, not production code bugs
- "Major" for missing tests on critical logic, "minor" for missing edge case tests
- A change that adds no tests is only an issue if the change adds behavior
- Refactors without behavior changes don't need new tests
- Prefer behavioral tests: test inputs and outputs, not internal implementation
- This repo uses **colocated tests**: `.test.ts` files live next to their source files (e.g., `MyComponent.test.ts` beside `MyComponent.vue`). When checking for missing tests, look for a colocated `.test.ts` file, not a separate `tests/` directory
## Repo-Specific Testing Conventions
- Tests use **Vitest** (not Jest) — run with `pnpm test:unit`
- Test files are **colocated**: `MyComponent.test.ts` next to `MyComponent.vue`
- Use `@vue/test-utils` for component testing, `@pinia/testing` (`createTestingPinia`) for store tests
- Browser/E2E tests use **Playwright** in `browser_tests/` — run with `pnpm test:browser:local`
- Mock composables using the singleton factory pattern inside `vi.mock()` — see `docs/testing/unit-testing.md` for the pattern
- Never use `any` in test code either — proper typing applies to tests too

View File

@@ -0,0 +1,47 @@
---
name: vue-patterns
description: Reviews Vue 3.5+ code for framework-specific anti-patterns
severity-default: medium
tools: [Read, Grep]
---
You are a Vue 3.5 framework specialist reviewing a code diff. Focus on Vue-specific patterns, anti-patterns, and missed framework features.
Check for:
1. **Options API in new files** - new .vue files using Options API instead of Composition API with `<script setup>`. Modifications to existing Options API files are fine.
2. **Reactivity anti-patterns** - destructuring reactive objects losing reactivity, using `ref()` for objects that should be `reactive()`, accessing `.value` inside templates, incorrectly using `toRefs`/`toRef`
3. **Watch/watchEffect cleanup** - watchers without cleanup functions when they set up side effects (timers, listeners, subscriptions)
4. **Flush timing issues** - DOM access in watch callbacks without `{ flush: 'post' }`, `nextTick` misuse, accessing template refs before mount
5. **defineEmits typing** - using array syntax `defineEmits(['event'])` instead of TypeScript syntax `defineEmits<{...}>()`
6. **defineExpose misuse** - exposing internal state via `defineExpose` when events would be more appropriate (expose is for imperative methods: validate, focus, open)
7. **Prop drilling** - passing props through 3+ component levels where provide/inject would be cleaner
8. **VueUse opportunities** - manual implementations of common composables that VueUse already provides (useLocalStorage, useEventListener, useDebounceFn, useIntersectionObserver, etc.)
9. **Computed vs method** - methods used in templates for derived state that should be computed properties, or computed properties that have side effects
10. **PrimeVue usage in new code** - New components must NOT use PrimeVue. This project is migrating to shadcn-vue (Reka UI primitives). If new code imports from `primevue/*`, flag it and suggest the shadcn-vue equivalent.
Available shadcn-vue replacements in `src/components/ui/`:
- `button/` — Button, variants
- `select/` — Select, SelectTrigger, SelectContent, SelectItem
- `textarea/` — Textarea
- `toggle-group/` — ToggleGroup, ToggleGroupItem
- `slider/` — Slider
- `skeleton/` — Skeleton
- `stepper/` — Stepper
- `tags-input/` — TagsInput
- `search-input/` — SearchInput
- `Popover.vue` — Popover
For Reka UI primitives not yet wrapped, create a new component in `src/components/ui/` following the pattern in existing components (see `src/components/ui/AGENTS.md`): use `useForwardProps`, `cn()`, design tokens.
Modifications to existing PrimeVue-based components are acceptable but should note the migration opportunity.
Rules:
- Only review .vue and composable .ts files — skip stores, services, utils
- Do NOT flag existing Options API files being modified (only flag NEW files)
- Flag new PrimeVue imports — the project is migrating to shadcn-vue/Reka UI
- When suggesting shadcn-vue alternatives, reference `src/components/ui/AGENTS.md` for the component creation pattern
- Use Iconify icons (`<i class="icon-[lucide--check]" />`) not PrimeIcons
- "Major" for reactivity bugs and flush timing, "minor" for API style and VueUse opportunities, "nitpick" for preference-level patterns

View File

@@ -0,0 +1,83 @@
---
name: regenerating-screenshots
description: 'Creates a PR to regenerate Playwright screenshot expectations. Use when screenshot tests are failing on main or PRs due to stale golden images. Triggers on: regen screenshots, regenerate screenshots, update expectations, fix screenshot tests.'
---
# Regenerating Playwright Screenshot Expectations
Automates the process of triggering the `PR: Update Playwright Expectations`
GitHub Action by creating a labeled PR from `origin/main`.
## Steps
1. **Fetch latest main**
```bash
git fetch origin main
```
2. **Create a timestamped branch** from `origin/main`
Format: `regen-screenshots/YYYY-MM-DDTHH` (hour resolution, local time)
```bash
git checkout -b regen-screenshots/<datetime> origin/main
```
3. **Create an empty commit**
```bash
git commit --allow-empty -m "test: regenerate screenshot expectations"
```
4. **Push the branch**
```bash
git push origin regen-screenshots/<datetime>
```
5. **Generate a poem** about regenerating screenshots. Be creative — a
new, unique poem every time. Short (48 lines). Can be funny, wistful,
epic, haiku-style, limerick, sonnet fragment — vary the form.
6. **Create the PR** with the poem as the body (no label yet).
Write the poem to a temp file and use `--body-file`:
```bash
# Write poem to temp file
# Create PR:
gh pr create \
--base main \
--head regen-screenshots/<datetime> \
--title "test: regenerate screenshot expectations" \
--body-file <temp-file>
```
7. **Add the label** as a separate step to trigger the GitHub Action.
The `labeled` event only fires when a label is added after PR
creation, not when applied during creation via `--label`.
Use the GitHub API directly (`gh pr edit --add-label` fails due to
deprecated Projects Classic GraphQL errors):
```bash
gh api repos/{owner}/{repo}/issues/<pr-number>/labels \
-f "labels[]=New Browser Test Expectations" --method POST
```
8. **Report the result** to the user:
- PR URL
- Branch name
- Note that the GitHub Action will run automatically and commit
updated screenshots to the branch.
## Notes
- The `New Browser Test Expectations` label triggers the
`pr-update-playwright-expectations.yaml` workflow.
- The workflow runs Playwright with `--update-snapshots`, commits results
back to the PR branch, then removes the label.
- This is fire-and-forget — no need to wait for or monitor the Action.
- Always return to the original branch/worktree state after pushing.

View File

@@ -5,3 +5,10 @@ reviews:
high_level_summary: false
auto_review:
drafts: true
ignore_title_keywords:
- '[release]'
- '[backport'
ignore_usernames:
- comfy-pr-bot
- github-actions
- github-actions[bot]

8
.github/AGENTS.md vendored
View File

@@ -13,3 +13,11 @@ This automated review performs comprehensive analysis:
- Integration concerns
For implementation details, see `.claude/commands/comprehensive-pr-review.md`.
## GitHub Actions: Fork PR Permissions
Fork PRs get a **read-only `GITHUB_TOKEN`** — no PR comments, no secret access, no pushing.
Any workflow that needs write access must use the **two-workflow split**: a `pull_request`-triggered `ci-*.yaml` uploads artifacts (including PR metadata), then a `workflow_run`-triggered `pr-*.yaml` downloads them and posts comments with write permissions. See `ci-size-data``pr-size-report` or `ci-perf-report``pr-perf-report`. Use `.github/actions/post-pr-report-comment` for the comment step.
Never write PR comments directly from `pull_request` workflows or use `pull_request_target` to run untrusted code.

View File

@@ -0,0 +1,35 @@
name: Post PR Report Comment
description: Reads a markdown report file and posts/updates a single idempotent comment on a PR.
inputs:
pr-number:
description: PR number to comment on
required: true
report-file:
description: Path to the markdown report file
required: true
comment-marker:
description: HTML comment marker for idempotent matching
required: true
token:
description: GitHub token with pull-requests write permission
required: true
runs:
using: composite
steps:
- name: Read report
id: report
uses: juliangruber/read-file-action@b549046febe0fe86f8cb4f93c24e284433f9ab58 # v1.1.7
with:
path: ${{ inputs.report-file }}
- name: Create or update PR comment
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
with:
token: ${{ inputs.token }}
number: ${{ inputs.pr-number }}
body: |
${{ steps.report.outputs.content }}
${{ inputs.comment-marker }}
body-include: ${{ inputs.comment-marker }}

3
.github/license-clarifications.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"posthog-js@*": { "licenses": "Apache-2.0" }
}

View File

@@ -23,7 +23,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: lts/*
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Update electron types

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: lts/*
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies

View File

@@ -27,7 +27,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: lts/*
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies

View File

@@ -26,7 +26,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
@@ -79,3 +79,22 @@ jobs:
exit 1
fi
echo '✅ No Mixpanel references found'
- name: Scan dist for PostHog telemetry references
run: |
set -euo pipefail
echo '🔍 Scanning for PostHog references...'
if rg --no-ignore -n \
-g '*.html' \
-g '*.js' \
-e '(?i)posthog\.init' \
-e '(?i)posthog\.capture' \
-e 'PostHogTelemetryProvider' \
-e 'ph\.comfy\.org' \
-e 'posthog-js' \
dist; then
echo '❌ ERROR: PostHog references found in dist assets!'
echo 'PostHog must be properly tree-shaken from OSS builds.'
exit 1
fi
echo '✅ No PostHog references found'

View File

@@ -27,7 +27,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
@@ -82,7 +82,7 @@ jobs:
- name: Use Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies
@@ -100,6 +100,7 @@ jobs:
--production \
--summary \
--excludePackages '@comfyorg/comfyui-frontend;@comfyorg/design-system;@comfyorg/registry-types;@comfyorg/shared-frontend-utils;@comfyorg/tailwind-utils;@comfyorg/comfyui-electron-types' \
--clarificationsFile .github/license-clarifications.json \
--onlyAllow 'MIT;MIT*;Apache-2.0;BSD-2-Clause;BSD-3-Clause;ISC;0BSD;BlueOak-1.0.0;Python-2.0;CC0-1.0;Unlicense;(MIT OR Apache-2.0);(MIT OR GPL-3.0);(Apache-2.0 OR MIT);(MPL-2.0 OR Apache-2.0);CC-BY-4.0;CC-BY-3.0;GPL-3.0-only'; then
echo ''
echo '✅ All production dependency licenses are approved!'

View File

@@ -14,7 +14,6 @@ concurrency:
permissions:
contents: read
pull-requests: write
jobs:
perf-tests:
@@ -45,7 +44,7 @@ jobs:
- name: Run performance tests
id: perf
continue-on-error: true
run: pnpm exec playwright test --project=performance --workers=1
run: pnpm exec playwright test --project=performance --workers=1 --repeat-each=3
- name: Upload perf metrics
if: always()
@@ -56,55 +55,16 @@ jobs:
retention-days: 30
if-no-files-found: warn
report:
needs: perf-tests
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
- name: Save PR metadata
if: github.event_name == 'pull_request'
run: |
mkdir -p temp/perf-meta
echo "${{ github.event.number }}" > temp/perf-meta/number.txt
echo "${{ github.event.pull_request.base.ref }}" > temp/perf-meta/base.txt
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v6
- name: Upload PR metadata
if: github.event_name == 'pull_request'
uses: actions/upload-artifact@v6
with:
node-version: 22
- name: Download PR perf metrics
continue-on-error: true
uses: actions/download-artifact@v7
with:
name: perf-metrics
path: test-results/
- name: Download baseline perf metrics
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
branch: ${{ github.event.pull_request.base.ref }}
workflow: ci-perf-report.yaml
event: push
name: perf-metrics
path: temp/perf-baseline/
if_no_artifact_found: warn
- name: Generate perf report
run: npx --yes tsx scripts/perf-report.ts > perf-report.md
- name: Read perf report
id: perf-report
uses: juliangruber/read-file-action@b549046febe0fe86f8cb4f93c24e284433f9ab58 # v1.1.7
with:
path: ./perf-report.md
- name: Create or update PR comment
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
number: ${{ github.event.pull_request.number }}
body: |
${{ steps.perf-report.outputs.content }}
<!-- COMFYUI_FRONTEND_PERF -->
body-include: '<!-- COMFYUI_FRONTEND_PERF -->'
name: perf-meta
path: temp/perf-meta/

View File

@@ -39,7 +39,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.13
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
@@ -87,7 +87,7 @@ jobs:
needs: setup
runs-on: ubuntu-latest
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.13
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -0,0 +1,69 @@
---
# Dispatches a frontend-asset-build event to the cloud repo on push to
# cloud/* branches and main. The cloud repo handles the actual build,
# GCS upload, and secret management (Sentry, Algolia, GCS creds).
#
# This is fire-and-forget — it does NOT wait for the cloud workflow to
# complete. Status is visible in the cloud repo's Actions tab.
name: Cloud Frontend Build Dispatch
on:
push:
branches:
- 'cloud/*'
- 'main'
pull_request:
types: [labeled, synchronize]
workflow_dispatch:
permissions: {}
concurrency:
group: cloud-dispatch-${{ github.ref }}
cancel-in-progress: true
jobs:
dispatch:
# Fork guard: prevent forks from dispatching to the cloud repo.
# For pull_request events, only dispatch for preview labels.
# - labeled: fires when a label is added; check the added label name.
# - synchronize: fires on push; check existing labels on the PR.
if: >
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
(github.event_name != 'pull_request' ||
(github.event.action == 'labeled' &&
contains(fromJSON('["preview","preview-cpu","preview-gpu"]'), github.event.label.name)) ||
(github.event.action == 'synchronize' &&
(contains(github.event.pull_request.labels.*.name, 'preview') ||
contains(github.event.pull_request.labels.*.name, 'preview-cpu') ||
contains(github.event.pull_request.labels.*.name, 'preview-gpu'))))
runs-on: ubuntu-latest
steps:
- name: Build client payload
id: payload
env:
EVENT_NAME: ${{ github.event_name }}
PR_HEAD_SHA: ${{ github.event.pull_request.head.sha }}
PR_HEAD_REF: ${{ github.event.pull_request.head.ref }}
run: |
if [ "${EVENT_NAME}" = "pull_request" ]; then
REF="${PR_HEAD_SHA}"
BRANCH="${PR_HEAD_REF}"
else
REF="${GITHUB_SHA}"
BRANCH="${GITHUB_REF_NAME}"
fi
payload="$(jq -nc \
--arg ref "${REF}" \
--arg branch "${BRANCH}" \
'{ref: $ref, branch: $branch}')"
echo "json=${payload}" >> "${GITHUB_OUTPUT}"
- name: Dispatch to cloud repo
uses: peter-evans/repository-dispatch@28959ce8df70de7be546dd1250a005dd32156697 # v4.0.1
with:
token: ${{ secrets.CLOUD_DISPATCH_TOKEN }}
repository: Comfy-Org/cloud
event-type: frontend-asset-build
client-payload: ${{ steps.payload.outputs.json }}

View File

@@ -36,7 +36,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies for analysis tools

102
.github/workflows/pr-perf-report.yaml vendored Normal file
View File

@@ -0,0 +1,102 @@
name: 'PR: Performance Report'
on:
workflow_run:
workflows: ['CI: Performance Report']
types:
- completed
permissions:
contents: read
pull-requests: write
issues: write
jobs:
comment:
runs-on: ubuntu-latest
if: >
github.repository == 'Comfy-Org/ComfyUI_frontend' &&
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.conclusion == 'success'
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'
- name: Download PR metadata
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
name: perf-meta
run_id: ${{ github.event.workflow_run.id }}
path: temp/perf-meta/
- name: Resolve and validate PR metadata
id: pr-meta
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
const artifactPr = Number(fs.readFileSync('temp/perf-meta/number.txt', 'utf8').trim());
const artifactBase = fs.readFileSync('temp/perf-meta/base.txt', 'utf8').trim();
// Resolve PR from trusted workflow context
let pr = context.payload.workflow_run.pull_requests?.[0];
if (!pr) {
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.payload.workflow_run.head_sha,
});
pr = prs.find(p => p.state === 'open');
}
if (!pr) {
core.setFailed('Unable to resolve PR from workflow_run context.');
return;
}
if (Number(pr.number) !== artifactPr) {
core.setFailed(`Artifact PR number (${artifactPr}) does not match trusted context (${pr.number}).`);
return;
}
const trustedBase = pr.base?.ref;
if (!trustedBase || artifactBase !== trustedBase) {
core.setFailed(`Artifact base (${artifactBase}) does not match trusted context (${trustedBase}).`);
return;
}
core.setOutput('number', String(pr.number));
core.setOutput('base', trustedBase);
- name: Download PR perf metrics
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
name: perf-metrics
run_id: ${{ github.event.workflow_run.id }}
path: test-results/
- name: Download baseline perf metrics
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
branch: ${{ steps.pr-meta.outputs.base }}
workflow: ci-perf-report.yaml
event: push
name: perf-metrics
path: temp/perf-baseline/
if_no_artifact_found: warn
- name: Generate perf report
run: npx --yes tsx scripts/perf-report.ts > perf-report.md
- name: Post PR comment
uses: ./.github/actions/post-pr-report-comment
with:
pr-number: ${{ steps.pr-meta.outputs.number }}
report-file: ./perf-report.md
comment-marker: '<!-- COMFYUI_FRONTEND_PERF -->'
token: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -45,28 +45,76 @@ jobs:
run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }}
path: temp/size
- name: Set PR number
id: pr-number
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "content=${{ inputs.pr_number }}" >> $GITHUB_OUTPUT
else
echo "content=$(cat temp/size/number.txt)" >> $GITHUB_OUTPUT
fi
- name: Resolve and validate PR metadata
id: pr-meta
uses: actions/github-script@v8
with:
script: |
const fs = require('fs');
- name: Set base branch
id: pr-base
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
echo "content=main" >> $GITHUB_OUTPUT
else
echo "content=$(cat temp/size/base.txt)" >> $GITHUB_OUTPUT
fi
// workflow_dispatch: validate artifact metadata against API-resolved PR
if (context.eventName === 'workflow_dispatch') {
const pullNumber = Number('${{ inputs.pr_number }}');
const { data: dispatchPr } = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pullNumber,
});
const artifactPr = Number(fs.readFileSync('temp/size/number.txt', 'utf8').trim());
const artifactBase = fs.readFileSync('temp/size/base.txt', 'utf8').trim();
if (artifactPr !== dispatchPr.number) {
core.setFailed(`Artifact PR number (${artifactPr}) does not match dispatch PR (${dispatchPr.number}).`);
return;
}
if (artifactBase !== dispatchPr.base.ref) {
core.setFailed(`Artifact base (${artifactBase}) does not match dispatch PR base (${dispatchPr.base.ref}).`);
return;
}
core.setOutput('number', String(dispatchPr.number));
core.setOutput('base', dispatchPr.base.ref);
return;
}
// workflow_run: validate artifact metadata against trusted context
const artifactPr = Number(fs.readFileSync('temp/size/number.txt', 'utf8').trim());
const artifactBase = fs.readFileSync('temp/size/base.txt', 'utf8').trim();
let pr = context.payload.workflow_run.pull_requests?.[0];
if (!pr) {
const { data: prs } = await github.rest.repos.listPullRequestsAssociatedWithCommit({
owner: context.repo.owner,
repo: context.repo.repo,
commit_sha: context.payload.workflow_run.head_sha,
});
pr = prs.find(p => p.state === 'open');
}
if (!pr) {
core.setFailed('Unable to resolve PR from workflow_run context.');
return;
}
if (Number(pr.number) !== artifactPr) {
core.setFailed(`Artifact PR number (${artifactPr}) does not match trusted context (${pr.number}).`);
return;
}
const trustedBase = pr.base?.ref;
if (!trustedBase || artifactBase !== trustedBase) {
core.setFailed(`Artifact base (${artifactBase}) does not match trusted context (${trustedBase}).`);
return;
}
core.setOutput('number', String(pr.number));
core.setOutput('base', trustedBase);
- name: Download previous size data
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
branch: ${{ steps.pr-base.outputs.content }}
branch: ${{ steps.pr-meta.outputs.base }}
workflow: ci-size-data.yaml
event: push
name: size-data
@@ -76,18 +124,10 @@ jobs:
- name: Generate size report
run: node scripts/size-report.js > size-report.md
- name: Read size report
id: size-report
uses: juliangruber/read-file-action@b549046febe0fe86f8cb4f93c24e284433f9ab58 # v1.1.7
with:
path: ./size-report.md
- name: Create or update PR comment
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
- name: Post PR comment
uses: ./.github/actions/post-pr-report-comment
with:
pr-number: ${{ steps.pr-meta.outputs.number }}
report-file: ./size-report.md
comment-marker: '<!-- COMFYUI_FRONTEND_SIZE -->'
token: ${{ secrets.GITHUB_TOKEN }}
number: ${{ steps.pr-number.outputs.content }}
body: |
${{ steps.size-report.outputs.content }}
<!-- COMFYUI_FRONTEND_SIZE -->
body-include: '<!-- COMFYUI_FRONTEND_SIZE -->'

View File

@@ -77,7 +77,7 @@ jobs:
needs: setup
runs-on: ubuntu-latest
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.13
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

View File

@@ -28,7 +28,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24.x'
node-version-file: '.nvmrc'
- name: Read desktop-ui version
id: get_version

View File

@@ -91,7 +91,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24.x'
node-version-file: '.nvmrc'
cache: 'pnpm'
registry-url: https://registry.npmjs.org

View File

@@ -82,7 +82,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: lts/*
node-version-file: 'frontend/.nvmrc'
- name: Install dependencies
working-directory: frontend

View File

@@ -26,7 +26,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 'lts/*'
node-version-file: '.nvmrc'
- name: Check version bump type
id: check_version

View File

@@ -26,7 +26,7 @@ jobs:
version: 10
- uses: actions/setup-node@v6
with:
node-version: 'lts/*'
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Get current version

View File

@@ -82,7 +82,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: 'lts/*'
node-version-file: '.nvmrc'
cache: 'pnpm'
registry-url: https://registry.npmjs.org

View File

@@ -22,7 +22,7 @@ jobs:
version: 10
- uses: actions/setup-node@v6
with:
node-version: 'lts/*'
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Get current version

View File

@@ -149,7 +149,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: lts/*
node-version-file: '.nvmrc'
- name: Bump version
id: bump-version

View File

@@ -58,7 +58,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '24.x'
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Bump desktop-ui version

View File

@@ -35,7 +35,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version: '20'
node-version-file: '.nvmrc'
cache: 'pnpm'
- name: Install dependencies for analysis tools

View File

@@ -35,7 +35,7 @@
}
],
"no-control-regex": "off",
"no-eval": "off",
"no-eval": "error",
"no-redeclare": "error",
"no-restricted-imports": [
"error",

View File

@@ -17,7 +17,7 @@ Have another idea? Drop into Discord or open an issue, and let's chat!
### Prerequisites & Technology Stack
- **Required Software**:
- Node.js (v24) and pnpm
- Node.js (see `.nvmrc`, currently v24) and pnpm
- Git for version control
- A running ComfyUI backend instance (otherwise, you can use `pnpm dev:cloud`)

View File

@@ -1,10 +1,7 @@
<template>
<div
ref="rootEl"
class="relative overflow-hidden h-full w-full bg-neutral-900"
>
<div class="p-terminal rounded-none h-full w-full p-2">
<div ref="terminalEl" class="h-full terminal-host" />
<div ref="rootEl" class="relative size-full overflow-hidden bg-neutral-900">
<div class="p-terminal size-full rounded-none p-2">
<div ref="terminalEl" class="terminal-host h-full" />
</div>
<Button
v-tooltip.left="{
@@ -16,7 +13,7 @@
size="small"
:class="
cn('absolute top-2 right-8 transition-opacity', {
'opacity-0 pointer-events-none select-none': !isHovered
'pointer-events-none opacity-0 select-none': !isHovered
})
"
:aria-label="tooltipText"

View File

@@ -1,12 +1,12 @@
<template>
<div class="flex flex-col gap-8 w-full max-w-3xl mx-auto select-none">
<div class="mx-auto flex w-full max-w-3xl flex-col gap-8 select-none">
<!-- Installation Path Section -->
<div class="grow flex flex-col gap-6 text-neutral-300">
<h2 class="font-inter font-bold text-3xl text-neutral-100 text-center">
<div class="flex grow flex-col gap-6 text-neutral-300">
<h2 class="text-center font-inter text-3xl font-bold text-neutral-100">
{{ $t('install.locationPicker.title') }}
</h2>
<p class="text-center text-neutral-400 px-12">
<p class="px-12 text-center text-neutral-400">
{{ $t('install.locationPicker.subtitle') }}
</p>
@@ -15,7 +15,7 @@
<InputText
v-model="installPath"
:placeholder="$t('install.locationPicker.pathPlaceholder')"
class="flex-1 bg-neutral-800/50 border-neutral-700 text-neutral-200 placeholder:text-neutral-500"
class="flex-1 border-neutral-700 bg-neutral-800/50 text-neutral-200 placeholder:text-neutral-500"
:class="{ 'p-invalid': pathError }"
@update:model-value="validatePath"
@focus="onFocus"
@@ -23,7 +23,7 @@
<Button
icon="pi pi-folder-open"
severity="secondary"
class="bg-neutral-700 hover:bg-neutral-600 border-0"
class="border-0 bg-neutral-700 hover:bg-neutral-600"
@click="browsePath"
/>
</div>
@@ -33,7 +33,7 @@
<Message
v-if="pathError"
severity="error"
class="whitespace-pre-line w-full"
class="w-full whitespace-pre-line"
>
{{ pathError }}
</Message>

View File

@@ -26,7 +26,7 @@
<img
v-if="task.headerImg"
:src="task.headerImg"
class="h-full w-full object-contain px-4 pt-4 opacity-25"
class="size-full object-contain px-4 pt-4 opacity-25"
/>
</template>
<template #title>
@@ -52,7 +52,7 @@
<i
v-if="!isLoading && runner.state === 'OK'"
class="pi pi-check pointer-events-none absolute -right-4 -bottom-4 col-span-full row-span-full z-10 text-[4rem] text-green-500 opacity-100 transition-opacity group-hover/task-card:opacity-20 [text-shadow:0.25rem_0_0.5rem_black]"
class="pi pi-check pointer-events-none absolute -right-4 -bottom-4 z-10 col-span-full row-span-full text-[4rem] text-green-500 opacity-100 transition-opacity [text-shadow:0.25rem_0_0.5rem_black] group-hover/task-card:opacity-20"
/>
</div>
</template>

View File

@@ -4,7 +4,7 @@
<template v-if="filter.tasks.length === 0">
<!-- Empty filter -->
<Divider />
<p class="text-neutral-400 w-full text-center">
<p class="w-full text-center text-neutral-400">
{{ $t('maintenance.allOk') }}
</p>
</template>
@@ -25,7 +25,7 @@
<!-- Display: Cards -->
<template v-else>
<div class="flex flex-wrap justify-evenly gap-8 pad-y my-4">
<div class="pad-y my-4 flex flex-wrap justify-evenly gap-8">
<TaskCard
v-for="task in filter.tasks"
:key="task.id"
@@ -45,7 +45,8 @@ import { useConfirm, useToast } from 'primevue'
import ConfirmPopup from 'primevue/confirmpopup'
import Divider from 'primevue/divider'
import { t } from '@/i18n'
import { useI18n } from 'vue-i18n'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import type {
MaintenanceFilter,
@@ -55,6 +56,7 @@ import type {
import TaskCard from './TaskCard.vue'
import TaskListItem from './TaskListItem.vue'
const { t } = useI18n()
const toast = useToast()
const confirm = useConfirm()
const taskStore = useMaintenanceTaskStore()
@@ -80,8 +82,7 @@ const executeTask = async (task: MaintenanceTask) => {
toast.add({
severity: 'error',
summary: t('maintenance.error.toastTitle'),
detail: message ?? t('maintenance.error.defaultDescription'),
life: 10_000
detail: message ?? t('maintenance.error.defaultDescription')
})
}

View File

@@ -1,6 +1,6 @@
<template>
<div class="w-full h-full flex flex-col rounded-lg p-6 justify-between">
<h1 class="font-inter font-semibold text-xl m-0 italic">
<div class="flex size-full flex-col justify-between rounded-lg p-6">
<h1 class="m-0 font-inter text-xl font-semibold italic">
{{ $t(`desktopDialogs.${id}.title`, title) }}
</h1>
<p class="whitespace-pre-wrap">

View File

@@ -1,7 +1,7 @@
<template>
<BaseViewTemplate dark>
<div
class="h-screen w-screen grid items-center justify-around overflow-y-auto"
class="grid h-screen w-screen items-center justify-around overflow-y-auto"
>
<div class="relative m-8 text-center">
<!-- Header -->
@@ -13,7 +13,7 @@
<span>{{ $t('desktopUpdate.description') }}</span>
</div>
<ProgressSpinner class="m-8 w-48 h-48" />
<ProgressSpinner class="m-8 size-48" />
<!-- Console button -->
<Button

View File

@@ -1,10 +1,10 @@
<template>
<BaseViewTemplate dark>
<!-- Fixed height container with flexbox layout for proper content management -->
<div class="w-full h-full flex flex-col">
<div class="flex size-full flex-col">
<Stepper
v-model:value="currentStep"
class="flex flex-col h-full"
class="flex h-full flex-col"
@update:value="handleStepChange"
>
<!-- Main content area that grows to fill available space -->
@@ -37,7 +37,7 @@
<!-- Install footer with navigation -->
<InstallFooter
class="w-full max-w-2xl my-6 mx-auto"
class="mx-auto my-6 w-full max-w-2xl"
:current-step
:can-proceed
:disable-location-step="noGpu"

View File

@@ -1,21 +1,21 @@
<template>
<BaseViewTemplate dark>
<div
class="min-w-full min-h-full font-sans w-screen h-screen grid justify-around text-neutral-300 bg-neutral-900 dark-theme overflow-y-auto"
class="dark-theme grid h-screen min-h-full w-screen min-w-full justify-around overflow-y-auto bg-neutral-900 font-sans text-neutral-300"
>
<div class="max-w-(--breakpoint-sm) w-screen m-8 relative">
<div class="relative m-8 w-screen max-w-(--breakpoint-sm)">
<!-- Header -->
<h1 class="backspan pi-wrench text-4xl font-bold">
{{ t('maintenance.title') }}
</h1>
<!-- Toolbar -->
<div class="w-full flex flex-wrap gap-4 items-center">
<div class="flex w-full flex-wrap items-center gap-4">
<span class="grow">
{{ t('maintenance.status') }}:
<StatusTag :refreshing="isRefreshing" :error="anyErrors" />
</span>
<div class="flex gap-4 items-center">
<div class="flex items-center gap-4">
<SelectButton
v-model="displayAsList"
:options="[PrimeIcons.LIST, PrimeIcons.TH_LARGE]"
@@ -56,10 +56,10 @@
:value="t('icon.exclamation-triangle')"
/>
<span>
<strong class="block mb-1">
<strong class="mb-1 block">
{{ t('maintenance.unsafeMigration.title') }}
</strong>
<span class="block mb-1">
<span class="mb-1 block">
{{ unsafeReasonText }}
</span>
<span class="block text-sm text-neutral-400">
@@ -71,13 +71,13 @@
<!-- Tasks -->
<TaskListPanel
class="border-neutral-700 border-solid border-x-0 border-y"
class="border-x-0 border-y border-solid border-neutral-700"
:filter
:display-as-list
/>
<!-- Actions -->
<div class="flex justify-between gap-4 flex-row">
<div class="flex flex-row justify-between gap-4">
<Button
:label="t('maintenance.consoleLogs')"
icon="pi pi-desktop"
@@ -189,8 +189,7 @@ const completeValidation = async () => {
toast.add({
severity: 'error',
summary: t('g.error'),
detail: t('maintenance.error.cannotContinue'),
life: 5_000
detail: t('maintenance.error.cannotContinue')
})
}
}

View File

@@ -1,8 +1,8 @@
<template>
<BaseViewTemplate dark hide-language-selector>
<div class="h-full p-8 2xl:p-16 flex flex-col items-center justify-center">
<div class="flex h-full flex-col items-center justify-center p-8 2xl:p-16">
<div
class="bg-neutral-800 rounded-lg shadow-lg p-6 w-full max-w-[600px] flex flex-col gap-6"
class="flex w-full max-w-[600px] flex-col gap-6 rounded-lg bg-neutral-800 p-6 shadow-lg"
>
<h2 class="text-3xl font-semibold text-neutral-100">
{{ $t('install.helpImprove') }}
@@ -15,7 +15,7 @@
<a
href="https://comfy.org/privacy"
target="_blank"
class="text-blue-400 hover:text-blue-300 underline"
class="text-blue-400 underline hover:text-blue-300"
>
{{ $t('install.privacyPolicy') }} </a
>.
@@ -33,7 +33,7 @@
}}
</span>
</div>
<div class="flex pt-6 justify-end">
<div class="flex justify-end pt-6">
<Button
:label="$t('g.ok')"
icon="pi pi-check"
@@ -72,8 +72,7 @@ const updateConsent = async () => {
toast.add({
severity: 'error',
summary: t('install.settings.errorUpdatingConsent'),
detail: t('install.settings.errorUpdatingConsentDetail'),
life: 3000
detail: t('install.settings.errorUpdatingConsentDetail')
})
} finally {
isUpdating.value = false

View File

@@ -9,7 +9,7 @@
/>
<div class="no-drag sad-text flex items-center">
<div class="flex flex-col gap-8 p-8 min-w-110">
<div class="flex min-w-110 flex-col gap-8 p-8">
<!-- Header -->
<h1 class="text-4xl font-bold text-red-500">
{{ $t('notSupported.title') }}
@@ -20,7 +20,7 @@
<p class="text-xl">
{{ $t('notSupported.message') }}
</p>
<ul class="list-disc list-inside space-y-1 text-neutral-800">
<ul class="list-inside list-disc space-y-1 text-neutral-800">
<li>{{ $t('notSupported.supportedDevices.macos') }}</li>
<li>{{ $t('notSupported.supportedDevices.windows') }}</li>
</ul>

View File

@@ -2,14 +2,14 @@
<BaseViewTemplate dark>
<div class="relative min-h-screen">
<!-- Terminal Background Layer (always visible during loading) -->
<div v-if="!isError" class="fixed inset-0 overflow-hidden z-0">
<div class="h-full w-full">
<div v-if="!isError" class="fixed inset-0 z-0 overflow-hidden">
<div class="size-full">
<BaseTerminal @created="terminalCreated" />
</div>
</div>
<!-- Semi-transparent overlay -->
<div v-if="!isError" class="fixed inset-0 bg-neutral-900/80 z-5"></div>
<div v-if="!isError" class="fixed inset-0 z-5 bg-neutral-900/80"></div>
<!-- Smooth radial gradient overlay -->
<div
@@ -45,9 +45,9 @@
<!-- Error Section (positioned at bottom) -->
<div
v-if="isError"
class="absolute bottom-20 left-0 right-0 flex flex-col items-center gap-4"
class="absolute inset-x-0 bottom-20 flex flex-col items-center gap-4"
>
<div class="flex gap-4 justify-center">
<div class="flex justify-center gap-4">
<Button
icon="pi pi-flag"
:label="$t('serverStart.reportIssue')"
@@ -71,10 +71,10 @@
<!-- Terminal Output (positioned at bottom when manually toggled in error state) -->
<div
v-if="terminalVisible && isError"
class="absolute bottom-4 left-4 right-4 max-w-4xl mx-auto z-10"
class="absolute inset-x-4 bottom-4 z-10 mx-auto max-w-4xl"
>
<div
class="bg-neutral-900/95 rounded-lg p-4 border border-neutral-700 h-[300px]"
class="h-[300px] rounded-lg border border-neutral-700 bg-neutral-900/95 p-4"
>
<BaseTerminal @created="terminalCreated" />
</div>

View File

@@ -27,7 +27,8 @@ cp -r tools/devtools/* /path/to/your/ComfyUI/custom_nodes/ComfyUI_devtools/
### Node.js & Playwright Prerequisites
Ensure you have Node.js v20 or v22 installed. Then, set up the Chromium test driver:
Ensure you have the Node.js version from `.nvmrc` installed (currently v24).
Then, set up the Chromium test driver:
```bash
pnpm exec playwright install chromium --with-deps

View File

@@ -0,0 +1,760 @@
{
"id": "9a37f747-e96b-4304-9212-7abcaad7bdac",
"revision": 0,
"last_node_id": 11,
"last_link_id": 18,
"nodes": [
{
"id": 2,
"type": "PreviewAny",
"pos": [1031, 434],
"size": [250, 178],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 5
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewAny"
},
"widgets_values": [null, null, null]
},
{
"id": 5,
"type": "1e38d8ea-45e1-48a5-aa20-966584201867",
"pos": [788, 433.5],
"size": [225, 380],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "string_a",
"type": "STRING",
"widget": {
"name": "string_a"
},
"link": 4
}
],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [5]
}
],
"properties": {
"proxyWidgets": [
["3", "string_a"],
["4", "value"],
["6", "value"],
["6", "value_1"]
]
},
"widgets_values": []
},
{
"id": 1,
"type": "PrimitiveStringMultiline",
"pos": [548, 451],
"size": [225, 142],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [4]
}
],
"title": "Outer",
"properties": {
"Node name for S&R": "PrimitiveStringMultiline"
},
"widgets_values": ["Outer\n"]
}
],
"links": [
[4, 1, 0, 5, 0, "STRING"],
[5, 5, 0, 2, 0, "STRING"]
],
"groups": [],
"definitions": {
"subgraphs": [
{
"id": "1e38d8ea-45e1-48a5-aa20-966584201867",
"version": 1,
"state": {
"lastGroupId": 0,
"lastNodeId": 11,
"lastLinkId": 18,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "Sub 0",
"inputNode": {
"id": -10,
"bounding": [351, 432.5, 120, 120]
},
"outputNode": {
"id": -20,
"bounding": [1352, 294.5, 120, 60]
},
"inputs": [
{
"id": "7bf3e1d4-0521-4b5c-92f5-47ca598b7eb4",
"name": "string_a",
"type": "STRING",
"linkIds": [1],
"localized_name": "string_a",
"pos": [451, 452.5]
},
{
"id": "5fb3dcf7-9bfd-4b3c-a1b9-750b4f3edf19",
"name": "value",
"type": "STRING",
"linkIds": [13],
"pos": [451, 472.5]
},
{
"id": "55d24b8a-7c82-4b02-8e3d-ff31ffb8aa13",
"name": "value_1",
"type": "STRING",
"linkIds": [16],
"pos": [451, 492.5]
},
{
"id": "c1fe7cc3-547e-4fb0-b763-61888558d4bd",
"name": "value_1_1",
"type": "STRING",
"linkIds": [18],
"pos": [451, 512.5]
}
],
"outputs": [
{
"id": "fbe975ba-d7c2-471e-a99a-a1e2c6ab466d",
"name": "STRING",
"type": "STRING",
"linkIds": [9],
"localized_name": "STRING",
"pos": [1372, 314.5]
}
],
"widgets": [],
"nodes": [
{
"id": 4,
"type": "PrimitiveStringMultiline",
"pos": [504, 437],
"size": [210, 88],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "value",
"name": "value",
"type": "STRING",
"widget": {
"name": "value"
},
"link": 13
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [2]
}
],
"title": "Inner 1",
"properties": {
"Node name for S&R": "PrimitiveStringMultiline"
},
"widgets_values": ["Inner 1\n"]
},
{
"id": 3,
"type": "StringConcatenate",
"pos": [743, 325],
"size": [347, 231],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"localized_name": "string_a",
"name": "string_a",
"type": "STRING",
"widget": {
"name": "string_a"
},
"link": 1
},
{
"localized_name": "string_b",
"name": "string_b",
"type": "STRING",
"widget": {
"name": "string_b"
},
"link": 2
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [7]
}
],
"properties": {
"Node name for S&R": "StringConcatenate"
},
"widgets_values": ["", "", ""]
},
{
"id": 6,
"type": "9be42452-056b-4c99-9f9f-7381d11c4454",
"pos": [1115, 301],
"size": [210, 196],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"localized_name": "string_a",
"name": "string_a",
"type": "STRING",
"widget": {
"name": "string_a"
},
"link": 7
},
{
"name": "value",
"type": "STRING",
"widget": {
"name": "value"
},
"link": 16
},
{
"name": "value_1",
"type": "STRING",
"widget": {
"name": "value_1"
},
"link": 18
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [9]
}
],
"properties": {
"proxyWidgets": [
["5", "string_a"],
["11", "value"],
["9", "value"],
["10", "string_a"]
]
},
"widgets_values": []
}
],
"groups": [],
"links": [
{
"id": 2,
"origin_id": 4,
"origin_slot": 0,
"target_id": 3,
"target_slot": 1,
"type": "STRING"
},
{
"id": 1,
"origin_id": -10,
"origin_slot": 0,
"target_id": 3,
"target_slot": 0,
"type": "STRING"
},
{
"id": 7,
"origin_id": 3,
"origin_slot": 0,
"target_id": 6,
"target_slot": 0,
"type": "STRING"
},
{
"id": 6,
"origin_id": 6,
"origin_slot": 0,
"target_id": -20,
"target_slot": 1,
"type": "STRING"
},
{
"id": 9,
"origin_id": 6,
"origin_slot": 0,
"target_id": -20,
"target_slot": 0,
"type": "STRING"
},
{
"id": 13,
"origin_id": -10,
"origin_slot": 1,
"target_id": 4,
"target_slot": 0,
"type": "STRING"
},
{
"id": 16,
"origin_id": -10,
"origin_slot": 2,
"target_id": 6,
"target_slot": 1,
"type": "STRING"
},
{
"id": 18,
"origin_id": -10,
"origin_slot": 3,
"target_id": 6,
"target_slot": 2,
"type": "STRING"
}
],
"extra": {}
},
{
"id": "9be42452-056b-4c99-9f9f-7381d11c4454",
"version": 1,
"state": {
"lastGroupId": 0,
"lastNodeId": 11,
"lastLinkId": 18,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "Sub 1",
"inputNode": {
"id": -10,
"bounding": [180, 739, 120, 100]
},
"outputNode": {
"id": -20,
"bounding": [1246, 612, 120, 60]
},
"inputs": [
{
"id": "01c05c51-86b5-4bad-b32f-9c911683a13d",
"name": "string_a",
"type": "STRING",
"linkIds": [4],
"localized_name": "string_a",
"pos": [280, 759]
},
{
"id": "d50f6a62-0185-43d4-a174-a8a94bd8f6e7",
"name": "value",
"type": "STRING",
"linkIds": [14],
"pos": [280, 779]
},
{
"id": "6b78450e-5986-49cd-b743-c933e5a34a69",
"name": "value_1",
"type": "STRING",
"linkIds": [17],
"pos": [280, 799]
}
],
"outputs": [
{
"id": "a8bcf3bf-a66a-4c71-8d92-17a2a4d03686",
"name": "STRING",
"type": "STRING",
"linkIds": [12],
"localized_name": "STRING",
"pos": [1266, 632]
}
],
"widgets": [],
"nodes": [
{
"id": 11,
"type": "PrimitiveStringMultiline",
"pos": [334, 742],
"size": [210, 88],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"localized_name": "value",
"name": "value",
"type": "STRING",
"widget": {
"name": "value"
},
"link": 14
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [7]
}
],
"title": "Inner 2",
"properties": {
"Node name for S&R": "PrimitiveStringMultiline"
},
"widgets_values": ["Inner 2\n"]
},
{
"id": 10,
"type": "StringConcatenate",
"pos": [581, 637],
"size": [400, 200],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "string_a",
"name": "string_a",
"type": "STRING",
"widget": {
"name": "string_a"
},
"link": 4
},
{
"localized_name": "string_b",
"name": "string_b",
"type": "STRING",
"widget": {
"name": "string_b"
},
"link": 7
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [11]
}
],
"properties": {
"Node name for S&R": "StringConcatenate"
},
"widgets_values": ["", "", ""]
},
{
"id": 9,
"type": "7c2915a5-5eb8-4958-a8fd-4beb30f370ce",
"pos": [1004, 613],
"size": [210, 142],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"localized_name": "string_a",
"name": "string_a",
"type": "STRING",
"widget": {
"name": "string_a"
},
"link": 11
},
{
"name": "value",
"type": "STRING",
"widget": {
"name": "value"
},
"link": 17
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [12]
}
],
"properties": {
"proxyWidgets": [
["7", "string_a"],
["8", "value"]
]
},
"widgets_values": []
}
],
"groups": [],
"links": [
{
"id": 4,
"origin_id": -10,
"origin_slot": 0,
"target_id": 10,
"target_slot": 0,
"type": "STRING"
},
{
"id": 7,
"origin_id": 11,
"origin_slot": 0,
"target_id": 10,
"target_slot": 1,
"type": "STRING"
},
{
"id": 11,
"origin_id": 10,
"origin_slot": 0,
"target_id": 9,
"target_slot": 0,
"type": "STRING"
},
{
"id": 10,
"origin_id": 9,
"origin_slot": 0,
"target_id": -20,
"target_slot": 0,
"type": "STRING"
},
{
"id": 12,
"origin_id": 9,
"origin_slot": 0,
"target_id": -20,
"target_slot": 0,
"type": "STRING"
},
{
"id": 14,
"origin_id": -10,
"origin_slot": 1,
"target_id": 11,
"target_slot": 0,
"type": "STRING"
},
{
"id": 17,
"origin_id": -10,
"origin_slot": 2,
"target_id": 9,
"target_slot": 1,
"type": "STRING"
}
],
"extra": {}
},
{
"id": "7c2915a5-5eb8-4958-a8fd-4beb30f370ce",
"version": 1,
"state": {
"lastGroupId": 0,
"lastNodeId": 11,
"lastLinkId": 18,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "Sub 2",
"inputNode": {
"id": -10,
"bounding": [262, 1222, 120, 80]
},
"outputNode": {
"id": -20,
"bounding": [1123.089999999999, 1125.1999999999998, 120, 60]
},
"inputs": [
{
"id": "934a8baa-d79c-428c-8ec9-814ad437d7c7",
"name": "string_a",
"type": "STRING",
"linkIds": [9],
"localized_name": "string_a",
"pos": [362, 1242]
},
{
"id": "3a545207-7202-42a9-a82f-3b62e1b0f459",
"name": "value",
"type": "STRING",
"linkIds": [15],
"pos": [362, 1262]
}
],
"outputs": [
{
"id": "4c3d243b-9ff6-4dcd-9dbf-e4ec8e1fc879",
"name": "STRING",
"type": "STRING",
"linkIds": [10],
"localized_name": "STRING",
"pos": [1143.089999999999, 1145.1999999999998]
}
],
"widgets": [],
"nodes": [
{
"id": 8,
"type": "PrimitiveStringMultiline",
"pos": [412.96000000000004, 1228.2399999999996],
"size": [210, 88],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "value",
"name": "value",
"type": "STRING",
"widget": {
"name": "value"
},
"link": 15
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [8]
}
],
"title": "Inner 3",
"properties": {
"Node name for S&R": "PrimitiveStringMultiline"
},
"widgets_values": ["Inner 3\n"]
},
{
"id": 7,
"type": "StringConcatenate",
"pos": [686.08, 1132.38],
"size": [400, 200],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"localized_name": "string_a",
"name": "string_a",
"type": "STRING",
"widget": {
"name": "string_a"
},
"link": 9
},
{
"localized_name": "string_b",
"name": "string_b",
"type": "STRING",
"widget": {
"name": "string_b"
},
"link": 8
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [10]
}
],
"properties": {
"Node name for S&R": "StringConcatenate"
},
"widgets_values": ["", "", ""]
}
],
"groups": [],
"links": [
{
"id": 8,
"origin_id": 8,
"origin_slot": 0,
"target_id": 7,
"target_slot": 1,
"type": "STRING"
},
{
"id": 9,
"origin_id": -10,
"origin_slot": 0,
"target_id": 7,
"target_slot": 0,
"type": "STRING"
},
{
"id": 10,
"origin_id": 7,
"origin_slot": 0,
"target_id": -20,
"target_slot": 0,
"type": "STRING"
},
{
"id": 15,
"origin_id": -10,
"origin_slot": 1,
"target_id": 8,
"target_slot": 0,
"type": "STRING"
}
],
"extra": {}
}
]
},
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [-412, 11]
},
"frontendVersion": "1.41.7"
},
"version": 0.4
}

View File

@@ -36,14 +36,7 @@
"properties": {
"Node name for S&R": "CheckpointLoaderSimple",
"cnr_id": "comfy-core",
"ver": "0.3.65",
"models": [
{
"name": "v1-5-pruned-emaonly-fp16.safetensors",
"url": "https://huggingface.co/Comfy-Org/stable-diffusion-v1-5-archive/resolve/main/v1-5-pruned-emaonly-fp16.safetensors?download=true",
"directory": "checkpoints"
}
]
"ver": "0.3.65"
},
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
},

View File

@@ -206,9 +206,7 @@ export class ComfyPage {
this.widgetTextBox = page.getByPlaceholder('text').nth(1)
this.resetViewButton = page.getByRole('button', { name: 'Reset View' })
this.queueButton = page.getByRole('button', { name: 'Queue Prompt' })
this.runButton = page
.getByTestId(TestIds.topbar.queueButton)
.getByRole('button', { name: 'Run' })
this.runButton = page.getByTestId(TestIds.topbar.queueButton)
this.workflowUploadInput = page.locator('#comfy-file-input')
this.searchBox = new ComfyNodeSearchBox(page)
@@ -432,7 +430,10 @@ export const comfyPageFixture = base.extend<{
'Comfy.VueNodes.AutoScaleLayout': false,
// Disable toast warning about version compatibility, as they may or
// may not appear - depending on upstream ComfyUI dependencies
'Comfy.VersionCompatibility.DisableWarnings': true
'Comfy.VersionCompatibility.DisableWarnings': true,
// Browser tests should opt into missing-model warnings explicitly so
// workflows do not render differently based on models present on disk.
'Comfy.Workflow.ShowMissingModelsWarning': false
})
} catch (e) {
console.error(e)

View File

@@ -172,6 +172,19 @@ export class VueNodeHelpers {
async enterSubgraph(nodeId?: string): Promise<void> {
const locator = nodeId ? this.getNodeLocator(nodeId) : this.page
const editButton = locator.getByTestId(TestIds.widgets.subgraphEnterButton)
await editButton.click()
// The footer tab button extends below the node body (visible area),
// but its bounding box center overlaps the node body div.
// Click at the bottom 25% of the button which is the genuinely visible
// and unobstructed area outside the node body boundary.
const box = await editButton.boundingBox()
if (!box) {
throw new Error(
'subgraph-enter-button has no bounding box: element may be hidden or not in DOM'
)
}
await editButton.click({
position: { x: box.width / 2, y: box.height * 0.75 }
})
}
}

View File

@@ -55,15 +55,22 @@ export class ComfyNodeSearchBox {
async fillAndSelectFirstNode(
nodeName: string,
options?: { suggestionIndex: number }
options?: { suggestionIndex?: number; exact?: boolean }
) {
await this.input.waitFor({ state: 'visible' })
await this.input.fill(nodeName)
await this.dropdown.waitFor({ state: 'visible' })
await this.dropdown
.locator('li')
.nth(options?.suggestionIndex || 0)
.click()
if (options?.exact) {
await this.dropdown
.locator(`li[aria-label="${nodeName}"]`)
.first()
.click()
} else {
await this.dropdown
.locator('li')
.nth(options?.suggestionIndex || 0)
.click()
}
}
async addFilter(filterValue: string, filterType: string) {

View File

@@ -33,6 +33,7 @@ export const TestIds = {
},
topbar: {
queueButton: 'queue-button',
queueModeMenuTrigger: 'queue-mode-menu-trigger',
saveButton: 'save-workflow-button'
},
nodeLibrary: {

View File

@@ -29,8 +29,10 @@ class ComfyQueueButton {
public readonly dropdownButton: Locator
constructor(public readonly actionbar: ComfyActionbar) {
this.root = actionbar.root.getByTestId(TestIds.topbar.queueButton)
this.primaryButton = this.root.locator('.p-splitbutton-button')
this.dropdownButton = this.root.locator('.p-splitbutton-dropdown')
this.primaryButton = this.root
this.dropdownButton = actionbar.root.getByTestId(
TestIds.topbar.queueModeMenuTrigger
)
}
public async toggleOptions() {

View File

@@ -89,6 +89,17 @@ test.describe('Execution error', () => {
})
test.describe('Missing models warning', () => {
test('Should be disabled by default in browser tests', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('missing/missing_models')
const dialogTitle = comfyPage.page.getByText(
'This workflow is missing models'
)
await expect(dialogTitle).not.toBeVisible()
})
test.beforeEach(async ({ comfyPage }) => {
await comfyPage.settings.setSetting(
'Comfy.Workflow.ShowMissingModelsWarning',

Binary file not shown.

Before

Width:  |  Height:  |  Size: 99 KiB

After

Width:  |  Height:  |  Size: 100 KiB

View File

@@ -171,6 +171,9 @@ test.describe('Node Interaction', () => {
test('Can drag node', { tag: '@screenshot' }, async ({ comfyPage }) => {
await comfyPage.nodeOps.dragTextEncodeNode2()
// Move mouse away to avoid hover highlight on the node at the drop position.
await comfyPage.canvasOps.moveMouseToEmptyArea()
await comfyPage.nextFrame()
await expect(comfyPage.canvas).toHaveScreenshot('dragged-node1.png')
})
@@ -816,16 +819,13 @@ test.describe('Load workflow', { tag: '@screenshot' }, () => {
await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()
const activeWorkflowName =
await comfyPage.menu.workflowsTab.getActiveWorkflowName()
const workflowPathA = `${workflowA}.json`
const workflowPathB = `${workflowB}.json`
expect(openWorkflows).toEqual(
expect.arrayContaining([workflowPathA, workflowPathB])
expect.arrayContaining([workflowA, workflowB])
)
expect(openWorkflows.indexOf(workflowPathA)).toBeLessThan(
openWorkflows.indexOf(workflowPathB)
expect(openWorkflows.indexOf(workflowA)).toBeLessThan(
openWorkflows.indexOf(workflowB)
)
expect(activeWorkflowName).toEqual(workflowPathB)
expect(activeWorkflowName).toEqual(workflowB)
})
})

View File

@@ -29,12 +29,27 @@ test.describe(
// Currently opens missing nodes dialog which is outside scope of AVIF loading functionality
// 'workflow.avif'
]
const filesWithUpload = new Set(['no_workflow.webp'])
fileNames.forEach(async (fileName) => {
test(`Load workflow in ${fileName} (drop from filesystem)`, async ({
comfyPage
}) => {
const shouldUpload = filesWithUpload.has(fileName)
const uploadRequestPromise = shouldUpload
? comfyPage.page.waitForRequest((req) =>
req.url().includes('/upload/')
)
: null
await comfyPage.dragDrop.dragAndDropFile(`workflowInMedia/${fileName}`)
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
if (uploadRequestPromise) {
const request = await uploadRequestPromise
expect(request.url()).toContain('/upload/')
} else {
await expect(comfyPage.canvas).toHaveScreenshot(`${fileName}.png`)
}
})
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 29 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 43 KiB

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -110,7 +110,9 @@ test.describe('Node search box', { tag: '@node' }, () => {
await comfyPage.canvasOps.disconnectEdge()
await expect(comfyPage.searchBox.input).toHaveCount(1)
await comfyPage.page.locator('.p-chip-remove-icon').click()
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler')
await comfyPage.searchBox.fillAndSelectFirstNode('KSampler', {
exact: true
})
await expect(comfyPage.canvas).toHaveScreenshot(
'added-node-no-connection.png'
)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 93 KiB

After

Width:  |  Height:  |  Size: 98 KiB

View File

@@ -67,4 +67,66 @@ test.describe('Performance', { tag: ['@perf'] }, () => {
recordMeasurement(m)
console.log(`Clipping: ${m.layouts} forced layouts`)
})
test('subgraph idle style recalculations', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('subgraphs/nested-subgraph')
await comfyPage.perf.startMeasuring()
for (let i = 0; i < 120; i++) {
await comfyPage.nextFrame()
}
const m = await comfyPage.perf.stopMeasuring('subgraph-idle')
recordMeasurement(m)
console.log(
`Subgraph idle: ${m.styleRecalcs} style recalcs, ${m.layouts} layouts`
)
})
test('subgraph mouse interaction style recalculations', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('subgraphs/nested-subgraph')
await comfyPage.perf.startMeasuring()
const canvas = comfyPage.canvas
const box = await canvas.boundingBox()
if (!box) throw new Error('Canvas bounding box not available')
for (let i = 0; i < 100; i++) {
await comfyPage.page.mouse.move(
box.x + (box.width * i) / 100,
box.y + (box.height * (i % 3)) / 3
)
}
const m = await comfyPage.perf.stopMeasuring('subgraph-mouse-sweep')
recordMeasurement(m)
console.log(
`Subgraph mouse sweep: ${m.styleRecalcs} style recalcs, ${m.layouts} layouts`
)
})
test('subgraph DOM widget clipping during node selection', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow('subgraphs/nested-subgraph')
await comfyPage.perf.startMeasuring()
const canvas = comfyPage.canvas
const box = await canvas.boundingBox()
if (!box) throw new Error('Canvas bounding box not available')
for (let i = 0; i < 20; i++) {
await comfyPage.page.mouse.click(
box.x + box.width / 3 + (i % 5) * 30,
box.y + box.height / 3 + (i % 4) * 30
)
await comfyPage.nextFrame()
}
const m = await comfyPage.perf.stopMeasuring('subgraph-dom-widget-clipping')
recordMeasurement(m)
console.log(`Subgraph clipping: ${m.layouts} forced layouts`)
})
})

View File

@@ -13,9 +13,9 @@ test.describe('Reroute Node', { tag: ['@screenshot', '@node'] }, () => {
})
test('loads from inserted workflow', async ({ comfyPage }) => {
const workflowName = 'single_connected_reroute_node.json'
const workflowName = 'single_connected_reroute_node'
await comfyPage.workflow.setupWorkflowsDirectory({
[workflowName]: 'links/single_connected_reroute_node.json'
[`${workflowName}.json`]: `links/${workflowName}.json`
})
await comfyPage.setup()
await comfyPage.menu.topbar.triggerTopbarCommand(['New'])

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 90 KiB

View File

@@ -21,14 +21,12 @@ test.describe('Workflows sidebar', () => {
test('Can create new blank workflow', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
expect(await tab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json'
])
expect(await tab.getOpenedWorkflowNames()).toEqual(['*Unsaved Workflow'])
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json',
'*Unsaved Workflow (2).json'
'*Unsaved Workflow',
'*Unsaved Workflow (2)'
])
})
@@ -41,37 +39,37 @@ test.describe('Workflows sidebar', () => {
const tab = comfyPage.menu.workflowsTab
await tab.open()
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
expect.arrayContaining(['workflow1.json', 'workflow2.json'])
expect.arrayContaining(['workflow1', 'workflow2'])
)
})
test('Can duplicate workflow', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
await comfyPage.menu.topbar.saveWorkflow('workflow1')
expect(await tab.getTopLevelSavedWorkflowNames()).toEqual(
expect.arrayContaining(['workflow1.json'])
expect.arrayContaining(['workflow1'])
)
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'workflow1.json',
'*workflow1 (Copy).json'
'workflow1',
'*workflow1 (Copy)'
])
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'workflow1.json',
'*workflow1 (Copy).json',
'*workflow1 (Copy) (2).json'
'workflow1',
'*workflow1 (Copy)',
'*workflow1 (Copy) (2)'
])
await comfyPage.command.executeCommand('Comfy.DuplicateWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'workflow1.json',
'*workflow1 (Copy).json',
'*workflow1 (Copy) (2).json',
'*workflow1 (Copy) (3).json'
'workflow1',
'*workflow1 (Copy)',
'*workflow1 (Copy) (2)',
'*workflow1 (Copy) (3)'
])
})
@@ -85,12 +83,12 @@ test.describe('Workflows sidebar', () => {
await comfyPage.command.executeCommand('Comfy.LoadDefaultWorkflow')
const originalNodeCount = await comfyPage.nodeOps.getNodeCount()
await tab.insertWorkflow(tab.getPersistedItem('workflow1.json'))
await tab.insertWorkflow(tab.getPersistedItem('workflow1'))
await expect
.poll(() => comfyPage.nodeOps.getNodeCount())
.toEqual(originalNodeCount + 1)
await tab.getPersistedItem('workflow1.json').click()
await tab.getPersistedItem('workflow1').click()
await expect.poll(() => comfyPage.nodeOps.getNodeCount()).toEqual(1)
})
@@ -113,22 +111,22 @@ test.describe('Workflows sidebar', () => {
const openedWorkflow = tab.getOpenedItem('foo/bar')
await tab.renameWorkflow(openedWorkflow, 'foo/baz')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json',
'foo/baz.json'
'*Unsaved Workflow',
'foo/baz'
])
})
test('Can save workflow as', async ({ comfyPage }) => {
await comfyPage.command.executeCommand('Comfy.NewBlankWorkflow')
await comfyPage.menu.topbar.saveWorkflowAs('workflow3.json')
await comfyPage.menu.topbar.saveWorkflowAs('workflow3')
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow.json', 'workflow3.json'])
.toEqual(['*Unsaved Workflow', 'workflow3'])
await comfyPage.menu.topbar.saveWorkflowAs('workflow4.json')
await comfyPage.menu.topbar.saveWorkflowAs('workflow4')
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow.json', 'workflow3.json', 'workflow4.json'])
.toEqual(['*Unsaved Workflow', 'workflow3', 'workflow4'])
})
test('Exported workflow does not contain localized slot names', async ({
@@ -184,15 +182,15 @@ test.describe('Workflows sidebar', () => {
})
test('Can save workflow as with same name', async ({ comfyPage }) => {
await comfyPage.menu.topbar.saveWorkflow('workflow5.json')
await comfyPage.menu.topbar.saveWorkflow('workflow5')
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'workflow5.json'
'workflow5'
])
await comfyPage.menu.topbar.saveWorkflowAs('workflow5.json')
await comfyPage.menu.topbar.saveWorkflowAs('workflow5')
await comfyPage.confirmDialog.click('overwrite')
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'workflow5.json'
'workflow5'
])
})
@@ -212,25 +210,25 @@ test.describe('Workflows sidebar', () => {
test('Can overwrite other workflows with save as', async ({ comfyPage }) => {
const topbar = comfyPage.menu.topbar
await topbar.saveWorkflow('workflow1.json')
await topbar.saveWorkflowAs('workflow2.json')
await topbar.saveWorkflow('workflow1')
await topbar.saveWorkflowAs('workflow2')
await comfyPage.nextFrame()
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['workflow1.json', 'workflow2.json'])
.toEqual(['workflow1', 'workflow2'])
await expect
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
.toEqual('workflow2.json')
.toEqual('workflow2')
await topbar.saveWorkflowAs('workflow1.json')
await topbar.saveWorkflowAs('workflow1')
await comfyPage.confirmDialog.click('overwrite')
// The old workflow1.json should be deleted and the new one should be saved.
// The old workflow1 should be deleted and the new one should be saved.
await expect
.poll(() => comfyPage.menu.workflowsTab.getOpenedWorkflowNames())
.toEqual(['workflow2.json', 'workflow1.json'])
.toEqual(['workflow2', 'workflow1'])
await expect
.poll(() => comfyPage.menu.workflowsTab.getActiveWorkflowName())
.toEqual('workflow1.json')
.toEqual('workflow1')
})
test('Does not report warning when switching between opened workflows', async ({
@@ -266,17 +264,15 @@ test.describe('Workflows sidebar', () => {
)
await closeButton.click()
expect(await comfyPage.menu.workflowsTab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json'
'*Unsaved Workflow'
])
})
test('Can close saved workflow with command', async ({ comfyPage }) => {
const tab = comfyPage.menu.workflowsTab
await comfyPage.menu.topbar.saveWorkflow('workflow1.json')
await comfyPage.menu.topbar.saveWorkflow('workflow1')
await comfyPage.command.executeCommand('Workspace.CloseWorkflow')
expect(await tab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json'
])
expect(await tab.getOpenedWorkflowNames()).toEqual(['*Unsaved Workflow'])
})
test('Can delete workflows (confirm disabled)', async ({ comfyPage }) => {
@@ -284,7 +280,7 @@ test.describe('Workflows sidebar', () => {
const { topbar, workflowsTab } = comfyPage.menu
const filename = 'workflow18.json'
const filename = 'workflow18'
await topbar.saveWorkflow(filename)
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
@@ -295,14 +291,14 @@ test.describe('Workflows sidebar', () => {
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json'
'*Unsaved Workflow'
])
})
test('Can delete workflows', async ({ comfyPage }) => {
const { topbar, workflowsTab } = comfyPage.menu
const filename = 'workflow18.json'
const filename = 'workflow18'
await topbar.saveWorkflow(filename)
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([filename])
@@ -314,7 +310,7 @@ test.describe('Workflows sidebar', () => {
await expect(workflowsTab.getOpenedItem(filename)).not.toBeVisible()
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json'
'*Unsaved Workflow'
])
})
@@ -326,16 +322,11 @@ test.describe('Workflows sidebar', () => {
const { workflowsTab } = comfyPage.menu
await workflowsTab.open()
await workflowsTab
.getPersistedItem('workflow1.json')
.click({ button: 'right' })
await workflowsTab.getPersistedItem('workflow1').click({ button: 'right' })
await comfyPage.contextMenu.clickMenuItem('Duplicate')
await comfyPage.nextFrame()
expect(await workflowsTab.getOpenedWorkflowNames()).toEqual([
'*Unsaved Workflow.json',
'*workflow1 (Copy).json'
])
await expect
.poll(() => workflowsTab.getOpenedWorkflowNames())
.toEqual(['*Unsaved Workflow', '*workflow1 (Copy)'])
})
test('Can drop workflow from workflows sidebar', async ({ comfyPage }) => {
@@ -347,7 +338,7 @@ test.describe('Workflows sidebar', () => {
// Wait for workflow to appear in Browse section after sync
const workflowItem =
comfyPage.menu.workflowsTab.getPersistedItem('workflow1.json')
comfyPage.menu.workflowsTab.getPersistedItem('workflow1')
await expect(workflowItem).toBeVisible({ timeout: 3000 })
const nodeCount = await comfyPage.nodeOps.getGraphNodesCount()
@@ -364,7 +355,7 @@ test.describe('Workflows sidebar', () => {
}
await comfyPage.page.dragAndDrop(
'.comfyui-workflows-browse .node-label:has-text("workflow1.json")',
'.comfyui-workflows-browse .node-label:has-text("workflow1")',
'#graph-canvas',
{ targetPosition }
)

View File

@@ -555,6 +555,74 @@ test.describe(
})
})
test.describe('Nested Promoted Widget Disabled State', () => {
test('Externally linked promoted widget is disabled, unlinked ones are not', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow(
'subgraphs/subgraph-nested-promotion'
)
await comfyPage.nextFrame()
// Node 5 (Sub 0) has 4 promoted widgets. The first (string_a) has its
// slot connected externally from the Outer node, so it should be
// disabled. The remaining promoted textarea widgets (value, value_1)
// are unlinked and should be enabled.
const promotedNames = await getPromotedWidgetNames(comfyPage, '5')
expect(promotedNames).toContain('string_a')
expect(promotedNames).toContain('value')
const disabledState = await comfyPage.page.evaluate(() => {
const node = window.app!.canvas.graph!.getNodeById('5')
return (node?.widgets ?? []).map((w) => ({
name: w.name,
disabled: !!w.computedDisabled
}))
})
const linkedWidget = disabledState.find((w) => w.name === 'string_a')
expect(linkedWidget?.disabled).toBe(true)
const unlinkedWidgets = disabledState.filter(
(w) => w.name !== 'string_a'
)
for (const w of unlinkedWidgets) {
expect(w.disabled).toBe(false)
}
})
test('Unlinked promoted textarea widgets are editable on the subgraph exterior', async ({
comfyPage
}) => {
await comfyPage.workflow.loadWorkflow(
'subgraphs/subgraph-nested-promotion'
)
await comfyPage.nextFrame()
// The promoted textareas that are NOT externally linked should be
// fully opaque and interactive.
const textareas = comfyPage.page.getByTestId(
TestIds.widgets.domWidgetTextarea
)
await expect(textareas.first()).toBeVisible()
const count = await textareas.count()
for (let i = 0; i < count; i++) {
const textarea = textareas.nth(i)
const wrapper = textarea.locator('..')
const opacity = await wrapper.evaluate(
(el) => getComputedStyle(el).opacity
)
if (opacity === '1' && (await textarea.isEditable())) {
const testContent = `nested-promotion-edit-${i}`
await textarea.fill(testContent)
await expect(textarea).toHaveValue(testContent)
}
}
})
})
test.describe('Promotion Cleanup', () => {
test('Removing subgraph node clears promotion store entries', async ({
comfyPage

Binary file not shown.

Before

Width:  |  Height:  |  Size: 88 KiB

After

Width:  |  Height:  |  Size: 103 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 112 KiB

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 60 KiB

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