Compare commits

...

473 Commits

Author SHA1 Message Date
bymyself
48ac559d00 fix: use v-show for frequently toggled canvas overlay components
Amp-Thread-ID: https://ampcode.com/threads/T-019cbb69-3404-726a-8888-182193115b88
2026-03-04 16:57:38 -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
Johnpaul Chiwetelu
45ca1beea2 fix: address small CodeRabbit issues (#9229)
## Summary

Address several small CodeRabbit-filed issues: clipboard simplification,
queue getter cleanup, pointer handling, and test parameterization.

## Changes

- **What**:
- Simplify `useCopyToClipboard` by using VueUse's built-in `legacy` mode
instead of a manual `document.execCommand` fallback
- Remove `queueIndex` getter alias from `TaskItemImpl`, replace all
usages with `job.priority`
- Add `pointercancel` event handling and try-catch around
`releasePointerCapture` in `useNodeResize` to prevent stuck resize state
- Parameterize repetitive `getNodeProvider` tests in
`modelToNodeStore.test.ts` using `it.each()`

- Fixes #9024
- Fixes #7955
- Fixes #7323
- Fixes #8703

## Review Focus

- `useCopyToClipboard`: VueUse's `legacy: true` enables the
`execCommand` fallback internally — verify browser compat is acceptable
- `useNodeResize`: cleanup logic extracted into shared function used by
both `pointerup` and `pointercancel`
2026-02-26 02:32:53 -08:00
Christian Byrne
aef299caf8 fix: add GLSLShader to canvas image preview node types (#9198)
## Summary

Add `GLSLShader` to `CANVAS_IMAGE_PREVIEW_NODE_TYPES` so GLSL shader
previews are promoted through subgraph nodes.

## Changes

- Add `'GLSLShader'` to the `CANVAS_IMAGE_PREVIEW_NODE_TYPES` set in
`src/composables/node/useNodeCanvasImagePreview.ts`

## Context

GLSLShader node previews were not showing on parent subgraph nodes
because `CANVAS_IMAGE_PREVIEW_NODE_TYPES` only included `PreviewImage`
and `SaveImage`. The `$$canvas-image-preview` pseudo-widget was never
created for GLSLShader nodes, so the promotion system had nothing to
promote. This degraded the UX of all 12 shipped GLSL blueprint subgraphs
— users couldn't see shader output previews without expanding the
subgraph.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9198-fix-add-GLSLShader-to-canvas-image-preview-node-types-3126d73d3650817dbe9beab4bdeaa414)
by [Unito](https://www.unito.io)
2026-02-26 01:15:24 -08:00
Johnpaul Chiwetelu
188fafa89a fix: address trivial CodeRabbit issues (#9196)
## Summary

Address several trivial CodeRabbit-filed issues: type guard extraction,
ESLint globals, curve editor optimizations, and type relocation.

## Changes

- **What**: Extract `isSingleImage()` type guard in WidgetImageCompare;
add `__DISTRIBUTION__`/`__IS_NIGHTLY__` to ESLint globals and remove
stale disable comments; remove unnecessary `toFixed(4)` from curve path
generation; optimize `histogramToPath` with array join; move
`CurvePoint` type to curve domain

- Fixes #9175
- Fixes #8281
- Fixes #9116
- Fixes #9145
- Fixes #9147

## Review Focus

All changes are mechanical/trivial. Curve path output changes from
fixed-precision to raw floats — SVG handles both fine.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9196-fix-address-trivial-CodeRabbit-issues-3126d73d365081f19a5ce20305403098)
by [Unito](https://www.unito.io)
2026-02-26 00:43:14 -08:00
Christian Byrne
3984408d05 docs: add comment explaining widget value store dom widgets getter nuance (#9202)
Adds comment explaining nuance with the differing registration semantics
between DOM widget vs base widet.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9202-fix-widget-value-store-dom-widgets-getter-3126d73d365081368b94f048efb101fa)
by [Unito](https://www.unito.io)
2026-02-25 23:44:33 -08:00
Christian Byrne
6034be9a6f fix: add GLSLShader to toolkit node telemetry tracking (#9197)
## Summary

Add `GLSLShader` to `TOOLKIT_NODE_NAMES` so Mixpanel telemetry tracks
GLSL shader node usage alongside other toolkit nodes.

## Changes

- Add `'GLSLShader'` to the `TOOLKIT_NODE_NAMES` set in
`src/constants/toolkitNodes.ts`

## Context

The Toolkit Nodes PRD defines success metrics that require tracking "%
of workflows using one of these nodes" and "how often each node is
used." GLSLShader was missing from the tracking list, so no
GLSL-specific telemetry was being collected despite 12 GLSL blueprints
shipping in prod (BlueprintsVersion 0.9.1).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9197-fix-add-GLSLShader-to-toolkit-node-telemetry-tracking-3126d73d3650814dad05fa78382d5064)
by [Unito](https://www.unito.io)
2026-02-25 22:19:50 -08:00
Christian Byrne
6a08e4ddde Revert "fix: sync DOM widget values to widgetValueStore on registration" (#9205)
Reverts Comfy-Org/ComfyUI_frontend#9166

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9205-Revert-fix-sync-DOM-widget-values-to-widgetValueStore-on-registration-3126d73d365081df8944d3c6508d2372)
by [Unito](https://www.unito.io)
2026-02-25 21:36:52 -08:00
Hunter
9ff985a792 fix: sync DOM widget default values to widgetValueStore on registration (#9164)
## Description

DOM widgets (textarea/customtext) override the `value` getter via
`Object.defineProperty` to use `getValue()/setValue()` with a fallback
to `inputEl.value`. But `BaseWidget.setNodeId()` registered
`_state.value` (undefined from constructor) instead of `this.value` (the
actual getter).

This caused Vue nodes (Nodes 2.0) to read `undefined` from the store and
display empty textareas, while execution correctly fell back to
`inputEl.value`.

**Fix:** Use `this.value` in `setNodeId()` so the store is initialized
with the actual widget value.

**Impact:** Fixes Nano Banana / Nano Banana Pro `system_prompt` showing
empty in Nodes 2.0 while still sending the correct value during
execution.

## Thread

https://ampcode.com/threads/T-019c8e99-49ce-77f5-bf2a-a32320fac477

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9164-fix-sync-DOM-widget-default-values-to-widgetValueStore-on-registration-3116d73d36508169a2fbd8308d9eec91)
by [Unito](https://www.unito.io)
2026-02-25 21:35:59 -08:00
Terry Jia
5cfd1aa77e feat: add Painter Node (#8521)
## Summary
Add PainterNode widget for freehand mask drawing directly on the canvas,
with brush/eraser tools, opacity, hardness, and background color
controls.

need BE changes https://github.com/Comfy-Org/ComfyUI/pull/12294

## Screenshots (if applicable)


https://github.com/user-attachments/assets/7222063a-0e40-40bb-b72e-b42c8984beb9



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8521-feat-add-Painter-Node-2fa6d73d36508124ab2ede449a0cc67a)
by [Unito](https://www.unito.io)
2026-02-25 21:08:49 -08:00
Christian Byrne
2cb4c5eff3 fix: textarea stays disabled after link disconnect on promoted widgets (#9199)
## Summary

Fix textarea widgets staying disabled after disconnecting a link on
promoted widgets in subgraphs.

## Changes

- **What**: `refreshNodeSlots` used `SafeWidgetData.name` for slot
metadata lookups, but for promoted widgets this is `sourceWidgetName`
(the interior widget name), which doesn't match the subgraph node's
input slot widget name. Added `slotName` field to `SafeWidgetData` to
track the original LiteGraph widget name, and updated `refreshNodeSlots`
to use `slotName ?? name` for correct matching.

## Review Focus

The key change is the `slotName` field on `SafeWidgetData` — it's only
populated when `name !== widget.name` (i.e., for promoted widgets). The
`refreshNodeSlots` function now uses `widget.slotName ?? widget.name` to
look up slot metadata, ensuring promoted widgets correctly update their
`linked` state on disconnect.

Fixes #8818

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9199-fix-textarea-stays-disabled-after-link-disconnect-on-promoted-widgets-3126d73d3650813db499c227e6587aca)
by [Unito](https://www.unito.io)
2026-02-25 20:50:11 -08:00
Benjamin Lu
b8cca4167b fix: show inline progress in QPOV2 despite stale overlay flag (#9214)
## Summary

Fix inline queue progress being hidden in QPOV2 mode when a stale
`Comfy.Queue.History.Expanded` setting remains true from legacy queue
overlay usage.

## Changes

- Update actionbar inline progress hide condition to respect
queue-overlay expansion only when QPOV2 is disabled
- Update top menu inline progress summary hide condition with the same
gate
- Keep legacy behavior unchanged for non-QPOV2 queue overlay mode

## Testing

- `pnpm exec eslint src/components/actionbar/ComfyActionbar.vue
src/components/TopMenuSection.vue` 
- `pnpm typecheck` 

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9214-fix-show-inline-progress-in-QPOV2-despite-stale-overlay-flag-3126d73d36508170ac27fbb26826dca9)
by [Unito](https://www.unito.io)
2026-02-25 20:42:17 -08:00
Benjamin Lu
d99d807c45 fix: open job history from top menu active jobs button (#9210)
## Summary

Make the top menu `N active` queue button open the Job History sidebar
tab when QPO V2 is enabled, so behavior matches the button label and
accessibility text.

## Changes

- Update `TopMenuSection.vue` so QPO V2 mode toggles `job-history`
instead of `assets`
- Update `aria-pressed` logic to track `job-history`
- Update `TopMenuSection` unit tests to assert `job-history`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9210-fix-open-job-history-from-top-menu-active-jobs-button-3126d73d365081758987fa3806b4b0e7)
by [Unito](https://www.unito.io)
2026-02-25 20:40:11 -08:00
jaeone94
80fe51bb8c feat: show missing node packs in Errors Tab with install support (#9213)
## Summary

Surfaces missing node pack information in the Errors Tab, grouped by
registry pack, with one-click install support via ComfyUI Manager.

## Changes

- **What**: Errors Tab now groups missing nodes by their registry pack
and shows a `MissingPackGroupRow` with pack name, node/pack counts, and
an Install button that triggers Manager installation. A
`MissingNodeCard` shows individual unresolvable nodes that have no
associated pack. `useErrorGroups` was extended to resolve missing node
types to their registry packs using the `/api/workflow/missing_nodes`
endpoint. `executionErrorStore` was refactored to track missing node
types separately from execution errors and expose them reactively.
- **Breaking**: None

## Review Focus

- `useErrorGroups.ts` — the new `resolveMissingNodePacks` logic fetches
pack metadata and maps node types to pack IDs; edge cases around partial
resolution (some nodes have a pack, some don't) produce both
`MissingPackGroupRow` and `MissingNodeCard` entries
- `executionErrorStore.ts` — the store now separates `missingNodeTypes`
state from `errors`; the deferred-warnings path in `app.ts` now calls
`setMissingNodeTypes` so the Errors Tab is populated even when a
workflow loads without executing

## Screenshots (if applicable)


https://github.com/user-attachments/assets/97f8d009-0cac-4739-8740-fd3333b5a85b


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9213-feat-show-missing-node-packs-in-Errors-Tab-with-install-support-3126d73d36508197bc4bf8ebfd2125c8)
by [Unito](https://www.unito.io)
2026-02-25 20:25:47 -08:00
Dante
c24c4ab607 feat: show loading spinner and uploading filename during image upload (#9189)
## Summary
- Show a canvas-based loading spinner on image upload nodes (LoadImage)
during file upload via drag-drop, paste, or file picker
- Display the uploading file's name immediately in the filename dropdown
instead of showing the previous file's name
- Show the uploading audio file's name immediately in the audio widget
during upload

## Changes
- **`useNodeImageUpload.ts`**: Add `isUploading` flag and
`onUploadStart` callback to the upload lifecycle; clear `node.imgs`
during upload to prevent stale previews
- **`useImagePreviewWidget.ts`**: Add `renderUploadSpinner` that draws
an animated arc spinner on the canvas when `node.isUploading` is true;
guard against empty `imgs` array
- **`useImageUploadWidget.ts`**: Set `fileComboWidget.value` to the new
filename on upload start; clear `node.imgs` on combo widget change
- **`uploadAudio.ts`**: Set `audioWidget.value` to the new filename on
upload start
- **`litegraph-augmentation.d.ts`**: Add `isUploading` property to
`LGraphNode`



https://github.com/user-attachments/assets/818ce529-cb83-428a-8c98-dd900a128343



## Test plan
- [x] Upload an image via file picker on LoadImage node — spinner shows
during upload, filename updates immediately
- [x] Drag-and-drop an image onto LoadImage node — same behavior
- [x] Paste an image onto LoadImage node — same behavior
- [x] Change the dropdown selection on LoadImage — old preview clears,
new image loads
- [x] Upload an audio file — filename updates immediately in the widget

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9189-feat-show-loading-spinner-and-uploading-filename-during-image-upload-3126d73d365081e4af27cd7252f34298)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-25 20:22:42 -08:00
Christian Byrne
8e215b3174 feat: add performance testing infrastructure with CDP metrics (#9170)
## Summary

Add a permanent, non-failing performance regression detection system
using Chrome DevTools Protocol metrics, with automatic PR commenting.

## Changes

- **What**: Performance testing infrastructure — `PerformanceHelper`
fixture class using CDP `Performance.getMetrics` to collect
`RecalcStyleCount`, `LayoutCount`, `LayoutDuration`, `TaskDuration`,
`JSHeapUsedSize`. Adds `@perf` Playwright project (Chromium-only,
single-threaded, 60s timeout), 4 baseline perf tests, CI workflow with
sticky PR comment reporting, and `perf-report.js` script for generating
markdown comparison tables.

## Review Focus

- `PerformanceHelper` uses `page.context().newCDPSession(page)` — CDP is
Chromium-only, so perf metrics are not collected on Firefox. This is
intentional since CDP gives us browser-level style recalc/layout counts
that `performance.mark/measure` cannot capture.
- The CI workflow uses `continue-on-error: true` so perf tests never
block merging.
- Baseline comparison uses `dawidd6/action-download-artifact` to
download metrics from the target branch, following the same pattern as
`pr-size-report.yaml`.

## Stack

This is the foundation PR for the Firefox performance fix stack:
1. **→ This PR: perf testing infrastructure**
2. `perf/fix-cursor-cache` — cursor style caching (depends on this)
3. `perf/fix-subgraph-svg` — SVG pre-rasterization (depends on this)
4. `perf/fix-clippath-raf` — RAF batching for clip-path (depends on
this)

PRs 2-4 are independent of each other.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9170-feat-add-performance-testing-infrastructure-with-CDP-metrics-3116d73d3650817cb43def6f8e9917f8)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-25 20:09:57 -08:00
Benjamin Lu
c957841862 fix: open previewable assets from list preview click/double-click (#9077)
## Summary
- emit `preview-click` from `AssetsListItem` when clicking the preview
tile
- wire assets sidebar rows and queue job-history rows so preview-tile
click and row double-click open the viewer/gallery
- gate job-history preview opening by `taskRef.previewOutput` (not
`iconImageUrl`) and use preview output URL/type so video previews are
supported
- add/extend tests for preview click and double-click behavior in assets
list and job history

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9077-fix-open-previewable-assets-from-list-preview-click-double-click-30f6d73d3650810a873cfa2dc085bf97)
by [Unito](https://www.unito.io)
2026-02-25 18:03:07 -08:00
Comfy Org PR Bot
d23c8026d0 1.41.6 (#9222)
Patch version increment to 1.41.6

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9222-1-41-6-3136d73d36508199bccbe6e08335bb19)
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-25 17:44:51 -08:00
AustinMroz
a309281ac5 Prevent serialization of progress text to prompt (#9221)
#8625 fixed a bug where `ProgressTextWidget`s would be serialized to
workflow data and, under rare circumstances, clobber over other widget
values on restore.

I was mistaken that the `serialize: false` being sent to options does
serve a purpose: preventing the widget value from being serialized to
the (api) prompt which is sent to the backend. This PR reverts the
removal so now both forms of disabling serialization apply.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9221-Prevent-serialization-of-progress-text-to-prompt-3126d73d365081c5b9ecc560f0a248d5)
by [Unito](https://www.unito.io)
2026-02-25 17:25:12 -08:00
Dante
e9bf113686 feat(settings): improve search to include nav items and show all results (#9195)
## Summary
- Settings search now matches sidebar navigation items (Keybinding,
About, Extension, etc.) and navigates to the corresponding panel
- Search results show all matching settings across all categories
instead of filtering to only the first matching category
- Search result group headers display parent category prefix (e.g.
"LiteGraph › Node") for clarity

## Test plan
- [x] Search "Keybinding" → sidebar highlights and navigates to
Keybinding panel
- [x] Search "badge" → shows all 4 badge settings (3 LiteGraph + 1
Comfy)
- [x] Search "canvas" → shows results from all categories
- [x] Clear search → returns to default category
- [x] Unit tests pass (`pnpm test:unit`)
<img width="1425" height="682" alt="스크린샷 2026-02-25 오후 3 01 05"
src="https://github.com/user-attachments/assets/956c4635-b140-4dff-8145-db312d295160"
/>



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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9195-feat-settings-improve-search-to-include-nav-items-and-show-all-results-3126d73d3650814dbf3ce1d59ad962cf)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
2026-02-25 17:14:37 -08:00
AustinMroz
1ab48b42a7 Add App I/O selection system (#8965)
Adds a system for selecting the inputs and outputs which should be
displayed when inside linear mode. Functions only in litegraph
currently. Vue support will require a separate, larger PR.
Inputs and outputs can be re-ordered by dragging and dropping on the
side panel.

![builder_00001](https://github.com/user-attachments/assets/6345adbd-519e-455d-b71e-0020aa03c6b7)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8965-Add-App-I-O-selection-system-30b6d73d365081569b36c1682a1fdbc5)
by [Unito](https://www.unito.io)
2026-02-25 08:53:00 -08:00
jaeone94
4689581674 feat: enhance manager dialog with initial pack id support (#9169)
## Summary
Adds `initialPackId` support to the manager dialog so callers can
deep-link directly to a specific node pack — pre-filling the search
query, switching to packs search mode, and auto-selecting the matching
pack once results load.

## Changes
- **ManagerDialog.vue**: Added `initialPackId` prop; wires it into
`useRegistrySearch` (forces `packs` mode and pre-fills query) and uses
VueUse `until()` to auto-select the target pack and open the right panel
once `resultsWithKeys` is populated (one-shot, never re-triggers). Also
fixes a latent bug where the effective initial tab (resolving the
persisted tab) was not used when determining the initial search mode and
query — previously `initialTab` (the raw prop) was checked directly,
which would produce incorrect pre-fill when no tab prop was passed but a
Missing tab was persisted.
- **useManagerDialog.ts**: Threads `initialPackId` through `show()` into
the dialog props
- **useManagerState.ts**: Exposes `initialPackId` in `openManager`
options and passes it to `managerDialog.show()`; also removes a stale
fallback `show(ManagerTab.All)` call that was redundant for the
legacy-only error path

### Refactor: remove `executionIdUtil.ts` and distribute its functions
- **`getAncestorExecutionIds` / `getParentExecutionIds`** → moved to
`src/types/nodeIdentification.ts`: both are pure `NodeExecutionId`
string operations with no external dependencies, consistent with the
existing `parseNodeExecutionId` / `createNodeExecutionId` helpers
already in that file
- **`buildSubgraphExecutionPaths`** → moved to
`src/platform/workflow/validation/schemas/workflowSchema.ts`: operates
entirely on `ComfyNode[]` and `SubgraphDefinition` (both defined there),
and `isSubgraphDefinition` is already co-located in the same file
- Tests redistributed accordingly: ancestor/parent ID tests into
`nodeIdentification.test.ts`, `buildSubgraphExecutionPaths` tests into
`workflowSchema.test.ts`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9169-feat-enhance-manager-dialog-with-initial-pack-id-support-3116d73d365081f7b6a3cbfb2f2755bf)
by [Unito](https://www.unito.io)
2026-02-25 22:23:53 +09:00
Benjamin Lu
e4b456bb2c fix: publish desktop-specific frontend release artifact (#9206)
## Summary
- add a desktop-specific frontend release artifact (`dist-desktop.zip`)
in release draft creation
- build `dist-desktop.zip` with `DISTRIBUTION=desktop`
- keep existing `dist.zip` behavior for core/PyPI consumers
- extend `scripts/zipdist.js` to support custom source and output paths

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9206-fix-publish-desktop-specific-frontend-release-artifact-3126d73d3650812495cdf6e9ad2ac280)
by [Unito](https://www.unito.io)
2026-02-25 03:35:41 -08:00
Alexander Brown
482ad401d4 fix: eradicate tailwind @apply usage in vue styles (#9146)
## Summary

Remove Tailwind `@apply` from Vue styles across `src/` and
`apps/desktop-ui/src/` to align with Tailwind v4 guidance, replacing
usages with template utilities or native CSS while preserving behavior.

## Changes

- **What**:
- Batch 1: migrated low-risk template/style utility bundles out of
`@apply`.
- Batch 2: converted PrimeVue/`:deep()` override `@apply` blocks to
native CSS declarations.
- Batch 3: converted `src/components/node/NodeHelpContent.vue` markdown
styling from `@apply` to native CSS/token-based declarations.
- Batch 4: converted final desktop pseudo-element `@apply` styles and
removed stale `@reference` directives no longer required.
- Verified `rg -n "^\s*@apply\b" src apps -g "*.vue"` has no real CSS
`@apply` directives remaining (only known template false-positive event
binding in `NodeSearchContent.vue`).

## Review Focus

- Visual parity in components that previously depended on `@apply` in
`:deep()` selectors and markdown content:
  - topbar tabs/popovers, dialogs, breadcrumb, terminal overrides
  - desktop install/dialog/update/maintenance surfaces
  - node help markdown rendering
- Confirm no regressions from removal of now-unneeded `@reference`
directives.

## Screenshots (if applicable)

- No new screenshots included in this PR.
- Screenshot Playwright suite was run with `--grep="@screenshot"` and
reports baseline diffs in this environment (164 passed, 39 failed, 3
skipped) plus a teardown `EPERM` restore error on local path
`C:\Users\DrJKL\ComfyUI\LTXV\user`.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9146-fix-eradicate-tailwind-apply-usage-in-vue-styles-3116d73d3650813d8642e0bada13df32)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-24 21:23:52 -08:00
Benjamin Lu
9082f6bc3c fix: resolve desktop-ui build failure from icon path cwd mismatch (#9185)
## Summary
Desktop UI production builds were failing in distribution due to an icon
path being resolved from the wrong working directory.

## Problem
`@comfyorg/desktop-ui:build` runs with `cwd: apps/desktop-ui`, but
design-system CSS config includes:
`from-folder(comfy, './packages/design-system/src/icons')`

That relative path only exists from workspace root, so desktop builds
errored with:
`ENOENT: no such file or directory, scandir
'./packages/design-system/src/icons/'`

## Fix
Update the desktop build target to run Vite from workspace root by
removing the app-local `cwd` and using a root-relative config path:
- from: `vite build --config vite.config.mts` with `cwd:
apps/desktop-ui`
- to: `vite build --config apps/desktop-ui/vite.config.mts`

This keeps the icon path resolvable while preserving the same desktop
build config.

## Validation
- `pnpm nx run @comfyorg/desktop-ui:build --skip-nx-cache` 
- `pnpm build:desktop --skip-nx-cache` 

(Separate pre-existing issues remain in `@comfyorg/desktop-ui:typecheck`
and `@comfyorg/desktop-ui:lint`; unchanged by this PR.)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9185-fix-resolve-desktop-ui-build-failure-from-icon-path-cwd-mismatch-3126d73d3650813c94cae25a9240f9b7)
by [Unito](https://www.unito.io)
2026-02-24 20:48:41 -08:00
Comfy Org PR Bot
7c34a0e0f6 1.41.5 (#9182)
Patch version increment to 1.41.5

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9182-1-41-5-3126d73d3650811d84d7eaf9e384567a)
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-24 20:42:38 -08:00
Hunter
8c3738fb77 feat: add Free subscription tier support (#8864)
## Summary

Add frontend support for a Free subscription tier — login/signup page
restructuring, telemetry instrumentation, and tier-aware billing gating.

## Changes

- **What**: 
- Restructure login/signup pages: OAuth buttons promoted as primary
sign-in method, email login available via progressive disclosure
- Add Free tier badge on Google sign-up button with dynamic credit count
from remote config
- Add `FREE` subscription tier to type system (tier pricing, tier rank,
registry types)
  - Add `isFreeTier` computed to `useSubscription()`
- Disable credit top-up for Free tier users (dialogService,
purchaseCredits, popover CTA)
- Show subscription/upgrade dialog instead of top-up dialog when Free
tier user hits out-of-credits
- Add funnel telemetry: `trackLoginOpened`, enrich `trackSignupOpened`
with `free_tier_badge_shown`, track email toggle clicks

## Review Focus

- Tier gating logic: Free tier users should see "Upgrade" instead of
"Add Credits" and never reach the top-up flow
- Telemetry event design for Mixpanel funnel analysis
- Progressive disclosure UX on login/signup pages

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8864-feat-add-Free-subscription-tier-support-3076d73d36508133b84ec5f0a67ccb03)
by [Unito](https://www.unito.io)
2026-02-24 23:28:51 -05:00
Jin Yi
aee207f16c [bugfix] Fix workspace dialog pt override losing base styles (#9188)
## Summary
Workspace dialog `pt` overrides were spreading `workspaceDialogPt` then
replacing `pt.root`, which discarded other `pt` properties from the base
config. This fix removes the redundant overrides so all workspace
dialogs consistently use `workspaceDialogPt` as-is.

## Changes
- **What**: Remove incorrect `pt` spread-and-override pattern in 5
workspace dialog calls
- **Why**: The override replaced the entire `pt` object, losing styles
like `header: { class: 'p-0! hidden' }`

## Review Focus
- Verify that the removed `max-w-[400px]` / `max-w-[512px]` constraints
are either unnecessary or already handled by `workspaceDialogPt` or the
dialog components themselves

<img width="709" height="357" alt="스크린샷 2026-02-25 오후 12 16 08"
src="https://github.com/user-attachments/assets/5020664d-1a8c-478b-a16a-14f59bcf0dde"
/>
<img width="784" height="390" alt="스크린샷 2026-02-25 오후 12 16 03"
src="https://github.com/user-attachments/assets/041dc09d-5639-4880-a95d-a8a6e29e303e"
/>
<img width="551" height="392" alt="스크린샷 2026-02-25 오후 12 15 56"
src="https://github.com/user-attachments/assets/b9769a9d-c0fa-4400-b6d7-0358ba806eaa"
/>


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9188-bugfix-Fix-workspace-dialog-pt-override-losing-base-styles-3126d73d365081b8a73ffc681ccb52a6)
by [Unito](https://www.unito.io)
2026-02-25 12:26:09 +09:00
Jin Yi
164379bf4b [refactor] Redesign missing models dialog (#9014)
## Summary
Redesign the missing models warning dialog to match the MissingNodes
dialog pattern with header/content/footer separation, type badges, file
sizes, and context-sensitive actions.

## Changes
- **What**: Split `MissingModelsWarning.vue` into `MissingModelsHeader`,
`MissingModelsContent`, `MissingModelsFooter` components following the
established MissingNodes pattern. Added model type badges (VAE,
DIFFUSION, LORA, etc.), inline file sizes, total download size, custom
model warnings, and context-sensitive footer buttons (Download all /
Download available / Ok, got it). Extracted security validation into
shared `missingModelsUtils.ts`. Removed orphaned `FileDownload`,
`ElectronFileDownload`, `useDownload`, and `useCivitaiModel` files.
- **Breaking**: None

## Review Focus
- Badge styling and icon button variants for theme compatibility
- Security validation logic preserved correctly in extracted utility
- E2e test locator updates for the new dialog structure

<img width="641" height="478" alt="스크린샷 2026-02-20 오후 7 35 23"
src="https://github.com/user-attachments/assets/ded27dc7-04e6-431d-9b2e-a96ba61043a4"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9014-refactor-Redesign-missing-models-dialog-30d6d73d365081809cb0c555c2c28034)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-25 10:51:18 +09:00
Dante
9108b7535a feat: support video file drag-drop and paste (#9154) 2026-02-25 07:59:26 +09:00
Alexander Brown
2ff14fadc2 fix: prevent infinite node resize loop in Vue mode (#9177)
## Summary

Fix infinite node resize loop in Vue mode where textarea widgets caused
nodes to grow ~33px per frame indefinitely.

## Changes

- **What**: Two feedback loops broken in the LiteGraph↔Vue layout sync:
1. `_arrangeWidgets()` in LiteGraph's draw loop was calling `setSize()`
every frame with its own computed widget height, which disagreed with
Vue's DOM-measured height. Guarded with `!LiteGraph.vueNodesMode`.
2. `useLayoutSync` was calling `setSize()` which triggers the size
setter → writes back to layoutStore with `source=Canvas` →
`handleLayoutChange` updates CSS vars → ResizeObserver fires → loop.
Changed to direct array assignment (matching the existing position sync
pattern).

## Review Focus

- The `_arrangeWidgets` guard: in Vue mode, the DOM/ResizeObserver is
the source of truth for node sizing, so LiteGraph should not grow nodes
via `setSize()`. Verify no Vue-mode features depend on this growth path.
- The `useLayoutSync` change: `liteNode.size[0] = ...` modifies `_size`
via the getter without triggering the setter, avoiding the Canvas-source
bounce. `onResize` is still called. Verify no downstream code relies on
the setter side effects when syncing from layout store.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9177-fix-prevent-infinite-node-resize-loop-in-Vue-mode-3116d73d365081e4ad88f1cfad51df18)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-02-24 21:12:16 +00:00
Comfy Org PR Bot
87341f2c6e 1.41.4 (#9139)
Patch version increment to 1.41.4

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9139-1-41-4-3116d73d3650813a9061e5695a844233)
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-24 03:40:20 -08:00
Dante
02a38110cd feat: audio drag-drop and paste support (#9152) 2026-02-24 18:59:57 +09:00
jaeone94
09989b7aff [refactor] Extract manager composables and execution utils (#9163)
## Summary
Extracts inline logic from manager components into dedicated composables
and utilities, and adds a cyclic subgraph fix.

## Changes
- **`usePackInstall`**: New composable extracted from
`PackInstallButton.vue` — handles conflict detection, payload
construction, and `Promise.allSettled`-based batch installation
- **`useApplyChanges`**: New shared composable extracted from
`ManagerProgressToast.vue` — manages ComfyUI restart flow with reconnect
timeout and post-reconnect refresh
- **`executionIdUtil`**: New utility (`getAncestorExecutionIds`,
`getParentExecutionIds`, `buildSubgraphExecutionPaths`) with unit tests;
fixes infinite recursion on cyclic subgraph definitions

## Review Focus
- `useApplyChanges` reconnect timeout (2 min) and setting restore logic
- `buildSubgraphExecutionPaths` visited-set guard for cyclic subgraph
defs

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9163-refactor-Extract-manager-composables-and-execution-utils-3116d73d365081f293d3d5484775ad48)
by [Unito](https://www.unito.io)
2026-02-24 01:39:44 -08:00
Hunter
0f455c73bb fix: sync DOM widget values to widgetValueStore on registration (#9166)
## Summary

Override `setNodeId` in `BaseDOMWidgetImpl` to sync the DOM-resolved
value into the widget value store, fixing empty system prompts in Vue
nodes (Nodes 2.0).

## Changes

- **What**: DOM widgets (e.g. textarea for Gemini system_prompt) resolve
their value through `options.getValue()` / DOM elements, not
`_state.value`. When `BaseWidget.setNodeId` registers with the store, it
spreads `_state.value` which is `undefined` for DOM widgets. The
override captures the DOM-resolved value before registration and syncs
it into the store afterward — keeping the fix in the DOM widget layer
where the mismatch originates, leaving `BaseWidget` unchanged.

## Review Focus

- Whether capturing `this.value` before `super.setNodeId()` and writing
it after is the right sequencing
- Whether this correctly handles all DOM widget subtypes
(`DOMWidgetImpl`, `ComponentWidgetImpl`)

Supersedes #9164

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9166-fix-sync-DOM-widget-values-to-widgetValueStore-on-registration-3116d73d3650816f8cece866a9272baa)
by [Unito](https://www.unito.io)
2026-02-24 01:05:44 -08:00
Christian Byrne
a94574d379 fix: open image in new tab on cloud fetches as blob to avoid GCS auto-download (#9122)
## Summary

Fix "Open Image" on cloud opening a new tab that auto-downloads the
asset instead of displaying it inline.

## Changes

- **What**: Add `openFileInNewTab()` to `downloadUtil.ts` that fetches
cross-origin URLs as blobs before opening in a new tab, avoiding GCS
`Content-Disposition: attachment` redirects. Opens the blank tab
synchronously to preserve user-gesture activation (avoiding popup
blockers), then navigates to a blob URL once the fetch completes. Blob
URLs are revoked after 60s or immediately if the tab was closed. Update
both call sites (`useImageMenuOptions` and `litegraphService`) to use
the new function.

## Review Focus

- The synchronous `window.open('', '_blank')` before the async fetch is
intentional to preserve user-gesture context and avoid popup blockers.
- Blob URL revocation strategy: 60s timeout for successful opens,
immediate revoke if tab was closed, tab closed on fetch failure.
- Shared `fetchAsBlob()` helper is also used by the existing
`downloadViaBlobFetch`.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9122-fix-open-image-in-new-tab-on-cloud-fetches-as-blob-to-avoid-GCS-auto-download-3106d73d365081a3bfa6eb7d77fde99f)
by [Unito](https://www.unito.io)
2026-02-23 21:28:16 -08:00
Johnpaul Chiwetelu
78fe639540 feat: periodically re-poll queue progress state (#9136)
## Summary
- Add `useQueuePolling` composable that polls `queueStore.update()`
every 5s while jobs are active
- Calls `update()` immediately on creation so the UI is current after
page reload
- Uses `useIntervalFn` + `watch` pattern (same as `assetDownloadStore`)
to pause/resume based on `activeJobsCount`

## Related Issue
- Related to #8136

## QA
- Queue a prompt, reload page mid-execution, verify queue UI updates
every ~5s
- Verify polling stops when queue empties
- Verify polling resumes when new jobs are queued

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9136-feat-periodically-re-poll-queue-progress-state-3106d73d36508119a32fc5b9c8eda21c)
by [Unito](https://www.unito.io)
2026-02-23 21:27:24 -08:00
Terry Jia
e333ad459e feat: add CurveEditor component (#8860)
## Summary
Prerequisite for upcoming native color correction nodes (ColorCurves).

Reusable curve editor with monotone cubic Hermite interpolation,
drag-to-add/move/delete control points, and SVG-based rendering.
Includes CurvePoint type, LUT generation utility, and useCurveEditor
composable for interaction logic.

## Screenshots (if applicable)



https://github.com/user-attachments/assets/948352c7-bdf2-40f9-a8f0-35bc2b2f3202

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8860-feat-add-CurveEditor-component-and-d3-shape-dependency-3076d73d3650817f8421f98e349569d0)
by [Unito](https://www.unito.io)
2026-02-23 21:19:06 -08:00
Johnpaul Chiwetelu
aab09b5f99 feat: allow custom event names in ComfyApi.addEventListener (#9140)
## Summary
- Add string fallback overloads to `addEventListener` and
`removeEventListener` on `ComfyApi`
- Extensions can now listen for custom event names without TypeScript
rejecting unknown event names
- Known events still get full type safety via the generic overload;
unknown strings fall through to `CustomEvent`

## Related Issue
- Fixes #2088

## QA
- Verify existing typed event listeners (e.g.
`api.addEventListener('status', ...)`) still infer correct types
- Verify custom event names (e.g.
`api.addEventListener('my-custom-event', ...)`) compile without errors

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9140-feat-allow-custom-event-names-in-ComfyApi-addEventListener-3116d73d36508128aad3fab98c34fac3)
by [Unito](https://www.unito.io)
2026-02-23 21:17:35 -08:00
Johnpaul Chiwetelu
1f0ca18737 fix: refresh image previews on media upload nodes when refreshing node definitions (#9141)
## Summary
- When pressing `R` to refresh node definitions, image previews on
LoadImage/LoadVideo nodes now update to reflect external file changes
- Re-triggers the combo widget callback to regenerate preview URLs with
a fresh cache-busting `&rand=` parameter
- Extracts `isMediaUploadComboInput` from `uploadImage.ts` to
`nodeDefSchema.ts` as a shared utility

- Fixes #2082



https://github.com/user-attachments/assets/d18d69ae-6ecd-448d-8d7c-76b2c49fdea5



## Test plan
- [ ] Open a workflow with a LoadImage node and select an image
- [ ] Edit and save the image externally (e.g. in an image editor)
- [ ] Press `R` to refresh node definitions
- [ ] Verify the preview updates to show the edited image
2026-02-23 20:57:24 -08:00
Terry Jia
3b5649232d feat: add batch image navigation to ImageCompare node (#9151)
## Summary

Add batch image navigation to the ImageCompare node so users can browse
all images in a batch instead of only seeing the first one.
## Changes
The backend already returns all batch images in a_images/b_images
arrays, but the frontend only used index [0]. Now all images are mapped
to URLs and a navigation bar with prev/next controls appears above the
comparison slider when either side has more than one image. A/B sides
navigate independently. Extracted a reusable BatchNavigation component
for the index selector UI.

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

## Screenshots (if applicable)


https://github.com/user-attachments/assets/a801cc96-9182-4b0d-a342-4e6107290f47

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9151-feat-add-batch-image-navigation-to-ImageCompare-node-3116d73d365081498be6d401773619a3)
by [Unito](https://www.unito.io)
2026-02-23 23:56:46 -05:00
Johnpaul Chiwetelu
724827d8cc refactor: replace withDefaults with Vue 3.5 props destructuring (#9150)
## Summary
- Replace all `withDefaults(defineProps<...>())` with Vue 3.5 reactive
props destructuring across 14 components
- Update `props.xxx` references to use destructured variables directly
in script and template

- Fixes #2334

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9150-refactor-replace-withDefaults-with-Vue-3-5-props-destructuring-3116d73d365081e7a721db3369600671)
by [Unito](https://www.unito.io)
2026-02-23 20:30:44 -08:00
Jin Yi
be70f6c1e6 [refactor] Merge sort button into view settings popover (#9143) 2026-02-24 13:18:07 +09:00
Christian Byrne
514425b560 fix: use getAuthHeader for API key auth in subscription/billing (#9142)
## Summary

Fix "User not authenticated" errors when API key users
(desktop/portable) trigger subscription status checks or billing
operations.

## Changes

- **What**: Replace `getFirebaseAuthHeader()` with `getAuthHeader()` in
subscription and billing call sites (`fetchSubscriptionStatus`,
`initiateSubscriptionCheckout`, `fetchBalance`, `addCredits`,
`accessBillingPortal`, `performSubscriptionCheckout`). `getAuthHeader()`
supports the full auth fallback chain (workspace token → Firebase token
→ API key), whereas `getFirebaseAuthHeader()` returns null for API key
users since they bypass Firebase entirely. Also add an `isCloud` guard
to the subscription status watcher so non-cloud environments skip
subscription checks.

## Review Focus

- The `isCloud` guard on the watcher ensures local/desktop users never
hit the subscription endpoint. This was the originally intended design
per code owner confirmation.
- `getAuthHeader()` already exists in `firebaseAuthStore` with proper
fallback logic — no new auth code was added.

Fixes
https://www.notion.so/comfy-org/Bug-Subscription-status-check-occurring-in-non-cloud-environments-causing-authentication-errors-3116d73d365081738b21db157e88a9ed

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9142-fix-use-getAuthHeader-for-API-key-auth-in-subscription-billing-3116d73d3650817fa345deaddc8c3fcd)
by [Unito](https://www.unito.io)
2026-02-23 18:49:32 -08:00
Alexander Brown
c25f9a0e93 feat: synthetic widgets getter for SubgraphNode (proxy-widget-v2) (#8856)
## Summary

Replace the Proxy-based proxy widget system with a store-driven
architecture where `promotionStore` and `widgetValueStore` are the
single sources of truth for subgraph widget promotion and widget values,
and `SubgraphNode.widgets` is a synthetic getter composing lightweight
`PromotedWidgetView` objects from store state.

## Motivation

The subgraph widget promotion system previously scattered state across
multiple unsynchronized layers:

- **Persistence**: `node.properties.proxyWidgets` (tuples on the
LiteGraph node)
- **Runtime**: Proxy-based `proxyWidget.ts` with `Overlay` objects,
`DisconnectedWidget` singleton, and `isProxyWidget` type guards
- **UI**: Each Vue component independently calling `parseProxyWidgets()`
via `customRef` hacks
- **Mutation flags**: Imperative `widget.promoted = true/false` set on
`subgraph-opened` events

This led to 4+ independent parsings of the same data, complex cache
invalidation, and no reactive contract between the promotion state and
the rendering layer. Widget values were similarly owned by LiteGraph
with no Vue-reactive backing.

The core principle driving these changes: **Vue owns truth**. Pinia
stores are the canonical source; LiteGraph objects delegate to stores
via getters/setters; Vue components react to store state directly.

## Changes

### New stores (single sources of truth)

- **`promotionStore`** — Reactive `Map<NodeId, PromotionEntry[]>`
tracking which interior widgets are promoted on which SubgraphNode
instances. Graph-scoped by root graph ID to prevent cross-workflow state
collision. Replaces `properties.proxyWidgets` parsing, `customRef`
hacks, `widget.promoted` mutation, and the `subgraph-opened` event
listener.
- **`widgetValueStore`** — Graph-scoped `Map<WidgetKey, WidgetState>`
that is the canonical owner of widget values. `BaseWidget.value`
delegates to this store via getter/setter when a node ID is assigned.
Eliminates the need for Proxy-based value forwarding.

### Synthetic widgets getter (SubgraphNode)

`SubgraphNode.widgets` is now a getter that reads
`promotionStore.getPromotions(rootGraphId, nodeId)` and returns cached
`PromotedWidgetView` objects. No stubs, no Proxies, no fake widgets
persisted in the array. The setter is a no-op — mutations go through
`promotionStore`.

### PromotedWidgetView

A class behind a `createPromotedWidgetView` factory, implementing the
`PromotedWidgetView` interface. Delegates value/type/options/drawing to
the resolved interior widget and stores. Owns positional state (`y`,
`computedHeight`) for canvas layout. Cached by
`PromotedWidgetViewManager` for object-identity stability across frames.

### DOM widget promotion

Promoted DOM widgets (textarea, image upload, etc.) render on the
SubgraphNode surface via `positionOverride` in `domWidgetStore`.
`DomWidgets.vue` checks for overrides and uses the SubgraphNode's
coordinates instead of the interior node's.

### Promoted previews

New `usePromotedPreviews` composable resolves image/audio/video preview
widgets from promoted entries, enabling SubgraphNodes to display
previews of interior preview nodes.

### Deleted

- `proxyWidget.ts` (257 lines) — Proxy handler, `Overlay`,
`newProxyWidget`, `isProxyWidget`
- `DisconnectedWidget.ts` (39 lines) — Singleton Proxy target
- `useValueTransform.ts` (32 lines) — Replaced by store delegation

### Key architectural changes

- `BaseWidget.value` getter/setter delegates to `widgetValueStore` when
node ID is set
- `LGraph.add()` reordered: `node.graph` assigned before widget
`setNodeId` (enables store registration)
- `LGraph.clear()` cleans up graph-scoped stores to prevent stale
entries across workflow switches
- `promotionStore` and `widgetValueStore` state nested under root graph
UUID for multi-workflow isolation
- `SubgraphNode.serialize()` writes promotions back to
`properties.proxyWidgets` for persistence compatibility
- Legacy `-1` promotion entries resolved and migrated on first load with
dev warning

## Test coverage

- **3,700+ lines of new/updated tests** across 36 test files
- **Unit**: `promotionStore.test.ts`, `widgetValueStore.test.ts`,
`promotedWidgetView.test.ts` (921 lines),
`subgraphNodePromotion.test.ts`, `proxyWidgetUtils.test.ts`,
`DomWidgets.test.ts`, `PromotedWidgetViewManager.test.ts`,
`usePromotedPreviews.test.ts`, `resolvePromotedWidget.test.ts`,
`subgraphPseudoWidgetCache.test.ts`
- **E2E**: `subgraphPromotion.spec.ts` (622 lines) — promote/demote,
manual/auto promotion, paste preservation, seed control augmentation,
image preview promotion; `imagePreview.spec.ts` extended with
multi-promoted-preview coverage
- **Fixtures**: 2 new subgraph workflow fixtures for preview promotion
scenarios

## Review focus

- Graph-scoped store keying (`rootGraphId`) — verify isolation across
workflows/tabs and cleanup on `LGraph.clear()`
- `PromotedWidgetView` positional stability — `_arrangeWidgets` writes
to `y`/`computedHeight` on cached objects; getter returns fresh array
but stable object references
- DOM widget position override lifecycle — overrides set on promote,
cleared on demote/removal/subgraph navigation
- Legacy `-1` entry migration — resolved and written back on first load;
unresolvable entries dropped with dev warning
- Serialization round-trip — `promotionStore` state →
`properties.proxyWidgets` on serialize, hydrated back on configure

## Diff breakdown (excluding lockfile)

- 153 files changed, ~7,500 insertions, ~1,900 deletions (excluding
pnpm-lock.yaml churn)
- ~3,700 lines are tests
- ~300 lines deleted (proxyWidget.ts, DisconnectedWidget.ts,
useValueTransform.ts)

<!-- Fixes #ISSUE_NUMBER -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8856-feat-synthetic-widgets-getter-for-SubgraphNode-proxy-widget-v2-3076d73d365081c7b517f5ec7cb514f3)
by [Unito](https://www.unito.io)

---------

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-02-23 13:33:41 -08:00
AustinMroz
d7546e68ef Add functionality to quickly disconnect moved input links (#7459)
Disconnections are frequently performed by dragging a link from an input
slot and dropping it on the canvas, but needing to wait for the
searchbox to pop up, and then needing to manually close out of this can
make it feel slow. Sometimes, this will even result in users disabling
the link release action for more responsive graph building.

Instead, this PR introduces new functionality where a link which is
moved only a short distance from a node input and dropped will be
immediately disconnected instead of performing the default link release
action.

![fast-disco_00001](https://github.com/user-attachments/assets/5b0795da-12c0-4347-8ea1-6fc1bcafaae2)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7459-Add-functionality-to-quickly-disconnect-moved-input-links-2c86d73d365081919052f3856db8e672)
by [Unito](https://www.unito.io)
2026-02-23 11:54:54 -08:00
pythongosssss
2634acdd8c App mode - builder toolbar save - 7 (#9030)
## Summary

Implements save flow for the builder toolbar.
The todo will be done in a future PR once the serailized format is
finalized

## Screenshots (if applicable)


https://github.com/user-attachments/assets/124cb7d8-e23b-476a-8691-0ee2c4c9150b

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9030-App-mode-builder-toolbar-save-7-30d6d73d3650815e8610fced20e95e6e)
by [Unito](https://www.unito.io)
2026-02-23 10:57:52 -08:00
pythongosssss
b3a5317500 App mode - builder toolbar - 6 (#9029)
## Summary

A toolbar for builder mode, hides various UI elements when in builder
mode

## Screenshots (if applicable)

Toolbar
<img width="706" height="166" alt="image"
src="https://github.com/user-attachments/assets/1f0b08b5-1661-4ed5-96bb-feecc73ca701"
/>

With disabled save and output required popover
<img width="711" height="299" alt="image"
src="https://github.com/user-attachments/assets/4a93aaf8-d850-4afe-ab9f-4abd44a25420"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9029-App-mode-builder-toolbar-6-30d6d73d365081e3aef5c90033ba347d)
by [Unito](https://www.unito.io)
2026-02-23 10:48:07 -08:00
Christian Byrne
19dc48b69a docs: document PrimitiveNode copy/paste semantics and widgets_values loss (#9119)
## Summary

Documents the PrimitiveNode copy/paste bug mechanism and connection
lifecycle semantics in `WIDGET_SERIALIZATION.md`. This is tribal
knowledge from debugging
[#1757](https://github.com/Comfy-Org/ComfyUI_frontend/issues/1757) and
the related [Slack
discussion](https://comfy-organization.slack.com/archives/C09AQRB49QX/p1771806268469089).

## What's documented

- **The clone→serialize gap**: `_serializeItems()` calls
`item.clone()?.serialize()`. The clone has no `this.widgets`
(PrimitiveNode creates them on connection), so `serialize()` silently
drops `widgets_values`.
- **Why seed survives but control_after_generate doesn't**: Primary
widget value is copied from the target on reconnect; secondary widgets
read from `this.widgets_values` which was lost.
- **Current vs. proposed lifecycle**: Empty-on-copy → morph-on-connect
(current) vs. clone-configured-instance → empty-on-disconnect
(proposed).
- **Design considerations**: `input.widget` override flexibility,
deserialization ordering, and the minimal `serialize()` override fix.

## Related

- Issue: #1757
- Fix PR: #8938
- Companion: #9102 (initial WIDGET_SERIALIZATION.md), #9105 (type/JSDoc
improvements)
- Notion: COM-15282

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9119-docs-document-PrimitiveNode-copy-paste-semantics-and-widgets_values-loss-3106d73d3650816ba7f7d9e6f3bb3868)
by [Unito](https://www.unito.io)
2026-02-23 10:47:11 -08:00
pythongosssss
f811b173c6 App Mode - Progress/outputs - 5 (#9028)
## Summary

Adds new store for tracking outputs lin linear mode and reworks outputs
to display the following: skeleton -> latent preview -> node output ->
job result.

## Changes

- **What**: 
- New store for wrangling various events into a usable list of live
outputs
- Replace manual list with reka-ui list box
- Extract various composables

## Review Focus
Getting all the events and stores aligned to happen consistently and in
the correct order was a challenge, unifying the various sources. so
suggestions there would be good

## Screenshots (if applicable)


https://github.com/user-attachments/assets/13449780-ee48-4d9a-b3aa-51dca0a124c7

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9028-App-Mode-Progress-outputs-5-30d6d73d3650817aaa9dee622fffe426)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-02-23 10:43:25 -08:00
pythongosssss
b29dbec916 App mode - landing/arrange screens - 4 (#9026)
## Summary

Updates messaging on app mode welcome pages and adds arrange view

## Screenshots (if applicable)

Build app view
<img width="628" height="470" alt="image"
src="https://github.com/user-attachments/assets/7f648769-1380-4093-8de9-414d05303b87"
/>

Run view
<img width="593" height="471" alt="image"
src="https://github.com/user-attachments/assets/cc81cfd0-fa89-4402-82f4-6bbdd0e7e2f9"
/>

Arrange view
<img width="1071" height="829" alt="image"
src="https://github.com/user-attachments/assets/ae8666ef-dff1-4fde-929e-89479c891261"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9026-App-mode-landing-arrange-screens-4-30d6d73d365081f4b209f34834a2ce5e)
by [Unito](https://www.unito.io)
2026-02-23 10:16:22 -08:00
pythongosssss
0f4bceafdd App mode - App mode toolbar - 3 (#9025)
## Summary

Adds a new toolbar for app mode

## Changes

- **What**:  Adds new toolbar with builder mode disabled

## Screenshots (if applicable)

<img width="172" height="220" alt="image"
src="https://github.com/user-attachments/assets/16f3cf61-bcbe-4b4d-a169-01c934140354"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9025-App-mode-App-mode-toolbar-3-30d6d73d365081549af6fdd1937b098f)
by [Unito](https://www.unito.io)
2026-02-23 10:05:52 -08:00
pythongosssss
d601aba721 App mode - Unify menus - 2 (#9023)
## Summary

Updates subgraph breadcrumbs menu, workflow tabs context menu & linear
mode menu to use a single implementation.
Adds new menu items for enter/exit app mode  
Hides menu when in builder mode

## Changes

- **What**: Changes the components to use either a reka-ui context menu
or dropdown, with a standard inner list
- **Breaking**: Remove existing linear toggle from sidebar as it is now
in the menu


## Screenshots (if applicable)
It looks basically identical other than the icon changes based on mode:

In Graph Mode:
<img width="261" height="497" alt="image"
src="https://github.com/user-attachments/assets/eb9968a2-b528-4e21-9e14-ab4a67e717ae"
/>

In App Mode:
<img width="254" height="499" alt="image"
src="https://github.com/user-attachments/assets/54a89fab-e7b2-4cb0-bcb7-43d6d076ac83"
/>

Right click tab:
<img width="321" height="564" alt="image"
src="https://github.com/user-attachments/assets/c12c7d64-2dba-45bb-be76-2615f3e38cc6"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9023-App-mode-Unify-menus-2-30d6d73d36508162bfc0e308d5f705de)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-23 09:49:52 -08:00
jaeone94
ddcfdb924d fix: fix error overlay and TabErrors filtering for nested subgraphs (#9129)
## Summary

Fix error overlays not showing on subgraph container nodes and nested
error cards not appearing in the Errors tab when a node inside a
subgraph fails.

## Changes

- **What**: Error overlay and Errors tab filtering now use full
hierarchical execution IDs (e.g. `65:70`) instead of local node IDs,
enabling correct ancestor detection at any nesting depth
- Added `getExecutionIdByNode` to
[graphTraversalUtil.ts](src/utils/graphTraversalUtil.ts) to compute a
node's full execution ID chain from the root graph
- Added `errorAncestorExecutionIds` computed set and
`isContainerWithInternalError(node)` helper to
[executionErrorStore.ts](src/stores/executionErrorStore.ts) for O(1)
container checks
- Updated `hasAnyError` in
[LGraphNode.vue](src/extensions/vueNodes/components/LGraphNode.vue) to
use the new store helper
- Fixed `isErrorInSelection` in
[useErrorGroups.ts](src/components/rightSidePanel/errors/useErrorGroups.ts)
to use full execution IDs for selected containers

## Review Focus

- `errorAncestorExecutionIds` is rebuilt reactively whenever the active
errors change — confirm this is efficient enough given typical error
counts
- `getExecutionIdByNode` walks up the graph hierarchy; verify the base
case (root graph, no parent) is handled correctly

## Screenshots


https://github.com/user-attachments/assets/b5be5892-80a9-4e5e-8b6f-fe754b4ebc4e



https://github.com/user-attachments/assets/92ff12b3-3bc9-4f02-ba4a-e2c7384bafe5



https://github.com/user-attachments/assets/be8e95be-ac8c-4699-9be9-b11902294bda



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9129-fix-fix-error-overlay-and-TabErrors-filtering-for-nested-subgraphs-3106d73d365081c1875bc1a3c89eae29)
by [Unito](https://www.unito.io)
2026-02-23 02:52:45 -08:00
pythongosssss
d6c902187d App mode - store - 1 (#9022)
## Summary

Adds a store to control the mode the app is in (app, graph, builder)
Changes the existing linearMode in canvasStore to update new store

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9022-App-mode-store-1-30d6d73d365081498df1c1192c9860f0)
by [Unito](https://www.unito.io)
2026-02-23 02:10:58 -08:00
jaeone94
dd8520020e feat(node): show Enter Subgraph and Error buttons side by side in node footer (#9126)
## Summary
Show the \"Enter Subgraph\" and \"Error\" buttons simultaneously in the
node footer when a subgraph node has errors, instead of only showing the
Error button.

## Changes
- **What**: Replaced the single mutually-exclusive `Button` in the node
footer with a flex container that can display both the \"Enter
Subgraph\" button and the \"Error\" button side by side, separated by a
divider
- **What**: When a subgraph node has errors, the Enter button shows a
short label \"Enter\" to fit alongside the Error button; otherwise it
shows the full \"Enter Subgraph\" label
- **What**: Advanced inputs toggle button is now explicitly restricted
to non-subgraph nodes (preserving existing behavior)
- **What**: Added `"enter": "Enter"` i18n key for the shortened subgraph
entry label

## Review Focus
- Layout behavior when both buttons are visible (subgraph node with
errors)
- Collapsed vs. expanded node states with the new flex container
- The `divide-x divide-component-node-border` divider between buttons

> Note: May need to backport to a stable release branch.

## Screenshot


https://github.com/user-attachments/assets/1eb4afe0-bf82-4677-ad86-6e15a9c0a487



- Fixes <!-- #ISSUE_NUMBER -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9126-feat-node-show-Enter-Subgraph-and-Error-buttons-side-by-side-in-node-footer-3106d73d3650818a9afdeb51813825d9)
by [Unito](https://www.unito.io)
2026-02-23 00:43:17 -08:00
Christian Byrne
a0e8f7d960 ci: skip unit and e2e tests for markdown-only changes (#9125)
## Summary

Skip unit and e2e tests when PRs only contain markdown file changes.

## Changes

- Add `paths-ignore: ['**/*.md']` to `push` and `pull_request` triggers
in ci-tests-unit.yaml and ci-tests-e2e.yaml
- Manual `workflow_dispatch` trigger preserved for e2e tests

## Testing

Create a PR with only `.md` changes to verify both test workflows are
skipped.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9125-ci-skip-unit-and-e2e-tests-for-markdown-only-changes-3106d73d365081bea0dcc06b608a87fc)
by [Unito](https://www.unito.io)
2026-02-22 20:53:17 -08:00
Johnpaul Chiwetelu
02e926471f fix: replace as-unknown-as casts with safer patterns (#9107)
## Summary

- Replace 83 `as unknown as` double casts with safer alternatives across
33 files
- Use `as Partial<X> as X` pattern where TypeScript allows it
- Create/reuse factory functions from `litegraphTestUtils.ts` for mock
objects
- Widen `getWorkflowDataFromFile` return type to include `ComfyMetadata`
directly
- Reduce total `as unknown as` count from ~153 to 71

The remaining 71 occurrences are genuinely necessary due to cross-schema
casts, generic variance, missing index signatures, Float64Array-to-tuple
conversions, and DOM type incompatibilities.

## Test plan

- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes
- [x] All affected unit tests pass

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9107-fix-replace-as-unknown-as-casts-with-safer-patterns-3106d73d3650815cb5bcd613ad635bd7)
by [Unito](https://www.unito.io)
2026-02-22 20:46:12 -08:00
Christian Byrne
8998d92e1b feat: add NightlySurveyPopover component for feature surveys (#9083)
## Summary

Adds NightlySurveyPopover component that displays a Typeform survey to
eligible nightly users after a configurable delay.

## Changes

- **What**: Vue component that uses `useSurveyEligibility` to show/hide
a survey popover with accept, dismiss, and opt-out actions. Loads
Typeform embed script dynamically with HTTPS and deduplication.

## Review Focus

- Typeform script injection security (HTTPS-only, load-once guard,
typeformId alphanumeric validation)
- Timeout lifecycle (clears pending timeout when eligibility changes)

## Part of Nightly Survey System

This is part 4 of a stacked PR chain:
1.  feat/feature-usage-tracker - useFeatureUsageTracker (merged in
#8189)
2.  feat/survey-eligibility - useSurveyEligibility (#8189, merged)
3.  feat/survey-config - surveyRegistry.ts (#8355, merged)
4. **feat/survey-popover** - NightlySurveyPopover.vue (this PR)
5. feat/survey-integration - NightlySurveyController.vue (#8480)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9083-feat-add-NightlySurveyPopover-component-for-feature-surveys-30f6d73d365081d1beb2f92555a4b2f4)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-02-22 20:20:09 -08:00
Comfy Org PR Bot
1267c4b9b3 1.41.3 (#9103)
Patch version increment to 1.41.3

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9103-1-41-3-3106d73d365081b9b4c7f402c860150e)
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-22 18:14:46 -08:00
Christian Byrne
ddc2159bed docs: clarify widget serialize vs options.serialize in types and JSDoc (#9105)
## Summary

Clarifies the two distinct `serialize` properties on widgets via
improved TypeScript types and JSDoc:

- **`IWidgetOptions.serialize`** — controls whether the widget value is
included in the **API prompt** sent for execution
- **`IBaseWidget.serialize`** — controls whether the widget value is
persisted in the **workflow JSON** (`widgets_values`)

These two properties are easily confused. This PR adds cross-linking
JSDoc, explicit `@default` tags, and a clarifying comment at the check
site in `executionUtil.ts`.

## Changes

| File | Change |
|------|--------|
| `src/lib/litegraph/src/types/widgets.ts` | Add `serialize?: boolean`
to `IWidgetOptions` with JSDoc; expand JSDoc on `IBaseWidget.serialize`
|
| `src/utils/executionUtil.ts` | Clarifying comment at the
`widget.options.serialize` check |
| `src/types/metadataTypes.ts` | Connect `ComfyMetadataTags` enum values
to their corresponding serialize properties |

## Related

- Companion doc: #9102 (`WIDGET_SERIALIZATION.md`)
- Issue: #1757

## Verification

- `vue-tsc --noEmit` passes clean
- `eslint` passes clean on all 3 files
- No runtime changes — JSDoc and types only

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9105-docs-clarify-widget-serialize-vs-options-serialize-in-types-and-JSDoc-3106d73d36508155b618ee56cf18f969)
by [Unito](https://www.unito.io)
2026-02-22 18:13:48 -08:00
sno
5687b44422 Add CI validation for OSS assets (fonts and licenses) (#8828)
## Summary
Adds CI workflow to validate OSS build compliance by checking for
proprietary fonts and non-approved dependency licenses.

## Context
- Part of comprehensive OSS compliance effort (split from closed PR
#6777)
- Uses simple bash/grep approach following proven #8623 pattern
- Complements telemetry checking in PR #8826 and existing #8354

## Implementation

### Font Validation
- Scans dist/ for proprietary ABCROM fonts (.woff, .woff2, .ttf, .otf)
- Fails if any ABCROM fonts found in OSS builds
- Provides clear fix instructions

### License Validation  
- Uses `license-checker` npm package
- Validates all production dependencies
- Only allows OSI-approved licenses:
  - MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC
  - 0BSD, BlueOak-1.0.0, Python-2.0, CC0-1.0
  - Unlicense, CC-BY-4.0, CC-BY-3.0
  - Common dual-license combinations

### Workflow Details
- Two separate jobs for parallel execution
- Runs on PRs and pushes to main/dev
- Builds with `DISTRIBUTION=localhost` for OSS mode
- Clear error messages with remediation steps

## Testing
- [ ] Font check passes on current main (no ABCROM fonts in dist)
- [ ] License check passes on current main (all approved licenses)
- [ ] Intentional violation testing

## Related
- Supersedes remaining parts of closed PR #6777
- Complements PR #8826 (Mixpanel telemetry)
- Follows pattern from PR #8623 (simple bash/grep)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8828-Add-CI-validation-for-OSS-assets-fonts-and-licenses-3056d73d3650812390d5d91ca2f319fc)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: bymyself <cbyrne@comfy.org>
2026-02-22 17:35:19 -08:00
Christian Byrne
26fa84ce1b docs: add widget serialization reference (widget.serialize vs widget.options.serialize) (#9102)
Documents the distinction between `widget.serialize` (workflow
persistence, checked by `LGraphNode.serialize()`) and
`widget.options.serialize` (API prompt, checked by `executionUtil.ts`).

These share a property name but live at different levels of the widget
object and are consumed by different code paths — a common source of
confusion when debugging serialization bugs.

Includes:
- Explanation of both properties with code references
- Permutation table of the 4 possible combinations with real examples
- Gotchas section covering the `addWidget` options bag behavior and
`PrimitiveNode` dynamic widgets
- Reference added to `src/lib/litegraph/AGENTS.md`

Context: discovered while debugging #1757 (PrimitiveNode
`control_after_generate` lost on copy-paste).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9102-docs-add-widget-serialization-reference-widget-serialize-vs-widget-options-serialize-30f6d73d365081cd86add44bdaa20d30)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-02-22 16:40:43 -08:00
Benjamin Lu
e37bba7250 fix: use AssetsListItem in queue overlay expanded (#9055)
## Summary
- replace `JobGroupsList` with `JobAssetsList` in `QueueOverlayExpanded`
- standardize expanded queue rows to use `AssetsListItem`
- add `QueueOverlayExpanded` tests to verify list rendering and action
re-emits

It works surprisingly well.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9055-fix-use-AssetsListItem-in-queue-overlay-expanded-30e6d73d365081c586c7c7bca222c290)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-02-22 02:02:04 -08:00
Benjamin Lu
35f15d18b4 feat: add job history and assets sidebar badge behavior (#9050)
## Summary
Add sidebar badge behavior for queue/asset visibility updates:
- Job History tab icon shows active jobs count (`queued + running`) only
when the Job History panel is closed.
- Assets tab icon no longer mirrors active jobs; when QPO V2 is enabled
it now shows the number of assets added since the last time Assets was
opened.
- Opening Assets clears the unseen added-assets badge count.

## Changes
- Added `iconBadge` logic to Job History sidebar tab.
- Replaced Assets sidebar badge source with new unseen-assets counter
logic.
- Added `assetsSidebarBadgeStore` to track unseen asset additions from
history updates and reset on Assets open.
- Added/updated unit tests for both sidebar tab composables and the new
store behavior.


https://github.com/user-attachments/assets/33588a2a-c607-4fcc-8221-e7f11c3d79cc



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9050-fix-add-job-history-and-assets-sidebar-badge-behavior-30e6d73d365081c38297fe6aac9cd34c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-02-22 01:05:39 -08:00
Comfy Org PR Bot
a82c984520 1.41.2 (#9089)
Patch version increment to 1.41.2

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9089-1-41-2-30f6d73d365081beb90de78b0571f396)
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-22 01:05:31 -08:00
Christian Byrne
54f13930a4 feat: add display name mappings for Essentials tab nodes (#9072)
## Summary

Add frontend-only display name mappings for nodes shown in the
Essentials tab, plus parse the new `essentials_category` field from the
backend.

## Changes

- **What**: Created `src/constants/essentialsDisplayNames.ts` with a
static mapping of node names to user-friendly display names (e.g.
`CLIPTextEncode` → "Text", `ImageScale` → "Resize Image"). Regular nodes
use exact name matching; blueprint nodes use prefix matching since their
filenames include model-specific suffixes. Integrated into
`NodeLibrarySidebarTab.vue`'s `renderedRoot` computed for leaf node
labels with fallback to `display_name`. Added `essentials_category`
(z.string().optional()) to the node def schema and `ComfyNodeDefImpl` to
parse the field already sent by the backend (PR #12357).

## Review Focus

Display names are resolved only in the Essentials tab tree view
(`NodeLibrarySidebarTab.vue`), not globally, to avoid side effects on
search, bookmarks, or other views. Blueprint prefix matching is ordered
longest-first so more specific prefixes (e.g. `image_inpainting_`) match
before shorter ones (e.g. `image_edit`).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9072-feat-add-display-name-mappings-for-Essentials-tab-nodes-30f6d73d3650817c9acdc9b0315ed0be)
by [Unito](https://www.unito.io)
2026-02-22 01:03:15 -08:00
Benjamin Lu
c972dca61e fix: remove duplicate running indicator from queue header (#9032)
## Summary
Remove the extra running-workflow indicator from the expanded Queue
Progress Overlay header to avoid duplicate running-count signals.

## Changes
- Remove `showConcurrentIndicator` and `concurrentWorkflowCount` props
from `QueueOverlayHeader`.
- Stop passing those props through `QueueOverlayExpanded` and
`QueueProgressOverlay`.
- Simplify `QueueOverlayHeader` tests to reflect the updated header
behavior.

## Why
The expanded header was showing redundant running status information
alongside existing running/queued summaries.

Prevent this:
<img width="240" height="92" alt="image"
src="https://github.com/user-attachments/assets/f4b1775c-b347-46f7-8668-3a1054365ada"
/>

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9032-fix-remove-duplicate-running-indicator-from-queue-header-30d6d73d365081d19041de2f1b0c8886)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-02-22 00:40:43 -08:00
Benjamin Lu
cf8952e205 fix: keep queue overlay clear action static (#9031)
## Summary
Make the Queue Progress Overlay header clear action label static text
(`Clear queue`) and remove queued-count text from that header area.

## Changes
- Replace dynamic header text (`{count} queued`) with static `Clear
queue` text.
- Keep the existing icon clear button behavior/style in the header.
- Keep the clear action visible when queue is empty, but disabled.
- Add disabled visual treatment for the static text when queue is empty.
- Update `QueueOverlayHeader` tests to cover the new static label and
disabled behavior.

<img width="630" height="103" alt="image"
src="https://github.com/user-attachments/assets/0a5870fc-2ad6-4241-a12d-f8c4ef72a5fa"
/>


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

<img width="528" height="105" alt="image"
src="https://github.com/user-attachments/assets/3d03a4ac-a0f1-449d-afc7-b9a6cb1c8820"
/>
<img width="529" height="124" alt="image"
src="https://github.com/user-attachments/assets/7452cd17-e388-4b6a-b164-fce0b0d55ca1"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9031-fix-keep-queue-overlay-clear-action-static-30d6d73d365081f8b908fa54775ab4d4)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-02-22 00:25:51 -08:00
Christian Byrne
1dcaf5d0dc perf: virtualize FormDropdownMenu to reduce DOM nodes and image requests (#8476)
## Summary

Virtualize the FormDropdownMenu to only render visible items, fixing
slow dropdown performance on cloud.

## Changes

- Integrate `VirtualGrid` into `FormDropdownMenu` for virtualized
rendering
- Add computed properties for grid configuration per layout mode
(grid/list/list-small)
- Extend `VirtualGrid` slot to provide original item index for O(1)
lookups
- Change container from `max-h-[640px]` to fixed `h-[640px]` for proper
virtualization

## Review Focus

- VirtualGrid integration within the popover context
- Layout mode switching with `:key="layoutMode"` to force re-render
- Grid style computed properties match original Tailwind classes

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8476-perf-virtualize-FormDropdownMenu-to-reduce-DOM-nodes-and-image-requests-2f86d73d365081b3a79dd5e0b84df944)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Dropdowns now render with a virtualized grid/list (stable indexes,
responsive sizing) and show an empty-state icon when no items exist.

* **Bug Fixes**
* Reduced layout shift and rendering glitches with improved
spacer/scroll calculations and more reliable media measurement.

* **Style**
* Simplified media rendering (standard img/video), unified item visuals
and hover/background behavior.

* **Tests**
* Added unit and end-to-end tests for virtualization, indexing, layouts,
dynamic updates, and empty states.

* **Breaking Changes**
* Dropdown item/selection shapes and related component props/events were
updated (adapter changes may be required).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: GitHub Action <action@github.com>
2026-02-21 22:54:19 -08:00
Christian Byrne
f707098f05 fix: subgraph unpacking creates extra link to seed widget (#9046)
## Summary

Fix subgraph unpacking creating spurious links to widget inputs (e.g.
seed) when the subgraph contains ComfySwitchNode with duplicate internal
links.

## Changes

- **What**: Two fixes in `_unpackSubgraphImpl`:
1. Strip links from serialized node data **before** `configure()` so
`onConnectionsChange` doesn't resolve subgraph-internal link IDs against
the parent graph's link map (which may contain unrelated links with
colliding numeric IDs).
2. Deduplicate links by `(origin, origin_slot, target, target_slot)`
before reconnecting, preventing repeated disconnect/reconnect cycles on
widget inputs that cause slot index drift.

## Review Focus

- The link-stripping before `configure()` mirrors what
`LGraphNode.clone()` already does — nodes should be configured without
stale link references when links will be recreated separately.
- Deduplication is defensive against malformed subgraph data; the
duplicate links in the reproduction workflow likely originated from a
prior serialization bug.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9046-fix-subgraph-unpacking-creates-extra-link-to-seed-widget-30e6d73d36508125a5fefa1309485516)
by [Unito](https://www.unito.io)
2026-02-21 22:38:05 -08:00
Christian Byrne
d2917be3a7 feat: support custom descriptions for subgraph tooltips (#9003)
## Summary

Adds support for custom descriptions on subgraph nodes that display as
tooltips when hovering.

## Changes

- Add optional `description` field to `ExportedSubgraph` interface and
`Subgraph` class
- Use description with fallback to default string in
`subgraphService.createNodeDef()`
- Add `description` to `SubgraphDefinitionBase` interface and Zod schema
for validation

## Review Focus

- Backwards compatibility: undefined description falls back to `Subgraph
node for ${name}`
- Serialization pattern: conditional spread `...(this.description && {
description })`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9003-feat-support-custom-descriptions-for-subgraph-tooltips-30d6d73d36508129bd75c77eb1c31cfb)
by [Unito](https://www.unito.io)
2026-02-21 22:29:28 -08:00
Christian Byrne
2639248867 fix: replace PrimeVue FloatLabel in WidgetTextarea with CSS-only IFTA label (#9076)
## Summary

Replace PrimeVue `FloatLabel` + `Textarea` in `WidgetTextarea` with a
CSS-only IFTA label and a new shadcn-vue Textarea component, fixing the
label-obscures-content bug.

<img width="965" height="754" alt="image"
src="https://github.com/user-attachments/assets/cab98527-834c-496d-a0ef-942fb21fd862"
/>


## Changes

- **What**: Add `src/components/ui/textarea/Textarea.vue` — thin wrapper
around native `<textarea>` with `cn()` class merging and `defineModel`.
Rewrite `WidgetTextarea.vue` to use a plain `<div>` wrapper with an
absolutely-positioned label and the new Textarea, replacing PrimeVue's
`FloatLabel variant="in"`. Add Storybook stories (Default, Disabled,
WithLabel). Update tests to remove PrimeVue plugin setup.

## Review Focus

- The label uses `absolute left-3 top-1.5 z-10 text-xxs` positioning —
verify it clears textarea content with `pt-5` padding
- `filteredProps` forwards widget options to a native textarea via
`v-bind="restAttrs"` — unknown attrs are silently ignored by the browser

Supersedes #8536

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9076-fix-replace-PrimeVue-FloatLabel-in-WidgetTextarea-with-CSS-only-IFTA-label-30f6d73d3650816fabe5ee30de0c793e)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-21 22:09:56 -08:00
Christian Byrne
b41f162607 dx: compact storybook PR comment to single-line header (#9078)
## Summary

Compact the Storybook build status PR comment to a single-line header
with collapsible details, matching the approach from #8677.

## Changes

- **Starting**: Collapsed from multi-line with build steps to `## 🎨
Storybook:  Building...`
- **Completed (success)**: `## 🎨 Storybook:  Built — [View
Storybook](url)` with timestamp/links in `<details>`
- **Completed (failure)**: `## 🎨 Storybook:  Failed` with details
collapsed
- Removed version-bump branch check (starting comment no longer varies
by branch type)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9078-dx-compact-storybook-PR-comment-to-single-line-header-30f6d73d365081b98666c48a94542b70)
by [Unito](https://www.unito.io)
2026-02-21 21:45:25 -08:00
Christian Byrne
3678e65bec feat: add feature flag to disable Essentials tab in node library (#9067)
## Summary

Add a `node_library_essentials_enabled` feature flag to gate the
Essentials tab, allowing the rest of the new node library to ship while
the Essentials tab is finalized.

## Changes

- **What**: New feature flag (`node_library_essentials_enabled`) that
hides the Essentials tab in the node library sidebar and search category
sidebar. Defaults to `true` in dev/nightly builds, `false` in
production. Overridable via remote config or server feature flags. Falls
back to the "All" tab if a user previously had Essentials selected.

**Disabled UI**

<img width="547" height="782" alt="image"
src="https://github.com/user-attachments/assets/bcfcecd4-cbae-4d7b-9bcc-64bdf57929e2"
/>

**Enabled UI**

<img width="547" height="782" alt="image"
src="https://github.com/user-attachments/assets/0fb030ea-3bde-475e-982b-45e8f190cb8f"
/>


## Review Focus

- Feature flag pattern follows existing conventions (e.g.
`linearToggleEnabled`)
- Fallback behavior when essentials tab was previously selected by user

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9067-feat-add-feature-flag-to-disable-Essentials-tab-in-node-library-30e6d73d36508103b3cad9fc5d260611)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-02-21 21:40:45 -08:00
Christian Byrne
16ddcfdbaf feat: add toolkit node tracking to execution telemetry (#9073)
## Summary

Add toolkit (Essentials) node tracking to execution telemetry, enabling
measurement of toolkit node adoption and popularity.

## Changes

- **What**: Add `has_toolkit_nodes`, `toolkit_node_names`, and
`toolkit_node_count` fields to `ExecutionContext` and
`RunButtonProperties`. Toolkit nodes are identified via a hardcoded set
of node type names (10 novel Essentials nodes) and by `python_module ===
'comfy_essentials'` for blueprint nodes. Detection runs inside the
existing `reduceAllNodes()` traversal — no additional graph walks.

## Review Focus

- Toolkit node identification is frontend-only (no backend flag) — uses
two mechanisms: hardcoded `TOOLKIT_NODE_NAMES` set and
`TOOLKIT_BLUEPRINT_MODULES` for blueprints
- API node overlap is intentional — a node can appear in both
`api_node_names` and `toolkit_node_names`
- Blueprint detection via `python_module` automatically picks up new
essentials blueprints without code changes

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9073-feat-add-toolkit-node-tracking-to-execution-telemetry-30f6d73d365081b3ac91e697889c58b6)
by [Unito](https://www.unito.io)
2026-02-21 21:31:09 -08:00
Christian Byrne
ef5198be25 fix: invalidate loader node dropdown cache after model asset deletion (#8434)
## Summary

When deleting a model asset (checkpoint, lora, etc.), the loader node
dropdowns now update correctly by invalidating the category-keyed cache.

## Problem

After deleting a model asset in the asset browser, the loader node
dropdowns (e.g., CheckpointLoaderSimple, LoraLoader) still showed the
deleted model. Users had to refresh or re-open the dropdown to see the
updated list.

## Solution

After successful asset deletion, check each deleted asset's tags for
model categories (checkpoints, loras, etc.) and call
`assetsStore.invalidateCategory()` for each affected category. This
triggers a refetch when the dropdown is next accessed.

## Changes

- In `useMediaAssetActions.ts`:
  - After deletion, iterate through deleted assets' tags
- Check if each tag corresponds to a model category using
`modelToNodeStore.getAllNodeProviders()`
  - Call `invalidateCategory()` for each affected category

- In `useMediaAssetActions.test.ts`:
  - Added mocks for `useAssetsStore` and `useModelToNodeStore`
  - Added tests for deletion invalidation behavior

## Testing

- Added unit tests verifying:
  - Model cache is invalidated when deleting model assets
  - Multiple categories are invalidated when deleting multiple assets
  - Non-model assets (input, output) don't trigger invalidation

## Part of Stack

This is **PR 2 of 2** in a stacked PR series:
1. **[PR 1](https://github.com/Comfy-Org/ComfyUI_frontend/pull/8433)**:
Refactor asset cache to category-keyed (architectural improvement)
2. **This PR**: Fix deletion invalidation using the clean architecture

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8434-fix-invalidate-loader-node-dropdown-cache-after-model-asset-deletion-2f76d73d3650813181aedc373d9799c6)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved model cache invalidation after asset deletions — only
relevant model categories are invalidated and non-model assets are
ignored.
* Fixed edge-rendering behavior so reroutes are cleared correctly in the
canvas.

* **Chores**
* Added category-aware cache management and targeted refreshes for model
assets.

* **Tests**
* Expanded tests for cache invalidation, category handling, workflow
interactions, and related mocks.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-21 20:55:32 -08:00
Dante
38675e658f feat: add dev-time feature flag overrides via localStorage (#9075)
## Summary
- Adds `localStorage`-based dev-time override for feature flags, with
`ff:` key prefix (e.g.
`localStorage.setItem('ff:team_workspaces_enabled', 'true')`)
- Override priority: dev localStorage > remoteConfig >
serverFeatureFlags
- Guarded by `import.meta.env.DEV` — tree-shaken to empty function in
production builds
- Extracts `resolveFlag` helper in `useFeatureFlags` to eliminate
repeated fallback pattern

Fixes #9054

## Test plan
- [x] `getDevOverride` unit tests: boolean/number/string/object parsing,
prefix isolation, invalid JSON warning
- [x] `api.getServerFeature` / `serverSupportsFeature` override tests
- [x] `useFeatureFlags` override priority tests, including
`teamWorkspacesEnabled` bypassing guards
- [x] Production build verified: `getDevOverride` compiles to empty
function body, localStorage never accessed
- [x] `pnpm typecheck`, `pnpm lint` clean

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9075-feat-add-dev-time-feature-flag-overrides-via-localStorage-30f6d73d365081b394d3ccc461987b1a)
by [Unito](https://www.unito.io)
2026-02-21 20:36:09 -08:00
Dante
bd95150f82 test: remove unused DebugHelper from browser_tests (#9017)
## Summary

- Remove unused `DebugHelper` class and its import from browser test
fixtures

Fixes #8581

## Test plan

- [x] Verify browser tests still pass without `DebugHelper`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9017-test-remove-unused-DebugHelper-from-browser_tests-30d6d73d36508158b8c9fc83670df4f3)
by [Unito](https://www.unito.io)
2026-02-22 11:52:54 +09:00
Alexander Brown
f9317e7078 fix: refresh deps and clear production audit vulnerabilities (#9068)
## Summary
- refresh workspace dependency catalog and lockfile for security and
maintenance updates
- resolve production audit findings (runtime dependencies now report
clean)
- align Tiptap dependencies on v2 and pin `@tiptap/pm` to avoid
mixed-version type issues
- update Storybook preview toolbar config for Storybook 10 typing
(`showName` removed)

## Validation
- `pnpm typecheck` 
- `pnpm lint`  (warnings only)
- `pnpm test:unit` 
- `pnpm audit --prod` 
- `pnpm audit` ⚠️ remaining dev-only transitive advisories
(`minimatch`/`ajv`/`brace-expansion`) in upstream toolchain deps

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9068-fix-refresh-deps-and-clear-production-audit-vulnerabilities-30e6d73d36508122b778ec3ca99d41a4)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-02-21 17:44:33 -08:00
Comfy Org PR Bot
79e71a5761 1.41.1 (#9069)
Patch version increment to 1.41.1

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9069-1-41-1-30f6d73d365081148845cb3024b7987f)
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-21 17:19:38 -08:00
Christian Byrne
3e4d273832 fix: update imagePreview browser tests to use current fixture APIs (#8995)
The browser tests added in #8143 were failing on main because they were
written against stale `ComfyPage` APIs that were refactored in #8510
(merged Feb 3, before #8143 merged Feb 18).

### Changes
- `comfyPage.dragAndDropFile` → `comfyPage.dragDrop.dragAndDropFile`
- `comfyPage.setSetting` → `comfyPage.settings.setSetting`
- `comfyPage.loadWorkflow` → `comfyPage.workflow.loadWorkflow`
- `comfyPage.getNodeRefsByType` → `comfyPage.nodeOps.getNodeRefsByType`
- Fix `comfyPage` type parameter to use `ComfyPage` import
- Remove `test.fixme` since root cause was API mismatch, not test logic

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8995-fix-update-imagePreview-browser-tests-to-use-current-fixture-APIs-30d6d73d365081219c1eda4ea7251160)
by [Unito](https://www.unito.io)
2026-02-21 16:56:25 -08:00
jaeone94
8aa4e36fd5 [refactor] Extract executionErrorStore from executionStore (#9060)
## Summary
Extracts error-related state and logic from `executionStore` into a
dedicated `executionErrorStore` for better separation of concerns.

## Changes
- **New store**: `executionErrorStore` with all error state
(`lastNodeErrors`, `lastExecutionError`, `lastPromptError`), computed
properties (`hasAnyError`, `totalErrorCount`,
`activeGraphErrorNodeIds`), and UI state (`isErrorOverlayOpen`,
`showErrorOverlay`, `dismissErrorOverlay`)
- **Moved util**: `executionIdToNodeLocatorId` extracted to
`graphTraversalUtil`, reusing `traverseSubgraphPath` and accepting
`rootGraph` as parameter
- **Updated consumers**: 12 files updated to import from
`executionErrorStore`
- **Backward compat**: Deprecated getters retained in `ComfyApp` for
extension compatibility

## Review Focus
- Deprecated getters in `app.ts` — can be removed in a future
breaking-change PR once extension authors migrate

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9060-refactor-Extract-executionErrorStore-from-executionStore-30e6d73d36508101973de835ab6b199f)
by [Unito](https://www.unito.io)
2026-02-21 16:51:22 -08:00
Christian Byrne
d9fdb01d9b fix: handle failed global subgraph blueprint loading gracefully (#9063)
## Summary

Fix "Failed to load subgraph blueprints Error: [ASSERT] Workflow content
should be loaded" error occurring on cloud.

## Changes

- **What**: `getGlobalSubgraphData` now throws on API failure instead of
returning empty string, global blueprint data is validated before
loading, and individual global blueprint errors are properly propagated
to the toast/console reporting instead of being silently swallowed.

## Review Focus

Two root causes were fixed:
1. `getGlobalSubgraphData` returned `""` on failure — this empty string
was set as `originalContent`, which is falsy, triggering the assertion
in `ComfyWorkflow.load()`.
2. `loadInstalledBlueprints` used an internal `Promise.allSettled` whose
results were discarded, so individual global blueprint failures never
reached the error reporting in `fetchSubgraphs`.

Fixes COM-15199

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9063-fix-handle-failed-global-subgraph-blueprint-loading-gracefully-30e6d73d3650818d9cc8ecf81cd0264e)
by [Unito](https://www.unito.io)
2026-02-21 16:30:01 -08:00
Alexander Brown
8f48b11f6a fix: resolve missing i18n key warnings (#9064)
## Summary

Fix multiple i18n missing key console warnings by correcting key paths
and adding missing translations.

## Changes

- **What**: 
- `ScrubableNumberInput`: Fixed references to non-existent
`g.ariaLabel.decrement`/`g.ariaLabel.increment` keys → use
`g.decrement`/`g.increment`
- `SidebarIcon`: Replaced `t()` with `st()` (safe translate) to prevent
double-translation when parent components pass pre-translated strings
- `en/main.json`: Added missing `menuLabels.Copy`, `menuLabels.Paste`,
`menuLabels.Select All` keys

## Review Focus

The `SidebarIcon` change from `t()` to `st()` is the key design
decision. `SidebarIcon` receives both i18n keys (from sidebar tabs via
`SideToolbar`) and pre-translated strings (from dedicated sidebar button
components). Using `st()` (which checks `te()` before translating)
handles both cases without warnings.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9064-fix-resolve-missing-i18n-key-warnings-30e6d73d3650816eaad3ce030f9c1d3f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-02-21 15:26:37 -08:00
Christian Byrne
bb40ffae3c feat: add survey registry for feature survey configurations (#8355)
## Summary
Adds a centralized registry for feature survey configurations.

## Changes
- Add `surveyRegistry.ts` with `FEATURE_SURVEYS` record for survey
configs
- Add helper functions `getSurveyConfig()` and `getEnabledSurveys()`
- Export `FeatureId` type for type-safe feature references

## Part of Nightly Survey System
This is part 3 of a stacked PR chain:
1.  feat/feature-usage-tracker - useFeatureUsageTracker (merged in
#8189)
2.  feat/survey-eligibility - useSurveyEligibility (#8189, merged)
3. **feat/survey-config** - surveyRegistry.ts (this PR)
4. feat/survey-popover - NightlySurveyPopover.vue
5. feat/survey-integration - NightlySurveyController.vue

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8355-feat-add-survey-registry-for-feature-survey-configurations-2f66d73d365081faae6bda0c14c069d9)
by [Unito](https://www.unito.io)
2026-02-21 13:38:06 -08:00
Dante
de131133bd fix: make serverFeatureFlags reactive via Vue ref (#9051) 2026-02-21 18:43:58 +09:00
Benjamin Lu
17f34788dc fix: disable inspect for non-previewable assets (#8989)
## Summary
Prevent text/other assets from opening a blank fullscreen viewer by
restricting inspect/zoom to previewable media kinds.

## Changes
- Add `isPreviewableMediaType` helper in shared `formatUtil`.
- Gate inspect/zoom actions in `AssetsSidebarTab`, `MediaAssetCard`, and
`MediaAssetContextMenu` using an allowlist (`image`, `video`, `audio`,
`3D`).
- Build gallery items from previewable assets only.
- Add unit tests for `isPreviewableMediaType`.

## Why
`ResultGallery` only renders image/video/audio; text/other assets could
previously enter fullscreen with no renderable content.

## Review Focus
- Verify text/other assets no longer show Inspect and do not open
fullscreen.
- Verify image/video/audio behavior is unchanged.
- Verify 3D still opens the 3D viewer dialog.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8989-fix-disable-inspect-for-non-previewable-assets-30c6d73d36508103a9b9da4fe50236ea)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-02-21 01:43:14 -08:00
Benjamin Lu
9184f9bce4 fix: support text and misc generated asset states (#8914)
## Summary
Align generated-asset state classification to a single shared source and
implement the missing text/misc states in both card and list previews.

## Changes
- **What**:
- Extended `getMediaTypeFromFilename` in
`packages/shared-frontend-utils` to return `text` and `other`, and
changed unknown/no-extension fallback from `image` to `other`.
- Added text extension handling (`txt`, `md`, `json`, `csv`, `yaml/yml`,
`xml`, `log`) and kept existing media kinds.
- Updated generated-assets UI to use shared media-type detection
directly (removed the local generated-assets classifier).
  - Added text and misc card preview components:
    - `text` -> `icon-[lucide--text]`
    - `other` -> `icon-[lucide--check-check]`
- Updated list-item preview behavior so only `image`/`video` use preview
media URLs; `text`/`other` use icon fallback.
- Widened media kind schema for asset display metadata to include `text`
and `other`.
- **Breaking**: No API breaking changes; internal media kind union
widened for frontend asset display paths.
- **Dependencies**: None.

## Review Focus
- Verify generated text assets render paragraph/text icon state in card
+ list.
- Verify unknown/misc assets consistently render double-check icon state
in card + list.
- Verify existing image/video/audio/3D behavior remains unchanged.

## Screenshots (if applicable)
<img width="282" height="158" alt="image"
src="https://github.com/user-attachments/assets/76cf2d1b-9d34-4c7c-92a1-50bbc55871e5"
/>
<img width="432" height="489" alt="image"
src="https://github.com/user-attachments/assets/024fece3-f241-484d-a37e-11948559ebbc"
/>
<img width="421" height="494" alt="image"
src="https://github.com/user-attachments/assets/ed64ba0c-bf46-4c3b-996e-4bc613ee029e"
/>


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8914-fix-support-text-and-misc-generated-asset-states-3096d73d365081f28ca7c32f306e4b50)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-02-21 01:27:28 -08:00
Comfy Org PR Bot
ea7bbb744f 1.41.0 (#9059)
Minor version increment to 1.41.0

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9059-1-41-0-30e6d73d36508103b6cbef6d402f05de)
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-21 01:13:57 -08:00
Benjamin Lu
25880aa024 fix: update asset video previews for card and list (#8908)
## Summary
Render generated video previews in list items using a real video element (instead of an image element (this caused errors before)) and include a custom play button with dimming per [the designs](https://www.figma.com/design/LVilZgHGk5RwWOkVN6yCEK/Queue-Progress-Modal?node-id=3928-39270&m=dev).

## Changes
- **What**:
  - List item preview path now renders a `video` element when `isVideoPreview` is true.
  - Video list preview uses `preload="metadata"`, `muted`, `playsinline`, and `pointer-events-none` so row click behavior stays unchanged.
  - Kept the custom overlay/play affordance and increased overlay dimming from `bg-black/10` to `bg-black/15`.
  - Updated tests for `AssetsListItem`, `MediaVideoTop`, and `AssetsSidebarListView`.

## Review Focus
- Confirm list item click behavior still opens/selects asset (no inline playback interaction).
- Confirm video list previews now show actual video frame path instead of broken image fallback.

## Limitation
Backend does not currently provide a dedicated poster/thumbnail image for video outputs in the job preview payload. In the frontend today, we can either show a video icon placeholder, or load/render the full video itself to obtain a preview frame.

## Screenshots (if applicable)
<img width="427" height="499" alt="image" src="https://github.com/user-attachments/assets/3f974817-9d73-4fee-9fa5-2f1f68942c06" />
<img width="230" height="92" alt="image" src="https://github.com/user-attachments/assets/1fbfdd6a-72dd-47e2-96bf-8f7eb41c36f2" />
2026-02-21 00:59:36 -08:00
Christian Byrne
40aa7c5974 feat(persistence): fix QuotaExceededError and cross-workspace draft leakage (#8520)
## Summary

Completes the workflow persistence overhaul by integrating the new draft
system into the app and migrating existing data. Fixes two critical
bugs:

1. **QuotaExceededError** - localStorage fills up with workflow drafts,
breaking auto-save
2. **Cross-workspace data leakage** - Drafts from one ComfyUI instance
appear in another

## Changes

- **What**: 
- `useWorkflowPersistenceV2.ts` - Main composable that hooks into graph
changes with 512ms debounce
- `migrateV1toV2.ts` - One-time migration of existing drafts to the new
scoped format
  - Updated E2E tests for new storage key patterns
- **Why**: Users lose work when storage quota is exceeded, and see
confusing workflows from other instances

## How It Works

- **Workspace scoping**: Each ComfyUI instance (identified by server
URL) has isolated draft storage
- **LRU eviction**: When storage is full, oldest drafts are
automatically removed (keeps 32 most recent)
- **Tab isolation**: Each browser tab tracks its own active/open
workflows via sessionStorage
- **Debounced saves**: Graph changes are batched with 512ms delay to
reduce storage writes

## Migration

Existing V1 drafts are automatically migrated on first load. The
migration:
1. Reads drafts from old `Comfy.Workflow.*` keys
2. Converts to new workspace-scoped format
3. Cleans up old keys after successful migration

---
*Part 4 of 4 in the workflow persistence improvements stack*

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Simula_r <18093452+simula-r@users.noreply.github.com>
2026-02-21 00:57:50 -08:00
Comfy Org PR Bot
3d3a4dd1a2 1.40.10 (#9058)
Patch version increment to 1.40.10

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9058-1-40-10-30e6d73d36508133b358d9f35840055a)
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-21 00:57:11 -08:00
Christian Byrne
39af93ae3e feat: read category from blueprint subgraph definition (#9053)
Read `category` from `definitions.subgraphs[0].category` in blueprint
JSON files as a fallback default for node categorization.

This allows blueprint authors to set the category directly in the
blueprint file without needing backend `index.json` support. The
precedence order is:
1. Explicit overrides (e.g. `info.category` from API, or `'Subgraph
Blueprints/User'` for user blueprints)
2. `definitions.subgraphs[0].category` from the blueprint JSON content
3. Bare `'Subgraph Blueprints'` fallback

Companion PR: Comfy-Org/ComfyUI#12552 (adds essential blueprints with
categories matching the Figma design)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9053-feat-read-category-from-blueprint-subgraph-definition-30e6d73d3650810ca23bfc5a1e97cb31)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-20 23:58:15 -08:00
Dante
4849d4a6c9 fix: raise graphdialog z-index above menu bars (#9049)
## Summary

- Raise `.graphdialog` z-index from 41 to 1500 so the widget prompt
dialog is no longer hidden behind menu bars when a node is near the top
of the screen.

### Why 1500 instead of 10000?

The issue suggested 10000, but that would place the dialog **above**
context menus (9999) and at the same level as toasts (10000). A value of
1500 is sufficient to sit above the top menu bar (1001) and actionbar
(1300), while staying **below** popovers (1700), context menus (9999),
and toasts (10000).

| Element | z-index |
|---------|---------|
| `.comfyui-body-top` (top menu) | 1001 |
| Actionbar | 1300 |
| **`.graphdialog` (this PR)** | **1500** |
| Popover | 1700 |
| Context menus / search box | 9999 |
| Toast | 10000 |

- Fixes #4573
### test

#### AS IS
<img width="534" height="120" alt="스크린샷 2026-02-21 오후 1 27 56"
src="https://github.com/user-attachments/assets/e58922b4-ae3f-4083-a0e1-06c27efb64af"
/>

#### TO BE
<img width="659" height="140" alt="스크린샷 2026-02-21 오후 1 47 09"
src="https://github.com/user-attachments/assets/980334f4-b5d2-43f5-a237-d7c2a7dfb6c9"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9049-fix-raise-graphdialog-z-index-above-menu-bars-30e6d73d36508172b9b3c728c3628477)
by [Unito](https://www.unito.io)

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 23:03:35 -08:00
Christian Byrne
55ee6e7e63 fix: "Add Subgraph to Library" context menu not saving subgraph (#9056)
## Summary

Fix "Add Subgraph to Library" context menu option which was bookmarking
(UI favorite) instead of actually saving the subgraph as a reusable
blueprint.

## Changes

- **What**: Replace `nodeBookmarkStore.addBookmark()` with
`subgraphStore.publishSubgraph()` in `addSubgraphToLibrary`, matching
the working toolbar button behavior. Hide the menu option when multiple
items are selected since `publishSubgraph` requires exactly one
SubgraphNode.

## Review Focus

The original implementation (PR #5218) used `addBookmark` which only
adds a star/favorite — it never called the `publishSubgraph` function
that serializes, prompts for a name, and saves the subgraph as a
blueprint file. The toolbar button (`SaveToSubgraphLibrary.vue`) worked
correctly because it calls the `Comfy.PublishSubgraph` command which
uses `publishSubgraph`.

The multi-select visibility guard (`!hasMultipleSelection`) matches
`SaveToSubgraphLibrary.vue`'s `v-show` guard. "Unpack Subgraph" remains
visible for multi-select since it handles multiple SubgraphNodes
correctly.

Fixes COM-15200

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9056-fix-Add-Subgraph-to-Library-context-menu-not-saving-subgraph-30e6d73d36508177ba7ef97a2fe9b893)
by [Unito](https://www.unito.io)
2026-02-20 22:56:53 -08:00
Yourz
74d285bda9 fix: resolve bookmark lookup for subgraph blueprints (#9057)
## Summary

Bookmarked subgraph blueprint nodes were not appearing in the favorites
tree because `buildBookmarkTree` looked up nodes only in
`nodeDefsByName`, which excludes subgraph blueprints.

## Changes

- **What**: Added `allNodeDefsByName` computed in `nodeDefStore` that
includes both regular nodes and subgraph blueprints. Updated
`nodeBookmarkStore.buildBookmarkTree` to use it for bookmark resolution.

## Review Focus

- `allNodeDefsByName` iterates over `nodeDefs` (which merges
`subgraphBlueprints` + `nodeDefsByName`). Confirm this doesn't introduce
performance concerns for large node sets.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9057-fix-resolve-bookmark-lookup-for-subgraph-blueprints-30e6d73d365081259bcae9d0c197de43)
by [Unito](https://www.unito.io)
2026-02-21 14:48:57 +08:00
Christian Byrne
1bcebd8293 fix: display Blueprint badge instead of UUID for global subgraph blueprints (#9048)
## Problem

The node search box badge displays long UUID strings (e.g.,
`comfyui-ltx-video-0fbc55c6-...`) for global/core blueprints, while
user-created subgraphs correctly show "Blueprint" as the badge.


![image](https://github.com/user-attachments/assets/placeholder-before.png)

## Root Cause

In `loadGlobalBlueprint`, global blueprints set `python_module:
v.info.node_pack` which contains UUID-like strings. The
`getNodeSource()` function processes `python_module` to determine badge
text, but UUID strings don't match any known pattern, resulting in ugly
badge text.

## Solution

- Change global blueprints to use `python_module: 'blueprint'` so they
display "Blueprint" badge like user blueprints
- Add `isGlobal` boolean flag to `ComfyNodeDef` schema to distinguish
global from user blueprints
- Update `isGlobalBlueprint()` to check the new `isGlobal` flag instead
of `python_module !== 'blueprint'`

## Changes

| File | Change |
|------|--------|
| `src/schemas/nodeDefSchema.ts` | Add optional `isGlobal?: boolean`
field |
| `src/stores/nodeDefStore.ts` | Add `isGlobal` field to
`ComfyNodeDefImpl` class |
| `src/stores/subgraphStore.ts` | Use `python_module: 'blueprint'` +
`isGlobal: true` for global blueprints; update `isGlobalBlueprint()`
check |

## Testing

- [x] Existing unit tests pass
- [x] TypeScript compiles without errors
- [x] Lint passes

Fixes COM-15168

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9048-fix-display-Blueprint-badge-instead-of-UUID-for-global-subgraph-blueprints-30e6d73d3650813cac27e02f8f2088df)
by [Unito](https://www.unito.io)
2026-02-20 22:34:48 -08:00
Dante
dac58ad811 feat: show template version in system info for local builds (#9018)
## Summary
<img width="985" height="417" alt="스크린샷 2026-02-21 오전 1 05 10"
src="https://github.com/user-attachments/assets/4543a5aa-d1ae-4be7-971e-7b1454625829"
/>



- Add `installed_templates_version` and `required_templates_version` to
the system stats schema
- Display the template version as a badge on the About page alongside
ComfyUI and Frontend badges
- Display the template version as a row in the local System Info table
- Highlight badge (red severity) and table row (red text) when installed
version doesn't match required version, so users can quickly spot
outdated templates

Fixes #4006

## Test plan

- [x] Open Settings > About and verify "Templates v{version}" badge
appears
- [x] Verify "Templates Version" row appears in System Info table
- [ ] When `installed_templates_version` matches
`required_templates_version`: badge is default color, table row is
default color
- [ ] When versions don't match: badge turns red, table row turns red
- [ ] When `installed_templates_version` is absent (package not
installed): badge is hidden, table row shows empty

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-21 15:21:21 +09:00
Yourz
6ee3803770 feat: implement NodeLibrarySidebarTabV2 with Reka UI components (#8548)
## Summary

Implement a redesigned Node Library sidebar using Reka UI components
with virtualized tree rendering and improved UX.

## Changes

- **What**: 
  - Add three-tab structure (Essential, All, Custom) using Reka UI Tabs
- Implement TreeExplorerV2 with virtualized tree using
TreeRoot/TreeVirtualizer for performance
  - Add node hover preview with teleport to show NodePreview component
  - Implement context menu for toggling favorites on nodes
  - Add search functionality that auto-expands matching folders
- Create panel components: EssentialNodesPanel, AllNodesPanel,
CustomNodesPanel
  - Add 'Open Manager' button in CustomNodesPanel
  - Use custom icons: comfy--node for nodes, ph--folder-fill for folders
  - New node preview component: `NodePreviewCard`
  - Api node folder icon
  - Node drag preview

- **Feature Flag**: Enabled via URL parameter `?nodeRedesign=true`

## Review Focus

- TreeExplorerV2.vue uses `[...expandedKeys]` to prevent internal
mutation by Reka UI TreeRoot
- Context menu injection key is exported from TreeExplorerV2Node.vue and
imported by TreeExplorerV2.vue
- Hover preview uses teleport to
`#node-library-node-preview-container-v2`

## Screenshots (if applicable)

| Feature | Screenshot | 
|---|---|
| All nodes tab |<img width="323" height="761" alt="image"
src="https://github.com/user-attachments/assets/1976222b-83dc-4a1b-838a-2d49aedea3b8"
/>|
| Custom nodes tab | <img width="308" height="748" alt="image"
src="https://github.com/user-attachments/assets/2c23bffb-bdaa-4c6c-8cac-7610fb7f3fb7"
/>|
|Api nodes icon | <img width="299" height="523" alt="image"
src="https://github.com/user-attachments/assets/e9ca05b0-1143-44cf-b227-6462173c7cd0"
/>|
| node preview|<img width="499" height="544" alt="image"
src="https://github.com/user-attachments/assets/8961a7b4-77ae-4e57-99cf-62d9e4e17088"
/>|
| node drag preview | <img width="434" height="289" alt="image"
src="https://github.com/user-attachments/assets/b5838c90-65d4-4bee-b2b3-c41b57870da8"
/>|



Test by adding `?nodeRedesign=true` to the URL

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8548-WIP-feat-implement-NodeLibrarySidebarTabV2-with-Reka-UI-components-2fb6d73d36508134b7e0f75a2c9b976a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: bymyself <cbyrne@comfy.org>
2026-02-20 22:06:09 -08:00
Jin Yi
fd2ffb7100 [feat] Replace view mode toggle with settings dropdown menu (#8950) 2026-02-21 13:49:19 +09:00
Jin Yi
c05644045f fix(assets): dismiss context menu on scroll and outside click (#8952) 2026-02-21 13:19:13 +09:00
Comfy Org PR Bot
5fe902358c 1.40.9 (#9034)
Patch version increment to 1.40.9

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9034-1-40-9-30e6d73d365081a1b1e4e7a1c0b77629)
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: Alexander Brown <drjkl@comfy.org>
2026-02-20 20:16:39 -08:00
Christian Byrne
c1a569211d fix: skip MatchType recalculation during graph configuration (#9004)
## Summary

Fixes COM-14955: "Bug: Switch node in subgraph causes link disconnection
on export"

## Problem

When a MatchType node (like Switch) inside a subgraph is
configured/restored, `LGraphNode.configure()` calls
`onConnectionsChange` for each input sequentially. The
`withComfyMatchType` callback was running before all links were
restored, seeing incomplete state and incorrectly computing types, which
could cause link disconnection.

## Solution

Add early return when `app.configuringGraph` is true to defer type
recalculation until after all links are restored. This pattern is
already used throughout the codebase:
- `widgetInputs.ts`
- `rerouteNode.ts`
- `customWidgets.ts`

Post-configure recomputation is handled by the existing
`requestAnimationFrame` callback in `applyMatchType`.

## Changes

- `src/core/graph/widgets/dynamicWidgets.ts` - Added 1 line: `if
(app.configuringGraph) return`
- `src/core/graph/widgets/matchTypeConfiguring.test.ts` - New test file
with 3 tests

## Testing

- All existing tests pass
- Added 3 new tests:
  - `skips type recalculation when configuringGraph is true`
  - `performs type recalculation during normal operation`
  - `connects both inputs with same type`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9004-fix-skip-MatchType-recalculation-during-graph-configuration-30d6d73d365081339088ffd8aebba107)
by [Unito](https://www.unito.io)
2026-02-20 19:44:34 -08:00
Jin Yi
2b69d7b49c [bugfix] Fix node replacements not loading due to feature flag timing (#9037)
## Summary
- Node replacements were never loaded because
`useNodeReplacementStore().load()` was called before `api.init()`,
meaning `serverFeatureFlags` was always empty at that point
- Dispatch `feature_flags` as a custom event from `api.ts` and trigger
`load()` in response within `addApiUpdateHandlers()`

## Changes
- **`api.ts`**: Dispatch `feature_flags` custom event after storing
server feature flags (already typed in `BackendApiCalls`)
- **`app.ts`**: Replace eager `load()` call with `feature_flags` event
listener inside `addApiUpdateHandlers()`, consistent with other API
event handlers
- **`nodeReplacementStore.ts`**: Use `api.getServerFeature()` directly
instead of `useFeatureFlags` composable; remove side effects from store
setup
- **`nodeReplacementStore.test.ts`**: Update mocks to match new
`api.getServerFeature` usage

## Review Focus
- Initialization ordering: listener registered before `api.init()`
ensures no missed events

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9037-bugfix-Fix-node-replacements-not-loading-due-to-feature-flag-timing-30e6d73d36508107ae2cd72e83c01e1a)
by [Unito](https://www.unito.io)
2026-02-20 19:16:01 -08:00
Christian Byrne
ee0789e153 fix: promoted widget labels show widgetName instead of "nodeId: widgetName" (#9013)
## Summary

Promoted/proxied widgets on subgraph nodes showed generic "nodeId:
widgetName" labels (e.g., "3: seed") in the LiteGraph renderer. Now they
correctly show just the widget name (e.g., "seed").

## Changes

- **What**: Set proxy widget overlay `label` to `widgetName` instead of
`name` (which contains the unique `"nodeId: widgetName"` format). The
overlay `name` still retains the unique format for internal
identification.
- Added 2 tests verifying proxy widget label defaults and user rename
behavior.

## Review Focus

- The one-line fix in `proxyWidget.ts` line 157: `label: widgetName`
instead of `label: name`
- The overlay `name` property is unchanged — only `label` (used for
display) is affected
- No code parses the label string for identification; all lookups use
`_overlay.nodeId` and `_overlay.widgetName` directly

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9013-fix-promoted-widget-labels-show-widgetName-instead-of-nodeId-widgetName-30d6d73d365081ca8d2feb68585fc187)
by [Unito](https://www.unito.io)
2026-02-20 19:14:57 -08:00
Christian Byrne
c1d07d6424 fix: restore strictExecutionOrder for Storybook Chromatic builds (#9038)
## Summary

Restore `strictExecutionOrder: true` in `.storybook/main.ts`
rolldownOptions, accidentally removed in #8834 as a merge artifact.

## Problem

Chromatic visual regression tests fail on `version-bump-*` release
branches with:
```
Error: __STORYBOOK_MODULE_CORE_EVENTS_PREVIEW_ERRORS__ is not defined
```

Rolldown without `strictExecutionOrder` doesn't guarantee module
execution order. Storybook's internal module system defines
`__STORYBOOK_MODULE_*` globals during initialization — without strict
ordering, downstream code references them before they're defined.

Only `version-bump-*` branches are affected because the
`chromatic-deployment` CI job (which actually loads and extracts stories
at runtime) is gated to those branches.

## Changes

- Restore `strictExecutionOrder: true` in `.storybook/main.ts`
rolldownOptions output config

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9038-fix-restore-strictExecutionOrder-for-Storybook-Chromatic-builds-30e6d73d365081489c53cea131b97a2f)
by [Unito](https://www.unito.io)
2026-02-20 19:03:06 -08:00
Jin Yi
4e9e3a0c26 [bugfix] Fix workspace settings member layout alignment (#9008) 2026-02-21 11:29:21 +09:00
Terry Jia
f7a83f6dfa feat: add gradient-slider widget for FLOAT inputs (#8992)
## Summary
Add a new 'gradient-slider' display mode for FLOAT widget inputs. Nodes
can specify gradient_stops (color stop arrays) to render a colored
gradient track behind the slider thumb, useful for color adjustment
parameters like hue, saturation, brightness, etc.

- GradientSlider.vue: reusable Reka UI-based gradient slider component
- GradientSliderWidget.ts: litegraph canvas-mode fallback rendering
- WidgetInputNumberGradientSlider.vue: Vue node widget integration
- Schema, registry, and type updates for gradient-slider support

this is prerequisite for color correct and balance

BE changes https://github.com/Comfy-Org/ComfyUI/pull/12536

## Screenshots (if applicable)

<img width="610" height="237" alt="image"
src="https://github.com/user-attachments/assets/b0577ca8-8576-4062-8f14-0a3612e56242"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8992-feat-add-gradient-slider-widget-for-FLOAT-inputs-30d6d73d36508199b3e8db6a0c213ab4)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-20 20:51:10 -05:00
Christian Byrne
4103379901 fix: intercept window.open in zendesk test to avoid external network dependency (#9035)
The zendesk support test was flaky because it waited for `networkidle`
on an external Zendesk page (`support.comfy.org`). External network
requests in CI are unreliable — they can timeout, be slow, or redirect.

**Fix:** Intercept `window.open` to capture the URL that would be
opened, without actually navigating to the external page. This makes the
test deterministic and fast.

- Fixes flaky test: `[chromium] ›
browser_tests/tests/dialog.spec.ts:339:3 › Support › Should open
external zendesk link with OSS tag`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9035-fix-intercept-window-open-in-zendesk-test-to-avoid-external-network-dependency-30e6d73d365081c2a021edd816b8c1d0)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-02-20 17:28:40 -08:00
Christian Byrne
337e0486ea feat: client-side distribution filtering for blueprint subgraphs (#8686)
Adds client-side filtering of blueprint subgraphs by distribution.

**Changes:**
- Added `includeOnDistributions` typed field to `GlobalSubgraphData` in
`api.ts`
- Distribution detection: `isCloud → 'cloud'`, `isDesktop → 'desktop'`,
else `'localhost'`
- Filters subgraphs before loading — excluded blueprints are never
fetched

**Depends on:** Comfy-Org/workflow_templates schema update (merge first)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8686-feat-client-side-distribution-filtering-for-blueprint-subgraphs-2ff6d73d365081d29f79c4e3cab174ac)
by [Unito](https://www.unito.io)
2026-02-20 17:02:54 -08:00
Christian Byrne
b27eb5861a fix(queue): allow deleting failed jobs from queue progress UI (#8478)
## Summary
Failed jobs could not be removed from the Media Assets queue progress
panel because `useJobActions` only supported cancel for pending/running
jobs.

## Changes
- Add `deleteAction`, `canDeleteJob`, `runDeleteJob` to `useJobActions`
composable
- Export `removeFailedJob` from `useJobMenu` with optional task
parameter
- Update `ActiveMediaAssetCard.vue` to show delete button on failed jobs

## Testing
1. Queue a workflow that will fail (e.g., missing model)
2. Open Media Assets panel
3. Hover over the failed job card → delete button (circle-minus icon)
appears
4. Click delete → job is removed from queue


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8478-fix-queue-allow-deleting-failed-jobs-from-queue-progress-UI-2f86d73d3650810ba3aaf6cf38703bf5)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-02-20 16:49:36 -08:00
Benjamin Lu
b3aed9afd0 feat: split job history into a dedicated sidebar tab (#8957)
## Summary

Move queue job history into a dedicated sidebar tab (gated by `Comfy.Queue.QPOV2`) and remove mixed job-history UI from the Assets sidebar so assets and job controls are separated.

## Changes

- **What**:
  - Added `JobHistorySidebarTab` with reusable job UI primitives: `JobFilterTabs`, `JobFilterActions`, `JobAssetsList`, and shared `JobHistoryActionsMenu`.
  - Added reactive `job-history` tab registration in `sidebarTabStore`; prepends above Assets when `Comfy.Queue.QPOV2` is enabled and unregisters cleanly when disabled.
  - Added debounced search to `useJobList` (filters by job title, metadata, and prompt id).
  - Extracted clear-history dialog logic to `useQueueClearHistoryDialog` and reused it from queue overlay and job history tab.
  - Removed active-job rendering and queue-clear controls from assets list/grid/tab views; assets sidebar now focuses on media assets only.
  - Removed the QPOV2 gate from `MediaAssetViewModeToggle` and updated queue/job localized copy.
  - Added and updated tests for queue overlay header actions, job filters, search filtering, sidebar tab registration, and assets sidebar behavior.

## Review Focus

- Verify QPOV2 toggle behavior:
  - `Docked Job History` menu action toggles `Comfy.Queue.QPOV2`.
  - `job-history` tab insertion/removal order and active-tab reset on removal.
- Verify behavior split between tabs:
  - Job controls (cancel/delete/view/filter/search/clear history/clear queue) live in Job History.
  - Assets sidebar loading/empty states and list/grid rendering remain correct after removing active jobs.

## Screenshots (if applicable)
<img width="670" height="707" alt="image" src="https://github.com/user-attachments/assets/3a201fcb-d104-4e95-b5fe-49c4006a30a5" />
2026-02-20 16:42:41 -08:00
AustinMroz
7baa14af86 Fix badges to bottom of node (#9033)
| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/75171909-829e-49c0-9d94-3d5588f28f05"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/1ee438c7-8231-4d8f-abea-f901a682dfa3"/>|

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9033-Fix-badges-to-bottom-of-node-30d6d73d365081a584b1ca925980e59a)
by [Unito](https://www.unito.io)
2026-02-20 15:49:15 -08:00
Benjamin Lu
7921c38db9 chore: trigger snapshot refresh (#9027)
## Summary
- apply a tiny docs-only wording update in `README.md`
- create a non-functional diff to trigger snapshot regeneration workflow

## Testing
- pnpm typecheck
- pnpm lint

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9027-chore-trigger-snapshot-refresh-30d6d73d365081038bc2ed6e463d0e5f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-20 14:48:25 -08:00
jaeone94
46c40c755e feat: node-specific error tab with selection-aware grouping and error overlay (#8956)
## Summary
Enhances the error panel with node-specific views: single-node selection
shows errors grouped by message in compact mode, container nodes
(subgraph/group) expose child errors via a badge and "See Error" button,
and a floating ErrorOverlay appears after execution failure with a
deduplicated summary and quick navigation to the errors tab.

## Changes
- **Consolidate error tab**: Remove `TabError.vue`; merge all error
display into `TabErrors.vue` and drop the separate `error` tab type from
`rightSidePanelStore`
- **Selection-aware grouping**: Single-node selection regroups errors by
message (not `class_type`) and renders `ErrorNodeCard` in compact mode
- **Container node support**: Detect child-node errors in subgraph/group
nodes via execution ID prefix matching; show error badge and "See Error"
button in `SectionWidgets`
- **ErrorOverlay**: New floating card shown after execution failure with
deduplicated error messages, "Dismiss" and "See Errors" actions;
`isErrorOverlayOpen` / `showErrorOverlay` / `dismissErrorOverlay` added
to `executionStore`
- **Refactor**: Centralize error ID collection in `executionStore`
(`allErrorExecutionIds`, `hasInternalErrorForNode`); split `errorGroups`
into `allErrorGroups` (unfiltered) and `tabErrorGroups`
(selection-filtered); move `ErrorOverlay` business logic into
`useErrorGroups`

## Review Focus
- `useErrorGroups.ts`: split into `allErrorGroups` / `tabErrorGroups`
and the new `filterBySelection` parameter flow
- `executionStore.ts`: `hasInternalErrorForNode` helper and
`allErrorExecutionIds` computed
- `ErrorOverlay.vue`: integration with `executionStore` overlay state
and `useErrorGroups`

## Screenshots
<img width="853" height="461" alt="image"
src="https://github.com/user-attachments/assets/a49ab620-4209-4ae7-b547-fba13da0c633"
/>
<img width="854" height="203" alt="image"
src="https://github.com/user-attachments/assets/c119da54-cd78-4e7a-8b7a-456cfd348f1d"
/>
<img width="497" height="361" alt="image"
src="https://github.com/user-attachments/assets/74b16161-cf45-454b-ae60-24922fe36931"
/>

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-20 12:14:52 -08:00
Robin Huang
c2452c5d20 feat: add tooltip to clear queued jobs button in expanded queue overlay (#9020)
Adds a "Clear all jobs" tooltip to the cancel queued jobs button in the
expanded job queue header, matching the tooltip pattern used elsewhere
in the queue overlay.

Helps the user understand what this button does.

## Test plan
- Open the expanded job queue modal with queued jobs
- Hover over the cancel (list-x) button next to the queued count
- Verify the "Clear all jobs" tooltip appears

<img width="399" height="267" alt="Screenshot 2026-02-20 at 11 24 36 AM"
src="https://github.com/user-attachments/assets/9c83a3e8-4905-44ee-b270-b16401e9a20c"
/>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 20:00:35 +00:00
Christian Byrne
7fba000d68 feat: default Vue Nodes (Nodes 2.0) to enabled for new cloud installs (#9009)
## Summary

Default Nodes 2.0 (`Comfy.VueNodes.Enabled`) to `true` for new cloud
installs (≥1.41.0).

## Changes

- **What**: Add `defaultsByInstallVersion: { '1.41.0': isCloud }` to the
`Comfy.VueNodes.Enabled` setting. Since `isCloud` is a compile-time
constant, cloud builds get `{ '1.41.0': true }` while local builds get
`{ '1.41.0': false }` (tree-shaken away).

## Review Focus

- Version threshold `1.41.0` — should this match a specific upcoming
release?
- Existing users (installed before 1.41.0) and non-cloud builds are
unaffected — they keep the `defaultValue: false` fallback.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9009-feat-default-Vue-Nodes-Nodes-2-0-to-enabled-for-new-cloud-installs-30d6d73d365081268337f7ee72c24d98)
by [Unito](https://www.unito.io)
2026-02-20 09:11:49 -08:00
AustinMroz
306fb94cf5 Linear mode arrangement tweaks (#8853)
Planning to keep updates smaller and more contained in the interest of
collaboration and velocity
- The breadcrumb hamburger menu that provides workflow options is now
displayed in linear mode
- As part of this change, the reka-ui popover component now accepts
primvevue format MenuItems
- I prefer the format I had, but this makes transitioning stuff easier.
- The simplified linear history is moved to always be horizontal and
shown beneath previews.
- The label has been removed from the "Give Feedback" button on desktop
so it does not overlap
- The full side toolbar is displayed in linear mode
  - This is temporary,  but it gets the dead code pruned out now.
- Lays some groundwork for selecting an asset from the assets panel to
also select the item in the main linear panel
- The api `promptQueued` event can now optionally include a promptIds,
which list the ids for all jobs that were queued together as part of
that batch
- Update the max for the `number of generations` field to respect the
recently updated cloud limits

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/e632679c-d727-4882-841b-09e99a2f81a4"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/a9bcd809-c314-49bd-a479-2448d1a88456"/>|

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8853-Linear-mode-arrangement-tweaks-3066d73d365081589355ef753513900b)
by [Unito](https://www.unito.io)
2026-02-20 03:17:19 -08:00
Jin Yi
7bf9d51d1d [bugfix] Show active job count in assets sidebar badge (#9015)
## Summary
Assets sidebar badge now shows total active jobs (pending + running)
instead of only pending jobs, making it consistent with the "X active
jobs" label inside the panel.

## Changes
- **What**: Changed `iconBadge` in `useAssetsSidebarTab` from
`pendingTasks.length` to `activeJobsCount` (pending + running)
- Badge still hidden when QPO V2 is disabled (legacy queue)

## Review Focus
- Whether `activeJobsCount` (pending + running) is the correct metric
for the badge

<img width="1718" height="1327" alt="스크린샷 2026-02-20 오후 7 46
40(2)"
src="https://github.com/user-attachments/assets/4d0d2bed-8be9-44d7-bd62-4dd8c075d265"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9015-bugfix-Show-active-job-count-in-assets-sidebar-badge-30d6d73d365081c287a6ce9bc0a60244)
by [Unito](https://www.unito.io)
2026-02-20 20:02:10 +09:00
Christian Byrne
01f59afff2 test: fix flaky 'Can drag node' screenshot test (#8967)
## Summary

Fix intermittently failing 'Can drag node' screenshot test that blocks
CI on main and all PR branches.

## Changes

- **What**: Add `nextFrame()` waits after switching `Comfy.UseNewMenu`
from `Top` to `Disabled` in the `beforeEach` hook. The setting change
removes the top bar, causing the canvas to resize. Without waiting, the
hardcoded drag coordinates can miss the node entirely (resulting in a
canvas pan instead of a node drag).

## Review Focus

The root cause: `setSetting('Comfy.UseNewMenu', 'Disabled')` triggers a
layout shift (top bar disappears → canvas grows vertically). Litegraph
needs 1-2 frames to process the canvas resize. The drag starts at
hardcoded screen coords `{622, 400}` which only map to the node after
the resize settles.
2026-02-20 02:37:32 -08:00
Dante
f4ca285d07 feat: add Copy, Paste, Select All commands to Edit menu (#8954)
## Summary

- Add Copy, Paste, and Select All commands to the Edit menu for
mobile/touch users and accessibility
- Menu-based copy uses LiteGraph internal clipboard; existing Ctrl+C/V
behavior is unchanged

## Changes

- `useCoreCommands.ts`: Register three new commands (`CopySelected`,
`PasteFromClipboard`, `SelectAll`)
- `coreMenuCommands.ts`: Add menu entries under Edit (between Undo/Redo
and Clear Workflow)
- `useCoreCommands.test.ts`: Add unit tests for the new commands

### AS IS
<img width="260" height="176" alt="스크린샷 2026-02-18 오후 5 44 14"
src="https://github.com/user-attachments/assets/8c9c86e1-55cc-411b-9d42-429001e04630"
/>


### TO BE
<img width="516" height="497" alt="스크린샷 2026-02-19 오후 5 07 28"
src="https://github.com/user-attachments/assets/a2047541-582f-4520-a08f-98c6e532d29f"
/>


## Test plan

- [x] Verify Copy/Paste/Select All appear in Edit menu
- [x] Select nodes → Edit > Copy → Edit > Paste → nodes duplicated
- [x] Edit > Select All → all canvas items selected
- [x] Copy with no selection → no-op (no error)
- [x] Existing Ctrl+C/V keyboard shortcuts still work

Fixes #2892

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8954-feat-add-Copy-Paste-Select-All-commands-to-Edit-menu-30b6d73d365081ec9270ed2a562eaf0b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-20 02:34:45 -08:00
sno
06732b84bb Add Mixpanel detection to telemetry tree-shaking validation (#8826)
## Summary
Enhances the existing CI telemetry scan workflow to also detect Mixpanel
code in dist files, ensuring it's properly tree-shaken from OSS builds.

## Context
- Extends existing `ci-dist-telemetry-scan.yaml` (added in PR #8354)
- Based on analysis in closed PR #6777 (split into focused PRs)
- Complements GTM detection already in place
- Part of comprehensive OSS compliance effort

## Implementation
- Adds separate Mixpanel check step with specific patterns:
  - `mixpanel.init`
  - `mixpanel.identify` 
  - `MixpanelTelemetryProvider`
  - `mp.comfy.org`
  - `mixpanel-browser`
  - `mixpanel.track(`
- Separates GTM and Mixpanel checks for clarity
- Adds `DISTRIBUTION=localhost` env var to build step
- Excludes source maps from scanning

## Related
- Supersedes part of closed PR #6777
- Complements existing GTM check from PR #8354
- Related to PR #8623 (GTM-focused, may be redundant)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8826-Add-Mixpanel-detection-to-telemetry-tree-shaking-validation-3056d73d36508153bab5f55d4bb17658)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-20 02:31:37 -08:00
Christian Byrne
c809ac5a43 refactor: extract shouldUseAssetBrowser(), fix missing inputNameForBrowser, sanitize logs (#8867)
## Summary

Extract duplicated asset-browser eligibility guard into
`shouldUseAssetBrowser()`, fix a missing parameter bug, and sanitize a
log statement.

## Changes

- **What**: 
- DRY: Extract the repeated 3-condition guard (`isCloud &&
isUsingAssetAPI && isAssetBrowserEligible`) into
`assetService.shouldUseAssetBrowser()`, used by `widgetInputs.ts` and
`useComboWidget.ts`
- Bug fix: `createAssetBrowserWidget()` in `useComboWidget.ts` was
missing the `inputNameForBrowser` parameter, which could show wrong
assets for nodes with multiple model inputs
- Security: `createAssetWidget.ts` no longer logs the full raw asset
object on validation failure
- `WidgetSelect.vue` keeps an inline guard because it has a special
`widget.type === "asset"` fallback that must stay gated behind
`isUsingAssetAPI`

## Review Focus

- `shouldUseAssetBrowser()` correctly combines the three conditions that
were previously duplicated
- `WidgetSelect.vue` preserves exact behavioral equivalence (a widget
can have `type === "asset"` when the setting is off if a user toggles
the setting after creating asset widgets)

Fixes #8744

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8867-refactor-extract-shouldUseAssetBrowser-fix-missing-inputNameForBrowser-sanitize-logs-3076d73d3650818cabdcd76a351dac31)
by [Unito](https://www.unito.io)
2026-02-20 02:23:29 -08:00
Christian Byrne
0792d26f77 fix: prevent confirm dialog buttons from being unreachable on mobile with long text (#8746)
## Summary

Fix confirm dialog buttons becoming unreachable on mobile when text
contains long unbreakable words (e.g. content-hashed filenames with 100+
characters).

<img width="1080" height="2277" alt="image"
src="https://github.com/user-attachments/assets/2f42afc9-c8ec-42aa-89d5-802dbaf788fd"
/>


## Changes

- **What**: Added `overflow-wrap: break-word` and `flex-wrap` to both
confirm dialog systems so long words break properly and buttons wrap on
narrow screens.
- `ConfirmationDialogContent.vue`: Added `overflow-wrap: break-word` to
the existing scoped style and `flex-wrap` to button row.
  - `ConfirmBody.vue`: Added `break-words` tailwind class.
  - `ConfirmFooter.vue`: Added `flex-wrap` to button section.

## Review Focus

Minimal CSS-only fix across both dialog systems (legacy
`dialogService.confirm()` and newer `showConfirmDialog()`). No
behavioral changes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8746-fix-prevent-confirm-dialog-buttons-from-being-unreachable-on-mobile-with-long-text-3016d73d36508116bf55f0dc5cd89d0b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-02-20 02:20:02 -08:00
Christian Byrne
03f597a496 fix: open minimap settings panel above on mobile (#8589)
## Summary

On mobile viewports, the minimap settings panel now opens above the
minimap instead of to the left, preventing it from extending off-screen
on narrow viewports.

<img width="918" height="974" alt="image"
src="https://github.com/user-attachments/assets/bd42fb38-207f-437e-86f3-65cd2eccc666"
/>

<img width="1074" height="970" alt="image"
src="https://github.com/user-attachments/assets/3fdd2109-a492-4570-a8ee-e67de171126b"
/>


## Changes

- Add mobile breakpoint detection using `useBreakpoints` from VueUse
- Use `flex-col-reverse` on mobile to position panel above the minimap
- Change margin from `mr-2` (right) to `mb-2` (bottom) on mobile

## Testing

- On desktop (≥768px width): Panel opens to the left of minimap
(unchanged)
- On mobile (<768px width): Panel opens above the minimap

## Related

- Fixes [Bug: Mobile minimap settings popover opens left instead of
up](https://www.notion.so/comfy-org/Bug-Mobile-minimap-settings-popover-opens-left-instead-of-up-2fc6d73d365081549a57c9132526edca)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Minimap now adapts layout between mobile and desktop for improved
usability.
* Panel spacing and alignment adjust automatically to better fit small
screens, improving readability and control placement.
* Responsive behavior provides a more consistent experience across
device sizes, with smoother transitions between compact (mobile) and
wide (desktop) layouts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8589-fix-open-minimap-settings-panel-above-on-mobile-2fc6d73d365081ed8125c16051865c2b)
by [Unito](https://www.unito.io)
2026-02-20 02:12:19 -08:00
Christian Byrne
473713cf02 refactor: rename internal promptId/PromptId to jobId/JobId (#8730)
## Summary

Rename all internal TypeScript usage of legacy `promptId`/`PromptId`
naming to `jobId`/`JobId` across ~38 files for consistency with the
domain model.

## Changes

- **What**: Renamed internal variable names, type aliases, function
names, class getters, interface fields, and comments from
`promptId`/`PromptId` to `jobId`/`JobId`. Wire-protocol field names
(`prompt_id` in Zod schemas and `e.detail.prompt_id` accesses) are
intentionally preserved since they match the backend API contract.

## Review Focus

- All changes are pure renames with no behavioral changes
- Wire-protocol fields (`prompt_id`) are deliberately unchanged to
maintain backend compatibility
- Test fixtures updated to use consistent `job-id` naming

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8730-refactor-rename-internal-promptId-PromptId-to-jobId-JobId-3016d73d3650813ca40ce337f7c5271a)
by [Unito](https://www.unito.io)
2026-02-20 02:10:53 -08:00
Benjamin Lu
541ad387b9 fix: show stop state for active instant run button (#8917)
Switch the Run (Instant) actionbar button into a stop-state while
instant auto-queue is actively running, so users can explicitly stop
that mode from the same control.

Figma context:
https://www.figma.com/design/LVilZgHGk5RwWOkVN6yCEK/Queue-Progress-Modal?node-id=3381-6181&m=dev

## Screenshots (if applicable)



https://github.com/user-attachments/assets/a4aca6ab-eb0c-41a2-9f05-3af7ecf2bedd
2026-02-20 01:59:15 -08:00
Benjamin Lu
7feaefd39c fix: disable job asset actions when preview output is missing (#8700)
### Motivation
- Prevent context-menu actions from operating on stale or non-existent
assets when a completed job has no `previewOutput`, since `Inspect
asset`, `Add to current workflow`, and `Download` should be inactive in
that case.
- Also ensure the `Inspect asset` entry is disabled when the optional
inspect callback is not provided to avoid unexpected behavior.

### Description
- Added an optional `disabled?: boolean` field to the `MenuEntry` type
returned by `useJobMenu` and computed `hasPreviewAsset` to detect when
`taskRef.previewOutput` is present.
- Mark `inspect-asset`, `add-to-current`, and `download` entries as
`disabled` when the preview is missing (and also when the inspect
callback is missing for `inspect-asset`), and keep `delete` omitted when
no preview exists.
- Updated `JobContextMenu.vue` to pass `:disabled="entry.disabled"` to
the rendered `Button` and to short-circuit the action emit when an entry
is disabled.
- Expanded `useJobMenu` unit tests to assert enabled states when a
preview exists and disabled states when preview or inspect handler is
missing.

### Testing
- Ran `pnpm vitest src/composables/queue/useJobMenu.test.ts` and all
tests passed (36 tests).
- Ran `pnpm lint` and the linter reported no warnings or errors.
- Ran `pnpm typecheck` (`vue-tsc --noEmit`) and type checking completed
without errors.

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8700-fix-disable-job-asset-actions-when-preview-output-is-missing-2ff6d73d365081b8b72ccadf0ae43e9d)
by [Unito](https://www.unito.io)
2026-02-20 01:40:16 -08:00
Christian Byrne
73e4ae2f70 refactor: replace raw buttons with Button component in WidgetActions (#8973)
Fixes #8889

Replaces custom-styled `<button>` elements in WidgetActions.vue with the
shared Button component for better consistency with the design system.
Uses `variant="textonly"` with `size="unset"` to match the existing
dropdown menu item styling.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8973-refactor-replace-raw-buttons-with-Button-component-in-WidgetActions-30c6d73d36508194beb2f5d810dde382)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-20 01:35:24 -08:00
Christian Byrne
f5c9c72234 test: refactor widgetUtil tests to use it.for parameterization (#8971)
Fixes #8888

Refactors repetitive test cases in `widgetUtil.test.ts` to use Vitest's
`it.for` syntax for parameterized testing. Tests that follow the same
"returns default for type" pattern are consolidated while keeping unique
test cases separate.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8971-test-refactor-widgetUtil-tests-to-use-it-for-parameterization-30c6d73d365081a48e2ecf52bb7b0f98)
by [Unito](https://www.unito.io)
2026-02-20 01:31:03 -08:00
Christian Byrne
a1c54ad7aa fix: add explicit type annotations to extension callback parameters (#8966)
Fixes #8882

Adds explicit type annotations to all extension callback parameters
(`nodeCreated`, `beforeRegisterNodeDef`, `addCustomNodeDefs`) across 14
core extension files. While the types were already inferred from the
`ComfyExtension` interface, explicit annotations improve readability and
make the code self-documenting.

## Changes

- Annotate `node` parameter as `LGraphNode` in all `nodeCreated`
callbacks
- Annotate `nodeType` as `typeof LGraphNode` and `nodeData` as
`ComfyNodeDef` in all `beforeRegisterNodeDef` callbacks
- Annotate `defs` as `Record<string, ComfyNodeDef>` in
`addCustomNodeDefs`
- Add necessary type imports where missing

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8966-fix-add-explicit-type-annotations-to-extension-callback-parameters-30b6d73d36508125b074f509aa38145f)
by [Unito](https://www.unito.io)
2026-02-20 01:26:11 -08:00
pythongosssss
6902e38e6a V2 Node Search (+ hidden Node Library changes) (#8987)
## Summary

Redesigned node search with categories

## Changes

- **What**: Adds a v2 search component, leaving the existing
implementation untouched
- It also brings onboard the incomplete node library & preview changes,
disabled and behind a hidden setting
- **Breaking**: Changes the 'default' value of the node search setting
to v2, adding v1 (legacy) as an option

## Screenshots (if applicable)




https://github.com/user-attachments/assets/2ab797df-58f0-48e8-8b20-2a1809e3735f

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8987-V2-Node-Search-hidden-Node-Library-changes-30c6d73d36508160902bcb92553f147c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Yourz <crazilou@vip.qq.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>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
2026-02-20 01:10:03 -08:00
Christian Byrne
8f5cdead73 refactor: use muted-textonly variant for SectionWidgets icon buttons (#8972)
Fixes #8890

Switches the Reset All and Locate Node buttons from `variant="textonly"`
with manual text color overrides to `variant="muted-textonly"`, which
already provides the muted text color. Removes redundant
`cursor-pointer`
and `text-muted-foreground` classes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8972-refactor-use-muted-textonly-variant-for-SectionWidgets-icon-buttons-30c6d73d3650819abca7e4ca5be77f15)
by [Unito](https://www.unito.io)
2026-02-20 01:00:33 -08:00
Christian Byrne
0cfd1d8e1f refactor: remove unnecessary comments from test files (#8974)
Fixes #8705

Removes redundant code comments from test files that restate what the
code already expresses through clear naming. Focuses on comments that
label obvious sections or restate assertions, while keeping comments
that explain non-obvious behavior or workarounds.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8974-refactor-remove-unnecessary-comments-from-test-files-30c6d73d3650814a826fd78da7d0d338)
by [Unito](https://www.unito.io)
2026-02-20 00:59:44 -08:00
Christian Byrne
102149fc04 fix: restore mouse-wheel scrolling in preview-as-text outputs (#8863)
## Summary

Restore mouse-wheel scrolling for read-only preview widgets (PreviewAny
plaintext and markdown modes), broken by the focus-gated wheel capture
in #6597.

## Changes

- **What**: Remove `disabled` attribute from read-only textareas (keep
`readonly`) so they can receive focus and capture wheel events. Add
`data-capture-wheel` and `tabindex` to WidgetMarkdown display div.
- **Root cause**: `disabled` elements cannot receive focus in browsers.
The focus-gated `wheelCapturedByFocusedElement()` from #6597 always
evaluated to false for disabled textareas, forwarding all wheel events
to the canvas.

## Review Focus

- Verify that removing `disabled` while keeping `readonly` does not
allow unintended editing
- Confirm `tabindex="0"` on the markdown display div does not cause
unexpected tab-order issues
- Ensure trackpad panning over unfocused widgets (#6523) still works
correctly

Fixes COM-14812

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8863-fix-restore-mouse-wheel-scrolling-in-preview-as-text-outputs-3076d73d365081719bf5e453235bb2b5)
by [Unito](https://www.unito.io)
2026-02-20 00:55:33 -08:00
Christian Byrne
7c4486ed29 feat: automatically fit view when loading templates (#8749)
## Description

When loading a template workflow, always call `fitView()` to ensure the
template is properly framed within the viewport. This provides a better
initial viewing experience for users.

## Problem

Previously, templates with embedded viewport positions (`extra.ds`)
would load at arbitrary positions, sometimes showing a blank canvas.
Users had to navigate significantly to find the workflow content.

## Solution

Added a single condition in `loadGraphData()` to check if `openSource
=== 'template'` and force `fitView()` when true, bypassing the saved
viewport position.

This change only affects template loading - normal workflow loading
behavior remains unchanged.

## Changes

- `src/scripts/app.ts`: Added condition to always call `fitView()` when
loading templates

## Testing

- Load various templates from the template selector
- Verify the workflow is fully visible and centered on load
- Verify normal workflow loading still respects saved positions

## Related

- Fixes COM-14156
- Related to COM-10087 (Center view on workflow when loading templates -
Done)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8749-feat-automatically-fit-view-when-loading-templates-3016d73d36508167a63de2ae407de7b8)
by [Unito](https://www.unito.io)
2026-02-20 00:37:38 -08:00
Christian Byrne
116685595b feat(persistence): add draft store and tab state management (#8519)
## Summary

Adds the Pinia store for managing workflow drafts and a composable for
tracking open workflow tabs per browser tab. Uses sessionStorage for
tab-specific state to support multiple ComfyUI tabs without conflicts.

## Changes

- **What**: 
- `workflowDraftStoreV2.ts` - Pinia store wrapping the LRU cache with
save/load/remove operations
- `useWorkflowTabState.ts` - Composable for tracking active workflow
path and open tabs in sessionStorage (scoped by clientId)
- **Why**: Browser tabs need independent workflow state, but the current
system uses shared localStorage keys causing tab conflicts

## Review Focus

- Store API design in `workflowDraftStoreV2.ts`
- Session vs local storage split in `useWorkflowTabState.ts`

---
*Part 3 of 4 in the workflow persistence improvements stack*

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 23:53:02 -08:00
Christian Byrne
8744d3dd54 feat: add Reka UI ToggleGroup for BOOLEAN widget label_on/label_off display (#8680)
## Summary

Surfaces the `label_on` and `label_off` (aka `on`/`off`) props for
BOOLEAN widgets in the Vue implementation using a new Reka UI
ToggleGroup component.

**Before:** Labels extended outside node boundaries, causing overflow
issues.
**After:** Labels truncate with ellipsis and share space equally within
the widget.

[Screencast from 2026-02-13
16-27-49.webm](https://github.com/user-attachments/assets/912bab39-50a0-4d4e-8046-4b982e145bd9)


## Changes

### New ToggleGroup Component (`src/components/ui/toggle-group/`)
- `ToggleGroup.vue` - Wrapper around Reka UI `ToggleGroupRoot` with
variant support
- `ToggleGroupItem.vue` - Styled toggle items with size variants
(sm/default/lg)
- `toggleGroup.variants.ts` - CVA variants adapted to ComfyUI design
tokens
- `ToggleGroup.stories.ts` - Storybook stories (Default, Disabled,
Outline, Sizes, LongLabels)

### WidgetToggleSwitch Updates
- Conditionally renders `ToggleGroup` when `options.on` or `options.off`
are provided
- Keeps PrimeVue `ToggleSwitch` for implicit boolean states (no labels)
- Items use `flex-1 min-w-0 truncate` to share space and handle overflow

### i18n
- Added `widgets.boolean.true` and `widgets.boolean.false` translation
keys for fallback labels

## Implementation Details

Follows the established pattern for adding Reka UI components in this
codebase:
- Uses Reka UI primitives (`ToggleGroupRoot`, `ToggleGroupItem`)
- Adapts shadcn-vue structure with ComfyUI design tokens
- Reactive provide/inject with typed `InjectionKey` for variant context
- Styling matches existing `FormSelectButton` appearance


Fixes COM-12709

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-19 23:21:31 -08:00
Christian Byrne
6c205cbf4c fix: jobs stuck in initializing state when failing before execution_start (#8689)
## Summary

Fix jobs getting permanently stuck in "initializing" state when they
fail before the `execution_start` WebSocket event fires.

## Changes

- **What**: Added `reconcileInitializingPrompts(activeJobIds)` to
`executionStore` that removes orphaned initializing prompt IDs not
present in the active jobs set. Called from `queueStore.update()` after
fetching Running/Pending jobs, ensuring stale initializing states are
cleaned up on every queue poll.

## Review Focus

- The reconciliation delegates to the existing
`clearInitializationByPromptIds` to avoid duplicating Set-diffing logic.
- Only runs during `queueStore.update()` which is already a periodic
poll — no additional network calls.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8689-fix-jobs-stuck-in-initializing-state-when-failing-before-execution_start-2ff6d73d3650814dbeeeda71c8bb7d43)
by [Unito](https://www.unito.io)
2026-02-19 22:10:01 -08:00
Christian Byrne
351d43a95a feat(persistence): add LRU draft cache with quota management (#8518)
## Summary

Adds an LRU (Least Recently Used) cache layer and storage I/O utilities
that handle localStorage quota limits gracefully. When storage is full,
the oldest drafts are automatically evicted to make room for new ones.

## Changes

- **What**: 
- `draftCacheV2.ts` - In-memory LRU cache with configurable max entries
(default 32)
- `storageIO.ts` - Storage read/write with automatic quota management
and eviction
- **Why**: Users experience `QuotaExceededError` when localStorage fills
up with workflow drafts, breaking auto-save functionality

## Review Focus

- LRU eviction logic in `draftCacheV2.ts`
- Quota error handling and recovery in `storageIO.ts`

---
*Part 2 of 4 in the workflow persistence improvements stack*

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-02-19 22:04:19 -08:00
Christian Byrne
9dc6203b3d docs: document subgraph limitations with autogrow and matchtype (#8709)
## Summary

Document why autogrow and matchtype don't work inside subgraphs,
covering root cause, symptoms, workarounds, and historical context.

## Changes

- **What**: Add `src/core/graph/subgraph-dynamic-input-limitations.md`
explaining the static-vs-dynamic impedance mismatch between subgraph
slots (fixed type at creation) and matchtype/autogrow (runtime
mutations). Includes Mermaid diagrams, workarounds, and PR history. Link
added to `src/lib/litegraph/AGENTS.md` for discoverability.

## Review Focus

- Accuracy of the technical explanation — @AustinMroz authored
matchtype/autogrow, @webfiltered authored the subgraph system
- Whether the workarounds section is practical and complete
- File placement: colocated at `src/core/graph/` next to both
`subgraph/` and `widgets/dynamicWidgets.ts`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8709-docs-document-subgraph-limitations-with-autogrow-and-matchtype-3006d73d36508134a64ce8c2c49e05f1)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-02-19 21:29:17 -08:00
Christian Byrne
18875fb5e7 fix: compact bundle size report to single-line header (#8677)
## Summary
Reduces bundle size report noise by:
- Single-line header: `## 📦 Bundle: 1.2 MB gzip 🟢 -5 kB`
- Moves detailed metrics (raw, gzip, brotli, bundle counts) into
collapsible Details section
- Category glance and per-file breakdowns remain available on expand

**Before:** Multi-line summary always visible
**After:** One-line header, details collapsed

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8677-fix-compact-bundle-size-report-to-single-line-header-2ff6d73d365081a6a1b1dce4e942bc83)
by [Unito](https://www.unito.io)
2026-02-19 21:28:00 -08:00
Christian Byrne
397af47035 fix: upgrade @lobehub/i18n-cli to fix 3D API nodes i18n (#8977)
## Summary

Upgrade `@lobehub/i18n-cli` to v1.26.1 and fix corrupted locale files
that caused 3D API nodes to break in non-English locales.

## Changes

- **What**: Upgrade `@lobehub/i18n-cli` from `^1.25.1` to `^1.26.1` —
v1.26.1 fixes numeric string keys (e.g. `"0"`, `"1"` in node outputs)
being incorrectly serialized as JSON arrays instead of objects. Fix all
11 non-English locale `nodeDefs.json` files where this corruption
already existed (23-35 entries per locale).
- **Dependencies**: `@lobehub/i18n-cli` `^1.25.1` → `^1.26.1`

## Review Focus

The locale file diffs are mechanical array→object conversions. The key
change is in `pnpm-workspace.yaml` (version bump). After this fix,
running `pnpm locale` will no longer re-corrupt numeric-keyed outputs.

Affected nodes include: `Load3D`, `Preview3D`, `MeshyImageToModelNode`,
`Hunyuan3Dv2Conditioning`, `SV3D_Conditioning`,
`ConditioningStableAudio`, `GetImageSize`, and ~30 others across all
non-English locales.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8977-fix-upgrade-lobehub-i18n-cli-to-fix-3D-API-nodes-i18n-30c6d73d365081b79f98e17e13652996)
by [Unito](https://www.unito.io)
2026-02-19 21:26:38 -08:00
Jin Yi
44733f010d [refactor] Unify small modal dialog styles with showSmallLayoutDialog (#8834)
## Summary
Extract a shared `showSmallLayoutDialog` utility and move
dialog-specific logic into composables, unifying the duplicated `pt`
configurations across small modal dialogs.

## Changes
- **`showSmallLayoutDialog`**: Added to `dialogService.ts` with a single
unified `pt` config for all small modal dialogs (missing nodes, missing
models, import failed, node conflict)
- **Composables**: Extracted 4 dialog functions from `dialogService`
into dedicated composables following the `useSettingsDialog` /
`useModelSelectorDialog` pattern:
  - `useMissingNodesDialog`
  - `useMissingModelsDialog`
  - `useImportFailedNodeDialog`
  - `useNodeConflictDialog`
- Each composable uses direct imports, synchronous `show()`, `hide()`,
and a `DIALOG_KEY` constant
- Updated all call sites (`app.ts`, `useHelpCenter`, `PackEnableToggle`,
`PackInstallButton`, `useImportFailedDetection`)

## Review Focus
- Unified `pt` config removes minor style variations between dialogs —
intentional design unification

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8834-refactor-Unify-small-modal-dialog-styles-with-showSmallLayoutDialog-3056d73d365081b6963beffc0e5943bf)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-19 20:58:59 -08:00
Johnpaul Chiwetelu
fe78bc6043 chore: remove unused draftTypes.ts to fix knip (#8993)
## Summary
- Remove `src/platform/workflow/persistence/base/draftTypes.ts` which is
not imported anywhere
- Fixes `knip` reporting it as an unused file

The file was added in #8517 but nothing consumes its exports yet.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8993-chore-remove-unused-draftTypes-ts-to-fix-knip-30d6d73d36508151a9e1e936864fd311)
by [Unito](https://www.unito.io)
2026-02-19 20:57:14 -08:00
Johnpaul Chiwetelu
25696ffe03 fix: remove false 'invalid directory' errors when asset API is enabled (#8961)
## Summary
- When `Comfy.Assets.UseAssetAPI` is enabled, `getAssetModelFolders()`
only discovers folders containing assets. Empty folders (e.g.
`text_encoders`, `vae`) were falsely flagged as invalid, showing
"Invalid directory specified" on every missing model dialog.
- Removes the `directory_invalid` concept entirely — the existing
`!paths` check via `getFolderPaths()` already correctly validates all
registered directories including empty ones, making `directory_invalid`
redundant.

## Before
<img width="1841" height="954" alt="Screenshot 2026-02-18 at 21 09 55"
src="https://github.com/user-attachments/assets/09cf4f28-5175-4ff6-aa9d-916568c6d9b3"
/>


## After
<img width="1134" height="738" alt="Screenshot 2026-02-18 at 21 23 29"
src="https://github.com/user-attachments/assets/578d2fa5-3fb8-401a-beee-0fd74667f08b"
/>

## Test plan
- [ ] Enable `Comfy.Assets.UseAssetAPI` setting
- [ ] Open a template referencing models in empty folders (e.g. "Image
Editing (New)")
- [ ] Verify the missing models dialog shows download buttons instead of
"Invalid directory specified" error
- [ ] Disable `Comfy.Assets.UseAssetAPI` and verify behavior is
unchanged

Fixes #8583
2026-02-19 20:51:21 -08:00
Christian Byrne
3b5c9762a4 fix: support Firefox and Safari network error messages (#8949)
Fixes #8912

The `isNetworkError` check in `useErrorHandling.ts` only matched
Chrome/Edge's `"Failed to fetch"` message, causing Firefox and Safari
users to see raw error text instead of the user-friendly
`disconnectedFromBackend` toast.

Updated the check to use a case-insensitive regex matching all three
browser variants:
- Chrome/Edge: `Failed to fetch`
- Firefox: `NetworkError when attempting to fetch resource.`
- Safari: `Load failed`

Added parameterized tests covering all three browsers plus a negative
case ensuring non-`TypeError` errors are not misclassified.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8949-fix-support-Firefox-and-Safari-network-error-messages-30b6d73d36508185b2cbd6b5447d3795)
by [Unito](https://www.unito.io)
2026-02-19 14:22:20 -08:00
Comfy Org PR Bot
7060133ff9 1.40.8 (#8968)
Patch version increment to 1.40.8

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8968-1-40-8-30c6d73d3650817d8a59e3bd9bdeb95c)
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-19 02:34:05 -08:00
Christian Byrne
cc2c10745b fix: use getAuthHeader in createCustomer for API key auth support (#8983)
## Summary

Re-apply the fix from PR #8408 that was accidentally reverted by PR
#8508 — `createCustomer` must use `getAuthHeader()` (not
`getFirebaseAuthHeader()`) so API key authentication works.

## Changes

- **What**: Changed `createCustomer` in `firebaseAuthStore.ts` to use
`getAuthHeader()` which falls back through workspace token → Firebase
token → API key. Added regression tests covering API key auth, Firebase
auth, and no-auth paths.

## Review Focus

This is the same one-line fix from #8408. PR #8508 ("Feat/workspaces 6
billing") overwrote it during merge because it was branched before #8408
landed. The regression test should prevent this from happening again.

Fixes COM-15060

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8983-fix-use-getAuthHeader-in-createCustomer-for-API-key-auth-support-30c6d73d365081c2aab6d5defa5298d6)
by [Unito](https://www.unito.io)
2026-02-18 20:47:12 -08:00
Christian Byrne
8ab9a7b887 feat(persistence): add workspace-scoped storage keys and types (#8517)
## Summary

Adds the foundational types and key generation utilities for
workspace-scoped workflow draft persistence. This enables storing drafts
per-workspace to prevent data leakage between different ComfyUI
instances.

[Screencast from 2026-02-08
18-17-45.webm](https://github.com/user-attachments/assets/f16226e9-c1db-469d-a0b7-aa6af725db53)

## Changes

- **What**: Type definitions for draft storage (`DraftIndexV2`,
`DraftPayloadV2`, session pointers) and key generation utilities with
workspace/client scoping
- **Why**: The current persistence system stores all drafts globally,
causing cross-workspace data leakage when users work with multiple
ComfyUI instances

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 19:31:24 -08:00
Christian Byrne
2dbd7e86c3 test: mark failing Vue Nodes Image Preview browser tests as fixme (#8980)
Mark the two browser tests added in #8143 as `test.fixme` — they are
failing on main.

- `opens mask editor from image preview button`
- `shows image context menu options`

- Fixes #8143 (browser test failures)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-02-19 02:46:55 +00:00
Alexander Brown
faede75bb4 fix: show skeleton loading state in asset folder view (#8979)
## Description

When clicking a multi-output job to enter folder view,
`resolveOutputAssetItems` fetches job details asynchronously. During
this fetch, the panel showed "No generated files found" because there
was no loading state for the folder resolution—only the media list fetch
had one.

This replaces the empty state flash with skeleton cards that match the
asset grid layout, using the known output count from metadata to render
the correct number of placeholders.

Supersedes #8960.

### Changes

- **Add shadcn/vue `Skeleton` component**
(`src/components/ui/skeleton/Skeleton.vue`)
- **Use `useAsyncState`** from VueUse to track folder asset resolution,
providing `isLoading` automatically
- **Wire `folderLoading`** into `showLoadingState` and `showEmptyState`
computeds
- **Replace `ProgressSpinner`** with a skeleton grid that mirrors the
asset card layout
- **Use `metadata.outputCount`** to predict skeleton count; falls back
to 6

### Before / After

| Before | After |
|--------|-------|
| "No generated files found" flash | Skeleton cards matching grid layout
|

## Checklist

- [x] Code follows project conventions
- [x] No `any` types introduced
- [x] Lint and typecheck pass

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8979-fix-show-skeleton-loading-state-in-asset-folder-view-30c6d73d365081fa9809f616204ed234)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 18:35:36 -08:00
Alexander Brown
8099cce232 feat: bulk asset export with ZIP download (#8712)
## Summary

Adds bulk asset export with ZIP download for cloud users. When selecting
2+ assets and clicking download, the frontend now requests a server-side
ZIP export instead of triggering individual file downloads.

## Changes

### New files
- **`AssetExportProgressDialog.vue`** — HoneyToast-based progress dialog
showing per-job export status with progress percentages, error
indicators, and a manual re-download button for completed exports
- **`assetExportStore.ts`** — Pinia store that tracks export jobs,
handles `asset_export` WebSocket events for real-time progress, polls
stale exports via the task API as a fallback, and auto-triggers ZIP
download on completion

### Modified files
- **`useMediaAssetActions.ts`** — `downloadMultipleAssets` now routes to
ZIP export (via `createAssetExport`) in cloud mode when 2+ assets are
selected; single assets and OSS mode still use direct download
- **`assetService.ts`** — Added `createAssetExport()` and
`getExportDownloadUrl()` endpoints
- **`apiSchema.ts`** — Added `AssetExportWsMessage` type for the
WebSocket event
- **`api.ts`** — Wired up `asset_export` WebSocket event
- **`GraphView.vue`** — Mounted `AssetExportProgressDialog`
- **`main.json`** — Added i18n keys for export toast UI

## How it works

1. User selects multiple assets and clicks download
2. Frontend calls `POST /assets/export` with asset/job IDs
3. Backend creates a ZIP task and streams progress via `asset_export`
WebSocket events
4. `AssetExportProgressDialog` shows real-time progress
5. On completion, the ZIP is auto-downloaded via a presigned URL from
`GET /assets/exports/{name}`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8712-feat-bulk-asset-export-with-ZIP-download-3006d73d365081839ec3dd3e7b0d3b77)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
2026-02-18 16:36:59 -08:00
Christian Byrne
27d4a34435 fix: sync node.imgs for legacy context menu in Vue Nodes mode (#8143)
## Summary

Fixes missing "Copy Image", "Open Image", "Save Image", and "Open in
Mask Editor" context menu options on SaveImage nodes when Vue Nodes mode
is enabled.

## Changes

- Add `syncLegacyNodeImgs` store method to sync loaded image elements to
`node.imgs`
- Call sync on image load in ImagePreview component
- Simplify mask editor handling to call composable directly

## Technical Details

- Only runs when `vueNodesMode` is enabled (no impact on legacy mode)
- Reuses already-loaded `<img>` element from Vue (no duplicate network
requests)
- Store owns the sync logic, component just hands off the element

Supersedes #7416

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8143-fix-sync-node-imgs-for-legacy-context-menu-in-Vue-Nodes-mode-2ec6d73d365081c59d42cd1722779b61)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-02-18 16:34:45 -08:00
Terry Jia
e1e560403e feat: reuse WidgetInputNumberInput for BoundingBox numeric inputs (#8895)
## Summary
Make WidgetInputNumberInput usable without the widget system by making
the widget prop optional and adding simple min/max/step/disabled props.

BoundingBox now uses this component instead of a separate
ScrubableNumberInput

## Screenshots (if applicable)
<img width="828" height="1393" alt="image"
src="https://github.com/user-attachments/assets/68e012cf-baae-4a53-b4f8-70917cf05554"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8895-feat-add-scrub-drag-to-adjust-to-BoundingBox-numeric-inputs-3086d73d36508194b4b5e9bc823b34d1)
by [Unito](https://www.unito.io)
2026-02-18 16:32:03 -08:00
Johnpaul Chiwetelu
aff0ebad50 fix: reload template workflows when locale changes (#8963)
## Summary
- Templates were fetched once with the initial locale and cached behind
an `isLoaded` guard. Changing language updated i18n UI strings but never
re-fetched locale-specific template data (names, descriptions) from the
server.
- Extracts core template fetching into `fetchCoreTemplates()` and adds a
`watch` on `i18n.global.locale` to re-fetch when the language changes.

## Test plan
- [ ] Open the templates panel
- [ ] Change language in settings (e.g. English -> French)
- [ ] Verify template names and descriptions update without a page
refresh
- [ ] Verify initial load still works correctly

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8963-fix-reload-template-workflows-when-locale-changes-30b6d73d36508178a2f8c2c8947b5955)
by [Unito](https://www.unito.io)
2026-02-18 15:59:37 -08:00
Christian Byrne
44dc208339 fix: app mode gets stale assets history (#8918)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8918-app-mode-fix-stale-assets-history-3096d73d36508114b81df071d7289c23)
by [Unito](https://www.unito.io)
2026-02-18 15:29:00 -08:00
Christian Byrne
388c21a88d fix: lint-format CI failing on fork PRs due to missing secret (#8948)
## Summary

Fall back to `github.token` when `PR_GH_TOKEN` secret is unavailable on
fork PRs, fixing checkout failure.

## Changes

- **What**: The `ci-lint-format` workflow uses `secrets.PR_GH_TOKEN` for
checkout. Repository secrets are not available to workflows triggered by
fork PRs, causing `Input required and not supplied: token` errors (e.g.
[run
22124451916](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/22124451916)).
The fix uses `github.token` as a fallback for fork PRs — this is always
available with read-only access, which is sufficient since the workflow
already skips commit/push for forks.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8948-fix-lint-format-CI-failing-on-fork-PRs-due-to-missing-secret-30b6d73d365081dfb78cf05362a6653c)
by [Unito](https://www.unito.io)
2026-02-18 15:28:48 -08:00
Alexander Brown
b28f46d237 Regenerate images (#8959)
```



```

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8959-Regenerate-image-30b6d73d3650811e9116cb7c6c9002cb)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-18 11:28:47 -08:00
Johnpaul Chiwetelu
2900e5e52e fix: asset browser filters stick when navigating categories (#8945)
## Summary

File format and base model filters in the asset browser persisted when
navigating to categories that don't contain matching assets, showing
empty results with no way to clear the filter.

## Changes

- **What**: Apply the same scope-aware filtering pattern from the
template selector dialog. Selected filters that don't exist in the
current category become inactive (excluded from filtering) but are
preserved so they reactivate when navigating back. Uses writable
computeds in `AssetFilterBar` (matching
`WorkflowTemplateSelectorDialog`) and active filter intersection in
`useAssetBrowser` (matching `useTemplateFiltering`).

## Before



https://github.com/user-attachments/assets/5c61e844-7ea0-489c-9c44-e0864dc916bc





## After


https://github.com/user-attachments/assets/8372e174-107c-41e2-b8cf-b7ef59fe741b



## Review Focus

The pattern mirrors `selectedModelObjects`/`selectedUseCaseObjects` in
`WorkflowTemplateSelectorDialog.vue` and `activeModels`/`activeUseCases`
in `useTemplateFiltering.ts`.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8945-fix-asset-browser-filters-stick-when-navigating-categories-30b6d73d365081609ac5c3982a1a03fc)
by [Unito](https://www.unito.io)
2026-02-18 12:55:05 +01:00
Comfy Org PR Bot
07e64a7f44 1.40.7 (#8944)
Patch version increment to 1.40.7

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8944-1-40-7-30b6d73d365081679aa8cba674700980)
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-18 02:23:34 -08:00
Benjamin Lu
34e21f3267 fix(queue): address follow-up review comments from #8740 (#8880)
## Summary
Address follow-up review feedback from #8740 by consolidating
completed-banner thumbnail fields and tightening queue count
sanitization.

## Changes
- **What**:
- Consolidated completed notification thumbnail data to `thumbnailUrls`
only by removing legacy `thumbnailUrl` support from the queue banner
notification type and renderer.
- Updated queue notification banner stories to use `thumbnailUrls`
consistently.
- Simplified `sanitizeCount` to treat any non-positive or non-number
value as the fallback count (`1`).

## Review Focus
Confirm the completed notification payload shape is now consistently
`thumbnailUrls` and no consumer relies on `thumbnailUrl`.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8880-fix-queue-address-follow-up-review-comments-from-8740-3076d73d365081719a70d860691c5f05)
by [Unito](https://www.unito.io)
2026-02-17 21:26:02 -08:00
jaeone94
1349fffbce Feat/errors tab panel (#8807)
## Summary

Add a dedicated **Errors tab** to the Right Side Panel that displays
prompt-level, node validation, and runtime execution errors in a
unified, searchable, grouped view — replacing the need to rely solely on
modal dialogs for error inspection.

## Changes

- **What**:
  - **New components** (`errors/` directory):
- `TabErrors.vue` — Main error tab with search, grouping by class type,
and canvas navigation (locate node / enter subgraph).
- `ErrorNodeCard.vue` — Renders a single error card with node ID badge,
title, action buttons, and error details.
- `types.ts` — Shared type definitions (`ErrorItem`, `ErrorCardData`,
`ErrorGroup`).
- **`executionStore.ts`** — Added `PromptError` interface,
`lastPromptError` ref and `hasAnyError` computed getter. Clears
`lastPromptError` alongside existing error state on execution start and
graph clear.
- **`rightSidePanelStore.ts`** — Registered `'errors'` as a valid tab
value.
- **`app.ts`** — On prompt submission failure (`PromptExecutionError`),
stores prompt-level errors (when no node errors exist) into
`lastPromptError`. On both runtime execution error and prompt error,
deselects all nodes and opens the errors tab automatically.
- **`RightSidePanel.vue`** — Shows the `'errors'` tab (with ⚠ icon) when
errors exist and no node is selected. Routes to `TabErrors` component.
- **`TopMenuSection.vue`** — Highlights the action bar with a red border
when any error exists, using `hasAnyError`.
- **`SectionWidgets.vue`** — Detects per-node errors by matching
execution IDs to graph node IDs. Shows an error icon (⚠) and "See Error"
button that navigates to the errors tab.
- **`en/main.json`** — Added i18n keys: `errors`, `noErrors`,
`enterSubgraph`, `seeError`, `promptErrors.*`, and `errorHelp*`.
- **Testing**: 6 unit tests (`TabErrors.test.ts`) covering
prompt/node/runtime errors, search filtering, and clipboard copy.
- **Storybook**: 7 stories (`ErrorNodeCard.stories.ts`) for badge
visibility, subgraph buttons, multiple errors, runtime tracebacks, and
prompt-only errors.
- **Breaking**: None
- **Dependencies**: None — uses only existing project dependencies
(`vue-i18n`, `pinia`, `primevue`)

## Related Work

> **Note**: Upstream PR #8603 (`New bottom button and badges`)
introduced a separate `TabError.vue` (singular) that shows per-node
errors when a specific node is selected. Our `TabErrors.vue` (plural)
provides the **global error overview** — a different scope. The two tabs
coexist:
> - `'error'` (singular) → appears when a node with errors is selected →
shows only that node's errors
> - `'errors'` (plural) → appears when no node is selected and errors
exist → shows all errors grouped by class type
>
> A future consolidation of these two tabs may be desirable after design
review.

## Architecture

```
executionStore
├── lastPromptError: PromptError | null     ← NEW (prompt-level errors without node IDs)
├── lastNodeErrors: Record<string, NodeError>  (existing)
├── lastExecutionError: ExecutionError         (existing)
└── hasAnyError: ComputedRef<boolean>       ← NEW (centralized error detection)

TabErrors.vue (errors tab - global view)
├── errorGroups: ComputedRef<ErrorGroup[]>  ← normalizes all 3 error sources
├── filteredGroups                          ← search-filtered view
├── locateNode()                            ← pan canvas to node
├── enterSubgraph()                         ← navigate into subgraph
└── ErrorNodeCard.vue                       ← per-node card with copy/locate actions

types.ts
├── ErrorItem      { message, details?, isRuntimeError? }
├── ErrorCardData  { id, title, nodeId?, errors[] }
└── ErrorGroup     { title, cards[], priority }
```

## Review Focus

1. **Error normalization logic** (`TabErrors.vue` L75–150): Three
different error sources (prompt, node validation, runtime) are
normalized into a common `ErrorGroup → ErrorCardData → ErrorItem`
hierarchy. Edge cases to verify:
- Prompt errors with known vs unknown types (known types use localized
descriptions)
   - Multiple errors on the same node (grouped into one card)
   - Runtime errors with long tracebacks (capped height with scroll)

2. **Canvas navigation** (`TabErrors.vue` L210–250): The `locateNode`
and `enterSubgraph` functions navigate to potentially nested subgraphs.
The double `requestAnimationFrame` is required due to LiteGraph's
asynchronous subgraph switching — worth verifying this timing is
sufficient.

3. **Store getter consolidation**: `hasAnyError` replaces duplicated
logic in `TopMenuSection` and `RightSidePanel`. Confirm that the
reactive dependency chain works correctly (it depends on 3 separate
refs).

4. **Coexistence with upstream `TabError.vue`**: The singular `'error'`
tab (upstream, PR #8603) and our plural `'errors'` tab serve different
purposes but share similar naming. Consider whether a unified approach
is preferred.

## Test Results

```
✓ renders "no errors" state when store is empty
✓ renders prompt-level errors (Group title = error message)
✓ renders node validation errors grouped by class_type
✓ renders runtime execution errors from WebSocket
✓ filters errors based on search query
✓ calls copyToClipboard when copy button is clicked

Test Files  1 passed (1)
     Tests  6 passed (6)
```

## Screenshots (if applicable)
<img width="1238" height="1914" alt="image"
src="https://github.com/user-attachments/assets/ec39b872-cca1-4076-8795-8bc7c05dc665"
/>
<img width="669" height="1028" alt="image"
src="https://github.com/user-attachments/assets/bdcaa82a-34b0-46a5-a08f-14950c5a479b"
/>
<img width="644" height="1005" alt="image"
src="https://github.com/user-attachments/assets/ffef38c6-8f42-4c01-a0de-11709d54b638"
/>
<img width="672" height="505" alt="image"
src="https://github.com/user-attachments/assets/5cff7f57-8d79-4808-a71e-9ad05bab6e17"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8807-Feat-errors-tab-panel-3046d73d36508127981ac670a70da467)
by [Unito](https://www.unito.io)
2026-02-17 21:01:15 -08:00
Terry Jia
cde872fcf7 fix: eliminate visual shaking when adjusting crop region (#8896)
## Summary
Replace the dual-image approach (dimmed <img> + background-image in crop
box) with a single <img> and a box-shadow overlay on the crop box. The
previous approach required two independently positioned images to be
pixel-perfectly aligned, which caused visible jitter from sub-pixel
rounding differences, especially when zoomed in.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8896-fix-eliminate-visual-shaking-when-adjusting-crop-region-3086d73d365081309703d3ab0d4ad44d)
by [Unito](https://www.unito.io)
2026-02-17 16:56:46 -08:00
Terry Jia
596df0f0c6 fix: resolve ImageCrop input image through subgraph nodes (#8899)
## Summary
When ImageCrop's input comes from a subgraph node, getInputNode returns
the subgraph node itself which has no image outputs.
Use resolveSubgraphOutputLink to trace through to the actual source node
(e.g. LoadImage) inside the subgraph.

Use canvas.graph (the currently active graph/subgraph) as the primary
lookup, falling back to rootGraph. When the ImageCrop node is inside a
subgraph, rootGraph cannot find it since it only contains root-level
nodes.

## Screenshots (if applicable)
before


https://github.com/user-attachments/assets/c3995f7c-6bcd-41fe-bc41-cfd87f9be94a


after


https://github.com/user-attachments/assets/ac660f58-6e6a-46ad-a441-84c7b88d28e2

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8899-fix-resolve-ImageCrop-input-image-through-subgraph-nodes-3086d73d36508172a759d7747190591f)
by [Unito](https://www.unito.io)
2026-02-17 16:56:24 -08:00
Johnpaul Chiwetelu
d3c0e331eb fix: detect video output from data in Nodes 2.0 (#8943)
## Summary

- Fixes SaveWebM node showing "Error loading image" in Vue nodes mode
- Extracts `isAnimatedOutput`/`isVideoOutput` utility functions from
inline logic in `unsafeUpdatePreviews` so both the litegraph canvas
renderer and Vue nodes renderer can detect video output directly from
execution data
- Uses output-based detection in `imagePreviewStore.isImageOutputs` to
avoid applying image preview format conversion to video files

## Background

In Vue nodes mode, `nodeMedia` relied on `node.previewMediaType` to
determine if output is video. This property is only set via
`onDrawBackground` → `unsafeUpdatePreviews` in the litegraph canvas
path, which doesn't run in Vue nodes mode. This caused webm output to
render via `<img>` instead of `<video>`.

## Before


https://github.com/user-attachments/assets/36f8a033-0021-4351-8f82-d19e3faa80c2


## After


https://github.com/user-attachments/assets/6558d261-d70e-4968-9637-6c24532e23ac
## Test plan

- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes
- [x] `pnpm test:unit` passes (4500 tests)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8943-fix-detect-video-output-from-data-in-Vue-nodes-mode-30a6d73d365081e98e91d6d1dcc88785)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-17 16:17:23 -08:00
Dante
b47414a52f fix: prevent duplicate node search filters (#8935)
## Summary

- Add duplicate check in `addFilter` to prevent identical filter chips
(same `filterDef.id` and `value`) from being added to the node search
box

## Related Issue

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

## Changes

- `NodeSearchBoxPopover.vue`: Guard `addFilter` with `isDuplicate` check
comparing `filterDef.id` and `value`
- `NodeSearchBoxPopover.test.ts`: Add unit tests covering duplicate
prevention, distinct id, and distinct value cases

## QA

- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes
- [x] `pnpm format:check` passes
- [x] Unit tests pass (4/4)
- [x] Bug reproduced with Playwright before fix

### as-is
<img width="719" height="269" alt="스크린샷 2026-02-17 오후 5 45 48"
src="https://github.com/user-attachments/assets/403bf53a-53dd-4257-945f-322717f304b3"
/>

### to-be
<img width="765" height="291" alt="스크린샷 2026-02-17 오후 5 44 25"
src="https://github.com/user-attachments/assets/7995b15e-d071-4955-b054-5e0ca7c5c5bf"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8935-fix-prevent-duplicate-node-search-filters-30a6d73d3650816797cfcc524228f270)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-17 14:53:08 -08:00
Simula_r
631d484901 refactor: workspaces DDD (#8921)
## Summary

Refactor: workspaces related functionality into DDD structure.

Note: this is the 1st PR of 2 more refactoring.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8921-refactor-DDD-3096d73d3650812bb7f6eb955f042663)
by [Unito](https://www.unito.io)
2026-02-17 12:28:47 -08:00
Christian Byrne
e83e396c09 feat: gate node replacement loading on server feature flag (#8750)
## Summary

Gates the node replacement store's `load()` call behind the
`node_replacements` server feature flag, so the frontend only calls
`/api/node_replacements` when the backend advertises support.

## Changes

- Added `NODE_REPLACEMENTS = 'node_replacements'` to `ServerFeatureFlag`
enum
- Added `nodeReplacementsEnabled` getter to `useFeatureFlags()`
- Added `api.serverSupportsFeature('node_replacements')` guard in
`useNodeReplacementStore.load()`

## Context

Without this guard, the frontend would attempt to fetch node
replacements from backends that don't support the endpoint, causing 404
errors.

Companion backend PR: https://github.com/Comfy-Org/ComfyUI/pull/12362

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8750-feat-gate-node-replacement-loading-on-server-feature-flag-3026d73d365081ec9246d77ad88f5bdc)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Jin Yi <jin12cc@gmail.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Claude <noreply@anthropic.com>
2026-02-17 11:39:07 -08:00
Benjamin Lu
821c1e74ff fix: use gtag get for checkout attribution (#8930)
## Summary

Replace checkout attribution GA identity sourcing from
`window.__ga_identity__` with GA4 `gtag('get', ...)` calls keyed by
remote config measurement ID.

## Changes

- **What**:
  - Add typed global `gtag` get definitions and shared GA field types.
- Fetch `client_id`, `session_id`, and `session_number` via `gtag('get',
measurementId, field, callback)` with timeout-based fallback.
- Normalize numeric GA values to strings before emitting checkout
attribution metadata.
- Update checkout attribution tests to mock `gtag` retrieval and verify
requested fields + numeric normalization.
  - Add `ga_measurement_id` to remote config typings.

## Review Focus

Validate the `gtag('get', ...)` retrieval path and failure handling
(`undefined` fallback on timeout/errors) and confirm analytics field
names match GA4 expectations.

## Screenshots (if applicable)

N/A

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8930-fix-use-gtag-get-for-checkout-attribution-30a6d73d365081dcb773da945daceee6)
by [Unito](https://www.unito.io)
2026-02-17 02:43:34 -08:00
Comfy Org PR Bot
d06cc0819a 1.40.6 (#8927)
Patch version increment to 1.40.6

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8927-1-40-6-30a6d73d365081498d88d11c5f24a0ed)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2026-02-17 02:22:09 -08:00
pythongosssss
f5f5a77435 Add support for dragging in multiple workflow files at once (#8757)
## Summary

Allows users to drag in multiple files that are/have embedded workflows
and loads each of them as tabs.
Previously it would only load the first one.

## Changes

- **What**: 
- process all files from drop event
- add defered errors so you don't get errors for non-visible workflows

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8757-Add-support-for-dragging-in-multiple-workflow-files-at-once-3026d73d365081c096e9dfb18ba01253)
by [Unito](https://www.unito.io)
2026-02-16 23:45:22 -08:00
Jin Yi
efe78b799f [feat] Node replacement UI (#8604)
## Summary
Add node replacement UI to the missing nodes dialog. Users can select
and replace deprecated/missing nodes with compatible alternatives
directly from the dialog.

## Changes
- Classify missing nodes into **Replaceable** (quick fix) and **Install
Required** sections
- Add select-all checkbox + per-node checkboxes for batch replacement
- `useNodeReplacement` composable handles in-place node replacement on
the graph:
  - Simple replacement (configure+copy) for nodes without mapping
  - Input/output connection remapping for nodes with mapping
  - Widget value transfer via `old_widget_ids`
  - Dot-notation input handling for Autogrow/DynamicCombo
  - Undo/redo support via `changeTracker` (try/finally)
  - Title and properties preservation
- Footer UX: "Skip for Now" button when all nodes are replaceable (cloud
+ OSS)
- Auto-close dialog when all replaceable nodes are replaced and no
non-replaceable remain
- Settings navigation link from "Don't show again" checkbox
- 505-line unit test suite for `useNodeReplacement`

## Review Focus
- `useNodeReplacement.ts` — core graph manipulation logic
- `MissingNodesContent.vue` — checkbox selection state management
- `MissingNodesFooter.vue` — conditional button rendering (cloud vs OSS
vs all-replaceable)


[screen-capture.webm](https://github.com/user-attachments/assets/7dae891c-926c-4f26-987f-9637c4a2ca16)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8604-feat-Node-replacement-UI-2fd6d73d36508148a371dabb8f4115af)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-16 23:33:41 -08:00
Benjamin Lu
e70484d596 fix: move queue assets action into filter controls (#8926)
## Summary

Move the queue overlay "Show assets" action into the filter controls as
an icon button, so the action is available inline with other list
controls while keeping existing behavior.

## Changes

- **What**:
- Remove the full-width "Show assets" button from
`QueueOverlayExpanded`.
- Add a secondary icon button in `JobFiltersBar` with tooltip +
aria-label and emit `showAssets` on click.
- Wire `showAssets` from `JobFiltersBar` through `QueueOverlayExpanded`
to the existing handler.
- Add `JobFiltersBar` unit coverage to verify `showAssets` is emitted
when the icon button is clicked.

## Review Focus

- Verify the icon button placement in the filter row is sensible and
discoverable.
- Verify clicking the new button opens the assets panel as before.
- Verify tooltip and accessibility label copy are correct.

## Screenshots (if applicable)
Design:
https://www.figma.com/design/LVilZgHGk5RwWOkVN6yCEK/Queue-Progress-Modal?node-id=3924-38560&m=dev
<img width="349" height="52" alt="Screenshot 2026-02-16 at 4 53 34 PM"
src="https://github.com/user-attachments/assets/347772d6-5536-457a-a65f-de251e35a0e4"
/>
2026-02-16 18:19:16 -08:00
Benjamin Lu
3dba245dd3 fix: move clear queued controls into queue header (#8920)
## Summary
- Move queued-count summary and clear-queued action into the Queue Overlay header so controls remain visible while expanded content scrolls.

## What changed
- `QueueOverlayExpanded.vue`
  - Passes `queuedCount` and `clearQueued` through to the header.
  - Removes duplicated summary/action content from the lower section.
- `QueueOverlayHeader.vue`
  - Accepts new header data/actions for queued count and clear behavior.
  - Renders queued summary and clear button beside the title.
  - Adjusts layout to support persistent header actions.
- Updated header unit tests to cover queued summary rendering and clear action behavior.

## Testing
- Header unit tests were updated for the new behavior.
- No additional test execution was requested.

## Notes
- UI composition change only; queue execution semantics are unchanged.

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

<img width="356" height="59" alt="Screenshot 2026-02-16 at 3 30 44 PM" src="https://github.com/user-attachments/assets/987e42bd-9e24-4e65-9158-3f96b5338199" />
2026-02-16 18:03:52 -08:00
Benjamin Lu
2ca0c30cf7 fix: localize queue overlay running and queued summary (#8919)
## Summary
- Localize QueueProgressOverlay header counts with dedicated i18n pluralization keys for running and queued jobs.
- Replace the previous aggregate active-job wording with a translated running/queued summary.

## What changed
- Updated `QueueProgressOverlay.vue` to derive `runningJobsLabel`, `queuedJobsLabel`, and `runningQueuedSummary` via `useI18n`.
- Added `QueueProgressOverlay.test.ts` coverage for expanded-header text in both active and empty queue states.
- Added new English locale keys in `src/locales/en/main.json`:
  - `runningJobsLabel`
  - `queuedJobsLabel`
  - `runningQueuedSummary`

## Testing
- `Storybook Build Status` passed.
- `Playwright Tests` were still running at the last check; merge should wait for completion.

## Notes
- Behavioral scope is limited to queue overlay header text/rendering.

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

<img width="356" height="59" alt="Screenshot 2026-02-16 at 3 30 44 PM" src="https://github.com/user-attachments/assets/987e42bd-9e24-4e65-9158-3f96b5338199" />
2026-02-16 17:48:47 -08:00
Benjamin Lu
c8ba5f7300 fix: add pulsing active-jobs indicator on queue button (#8915)
## Summary

Add a small pulsing blue indicator dot to the top-right of the `N
active` queue button when there are active jobs.

## Changes

- **What**: Reused `StatusBadge` (`variant="dot"`) in `TopMenuSection`
as a top-right indicator on the queue toggle button, shown only when
`activeJobsCount > 0` and animated with `animate-pulse`.
- **What**: Added tests to verify the indicator appears for nonzero
active jobs and is hidden when there are no active jobs.

## Review Focus

- Dot positioning on the queue button (`-top-0.5 -right-0.5`) across top
menu layouts.
- Indicator visibility behavior tied to `activeJobsCount > 0`.

## Screenshots (if applicable)


https://github.com/user-attachments/assets/9bdb7675-3e58-485b-abdd-446a76b2dafc

won't be shown on 0 active, I was just testing locally

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8915-fix-add-pulsing-active-jobs-indicator-on-queue-button-3096d73d36508181abf5c27662e0d9ae)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-17 01:32:33 +00:00
AustinMroz
39cc8ab97a A heavy-handed fix for middlemouse pan (#8865)
Sometimes, middle mouse clicks would fail to initiate a canvas pan,
depending on the target of the initial pan. This PR adds a capturing
event handler to the transform pane that forwards the pointer event to
canvas if
- It is a middle mouse click
- The target element is not a focused text element

Resolves #6911

While testing this, I encountered infrequent cases of "some nodes
unintentionally translating continually to the left". Reproduction was
too unreliable to properly track down, but did appear unrelated to this
PR.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8865-A-heavy-handed-fix-for-middlemouse-pan-3076d73d365081ea9a4ddd5786fc647a)
by [Unito](https://www.unito.io)
2026-02-16 15:40:36 -08:00
Alexander Brown
2ee0a1337c fix: prevent XSS vulnerability in context menu labels (#8887)
Replace innerHTML with textContent when setting context menu item labels
to prevent XSS attacks via malicious filenames. This fixes a security
vulnerability where filenames like "<img src=x onerror=alert()>" could
execute arbitrary JavaScript when displayed in dropdowns.

https://claude.ai/code/session_01LALt1HEgGvpWD7hhqcp2Gu

## 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)

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8887-fix-prevent-XSS-vulnerability-in-context-menu-labels-3086d73d365081ccbe3cdb35cd7e5cb1)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-16 15:31:00 -08:00
Christian Byrne
980f280b3c fix: align in-app pricing copy with comfy.org/cloud/pricing (#8725)
## Summary

Align stale in-app pricing strings and links with the current
comfy.org/cloud/pricing page.

## Changes

- **What**: Update video estimate numbers (standard 120→380, creator
211→670, pro 600→1915), fix template URL (`video_wan2_2_14B_fun_camera`
→ `video_wan2_2_14B_i2v`), fix `templateNote` to reference Wan 2.2
Image-to-Video, align `videoEstimateExplanation` wording order with
website, remove stale "$10" from `benefit1` string.

## Review Focus

Copy-only changes across 4 files — no logic or UI changes. Source of
truth: https://www.comfy.org/cloud/pricing

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8725-fix-align-in-app-pricing-copy-with-comfy-org-cloud-pricing-3006d73d3650811faf11c248e6bf27c3)
by [Unito](https://www.unito.io)
2026-02-16 11:06:20 -08:00
Comfy Org PR Bot
4856fb0802 1.40.5 (#8905)
Patch version increment to 1.40.5

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8905-1-40-5-3096d73d365081c2aecccb7ae55def79)
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: pythongosssss <125205205+pythongosssss@users.noreply.github.com>
2026-02-16 10:06:15 -08:00
Christian Byrne
82ace36982 fix: show user-friendly message for network errors (#8748)
## Summary

Replaces cryptic "Failed to fetch" error messages with user-friendly
"Disconnected from backend" messages.

## Changes

- **What**: Detect network fetch errors in the central toast error
handler and display a helpful message with suggested action ("Check if
the server is running")

## Review Focus

The fix is intentionally minimal—only the central `toastErrorHandler` is
modified since all user-facing API errors flow through it.

Fixes COM-1839

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8748-fix-show-user-friendly-message-for-network-errors-3016d73d365081b3869bf19550c9af93)
by [Unito](https://www.unito.io)
2026-02-16 03:01:17 -08:00
Terry Jia
3d88d0a6ab fix: resolve errors when converting ImageCrop node to subgraph (#8898)
## Summary

- Pass callback directly to addWidget instead of null to eliminate
'addWidget without a callback or property assigned' warning
- Serialize object widget values as plain objects in
LGraphNode.serialize to prevent DataCloneError when structuredClone
encounters Vue reactive proxies

## Screenshots (if applicable)
before

https://github.com/user-attachments/assets/af20bd84-3e0e-4eca-b095-eaf4d5bb6884

after


https://github.com/user-attachments/assets/5a17772e-04bc-4f3e-abec-78c540e0efa3

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8898-fix-resolve-errors-when-converting-ImageCrop-node-to-subgraph-3086d73d365081b2ae34db31225683ad)
by [Unito](https://www.unito.io)
2026-02-16 05:46:46 -05:00
Comfy Org PR Bot
21cfd44a2d 1.40.4 (#8885)
Patch version increment to 1.40.4

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8885-1-40-4-3086d73d3650814d81c4fd16ab570538)
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-15 04:15:03 -08:00
Christian Byrne
d8d0dcbf71 feat: add reset-to-default for widget parameters in right side panel (#8861)
## Summary

Add per-widget and reset-all-parameters functionality to the right side
panel, allowing users to quickly revert widget values to their defaults.

## Changes

- **What**: Per-widget "Reset to default" option in the WidgetActions
overflow menu, plus a "Reset all parameters" button in each
SectionWidgets header. Defaults are derived from the InputSpec (explicit
default, then type-specific fallbacks: 0 for INT/FLOAT, false for
BOOLEAN, empty string for STRING, first option for COMBO).
- **Dependencies**: Builds on #8594 (WidgetValueStore) for reactive UI
updates after reset.

## Review Focus

- `getWidgetDefaultValue` fallback logic in `src/utils/widgetUtil.ts` —
are the type-specific defaults appropriate?
- Deep equality check (`isEqual`) for disabling the reset button when
the value already matches the default.
- Event flow: WidgetActions emits `resetToDefault` → WidgetItem forwards
→ SectionWidgets handles via `writeWidgetValue` (sets value, triggers
callback, marks canvas dirty).

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8861-feat-add-reset-to-default-for-widget-parameters-in-right-side-panel-3076d73d365081d1aa08d5b965a16cf4)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2026-02-15 00:55:04 -08:00
Christian Byrne
066a1f1f11 fix: drag-and-drop screenshot creates LoadImage node instead of showing error (#8886)
## Summary

Fix drag-and-drop of local screenshots onto the canvas failing to create
a LoadImage node.

## Changes

- **What**: Replace `_.isEmpty(workflowData)` check in `handleFile` with
a check for workflow-relevant keys (`workflow`, `prompt`, `parameters`,
`templates`). PNG screenshots often contain non-workflow `tEXt` metadata
(e.g. `Software`, `Creation Time`) which made `_.isEmpty()` return
`false`, skipping the LoadImage fallback and showing an error instead.

## Review Focus

The root cause is that `getPngMetadata` extracts all `tEXt`/`iTXt` PNG
chunks indiscriminately. Rather than filtering at the parser level
(which could break extensions relying on arbitrary metadata), the fix
checks for workflow-relevant keys before deciding whether to treat the
file as a workflow or a plain image.

Fixes #7752

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8886-fix-drag-and-drop-screenshot-creates-LoadImage-node-instead-of-showing-error-3086d73d3650817d86c5f1386aa041c2)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-14 22:25:59 -08:00
Jin Yi
2b896a722b [bugfix] Restore scroll-to-setting in SettingDialog (#8833)
## Summary
Restores the scroll-to-setting and highlight animation that was lost
during the BaseModalLayout migration in #8270. Originally implemented in
#8761.

## Changes
- **What**: Re-added scroll-into-view + pulse highlight logic and CSS
animation to `SettingDialog.vue`, ported from the deleted
`SettingDialogContent.vue`

Fixes #3437

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8833-bugfix-Restore-scroll-to-setting-in-SettingDialog-3056d73d36508161abeee047a40dc1e5)
by [Unito](https://www.unito.io)
2026-02-15 03:39:31 +00:00
Jin Yi
96b9e886ea feat: classify missing nodes by replacement availability (#8483)
## Summary
- Extend `MissingNodeType` with `isReplaceable` and `replacement` fields
- Classify missing nodes by checking
`nodeReplacementStore.getReplacementFor()` during graph load
- Wrap hardcoded node patches (T2IAdapterLoader, ConditioningAverage,
etc.) in `if (!isEnabled)` guard so they only run when node replacement
setting is disabled
- Change `useNodeReplacementStore().load()` from fire-and-forget
(`void`) to `await` so replacement data is available before missing node
detection
- Fix guard condition order in `nodeReplacementStore.load()`: check
`isEnabled` before `isLoaded`
- Align `InputMap` types with actual API response (flat
`old_id`/`set_value` fields instead of nested `assign` wrapper)

## Test plan
- [x] Load workflow with deprecated nodes (T2IAdapterLoader,
Load3DAnimation, SDV_img2vid_Conditioning)
- [x] Verify missing nodes are classified with `isReplaceable: true` and
`replacement` object
- [x] Verify hardcoded patches only run when node replacement setting is
OFF
- [x] Verify `nodeReplacementStore.load()` is not called when setting is
disabled
- [x] Unit tests pass (`nodeReplacementStore.test.ts` - 16 tests)
- [x] Typecheck passes

🤖 Generated with [Claude Code](https://claude.ai/code)
2026-02-15 11:26:46 +09:00
Yourz
58182ddda7 fix: skip loading drafts when Comfy.Workflow.Persist is disabled (#8851)
## Summary

Skip draft loading and clear stored drafts when `Comfy.Workflow.Persist`
is disabled, preventing unsaved changes from reappearing.

## Changes

- **What**: Guard draft loading in `ComfyWorkflow.load()` with
`Comfy.Workflow.Persist` setting check. Clear all localStorage drafts
when Persist is toggled from true to false.

## Review Focus

`ComfyWorkflow.load()` previously read drafts unconditionally regardless
of the Persist setting. This meant that after disabling Persist,
previously stored drafts would still be applied when opening a saved
workflow. The fix adds a guard in two places:
1. `comfyWorkflow.ts`: `load()` now checks `Comfy.Workflow.Persist`
before calling `getDraft()`
2. `useWorkflowPersistence.ts`: A `watch` on the Persist setting calls
`draftStore.reset()` when disabled

Fixes Comfy-Org/ComfyUI#12323

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8851-fix-skip-loading-drafts-when-Comfy-Workflow-Persist-is-disabled-3066d73d36508119ac2ce13564e18c01)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-02-15 10:01:54 +08:00
Terry Jia
0f0029ca29 fix: hide output images for ImageCropV2 node (#8873)
## Summary
using the new hideOutputImages flag for image crop.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8873-fix-hide-output-images-for-ImageCropV2-node-3076d73d365081079839dfc9b801606c)
by [Unito](https://www.unito.io)
2026-02-14 17:10:51 -05:00
AustinMroz
ba7f622fbd Fix primitive assets (#8879)
#8598 made primitve widgets connected to an asset have the asset type,
but the `nodeType` parameter required to actually resolve valid models
wasn't getting passed correctly.

This `nodeType`, introduced by me back in #7576, was a mistake. I'm
pulling it out now and instead passing nodeType as an option. Of note:
code changes are only required to pass the option, not to utilize it.

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/f1abfbd1-2502-4b82-841c-7ef697b3a431"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/099cd511-0101-496c-b24e-ee2c19f23384"/>|

The backport PR was made first in #8878. Fixing the bug was time
sensitive and backport would not be clean due to the `widget.value`
changes. Changes were still simpler than expected. I probably should
have made this PR first and then backported, but I misjudged the
complexity of conflict resolution.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8879-Fix-primitive-assets-3076d73d365081b89ed4e6400dbf8e74)
by [Unito](https://www.unito.io)
2026-02-14 12:49:54 -08:00
Benjamin Lu
fcb4341c98 feat(queue): introduce queue notification banners and remove completion summary flow (#8740)
## Summary
Replace the old completion-summary overlay path with queue notification
banners for queueing/completed/failed lifecycle feedback.

## Key changes
- Added `QueueNotificationBanner`, `QueueNotificationBannerHost`,
stories, and tests.
- Added `useQueueNotificationBanners` to handle:
  - immediate `queuedPending` on `promptQueueing`
  - transition to `queued` on `promptQueued` (request-id aware)
  - completed/failed notification sequencing from finished batch history
  - timed notification queueing/dismissal
- Removed completion-summary implementation:
  - `useCompletionSummary`
  - `CompletionSummaryBanner`
  - `QueueOverlayEmpty`
- Simplified `QueueProgressOverlay` to `hidden | active | expanded`
states.
- Top menu behavior:
  - restored `QueueInlineProgressSummary` as separate UI
  - ordering is inline summary first, notification banner below
- notification banner remains under the top menu section (not teleported
to floating actionbar target)
- Kept established API-event signaling pattern
(`promptQueueing`/`promptQueued`) instead of introducing a separate bus.
- Updated tests for top-menu visibility/ordering and notification
behavior across QPOV2 enabled/disabled.

## Notes
- Completion notifications now support stacked thumbnails (cap: 3).
-
https://www.figma.com/design/LVilZgHGk5RwWOkVN6yCEK/Queue-Progress-Modal?node-id=3843-20314&m=dev

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8740-feat-Queue-Notification-Toasts-3016d73d3650814c8a50d9567a40f44d)
by [Unito](https://www.unito.io)
2026-02-14 12:14:55 -08:00
AustinMroz
27da781029 Fix labels on output slots in vue mode (#8846)
Vue output slots were not respecting the `slot.label`. As a result, a
renamed subgraph output slot would still display the original name when
in vue mode.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8846-Fix-labels-on-output-slots-in-vue-mode-3066d73d3650811c986cffee03f56ac2)
by [Unito](https://www.unito.io)
2026-02-14 10:05:32 -08:00
Christian Byrne
36d59f26cd fix: SaveImage node not updating outputs during batch runs (vue-nodes) (#8862)
## Summary

Fix vue-node outputs not updating during batch runs by creating a new
object reference on merge.

## Changes

- **What**: Spread merged output object in `setOutputsByLocatorId` so
Vue detects the assignment as a change. Adds regression test asserting
reference identity changes on merge.

## Review Focus

One-line fix at `imagePreviewStore.ts:155`: `{ ...existingOutput }`
instead of `existingOutput`. This matches the spread pattern already
used in `restoreOutputs` (line 368).

The root cause: Vue skips reactivity triggers for same-reference
assignments. The merge path mutated `existingOutput` in-place then
reassigned the same object, so `nodeMedia` computed in `LGraphNode.vue`
never re-evaluated.

> Notion:
https://www.notion.so/comfy-org/3066d73d36508165873fcbb9673dece7

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8862-fix-SaveImage-node-not-updating-outputs-during-batch-runs-vue-nodes-3076d73d36508133b1faeae66dcccf01)
by [Unito](https://www.unito.io)

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2026-02-14 07:16:14 -08:00
Christian Byrne
5f7a6e7aba fix: clear draft on workflow close to prevent stale state on reopen (#8854)
## Summary

Clear the workflow draft from localStorage when any workflow tab is
closed, preventing stale cached state from being served when the
workflow is re-opened.

## Changes

- **What**: `closeWorkflow()` in `workflowStore.ts` now calls
`removeDraft()` for all workflows, not just temporary ones.
`closeWorkflow()` in `workflowService.ts` removes the draft before
switching tabs, preventing `beforeLoadNewGraph()` from re-saving it.

## Review Focus

- Draft is removed before the tab switch in
`workflowService.closeWorkflow()` to prevent `beforeLoadNewGraph()` from
re-saving it during the switch
- Crash recovery is preserved: drafts are only cleared on explicit
close, not on unload/crash
- Tab restore on restart is unaffected: drafts for intentionally-open
tabs are saved on graph change events, not on close

Fixes #8778
Fixes https://github.com/Comfy-Org/ComfyUI/issues/12323

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8854-fix-clear-draft-on-workflow-close-to-prevent-stale-state-on-reopen-3066d73d365081a2a633c9b352d0b0d1)
by [Unito](https://www.unito.io)
2026-02-14 02:50:05 -08:00
Comfy Org PR Bot
2c07bedbb1 1.40.3 (#8859)
Patch version increment to 1.40.3

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8859-1-40-3-3076d73d36508130ab36d2b00a9fb1f3)
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: Alexander Brown <drjkl@comfy.org>
2026-02-14 02:41:47 -08:00
Terry Jia
78635294ce feat: add hideOutputImages flag for nodes with custom preview (#8857)
## Summary

Prerequisite for upcoming native color correction nodes (ColorCorrect,
ColorBalance, ColorCurves) which render their own WebGL preview and need
to suppress the default output image display while keeping data in the
store for downstream nodes.

## Screenshots (if applicable)
before
<img width="980" height="1580" alt="image"
src="https://github.com/user-attachments/assets/2e08869f-1cad-4637-8174-96d034da524c"
/>

after
<img width="783" height="1276" alt="image"
src="https://github.com/user-attachments/assets/3f9b50ee-268c-48f4-9e63-89ef8d732157"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8857-feat-add-hideOutputImages-flag-for-nodes-with-custom-preview-3076d73d365081a2aa55d34280601b47)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-14 03:55:36 -05:00
Alexander Brown
2f09c6321e Regenerate images (#8866)
```
```

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8866-Regenerate-images-3076d73d365081018009ff8e79c5a418)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-14 00:43:04 -08:00
Christian Byrne
38edba7024 fix: exclude missing assets from cloud mode dropdown (COM-14333) (#8747)
## Summary

Fixes a bug where non-existent images appeared in the asset search
dropdown when loading workflows that reference images the user doesn't
have in cloud mode.

## Changes

- Add `displayItems` prop to `FormDropdown` and `FormDropdownInput` for
showing selected values that aren't in the dropdown list
- Exclude `missingValueItem` from cloud asset mode `dropdownItems` while
still displaying it in the input field via `displayItems`
- Use localized error messages in `ImagePreview` for missing images
(`g.imageDoesNotExist`, `g.unknownFile`)
- Add tests for cloud asset mode behavior in
`WidgetSelectDropdown.test.ts`

## Context

The `missingValueItem` was originally added in PR #8276 for template
workflows. This fix keeps that behavior for local mode but excludes it
from cloud asset mode dropdown. Cloud users can't access files they
don't own, so showing them as search results causes confusion.

## Testing

- Added unit tests for cloud asset mode behavior
- Verified existing tests pass
- All quality gates pass: typecheck, lint, format, tests

Fixes COM-14333



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8747-fix-exclude-missing-assets-from-cloud-mode-dropdown-COM-14333-3016d73d365081e3ab47c326d791257e)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-13 14:30:55 -08:00
Comfy Org PR Bot
f851c3189f 1.40.2 (#8842)
Patch version increment to 1.40.2

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8842-1-40-2-3066d73d365081638b92c580d8d8579d)
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: Alexander Brown <drjkl@comfy.org>
Co-authored-by: pythongosssss <125205205+pythongosssss@users.noreply.github.com>
2026-02-13 08:12:53 -08:00
pythongosssss
71d26eb4d9 Fix storybook build (#8849)
## Summary

The strictExecutionOrder looks to have accidently been removed.
This breaks the storybook build:
https://github.com/vitejs/rolldown-vite/issues/182#issuecomment-3035289157

https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/21979369339/job/63498007630?pr=8842
2026-02-13 15:57:58 +01:00
Terry Jia
d04dd32235 fix: make ResizeHandle interface properties readonly (#8847)
## Summary

improve for https://github.com/Comfy-Org/ComfyUI_frontend/pull/8845

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8847-fix-make-ResizeHandle-interface-properties-readonly-3066d73d3650814da6cee7bb955bbeb7)
by [Unito](https://www.unito.io)
2026-02-12 23:23:44 -05:00
Terry Jia
c52f48af45 feat(vueNodes): support resizing from all four corners (#8845)
## Summary
(Not sure we need this, and I don't know the reason why we only have one
cornor support previously, but it is requested by QA reporting in
Notion)

Add resize handles at all four corners (NW, NE, SW, SE) of Vue nodes,
matching litegraph's multi-corner resize behavior.

Vue nodes previously only supported resizing from the bottom-right (SE)
corner. This adds handles at all four corners with direction-aware delta
math, snap-to-grid support, and minimum size enforcement that keeps the
opposite corner anchored.
The content-driven minimum height is measured from the DOM at resize
start to prevent the node from sliding when dragged past its minimum
size.

## Screenshots (if applicable)


https://github.com/user-attachments/assets/c9d30d93-8243-4c44-a417-5ca6e9978b3b
2026-02-12 22:28:21 -05:00
AustinMroz
01cf3244b8 Fix hover state on collapsed bottom button (#8837)
| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/0de0790a-e9f0-432c-9501-eae633e341b6"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/588221b0-b34a-4d35-8393-1bc1ec36c285"
/>|

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8837-Fix-hover-state-on-collapsed-bottom-button-3056d73d36508139919fc1d750c6b135)
by [Unito](https://www.unito.io)
2026-02-12 18:35:37 -08:00
Terry Jia
0f33444eef fix: undo breaking Vue node image preview reactivity (#8839)
## Summary
restoreOutputs was assigning the same object reference to both
app.nodeOutputs and the Pinia reactive ref. This caused subsequent
writes via setOutputsByLocatorId to mutate the reactive proxy's target
through the raw reference before the proxy write, making Vue detect no
change and skip reactivity updates permanently.

Shallow-copy the outputs when assigning to the reactive ref so the proxy
target remains a separate object from app.nodeOutputs.

## Screenshots
before


https://github.com/user-attachments/assets/98f2b17c-87b9-41e7-9caa-238e36c3c032


after


https://github.com/user-attachments/assets/cb6e1d25-bd2e-41ed-a536-7b8250f858ec

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8839-fix-undo-breaking-Vue-node-image-preview-reactivity-3056d73d365081d2a1c7d4d9553f30e0)
by [Unito](https://www.unito.io)
2026-02-12 15:37:02 -05:00
pythongosssss
44ce9379eb Defer vue node layout calculations on hidden browser tabs (#8805)
## Summary

If you load the window in Nodes 2.0, then switch tabs while it is still
loading, the position of the nodes is calculated incorrectly due to
useElementBounding returning left=0, top=0 for the canvas element in a
hidden tab, causing clientPosToCanvasPos to miscalculate node positions
from the ResizeObserver measurements

## Changes

- **What**: 
- Store observed elements while document is in hidden state
- Re-observe when tab becomes visible

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8805-Defer-vue-node-layout-calculations-on-hidden-browser-tabs-3046d73d365081019ae6c403c0ac6d1a)
by [Unito](https://www.unito.io)
2026-02-12 11:48:20 -08:00
pythongosssss
138fa6a2ce Resolve issues with undo with Nodes 2.0 to fix link dragging/rendering (#8808)
## Summary

Resolves the following issue:
1. Enable Nodes 2.0
2. Load default workflow
3. Move any node e.g. VAE decode
4. Undo

All links go invisible, input/output slots no longer function

## Changes

- **What**
- Fixes slot layouts being deleted during undo/redo in
`handleDeleteNode`, which prevented link dragging from nodes after undo.
Vue patches (not remounts) components with the same key, so `onMounted`
never fires to re-register them - these were already being cleared up on
unmounted
- Fixes links disappearing after undo by clearing `pendingSlotSync` when
slot layouts already exist (undo/redo preserved them), rather than
waiting for Vue mounts that do not happen

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8808-Resolve-issues-with-undo-with-Nodes-2-0-to-fix-link-dragging-rendering-3046d73d3650818bbb0adf0104a5792d)
by [Unito](https://www.unito.io)
2026-02-12 11:46:49 -08:00
Comfy Org PR Bot
ce9d0ca670 1.40.1 (#8815)
Patch version increment to 1.40.1

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8815-1-40-1-3056d73d365081ed8adcf690bd07e1cd)
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-12 02:48:38 -08:00
Terry Jia
6cf0357b3e fix(vueNodes): sync node size changes from extensions to Vue components (#7993)
## Summary
When extensions like KJNodes call node.setSize(), the Vue component now
properly updates its CSS variables to reflect the new size.

## Changes:
- LGraphNode pos/size setters now always sync to layoutStore with Canvas
source
- LGraphNode.vue listens to layoutStore changes and updates CSS
variables
- Fixed height calculation to account for NODE_TITLE_HEIGHT difference
- Removed _syncToLayoutStore flag (simplified - layoutStore ignores
non-existent nodes)
- Use setPos() helper method instead of direct pos[0]/pos[1] assignment

## Screenshots (if applicable)
before

https://github.com/user-attachments/assets/236a173a-e41d-485b-8c63-5c28ef1c69bf


after

https://github.com/user-attachments/assets/5fc3f7e4-35c7-40e1-81ac-38a35ee0ac1b

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7993-fix-vueNodes-sync-node-size-changes-from-extensions-to-Vue-components-2e76d73d3650815799c5f2d9d8c7dcbf)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-02-12 05:38:18 -05:00
Christian Byrne
c0c81dba49 fix: rename "Manage Extensions" label to "Extensions" (#8681)
## Summary

Rename the manager button label from "Manage Extensions" to "Extensions"
in both the `menu` and `commands` i18n sections.

## Changes

Updated `manageExtensions` value in `src/locales/en/main.json` (both
`menu` and `commands` sections).

- Fixes follow-up from #8644

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8681-fix-rename-Manage-Extensions-label-to-Extensions-2ff6d73d365081c9aef5c94f745299f6)
by [Unito](https://www.unito.io)
2026-02-12 18:36:07 +09:00
Jin Yi
553ea63357 [refactor] Migrate SettingDialog to BaseModalLayout design system (#8270) 2026-02-12 16:27:11 +09:00
Alexander Brown
995ebc4ba4 fix: default onboardingSurveyEnabled flag to false (#8829)
Fixes race condition where the onboarding survey could appear
intermittently before the actual feature flag value is fetched from the
server.

https://claude.ai/code/session_01EpCtunck6b89gpUFfWGKmZ

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8829-fix-default-onboardingSurveyEnabled-flag-to-false-3056d73d3650819b9f4cd0530efe8f39)
by [Unito](https://www.unito.io)

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-11 23:13:12 -08:00
AustinMroz
d282353370 Add z-index to popover component (#8823)
This fixes the inability to see the value control popover in the
properties panel.
<img width="574" height="760" alt="image"
src="https://github.com/user-attachments/assets/c2cfa00f-f9b1-4c86-abda-830fd780d3f8"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8823-Add-z-index-to-popover-component-3056d73d365081e6b2dbe15517e4c4e0)
by [Unito](https://www.unito.io)
2026-02-11 21:33:05 -08:00
Simula_r
85ae0a57c3 feat: invite member upsell for single-seat plans (#8801)
## Summary

- Show an upsell dialog when single-seat plan users try to invite
members, with a banner on the members panel directing them to upgrade.
- Misc fixes for member max seat display

## Changes

- **What**: `InviteMemberUpsellDialogContent.vue`,
`MembersPanelContent.vue`, `WorkspacePanelContent.vue`

## Screenshots

<img width="2730" height="1907" alt="image"
src="https://github.com/user-attachments/assets/e39a23be-8533-4ebb-a4ae-2797fc382bc2"
/>
<img width="2730" height="1907" alt="image"
src="https://github.com/user-attachments/assets/bec55867-1088-4d3a-b308-5d5cce64c8ae"
/>



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8801-feat-invite-member-upsell-for-single-seat-plans-3046d73d365081349b09fe1d4dc572e8)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
2026-02-11 21:15:59 -08:00
Terry Jia
0d64d503ec fix(vueNodes): propagate height to DOM widget children (#8821)
## Summary
DOM widgets using CSS background-image (e.g. KJNodes FastPreview)
collapsed to 0px height in vueNodes mode because background-image
doesn't contribute to intrinsic element size. Make WidgetDOM a flex
column container so mounted extension elements fill the available grid
row space.

## Screenshots (if applicable)
before 



https://github.com/user-attachments/assets/85de0295-1f7c-4142-ac15-1e813c823e3f


after


https://github.com/user-attachments/assets/824ab662-14cb-412d-93dd-97c0f549f992

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8821-fix-vueNodes-propagate-height-to-DOM-widget-children-3056d73d36508134926dc67336fd0d70)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-11 23:05:54 -05:00
Alexander Brown
30ef6f2b8c Fix: Disabling textarea when linked. (#8818)
## Summary

Misaligned option setting when building the SimplifiedWidget.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8818-Fix-Disabling-textarea-when-linked-3056d73d365081f581a9f1322aaf60bd)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-02-11 18:45:52 -08:00
Alexander Brown
6012341fd1 Fix: Widgets Column sizing and ProgressText Widget (#8817)
## Summary

Fixes an issue where the label/control columns for widgets were
dependent on the contents of the progress text display widget.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8817-Fix-Widgets-Column-sizing-and-ProgressText-Widget-3056d73d36508141a714fe342c386eef)
by [Unito](https://www.unito.io)
2026-02-11 18:45:35 -08:00
Brian Jemilo II
a80f6d7922 Batch Drag & Drop Images (#8282)
## Summary

<!-- One sentence describing what changed and why. -->
Added feature to drag and drop multiple images into the UI and connect
them with a Batch Images node with tests to add convenience for users.
Only works with a group of images, mixing files not supported.

## Review Focus
<!-- Critical design decisions or edge cases that need attention -->
I've updated our usage of Litegraph.createNode, honestly, that method is
pretty bad, onNodeCreated option method doesn't even return the node
created. I think I will probably go check out their repo to do a PR over
there. Anyways, I made a createNode method to avoid race conditions when
creating nodes for the paste actions. Will allow us to better
programmatically create nodes that do not have workflows that also need
to be connected to other nodes.

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

https://www.notion.so/comfy-org/Implement-Multi-image-drag-and-drop-to-canvas-2eb6d73d36508195ad8addfc4367db10

## Screenshots (if applicable)

https://github.com/user-attachments/assets/d4155807-56e2-4e39-8ab1-16eda90f6a53

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8282-Batch-Drag-Drop-Images-2f16d73d365081c1ab31ce9da47a7be5)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Austin Mroz <austin@comfy.org>
2026-02-11 17:39:41 -08:00
Johnpaul Chiwetelu
0f5aca6726 fix: set audio widget value after file upload (#8814)
## Summary

Set `audioWidget.value` after uploading an audio file so the combo
dropdown reflects the newly uploaded file.

## Changes

- **What**: Added `audioWidget.value = path` in the `uploadFile`
function before the callback call, matching the pattern used by the
image upload widget.

## Review Focus

One-line fix. The image upload widget already does this correctly in
`useImageUploadWidget.ts:86`. Without this line, the file uploads but
the dropdown doesn't update to show the selection.

Fixes #8800

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8814-fix-set-audio-widget-value-after-file-upload-3056d73d365081a0af90d4e096eb4975)
by [Unito](https://www.unito.io)
2026-02-11 17:00:28 -08:00
Johnpaul Chiwetelu
4fc1d2ef5b feat: No Explicit Any (#8601)
## Summary
- Add `typescript/no-explicit-any` rule to `.oxlintrc.json` to enforce
no explicit `any` types
- Fix all 40 instances of explicit `any` throughout the codebase
- Improve type safety with proper TypeScript types

## Changes Made

### Configuration
- Added `typescript/no-explicit-any` rule to `.oxlintrc.json`

### Type Fixes
- Replaced `any` with `unknown` for truly unknown types
- Updated generic type parameters to use `unknown` defaults instead of
`any`
- Fixed method `this` parameters to avoid variance issues
- Updated component props to match new generic types
- Fixed test mocks to use proper type assertions

### Key Files Modified
- `src/types/treeExplorerTypes.ts`: Updated TreeExplorerNode interface
generics
- `src/platform/settings/types.ts`: Fixed SettingParams generic default
- `src/lib/litegraph/src/LGraph.ts`: Fixed ParamsArray type constraint
- `src/extensions/core/electronAdapter.ts`: Fixed onChange callbacks
- `src/views/GraphView.vue`: Added proper type imports
- Multiple test files: Fixed type assertions and mocks

## Test Plan
- [x] All lint checks pass (`pnpm lint`)
- [x] TypeScript compilation succeeds (`pnpm typecheck`)
- [x] Pre-commit hooks pass
- [x] No regression in functionality

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8601-feat-add-typescript-no-explicit-any-rule-and-fix-all-instances-2fd6d73d365081fd9beef75d5a6daf5b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-12 00:13:48 +01:00
Benjamin Lu
92b7437d86 fix: scope manager button red dot to conflicts (#8810)
## Summary

Scope the top-menu Manager button red dot to manager conflict state
only, so release-update notifications do not appear on the Manager
button.

## Changes

- **What**:
- In `TopMenuSection`, remove release-store coupling and use only
`useConflictAcknowledgment().shouldShowRedDot` for the Manager button
indicator.
- Add a regression test in `TopMenuSection.test.ts` that keeps the
release red dot true while asserting the Manager button dot only appears
when the conflict red dot is true.

## Review Focus

- Confirm Manager button notification semantics are conflict-specific
and no longer mirror release notifications.
- Confirm the new test fails if release-store coupling is reintroduced.

## Screenshots (if applicable)

N/A

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8810-fix-scope-manager-button-red-dot-to-conflicts-3046d73d3650817887b9ca9c33919f48)
by [Unito](https://www.unito.io)
2026-02-11 15:00:25 -08:00
Simula_r
dd1fefe843 fix: credit display and top up and other UI display if personal membe… (#8784)
## Summary

Consolidate scattered role checks for credits, top-up, and subscribe
buttons into centralized workspace permissions (canTopUp,
canManageSubscription), ensuring "Add Credits" requires an active
subscription, subscribe buttons only appear when needed, and team
members see appropriately restricted billing UI.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8784-fix-credit-display-and-top-up-and-other-UI-display-if-personal-membe-3036d73d3650810fbc2de084f738943c)
by [Unito](https://www.unito.io)
2026-02-11 14:26:35 -08:00
Johnpaul Chiwetelu
adcb663b3e fix: link dragging offset on external monitors in Vue nodes mode (#8809)
## Summary

Fix link dragging offset when using Vue nodes mode on external monitors
with different DPI than the primary display.

## Changes

- **What**: Derive overlay canvas scale from actual canvas dimensions
(`canvas.width / canvas.clientWidth`) instead of
`window.devicePixelRatio`, fixing DPR mismatch. Map `LinkDirection.NONE`
to `'none'` in `convertDirection()` instead of falling through to
`'right'`.

## Before


https://github.com/user-attachments/assets/f5d04617-369f-4649-af60-11d31e27a75c



## After


https://github.com/user-attachments/assets/76434d2b-d485-43de-94f6-202a91f73edf


## Review Focus

The overlay canvas copies dimensions from the main canvas (which
includes DPR scaling from `resizeCanvas`). When the page loads on a
monitor whose DPR differs from what `resizeCanvas` used,
`window.devicePixelRatio` no longer matches the canvas's internal-to-CSS
ratio, causing all drawn link positions to be offset. The fix derives
scale directly from the canvas itself.

`LinkDirection.NONE = 0` is falsy, so it was caught by the `default`
case in `convertDirection()`, adding an unwanted directional curve to
moved input links.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8809-fix-link-dragging-offset-on-external-monitors-in-Vue-nodes-mode-3046d73d36508143b600f23f5fe07044)
by [Unito](https://www.unito.io)
2026-02-11 22:40:17 +01:00
AustinMroz
28b171168a New bottom button and badges (#8603)
- "Enter Subgraph" "Show advanced inputs" and a new "show node Errors"
button now use a combined button design at the bottom of the node.
- A new "Errors" tab is added to the right side panel
- After a failed queue, the label of an invalid widget is now red.
- Badges other than price are now displayed on the bottom of the node.
- Price badge will now truncate from the first space, prioritizing the
sizing of the node title
- An indicator for the node resize handle is now displayed while mousing
over the node.

<img width="669" height="233" alt="image"
src="https://github.com/user-attachments/assets/53b3b59c-830b-474d-8f20-07f557124af7"
/>


![resize](https://github.com/user-attachments/assets/e2473b5b-fe4d-4f1e-b1c3-57c23d2a0349)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-10 23:29:45 -08:00
Alexander Brown
69062c6da1 deps: Update vite (#8509)
## Summary

Update from beta.8 to beta.12

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8509-deps-Update-vite-2f96d73d3650814c96dbe14cdfb02151)
by [Unito](https://www.unito.io)
2026-02-10 22:42:26 -08:00
Alexander Brown
a7c2115166 feat: add WidgetValueStore for centralized widget value management (#8594)
## Summary

Implements Phase 1 of the **Vue-owns-truth** pattern for widget values.
Widget values are now canonical in a Pinia store; `widget.value`
delegates to the store while preserving full backward compatibility.

## Changes

- **New store**: `src/stores/widgetValueStore.ts` - centralized widget
value storage with `get/set/remove/removeNode` API
- **BaseWidget integration**: `widget.value` getter/setter now delegates
to store when widget is associated with a node
- **LGraphNode wiring**: `addCustomWidget()` automatically calls
`widget.setNodeId(this.id)` to wire widgets to their nodes
- **Test fixes**: Added Pinia setup to test files that use widgets

## Why

This foundation enables:
- Vue components to reactively bind to widget values via `computed(() =>
store.get(...))`
- Future Yjs/CRDT backing for real-time collaboration
- Cleaner separation between Vue state and LiteGraph rendering

## Backward Compatibility

| Extension Pattern | Status |
|-------------------|--------|
| `widget.value = x` |  Works unchanged |
| `node.widgets[i].value` |  Works unchanged |
| `widget.callback` |  Still fires |
| `node.onWidgetChanged` |  Still fires |

## Testing

-  4252 unit tests pass
-  Build succeeds

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8594-feat-add-WidgetValueStore-for-centralized-widget-value-management-2fc6d73d36508160886fcb9f3ebd941e)
by [Unito](https://www.unito.io)

---------

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-02-10 19:37:17 -08:00
Csongor Czezar
d044bed9b2 feat: use object info display_name as fallback before node name (#7622)
## Description
Implements fallback to object info display_name before using internal
node name when display_name is unavailable.

## Related Issue
Related to backend PR: comfyanonymous/ComfyUI#11340

## Changes
- Modified `getNodeDefs()` to use object info `display_name` before
falling back to `name`
- Added unit tests for display name fallback behavior

## Testing
- All existing tests pass
- Added 4 new unit tests covering various display_name scenarios

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7622-W-I-P-feat-use-object-info-display_name-as-fallback-before-node-name-2cd6d73d365081deb22fe5ed00e6dc2e)
by [Unito](https://www.unito.io)
2026-02-10 22:18:48 -05:00
Comfy Org PR Bot
d873c8048f 1.40.0 (#8797)
Minor version increment to 1.40.0

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8797-1-40-0-3046d73d36508109b4b7ed3f6ca4530e)
by [Unito](https://www.unito.io)

---------

Co-authored-by: AustinMroz <4284322+AustinMroz@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-10 19:06:48 -08:00
Comfy Org PR Bot
475d7035f7 1.39.12 (#8790)
Patch version increment to 1.39.12

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8790-1-39-12-3046d73d3650812faaf5dfaf71f6a02a)
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-10 18:52:06 -08:00
guill
eb6bf91e20 fix(download): Use content-disposition filename (#8785)
When we download an output, we now check if there's a filename defined
in the content-disposition and use that if there is.

## Summary
This has been primarily an issue on Comfy Cloud where assets are
content-addressed. Before now,
the downloaded files have retained the hash as the filename. With this
change, downloaded files
will use the user-supplied filename instead.

## Changes

- **What**: Use content-disposition filename when downloading assets

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8785-fix-download-Use-content-disposition-filename-3046d73d365081ec952ef3c1930e773d)
by [Unito](https://www.unito.io)
2026-02-10 18:50:42 -08:00
Csongor Czezar
422227d2fc fix: viewport overflow in manager (#7775)
### **Summary**
Fixes viewport overflow in version selector dropdown when Manager dialog
is positioned near edges

**Changes**
Removed popover arrow for visual consistency across all positions
Implemented dialog boundary detection to constrain popover within
Manager viewport

**Testing**
All existing unit tests pass (17/17)
Visually tested across different screen positions


![after-fix-viewport-all-positions](https://github.com/user-attachments/assets/287952f1-eda3-4388-9d6a-8f4316acea7f)

![before-fix-viewport-low](https://github.com/user-attachments/assets/b88dc61d-896b-48af-870f-2b5d52a11a98)

![before-fix-viewport-high](https://github.com/user-attachments/assets/7a39c845-0593-480e-843e-d5da30b48661)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7775-fix-viewport-overflow-in-manager-2d76d73d365081a88c1df2a103c5925e)
by [Unito](https://www.unito.io)
2026-02-10 21:29:44 -05:00
Terry Jia
10e9bc2f8d fix: extract WidgetCallbackOptions interface and add curly braces (#8791)
## Summary

improve for https://github.com/Comfy-Org/ComfyUI_frontend/pull/8774

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8791-fix-extract-WidgetCallbackOptions-interface-and-add-curly-braces-3046d73d365081c49e37c0a2596f1958)
by [Unito](https://www.unito.io)
2026-02-10 17:47:37 -08:00
Terry Jia
f7b835e6a5 fix: disable control after generate during partial execution (#8774)
## Summary
Passes an isPartialExecution flag through widget
beforeQueued/afterQueued callbacks so control-after-generate widgets
skip value modifications (randomize, increment, decrement) when the user
queues selected output nodes via partial execution.

requested by @christian-byrne in notion

## Screenshots (if applicable)
before


https://github.com/user-attachments/assets/3e723087-8849-457b-9f95-b8b5fceab0ed


after


https://github.com/user-attachments/assets/d9816667-51e0-4538-a012-9c84d0944019

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8774-fix-disable-control-after-generate-during-partial-execution-3036d73d365081688ca3d6b0506d69ca)
by [Unito](https://www.unito.io)
2026-02-10 20:13:03 -05:00
Johnpaul Chiwetelu
7f30d6b6a5 feat: add visual indicator for list output slots (#8766)
## Summary

Add rounded square dot shape and "(Iterative)" tooltip for list-type
output slots in Vue nodes, matching litegraph's visual indicator.

## Changes

- **What**: `SlotConnectionDot.vue` renders `rounded-[1px]` instead of
`rounded-full` when slot shape is `RenderShape.GRID`. `OutputSlot.vue`
appends "(Iterative)" to the tooltip for these slots.

<img width="807" height="542" alt="Screenshot 2026-02-10 at 03 38 42"
src="https://github.com/user-attachments/assets/137b60c5-ac3b-457f-a52d-58f5f28a59ea"
/>


## Review Focus
- i18n key added for the iterative suffix

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8766-feat-add-visual-indicator-for-list-output-slots-3036d73d3650813aad85ce094d29c42b)
by [Unito](https://www.unito.io)
2026-02-11 01:49:58 +01:00
Benjamin Lu
da56c9e554 feat: integrate Impact telemetry with checkout attribution for subscriptions (#8688)
Implement Impact telemetry and checkout attribution through cloud
subscription checkout flows.

This PR adds Impact.com tracking support and carries attribution context
from landing-page visits into subscription checkout requests so
conversion attribution can be validated end-to-end.

- Register a new `ImpactTelemetryProvider` during cloud telemetry
initialization.
- Initialize the Impact queue/runtime (`ire`) and load the Universal
Tracking Tag script once.
- Invoke `ire('identify', ...)` on page views with dynamic `customerId`
and SHA-1 `customerEmail` (or empty strings when unknown).
- Expand checkout attribution capture to include `im_ref`, UTM fields,
and Google click IDs, with local persistence across navigation.
- Attempt `ire('generateClickId')` with a timeout and fall back to
URL/local attribution when unavailable.
- Include attribution payloads in checkout creation requests for both:
  - `/customers/cloud-subscription-checkout`
  - `/customers/cloud-subscription-checkout/{tier}`
- Extend begin-checkout telemetry metadata typing to include attribution
fields.
- Add focused unit coverage for provider behavior, attribution
persistence/fallback logic, and checkout request payloads.

Tradeoffs / constraints:
- Attribution collection is treated as best-effort in tiered checkout
flow to avoid blocking purchases.
- Backend checkout handlers must accept and process the additional JSON
attribution fields.

## Screenshots

<img width="908" height="208" alt="image"
src="https://github.com/user-attachments/assets/03c16d60-ffda-40c9-9bd6-8914d841be50"/>
<img width="1144" height="460" alt="image"
src="https://github.com/user-attachments/assets/74b97fde-ce0a-43e6-838e-9a4aba484488"/>
<img width="1432" height="320" alt="image"
src="https://github.com/user-attachments/assets/30c22a9f-7bd8-409f-b0ef-e4d02343780a"/>
<img width="341" height="135" alt="image"
src="https://github.com/user-attachments/assets/f6d918ae-5f80-45e0-855a-601abea61dec"/>
2026-02-10 16:40:51 -08:00
Alexander Brown
79063edf54 Remove comfy logo splash screen. (#8786)
## Summary

```



```

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8786-Remove-comfy-logo-splash-screen-3046d73d3650816d92c3ff04afeb8cf6)
by [Unito](https://www.unito.io)
2026-02-10 16:32:18 -08:00
Johnpaul Chiwetelu
d4c40f5255 fix: right-click context menu disabled when selection toolbox is off (#8781)
## Summary

- Move `NodeContextMenu` from `SelectionToolbox.vue` to
`GraphCanvas.vue` so the right-click context menu renders independently
of the `Comfy.Canvas.SelectionToolbox` setting

- Fixes #8417

## Test plan

- [x] Disable selection toolbox in settings, right-click a node —
context menu appears
- [x] Enable selection toolbox, right-click a node — context menu still
appears
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8781-fix-right-click-context-menu-disabled-when-selection-toolbox-is-off-3036d73d36508168a9add58e060b7e93)
by [Unito](https://www.unito.io)
2026-02-11 00:35:52 +01:00
Johnpaul Chiwetelu
1e1d5c8308 fix: stop suppressing link rendering during node resize (#8780)
## Summary

Stop link flickering when resizing nodes by removing the
`pendingSlotSync` flag assertion from `scheduleSlotLayoutSync`.

## Changes

- **What**: Remove `layoutStore.setPendingSlotSync(true)` from
`scheduleSlotLayoutSync()` in `useSlotElementTracking.ts`. This call was
introduced in #8367 for graph reconfiguration but was also triggered on
every node resize, causing all links to disappear for one frame per
resize tick. The reconfigure path in `app.ts`
(`addAfterConfigureHandler`) still sets the flag explicitly, so
undo/redo link suppression is unaffected.

## Review Focus

The `pendingSlotSync` flag is still managed correctly for graph
reconfiguration: `app.ts:748` sets it before configure, and the
`finally` block flushes it synchronously. The
`flushScheduledSlotLayoutSync` early-return (pendingNodes empty but
graph has nodes) continues to handle late-mounting Vue components during
reconfigure.

## Before

https://github.com/user-attachments/assets/28cfe4d8-f3f0-46f1-a717-5cb81a28dd75



## After




https://github.com/user-attachments/assets/9445fd00-91f8-4d1e-90ac-86d138d29842

Fixes #8696

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8780-fix-stop-suppressing-link-rendering-during-node-resize-3036d73d365081029820ccfd57425a07)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-11 00:03:40 +01:00
Johnpaul Chiwetelu
e411a104f4 feat: scroll to specific setting when opening settings dialog (#8761)
## Summary

- Adds `settingId` parameter to `showSettingsDialog` that auto-navigates
to the correct category tab, scrolls to the setting, and briefly
highlights it with a CSS pulse animation
- Adds `data-setting-id` attributes to setting items for stable DOM
targeting
- Adds "Don't show this again" checkbox with "Re-enable in Settings"
deep-link to the missing nodes dialog
- Adds "Re-enable in Settings" deep-link to missing models and blueprint
overwrite "Don't show this again" checkboxes

- Fixes #3437

## Test plan

- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes
- [x] Unit tests pass (59/59 including 5 new tests for `useSettingUI`)



https://github.com/user-attachments/assets/a9e80aea-7b69-4686-b030-55a2e0570ff0



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8761-feat-scroll-to-specific-setting-when-opening-settings-dialog-3036d73d365081d18d9afe9f9ed41ebc)
by [Unito](https://www.unito.io)
2026-02-10 23:00:46 +01:00
Terry Jia
19a724710c fix: address review nits in load3d (#8779)
## Summary
- Refactor getModelUrl to use const instead of let
- add missing language key

improve for https://github.com/Comfy-Org/ComfyUI_frontend/pull/8765

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8779-fix-address-review-nits-in-load3d-3036d73d36508183af11c5e9bc545650)
by [Unito](https://www.unito.io)
2026-02-10 16:09:54 -05:00
Terry Jia
9ecbb3af27 Feat/3d dropdown (#8765)
## Summary
Add mesh_upload and upload_subfolder to combo input schema so
WidgetSelect detects mesh uploads generically instead of hardcoding node
type checks. Inject these flags in load3dLazy.ts so they are available
before THREE.js loads.

Also unify SUPPORTED_EXTENSIONS_ACCEPT across load3d and dropdown, pass
uploadSubfolder prop through to WidgetSelectDropdown for correct upload
path, and update error message to list all supported extensions.

replacement for https://github.com/Comfy-Org/ComfyUI_frontend/pull/7975

(We should include thumbnail but not yet, will do it later)

## Screenshots (if applicable)


https://github.com/user-attachments/assets/2cb4b1da-af4f-439b-9786-3ac780c2480d

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8765-Feat-3d-dropdown-3036d73d365081d8a10ee19d3ed7d295)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Kelly Yang <124ykl@gmail.com>
2026-02-10 15:36:57 -05:00
AustinMroz
581452d312 Austin/fix move subgraph input (#8777)
Previously, moving a subgraph input link and re-attaching to the same
input slot would result in an invalid link


![broken-link](https://github.com/user-attachments/assets/085a0a6f-281d-4e06-be58-e5bdc873f1d5)

This occurred because:
- A new link is created to which overwrites the target `input.link`
- The previous link is then disconnected, which clears `input.link`

This is solved by instead returning early if the target is the same as
the existing link.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8777-Austin-fix-move-subgraph-input-3036d73d365081318de3cccb926f7fe7)
by [Unito](https://www.unito.io)
2026-02-10 12:31:09 -08:00
Simula_r
9dde4e7bc7 feat: sort workspaces (#8770)
## Summary

Sort workspaces so that the personal workspace appears first, followed
by the rest in ascending order (oldest first) by created_at / joined_at.

## Changes

- **What**: teamWorkspaceStore.ts, teamWorkspaceStore.test.ts
- **Breaking**: <!-- Any breaking changes (if none, remove this line)
-->
- **Dependencies**: <!-- New dependencies (if none, remove this line)
-->
2026-02-10 10:11:35 -08:00
Johnpaul Chiwetelu
0288ea5b39 feat: add setMany to settingStore for batch setting updates (#8767)
## Summary
- Adds `setMany()` method to `settingStore` for updating multiple
settings in a single API call via the existing `storeSettings` endpoint
- Extracts shared setting-apply logic (`applySettingLocally`) to reduce
duplication between `set()` and `setMany()`
- Migrates all call sites where multiple settings were updated
sequentially to use `setMany()`

## Call sites updated
- `releaseStore.ts` — `handleSkipRelease`, `handleShowChangelog`,
`handleWhatsNewSeen` (3 settings each)
- `keybindingService.ts` — `persistUserKeybindings` (2 settings)
- `coreSettings.ts` — `NavigationMode.onChange` (2 settings)

## Test plan
- [x] Unit tests for `setMany` (batch update, skip unchanged, no-op when
unchanged)
- [x] Updated `releaseStore.test.ts` assertions to verify `setMany`
usage
- [x] Updated `useCoreCommands.test.ts` mock to include `setMany`
- [x] All existing tests pass
- [x] `pnpm typecheck`, `pnpm lint`, `pnpm format` pass

Fixes #1079

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8767-feat-add-setMany-to-settingStore-for-batch-setting-updates-3036d73d36508161b8b6d298e1be1b7a)
by [Unito](https://www.unito.io)
2026-02-10 13:47:53 +01:00
Comfy Org PR Bot
061e96e488 1.39.11 (#8763)
Patch version increment to 1.39.11

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8763-1-39-11-3036d73d365081458389fe558cd921ee)
by [Unito](https://www.unito.io)

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2026-02-09 21:28:11 -08:00
Alexander Brown
ff9642d0cb feat: deduplicate subgraph node IDs on workflow load (experimental) (#8762)
## Summary

Add `ensureGlobalIdUniqueness` to reassign duplicate node IDs across
subgraphs when loading workflows, gated behind an experimental setting.

## Changes

- **What**: Shared `LGraphState` between root graph and subgraphs so ID
counters are global. Added `ensureGlobalIdUniqueness()` method that
detects and remaps colliding node IDs in subgraphs, preserving root
graph IDs as canonical and patching link references. Gated behind
`Comfy.Graph.DeduplicateSubgraphNodeIds` (experimental, default
`false`).
- **Dependencies**: None

## Review Focus

- Shared state override on `Subgraph` (getter delegates to root, setter
is no-op) — verify no existing code sets `subgraph.state` directly.
- `Math.max` state merging in `configure()` prevents ID counter
regression when loading subgraph definitions.
- Feature flag wiring: static property on `LGraph`, synced from settings
via `useLitegraphSettings`.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8762-feat-deduplicate-subgraph-node-IDs-on-workflow-load-experimental-3036d73d36508184b6cee5876dc4d935)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
2026-02-09 18:01:58 -08:00
AustinMroz
a6620a4ddc Fix edge cases in subgraph removal logic (#8758)
#8187 made removal of subgraphs cleanup the subgraph definition for the
removed graph and call onRemove handlers. However, it missed some edge
cases and broke subgraph conversion of selections containing subgraphs
which this PR tries to address.
- Deeply nested subgraphs are now also cleaned
- Adding a subgraphNode to the graph also ensures nested subgraphs are
added to subgraph definitions

Reminder: under this change, nodes can continue to exist after their
onRemoved handler has been called
- It may be better to instead perform the "garbage collection" of
subgraphs outside of the graph removal step to better handle edge cases
like subgraph conversion where a subgraph may continue to persist after
a parent subgraphNode has been removed from a graph.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8758-Fix-edge-cases-in-subgraph-removal-logic-3026d73d36508177b34ffdd2e0a114fe)
by [Unito](https://www.unito.io)
2026-02-09 13:55:23 -08:00
Benjamin Lu
9209badd37 feat: add KSampler live previews to assets sidebar jobs (#8723)
## Summary
Show live KSampler previews on active job cards/list items in the Assets
sidebar, while preserving existing fallback behavior.

## Changes
- **What**:
- Added a prompt-scoped job preview store (`jobPreviewStore`) gated by
`Comfy.Execution.PreviewMethod`.
- Wired `b_preview_with_metadata` handling to map previews by
`promptId`.
- Extended queue job view model with `livePreviewUrl` and consumed it in
both sidebar list and grid active job UIs.
  - Cleared prompt previews on execution reset.
- Added ref-counted shared blob URL lifecycle utility (`objectUrlUtil`)
and updated preview stores to retain/release shared URLs so each preview
event creates one object URL.
- Added/updated unit coverage in `useJobList.test.ts` for preview
enable/disable mapping.

## Review Focus
- Object URL lifecycle correctness across node previews and job previews
(retain/release behavior).
- Preview gating behavior when `Comfy.Execution.PreviewMethod` is
`none`.
- Active job UI fallback behavior (`livePreviewUrl` -> `iconImageUrl`).

## Screenshots (if applicable)
<img width="808" height="614" alt="image"
src="https://github.com/user-attachments/assets/37c66eb2-8c28-4eb4-bb86-5679cb77d740"
/>
<img width="775" height="345" alt="image"
src="https://github.com/user-attachments/assets/aa420642-b0d4-4ae6-b94a-e7934b5df9d6"
/>


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8723-feat-add-KSampler-live-previews-to-assets-sidebar-jobs-3006d73d365081aeb81dd8279bf99f94)
by [Unito](https://www.unito.io)
2026-02-09 10:49:27 -08:00
Benjamin Lu
815be49112 fix: keep begin_checkout user_id reactive in subscription flows (#8726)
## Summary

Use reactive `userId` reads for `begin_checkout` telemetry so delayed
auth state updates are reflected at event time instead of using a stale
snapshot.

## Changes

- **What**: switched subscription checkout telemetry paths to
`storeToRefs(useFirebaseAuthStore())` and read `userId.value` when
dispatching `trackBeginCheckout`.
- **What**: added regression tests that mutate `userId` after setup /
after checkout starts and assert telemetry uses the updated ID.

## Review Focus

- Verify `PricingTable` and `performSubscriptionCheckout` still emit
exactly one `begin_checkout` event per action, with `checkout_type:
change` and `checkout_type: new` in their respective paths.
- Verify the new tests would fail with stale store destructuring
(manually validated during development).

## Screenshots (if applicable)

N/A

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8726-fix-keep-begin_checkout-user_id-reactive-in-subscription-flows-3006d73d365081888c84c0335ab52e09)
by [Unito](https://www.unito.io)
2026-02-09 02:01:23 -08:00
Hunter
adbfb83767 feat: wire renewal_date from cloud billing status (#8754)
## Summary

Wire `renewal_date` from the cloud `/billing/status` response into the
workspace subscription UI so users can see when their subscription
renews.

## Problem

The workspace billing adapter hardcoded `renewalDate: null` because the
cloud billing status endpoint didn't return a renewal date. The
`SubscriptionPanelContentWorkspace` component already has UI for
displaying it — it just had no data.

Personal Workspace (existing `cloud-subscription-status`):
<img width="181" height="112" alt="Screenshot 2026-02-08 at 7 54 53 PM"
src="https://github.com/user-attachments/assets/a96ba2cd-10d0-442a-ae72-dbc663a9e52b"
/>

Current missing data from `/billing/status`:
<img width="240" height="124" alt="Screenshot 2026-02-08 at 7 55 38 PM"
src="https://github.com/user-attachments/assets/a3f51ce3-6663-43e1-97ed-d012a6a8a5ba"
/>

## Solution

- Add `renewal_date?: string` to `BillingStatusResponse` interface
- Use `status.renewal_date ?? null` instead of hardcoded `null` in
`useWorkspaceBilling`

### Related
- Cloud PR: Comfy-Org/cloud#2370 (adds `renewal_date` to the endpoint)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8754-feat-wire-renewal_date-from-cloud-billing-status-3026d73d365081c7ae51d79ef0633a1d)
by [Unito](https://www.unito.io)
2026-02-08 23:49:20 -08:00
Terry Jia
3238ad3d32 fix: re-mount DOM widget elements after leaving Linear mode (#8753)
## Summary

LinearView renders its own WidgetDOM instances which steal the
widget.element via replaceChildren. When LinearView unmounts
(v-if="linearMode") the element is removed from the DOM entirely.
The canvas-side WidgetDOM stays mounted but its container is now empty,
so DOM widgets (e.g. Three.js scenes) disappear.

Watch canvasStore.linearMode and reclaim the element when switching back
from Linear to Canvas mode.

## Screenshots (if applicable)
before


https://github.com/user-attachments/assets/78cea2bc-c4b3-4b21-bdb3-a521bb0d3062


after


https://github.com/user-attachments/assets/8f92c44d-9514-4001-bbdb-bc4c80468ed7

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8753-fix-re-mount-DOM-widget-elements-after-leaving-Linear-mode-3026d73d3650810b8968eff13dc84e9a)
by [Unito](https://www.unito.io)
2026-02-08 22:51:57 -05:00
Comfy Org PR Bot
be515d6fcc 1.39.10 (#8752)
Patch version increment to 1.39.10

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8752-1-39-10-3026d73d3650816ebe1edb895feb37a1)
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-08 17:30:05 -08:00
Christian Byrne
b583c92c64 fix: only show Failed Tests section when there are actual failures (#8575)
## Description

The Playwright test comment was showing a " Failed Tests" section
header even when there were only flaky tests (no actual failures). This
was confusing because the red X suggested failure when tests actually
passed.

**Before:** Shows " Failed Tests" section for flaky-only runs
**After:** Only shows " Failed Tests" section when there are actual
failures; flaky tests are treated as passing

## Related Issue

Fixes the misleading comment behavior seen in PR #8573



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8575-fix-only-show-Failed-Tests-section-when-there-are-actual-failures-2fc6d73d36508167889cc252e4e06f2e)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-08 13:14:38 -08:00
Christian Byrne
c0a209226d fix: handle RIFF padding for odd-sized WEBP chunks (#8527)
## Summary

Fix WEBP workflow loading failures for files with odd-sized chunks
before the EXIF chunk.

## Problem

WEBP files use the RIFF container format which [requires odd-sized
chunks to be padded with a single
byte](https://developers.google.com/speed/webp/docs/riff_container#riff_file_format):

> If Chunk Size is odd, a single padding byte -- which MUST be 0 to
conform with RIFF -- is added.

The `getWebpMetadata` function was not accounting for this padding,
causing it to miss the EXIF chunk in files with odd-sized preceding
chunks. This resulted in "Unable to find workflow in [filename].webp"
errors for valid WEBP files.

## Solution

Add the padding byte to the offset calculation:
```typescript
offset += 8 + chunk_length + (chunk_length % 2)
```

## Testing

- Tested with the sample image provided in the issue which previously
failed to load

Fixes #8076

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8527-fix-handle-RIFF-padding-for-odd-sized-WEBP-chunks-2fa6d73d3650815fb849fb6a4e767162)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-02-08 12:20:31 -08:00
Christian Byrne
c91d811d00 feat(cloud): add asset widget support for PrimitiveNode model selection (#8598)
Add cloud asset widget creation in `_createWidget()` using
`isAssetBrowserEligible()`
- Extract shared `createAssetWidget` factory to
`src/platform/assets/utils/`
- Refactor `useComboWidget.ts` to use the shared factory
- Add `_finalizeWidget()` helper to DRY up widget sizing/callback setup
- Pass target node's `comfyClass` and input name to Asset Browser for
correct model filtering
- Check `Comfy.Assets.UseAssetAPI` setting (matches `useComboWidget.ts`
behavior)
- Sync existing target widget value to asset widget
- Add toast notifications for asset validation errors
- Add i18n translations for invalidAsset and invalidFilename errors

Supersedes #8461 (clean rebase, no merge commits)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8598-feat-cloud-add-asset-widget-support-for-PrimitiveNode-model-selection-2fd6d73d365081a8afa7c2e91762f11c)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Introduced asset widget integration for cloud-based model selection,
enabling users to browse and select assets through an improved
interface.
* Added comprehensive asset validation with enhanced error messages for
invalid assets and filenames.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: guill <jacob.e.segal@gmail.com>
Co-authored-by: Jin Yi <jin12cc@gmail.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: AustinMroz <austin@comfy.org>
Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
Co-authored-by: Rizumu Ayaka <rizumu@ayaka.moe>
Co-authored-by: Kelly Yang <124ykl@gmail.com>
Co-authored-by: sno <snomiao@gmail.com>
Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
Co-authored-by: Terry Jia <terryjia88@gmail.com>
Co-authored-by: Luke Mino-Altherr <luke@comfy.org>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-08 12:18:13 -08:00
Alexander Brown
e625b0351c feat: migrate from @iconify/tailwind to @iconify/tailwind4 (#8724)
## Summary

Migrate from `@iconify/tailwind` (Tailwind 3 JS plugin) to
`@iconify/tailwind4` (native Tailwind 4 CSS plugin), moving all config
into CSS directives.

## Changes

- **What**: Replace `addDynamicIconSelectors()` JS plugin with `@plugin
"@iconify/tailwind4"` CSS directive. Move `boxShadow` theme extension
into `@theme` block. Delete both `tailwind.config.ts` files and the
runtime `iconCollection.ts` module.
- **Dependencies**: `@iconify/tailwind` removed, `@iconify/tailwind4`
added

## Review Focus

- `from-folder` path resolution in monorepo context (paths relative to
project root)
- SVG auto-cleanup behavior of `from-folder` vs the previous manual
`iconCollection.ts` loader
- Removal of `@config` directive and both tailwind config files — all
config now in CSS

## Files

| File | Change |
|------|--------|
| `pnpm-workspace.yaml` | Swap catalog entry |
| `packages/design-system/package.json` | Swap dep, remove
`tailwind-config` export |
| `packages/design-system/src/css/style.css` | Add `@plugin`,
`--shadow-interface` theme token, remove `@config` |
| `packages/design-system/tailwind.config.ts` | Deleted |
| `packages/design-system/src/iconCollection.ts` | Deleted |
| `tailwind.config.ts` | Deleted |
| `tsconfig.json`, `components.json` | Remove stale references |
| `knip.config.ts` | Ignore `@iconify-json/lucide` (CSS-consumed, not
JS-imported) |
| Docs | Updated `CONTRIBUTING.md` and `icons/README.md` |

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8724-feat-migrate-from-iconify-tailwind-to-iconify-tailwind4-3006d73d36508144a9b3e7ae73448f98)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
2026-02-07 22:35:34 -08:00
Benjamin Lu
a56f2d3883 fix: stabilize flaky workflows save-as browser assertions (#8735)
## Summary

Stabilize flaky workflows sidebar browser tests by waiting for eventual
UI state after `Save As`/overwrite operations.

## Changes

- **What**: Replace immediate assertions with retrying
`expect.poll(...)` in `browser_tests/tests/sidebar/workflows.spec.ts`
for:
  - `Can save workflow as`
  - `Can overwrite other workflows with save as`

## Review Focus

Verify the polling assertions are scoped to the intended eventual UI
state and do not hide real regressions.

## Screenshots (if applicable)

N/A

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8735-fix-stabilize-flaky-workflows-save-as-browser-assertions-3016d73d3650814abad1d767c0910ef6)
by [Unito](https://www.unito.io)
2026-02-07 22:35:14 -08:00
Alexander Brown
2eb7b8c994 Add @Comfy-org/comfy_frontend_devs to CODEOWNERS (#8739)
Updated CODEOWNERS file to include @Comfy-org/comfy_frontend_devs as an
owner for multiple paths.

## 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)

<!-- Add screenshots or video recording to help explain your changes -->
2026-02-07 20:52:46 -08:00
Christian Byrne
3eccf3ec61 feat: default to Getting Started category for new users in templates modal (#8599)
## Summary

Updates the templates modal to default to the "Getting Started" category
for new users.

## Changes

- Add `initialCategory` prop to `WorkflowTemplateSelectorDialog`
component
- Integrate `useNewUserService` in the dialog composable to detect
first-time users
- New users automatically see the "basics-getting-started" category
- Existing users continue to see "all" templates as default
- Allow explicit category override via options parameter

## Testing

- Added unit tests covering all scenarios (new user, non-new user,
undetermined, explicit override)
- 6 tests pass

Fixes COM-9146

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8599-feat-default-to-Getting-Started-category-for-new-users-in-templates-modal-2fd6d73d365081d4a5fad2abdb768269)
by [Unito](https://www.unito.io)
2026-02-07 20:30:10 -08:00
Hunter
1b73b5b31e fix: show credit balance for unsubscribed personal workspaces (#8719)
## Summary

Credit balance was not displayed in the user popover for personal
workspace users without an active subscription. The `displayedCredits`
computed returned `"0"` and `refreshBalance` skipped the API call when
there was no active subscription, hiding any existing balance.

## Changes

- **What**: Remove subscription-gated guards in
`CurrentUserPopoverWorkspace.vue`:
- `displayedCredits`: no longer returns early `""` / `"0"` when
subscription is null or inactive — always reads from the balance API
response
- `refreshBalance`: always fetches balance on popover open regardless of
subscription status

## Review Focus

The credits section visibility is already gated by `showCreditsSection`
(personal workspace or owner role). This change only affects what value
is displayed and whether the balance API is called — it does not change
who sees the section.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8719-fix-show-credit-balance-for-unsubscribed-personal-workspaces-3006d73d3650812e9d70e5a8629c5f60)
by [Unito](https://www.unito.io)
2026-02-07 20:22:00 -08:00
Comfy Org PR Bot
882d595d4a 1.39.9 (#8707)
Patch version increment to 1.39.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-02-07 20:17:39 -08:00
Alexander Brown
10845cb865 Revert "fix: add post-processing script to fix i18n nodeDefs array corruption" (#8736)
Reverts Comfy-Org/ComfyUI_frontend#8718
2026-02-07 20:08:02 -08:00
Christian Byrne
6c8473e4e4 refactor: replace runtime isElectron() with build-time isDesktop constant (#8710)
## Summary

Replace all runtime `isElectron()` function calls with the build-time
`isDesktop` constant from `@/platform/distribution/types`, enabling
dead-code elimination in non-desktop builds.

## Changes

- **What**: Migrate 30 files from runtime `isElectron()` detection
(checking `window.electronAPI`) to the compile-time `isDesktop` constant
(driven by `__DISTRIBUTION__` Vite define). Remove `isElectron` from
`envUtil.ts`. Update `isNativeWindow()` to use `isDesktop`. Guard
`electronAPI()` calls behind `isDesktop` checks in stores. Update 7 test
files to use `vi.hoisted` + getter mock pattern for per-test `isDesktop`
toggling. Add `DISTRIBUTION=desktop` to `dev:electron` script.

## Review Focus

- The `electronDownloadStore.ts` now guards the top-level
`electronAPI()` call behind `isDesktop` to prevent crashes on
non-desktop builds.
- Test mocking pattern uses `vi.hoisted` with a getter to allow per-test
toggling of the `isDesktop` value.
- Pre-existing issues not addressed: `as ElectronAPI` cast in
`envUtil.ts`, `:class="[]"` in `BaseViewTemplate.vue`,
`@ts-expect-error` in `ModelLibrarySidebarTab.vue`.
- This subsumes PR #8627 and renders PR #6122 and PR #7374 obsolete.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8710-refactor-replace-runtime-isElectron-with-build-time-isDesktop-constant-3006d73d365081c08037f0e61c2f6c77)
by [Unito](https://www.unito.io)
2026-02-07 19:47:05 -08:00
Benjamin Lu
b7fef1c744 fix: update queue tooltip copy to include right-click hint (#8733)
### Motivation
- Update the run-menu queue tooltip to include the
right-click-to-clear-queue hint so the UI copy matches the requested
product wording.

### Description
- Replaced `sideToolbar.queueProgressOverlay.viewJobHistory` value in
`src/locales/en/main.json` with `View active jobs (right-click to clear
queue)`.

### Testing
- Ran `pnpm lint` and `pnpm typecheck`, and both completed successfully.

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8733-fix-update-queue-tooltip-copy-to-include-right-click-hint-3016d73d365081c09fa7f582ee51c6c2)
by [Unito](https://www.unito.io)
2026-02-07 19:46:23 -08:00
Terry Jia
828323e263 fix: add post-processing script to fix i18n nodeDefs array corruption (#8718)
## Summary
@lobehub/i18n-cli (GPT-4.1) converts numeric-keyed objects like {"0":
{...}, "1": {...}} into JSON arrays with null gaps, which
crashes vue-i18n path resolution. 

Add a post-processing step that converts arrays back to objects after
translation.

## Screenshots (if applicable)
before


https://github.com/user-attachments/assets/44e81790-feae-405b-b2c4-098b06a98785


after


https://github.com/user-attachments/assets/5d1bd836-c923-437a-aca0-7ebd4d8acb89
<img width="2703" height="1083" alt="image"
src="https://github.com/user-attachments/assets/370a2eb0-c46d-4901-a23a-ab3002a9660d"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8718-fix-add-post-processing-script-to-fix-i18n-nodeDefs-array-corruption-3006d73d365081dab020fea997ec4c0a)
by [Unito](https://www.unito.io)
2026-02-07 18:19:37 -08:00
Christian Byrne
6535138e0b fix(vue-nodes): hide slot labels for reroute nodes with empty names (#8574)
## Summary
Fixes reroute node styling in Vue Nodes 2.0 by hiding slot labels when
slot names are intentionally empty.


| Before | After |
|
---------------------------------------------------------------------------------------------------------------------------------------------
|
---------------------------------------------------------------------------------------------------------------------------------------------
|
| <img width="1437" height="473" alt="image"
src="https://github.com/user-attachments/assets/603f52e0-7b75-4822-8c91-0a8374cc0cb6"
/> | <img width="1350" height="493" alt="image"
src="https://github.com/user-attachments/assets/38168955-4d35-4c61-a685-a54efb44cd5d"
/> |


## Problem
Reroute nodes displayed unwanted fallback labels ("Input 0", "Output 0")
instead of appearing as minimal connection-only nodes. This happened
because:
- Reroute nodes intentionally use empty string (`""`) for slot names
- Slot components used `||` operator for fallback labels, treating `''`
as falsy

## Solution
- Add `hasNoLabel` computed property to detect when all label sources
(`label`, `localized_name`, `name`) are empty/falsy
- Derive `dotOnly` from either the existing prop OR `hasNoLabel` being
true
- When `dotOnly` is true: label text is hidden, padding removed
(`lg-slot--dot-only` class), only connection dot visible

Combined with existing `NO_TITLE` support from #7589, reroutes now
display as minimal nodes with just connection dots—matching classic
reroute appearance.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Bug Fixes**
* Enhanced input and output slot label handling to automatically conceal
labels when unavailable
* Improved fallback display names for slots with more reliable naming
logic

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8574-fix-vue-nodes-hide-slot-labels-for-reroute-nodes-with-empty-names-2fc6d73d365081c38031e260402283d3)
by [Unito](https://www.unito.io)
2026-02-07 15:30:20 -08:00
Terry Jia
ad6f856a31 fix: terminal tabs fail to register due to useI18n() after await (#8717)
## Summary
useI18n() requires an active Vue component instance via
getCurrentInstance(), which returns null after an await in the setup
context. Since terminal tabs are loaded via dynamic import (await),
useI18n() was silently failing, preventing terminal tab registration.

Replace useI18n() calls with static English strings for the title field,
which is only used in command labels. The titleKey field already handles
reactive i18n in the UI via BottomPanel.vue's getTabDisplayTitle().

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

## Screenshots (if applicable)
before


https://github.com/user-attachments/assets/44e0118e-5566-4299-84cf-72b63d85521a


after


https://github.com/user-attachments/assets/3e99fb81-7a81-4065-a889-3ab5a393d8cf

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8717-fix-terminal-tabs-fail-to-register-due-to-useI18n-after-await-3006d73d3650810fb011c08862434bd5)
by [Unito](https://www.unito.io)
2026-02-07 12:45:07 -08:00
Terry Jia
7ed71c7769 fix: exclude transient image URLs from ImageCompare workflow serialization (#8715)
## Summary
Image URLs set by onExecuted are execution results that don't exist on
other machines. Disable workflow persistence (widget.serialize) while
keeping prompt serialization (widget.options.serialize) so compare_view
is still sent to the backend.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8715-fix-exclude-transient-image-URLs-from-ImageCompare-workflow-serialization-3006d73d365081b8aa87c7e05cb25f2f)
by [Unito](https://www.unito.io)
2026-02-07 15:08:05 -05:00
Hunter
442eff1094 fix: use useI18n() instead of @/i18n import in PricingTableWorkspace (#8720)
## Summary

PricingTableWorkspace.vue was missed in #8704 which migrated all Vue
components from `import { t } from '@/i18n'` to `useI18n()` and upgraded
the lint rule to `error`. This breaks `pnpm lint` on main.

## Changes

- **What**: Removed `import { t } from '@/i18n'` and destructured `t`
from the existing `useI18n()` call. Moved `useI18n()` above static
initializers that reference `t`.

## Review Focus

The `billingCycleOptions` and `tiers` arrays call `t()` at module init
time — this is fine in `<script setup>` since `useI18n()` is called
first in the same synchronous scope.
2026-02-07 09:31:26 -08:00
Benjamin Lu
dd4d36d459 fix: route gtm through telemetry entrypoint (#8354)
Wire checkout attribution into GTM events and checkout POST payloads.

This updates the cloud telemetry flow so the backend team can correlate checkout events without relying on frontend cookie parsing. We now surface GA4 identity via a GTM-provided global and include attribution on both `begin_checkout` telemetry and the checkout POST body. The backend should continue to derive the Firebase UID from the auth header; the checkout POST body does not include a user ID.

GTM events pushed (unchanged list, updated payloads):
- `page_view` (page title/location/referrer as before)
- `sign_up` / `login`
- `begin_checkout` now includes:
  - `user_id`, `tier`, `cycle`, `checkout_type`, `previous_tier` (if change flow)
  - `ga_client_id`, `ga_session_id`, `ga_session_number`
  - `gclid`, `gbraid`, `wbraid`

Backend-facing change:
- `POST /customers/cloud-subscription-checkout/:tier` now includes a JSON body with attribution fields only:
  - `ga_client_id`, `ga_session_id`, `ga_session_number`
  - `gclid`, `gbraid`, `wbraid`
- Backend should continue to derive the Firebase UID from the auth header.

Required GTM setup:
- Provide `window.__ga_identity__` via a GTM Custom HTML tag (after GA4/Google tag) with `{ client_id, session_id, session_number }`. The frontend reads this to populate the GA fields.

<img width="1416" height="1230" alt="image" src="https://github.com/user-attachments/assets/b77cf0ed-be69-4497-a540-86e5beb7bfac" />

## Screenshots (if applicable)

<img width="991" height="385" alt="image" src="https://github.com/user-attachments/assets/8309cd9e-5ab5-4fba-addb-2d101aaae7e9"/>

Manual Testing:
<img width="3839" height="2020" alt="image" src="https://github.com/user-attachments/assets/36901dfd-08db-4c07-97b8-a71e6783c72f"/>
<img width="2141" height="851" alt="image" src="https://github.com/user-attachments/assets/2e9f7aa4-4716-40f7-b147-1c74b0ce8067"/>
<img width="2298" height="982" alt="image" src="https://github.com/user-attachments/assets/72cbaa53-9b92-458a-8539-c987cf753b02"/>
<img width="2125" height="999" alt="image" src="https://github.com/user-attachments/assets/4b22387e-8027-4f50-be49-a410282a1adc"/>

To manually test, you will need to override api/features in devtools to also return this:

```
"gtm_container_id": "GTM-NP9JM6K7"
```

┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-8354-fix-route-gtm-through-telemetry-entrypoint-2f66d73d36508138afacdeffe835f28a) by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Analytics expanded: page view tracking, richer auth telemetry (includes user IDs), and checkout begin events with attribution.
  * Google Tag Manager support and persistent checkout attribution (GA/client/session IDs, gclid/gbraid/wbraid).

* **Chores**
  * Telemetry reworked to support multiple providers via a registry with cloud-only initialization.
  * Workflow module refactored for clearer exports.

* **Tests**
  * Added/updated tests for attribution, telemetry, and subscription flows.

* **CI**
  * New check prevents telemetry from leaking into distribution artifacts.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-07 01:08:48 -08:00
Alexander Brown
69c8c84aef fix: resolve i18n no-restricted-imports lint warnings (#8704)
## Summary

Fix all i18n `no-restricted-imports` lint warnings and upgrade rules
from `warn` to `error`.

## Changes

- **What**: Migrate Vue components from `import { t/d } from '@/i18n'`
to `const { t } = useI18n()`. Migrate non-component `.ts` files from
`useI18n()` to `import { t/d } from '@/i18n'`. Allow `st` import from
`@/i18n` in Vue components (it wraps `te`/`t` for safe fallback
translation). Remove `@deprecated` tag from `i18n.ts` global exports
(still used by `st` and non-component code). Upgrade both lint rules
from `warn` to `error`.

## Review Focus

- The `st` helper is intentionally excluded from the Vue component
restriction since it provides safe fallback translation needed for
custom node definitions.

Fixes #8701

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8704-fix-resolve-i18n-no-restricted-imports-lint-warnings-2ff6d73d365081ae84d8eb0dfef24323)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-02-06 20:54:53 -08:00
Simula_r
c5431de123 Feat/workspaces 6 billing (#8508)
## Summary

Implements billing infrastructure for team workspaces, separate from
legacy personal billing.

## Changes

- **Billing abstraction**: New `useBillingContext` composable that
switches between legacy (personal) and workspace billing based on
context
- **Workspace subscription flows**: Pricing tables, plan transitions,
cancellation dialogs, and payment preview components for workspace
billing
- **Top-up credits**: Workspace-specific top-up dialog with polling for
payment confirmation
- **Workspace API**: Extended with billing endpoints (subscriptions,
invoices, payment methods, credits top-up)
- **Workspace switcher**: Now displays tier badges for each workspace
- **Subscribe polling**: Added polling mechanisms
(`useSubscribePolling`, `useTopupPolling`) for async payment flows

## Review Focus

- Billing flow correctness for workspace vs legacy contexts
- Polling timeout and error handling in payment flows

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8508-Feat-workspaces-6-billing-2f96d73d365081f69f65c1ddf369010d)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-06 20:52:53 -08:00
Benjamin Lu
030d4fd4d5 fix: hide assets sidebar badge when legacy queue is enabled (#8708)
### Motivation
- The Assets sidebar shows a notification-style badge immediately when a
job is queued using the legacy queue, which is misleading because that
badge is intended for the newer Queue Panel V2 experience.

### Description
- Gate the Assets sidebar `iconBadge` on the `Comfy.Queue.QPOV2` setting
by importing `useSettingStore` and returning `null` when QPO V2 is
disabled; otherwise show `pendingTasks.length` as before
(`src/composables/sidebarTabs/useAssetsSidebarTab.ts`).
- Add a focused unit test that mocks the settings and queue store to
verify the badge is hidden when QPO V2 is disabled and shows the pending
count when enabled
(`src/composables/sidebarTabs/useAssetsSidebarTab.test.ts`).
- Keep the Assets component import mocked in the test to avoid
bootstrapping the full UI during unit runs.

### Testing
- Ran the new unit test with `pnpm vitest
src/composables/sidebarTabs/useAssetsSidebarTab.test.ts` and it passed
(2 tests).
- Ran type checking with `pnpm typecheck` and it completed successfully.
- Ran linting with `pnpm lint` and no errors were reported.

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8708-fix-hide-assets-sidebar-badge-when-legacy-queue-is-enabled-3006d73d3650818eb809de399583088e)
by [Unito](https://www.unito.io)
2026-02-06 20:21:16 -08:00
Christian Byrne
473fa609e3 devex: add Playwright test writing Agent Skill (#8512)
Since there is so much ground to cover, tried to use the progressive
disclosure approach (often recommended when writing skills) such that
the agent only gets the info it needs for the given type of tests it is
writing.

Still need to try using the skill to write tests and iterate a bit from
there.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8512-devtool-add-Playwright-test-writing-Agent-Skill-2fa6d73d365081aaaf6dd186b9dcf8ce)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-06 19:15:56 -08:00
Terry Jia
a2c8324c0a fix: resolve 3D nodes missing after page refresh (#8711)
## Summary
Await all registerNodeDef calls in registerNodesFromDefs to prevent race
condition where lazy-loaded 3D node types (Load3D, Preview3D, SaveGLB)
are not registered in LiteGraph before workflow loading.

Replay lazily loaded extensions' beforeRegisterNodeDef hooks so that
input type modifications (e.g. Preview3D → PREVIEW_3D) are applied
correctly despite the extensions being registered mid-invocation.

Fixes the issue introduced by code splitting (#8542) where THREE.js lazy
import caused node registration to complete after workflow load.

## Screenshots (if applicable)
before

https://github.com/user-attachments/assets/370545dc-4081-4164-83ed-331a092fc690

after

https://github.com/user-attachments/assets/bf9dc887-0076-41fe-93ad-ab0bb984c5ce
2026-02-06 22:05:44 -05:00
Terry Jia
d9ce4ff5e0 feat: render dragging links above Vue nodes via overlay canvas (#8695)
## Summary
When vueNodesMode is enabled, the dragging link preview was rendered on
the background canvas behind DOM-based Vue nodes, making it invisible
when overlapping node bodies.

Add a new overlay canvas layer between TransformPane and
SelectionRectangle that renders the dragging link preview and snap
highlight above the Vue node DOM layer.

Static connections remain on the background canvas as before.

fix https://github.com/Comfy-Org/ComfyUI_frontend/issues/8414
discussed with @DrJKL 

## Screenshots
before

https://github.com/user-attachments/assets/94508efa-570c-4e32-a373-360b72625fdd

after

https://github.com/user-attachments/assets/4b0f924c-66ce-4f49-97d7-51e6e923a1b9

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8695-feat-render-dragging-links-above-Vue-nodes-via-overlay-canvas-2ff6d73d365081599b2fe18b87f34b7a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-06 21:19:18 -05:00
AustinMroz
e7932f2fc2 Tighter image sizing in vue mode (#8702)
Fixes multiple overlapping issues with both the ImagePreviews (LoadImage
node) and LivePreview (Ksampler node) to eliminate empty space and move
the bahviour to be closer to the litegraph implementation.
- NodeWidgets will no longer no longer flex-grow when it contains no
widgets capable of growing
<img width="278" height="585" alt="image"
src="https://github.com/user-attachments/assets/c4c39805-1474-457b-86d1-b64ecf01319f"
/>

- The number of element layers for LivePreview has been reduced. Sizing
is difficult to properly spread across nested flex levels.
- The ImagePreview and LivePreview now have `contain-size` set with a
min height of 220 pixels (the same as the litegraph implementation).
This allows images to "pillarbox" by increasing width without increasing
height.
  | Before | After |
  | ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/3fe38a20-47d3-4a77-a0db-63167f76b0be"/>
| <img width="360" alt="after"
src="https://github.com/user-attachments/assets/22dc6bf6-1812-49bb-95a1-3febfb3e40dd"
/>|
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/99b24547-6850-4b46-a972-53411676c78f"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/0a7783c8-cf93-47aa-8c60-608b9a4b4498"
/>|

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8702-Tighter-image-sizing-in-vue-mode-2ff6d73d3650814196f0d55d81a42e2d)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-06 16:39:26 -08:00
Christian Byrne
f53b0879ed feat: add model-to-node mappings for cloud asset categories (#8468)
## Summary

Add mappings for 13 previously unmapped model categories in the Cloud
asset browser, enabling users to click on models to create corresponding
loader nodes on the canvas.

## Changes

### Core nodes
- `latent_upscale_models` → `LatentUpscaleModelLoader`

### Extension nodes
| Category | Node Class | Widget Key |
|----------|-----------|-----------|
| `sam2` | `DownloadAndLoadSAM2Model` | `model` |
| `sams` | `SAMLoader` | `model_name` |
| `ultralytics` | `UltralyticsDetectorProvider` | `model_name` |
| `depthanything` | `DownloadAndLoadDepthAnythingV2Model` | `model` |
| `ipadapter` | `IPAdapterModelLoader` | `ipadapter_file` |
| `segformer_b2_clothes` | `LS_LoadSegformerModel` | `model_name` |
| `segformer_b3_clothes` | `LS_LoadSegformerModel` | `model_name` |
| `segformer_b3_fashion` | `LS_LoadSegformerModel` | `model_name` |
| `nlf` | `LoadNLFModel` | `nlf_model` |
| `FlashVSR` | `FlashVSRNode` | (auto-load) |
| `FlashVSR-v1.1` | `FlashVSRNode` | (auto-load) |

### Hierarchical fallback
- `ultralytics/bbox` and `ultralytics/segm` fall back to the
`ultralytics` mapping

### Skipped categories
- `vae_approx` - No user-facing loader (used internally for latent
previews)
- `detection` - No specific loader exists

## Testing
- Added unit tests for all new mappings
- Tests verify hierarchical fallback works correctly
- All 40 tests pass

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8468-feat-add-model-to-node-mappings-for-cloud-asset-categories-2f86d73d365081389ea5fbfc52ecbfad)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
2026-02-06 16:04:27 -08:00
Alexander Brown
5441f70cd5 feat: enforce i18n import conventions via ESLint (#8701)
## Summary

Enforce i18n import conventions via ESLint: Vue components must use
`useI18n()`, non-composable `.ts` files must use the global `t` from
`@/i18n`.

## Changes

- **What**: Two new `no-restricted-imports` rules in `eslint.config.ts`
(both `warn` severity for incremental migration). Removed the disabled
`@/i18n--to-enable` placeholder from `.oxlintrc.json`.
- `.vue` files: disallow importing `t`/`d`/`st`/`te` from `@/i18n` (37
existing violations to migrate)
- Non-composable `.ts` files: disallow importing `useI18n` from
`vue-i18n` (2 existing violations)
- Composable `use*.ts`, test files, and `src/i18n.ts` are excluded from
Rule 2

## Review Focus

- The rules are placed after `oxlint.buildFromOxlintConfigFile()` to
re-enable ESLint's `no-restricted-imports` for these specific file
scopes without conflicting with oxlint's base rule (which handles
PrimeVue deprecations).
- `warn` severity chosen so CI is not blocked while existing violations
are migrated.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8701-feat-enforce-i18n-import-conventions-via-ESLint-2ff6d73d36508123b6f9edf2693fb5e0)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-06 14:03:00 -08:00
pythongosssss
0e3314bbd3 Node ghost mode when adding nodes (#8694)
## Summary

Adds option for adding a node as a "ghost" that follows the cursor until
the user left clicks to confirm, or esc/right click to cancel.

## Changes

- **What**: 
Adds option for `ghost` when calling `graph.add`  
This adds the node with a `flag` of ghost which causes it to render
transparent
Selects the node, then sets the canvas as dragging to stick the node to
the cursor

## Screenshots (if applicable)



https://github.com/user-attachments/assets/dcb5702f-aba3-4983-aa40-c51f24a4767a

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8694-Node-ghost-mode-when-adding-nodes-2ff6d73d3650815591f2c28415050463)
by [Unito](https://www.unito.io)
2026-02-06 13:42:38 -08:00
pythongosssss
8f301ec94b Fix hit detection on vue node slots (#8609)
## Summary

Vue node slots extend outside the bounds of the node:
<img width="123" height="107" alt="image"
src="https://github.com/user-attachments/assets/96f7f28b-de54-4978-bc78-f38fc1fd4ea1"
/>
When clicking on the outer half of the slot, the matching node is not
found as the click was technically not over a node, however in reality
the action should still be associated with the node the slot is for.

This specifically fixes middle click to add reroute not working on the
outer half of the slot.

## Changes

- **What**: 
- If the event is not over a node, check if is over a Vue slot, if so,
use the node associated with that slot.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8609-Fix-hit-detection-on-vue-node-slots-2fd6d73d3650815c8328f9ea8fa66b0c)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Tests**
* Added comprehensive test suite for slot hit-detection in Vue nodes
mode, covering standard and fallback interaction paths.

* **Bug Fixes**
* Improved hit-detection accuracy for slots that extend beyond node
boundaries in Vue mode, ensuring clicks map to the correct node.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-06 13:41:18 -08:00
Terry Jia
17c1b1f989 fix: prevent node height shrinking on vueNodes/litegraph mode switch (#8697)
## Summary
Three interacting bugs caused cumulative height loss (~66px per cycle):

1. useVueNodeLifecycle incorrectly subtracted NODE_TITLE_HEIGHT from
LGraphNode.size[1] during layout init, but size[1] is already
content-only (title excluded per LGraphNode.measure()).

2. ensureCorrectLayoutScale added/subtracted NODE_TITLE_HEIGHT in scale
formulas, breaking the round-trip.
Simplified to pure ratio scaling (size * scaleFactor). 
Also set LayoutSource.Canvas before batchUpdateNodeBounds to prevent
stale DOM source from triggering incorrect height normalization.

3. initSizeStyles set --node-height (min-height of the full DOM element
including title) to the content-only layout height.
Added NODE_TITLE_HEIGHT so ResizeObserver captures the correct total
height and normalization recovers the exact content height.

## Screenshots (if applicable)
before

https://github.com/user-attachments/assets/ae41124b-f9e3-4061-8127-eeacddc67a55

after

https://github.com/user-attachments/assets/5ff288a6-73a3-481a-a750-150d9bdbc8fe

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8697-fix-prevent-node-height-shrinking-on-vueNodes-litegraph-mode-switch-2ff6d73d365081c7a2acdc7ec57e2e19)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-06 12:52:13 -08:00
AustinMroz
a4b725b85e Fix legacy history (#8687)
Restores functionality of the history and queue sections in the legacy
"floating menu" which were broken in #7650

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8687-Fix-legacy-history-2ff6d73d3650810a8a05f2ee18cbfa1d)
by [Unito](https://www.unito.io)
2026-02-06 10:14:55 -08:00
AustinMroz
8283438ee6 Fix incorrect widgetValue migration (#8625)
Under a combination of many edge cases, the `widget_values` migration
code added in #3326 would cause the progress text on a "Recraft Text to
Image" node to incorrectly deserialize into the `control_after_generate`
- widgets_values is of length 1 greater than it should be because
progress text serializes
  - It should not, there is no code to deserialize it
- negative_prompt has force_input set and skips serialization
- Migration only applies when `widgets_values` is equal to actual inputs
length. The two above edge cases cancel to make this true
- Seed is accounted for when calculating the length of widgets, but not
when applying the migration
- Migration occurs even though we track workflow version now and have an
accurate way of determining that it can not be needed

The two primary edge cases which cause the bug are both addressed
- `options.serialize` does nothing and has never done anything. I've
been guilty of making the same mistake in the ancient past, and want to
clean up the misconception where I can.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8625-Fix-incorrect-widgetValue-migration-2fe6d73d365081a683b4c675eaeebb6c)
by [Unito](https://www.unito.io)
2026-02-05 21:27:11 -08:00
Johnpaul Chiwetelu
d05e4eac58 fix: include subfolder in asset download URL for audio/video files (#8684)
## Summary

- `getAssetUrl()` was constructing `/view` URLs without the `subfolder`
query parameter, causing backend to return "file not found" for assets
stored in subfolders (common for audio/video outputs)
- Preview/playback was unaffected because it uses `preview_url` from
`ResultItemImpl.url` which correctly includes `subfolder`
- Fixed `getAssetUrl()` to include `subfolder` from
`asset.user_metadata` when present
- Simplified download functions to prefer `preview_url` (already
correct) with `getAssetUrl` as fallback

## Test plan

- [ ] Generate audio/video output (e.g. via SaveAudio node) that saves
to a subfolder
- [ ] Right-click the asset in the assets sidebar and click Download —
should download successfully
- [ ] Select multiple audio/video assets and use bulk download — should
download all
- [ ] Verify image downloads still work as before
- [ ] Verify cloud environment downloads still work (uses `preview_url`
path)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **New Features**
  * Added support for organizing and downloading assets from subfolders.

* **Refactor**
* Improved asset URL generation and download handling for better
reliability and performance.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-06 05:06:11 +01:00
Comfy Org PR Bot
7f509cc018 1.39.8 (#8678)
Patch version increment to 1.39.8

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8678-1-39-8-2ff6d73d36508144b359cdd15822f6fb)
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-05 19:30:45 -08:00
Christian Byrne
23c8757447 fix: increase active node border weight from 2 to 3 (#8654)
## Summary

Increase stroke/outline weight for active node states to improve
visibility during workflow execution (COM-7770).

## Changes

- **What**: Increased border/stroke weight from 2 to 3 for active nodes
in both Vue Nodes and LiteGraph renderers
  - Vue Nodes: `outline-2` → `outline-3` in `LGraphNode.vue`
- LiteGraph: `lineWidth: 3` for `running` stroke (was default 1) and
`executionError` stroke (was 2) in `litegraphService.ts`
  - Updated test assertion to match

## Review Focus

Minimal visual change. The `executionError` lineWidth was also bumped
from 2 → 3 so error states remain at least as prominent as running
states.

> **Note:**
[#8603](https://github.com/Comfy-Org/ComfyUI_frontend/pull/8603) (by
@AustinMroz) also modifies `LGraphNode.vue` with a larger restructuring
(bottom buttons, badges, resize handle). The two PRs do not conflict —
#8603 does not touch the outline/border state classes or
`litegraphService.ts`, so both changes merge cleanly.

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-05 17:44:19 -08:00
Johnpaul Chiwetelu
7d3d00858a feat: add download button to audio preview player (#8628)
## Summary
- Adds a download icon button to the `AudioPreviewPlayer` widget for
PreviewAudio and SaveAudio nodes
- Reuses the existing `downloadFile` utility (same as video download)
- Button appears inline next to volume/options controls, matching the
player's existing UI style

## Test plan
- [x] Add a PreviewAudio or SaveAudio node, run a workflow that produces
audio output
- [x] Verify the download icon appears in the audio player controls
- [x] Click the download button and confirm the audio file downloads
correctly
- [x] Verify the button does not appear when no audio is loaded


https://github.com/user-attachments/assets/7fb2df16-82a6-40aa-a938-aed57032e30b

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8628-feat-add-download-button-to-audio-preview-player-2fe6d73d365081e3997fc45d3bb8cffc)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-06 01:35:32 +01:00
Christian Byrne
478cfc0b5e feat: replace puzzle icon with extensions-blocks icon for manager button (#8644)
## Summary

Replace the manager button puzzle icon with a custom extensions-blocks
SVG icon and add a "Manage Extensions" text label to the top bar button.

## Changes

- **What**: Swap `icon-[lucide--puzzle]` →
`icon-[comfy--extensions-blocks]` in TopMenuSection, ComfyMenuButton,
and ManagerDialog. Add visible "Manage Extensions" label (hidden below
md). Align tooltip with new label text.

## Review Focus

- Visual appearance of the new icon at different sizes
- Button layout with text label at md+ breakpoints
- Red dot notification positioning with wider button

Fixes COM-12161

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8644-feat-replace-puzzle-icon-with-extensions-blocks-icon-for-manager-button-2fe6d73d3650815c8867efc5a0842ef7)
by [Unito](https://www.unito.io)

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-05 16:33:26 -08:00
Johnpaul Chiwetelu
90a701dd67 Road to No Explicit Any Part 11 (#8565)
## Summary

This PR removes `any` types from widgets, services, stores, and test
files, replacing them with proper TypeScript types.

### Key Changes

#### Type Safety Improvements
- Replaced `any` with `unknown`, explicit types, or proper interfaces
across widgets and services
- Added proper type imports (TgpuRoot, Point, StyleValue, etc.)
- Created typed interfaces (NumericWidgetOptions, TestWindow,
ImportFailureDetail, etc.)
- Fixed function return types to be non-nullable where appropriate
- Added type guards and null checks instead of non-null assertions
- Used `ComponentProps` from vue-component-type-helpers for component
testing

#### Widget System
- Added index signature to IWidgetOptions for Record compatibility
- Centralized disabled logic in WidgetInputNumberInput
- Moved template type assertions to computed properties
- Fixed ComboWidget getOptionLabel type assertions
- Improved remote widget type handling with runtime checks

#### Services & Stores
- Fixed getOrCreateViewer to return non-nullable values
- Updated addNodeOnGraph to use specific options type `{ pos?: Point }`
- Added proper type assertions for settings store retrieval
- Fixed executionIdToCurrentId return type (string | undefined)

#### Test Infrastructure
- Exported GraphOrSubgraph from litegraph barrel to avoid circular
dependencies
- Updated test fixtures with proper TypeScript types (TestInfo,
LGraphNode)
- Replaced loose Record types with ComponentProps in tests
- Added proper error handling in WebSocket fixture

#### Code Organization
- Created shared i18n-types module for locale data types
- Made ImportFailureDetail non-exported (internal use only)
- Added @public JSDoc tag to ElectronWindow type
- Fixed console.log usage in scripts to use allowed methods

### Files Changed

**Widgets & Components:**
-
src/renderer/extensions/vueNodes/widgets/components/WidgetInputNumberInput.vue
-
src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDefault.vue
-
src/renderer/extensions/vueNodes/widgets/components/WidgetSelectDropdown.vue
- src/renderer/extensions/vueNodes/widgets/components/WidgetTextarea.vue
-
src/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget.ts
- src/lib/litegraph/src/widgets/ComboWidget.ts
- src/lib/litegraph/src/types/widgets.ts
- src/components/common/LazyImage.vue
- src/components/load3d/Load3dViewerContent.vue

**Services & Stores:**
- src/services/litegraphService.ts
- src/services/load3dService.ts
- src/services/colorPaletteService.ts
- src/stores/maskEditorStore.ts
- src/stores/nodeDefStore.ts
- src/platform/settings/settingStore.ts
- src/platform/workflow/management/stores/workflowStore.ts

**Composables & Utils:**
- src/composables/node/useWatchWidget.ts
- src/composables/useCanvasDrop.ts
- src/utils/widgetPropFilter.ts
- src/utils/queueDisplay.ts
- src/utils/envUtil.ts

**Test Files:**
- browser_tests/fixtures/ComfyPage.ts
- browser_tests/fixtures/ws.ts
- browser_tests/tests/actionbar.spec.ts
-
src/workbench/extensions/manager/components/manager/skeleton/PackCardGridSkeleton.test.ts
- src/lib/litegraph/src/subgraph/subgraphUtils.test.ts
- src/components/rightSidePanel/shared.test.ts
- src/platform/cloud/subscription/composables/useSubscription.test.ts
-
src/platform/workflow/persistence/composables/useWorkflowPersistence.test.ts

**Scripts & Types:**
- scripts/i18n-types.ts (new shared module)
- scripts/diff-i18n.ts
- scripts/check-unused-i18n-keys.ts
- src/workbench/extensions/manager/types/conflictDetectionTypes.ts
- src/types/algoliaTypes.ts
- src/types/simplifiedWidget.ts

**Infrastructure:**
- src/lib/litegraph/src/litegraph.ts (added GraphOrSubgraph export)
- src/lib/litegraph/src/infrastructure/CustomEventTarget.ts
- src/platform/assets/services/assetService.ts

**Stories:**
- apps/desktop-ui/src/views/InstallView.stories.ts
- src/components/queue/job/JobDetailsPopover.stories.ts

**Extension Manager:**
- src/workbench/extensions/manager/composables/useConflictDetection.ts
- src/workbench/extensions/manager/composables/useManagerQueue.ts
- src/workbench/extensions/manager/services/comfyManagerService.ts
- src/workbench/extensions/manager/utils/conflictMessageUtil.ts

### Testing

- [x] All TypeScript type checking passes (`pnpm typecheck`)
- [x] ESLint passes without errors (`pnpm lint`)
- [x] Format checks pass (`pnpm format:check`)
- [x] Knip (unused exports) passes (`pnpm knip`)
- [x] Pre-commit and pre-push hooks pass

Part of the "Road to No Explicit Any" initiative.

### Previous PRs in this series:
- Part 2: #7401
- Part 3: #7935
- Part 4: #7970
- Part 5: #8064
- Part 6: #8083
- Part 7: #8092
- Part 8 Group 1: #8253
- Part 8 Group 2: #8258
- Part 8 Group 3: #8304
- Part 8 Group 4: #8314
- Part 8 Group 5: #8329
- Part 8 Group 6: #8344
- Part 8 Group 7: #8459
- Part 8 Group 8: #8496
- Part 9: #8498
- Part 10: #8499

---------

Co-authored-by: Comfy Org PR Bot <snomiao+comfy-pr@gmail.com>
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-05 16:29:28 -08:00
Johnpaul Chiwetelu
7f81e1afac ci: filter snapshot update job to only run @screenshot tagged tests (#8629)
## Summary
- Adds `--grep @screenshot` to the Playwright command in the
update-snapshots CI workflow
- Skips ~146 non-screenshot tests that don't produce any snapshot files,
reducing CI time and resource usage

## Details
All tests that call `toHaveScreenshot` are already tagged with
`@screenshot` (either at the `test.describe` or individual `test`
level). The snapshot update job was previously running every test
unnecessarily.

The `--grep` CLI flag is ANDed with each project's existing
`grep`/`grepInvert` settings, so all projects continue to work
correctly:
- `chromium`: `@screenshot` AND NOT `@mobile`
- `chromium-2x`: `@screenshot` AND `@2x`
- `mobile-chrome`: `@screenshot` AND `@mobile`

## Test plan
- [x] Trigger the update-snapshots workflow on a PR with the "New
Browser Test Expectations" label and verify only screenshot-tagged tests
run
- [x] Verify snapshot files are still correctly updated
2026-02-05 15:18:21 -08:00
AustinMroz
e26283e754 Revert delay of layout initialization (#8619)
#7591 added a one tick delay to layout initialization in an attempt to
resolve some layouting discrepancies. However, it appears to have
reintroduced node scaling issues and introduced a new bug that prevents
cloning nodes with alt+drag in vue.

Alternatives methods of resolving the original issue are being
investigated, but this change was causing more harm than good.

The prior PR included other changes (like a testing fix). Those changes
remain beneficial and do not need to be reverted.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8619-Revert-delay-of-layout-initialization-2fe6d73d365081fc9111c9457ea9752d)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-05 09:17:41 -08:00
Jin Yi
1ca6e57ac4 fix: skip node replacement API call when feature is disabled (#8618) 2026-02-05 16:37:18 +09:00
Johnpaul Chiwetelu
3adecc4ded fix: prevent duplicate context menu items by using content-based comparison (#8602)
## Summary
- Switches from reference-based to content-based duplicate detection for
context menu items
- Fixes cases where extensions create duplicate menu items (different
objects with same content)
- Improves removal detection accuracy by comparing content strings
instead of object references

## Details
The previous implementation compared menu items by object reference,
which would miss duplicates when extensions added new objects with the
same content. This change:
- Creates Sets of content strings from menu items for efficient
duplicate detection
- Filters additions by checking if their content already exists
- Provides accurate count of removed items through content comparison

## Test plan
- [x] Unit tests pass (`pnpm test:unit
src/lib/litegraph/src/contextMenuCompat.test.ts`)
- [x] TypeScript compilation succeeds (`pnpm typecheck`)
- [x] Linting passes (`pnpm lint`)
- [x] Pre-commit hooks pass

## Before
<img width="633" height="1316" alt="Screenshot 2026-02-04 045422"
src="https://github.com/user-attachments/assets/972871e2-1fd6-45a4-bb6c-9ce73ce7aed7"
/>


## After
<img width="531" height="854" alt="Screenshot 2026-02-04 045918"
src="https://github.com/user-attachments/assets/977bed37-dfb8-41d7-b659-88c477ff8a02"
/>

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Bug Fixes**
* Improved context menu compatibility: more accurate detection of
added/removed menu items and clearer warnings when items are removed.
* **Refactor**
* Updated numeric input formatting to use locale-aware formatting logic,
preserving grouping and precision behavior.
* **Tests**
* Added a test ensuring legacy menu extraction handles items with
undefined content correctly.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8602-fix-prevent-duplicate-context-menu-items-by-using-content-based-comparison-2fd6d73d36508197aa74c3409c7425fa)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Terry Jia <terryjia88@gmail.com>
2026-02-04 20:06:58 -08:00
Comfy Org PR Bot
7404756b6d 1.39.7 (#8614)
Patch version increment to 1.39.7

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8614-1-39-7-2fe6d73d365081449fa7cc9143adf0fa)
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-04 18:38:15 -08:00
Christian Byrne
7c6e2d2c7a fix: disable CodeRabbit high_level_summary to stop PR description auto-updates (#8615)
## Summary

Disables the `high_level_summary` feature in CodeRabbit's configuration.
This feature automatically modifies the PR description every time a
commit is pushed, which was causing frustration for developers.

## Changes

- Set `high_level_summary: false` in `.coderabbit.yaml`

## Context

Discussed in #frontend-code-reviews Slack channel. The team agreed that
having PR descriptions automatically modified with every commit push is
not desirable behavior.

---

Fixes
https://www.notion.so/comfy-org/Ops-Disable-high_level_summary-in-ComfyUI_frontend-coderabbit-yaml-2fd6d73d3650812b9eb8d6680fa11932

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8615-fix-disable-CodeRabbit-high_level_summary-to-stop-PR-description-auto-updates-2fe6d73d36508112ac16f5df51845fcd)
by [Unito](https://www.unito.io)
2026-02-04 18:37:44 -08:00
pythongosssss
6feb2022a4 Add support for search aliases on subgraphs (#8608)
## Summary

- add commands for setting search aliases and description when in
subgraph
- in future we can add these fields to the dialog when publishing a
subgraph
- map workflow extra metadata on save/load from from/to subgraph node to
allow access via `canvas.subgraph.extra`

## Changes

**What**: 
- new core commands for Comfy.Subgraph.SetSearchAliases &
Comfy.Subgraph.SetDescription to be called when in a subgraph context
- update Publish command to allow command metadata arg for name
- update test executeCommand to allow passing metadata arg

## Review Focus

- When saving a subgraph, the outer workflow "wrapper" is created at the
point of publishing. So unlike a normal workflow `extra` property that
is available at any point, for a subgraph this is not accessible.
To workaround this, the `extra` property that exists on the inner
subgraph node is copied to the top level on save, and restored on load
so extra properties can be set via `canvas.subgraph.extra`.
- I have kept the existing naming format matching `BlueprintDescription`
for `BlueprintSearchAliases` but i'm not sure if the description was
ever used before

## Screenshots


https://github.com/user-attachments/assets/4d4df9c1-2281-4589-aa56-ab07cdecd353

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8608-Add-support-for-search-aliases-on-subgraphs-2fd6d73d365081d083caebd6befcacdd)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Set subgraph search aliases (comma-separated) and descriptions;
aliases enable discovery by alternative names.
  * Publish subgraphs using a provided name.
* Node definitions now support search aliases so nodes can be found by
alternate names.
  * UI strings added for entering descriptions and search aliases.

* **Tests**
* Added end-to-end and unit tests covering aliases, descriptions, search
behavior, publishing, and persistence.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-04 12:52:30 -08:00
Alexander Brown
b8287f6c2f Update manifest.json to remove color properties (#8612)
Removed background and theme colors from manifest.



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Removed custom color theme settings from the app's configuration. The
app's display mode and all other properties remain unchanged.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-04 12:25:28 -08:00
Luke Mino-Altherr
036675bb49 fix: Safari compatibility issues in Secrets panel dialog (#8610)
## Summary
Fixes multiple Safari-specific issues in the Secrets panel dialog:

1. **Dropdown not opening** - Safari has issues with click events on
portaled content inside dialogs. Added `disablePortal` prop to
`SelectContent` to render content inline.
2. **Close button focused on open** - Added `autofocus` to
`SelectTrigger` so focus goes to the first form field.
3. **Buttons not focusable** - Safari doesn't focus buttons on click by
default. Added `tabindex="0"` to Cancel/Save buttons.

## Changes
- `SelectContent.vue`: Added `disablePortal` prop with explanatory
comment linking to upstream issue
- `SecretFormDialog.vue`: Applied Safari workarounds

## References
- https://github.com/chakra-ui/ark/issues/1782 (Portal issue in Safari)
- https://mayank.co/blog/safari-focus/ (Button focus behavior)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added option to disable portal rendering in select components for
greater control over component behavior.

* **Bug Fixes**
* Improved keyboard accessibility in form dialogs with enhanced focus
management, auto-focus on select triggers, and optimized tab navigation
for action buttons.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Amp <amp@ampcode.com>
2026-02-04 19:58:40 +00:00
Terry Jia
507500a9d7 fix: use Intl.NumberFormat instead of vue-i18n n() for number formatting (#8600)
## Summary

vue-i18n's n() function does not support passing
Intl.NumberFormatOptions directly as the second argument. When widgets
without precision defined (e.g., KJNodes' CreateShapeImageOnPath) were
rendered, the n() function threw a SyntaxError in parseNumberArgs.

Replace n() with native Intl.NumberFormat which properly supports
NumberFormatOptions while still using the i18n locale for localization.

## Screenshots (if applicable)
before

https://github.com/user-attachments/assets/eb48379b-fe45-4f1c-a674-b92130f0fcac

after

https://github.com/user-attachments/assets/1abcd2da-ad8b-4432-831a-2a7e91f375a5



<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
* Enhanced number input widgets with improved locale-aware number
formatting, ensuring proper decimal and group separator display based on
user locale settings.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8600-fix-use-Intl-NumberFormat-instead-of-vue-i18n-n-for-number-formatting-2fd6d73d365081b78509e87d516c6067)
by [Unito](https://www.unito.io)
2026-02-03 20:57:49 -05:00
Comfy Org PR Bot
6499eda004 1.39.6 (#8595)
Patch version increment to 1.39.6

**Base branch:** `main`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
  * Version bumped to 1.39.6.

* **New Features**
* Added multilingual UI strings for secrets management, asset import
errors, app-mode prompts, and HitPaw tools.
* New node types for image/video/audio workflows and expanded export
formats (GLB/FBX/OBJ) for 3D/model outputs.

* **Bug Fixes / Removals**
* Removed the "Open 3D Viewer (Beta) for Selected Node" menu entry and
related 3D viewer settings.
* Added setting to enable automatic node replacement when mappings
exist.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8595-1-39-6-2fd6d73d3650818cba9cffca313909e8)
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-03 17:27:52 -08:00
Christian Byrne
a7e4a86ec8 fix: correct #visibleReroutes to _visibleReroutes typo (#8597)
Fixes build failure on main caused by merge conflict typo.

- `#visibleReroutes` → `_visibleReroutes` on line 5644 of
LGraphCanvas.ts

Fixes
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/21653103679/job/62422017295

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
  * Internal code style adjustment with no user-facing impact.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8597-fix-correct-visibleReroutes-to-_visibleReroutes-typo-2fd6d73d365081ce9202d070f80fc5c3)
by [Unito](https://www.unito.io)
2026-02-03 16:54:00 -08:00
Christian Byrne
ffc0bf00ef fix: make SubgraphNode.graph nullable to allow proper cleanup (#8180)
## Summary

Fix `SubgraphNode.graph` property to match `LGraphNode` lifecycle
contract. Previously declared as `override readonly graph` via
constructor parameter promotion, which prevented `LGraph.remove()` from
setting `node.graph = null`.

## Changes

- Remove `readonly` from `SubgraphNode.graph` constructor parameter
- Add `override graph: GraphOrSubgraph | null` as class property  
- Add `NullGraphError` guard in `rootGraph` getter with node ID for
debugging
- Add null guards in `ExecutableNodeDTO.resolveInput` and
`imagePreviewStore.revokeSubgraphPreviews`
- Add test verifying `rootGraph` throws after node removal

## Testing

- Existing subgraph tests pass
- New test confirms `NullGraphError` is thrown when accessing
`rootGraph` on removed node

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-02-03 16:18:48 -08:00
Christian Byrne
66e776774a feat: add default keybindings for toggle mode and assets panel (#8593)
## Summary
Add default keyboard shortcuts for mode toggle and assets panel access.

## Changes
- **Ctrl+Shift+A**: Toggle between Simple Mode and Graph Mode
(`Comfy.ToggleLinear`)
- **A**: Open/toggle assets panel (`Workspace.ToggleSidebarTab.assets`)
- Show keybinding in ModeToggle button tooltip (e.g. "Simple Mode
(Ctrl+Shift+A)")

## Keybinding Rationale
- `A` follows the existing pattern: `w` (workflows), `n` (node-lib), `m`
(model-lib)
- `Ctrl+Shift+A` chosen because:
  - Ctrl+Shift is the standard modifier pattern for toggle commands
  - "A" mnemonic for "App mode"
  - Does not conflict with existing keybindings

## Testing
- Verified typecheck passes
- Verified lint passes

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Tooltips now show dynamic keyboard shortcut suffixes for mode and tab
controls.
  * Added keyboard shortcut: A — toggles the assets sidebar.
  * Added keyboard shortcut: Ctrl+Shift+A — toggles linear mode.

* **Localization**
* Added a localized shortcut suffix template so displayed shortcuts
respect translations.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8593-feat-add-default-keybindings-for-toggle-mode-and-assets-panel-2fc6d73d36508172bd6ed3378f43de55)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-02-03 16:06:33 -08:00
Christian Byrne
f98232c272 fix: suppress link rendering during slot sync after graph reconfigure (#8367)
## Description

Fixes link alignment issues after undo/redo operations in Vue Nodes 2.0.
When multiple connections exist from the same output to different nodes,
performing an undo would cause the connections to become misaligned with
their inputs.

## Root Cause

When undo triggers `loadGraphData`, the graph is reconfigured and Vue
node components are destroyed and recreated. The new slot elements mount
and schedule RAF-batched position syncs via `scheduleSlotLayoutSync`.
However, links are drawn **before** the RAF batch completes, causing
`getSlotPosition()` to return stale/missing positions.

## Solution

- Export a new `flushPendingSlotLayoutSyncs()` function from
`useSlotElementTracking.ts`
- Create a `useGraphConfigureSlotSync` composable that flushes pending
syncs after graph configuration
- Integrate the flush into `addAfterConfigureHandler` in `app.ts`,
called after `onAfterGraphConfigured`
- Force canvas redraw after flushing to render links with correct
positions

## Testing

- Added unit tests for `flushPendingSlotLayoutSyncs`
- Added unit tests for `useGraphConfigureSlotSync` composable
- Manual verification: connections now align correctly after undo/redo
operations

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8367-fix-flush-pending-slot-layout-syncs-after-graph-configure-2f66d73d365081a3a564fac96c44a048)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-03 15:56:31 -08:00
Christian Byrne
278d491030 refactor: move ellipsis and punctuation into i18n translation strings (#8573)
## Summary

Move ellipsis and punctuation characters into i18n translation strings
for proper internationalization support.

## Changes

- Add 12 new translation keys with punctuation included:
- Placeholder keys with trailing ellipsis (e.g.,
`searchNodesPlaceholder: "Search Nodes..."`)
  - `downloadWithSize` with interpolation: `"Download ({size})"`
  - `completedWithCheckmark`: `"Completed ✓"`
- Prompt keys with colons (e.g., `enterNewNamePrompt: "Enter new
name:"`)
- Update 20 files to use new translation keys instead of string
concatenation

## Review Focus

This eliminates string concatenation patterns like `$t('key') + '...'`
that break proper internationalization, since different languages may
use different punctuation or may not need ellipsis/colons in the same
contexts.

Fixes #7333


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Standardized localization across the app: unified search placeholders
and input hints; updated dialog prompt texts for renaming,
saving/exporting, and related prompts.
* **New Features**
  * Download buttons now show file size via localized text.
  * Completed status displays a localized label with a checkmark.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8573-refactor-move-ellipsis-and-punctuation-into-i18n-translation-strings-2fc6d73d365081828ad3f257bcac7799)
by [Unito](https://www.unito.io)
2026-02-03 15:50:18 -08:00
Christian Byrne
4b43eb5fff refactor: use isActiveJobState instead of duplicated cancellableStates (#8572)
## Summary

Eliminates code duplication by replacing local `cancellableStates`
arrays with the centralized `isActiveJobState` utility function.

## Changes

- Remove duplicated `cancellableStates: JobState[] = ['pending',
'initialization', 'running']` from:
  - `src/composables/queue/useJobActions.ts`
  - `src/storybook/mocks/useJobActions.ts`
- Import and use `isActiveJobState` from `src/utils/queueUtil.ts`
instead

## Testing

- Typecheck passes
- Lint passes
- Related queue tests pass

Fixes #7947

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8572-refactor-use-isActiveJobState-instead-of-duplicated-cancellableStates-2fc6d73d365081f89decfc869fa952a0)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Refactor**
  * Internal code improvements to simplify job state management logic.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-03 15:49:11 -08:00
Christian Byrne
67107d6f56 fix: use consistent chevron icon in Vue node dropdown widgets (#8576)
## Summary

Fixes styling inconsistency where Vue node select widget chevrons look
different between dropdown implementations.

## Changes

- **What**: Added `#dropdownicon` slot to `WidgetSelectDefault.vue`
using Lucide chevron icon with
`text-component-node-foreground-secondary` styling, matching
`FormDropdownInput.vue`

## Review Focus

Both dropdown implementations now use identical chevron icons with
consistent sizing (`size-4`) and color tokens.

Fixes #COM-11645


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Style**
* Updated select/dropdown appearance: replaced the previous icon
rendering with a customizable dropdown icon slot, improving visual
consistency and allowing a custom icon to display in select controls
across the UI. This change affects only presentation—no behavior or data
handling was altered.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8576-fix-use-consistent-chevron-icon-in-Vue-node-dropdown-widgets-2fc6d73d3650814e923bcbee83fae498)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
2026-02-03 15:48:25 -08:00
Christian Byrne
32da91ea32 perf: code-split xterm bundle and gate terminal features for cloud (#8528)
## Summary

Code-splits the ~400KB xterm bundle and excludes terminal features from
cloud distribution where they are not needed.

### Changes

- **bottomPanelStore.ts**: Gate terminal tab registration behind
`__DISTRIBUTION__ !== 'cloud'` check with dynamic import, enabling
tree-shaking
- **keybindingService.ts**: Skip logs-terminal keybinding registration
for cloud distribution
- **vite.config.mts**: Add `vendor-xterm` code splitting group

### Bundle Impact

| Distribution | xterm in bundle | Terminal tabs |
|--------------|-----------------|---------------|
| cloud |  No (~400KB saved) | None |
| localhost |  Yes (vendor-xterm chunk) | Logs terminal |
| desktop |  Yes (vendor-xterm chunk) | Logs + Command terminal |

### Verification Script

Run locally to verify xterm exclusion works correctly:

```bash
#!/bin/bash
set -e

echo "=== Verifying xterm bundle exclusion ==="
echo

# Clear Nx cache to ensure fresh builds
pnpm nx reset 2>/dev/null

# Build cloud distribution
echo "Building CLOUD distribution..."
rm -rf dist
DISTRIBUTION=cloud pnpm build --mode production 2>/dev/null

echo "Cloud build - checking for xterm:"
if grep -r "xterm" dist/assets/*.js >/dev/null 2>&1; then
  echo "   FAIL: xterm found in cloud bundle"
  grep -l "xterm" dist/assets/*.js | head -5
else
  echo "   PASS: xterm NOT in cloud bundle"
fi
echo

# Build localhost distribution
echo "Building LOCALHOST distribution..."
pnpm nx reset 2>/dev/null
rm -rf dist
DISTRIBUTION=localhost pnpm build --mode production 2>/dev/null

echo "Localhost build - checking for xterm:"
if grep -r "xterm" dist/assets/*.js >/dev/null 2>&1; then
  echo "   PASS: xterm found in localhost bundle"
  echo "  Files containing xterm:"
  grep -l "xterm" dist/assets/*.js | while read f; do
    size=$(wc -c < "$f")
    echo "    $(basename $f) ($(numfmt --to=iec $size))"
  done
else
  echo "   FAIL: xterm NOT in localhost bundle"
fi
echo

echo "=== Verification complete ==="
```

**Note**: Nx cache must be reset between builds with different
`DISTRIBUTION` values or it may return stale results.

## Test Plan

- [x] Quality gates pass (typecheck, lint, format, tests)
- [x] Cloud build verified: xterm NOT present
- [x] Localhost build verified: xterm present as vendor-xterm chunk

Fixes COM-14129

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8528-perf-code-split-xterm-bundle-and-gate-terminal-features-for-cloud-2fa6d73d365081a093ecc74ca7ce6e7c)
by [Unito](https://www.unito.io)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Terminal tab declarations adjusted (no user-visible behavior change).
* Bottom panel tabs now load asynchronously; panel toggle falls back to
shortcuts when terminal tabs aren’t loaded.

* **Chores**
* Terminal tabs and related keybindings suppressed in cloud deployments.
  * Core commands wired to a new utility to support widget promotion.

* **Tests**
* E2E tests updated to handle async terminal-tab loading and minor test
cleanup.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-02-03 15:15:37 -08:00
Christian Byrne
cfc24e3567 docs: audit and update litegraph documentation (#8588)
## Summary

Audits and updates litegraph documentation files that came from the old
litegraph.js repo merged via git subtree (per #8341 discussion).

## Changes

- **Delete** `CONTRIBUTING.md` - completely outdated, referenced
original jagenjo/litegraph.js from 2020
- **Fix** `API.md` - remove Subgraph from "Removed public interfaces"
(actively used in 25+ files)
- **Update** `README.md` - replace standalone releasing instructions
with note about embedded subtree workflow
- **Fix** `AGENTS.md` and `__fixtures__/README.md` - correct import path
typos (`./fixtures/` → `./__fixtures__/`)

Fixes #8347

cc @DrJKL

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Documentation

* Updated release information to clarify that the library is embedded
via git subtree and managed through the parent repository's release
process
* Removed contribution guidelines documentation file
* Updated API documentation by removing Subgraph from the list of
removed public interfaces
* Updated documentation examples to align with current project structure

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8588-docs-audit-and-update-litegraph-documentation-2fc6d73d365081fca335c78ce8493d3e)
by [Unito](https://www.unito.io)
2026-02-03 15:07:43 -08:00
Christian Byrne
eb1d08e9fe feat: enable CodeRabbit reviews on draft PRs (#8587)
Enable CodeRabbit to automatically review draft pull requests.

## Changes
- Added `reviews.auto_review.drafts: true` to `.coderabbit.yaml`

This allows getting early feedback from CodeRabbit while PRs are still
in draft status.

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Enabled automatic review drafting to streamline the review process.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8587-feat-enable-CodeRabbit-reviews-on-draft-PRs-2fc6d73d365081ea8f1bcae4a2a8277b)
by [Unito](https://www.unito.io)
2026-02-03 13:41:29 -08:00
Christian Byrne
a350b59361 feat: Add educational hint to favorites empty state (#8559)
## Summary

Adds an educational hint to the favorites empty state to teach users how
to favorite inputs.

## Changes

- Add new i18n key `favoritesNoneHint` with text "In the Parameters tab,
click ⋮ on any input to add it here"
- Update empty state template to show hint below the existing
description
- Hint is hidden during search to avoid redundant messaging

## Before/After

**Before:** "Inputs you favorite will show up here"

**After:**
- "Inputs you favorite will show up here"
- "In the Parameters tab, click ⋮ on any input to add it here*

## Context

Per Slack discussion, uses in-app education only (no external docs link)
to ensure text matches the user's specific version of the feature.

Fixes #2fa6d73d

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8559-feat-Add-educational-hint-to-favorites-empty-state-2fc6d73d365081f5827dfafcc1ece95c)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Improved empty state in the Parameters panel: message is now two
paragraphs and shows an additional hint (with an icon) when not
searching.
* **Localization**
* Added a new localized hint that explains how to add inputs to
favorites, shown in the Parameters panel empty state.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Rizumu Ayaka <rizumu@ayaka.moe>
2026-02-03 13:24:52 -08:00
Christian Byrne
1031af0ad4 feat: update user-facing 'Simple Mode' terminology to 'App Mode' (#8590)
## Summary
Updates all user-facing instances of 'Simple Mode' / 'Linear Mode' to
'App Mode' as discussed in PR #8562.

## Changes
- Updated command label in `useCoreCommands.ts`
- Updated `src/locales/en/commands.json`
- Updated all 12 locale `main.json` files with:
  - `linearMode.linearMode` → "App Mode" (translated)
  - `linearMode.beta` → "App Mode in Beta - Feedback" (translated)
  - `Toggle Simple Mode` command → `Toggle App Mode` (translated)

## Languages Updated
en, zh, zh-TW, ja, ko, fr, es, pt-BR, ru, tr, ar, fa

## Notes
Internal variable names (`linearMode`, `canvasStore.linearMode`) remain
unchanged to avoid breaking changes.

Fixes #8577

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Style**
* Renamed application terminology from "Simple Mode" to "App Mode" in
user interface labels, mode descriptions, and mode toggle command.
Updates applied consistently across all 12 supported language
translations to ensure consistent terminology throughout the
application.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8590-feat-update-user-facing-Simple-Mode-terminology-to-App-Mode-2fc6d73d36508160a19af70b228b0a7e)
by [Unito](https://www.unito.io)
2026-02-03 13:06:25 -08:00
Christian Byrne
4d8254740d refactor: replace type catch-all with generic ComponentAttrs in showLayoutDialog (#8571)
## Summary

Replaces `Record<string, unknown>` catch-all with properly typed generic
props in `showLayoutDialog`, improving type safety.

## Changes

- Made `showLayoutDialog` generic over component type `C extends
Component`
- Changed props type from `{ onClose: () => void } & Record<string,
unknown>` to `ComponentAttrs<C> & { onClose: () => void }`

This matches existing patterns used elsewhere in `dialogService.ts`
(e.g., `showLoadWorkflowWarning`, `showMissingModelsWarning`).

Fixes #8102

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Refactor**
* Dialog handling updated to infer props per component, resulting in
more consistent and reliable dialog behavior and fewer runtime prop
mismatches when opening dialogs. Developers will have clearer, safer
dialog configuration, while end users should experience steadier dialog
interactions. No visible UI changes are expected.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8571-fix-replace-type-catch-all-with-generic-ComponentAttrs-in-showLayoutDialog-2fc6d73d36508104bf60d27c9b94af69)
by [Unito](https://www.unito.io)
2026-02-03 13:05:13 -08:00
Alexander Brown
f2d5bfab73 test(browser): refactor browser tests for reliability and maintainability (#8510)
## Summary

Major refactoring of browser tests to improve reliability,
maintainability, and type safety.

## Changes

### Test Infrastructure Decomposition
- Decomposed `ComfyPage.ts` (~1000 lines) into focused helpers:
- `CanvasHelper`, `DebugHelper`, `SubgraphHelper`,
`NodeOperationsHelper`
- `SettingsHelper`, `WorkflowHelper`, `ClipboardHelper`,
`KeyboardHelper`
- Created `ContextMenu` page object, `BaseDialog` base class, and
`BottomPanel` page object
- Extracted `DefaultGraphPositions` constants

### Locator Stability
- Added `data-testid` attributes to Vue components (sidebar, dialogs,
node library)
- Created centralized `selectors.ts` with test ID constants
- Replaced fragile CSS selectors (`.nth()`, `:nth-child()`) with
`getByTestId`/`getByRole`

### Performance & Reliability
- Removed `setTimeout` anti-patterns (replaced with `waitForFunction`)
- Replaced `waitForTimeout` with retrying assertions
- Replaced hardcoded coordinates with computed `NodeReference` positions
- Enforced LF line endings for all text files

### Type Safety
- Enabled `no-explicit-any` lint rule for browser_tests via oxlint
- Purged `as any` casts from browser_tests
- Added Window type augmentation for standardized window access
- Added proper type annotations throughout

### Bug Fixes
- Restored `ExtensionManager` API contract
- Removed test-only settings from production schema
- Fixed flaky selectors and missing test setup

## Testing
- All browser tests pass
- Typecheck passes


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Tests**
* Overhauled browser E2E test infrastructure with many new
helpers/fixtures, updated test APIs, and CI test container image bumped
for consistency.

* **Chores**
* Standardized line endings and applied stricter lint rules for browser
tests; workspace dependency version updated.

* **Documentation**
* Updated Playwright and TypeScript testing guidance and test-run
commands.

* **UI**
* Added stable data-testids to multiple components to improve
testability.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-02-03 12:29:40 -08:00
Jin Yi
eb14a2947f style: scale down node previews in manager info panel (#8579)
## Summary
Scale down node previews in the manager info panel to 75% for better
visual fit.

## Changes
- Add 75% scale to node preview items in NodesTabPanel
- Add gap between scaled items for better spacing

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Style**
* Enhanced the visual layout of node items in the manager panel with
improved spacing and individual item scaling for better visual
presentation.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8579-style-scale-down-node-previews-in-manager-info-panel-2fc6d73d36508176a307d980cd3b9329)
by [Unito](https://www.unito.io)
2026-02-03 11:41:19 -08:00
Johnpaul Chiwetelu
b2c8ea3e50 refactor: use emit events pattern instead of mutating props in widget components (#8549) 2026-02-03 11:21:14 +01:00
Terry Jia
f9af2cc4bd feat: add aspect ratio lock for ImageCrop widget (#8533)
## Summary

Add ratio preset dropdown (1:1, 3:4, 4:3, 16:9, 9:16, custom) and lock
toggle to the ImageCrop widget. When locked, only corner handles are
shown and resizing maintains the selected aspect ratio using diagonal
projection for smooth, jump-free interaction.
Locking with custom selected captures the current crop's ratio.

## Screenshots (if applicable)


https://github.com/user-attachments/assets/a7b5c0a0-c18c-4785-940f-59793702e892

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8533-feat-add-aspect-ratio-lock-for-ImageCrop-widget-2fa6d73d365081a98546e43ed0d7d4fe)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Image crop widget adds a ratio selector with preset and custom
options, plus a lock/unlock toggle to preserve aspect while cropping.
* Constrained corner-resize behavior enforces locked aspect ratios
during resizing.

* **Documentation**
* Added UI text entries for ratio controls (labels for ratio,
lock/unlock, custom).
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-03 04:24:33 -05:00
Christian Byrne
a3fba58c79 refactor: move cancellable states to module-level constant (#8570)
## Summary

Moves static data outside the `useJobActions` composable to
module-level.

## Changes

- **What**: Extracted `cancellableStates` array to a module-level
`CANCELLABLE_STATES` constant. The `cancelAction` object remains inside
the composable because it depends on `t()` for i18n reactivity.

## Review Focus

This is a pure refactor with no behavior change - the constant is now
allocated once per module instead of per-composable-call.

Fixes #7946

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8570-refactor-move-cancellable-states-to-module-level-constant-2fc6d73d3650814fa1fefa172f7ace2f)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
* Internal code refactoring to improve maintainability of job
cancellation logic.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-02 21:46:12 -08:00
Christian Byrne
ae7728cd91 feat: add welcome message for Linear Mode (#8562)
## Summary

Adds a welcome/onboarding message for App Mode (Linear Mode) that
displays when no workflow output is selected, targeting first-time
users.


| Before | After |
|
---------------------------------------------------------------------------------------------------------------------------------------------
|
---------------------------------------------------------------------------------------------------------------------------------------------
|
| <img width="1914" height="1084" alt="Screenshot from 2026-02-02
20-47-30"
src="https://github.com/user-attachments/assets/f3693422-b6ee-496a-a354-d9cede501330"
/> | <img width="1919" height="1085" alt="Screenshot from 2026-02-02
21-32-20"
src="https://github.com/user-attachments/assets/dbfda4d0-251c-4099-ad36-3d7c0220d264"
/> |


## Changes

- **LinearWelcome.vue**: New component with 4 sections explaining App
Mode interface, sharing workflows, and widget control
- **LinearPreview.vue**: Replace dimmed logo fallback with LinearWelcome
component
- **i18n**: Add `linearMode.welcome.*` translation keys

## Review Focus

- Copy targets users who have never used ComfyUI—assumes no knowledge of
nodes/graphs
- Component uses semantic Tailwind classes from design system

Fixes COM-14274

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8562-feat-add-welcome-message-for-Linear-Mode-2fc6d73d365081b2b736f211152e1cce)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Added a comprehensive welcome screen in App Mode that introduces users
to the simplified interface hiding node graphs, explains the layout
structure with outputs and controls, provides instructions for sharing
workflows as simple tools, and guides users on customizing which
settings are visible through widget promotion.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-02 21:44:30 -08:00
Jin Yi
5847071bc1 [feat] Add node replacement store and types (#8364)
## Summary
Add infrastructure for automatic node replacement feature that allows
missing/deprecated nodes to be replaced with their newer equivalents.

## Changes
- **Types**: `NodeReplacement`, `NodeReplacementResponse` types matching
backend API spec (PR #12014)
- **Service**: `fetchNodeReplacements()` API wrapper
- **Store**: `useNodeReplacementStore` with `getReplacementFor()`,
`hasReplacement()`, `isEnabled()`
- **Setting**: `Comfy.NodeReplacement.Enabled` toggle (experimental)
- **Tests**: 11 unit tests covering store functionality

## Related
- Backend PR: Comfy-Org/ComfyUI#12014

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8364-feat-Add-node-replacement-store-and-types-2f66d73d3650816bb771c9cc6a8e1774)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
* Automatic node replacement is now available, allowing missing nodes to
be replaced with their newer equivalents when replacement mappings
exist.
* Added "Enable automatic node replacement" experimental setting in
Workflow preferences (enabled by default).
  * Replacement data is loaded during app initialization.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-03 14:15:05 +09:00
Christian Byrne
cc10684c19 feat: enable linear mode toggle for nightly builds (#8569)
Enables the linear mode toggle for all nightly build users by
short-circuiting the `linearToggleEnabled` feature flag.

## Changes
- Adds `isNightly` check in `linearToggleEnabled` getter  
- Returns `true` for nightly builds, bypassing remote config/server
feature checks
- Adds unit tests for the new behavior

## Reviewers
- @AustinMroz (linear mode maintainer)
- @christian-byrne (isNightly author)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8569-feat-enable-linear-mode-toggle-for-nightly-builds-2fc6d73d3650819681f8dcdc23b6eefe)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **New Features**
  * Enabled linear toggle feature for nightly distribution builds.
* Feature flag system now respects nightly vs. standard build
configurations.

* **Tests**
* Added test coverage for nightly build feature flag behavior and remote
configuration handling.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-02 19:54:50 -08:00
Christian Byrne
380db3ee10 chore: add deprecation warning for legacy node templates extension (#8463)
## Summary

Adds the legacy `nodeTemplates` extension to the `DEPRECATED_FILES` list
in the build plugin, causing a console warning when external extensions
import from this file.

## Changes

Adds `'extensions/core/nodeTemplates'` to `DEPRECATED_FILES` in
`build/plugins/comfyAPIPlugin.ts`.

## Effect

When an external extension imports from
`extensions/core/nodeTemplates.js`, they will see:
```
[ComfyUI Deprecated] Importing from "extensions/core/nodeTemplates.js" is deprecated and will be removed in v1.34.
```

## Related

- Follows pattern from PR #6090 (feat: deprecated API alert)
- Related issue: #4056 (Migrate Node Templates to Workflows)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8463-chore-add-deprecation-warning-for-legacy-node-templates-extension-2f86d73d3650811e9f53fa3e5a572332)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-02-02 19:22:07 -08:00
Alexander Brown
cbdc7d030f feat: code splitting optimization - reduce initial bundle by 36% (#8542)
## Summary

Reduces initial module preload from 12.94 MB to 8.24 MB (-4.7 MB, -36%).

## Changes

- Split vendor chunks for better cache isolation (firebase, sentry,
i18n, zod, etc.)
- Exclude heavy optional chunks from initial preload: THREE.js, xterm,
tiptap, chart.js, yjs
- Lazy load 16 dialog components in dialogService
- Add \endor-yjs\ chunk for CRDT library

## Metrics

| Metric | Before | After |
|--------|--------|-------|
| Initial preload | 12.94 MB | 8.24 MB |
| vendor-other | 4,006 KB | 2,156 KB |
| dialogService | 1,860 KB | 1,304 KB |

All excluded chunks still load on-demand when their features are used.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8542-feat-code-splitting-optimization-reduce-initial-bundle-by-36-2fb6d73d36508146aaf7fdaed3274033)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-02 19:05:28 -08:00
Alexander Brown
21492ecca5 fix: update imports for VueUse v14 compatibility (#8550)
Update imports for VueUse v14 compatibility:
- `toValue` → import from `vue` (dropped from VueUse v14)
- `MaybeRef` type → import from `vue` (dropped from VueUse v14)

These APIs were deprecated in VueUse v12 and removed in v14 in favor of
Vue's native exports.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8550-fix-update-imports-for-VueUse-v14-compatibility-2fb6d73d365081b0bac1d01d4092c176)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Chores**
* Updated VueUse dependencies to newer versions for improved
compatibility
  * Minor package entry reorganization (no functional changes)

* **Refactor**
  * Consolidated imports to use native Vue utilities where applicable

* **Tests**
* Updated unit tests: improved outside-click simulation and cleanup;
migrated tests to use the real store setup for more realistic test
behavior
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-02-02 18:58:14 -08:00
Christian Byrne
22755d2cb2 fix: display active jobs in oldest-first order in media assets panel (#8561)
## Summary

Active jobs (pending/running) in the media assets panel now display in
FIFO order - oldest jobs first (first to be processed at top). This
matches the queue processing order and the old queue panel behavior.

## Changes

- **AssetsSidebarListView.vue**: Add `.toReversed()` to `activeJobItems`
computed to reverse job order for display
- **AssetsSidebarGridView.vue**: Same change for grid view consistency
- **AssetsSidebarListView.test.ts**: Unit test verifying oldest-first
ordering

## Root Cause

PR #8225 changed sorting from `queueIndex` to `createTime` descending in
`useJobList.ts`, which placed newest jobs first. For active jobs, users
expect oldest first (FIFO - first to be processed appears at top).

## Solution

Rather than modifying the shared `useJobList` composable (which serves
both the assets panel and queue overlay), the fix applies
`.toReversed()` at the view layer for the active jobs section only.
This:
- Preserves the newest-first order for completed/history jobs
- Displays active jobs in oldest-first order
- Maintains backward compatibility with the queue overlay

## Testing

- Unit test added to verify FIFO ordering of active jobs
- All existing `useJobList` tests pass

Fixes COM-14151

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Bug Fixes**
* Corrected the display order of active jobs in the sidebar to show jobs
in oldest-first (FIFO) sequence.

* **Tests**
* Added unit tests for the assets sidebar list view to verify job
ordering and filtering behavior.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8561-fix-display-active-jobs-in-oldest-first-order-in-media-assets-panel-2fc6d73d365081c6bf31cb076a8d6014)
by [Unito](https://www.unito.io)
2026-02-02 18:57:42 -08:00
Luke Mino-Altherr
15f4f11ee2 feat: remove obsolete model asset feature flags (#8566)
## Summary
Remove feature flags that are now enabled for all users and clean up
dead code paths.

## Changes

**Removed feature flags:**
- `huggingfaceModelImportEnabled` - always show generic URL input with
both Civitai/HuggingFace support
- `assetDeletionEnabled` - delete button always shown for mutable assets
- `asyncModelUploadEnabled` - always use async upload path (removed sync
upload and file size warnings)

**Kept feature flags:**
- `modelUploadButtonEnabled` - controls upload button visibility (local
vs cloud)
- `privateModelsEnabled` - controls upgrade modal vs upload dialog

**Deleted unused component:**
- `UploadModelUrlInputCivitai.vue` (replaced by generic input)

## Testing
- `pnpm typecheck` passes
- `pnpm lint` passes

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8566-feat-remove-obsolete-model-asset-feature-flags-2fc6d73d365081149ec6e78d995c8a44)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

## Release Notes

* **Improvements**
  * Asset deletion option is now always available
  * Model upload experience simplified with unified import flow
  * Model uploads now consistently use asynchronous processing
  * HuggingFace model imports are now always supported

<!-- end of auto-generated comment: release notes by coderabbit.ai -->

Co-authored-by: Amp <amp@ampcode.com>
2026-02-02 18:53:41 -08:00
Luke Mino-Altherr
bc19bb60fb feat: add user secrets management panel (#8473)
## Summary

Add a Secrets panel to user settings for managing third-party API keys
(HuggingFace, Civitai). Secrets are encrypted server-side; plaintext
values are never returned.

## Changes

- Add `SecretsPanel` to settings for managing third-party API keys
- Create `secretsApi` service following the `workspaceApi` pattern
- Add `SecretListItem` and `SecretFormDialog` components
- Add `user_secrets_enabled` feature flag
- Support HuggingFace and Civitai providers
- Add i18n translations for secrets UI

## Files Added

- `src/platform/secrets/types.ts` - TypeScript types
- `src/platform/secrets/api/secretsApi.ts` - Axios-based API service
- `src/platform/secrets/components/SecretsPanel.vue` - Main settings
panel
- `src/platform/secrets/components/SecretListItem.vue` - Individual
secret row
- `src/platform/secrets/components/SecretFormDialog.vue` - Create/edit
dialog

## Files Modified

- `src/platform/remoteConfig/types.ts` - Add `user_secrets_enabled` flag
type
- `src/composables/useFeatureFlags.ts` - Add flag getter
- `src/platform/settings/composables/useSettingUI.ts` - Integrate
secrets panel
- `src/locales/en/main.json` - Add translations

## Testing

Panel appears in Settings under:
- "Workspace" group when team workspaces is enabled
- "Account" group in legacy mode

Only visible when user is logged in AND `user_secrets_enabled` feature
flag is enabled.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8473-feat-add-user-secrets-management-panel-2f86d73d36508187b4a1ed04ce07ce51)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Secrets management UI in Settings with create/edit/delete for API
keys/secrets; settings dialog entry and contextual Secrets hint in
upload/import flows (feature-flag gated).
* **APIs**
* Added backend-facing secrets CRUD surface and client-side
form/composable support for managing secrets.
* **Localization**
* New English translations for Secrets UI and many expanded asset
import/upload error and hint messages.
* **Tests**
* Comprehensive unit tests for secrets UI, form flows, and composables.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-02-02 17:28:58 -08:00
Christian Byrne
139ee32d78 fix: properly parse PNG iTXt chunks per specification (#8530)
## Summary
Fixes PNG iTXt chunk parsing to comply with the PNG specification.

## Problem
The current iTXt parser incorrectly reads the text content immediately
after the keyword null terminator, but iTXt chunks have additional
fields:
- Compression flag (1 byte)
- Compression method (1 byte)
- Language tag (null-terminated)
- Translated keyword (null-terminated)
- Text content

This caused PNGs that correctly follow the spec to fail loading with
JSON parse errors due to leading null bytes.

## Solution
Skip the compression flag, method, language tag, and translated keyword
fields before reading the text content.

Fixes #8150

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8530-fix-properly-parse-PNG-iTXt-chunks-per-specification-2fa6d73d36508189bef4cc5fa3899096)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-02 17:12:11 -08:00
Comfy Org PR Bot
7928e8797d 1.39.5 (#8535)
Patch version increment to 1.39.5

**Base branch:** `main`

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Added and updated localized UI text across multiple languages: new
assetBrowser.baseModel and sortDefault labels, nodeFilters group (filter
labels/descriptions), templates.logoProviderSeparator, and
legacyManagerSearchTip; unified account-deletion messaging to a
contactSupport entry.
* **Chores**
  * Updated front-end version to 1.39.5.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-02-02 16:20:51 -08:00
Benjamin Lu
cd6cc7a51d fix: make custom nodes checkbox optional (#8546)
## Summary

Lower the barrier for bug reports by making custom-node info optional
rather than a required prerequisite. Many users run custom nodes and can
still hit core or interaction bugs that aren't caused by those nodes;
even when custom nodes are involved, the issue may still be ours, so
this change unblocks reports and feature requests while preserving
context.

## Changes

- **What**: Replace the required "tested with all custom nodes disabled"
checkbox with a neutral "I'm using custom nodes" checkbox to capture
context without blocking submissions.

## Review Focus

Confirm the new checkbox wording captures custom-node context without
discouraging reports from users who can't or shouldn't disable custom
nodes.

<!-- 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-8546-fix-make-custom-nodes-checkbox-optional-2fb6d73d3650816fa743e355f1877de1)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **Documentation**
* Updated the bug report issue template to clarify prerequisites: the
primary prerequisite remains required, while the custom-nodes option is
now optional. This reduces mandatory checklist items and improves
guidance on which setup details are essential for effective bug reports.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-02 13:08:51 -08:00
Benjamin Lu
aa979aa98c Enable CodeRabbit issue enrichment (#8543)
## Summary

Enable CodeRabbit issue enrichment and remove duplicate-check
requirements to lower the barrier for reporting problems.

## Changes

- **What**: Add `.coderabbit.yaml` enabling issue enrichment; remove
dedupe checkboxes from bug and feature issue templates.

## Review Focus

Confirm we want CodeRabbit/Dosu to handle deduplication so users aren’t
blocked by a mandatory “search for duplicates” step.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8543-Enable-CodeRabbit-issue-enrichment-2fb6d73d36508132947acf5f01c9cb22)
by [Unito](https://www.unito.io)


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

* **Chores**
  * Enabled automatic issue enrichment to enhance issue tracking.
* Streamlined issue submission forms by removing prerequisite
verification steps from bug report and feature request templates.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-02-02 12:06:34 -08:00
Christian Byrne
cc0307032a deprecate: add warning for unsupported widgets_up property (#8541)
## Summary
Adds deprecation warning for the `widgets_up` property which has known
layout bugs and is not officially supported.

## Changes
- Added `@deprecated` JSDoc comment on `widgets_up` property
- Added console warning during configure path
- Warning directs users to use `widgets_start_y` or custom `arrange()`
override instead

## Context
The `widgets_up` property is an undocumented legacy LiteGraph feature
that positions widgets at the top of nodes. However, the `arrange()`
method doesn't properly handle slot positioning when this is enabled,
causing widgets and slots to overlap (see #7878).

Rather than fix this edge case in an unsupported feature, we're formally
deprecating it with a warning.

- Closes #7878

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8541-deprecate-add-warning-for-unsupported-widgets_up-property-2fb6d73d365081a89bb6e41038d94455)
by [Unito](https://www.unito.io)
2026-02-02 11:53:28 -08:00
Christian Byrne
bc3b9585f9 fix: localize node definition filter names and descriptions (#8540)
## Summary

Localizes node definition filter names and descriptions using vue-i18n,
following the project's coding guidelines.

## Changes

- Updated `registerCoreNodeDefFilters()` in `src/stores/nodeDefStore.ts`
to use `t()` function calls
- Added translation keys under `nodeFilters` namespace in
`src/locales/en/main.json`

## Affected Filters

| Filter ID | Name |
|-----------|------|
| `core.deprecated` | Hide Deprecated Nodes |
| `core.experimental` | Hide Experimental Nodes |
| `core.dev_only` | Hide Dev-Only Nodes |
| `core.subgraph` | Hide Subgraph Nodes |

Fixes #8390

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8540-fix-localize-node-definition-filter-names-and-descriptions-2fb6d73d365081f29506ed804c233b5a)
by [Unito](https://www.unito.io)
2026-02-02 11:48:00 -08:00
Christian Byrne
1af9e00dd2 fix: add Frame Nodes to core menu items for multi-selection context menu (#8524)
## Summary

Fix "Frame Nodes" option being incorrectly categorized as an extension
item instead of appearing in the core menu section when multiple nodes
are selected.

## Changes

- **What**: Added "Frame Nodes" to `CORE_MENU_ITEMS` set and
`MENU_ORDER` array in contextMenuConverter.ts, and added frame
equivalents to duplicate detection to prevent duplicate menu items

## Review Focus

The fix ensures the Vue-based "Frame Nodes" menu option (i18n key
`g.frameNodes`) is recognized as a core menu item alongside the legacy
LiteGraph "Frame selection" label. Both are now treated as equivalent
for deduplication.

Fixes COM-13922

## Screenshots (if applicable)

N/A - menu item positioning fix

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8524-fix-add-Frame-Nodes-to-core-menu-items-for-multi-selection-context-menu-2fa6d73d365081989799f7bc74c65868)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-02-02 11:46:55 -08:00
Johnpaul Chiwetelu
2f8bd7b04f Road to No Explicit Any Part 9 (#8498)
## Summary

This PR removes `any` types from core source files and replaces them
with proper TypeScript types.

### Key Changes

#### Type Safety Improvements
- Replaced `any` with `unknown`, explicit types, or proper interfaces
across core files
- Introduced new type definitions: `SceneConfig`, `Load3DNode`,
`ElectronWindow`
- Used `Object.assign` instead of `as any` for dynamic property
assignment
- Replaced `as any` casts with proper type assertions

### Files Changed

Source files:
- src/extensions/core/widgetInputs.ts - Removed unnecessary `as any`
cast
- src/platform/cloud/onboarding/auth.ts - Used `Record<string, unknown>`
and Sentry types
- src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.ts -
Used `AuditLog[]` type
- src/platform/workflow/management/stores/workflowStore.ts - Used
`typeof ComfyWorkflow` constructor type
- src/scripts/app.ts - Used `ResultItem[]` for Clipspace images
- src/services/colorPaletteService.ts - Used `Object.assign` instead of
`as any`
- src/services/customerEventsService.ts - Used `unknown` instead of
`any`
- src/services/load3dService.ts - Added proper interface types for
Load3D nodes
- src/types/litegraph-augmentation.d.ts - Used `TWidgetValue[]` type
- src/utils/envUtil.ts - Added ElectronWindow interface
- src/workbench/extensions/manager/stores/comfyManagerStore.ts - Typed
event as `CustomEvent<{ ui_id?: string }>`

### Testing
- All TypeScript type checking passes (`pnpm typecheck`)
- Linting passes without errors (`pnpm lint`)
- Code formatting applied (`pnpm format`)

Part of the "Road to No Explicit Any" initiative.

### Previous PRs in this series:
- Part 2: #7401
- Part 3: #7935
- Part 4: #7970
- Part 5: #8064
- Part 6: #8083
- Part 7: #8092
- Part 8 Group 1: #8253
- Part 8 Group 2: #8258
- Part 8 Group 3: #8304
- Part 8 Group 4: #8314
- Part 8 Group 5: #8329
- Part 8 Group 6: #8344
- Part 8 Group 7: #8459
- Part 8 Group 8: #8496
- Part 9: #8498 (this PR)
2026-02-02 17:30:49 +01:00
Johnpaul Chiwetelu
cfdd002b7c Road to No Explicit Any Part 10 (#8499)
## Summary

This PR removes `any` types from UI component files and replaces them
with proper TypeScript types.

### Key Changes

#### Type Safety Improvements
- Replaced `any` with `unknown`, explicit types, or proper interfaces
across UI components
- Used `ComponentPublicInstance` with explicit method signatures for
component refs
- Used `Record<string, unknown>` for dynamic property access
- Added generics for form components with flexible value types
- Used `CSSProperties` for style objects

### Files Changed

UI Components:
- src/components/common/ComfyImage.vue - Used proper class prop type
- src/components/common/DeviceInfo.vue - Used `string | number` for
formatValue
- src/components/common/FormItem.vue - Used `unknown` for model value
- src/components/common/FormRadioGroup.vue - Added generic type
parameter
- src/components/common/TreeExplorer.vue - Used proper async function
signature
- src/components/custom/widget/WorkflowTemplateSelectorDialog.vue -
Fixed duplicate import
- src/components/graph/CanvasModeSelector.vue - Used
`ComponentPublicInstance` for ref
- src/components/node/NodePreview.vue - Changed `any` to `unknown`
- src/components/queue/job/JobDetailsPopover.vue - Removed unnecessary
casts
- src/components/queue/job/JobFiltersBar.vue - Removed `as any` casts
- src/platform/assets/components/MediaAssetContextMenu.vue - Added
`ContextMenuInstance` type
- src/renderer/extensions/minimap/MiniMapPanel.vue - Used
`CSSProperties`
- src/renderer/extensions/vueNodes/composables/useNodeTooltips.ts -
Added `PrimeVueTooltipElement` interface
-
src/renderer/extensions/vueNodes/widgets/components/form/FormSelectButton.vue
- Used `Record<string, unknown>`
-
src/workbench/extensions/manager/components/manager/infoPanel/tabs/DescriptionTabPanel.vue
- Added `LicenseObject` interface

### Testing
- All TypeScript type checking passes (`pnpm typecheck`)
- Linting passes without errors (`pnpm lint`)

Part of the "Road to No Explicit Any" initiative.

### Previous PRs in this series:
- Part 2: #7401
- Part 3: #7935
- Part 4: #7970
- Part 5: #8064
- Part 6: #8083
- Part 7: #8092
- Part 8 Group 1: #8253
- Part 8 Group 2: #8258
- Part 8 Group 3: #8304
- Part 8 Group 4: #8314
- Part 8 Group 5: #8329
- Part 8 Group 6: #8344
- Part 8 Group 7: #8459
- Part 8 Group 8: #8496
- Part 9: #8498
- Part 10: #8499

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8499-Road-to-No-Explicit-Any-Part-10-2f86d73d365081aab129f165c7d02434)
by [Unito](https://www.unito.io)
2026-02-02 17:16:40 +01:00
Benjamin Lu
7dd3098af5 fix: use NodeId for output key parts (#8547)
## Summary

Use the shared NodeId type for output asset key construction to keep
node id typing consistent across assets and workflow schemas.

## Changes

- **What**: replace the local `string | number` nodeId type in output
key parts with the shared `NodeId` alias

## Review Focus

Type consistency for `nodeId` used in output asset key generation.

## Screenshots (if applicable)

N/A

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8547-fix-use-NodeId-for-output-key-parts-2fb6d73d36508127a022cc6052e5f59e)
by [Unito](https://www.unito.io)
2026-02-02 03:42:07 -08:00
Benjamin Lu
2740c7cdd5 Add expandable output stacks to assets list view (#8283)
Add expandable output stacks to the assets list view.

Monolith ver. of https://github.com/Comfy-Org/ComfyUI_frontend/pull/8298
and its children

List view currently collapses multi-output jobs into a single row, which
makes sibling outputs easy to miss and causes selection/zoom behavior to
drift once items are expanded elsewhere. This change adds a stack toggle
to list rows, expands child outputs derived from job data, and keeps
list-view selection and gallery navigation aligned with the expanded
list. Output mapping and “load full outputs” checks are centralized so
folder view and stacks share the same helper, and job-detail parsing now
yields previewable outputs for the list view. Asset actions now prefer
metadata prompt IDs to support the composite IDs used by stacked
outputs.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8283-Add-expandable-output-stacks-to-assets-list-view-2f16d73d365081a99fc6f1519ac2e57c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Johnpaul Chiwetelu <49923152+Myestery@users.noreply.github.com>
2026-02-02 03:31:01 -08:00
Comfy Org PR Bot
22daf48748 docs: Weekly Documentation Update (#7849)
# Documentation Audit and Corrections

## Summary

- Fixed incorrect test directory reference in AGENTS.md (`tests-ui/` →
removed)
- Updated outdated timestamps in composables and stores READMEs
- Fixed critical workflow name error in create-frontend-release command
- Removed misleading i18n workflow warnings from release documentation

## Changes Made

### AGENTS.md
- **Line 24**: Removed non-existent `tests-ui/` directory from test file
locations
  - Unit/component tests are located in `src/**/*.test.ts` only
- **Line 267**: Removed `tests-ui` from repository navigation guidance

### src/composables/README.md
- **Line 88**: Updated timestamp from "2025-01-30" to "2026-01-30" to
reflect current state

### src/stores/README.md
- **Line 105**: Updated timestamp from "2025-09-01" to "2026-01-29" to
reflect last modification date

### .claude/commands/create-frontend-release.md
- **Line 394**: Fixed critical workflow name error
  - Changed `version-bump.yaml` → `release-version-bump.yaml`
  - This was causing command failures as the workflow file doesn't exist
- **Lines 446-450**: Removed outdated warning about `update-locales`
workflow adding `[skip ci]`
  - No such workflow exists in the repository
- Current i18n workflows are: `i18n-update-core.yaml`,
`i18n-update-custom-nodes.yaml`, `i18n-update-nodes.yaml`
  - None of these add `[skip ci]` to commits
- **Lines 464-465**: Removed duplicate critical warning about
non-existent `update-locales` workflow

## Review Notes

### Documentation Accuracy Verification Process

Conducted comprehensive fact-checking of all documentation against the
current codebase:

1. **Configuration Files**: Verified all referenced config files exist
(vite.config.mts, playwright.config.ts, eslint.config.ts, .oxfmtrc.json,
.oxlintrc.json)
2. **Package Scripts**: Validated all npm/pnpm commands referenced in
documentation
3. **Directory Structure**: Confirmed project structure matches
documented layout
4. **Extension Documentation**: Verified TypeScript interface paths and
external resource links
5. **Testing Documentation**: Confirmed test file patterns and framework
references
6. **Command Documentation**: Validated workflow names and GitHub CLI
commands

### Known Documentation Gaps (Not Addressed)

These items were identified but not changed as they represent
aspirational guidance or current migration paths:

1. **docs/guidance/vue-components.md**: Documents Vue 3.5 prop
destructuring pattern (`const { prop } = defineProps<>()`) while
codebase still uses `withDefaults()` in many files
   - This is intentional - guidance shows preferred pattern for new code
2. **docs/guidance/playwright.md**: Advises against `waitForTimeout` but
4 test files still use it
   - Existing violations are technical debt
3. **AGENTS.md Line 163**: States "Avoid new usage of PrimeVue
components" but some components still use PrimeVue
   - Guidance is for new code; existing usage being gradually migrated
4. **docs/guidance/typescript.md**: Discourages `as any` but 11
instances exist in codebase
   - Known technical debt

### Additional Findings

- All README files in key directories (composables, stores, services,
extensions, testing) are accurate and comprehensive
- External documentation links (Vue, Tailwind, VueUse, etc.) are valid
- Code examples in documentation are syntactically correct
- i18n structure and paths are correctly documented

## Verification Commands

To verify the fixes:

```bash
# Verify tests-ui directory doesn't exist
ls tests-ui 2>&1 | grep "No such file"

# Verify correct workflow file exists
ls .github/workflows/release-version-bump.yaml

# Verify no workflows add [skip ci]
grep -r "skip ci" .github/workflows/*.yaml || echo "None found (expected)"

# Verify test files are in src/
find src -name "*.test.ts" | wc -l  # Should show many test files
```

---------

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-02-02 01:30:34 -08:00
Christian Byrne
2ec954459c fix: show Missing Nodes dialog for missing node prompt errors (#8511)
## Summary
When a workflow is queued with a missing node type, show the Missing
Nodes dialog instead of a generic error toast. This gives users
actionable options like "Open Manager" and "Install All".

## Changes
- Detect `missing_node_type` error from backend in `queuePrompt()` catch
block
- Construct `MissingNodeType` object with class type and contextual hint
- Reuse existing `showMissingNodesError()` to trigger the dialog

## Dependencies
⚠️ **Requires backend PR:**
https://github.com/Comfy-Org/ComfyUI/pull/12177

The backend PR changes the error type from `invalid_prompt` to
`missing_node_type` and adds `extra_info` with node details.

## Related
- Fixes COM-12528
- Addresses ~49 GitHub issues with confusing "missing class_type" errors

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8511-fix-show-Missing-Nodes-dialog-for-missing-node-prompt-errors-2f96d73d3650812b95f0f08e51abaabb)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-02-02 01:28:17 -08:00
Terry Jia
4e9f581625 fix: align BOUNDING_BOX type with backend and target ImageCropV2 node (#8531)
## Summary
- Update imageCrop extension to target new ImageCropV2 comfyClass
- Fix BOUNDING_BOX io_type mismatch (was BOUNDINGBOX, backend sends
BOUNDING_BOX)
- Keep old ImageCrop node_id unchanged for backward compatibility

BE Changes is https://github.com/Comfy-Org/ComfyUI/pull/11594

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8531-fix-align-BOUNDING_BOX-type-with-backend-and-target-ImageCropV2-node-2fa6d73d36508160a06ad06108b8a15e)
by [Unito](https://www.unito.io)
2026-02-02 04:03:55 -05:00
Christian Byrne
d34d6a8a92 feat: show legacy manager search tip in no-results empty state (#8537)
## Summary

Show a non-intrusive tip in the no-results empty state when users search
legacy manager-related terms in the new manager UI.

## Changes

- **What**: Add `useLegacySearchTip` composable that detects when search
query matches legacy manager keywords ("manager", "comfyui-manager",
etc.) and appends a tip to the empty state message suggesting the
`--enable-manager-legacy-ui` flag
- **Integration**: Tip appears as secondary muted text below "Try a
different search" message when no results found
- **Tests**: 9 test cases covering detection logic and edge cases

## Review Focus

- Keyword list covers common legacy manager search terms
- Non-intrusive approach: tip only shows in no-results state, no dismiss
button needed

Fixes COM-12509
2026-02-01 23:43:42 -08:00
Christian Byrne
2a167a675d fix: use fileURL for static template logo index path (#8539)
## Summary

Fix template logo loading by using `api.fileURL()` instead of
`api.fetchApi()` for the static logo index file.

## Problem

`fetchLogoIndex()` was using
`api.fetchApi('/templates/index_logo.json')` which adds an `/api` prefix
to URLs, resulting in requests to `/api/templates/index_logo.json`
instead of `/templates/index_logo.json`.

Since `/templates/index_logo.json` is a static asset file (not an API
endpoint), it should use `api.fileURL()` which doesn't add the `/api`
prefix.

## Changes

- Change `api.fetchApi('/templates/index_logo.json')` to
`fetch(api.fileURL('/templates/index_logo.json'))`

## Testing

- Template logos should now load correctly in the workflow template
selector dialog

Fixes COM-14279

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8539-fix-use-fileURL-for-static-template-logo-index-path-2fb6d73d365081178788dc0cf196ada4)
by [Unito](https://www.unito.io)
2026-02-01 22:42:04 -08:00
Christian Byrne
e1385b0d18 refactor: use singleton mock pattern for useDialogStore in escape test (#8538)
## Summary

Refactors `keybindingService.escape.test.ts` to use the singleton mock
pattern with reactive state, following project testing guidance.

## Changes

- **What**: Replaced `vi.fn(() => ({dialogStack: []}))` anti-pattern
with `reactive()` array defined inline in `vi.mock()` factory. Tests now
use array mutation (`.push()`, `.length = 0`) instead of
`mockReturnValue` calls.

## Review Focus

Pattern aligns with docs/testing/unit-testing.md section "Mocking
Composables with Reactive State".

Fixes #8467

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8538-refactor-use-singleton-mock-pattern-for-useDialogStore-in-escape-test-2fb6d73d3650812a95c6e223e38d50ad)
by [Unito](https://www.unito.io)
2026-02-01 22:09:10 -08:00
Alexander Brown
eaa3ff1579 feat: add ownership and base model filtering, unify asset/dropdown types (#8497)
Add ownership and base model filtering to AssetBrowserModal and
FormDropdown widgets.

## Changes

- **Ownership filter**: Filter by All/My Models/Public Models (uses
`is_immutable` field)
- **Base model filter**: Multi-select filter with Clear Filters button
- **Type unification**: Replace `AssetDropdownItem` with
`FormDropdownItem`
- **Sorting unification**: Extract shared utilities to
`assetSortUtils.ts`
- **UI refactor**: Use `Button` component, Vue 3.5 prop shorthand, i18n
improvements

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-02-01 20:01:18 -08:00
Benjamin Lu
4e20b7522b Forward scroll unless focused (#6597)
## Summary

Forward wheel events to the canvas unless a wheel-capturing element is
focused, and ensure the Load3D scene becomes focusable on pointer
interaction so its wheel zoom/pan works after the user clicks into it.

## Changes

- **What**: gate wheel forwarding on focused capture elements; focus the
Load3D scene container on pointerdown to opt into wheel capture.
- **Dependencies**: none

## Review Focus

- Validate wheel forwarding behavior across focusable inputs vs.
non-focusable capture zones.
- Confirm Load3D zoom/pan only captures wheel after a user click (canvas
pan should still work when merely hovering).

## Screenshots (if applicable)

N/A

---------

Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-02-01 09:32:10 -05:00
Christian Byrne
544ef5bb70 fix: dedupe queueStore.update() to prevent race conditions (#8523)
## Summary

Prevents stale API responses from overwriting fresh queue state during
rapid consecutive `update()` calls.

## Problem

When `queueStore.update()` is called multiple times in quick succession
(e.g., during websocket reconnection), responses could resolve
out-of-order. A stale response resolving after a fresh one would
overwrite the current state with outdated data.

## Solution

Added request ID tracking that:
1. Increments a counter on each `update()` call
2. Guards all state mutations with a staleness check
3. Only sets `isLoading=false` for the latest request

This pattern matches the existing approach in `jobOutputCache.ts`.

## Changes

- `src/stores/queueStore.ts`: Added `updateRequestId` counter and
staleness guards
- `src/stores/queueStore.test.ts`: Added tests for deduplication
behavior

## Testing

- All 49 existing tests pass
- Added 3 new tests for race condition handling:
  - Stale responses are discarded
  - isLoading state is correctly managed
  - Stale request failures don't affect latest state

## Fixes COM-12784

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8523-fix-dedupe-queueStore-update-to-prevent-race-conditions-2fa6d73d3650810daa0dc63af359e1ea)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-01-31 22:01:14 -08:00
Alexander Brown
69129414d0 fix: use PR_GH_TOKEN to trigger e2e after updating expectations (#8525)
## Summary

See https://github.com/Comfy-Org/ComfyUI_frontend/pull/8484

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8525-fix-use-PR_GH_TOKEN-to-trigger-e2e-after-updating-expectations-2fa6d73d3650817c81fccd1cbb3ddadb)
by [Unito](https://www.unito.io)
2026-01-31 21:07:13 -08:00
Christian Byrne
1bbbcfedf0 feat: add provider logo overlays to workflow template thumbnails (#8365)
## Summary

Add support for overlaying provider logos on workflow template
thumbnails at runtime.

## Changes

- **What**: 
  - Add `LogoInfo` interface and `logos` field to `TemplateInfo` type
  - Create `LogoOverlay.vue` component for rendering positioned logos
  - Fetch logo index from `templates/index_logo.json` in store
  - Add `getLogoUrl` helper to `useTemplateWorkflows` composable
  - Integrate `LogoOverlay` into `WorkflowTemplateSelectorDialog`

## Review Focus

- Logo positioning uses Tailwind classes (e.g. `absolute bottom-2
right-2`)
- Supports multiple logos per template with configurable size/opacity
- Gracefully handles missing logos (returns empty string, renders
nothing)
- Templates must explicitly declare logos - no magic inference from
models

## Dependencies

Requires separate PR in workflow_templates repo to:
1. Update `index.schema.json` with logos definition
2. Add `logos` field to templates in `index.json`

## Screenshots (if applicable)

<img width="869" height="719" alt="image"
src="https://github.com/user-attachments/assets/65ed1ee4-fbb4-42c9-95d4-7e37813b3655"
/>


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8365-feat-add-provider-logo-overlays-to-workflow-template-thumbnails-2f66d73d365081309236c6b991cb6f7b)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-31 19:18:13 -08:00
Christian Byrne
4f2872460c fix: remove delete account button and direct users to support (#8515)
## Summary
Reverts PR #5216 delete account functionality. The delete button only
removed Firebase accounts without canceling Stripe subscriptions,
causing orphaned accounts and support issues.

## Changes
- Removed delete account button from UserPanel.vue
- Added text directing users to contact support@comfy.org for account
deletion (clickable mailto: link)
- Cleaned up related code: removed `handleDeleteAccount` from
useCurrentUser.ts, `deleteAccount` from useFirebaseAuthActions.ts,
`_deleteAccount` from firebaseAuthStore.ts
- Updated en/main.json locale with `contactSupport` key using {email}
placeholder

## Testing
- Typecheck and lint pass
- Manual verification: user settings panel shows contact support text
instead of delete button

## Related Issues
Fixes COM-14243

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8515-fix-remove-delete-account-button-and-direct-users-to-support-2fa6d73d3650819dbc83efb41c07a809)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-01-31 19:14:44 -08:00
Comfy Org PR Bot
ea6928268f 1.39.4 (#8513)
Patch version increment to 1.39.4

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8513-1-39-4-2fa6d73d365081f1b8c2d5ae8e764eb3)
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-01-31 18:18:57 -08:00
Christian Byrne
4a85bffb1f fix: node header on preview has a gap on the right (not flush) (#8487)
## Summary

After changes:

[Screencast from 2026-01-30
00-34-59.webm](https://github.com/user-attachments/assets/9111b8ce-936d-4b30-8b56-6f44aabfe009)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8487-fix-node-header-on-preview-has-a-gap-on-the-right-not-flush-2f86d73d365081f38cabfca9ab9e04e4)
by [Unito](https://www.unito.io)
2026-01-31 15:57:38 -08:00
AustinMroz
be7c34e28b Update control_after_generate schema (#8505)
Updates `control_after_generate` in the schema to support specifying the
default control value as a string

See Comfy-Org/ComfyUI#12187

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8505-Update-control_after_generate-schema-2f96d73d365081f9bf73c804072bb415)
by [Unito](https://www.unito.io)
2026-01-30 21:34:50 -08:00
Comfy Org PR Bot
a807986835 1.39.3 (#8500)
Patch version increment to 1.39.3

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8500-1-39-3-2f96d73d365081a9a74fd1fcb7e33a41)
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-01-30 20:30:35 -08:00
Christian Byrne
679288500a fix(cloud): disable legacy node templates feature on cloud (#8462)
## Summary

Disables the legacy "node templates" feature on ComfyUI Cloud. This
feature is accessed via right-clicking on the canvas and is distinct
from both subgraphs and workflow templates.

## Problem

The legacy node templates feature presents false-positive errors on
cloud:
- Errors appear when no existing templates exist initially
- After refresh, workflow progress is lost from workflow tabs
- Node templates fail to delete (except the first one)

## Solution

Conditionally load the `nodeTemplates` extension only for non-cloud
builds by wrapping the import in `if (!isCloud)`.

## Testing

- [ ] Verify right-click canvas menu no longer shows "Node Templates"
submenu on cloud
- [ ] Verify right-click canvas menu no longer shows "Save Selected as
Template" on cloud
- [ ] Verify the feature still works on desktop/OSS builds

## Related

- Refs: COM-14105
- Related issue: #4056 (Migrate Node Templates to Workflows)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8462-fix-cloud-disable-legacy-node-templates-feature-on-cloud-2f86d73d36508162948cc897d4c7ef5e)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-01-30 16:51:31 -08:00
Christian Byrne
34d42311ea fix: add security params to Contact Support window.open call (#8470)
## Summary

Adds `'noopener,noreferrer'` to the `window.open()` call in
`Comfy.ContactSupport` command, aligning with the established codebase
pattern for external links.

## Changes

- Updated `src/composables/useCoreCommands.ts` line 865 to include
security parameters

## Testing

- [x] Typecheck passes
- [x] Lint passes

## Related

- Fixes COM-13631
- Notion: https://www.notion.so/2ee6d73d365081328ae5cc7bce64b310

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8470-fix-add-security-params-to-Contact-Support-window-open-call-2f86d73d365081e38ea1c1ea9c9883f5)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-01-30 16:51:04 -08:00
Christian Byrne
c4e5fc8dbf fix: update reactive ref after merge in imagePreviewStore (#8479)
## Summary
Fix for COM-14110: Preview image does not display new outputs in
vue-nodes.

## Problem
The merge logic in `setOutputsByLocatorId` updated `app.nodeOutputs` but
returned early without updating the reactive `nodeOutputs.value` ref.
This caused Vue components to never receive merged output updates
because only the non-reactive `app.nodeOutputs` was being updated.

## Solution
Added `nodeOutputs.value[nodeLocatorId] = existingOutput` after the
merge loop, before the return statement.

## Testing
- Added 2 unit tests covering the merge behavior
- All 4076 existing unit tests pass
- Typechecks pass
- Lint passes

## Notes
- Related open PRs touching same files: #8143, #8366 - potential minor
conflicts possible

Fixes COM-14110

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8479-fix-update-reactive-ref-after-merge-in-imagePreviewStore-2f86d73d365081f1a145fa5a9782515f)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-01-30 16:49:57 -08:00
Johnpaul Chiwetelu
a64c561a5f Road to No explicit any: Group 8 (part 8) test files (#8496)
## Summary

This PR removes unsafe type assertions ("as unknown as Type") from test
files and improves type safety across the codebase.

### Key Changes

#### Type Safety Improvements
- Removed improper `as unknown as Type` patterns from test files in
Group 8 part 8
- Replaced with proper TypeScript patterns using Pinia store testing
patterns
- Fixed parameter shadowing issue in typeGuardUtil.test.ts (constructor
→ nodeConstructor)
- Fixed stale mock values in useConflictDetection.test.ts using getter
functions
- Refactored useManagerState tests to follow proper Pinia store testing
patterns with createTestingPinia

### Files Changed

Test files (Group 8 part 8 - utils and manager composables):
- src/utils/typeGuardUtil.test.ts - Fixed parameter shadowing
- src/utils/graphTraversalUtil.test.ts - Removed unsafe type assertions
- src/utils/litegraphUtil.test.ts - Improved type handling
- src/workbench/extensions/manager/composables/useManagerState.test.ts -
Complete rewrite using Pinia testing patterns
-
src/workbench/extensions/manager/composables/useConflictDetection.test.ts
- Fixed stale mock values with getters
- src/workbench/extensions/manager/composables/useManagerQueue.test.ts -
Type safety improvements
-
src/workbench/extensions/manager/composables/nodePack/useMissingNodes.test.ts
- Removed unsafe casts
-
src/workbench/extensions/manager/composables/nodePack/usePacksSelection.test.ts
- Type improvements
-
src/workbench/extensions/manager/composables/nodePack/usePacksStatus.test.ts
- Type improvements
- src/workbench/extensions/manager/utils/versionUtil.test.ts - Type
safety fixes

Source files (minor type fixes):
- src/utils/fuseUtil.ts - Type improvements
- src/utils/linkFixer.ts - Type safety fixes
- src/utils/syncUtil.ts - Type improvements
-
src/workbench/extensions/manager/composables/nodePack/useWorkflowPacks.ts
- Type fix
-
src/workbench/extensions/manager/composables/useConflictAcknowledgment.ts
- Type fix

### Testing
- All TypeScript type checking passes (`pnpm typecheck`)
- All affected test files pass (`pnpm test:unit`)
- Linting passes without errors (`pnpm lint`)
- Code formatting applied (`pnpm format`)

Part of the "Road to No Explicit Any" initiative, cleaning up type
casting issues from branch `fix/remove-any-types-part8`.

### Previous PRs in this series:
- Part 2: #7401
- Part 3: #7935
- Part 4: #7970
- Part 5: #8064
- Part 6: #8083
- Part 7: #8092
- Part 8 Group 1: #8253
- Part 8 Group 2: #8258
- Part 8 Group 3: #8304
- Part 8 Group 4: #8314
- Part 8 Group 5: #8329
- Part 8 Group 6: #8344
- Part 8 Group 7: #8459
- Part 8 Group 8: #8496 (this PR)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8496-Road-to-No-explicit-any-Group-8-part-8-test-files-2f86d73d365081f3afdcf8d01fba81e1)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-01-30 22:25:10 +01:00
Alexander Brown
59c58379fe fix: migrate remaining ECMAScript private fields to TypeScript private (#8495)
Migrates remaining `#field` syntax to `private _field` for Vue
reactivity compatibility.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8495-fix-migrate-remaining-ECMAScript-private-fields-to-TypeScript-private-2f86d73d365081ec87afe2273c0ff6eb)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-01-30 13:24:16 -08:00
AustinMroz
6c14ae6f90 Don't bypass subgraph contents with subgraph (#8494)
When bypassing or muting a subgraph the contents are no longer bypassed
along with it. This behaviour was less than ideal because it meant that
toggling the bypass of a single subgraph node twice could change the
behaviour of the node.

It is entirely intended that a subgraph node which is bypassed does not
have it's children execute. As part of testing this behaviour, it was
found that nodes inside of a bypassed subgraph are still considered for
execution even if boundry links are treated as disconnected. The
following example would execute even if the subgraph is muted.
<img width="826" height="476" alt="image"
src="https://github.com/user-attachments/assets/7b282873-e114-494d-b8f1-74c373859151"
/>

To resolve this, the PR does not add the contents of a subgraphNode
which is muted or bypassed to the execution map.

Resolves #8489

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8494-Don-t-bypass-subgraph-contents-with-subgraph-2f86d73d365081aeba8dd2990b6ba0ad)
by [Unito](https://www.unito.io)
2026-01-30 10:56:44 -08:00
Christian Byrne
ee600a8951 fix: prevent image/video preview reset on dynamic widget addition (#8366)
## Summary

Fixes image/video previews getting stuck in loading state when widgets
are added dynamically to a node.

## Problem

When dynamic widgets are added to a node (e.g., by extensions), Vue
reactivity triggers the watch on `imageUrls` prop even when the URL
content is identical—the array has a new reference but the same values.
This caused:
1. `startDelayedLoader()` to reset loading state to pending
2. If the cached image doesn't trigger `@load` before the 250ms timeout,
the loader shows and stays stuck

## Solution

Compare URL arrays by content, not reference. Only reset loading state
when URLs actually change:
- Check array length and element-by-element equality
- Return early if URLs are identical (just a new array reference)
- Remove `deep: true` since we compare manually

## Screenshots

<img width="749" height="647" alt="image"
src="https://github.com/user-attachments/assets/3a1ff656-59ed-467a-a121-b70b91423a50"
/>


<img width="749" height="647" alt="Screenshot from 2026-01-28 15-24-18"
src="https://github.com/user-attachments/assets/28265dad-1d79-47c8-9fd4-5a82b94e72cd"
/>

<img width="749" height="647" alt="Screenshot from 2026-01-28 15-24-05"
src="https://github.com/user-attachments/assets/c7af93b7-c898-405f-860b-0f82abe5af6d"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8366-fix-prevent-image-video-preview-reset-on-dynamic-widget-addition-2f66d73d3650819483b2d5cbfb78187f)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-30 08:27:39 -08:00
Christian Byrne
e7d3bc7285 fix: properties panel obscures menus in legacy layout (#8474)
## Summary

Fixes overlap issue where the new menu's node properties panel could
cover the old floating menu.

## Changes

Hide the properties panel when using the legacy menu, so it doesn't
obscure the old menu. The properties panel did not work anyway (empy
content).


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8474-fix-increase-z-index-of-legacy-floating-menu-2f86d73d365081a0ab44db24c2ea6357)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-01-30 08:16:11 -08:00
Alexander Brown
b4649bc96d Set IS_NIGHTLY right when we build (#8482)
## Summary

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8482-Set-IS_NIGHTLY-right-when-we-build-2f86d73d365081009fdbc2aee8f7545d)
by [Unito](https://www.unito.io)
2026-01-30 01:11:41 -08:00
sno
d7ec24abc3 fix: use PR_GH_TOKEN in lint/i18n workflows to trigger e2e tests (#8484)
## Summary

Add `PR_GH_TOKEN` to ci-lint-format and i18n-update-core workflows to
ensure commits trigger e2e test runs.

## Changes

- Add `token: ${{ secrets.PR_GH_TOKEN }}` to checkout step in
`.github/workflows/ci-lint-format.yaml`
- Add `token: ${{ secrets.PR_GH_TOKEN }}` to checkout step in
`.github/workflows/i18n-update-core.yaml`

## Context

This matches the pattern used consistently across all other workflows in
the repository (release-version-bump, version-bump-desktop-ui,
api-update-registry-api-types, i18n-update-nodes, etc.).

Without this token, commits made by these workflows don't trigger
downstream e2e tests, which can lead to issues being missed.

## Original Work

Original idea & implementation by @drjkl in commits:
- be3a8d61c - fix: use GitHub App token for lint/i18n workflows to
trigger e2e tests
- 50ccb254b - Add github app token action to pinact

This PR simplifies the approach to use the existing `PR_GH_TOKEN` secret
instead of GitHub App tokens, for consistency with the rest of the
repository.

## Test Plan

- [x] Verify workflows can checkout code successfully
- [x] Confirm commits from these workflows trigger e2e test runs

## It works!
<img width="1075" height="402" alt="image"
src="https://github.com/user-attachments/assets/31762d68-a37f-4926-b463-84d184d4309d"
/>

---------

Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
2026-01-30 00:09:28 -08:00
Christian Byrne
1ac214a1cd docs: add Vite preload error handling documentation comment (#8475)
## Summary

Add documentation comment explaining the Vite preload error handler with
a link to official documentation.

## Changes

- **What**: Added a 2-line comment above the `vite:preloadError` event
listener in App.vue explaining its purpose and linking to
https://vite.dev/guide/build#load-error-handling

Fixes COM-14132

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8475-docs-add-Vite-preload-error-handling-documentation-comment-2f86d73d3650815f9731f30bd9c5eb57)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-01-29 21:06:30 -08:00
Christian Byrne
985d024a6e fix: handle non-string serverLogs in error report (#8460)
## Summary
- Fix `[object Object]` display in error report logs section

## Changes
- Add runtime type check for `serverLogs` in error report template
- JSON stringify object logs with proper formatting

## Test plan
- [x] Verify string logs still display correctly
- [x] Verify object logs are properly stringified instead of showing
`[object Object]`

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8460-fix-handle-non-string-serverLogs-in-error-report-2f86d73d36508179af5afcdeec025a75)
by [Unito](https://www.unito.io)

Co-authored-by: Subagent 5 <subagent@example.com>
2026-01-29 19:47:59 -08:00
Christian Byrne
ee4a205d32 fix: Vue mode socket map data not cleaned up on dynamic input changes (#8469)
## Summary

Fixes a bug where socket map data was not properly removed when sockets
are dynamically added/removed via DynamicCombo widgets in Vue mode
(Nodes 2.0).

## Problem

When DynamicCombo widgets (e.g., `should_remesh` on Meshy nodes) change
their selection, inputs are dynamically added/removed. The Vue `v-for`
loop in `NodeSlots.vue` was using array index as the `:key`, causing Vue
to **reuse** slot components instead of properly unmounting them.

This led to:
- Socket map entries leaking (never cleaned up)
- Socket positions becoming desynced
- Stale cached offset data

## Solution

1. **Use slot `name` as Vue key** instead of array index in
`NodeSlots.vue`
   - Slot names are unique per node (enforced by ComfyUI backend)
- When a slot is removed, Vue sees the key disappear and properly
unmounts the component
- `onUnmounted` cleanup in `useSlotElementTracking` now runs correctly

2. **Add defensive cleanup** in `useSlotElementTracking.ts`
- Before registering a new slot, check if a stale entry exists with the
same key
   - Clean up stale entry to handle any edge cases

## Related

- Fixes COM-12970
- Related to #7837 (fixed LiteGraph version of this bug, but not Vue
mode)

## Testing

- Quality checks pass (typecheck, lint, format)
- Manual testing with DynamicCombo nodes (Meshy, nodes_logic)
recommended

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8469-fix-Vue-mode-socket-map-data-not-cleaned-up-on-dynamic-input-changes-2f86d73d365081e599eadca4f15e6b6e)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-29 19:47:43 -08:00
Kelly Yang
d784d4982b Improve template search input performance issue (#8343)
## Summary

Improve Template search input performance issue #8134
This was caused by the search logic running too frequently (throttled at
50ms), causing the main thread to block on every few keystrokes.


## Changes
Use debouncing that wait until you stop typing for a specific time
(300ms) before running.
It makes the searching function more smoothly.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8343-Improve-template-search-input-performance-issue-2f56d73d36508144bdf9fa5e0cd76818)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-01-29 19:05:37 -08:00
Christian Byrne
47113b117e refactor: migrate keybindings to DDD structure (#8369)
## Summary

Migrate keybindings domain to `src/platform/keybindings/` following DDD
principles.

## Changes

- **What**: Consolidate keybinding-related code (types, store, service,
defaults, reserved keys) into a single domain module with flat structure
- Extracted `KeyComboImpl` and `KeybindingImpl` classes into separate
files
- Updated all consumers to import from new location
- Colocated tests with source files
- Updated stores/README.md and services/README.md to remove migrated
entries

## Review Focus

- Verify all import paths were updated correctly
- Check that the flat structure is appropriate (vs nested core/data/ui
layers)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8369-refactor-migrate-keybindings-to-DDD-structure-2f66d73d36508120b169dc737075fb45)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-29 18:40:58 -08:00
Johnpaul Chiwetelu
13311a46ea Road to No explicit any: Group 8 (part 7) test files (#8459)
## Summary

This PR removes unsafe type assertions ("as unknown as Type") from test
files and improves type safety across the codebase.

### Key Changes

#### Type Safety Improvements
- Removed improper `as unknown as Type` patterns from 17 test files in
Group 8 part 7
- Replaced with proper TypeScript patterns using factory functions and
Mock types
- Fixed createTestingPinia usage in test files (was incorrectly using
createPinia)
- Fixed vi.hoisted pattern for mockSetDirty in viewport tests  
- Fixed vi.doMock lint issues with vi.mock and vi.hoisted pattern
- Retained necessary `as unknown as` casts only for complex mock objects
where direct type assertions would fail

### Files Changed

Test files (Group 8 part 7 - services, stores, utils):
- src/services/nodeOrganizationService.test.ts
- src/services/providers/algoliaSearchProvider.test.ts
- src/services/providers/registrySearchProvider.test.ts
- src/stores/comfyRegistryStore.test.ts
- src/stores/domWidgetStore.test.ts
- src/stores/executionStore.test.ts
- src/stores/firebaseAuthStore.test.ts
- src/stores/modelToNodeStore.test.ts
- src/stores/queueStore.test.ts
- src/stores/subgraphNavigationStore.test.ts
- src/stores/subgraphNavigationStore.viewport.test.ts
- src/stores/subgraphStore.test.ts
- src/stores/systemStatsStore.test.ts
- src/stores/workspace/nodeHelpStore.test.ts
- src/utils/colorUtil.test.ts
- src/utils/executableGroupNodeChildDTO.test.ts

Source files:
- src/stores/modelStore.ts - Improved type handling

### Testing
- All TypeScript type checking passes (`pnpm typecheck`)
- All affected test files pass (`pnpm test:unit`)
- Linting passes without errors (`pnpm lint`)
- Code formatting applied (`pnpm format`)

Part of the "Road to No Explicit Any" initiative, cleaning up type
casting issues from branch `fix/remove-any-types-part8`.

### Previous PRs in this series:
- Part 2: #7401
- Part 3: #7935
- Part 4: #7970
- Part 5: #8064
- Part 6: #8083
- Part 7: #8092
- Part 8 Group 1: #8253
- Part 8 Group 2: #8258
- Part 8 Group 3: #8304
- Part 8 Group 4: #8314
- Part 8 Group 5: #8329
- Part 8 Group 6: #8344
- Part 8 Group 7: #8459 (this PR)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8459-Road-to-No-explicit-any-Group-8-part-7-test-files-2f86d73d36508114ad28d82e72a3a5e9)
by [Unito](https://www.unito.io)
2026-01-30 03:38:06 +01:00
Alexander Brown
067d80c4ed refactor: migrate ES private fields to TypeScript private for Vue Proxy compatibility (#8440)
## Summary

Migrates ECMAScript private fields (`#`) to TypeScript private
(`private`) across LiteGraph to fix Vue Proxy reactivity
incompatibility.

## Problem

ES private fields (`#field`) are incompatible with Vue's Proxy-based
reactivity system - accessing `#field` through a Proxy throws
`TypeError: Cannot read private member from an object whose class did
not declare it`.

## Solution

- Converted all `#field` to `private _field` across 10 phases
- Added `toJSON()` methods to `LGraph`, `NodeSlot`, `NodeInputSlot`, and
`NodeOutputSlot` to prevent circular reference errors during
serialization (TypeScript private fields are visible to `JSON.stringify`
unlike true ES private fields)
- Made `DragAndScale.element.data` non-enumerable to break canvas
circular reference chain

## Testing

- All 4027 unit tests pass
- Added 9 new serialization tests to catch future circular reference
issues
- Browser tests (undo/redo, save workflows) verified working

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8440-refactor-migrate-ES-private-fields-to-TypeScript-private-for-Vue-Proxy-compatibility-2f76d73d365081a3bd82d429a3e0fcb7)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-01-29 18:18:58 -08:00
Alexander Brown
82bacb82a7 test: add Playwright test tags for filtering (@smoke, @slow, @screenshot, domains) (#8441)
## Summary

Adds structured test tags to all 54 Playwright test files to enable
flexible test filtering during development and CI.

## Tags Added

| Tag | Count | Purpose |
|-----|-------|---------|
| `@screenshot` | 32 files | Tests with visual assertions
(`toHaveScreenshot`) |
| `@smoke` | 5 files | Quick essential tests for fast validation |
| `@slow` | 5 files | Long-running tests (templates, subgraph,
featureFlags) |
| `@canvas` | 15 files | Canvas/graph rendering tests |
| `@node` | 10 files | Node behavior tests |
| `@ui` | 8 files | UI component tests |
| `@widget` | 5 files | Widget-specific tests |
| `@workflow` | 3 files | Workflow operations |
| `@subgraph` | 1 file | Subgraph functionality |
| `@keyboard` | 2 files | Keyboard shortcuts |
| `@settings` | 2 files | Settings/preferences |

## Usage Examples

```bash
# Quick validation (~16 tests, ~30s)
pnpm test:browser -- --grep @smoke

# Skip slow tests for faster CI feedback
pnpm test:browser -- --grep-invert @slow

# Skip visual tests (useful for local development without snapshots)
pnpm test:browser -- --grep-invert @screenshot

# Run only canvas-related tests
pnpm test:browser -- --grep @canvas

# Combine filters
pnpm test:browser -- --grep @smoke --grep-invert @screenshot
```

## Implementation Details

- Uses Playwright's native tag syntax: `test.describe('Name', { tag:
'@tag' }, ...)`
- Tags inherit from describe blocks to child tests
- Preserves existing project-level tags: `@mobile`, `@2x`, `@0.5x`
- Multiple tags supported: `{ tag: ['@screenshot', '@smoke'] }`

## Test Plan

- [x] All existing tests pass unchanged
- [x] Tag filtering works with `--grep` and `--grep-invert`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8441-test-add-Playwright-test-tags-for-filtering-smoke-slow-screenshot-domains-2f76d73d36508184990ec859c8fd7629)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
2026-01-29 16:34:56 -08:00
Christian Byrne
80ccc13659 feat: add Chatterbox model support for Cloud asset browser (#8418)
## Summary

Adds support for creating Chatterbox TTS nodes when clicking Chatterbox
models in the Cloud asset browser.

## Changes

### modelToNodeStore.ts
- Add `findProvidersWithFallback()` helper for hierarchical model type
lookups (e.g., `parent/child` falls back to `parent`)
- Register 4 Chatterbox model directories with empty widget keys:
  - `chatterbox/chatterbox` → `FL_ChatterboxTTS`
  - `chatterbox/chatterbox_turbo` → `FL_ChatterboxTurboTTS`
- `chatterbox/chatterbox_multilingual` → `FL_ChatterboxMultilingualTTS`
  - `chatterbox/chatterbox_vc` → `FL_ChatterboxVC`

### createModelNodeFromAsset.ts
- Skip widget assignment when `provider.key` is empty (for nodes that
auto-load models without a widget selector)

### Tests
- Add tests for hierarchical fallback behavior
- Add tests for empty widget key (auto-load nodes)
- Add Chatterbox node types to mock data

## Notes

- Empty `key` convention: Chatterbox nodes auto-load their models and
don't have a model selector widget, so we register them with `key: ''`
and skip the widget assignment step
- Hierarchical fallback only goes one level deep (`a/b/c` → `a`, not
`a/b`)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8418-feat-add-Chatterbox-model-support-for-Cloud-asset-browser-2f76d73d365081be822bc369b155f099)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
2026-01-29 16:26:51 -08:00
AustinMroz
dce6fd1040 Fix invalid keybind flash (#8435)
Previously the save keybind action would
- apply the new keybind
- wait for a network request to persist the change
- close the dialogue regardless of the results of the above changes

During this network request, the dialog would show a warning that the
keybind is invalid because the dialogue "contains a modified keybind
which conflicts with an existing keybind"
<img width="506" height="261" alt="image"
src="https://github.com/user-attachments/assets/e46150ce-9349-4f8e-b3b5-fb0b20dd3db9"
/>


This PR changes the order these actions are applied in.
- The dialogue is immediately closed
- The keybinding is updated if valid
- The keybinding is persisted.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8435-Fix-invalid-keybind-flash-2f76d73d3650815c9657f35e77d331fe)
by [Unito](https://www.unito.io)
2026-01-29 16:26:07 -08:00
AustinMroz
e2625a4055 Fix paste-with-links breaking autogrow connections (#8442)
The extra `linf` check was made in 878c8c0f39. I'm no longer able to
replicate the cloning bug the check was introduced for.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8442-Fix-paste-with-links-breaking-autogrow-connections-2f76d73d3650817aa99cc3b9e4e6412c)
by [Unito](https://www.unito.io)
2026-01-29 16:25:16 -08:00
Alexander Brown
a4cf9a1ca8 Fix: Hide Jobs in Assets Panel when Queue V2 is disabled. (#8450)
## Summary

See Title.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8450-Fix-Hide-Jobs-in-Assets-Panel-when-Queue-V2-is-disabled-2f86d73d3650810c8155c1fea92fc0aa)
by [Unito](https://www.unito.io)
2026-01-29 16:23:16 -08:00
Jin Yi
d437b96238 [bugfix] Fix shift+click deselection in asset panel (#8396)
## Summary
Fix shift+click range selection not properly deselecting assets when
selecting a smaller range, and improve selection performance.

## Changes
- **Bug Fix**: Shift+click now replaces selection with the new range
instead of combining with existing selection
- **Performance**: Remove unnecessary `.every()` check in `setSelection`
(O(n) → O(1))
- **Tests**: Add 23 unit tests for asset selection logic

## Test Plan
- [x] Click 1st asset → only 1st selected
- [x] Shift+click 3rd asset → items 1-3 selected
- [x] Shift+click 1st asset → only 1st selected (was broken, now fixed)
- [x] All 23 new unit tests pass

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8396-bugfix-Fix-shift-click-deselection-in-asset-panel-2f76d73d3650814ca060d1e6a40cf6d4)
by [Unito](https://www.unito.io)
2026-01-29 16:21:08 -08:00
Christian Byrne
f1cf8073d6 fix: garbage collect subgraph definitions when SubgraphNode is removed (#8187)
## Summary

When removing a SubgraphNode via `LGraph.remove()`:
- Fire `onRemoved` for all nodes inside the subgraph
- Fire `onNodeRemoved` callback on the subgraph for each inner node
- Remove subgraph definition from `rootGraph.subgraphs` when no other
nodes reference it (checks both root graph nodes and nodes inside other
subgraphs)

This ensures proper cleanup of subgraph definitions and lifecycle
callbacks for nested nodes when subgraph nodes are deleted.

## Changes

### LGraph.ts
Added SubgraphNode-specific cleanup in `remove()` method that:
1. Iterates inner nodes and fires their `onRemoved` callbacks
2. Fires `onNodeRemoved` on the subgraph for downstream listeners (e.g.,
minimap)
3. Garbage collects the subgraph definition when no other nodes
reference it

### SubgraphNode.ts
Fixed `graph` property to match `LGraphNode` lifecycle contract.
Previously it was declared as `override readonly graph` via constructor
parameter promotion, which prevented `LGraph.remove()` from setting
`node.graph = null`. Changed to a regular mutable property with null
guard in `rootGraph` getter.

### LGraph.test.ts
Added 4 tests:
- `removing SubgraphNode fires onRemoved for inner nodes`
- `removing SubgraphNode fires onNodeRemoved callback`
- `subgraph definition is removed when last referencing node is removed`
- `subgraph definition is retained when other nodes still reference it`

## Related

- Fixes #8145
- Part of the subgraph lifecycle cleanup plan (Slice 2: Definition
garbage collection)
2026-01-29 16:19:25 -08:00
Jin Yi
d7654baebf [feat] Show context-appropriate empty state messages in Manager tabs (#8415)
## Summary
Shows tab-specific empty state messages in Node Manager instead of
generic "No search results found" message.

## Changes
- Added computed properties to determine empty state messages based on
current tab and search state
- Display tab-specific messages when a tab is empty without active
search (e.g., "No Missing Nodes" for Missing tab)
- Fall back to search-related messages only when there's an active
search query
- Added Korean translations for empty state messages

| Tab | Empty State Title |
|-----|-------------------|
| All Installed | No Extensions Installed |
| Update Available | All Up to Date |
| Conflicting | No Conflicts Detected |
| Workflow | No Extensions in Workflow |
| Missing | No Missing Nodes |

## Review Focus
- Verify i18n key structure matches existing patterns

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8415-feat-Show-context-appropriate-empty-state-messages-in-Manager-tabs-2f76d73d3650817ab8a0d41b45df3411)
by [Unito](https://www.unito.io)
2026-01-29 16:16:28 -08:00
Comfy Org PR Bot
a032e50721 1.39.2 (#8447)
Patch version increment to 1.39.2

**Base branch:** `main`

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

Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
2026-01-29 16:12:49 -08:00
Christian Byrne
d9ea36a1d0 refactor: change asset cache from nodeType-keyed to category-keyed (#8433)
## Summary

Refactors the model assets cache in `assetsStore.ts` to be keyed by
category (e.g., 'checkpoints', 'loras') instead of nodeType (e.g.,
'CheckpointLoaderSimple').

## Changes

- Rename `modelStateByKey` to `modelStateByCategory`
- Add `resolveCategory()` helper to translate nodeType to category for
cache lookup
- Multiple node types sharing the same category now share one cache
entry
- Add `invalidateCategory()` method for cache invalidation
- Maintain backwards-compatible public API accepting nodeType
- Update tests for new category-keyed behavior

## Benefits

1. **Deduplication**: Same category = same cache entry = single API call
2. **Simple invalidation**: Delete asset with tag 'checkpoints' then
invalidate cache
3. **Cleaner mental model**: Store to View reactive flow works naturally

## Testing

- All existing tests pass with updates
- Added new tests for category-keyed cache sharing, invalidateCategory,
and unknown node type handling

## Part of Stack

This is **PR 1 of 2** in a stacked PR series:
1. **This PR**: Refactor asset cache to category-keyed (architectural
improvement)
2. **[PR 2
#8434](https://github.com/Comfy-Org/ComfyUI_frontend/pull/8434)**: Fix
deletion invalidation

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8433-refactor-change-asset-cache-from-nodeType-keyed-to-category-keyed-2f76d73d365081999b7fda12c9706ab5)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
2026-01-29 16:03:29 -08:00
AustinMroz
eceed972f5 Fix Help Center display in linear mode (#8438)
The Help Center popover wasn't working in linear mode because the
`#graph-canvas-container` is hidden. This is fixed by instead setting
the target to body. This matches the [default behaviour used by portals
in
rekai-ui](https://github.com/unovue/reka-ui/blob/v2/packages/core/src/Teleport/Teleport.vue)
<img width="476" height="677" alt="image"
src="https://github.com/user-attachments/assets/cca46648-3bce-4b72-a5be-3727e2358217"
/>

I've got a working branch for moving the Help Center to our reka-ui
Popover component, but that'll require a much larger surface area of
code changes.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8438-Fix-Help-Center-display-in-linear-mode-2f76d73d36508112abedf1160f7c3a90)
by [Unito](https://www.unito.io)
2026-01-29 16:01:38 -08:00
Christian Byrne
72a6af4b9e make new queue panel disabled by default (#8444)
## Summary

Was enabled by default for 1.38 during nightly, but should be reverted
back now.

In the future we can now just use `isNightly` flag for this (ref:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/8149)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8444-make-new-queue-panel-disabled-by-default-2f76d73d365081139b54f76d9325101d)
by [Unito](https://www.unito.io)
2026-01-29 23:56:54 +00:00
Rizumu Ayaka
fc38f16543 fix: default image input for the template is displayed as empty on dropdown selection (#8276)
The image input for nodes loaded from templates appears empty in the
Properties Panel.

When the widget's current value (saved in the template) is not in the
available file list returned by the server, the selectedSet is empty,
causing a placeholder to be displayed instead of the actual value.

Added a missingValueItem computed property in WidgetSelectDropdown.vue.
When the current value is not in inputItems or outputItems, it creates a
fallback item and adds it to dropdownItems.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8276-fix-default-image-input-for-the-template-is-displayed-as-empty-on-dropdown-selection-2f16d73d3650817eaad5e4e33637fb74)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: github-actions <github-actions@github.com>
2026-01-29 14:19:52 -08:00
Christian Byrne
faad2c03de feat: increase allowed batch count (on Run button) on cloud (from 4 to 32) (#8436)
## Summary

cloud.comfy.org now suports up to 100 queued jobs at a time
([details](https://x.com/ComfyUI/status/2016622139722572032?s=20)). We
can increase the batch count limit to 32. Possible downside is cloud
having to reject larger number of jobs over the 100 limit if someone go
to 32 and clicks 4+ times. This setting was configurable anyway before,
so this is mostly a QoL change.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8436-feat-increase-allowed-batch-count-on-Run-button-on-cloud-from-4-to-32-2f76d73d365081728650fabefc394046)
by [Unito](https://www.unito.io)
2026-01-29 13:54:52 -08:00
Christian Byrne
d4cec49db5 fix: use merge-multiple for snapshot artifact download (#8432)
## Summary

Fixes the snapshot merge failure introduced by PR #8377
(actions/download-artifact v4→v7 upgrade).

## Root Cause

The v5+ release of `download-artifact` changed behavior: when a
`pattern` matches only a **single artifact**, files are extracted
directly to `path/` without the artifact name subdirectory. When only
one shard had changes, the merge loop couldn't find the expected
`snapshots-shard-*/` directories.

## Fix

Use `merge-multiple: true` — the documented pattern for combining
sharded artifacts. This merges all matched artifacts directly into the
target path, eliminating directory structure assumptions.

## Testing

This fix can be validated by re-running the workflow on [PR
#8276](https://github.com/Comfy-Org/ComfyUI_frontend/pull/8276) after
merge.

---
- Fixes snapshot update workflow regression from #8377

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8432-fix-use-merge-multiple-for-snapshot-artifact-download-2f76d73d3650810b97fdfe28cd3c7694)
by [Unito](https://www.unito.io)

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-29 12:58:22 -08:00
AustinMroz
af8433fb3d Support widget specific contextmenu options in vue (#8431)
<img width="614" height="485" alt="image"
src="https://github.com/user-attachments/assets/2a635dec-8bed-4fab-9881-5e6057d482e1"
/>

These options were defined in `litegraphService`. While the existing
code for defining options is reused (to ensure there's no implementation
drift) these extra widget options use the litegraph format for context
menu options and do not belong in `useSelectionMenuOptions`. They have
been moved out of `useLitegraphService` (good), but left in
`litegraphService` (not great)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8431-Support-widget-specific-contextmenu-options-in-vue-2f76d73d3650814fb20fca352dc81e3b)
by [Unito](https://www.unito.io)
2026-01-29 11:38:41 -08:00
Johnpaul Chiwetelu
cabd08f0ec Road to No explicit any: Group 8 (part 6) test files (#8344)
## Summary

This PR removes unsafe type assertions ("as unknown as Type") from test
files and improves type safety across the codebase.

### Key Changes

#### Type Safety Improvements
- Removed all instances of "as unknown as" patterns from test files
- Used proper factory functions from litegraphTestUtils instead of
custom mocks
- Made incomplete mocks explicit using Partial<T> types
- Fixed DialogStore mocking with proper interface exports
- Improved type safety with satisfies operator where applicable

#### App Parameter Removal
- **Removed the unused `app` parameter from all ComfyExtension interface
methods**
- The app parameter was always undefined at runtime as it was never
passed from invokeExtensions
- Affected methods: init, setup, addCustomNodeDefs,
beforeRegisterNodeDef, beforeRegisterVueAppNodeDefs,
registerCustomNodes, loadedGraphNode, nodeCreated, beforeConfigureGraph,
afterConfigureGraph

##### Breaking Change Analysis
Verified via Sourcegraph that this is NOT a breaking change:
- Searched all 10 affected methods across GitHub repositories
- Only one external repository
([drawthingsai/draw-things-comfyui](https://github.com/drawthingsai/draw-things-comfyui))
declares the app parameter in their extension methods
- That repository never actually uses the app parameter (just declares
it in the function signature)
- All other repositories already omit the app parameter
- Search queries used:
- [init method
search](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/.*+lang:typescript+%22init%28app%22+-repo:Comfy-Org/ComfyUI_frontend&patternType=standard)
- [setup method
search](https://sourcegraph.com/search?q=context:global+repo:%5Egithub%5C.com/.*+lang:typescript+%22setup%28app%22+-repo:Comfy-Org/ComfyUI_frontend&patternType=standard)
  - Similar searches for all 10 methods confirmed no usage

### Files Changed

Test files:
-
src/components/settings/widgets/__tests__/WidgetInputNumberInput.test.ts
- src/services/keybindingService.escape.test.ts  
- src/services/keybindingService.forwarding.test.ts
- src/utils/__tests__/newUserService.test.ts →
src/utils/__tests__/useNewUserService.test.ts
- src/services/jobOutputCache.test.ts
-
src/renderer/extensions/vueNodes/widgets/composables/useRemoteWidget.test.ts
-
src/renderer/extensions/vueNodes/widgets/composables/useIntWidget.test.ts
-
src/renderer/extensions/vueNodes/widgets/composables/useFloatWidget.test.ts

Source files:
- src/types/comfy.ts - Removed app parameter from ComfyExtension
interface
- src/services/extensionService.ts - Improved type safety with
FunctionPropertyNames helper
- src/scripts/metadata/isobmff.ts - Fixed extractJson return type per
review
- src/extensions/core/*.ts - Updated extension implementations
- src/scripts/app.ts - Updated app initialization

### Testing
- All existing tests pass
- Type checking passes  
- ESLint/oxlint checks pass
- No breaking changes for external repositories

Part of the "Road to No Explicit Any" initiative.

### Previous PRs in this series:
- Part 2: #7401
- Part 3: #7935
- Part 4: #7970
- Part 5: #8064
- Part 6: #8083
- Part 7: #8092
- Part 8 Group 1: #8253
- Part 8 Group 2: #8258
- Part 8 Group 3: #8304
- Part 8 Group 4: #8314
- Part 8 Group 5: #8329
- Part 8 Group 6: #8344 (this PR)
2026-01-29 11:03:17 -08:00
AustinMroz
868180eb28 Disable logs button in sidebar on cloud (#8429)
Since cloud doesn't currently provide logs, this button was just causing
confusion.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8429-Disable-logs-button-in-sidebar-on-cloud-2f76d73d365081a4b909dd9a105e381e)
by [Unito](https://www.unito.io)
2026-01-29 09:58:25 -08:00
Alexander Brown
c51916d103 Chore: Add workflow dispatch to E2E (#8422)
Also remove old branches from the ignore

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8422-Chore-Add-workflow-dispatch-to-E2E-2f76d73d365081d0940dceaa37231ca7)
by [Unito](https://www.unito.io)
2026-01-29 08:47:18 -08:00
Comfy Org PR Bot
d2ff7d518a 1.39.1 (#8382)
Patch version increment to 1.39.1

**Base branch:** `main`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8382-1-39-1-2f76d73d36508126840cdc74593952e5)
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: Austin Mroz <austin@comfy.org>
2026-01-29 00:31:49 -08:00
AustinMroz
44baadd7ca Implement clickable badges (#8401)
Adds an `onClick` handler to LGraphBadge

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8401-Implement-clickable-badges-2f76d73d365081b3b23fc1eaa3bc65b8)
by [Unito](https://www.unito.io)
2026-01-29 00:04:28 -08:00
AustinMroz
3866fe7eaa Fix flake hidream test (#8406)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8406-Fix-flake-hidream-test-2f76d73d3650814daae8fb7775fa3073)
by [Unito](https://www.unito.io)
2026-01-29 00:02:30 -08:00
Jin Yi
4debbf8268 [bugfix] Disable install button when already installed version is selected (#8412)
## Summary
Prevent re-installing an already installed version by disabling the
Install button and the version option in the selector.

## Changes
- Add `isVersionInstalled()` function to check if a version is already
installed
- Add `isInstallDisabled` computed to disable Install button when
selected version is installed
- Add `option-disabled="isInstalled"` to Listbox to prevent selecting
installed versions

Fixes the issue where users could trigger duplicate install operations
by selecting the currently installed version.

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

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8412-bugfix-Disable-install-button-when-already-installed-version-is-selected-2f76d73d365081cb859ff98a3d1c64e6)
by [Unito](https://www.unito.io)
2026-01-28 23:58:12 -08:00
Christian Byrne
23a5baef43 feat: add category support for blueprints and protect global blueprints (#8378)
## Summary

This PR adds two related features for subgraph blueprints:

### 1. Protect Global Blueprints from Deletion
- Added `isGlobalBlueprint()` helper that distinguishes blueprints by
`python_module` field
- User blueprints have `python_module: 'blueprint'`
- Global (installed) blueprints have `python_module` set to the node
pack name
- Guarded `deleteBlueprint()` to show a warning toast for global
blueprints
- Hidden delete menu in node library for global blueprints

### 2. Category Support for Blueprints
- User blueprints now use category `Subgraph Blueprints/User`
- Global blueprints use `Subgraph Blueprints/{category}` if category is
provided, otherwise `Subgraph Blueprints`
- Extended `GlobalSubgraphData.info` type with optional `category` field
- Added `category` to `zSubgraphDefinition` schema

## Files Changed
- `src/stores/subgraphStore.ts` - Core logic for both features
- `src/stores/subgraphStore.test.ts` - Tests for `isGlobalBlueprint`
- `src/components/sidebar/tabs/nodeLibrary/NodeTreeLeaf.vue` - Hide
delete menu for global blueprints
- `src/scripts/api.ts` - Extended `GlobalSubgraphData` type
- `src/platform/workflow/validation/schemas/workflowSchema.ts` - Added
category to schema
- `src/locales/en/main.json` - Added translation key

## Testing
-  `pnpm typecheck` passed
-  `pnpm lint` passed

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8378-feat-add-category-support-for-blueprints-and-protect-global-blueprints-2f66d73d36508137aa67c2d88c358b69)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: AustinMroz <austin@comfy.org>
2026-01-28 23:30:48 -08:00
Christian Byrne
0faf2220b8 fix: dragging (e.g., when selecting text) in Markdown note causes node to drag (#8413)
## Summary

When attempting to select text inside Vue node Markdown widget's
textarea (edit mode), the node would drag instead of text being
selected.

Root cause: WidgetMarkdown.vue's Textarea only had @click.stop and
@keydown.stop, but was missing pointer event modifiers. The pointerdown
event bubbled up to LGraphNode.vue which initiated node drag.

*Fix*: Add @pointerdown.capture.stop, @pointermove.capture.stop, and
@pointerup.capture.stop to match the pattern used in WidgetTextarea.vue.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8413-fix-dragging-e-g-when-selecting-text-in-Markdown-note-causes-node-to-drag-2f76d73d3650816dbf9bdf893775c3d4)
by [Unito](https://www.unito.io)

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-28 23:18:23 -08:00
Jin Yi
65ff23c5af [bugfix] Fix manager missing node tab with shared composable (#8409) 2026-01-29 06:23:47 +00:00
Alexander Brown
6ce60a11a4 test: use createTestingPinia instead of createPinia (#8376)
Replace \createPinia\ with \createTestingPinia({ stubActions: false })\
from \@pinia/testing\ across 45 test files for proper test isolation.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8376-test-use-createTestingPinia-instead-of-createPinia-2f66d73d36508137a9f0daffcddc86f7)
by [Unito](https://www.unito.io)

Co-authored-by: Amp <amp@ampcode.com>
2026-01-28 22:21:38 -08:00
Christian Byrne
3b5d124029 fix: use getAuthHeader in createCustomer to support API key auth (#8408)
## Summary

Fixes authentication failure when using API key authentication on
staging server after frontend update to 1.33.10.

<img width="1160" height="709" alt="image"
src="https://github.com/user-attachments/assets/fe56866d-1819-419e-9f53-35a123d764c3"
/>


## Changes

- **What**: Changed `createCustomer()` to use `getAuthHeader()` instead
of `getFirebaseAuthHeader()`, allowing API key users to authenticate
successfully

## Review Focus

- Verify `getAuthHeader()` correctly falls back to API key when no
Firebase token exists
- Backend `/customers` endpoint supports `X-API-KEY` header (per cloud
PR #1766)

Fixes COM-12398

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8408-fix-use-getAuthHeader-in-createCustomer-to-support-API-key-auth-2f76d73d3650819994e3e6d3ed9f3dfa)
by [Unito](https://www.unito.io)

Co-authored-by: Subagent 5 <subagent@example.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-01-28 22:12:28 -08:00
Alexander Brown
bd4920febc Chore: Actions updates and cleanup (#8377)
## Summary

...

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8377-WIP-Chore-Actions-updates-and-cleanup-2f66d73d3650818483a8dffa32a6f245)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
2026-01-28 21:22:39 -08:00
Jin Yi
bd916096ac fix: add null check in getCanvasCenter to prevent crash on asset insert (#8399)
## Summary
Adds null check in `getCanvasCenter()` to prevent crash when inserting
asset as node before canvas is fully initialized.

## Changes
- **What**: Added optional chaining for `app.canvas?.ds?.visible_area`
with fallback to `[0, 0]`

## Review Focus
- Simple defensive fix - returns origin position if canvas not ready

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

Co-Authored-By: Claude <noreply@anthropic.com>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8399-fix-add-null-check-in-getCanvasCenter-to-prevent-crash-on-asset-insert-2f76d73d365081e88c08ef40ea9e7b78)
by [Unito](https://www.unito.io)
2026-01-29 13:19:51 +09:00
guill
9be853f6b5 feat: support dev-only nodes (#8359)
## Summary

Support `dev_only` property to node definitions that hides nodes from
search and menus unless dev mode is enabled. Dev-only nodes display a
"DEV" badge when visible.

This functionality is primarily intended to support unit-testing nodes
on Comfy Cloud, but also has other uses.

## Changes

- **What**: Nodes flagged as dev_only in the node schema will only
appear in search and menus if Dev Mode is on.

## Screenshots (if applicable)

With Dev Mode off:
<img width="2189" height="1003" alt="image"
src="https://github.com/user-attachments/assets/a08e1fd7-dca9-4ce1-9964-5f4f3b7b95ac"
/>

With Dev Mode on:
<img width="2201" height="1066" alt="image"
src="https://github.com/user-attachments/assets/7fe6cd1f-f774-4f48-b604-a528e286b584"
/>

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8359-feat-support-dev-only-nodes-2f66d73d36508102839ee7cd66a26129)
by [Unito](https://www.unito.io)
2026-01-28 19:41:45 -08:00
1570 changed files with 128382 additions and 25002 deletions

View File

@@ -391,7 +391,7 @@ echo "Last stable release: $LAST_STABLE"
```bash
# Trigger the workflow
gh workflow run version-bump.yaml -f version_type=${VERSION_TYPE}
gh workflow run release-version-bump.yaml -f version_type=${VERSION_TYPE}
# Workflow runs quickly - usually creates PR within 30 seconds
echo "Workflow triggered. Waiting for PR creation..."
@@ -443,28 +443,21 @@ echo "Workflow triggered. Waiting for PR creation..."
gh pr view ${PR_NUMBER} --json labels | jq -r '.labels[].name' | grep -q "Release" || \
echo "ERROR: Release label missing! Add it immediately!"
```
2. Check for update-locales commits:
```bash
# WARNING: update-locales may add [skip ci] which blocks release workflow!
gh pr view ${PR_NUMBER} --json commits | grep -q "skip ci" && \
echo "WARNING: [skip ci] detected - release workflow may not trigger!"
```
3. Verify version number in package.json
4. Review all changed files
5. Ensure no unintended changes included
6. Wait for required PR checks:
2. Verify version number in package.json
3. Review all changed files
4. Ensure no unintended changes included
5. Wait for required PR checks:
```bash
gh pr checks ${PR_NUMBER} --watch
```
7. **FINAL CODE REVIEW**: Release label present and no [skip ci]?
6. **FINAL CODE REVIEW**: Release label present and no [skip ci]?
### Step 12: Pre-Merge Validation
1. **Review Requirements**: Release PRs require approval
2. Monitor CI checks - watch for update-locales
3. **CRITICAL WARNING**: If update-locales adds [skip ci], the release workflow won't trigger!
4. Check no new commits to main since PR creation
5. **DEPLOYMENT READINESS**: Ready to merge?
2. Monitor CI checks
3. Check no new commits to main since PR creation
4. **DEPLOYMENT READINESS**: Ready to merge?
### Step 13: Execute Release

View File

@@ -0,0 +1,200 @@
---
name: writing-playwright-tests
description: 'Writes Playwright e2e tests for ComfyUI_frontend. Use when creating, modifying, or debugging browser tests. Triggers on: playwright, e2e test, browser test, spec file.'
---
# Writing Playwright Tests for ComfyUI_frontend
## Golden Rules
1. **ALWAYS look at existing tests first.** Search `browser_tests/tests/` for similar patterns before writing new tests.
2. **ALWAYS read the fixture code.** The APIs are in `browser_tests/fixtures/` - read them directly instead of guessing.
3. **Use premade JSON workflow assets** instead of building workflows programmatically.
- Assets live in `browser_tests/assets/`
- Load with `await comfyPage.workflow.loadWorkflow('feature/my_workflow')`
- Create new assets by starting with `browser_tests/assets/default.json` and manually editing the JSON to match your desired graph state
## Vue Nodes vs LiteGraph: Decision Guide
Choose based on **what you're testing**, not personal preference:
| Testing... | Use | Why |
| ---------------------------------------------- | -------------------------------- | ---------------------------------------- |
| Vue-rendered node UI, DOM widgets, CSS states | `comfyPage.vueNodes.*` | Nodes are DOM elements, use locators |
| Canvas interactions, connections, legacy nodes | `comfyPage.nodeOps.*` | Canvas-based, use coordinates/references |
| Both in same test | Pick primary, minimize switching | Avoid confusion |
**Vue Nodes requires explicit opt-in:**
```typescript
await comfyPage.settings.setSetting('Comfy.VueNodes.Enabled', true)
await comfyPage.vueNodes.waitForNodes()
```
**Vue Node state uses CSS classes:**
```typescript
const BYPASS_CLASS = /before:bg-bypass\/60/
await expect(node).toHaveClass(BYPASS_CLASS)
```
## Common Issues
These are frequent causes of flaky tests - check them first, but investigate if they don't apply:
| Symptom | Common Cause | Typical Fix |
| ---------------------------------- | ------------------------- | -------------------------------------------------------------------------------------- |
| Test passes locally, fails in CI | Missing nextFrame() | Add `await comfyPage.nextFrame()` after canvas ops (not needed after `loadWorkflow()`) |
| Keyboard shortcuts don't work | Missing focus | Add `await comfyPage.canvas.click()` first |
| Double-click doesn't trigger | Timing too fast | Add `{ delay: 5 }` option |
| Elements end up in wrong position | Drag animation incomplete | Use `{ steps: 10 }` not `{ steps: 1 }` |
| Widget value wrong after drag-drop | Upload incomplete | Add `{ waitForUpload: true }` |
| Test fails when run with others | Test pollution | Add `afterEach` with `resetView()` |
| Local screenshots don't match CI | Platform differences | Screenshots are Linux-only, use PR label |
## Test Tags
Add appropriate tags to every test:
| Tag | When to Use |
| ------------- | ----------------------------------------- |
| `@smoke` | Quick essential tests |
| `@slow` | Tests > 10 seconds |
| `@screenshot` | Visual regression tests |
| `@canvas` | Canvas interactions |
| `@node` | Node-related |
| `@widget` | Widget-related |
| `@mobile` | Mobile viewport (runs on Pixel 5 project) |
| `@2x` | HiDPI tests (runs on 2x scale project) |
```typescript
test.describe('Feature', { tag: ['@screenshot', '@canvas'] }, () => {
```
## Retry Patterns
**Never use `waitForTimeout`** - it's always wrong.
| Pattern | Use Case |
| ------------------------ | ---------------------------------------------------- |
| Auto-retrying assertions | `toBeVisible()`, `toHaveText()`, etc. (prefer these) |
| `expect.poll()` | Single value polling |
| `expect().toPass()` | Multiple assertions that must all pass |
```typescript
// Prefer auto-retrying assertions when possible
await expect(node).toBeVisible()
// Single value polling
await expect.poll(() => widget.getValue(), { timeout: 2000 }).toBe(100)
// Multiple conditions
await expect(async () => {
expect(await node1.getValue()).toBe('foo')
expect(await node2.getValue()).toBe('bar')
}).toPass({ timeout: 2000 })
```
## Screenshot Baselines
- **Screenshots are Linux-only.** Don't commit local screenshots.
- **To update baselines:** Add PR label `New Browser Test Expectations`
- **Mask dynamic content:**
```typescript
await expect(comfyPage.canvas).toHaveScreenshot('page.png', {
mask: [page.locator('.timestamp')]
})
```
## CI Debugging
1. Download artifacts from failed CI run
2. Extract and view trace: `npx playwright show-trace trace.zip`
3. CI deploys HTML report to Cloudflare Pages (link in PR comment)
4. Reproduce CI: `CI=true pnpm test:browser`
5. Local runs: `pnpm test:browser:local`
## Anti-Patterns
Avoid these common mistakes:
1. **Arbitrary waits** - Use retrying assertions instead
```typescript
// ❌ await page.waitForTimeout(500)
// ✅ await expect(element).toBeVisible()
```
2. **Implementation-tied selectors** - Use test IDs or semantic selectors
```typescript
// ❌ page.locator('div.container > button.btn-primary')
// ✅ page.getByTestId('submit-button')
```
3. **Missing nextFrame after canvas ops** - Canvas needs sync time
```typescript
await node.drag({ x: 50, y: 50 })
await comfyPage.nextFrame() // Required
```
4. **Shared state between tests** - Tests must be independent
```typescript
// ❌ let sharedData // Outside test
// ✅ Define state inside each test
```
## Quick Start Template
```typescript
// Path depends on test file location - adjust '../' segments accordingly
import {
comfyPageFixture as test,
comfyExpect as expect
} from '../fixtures/ComfyPage'
test.describe('FeatureName', { tag: ['@canvas'] }, () => {
test.afterEach(async ({ comfyPage }) => {
await comfyPage.canvasOps.resetView()
})
test('should do something', async ({ comfyPage }) => {
await comfyPage.workflow.loadWorkflow('myWorkflow')
const node = (await comfyPage.nodeOps.getNodeRefsByTitle('KSampler'))[0]
// ... test logic
await expect(comfyPage.canvas).toHaveScreenshot('expected.png')
})
})
```
## Finding Patterns
```bash
# Find similar tests
grep -r "KSampler" browser_tests/tests/
# Find usage of a fixture method
grep -r "loadWorkflow" browser_tests/tests/
# Find tests with specific tag
grep -r '@screenshot' browser_tests/tests/
```
## Key Files to Read
| Purpose | Path |
| ----------------- | ------------------------------------------ |
| Main fixture | `browser_tests/fixtures/ComfyPage.ts` |
| Helper classes | `browser_tests/fixtures/helpers/` |
| Component objects | `browser_tests/fixtures/components/` |
| Test selectors | `browser_tests/fixtures/selectors.ts` |
| Vue Node helpers | `browser_tests/fixtures/VueNodeHelpers.ts` |
| Test assets | `browser_tests/assets/` |
| Existing tests | `browser_tests/tests/` |
**Read the fixture code directly** - it's the source of truth for available methods.

14
.coderabbit.yaml Normal file
View File

@@ -0,0 +1,14 @@
issue_enrichment:
auto_enrich:
enabled: true
reviews:
high_level_summary: false
auto_review:
drafts: true
ignore_title_keywords:
- '[release]'
- '[backport'
ignore_usernames:
- comfy-pr-bot
- github-actions
- github-actions[bot]

15
.gitattributes vendored
View File

@@ -1,16 +1,5 @@
# Default
* text=auto
# Force TS to LF to make the unixy scripts not break on Windows
*.cjs text eol=lf
*.js text eol=lf
*.json text eol=lf
*.mjs text eol=lf
*.mts text eol=lf
*.snap text eol=lf
*.ts text eol=lf
*.vue text eol=lf
*.yaml text eol=lf
# Force all text files to use LF line endings
* text=auto eol=lf
# Generated files
packages/registry-types/src/comfyRegistryTypes.ts linguist-generated=true

View File

@@ -10,10 +10,7 @@ body:
options:
- label: I am running the latest version of ComfyUI
required: true
- label: I have searched existing issues to make sure this isn't a duplicate
required: true
- label: I have tested with all custom nodes disabled ([see how](https://docs.comfy.org/troubleshooting/custom-node-issues#step-1%3A-test-with-all-custom-nodes-disabled))
required: true
- label: I have custom nodes enabled
- type: textarea
id: description

View File

@@ -4,13 +4,6 @@ labels: []
type: Feature
body:
- type: checkboxes
attributes:
label: Is there an existing issue for this?
description: Please search to see if an issue already exists for the problem you're experiencing, and that it's not addressed in a recent build/commit.
options:
- label: I have searched the existing issues and checked the recent builds/commits
required: true
- type: markdown
attributes:
value: |

View File

@@ -104,14 +104,14 @@ runs:
- name: Find existing comment
id: find
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
with:
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
comment-author: github-actions[bot]
body-includes: ${{ steps.build.outputs.marker_search }}
- name: Post or update comment
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
issue-number: ${{ inputs.issue-number || github.event.pull_request.number }}
comment-id: ${{ steps.find.outputs.comment-id }}

View File

@@ -16,7 +16,7 @@ runs:
# Checkout ComfyUI repo, install the dev_tools node and start server
- name: Checkout ComfyUI
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: 'comfyanonymous/ComfyUI'
path: 'ComfyUI'
@@ -33,7 +33,7 @@ runs:
fi
- name: Setup Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: '3.10'

View File

@@ -12,29 +12,17 @@ runs:
# Install pnpm, Node.js, build frontend
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 'lts/*'
cache: 'pnpm'
cache-dependency-path: './pnpm-lock.yaml'
# Restore tool caches before running any build/lint operations
- name: Restore tool output cache
uses: actions/cache/restore@v4
with:
path: |
./.cache
./tsconfig.tsbuildinfo
key: tool-cache-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}-${{ hashFiles('./src/**/*.{ts,vue,js,mts}', './*.config.*') }}
restore-keys: |
tool-cache-${{ runner.os }}-${{ hashFiles('./pnpm-lock.yaml') }}-
tool-cache-${{ runner.os }}-
- name: Install dependencies
shell: bash
run: pnpm install --frozen-lockfile

View File

@@ -11,7 +11,7 @@ runs:
echo "playwright-version=$PLAYWRIGHT_VERSION" >> $GITHUB_OUTPUT
- name: Cache Playwright Browsers
uses: actions/cache@v4
uses: actions/cache@v5 # v5.0.2
id: cache-playwright-browsers
with:
path: '~/.cache/ms-playwright'

View File

@@ -13,15 +13,15 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: 'pnpm'
@@ -36,7 +36,7 @@ jobs:
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update electron-types to ${{ steps.get-version.outputs.NEW_VERSION }}'

View File

@@ -18,15 +18,15 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: 'pnpm'
@@ -35,7 +35,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Checkout ComfyUI-Manager repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: Comfy-Org/ComfyUI-Manager
path: ComfyUI-Manager
@@ -86,7 +86,7 @@ jobs:
- name: Create Pull Request
if: steps.check-changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update ComfyUI-Manager API types from ComfyUI-Manager@${{ steps.manager-info.outputs.commit }}'

View File

@@ -17,15 +17,15 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: lts/*
cache: 'pnpm'
@@ -34,7 +34,7 @@ jobs:
run: pnpm install --frozen-lockfile
- name: Checkout comfy-api repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: Comfy-Org/comfy-api
path: comfy-api
@@ -87,7 +87,7 @@ jobs:
- name: Create Pull Request
if: steps.check-changes.outputs.changed == 'true'
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[chore] Update Comfy Registry API types from comfy-api@${{ steps.api-info.outputs.commit }}'

View File

@@ -0,0 +1,81 @@
name: 'CI: Dist Telemetry Scan'
on:
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project
run: pnpm build
env:
DISTRIBUTION: localhost
- name: Scan dist for GTM telemetry references
run: |
set -euo pipefail
echo '🔍 Scanning for Google Tag Manager references...'
if rg --no-ignore -n \
-g '*.html' \
-g '*.js' \
-e 'Google Tag Manager' \
-e '(?i)\bgtm\.js\b' \
-e '(?i)googletagmanager\.com/gtm\.js\\?id=' \
-e '(?i)googletagmanager\.com/ns\.html\\?id=' \
dist; then
echo '❌ ERROR: Google Tag Manager references found in dist assets!'
echo 'GTM must be properly tree-shaken from OSS builds.'
exit 1
fi
echo '✅ No GTM references found'
- name: Scan dist for Mixpanel telemetry references
run: |
set -euo pipefail
echo '🔍 Scanning for Mixpanel references...'
if rg --no-ignore -n \
-g '*.html' \
-g '*.js' \
-e '(?i)mixpanel\.init' \
-e '(?i)mixpanel\.identify' \
-e 'MixpanelTelemetryProvider' \
-e 'mp\.comfy\.org' \
-e 'mixpanel-browser' \
-e '(?i)mixpanel\.track\(' \
dist; then
echo '❌ ERROR: Mixpanel references found in dist assets!'
echo 'Mixpanel must be properly tree-shaken from OSS builds.'
echo ''
echo 'To fix this:'
echo '1. Use the TelemetryProvider pattern (see src/platform/telemetry/)'
echo '2. Call telemetry via useTelemetry() hook'
echo '3. Use conditional dynamic imports behind isCloud checks'
exit 1
fi
echo '✅ No Mixpanel references found'

View File

@@ -13,6 +13,6 @@ jobs:
json-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Validate JSON syntax
run: ./scripts/cicd/check-json.sh

View File

@@ -18,23 +18,21 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout PR
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ !github.event.pull_request.head.repo.fork && github.head_ref || github.ref }}
token: ${{ !github.event.pull_request.head.repo.fork && secrets.PR_GH_TOKEN || github.token }}
- name: Install pnpm
uses: pnpm/action-setup@v4
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Detect browser_tests changes
id: changed-paths
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
filters: |
browser_tests:
- 'browser_tests/**'
- name: Run ESLint with auto-fix
run: pnpm lint:fix
@@ -70,10 +68,14 @@ jobs:
pnpm format:check
pnpm knip
- name: Typecheck browser tests
if: steps.changed-paths.outputs.browser_tests == 'true'
run: pnpm typecheck:browser
- name: Comment on PR about auto-fix
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name == github.repository
continue-on-error: true
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
github.rest.issues.createComment({
@@ -86,7 +88,7 @@ jobs:
- name: Comment on PR about manual fix needed
if: steps.verify-changed-files.outputs.changed == 'true' && github.event.pull_request.head.repo.full_name != github.repository
continue-on-error: true
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
github.rest.issues.createComment({

View File

@@ -0,0 +1,118 @@
name: 'CI: OSS Assets Validation'
on:
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
push:
branches: [main, dev*]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
validate-fonts:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install pnpm
uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build project
run: pnpm build
env:
DISTRIBUTION: localhost
- name: Check for proprietary fonts in dist
run: |
set -euo pipefail
echo '🔍 Checking dist for proprietary ABCROM fonts...'
if [ ! -d "dist" ] || [ -z "$(ls -A dist)" ]; then
echo '❌ ERROR: dist/ directory missing or empty!'
exit 1
fi
# Check for ABCROM font files
if find dist/ -type f -iname '*abcrom*' \
\( -name '*.woff' -o -name '*.woff2' -o -name '*.ttf' -o -name '*.otf' \) \
-print -quit | grep -q .; then
echo ''
echo '❌ ERROR: Found proprietary ABCROM font files in dist!'
echo ''
find dist/ -type f -iname '*abcrom*' \
\( -name '*.woff' -o -name '*.woff2' -o -name '*.ttf' -o -name '*.otf' \)
echo ''
echo 'ABCROM fonts are proprietary and should not ship to OSS builds.'
echo ''
echo 'To fix this:'
echo '1. Use conditional font loading based on isCloud'
echo '2. Ensure fonts are dynamically imported, not bundled'
echo '3. Check vite config for font handling'
exit 1
fi
echo '✅ No proprietary fonts found in dist'
validate-licenses:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
- name: Install pnpm
uses: pnpm/action-setup@9fd676a19091d4595eefd76e4bd31c97133911f1 # v4.2.0
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6.2.0
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Validate production dependency licenses
run: |
set -euo pipefail
echo '🔍 Checking production dependency licenses...'
# Use license-checker-rseidelsohn (actively maintained fork, handles monorepos)
# Exclude internal @comfyorg packages from license check
# Run in if condition to capture exit code
if npx license-checker-rseidelsohn@4 \
--production \
--summary \
--excludePackages '@comfyorg/comfyui-frontend;@comfyorg/design-system;@comfyorg/registry-types;@comfyorg/shared-frontend-utils;@comfyorg/tailwind-utils;@comfyorg/comfyui-electron-types' \
--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!'
else
echo ''
echo '❌ ERROR: Found dependencies with non-approved licenses!'
echo ''
echo 'To fix this:'
echo '1. Check the license of the problematic package'
echo '2. Find an alternative package with an approved license'
echo '3. If the license is safe and OSI-approved, add it to the --onlyAllow list'
echo ''
echo 'For more info on OSI-approved licenses:'
echo 'https://opensource.org/licenses'
exit 1
fi

136
.github/workflows/ci-perf-report.yaml vendored Normal file
View File

@@ -0,0 +1,136 @@
name: 'CI: Performance Report'
on:
push:
branches: [main, core/*]
paths-ignore: ['**/*.md']
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
paths-ignore: ['**/*.md']
concurrency:
group: perf-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: read
pull-requests: write
jobs:
perf-tests:
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
runs-on: ubuntu-latest
timeout-minutes: 30
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
permissions:
contents: read
packages: read
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
include_build_step: true
- name: Start ComfyUI server
uses: ./.github/actions/start-comfyui-server
- name: Run performance tests
id: perf
continue-on-error: true
run: pnpm exec playwright test --project=performance --workers=1 --repeat-each=3
- name: Upload perf metrics
if: always()
uses: actions/upload-artifact@v6
with:
name: perf-metrics
path: test-results/perf-metrics.json
retention-days: 30
if-no-files-found: warn
report:
needs: perf-tests
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Setup Node
uses: actions/setup-node@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: Download historical perf baselines
continue-on-error: true
run: |
RUNS=$(gh api \
"/repos/${{ github.repository }}/actions/workflows/ci-perf-report.yaml/runs?branch=${{ github.event.pull_request.base.ref }}&event=push&status=success&per_page=5" \
--jq '.workflow_runs[].id' || true)
if [ -z "$RUNS" ]; then
echo "No historical runs available"
exit 0
fi
mkdir -p temp/perf-history
INDEX=0
for RUN_ID in $RUNS; do
DIR="temp/perf-history/$INDEX"
mkdir -p "$DIR"
gh run download "$RUN_ID" -n perf-metrics -D "$DIR/" 2>/dev/null || true
INDEX=$((INDEX + 1))
done
echo "Downloaded $(ls temp/perf-history/*/perf-metrics.json 2>/dev/null | wc -l) historical baselines"
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- 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 -->'

View File

@@ -16,10 +16,10 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.11'

View File

@@ -17,21 +17,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
with:
version: 10
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: '24.x'
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Build project
run: pnpm build
@@ -46,7 +35,7 @@ jobs:
echo ${{ github.base_ref }} > ./temp/size/base.txt
- name: Upload size data
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: size-data
path: temp/size

View File

@@ -6,9 +6,6 @@ on:
workflows: ['CI: Tests E2E']
types: [requested, completed]
env:
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
jobs:
deploy-and-comment-forked-pr:
runs-on: ubuntu-latest
@@ -31,11 +28,11 @@ jobs:
echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}"
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Get PR Number
id: pr
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const { data: prs } = await github.rest.pulls.list({
@@ -63,12 +60,11 @@ jobs:
./scripts/cicd/pr-playwright-deploy-and-comment.sh \
"${{ steps.pr.outputs.result }}" \
"${{ github.event.workflow_run.head_branch }}" \
"starting" \
"$(date -u '${{ env.DATE_FORMAT }}')"
"starting"
- name: Download and Deploy Reports
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed'
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

View File

@@ -4,9 +4,11 @@ name: 'CI: Tests E2E'
on:
push:
branches: [main, master, core/*, desktop/*]
paths-ignore: ['**/*.md']
pull_request:
branches-ignore:
[wip/*, draft/*, temp/*, vue-nodes-migration, sno-playwright-*]
branches-ignore: [wip/*, draft/*, temp/*]
paths-ignore: ['**/*.md']
workflow_dispatch:
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -17,7 +19,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Setup frontend
uses: ./.github/actions/setup-frontend
with:
@@ -25,7 +27,7 @@ jobs:
# Upload only built dist/ (containerized test jobs will pnpm install without cache)
- name: Upload built frontend
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: frontend-dist
path: dist/
@@ -37,7 +39,7 @@ jobs:
runs-on: ubuntu-latest
timeout-minutes: 60
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
@@ -51,9 +53,9 @@ jobs:
shardTotal: [8]
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download built frontend
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: frontend-dist
path: dist/
@@ -72,7 +74,7 @@ jobs:
PLAYWRIGHT_BLOB_OUTPUT_DIR: ./blob-report
- name: Upload blob report
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: ${{ !cancelled() }}
with:
name: blob-report-chromium-${{ matrix.shardIndex }}
@@ -85,7 +87,7 @@ jobs:
needs: setup
runs-on: ubuntu-latest
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
@@ -98,9 +100,9 @@ jobs:
browser: [chromium-2x, chromium-0.5x, mobile-chrome]
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download built frontend
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: frontend-dist
path: dist/
@@ -128,7 +130,7 @@ jobs:
pnpm exec playwright merge-reports --reporter=json ./blob-report
- name: Upload Playwright report
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report-${{ matrix.browser }}
@@ -141,16 +143,13 @@ jobs:
runs-on: ubuntu-latest
if: ${{ !cancelled() }}
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Download blob reports
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
path: ./all-blob-reports
pattern: blob-report-chromium-*
@@ -165,7 +164,7 @@ jobs:
pnpm dlx @playwright/test merge-reports --reporter=json ./all-blob-reports
- name: Upload HTML report
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: playwright-report-chromium
path: ./playwright-report/
@@ -183,11 +182,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Get start time
id: start-time
run: echo "time=$(date -u '+%m/%d/%Y, %I:%M:%S %p')" >> $GITHUB_OUTPUT
uses: actions/checkout@v6
- name: Post starting comment
env:
@@ -197,8 +192,7 @@ jobs:
./scripts/cicd/pr-playwright-deploy-and-comment.sh \
"${{ github.event.pull_request.number }}" \
"${{ github.head_ref }}" \
"starting" \
"${{ steps.start-time.outputs.time }}"
"starting"
# Deploy and comment for non-forked PRs only
deploy-and-comment:
@@ -210,10 +204,10 @@ jobs:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download all playwright reports
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
pattern: playwright-report-*
path: reports

View File

@@ -6,9 +6,6 @@ on:
workflows: ['CI: Tests Storybook']
types: [requested, completed]
env:
DATE_FORMAT: '+%m/%d/%Y, %I:%M:%S %p'
jobs:
deploy-and-comment-forked-pr:
runs-on: ubuntu-latest
@@ -31,11 +28,11 @@ jobs:
echo "Is forked: ${{ github.event.workflow_run.head_repository.full_name != github.event.workflow_run.repository.full_name }}"
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Get PR Number
id: pr
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const { data: prs } = await github.rest.pulls.list({
@@ -63,12 +60,11 @@ jobs:
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
"${{ steps.pr.outputs.result }}" \
"${{ github.event.workflow_run.head_branch }}" \
"starting" \
"$(date -u '${{ env.DATE_FORMAT }}')"
"starting"
- name: Download and Deploy Storybook
if: steps.pr.outputs.result != 'null' && github.event.action == 'completed' && github.event.workflow_run.conclusion == 'success'
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
run-id: ${{ github.event.workflow_run.id }}

View File

@@ -14,7 +14,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Post starting comment
env:
@@ -24,8 +24,7 @@ jobs:
./scripts/cicd/pr-storybook-deploy-and-comment.sh \
"${{ github.event.pull_request.number }}" \
"${{ github.head_ref }}" \
"starting" \
"$(date -u '+%m/%d/%Y, %I:%M:%S %p')"
"starting"
# Build Storybook for all PRs (free Cloudflare deployment)
storybook-build:
@@ -36,21 +35,10 @@ jobs:
workflow-url: ${{ steps.workflow-url.outputs.url }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Build Storybook
run: pnpm build-storybook
@@ -69,7 +57,7 @@ jobs:
- name: Upload Storybook build
if: success() && github.event.pull_request.head.repo.fork == false
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: storybook-static
path: storybook-static/
@@ -86,27 +74,16 @@ jobs:
chromatic-storybook-url: ${{ steps.chromatic.outputs.storybookUrl }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0 # Required for Chromatic baseline
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Build Storybook and run Chromatic
id: chromatic
uses: chromaui/action@latest
uses: chromaui/action@07791f8243f4cb2698bf4d00426baf4b2d1cb7e0 # v13.3.5
with:
projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
buildScriptName: build-storybook
@@ -136,11 +113,11 @@ jobs:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download Storybook build
if: needs.storybook-build.outputs.conclusion == 'success'
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: storybook-static
path: storybook-static
@@ -170,7 +147,7 @@ jobs:
pull-requests: write
steps:
- name: Update comment with Chromatic URLs
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
script: |
const buildUrl = '${{ needs.chromatic-deployment.outputs.chromatic-build-url }}';

View File

@@ -4,8 +4,10 @@ name: 'CI: Tests Unit'
on:
push:
branches: [main, master, dev*, core/*, desktop/*]
paths-ignore: ['**/*.md']
pull_request:
branches-ignore: [wip/*, draft/*, temp/*]
paths-ignore: ['**/*.md']
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
@@ -16,21 +18,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
with:
version: 10
- name: Use Node.js
uses: actions/setup-node@v4
with:
node-version: 'lts/*'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Run Vitest tests
run: pnpm test:unit

View File

@@ -0,0 +1,21 @@
name: Validate Action SHA Pins
on:
pull_request:
paths:
- '.github/workflows/**'
- '.github/actions/**'
- '.pinact.yaml'
permissions:
contents: read
jobs:
validate-pins:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: suzuki-shunsuke/pinact-action@3d49c6412901042473ffa78becddab1aea46bbea # v1.3.1
with:
skip_push: 'true'

View File

@@ -17,10 +17,10 @@ jobs:
yaml-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: '3.x'

View File

@@ -18,12 +18,12 @@ jobs:
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version-file: '.nvmrc'

View File

@@ -0,0 +1,45 @@
---
# 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'
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
if: github.repository == 'Comfy-Org/ComfyUI_frontend'
runs-on: ubuntu-latest
steps:
- name: Build client payload
id: payload
run: |
payload="$(jq -nc \
--arg ref "${GITHUB_SHA}" \
--arg branch "${GITHUB_REF_NAME}" \
'{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

@@ -16,7 +16,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
token: ${{ secrets.PR_GH_TOKEN }}
# Setup playwright environment
- name: Setup ComfyUI Frontend

View File

@@ -22,7 +22,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# Setup playwright environment with custom node repository
- name: Setup ComfyUI Server (without launching)
@@ -36,7 +36,7 @@ jobs:
# Install the custom node repository
- name: Checkout custom node repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: ${{ inputs.owner }}/${{ inputs.repository }}
path: 'ComfyUI/custom_nodes/${{ inputs.repository }}'
@@ -113,7 +113,7 @@ jobs:
git commit -m "Update locales"
- name: Install SSH key For PUSH
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4
uses: shimataro/ssh-key-action@d4fffb50872869abe2d9a9098a6d9c5aa7d16be4 # v2.7.0
with:
# PR private key from action server
key: ${{ secrets.PR_SSH_PRIVATE_KEY }}

View File

@@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
# Setup playwright environment
- name: Setup ComfyUI Server (and start)
uses: ./.github/actions/setup-comfyui-server
@@ -40,7 +40,7 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: 'Update locales for node definitions'

View File

@@ -64,7 +64,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0

View File

@@ -23,18 +23,18 @@ jobs:
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
ref: refs/pull/${{ github.event.pull_request.number }}/head
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'pnpm'
@@ -44,7 +44,7 @@ jobs:
pnpm install -g typescript @vue/compiler-sfc
- name: Run Claude PR Review
uses: anthropics/claude-code-action@v1.0.6
uses: anthropics/claude-code-action@ff34ce0ff04a470bd3fa56c1ef391c8f1c19f8e9 # v1.0.38
with:
label_trigger: 'claude-review'
prompt: |

View File

@@ -33,24 +33,13 @@ jobs:
github.event_name == 'workflow_dispatch'
)
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4.1.0
with:
version: 10
- name: Install Node.js
uses: actions/setup-node@v5
with:
node-version: '24.x'
cache: pnpm
- name: Install dependencies
run: pnpm install
- name: Setup frontend
uses: ./.github/actions/setup-frontend
- name: Download size data
uses: dawidd6/action-download-artifact@v11
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
name: size-data
run_id: ${{ github.event_name == 'workflow_dispatch' && inputs.run_id || github.event.workflow_run.id }}
@@ -75,7 +64,7 @@ jobs:
fi
- name: Download previous size data
uses: dawidd6/action-download-artifact@v11
uses: dawidd6/action-download-artifact@0bd50d53a6d7fb5cb921e607957e9cc12b4ce392 # v12
with:
branch: ${{ steps.pr-base.outputs.content }}
workflow: ci-size-data.yaml
@@ -89,12 +78,12 @@ jobs:
- name: Read size report
id: size-report
uses: juliangruber/read-file-action@v1
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@v3
uses: actions-cool/maintain-one-comment@4b2dbf086015f892dcb5e8c1106f5fccd6c1476b # v3.2.0
with:
token: ${{ secrets.GITHUB_TOKEN }}
number: ${{ steps.pr-number.outputs.content }}

View File

@@ -38,7 +38,7 @@ jobs:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Find Update Comment
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad
uses: peter-evans/find-comment@b30e6a3c0ed37e7c023ccd3f1db5c6c0b0c23aad # v4.0.0
id: 'find-update-comment'
with:
issue-number: ${{ steps.pr-info.outputs.pr-number }}
@@ -46,7 +46,7 @@ jobs:
body-includes: 'Updating Playwright Expectations'
- name: Add Starting Reaction
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
with:
comment-id: ${{ steps.find-update-comment.outputs.comment-id }}
issue-number: ${{ steps.pr-info.outputs.pr-number }}
@@ -56,7 +56,7 @@ jobs:
reactions: eyes
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ steps.pr-info.outputs.branch }}
- name: Setup frontend
@@ -66,7 +66,7 @@ jobs:
# Upload built dist/ (containerized test jobs will pnpm install without cache)
- name: Upload built frontend
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: frontend-dist
path: dist/
@@ -77,7 +77,7 @@ jobs:
needs: setup
runs-on: ubuntu-latest
container:
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.10
image: ghcr.io/comfy-org/comfyui-ci-container:0.0.12
credentials:
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
@@ -91,11 +91,11 @@ jobs:
shardTotal: [4]
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ needs.setup.outputs.branch }}
- name: Download built frontend
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: frontend-dist
path: dist/
@@ -109,7 +109,7 @@ jobs:
# Run sharded tests with snapshot updates (browsers pre-installed in container)
- name: Update snapshots (Shard ${{ matrix.shardIndex }}/${{ matrix.shardTotal }})
id: playwright-tests
run: pnpm exec playwright test --update-snapshots --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
run: pnpm exec playwright test --update-snapshots --grep @screenshot --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
continue-on-error: true
- name: Stage changed snapshot files
@@ -149,7 +149,7 @@ jobs:
# Upload ONLY the changed files from this shard
- name: Upload changed snapshots
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: steps.changed-snapshots.outputs.has-changes == 'true'
with:
name: snapshots-shard-${{ matrix.shardIndex }}
@@ -157,7 +157,7 @@ jobs:
retention-days: 1
- name: Upload test report
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
if: always()
with:
name: playwright-report-shard-${{ matrix.shardIndex }}
@@ -170,17 +170,18 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ needs.setup.outputs.branch }}
token: ${{ secrets.PR_GH_TOKEN }}
# Download all changed snapshot files from shards
- name: Download snapshot artifacts
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
pattern: snapshots-shard-*
path: ./downloaded-snapshots
merge-multiple: false
merge-multiple: true
- name: List downloaded files
run: |
@@ -206,13 +207,13 @@ jobs:
echo "MERGING CHANGED SNAPSHOTS"
echo "=========================================="
# Check if any artifacts were downloaded
# Check if any artifacts were downloaded (merge-multiple puts files directly in path)
if [ ! -d "./downloaded-snapshots" ]; then
echo "No snapshot artifacts to merge"
echo "=========================================="
echo "MERGE COMPLETE"
echo "=========================================="
echo "Shards merged: 0"
echo "Files merged: 0"
exit 0
fi
@@ -222,37 +223,29 @@ jobs:
exit 1
fi
merged_count=0
# Count files to merge
file_count=$(find ./downloaded-snapshots -type f | wc -l)
# For each shard's changed files, copy them directly
for shard_dir in ./downloaded-snapshots/snapshots-shard-*/; do
if [ ! -d "$shard_dir" ]; then
continue
fi
if [ "$file_count" -eq 0 ]; then
echo "No snapshot files found in downloaded artifacts"
echo "=========================================="
echo "MERGE COMPLETE"
echo "=========================================="
echo "Files merged: 0"
exit 0
fi
shard_name=$(basename "$shard_dir")
file_count=$(find "$shard_dir" -type f | wc -l)
echo "Merging $file_count snapshot file(s)..."
if [ "$file_count" -eq 0 ]; then
echo " $shard_name: no files"
continue
fi
echo "Processing $shard_name ($file_count file(s))..."
# Copy files directly, preserving directory structure
# Since files are already in correct structure (no browser_tests/ prefix), just copy them all
cp -v -r "$shard_dir"* browser_tests/ 2>&1 | sed 's/^/ /'
merged_count=$((merged_count + 1))
echo " ✓ Merged"
echo ""
done
# Copy all files directly, preserving directory structure
# With merge-multiple: true, files are directly in ./downloaded-snapshots/ without shard subdirs
cp -v -r ./downloaded-snapshots/* browser_tests/ 2>&1 | sed 's/^/ /'
echo ""
echo "=========================================="
echo "MERGE COMPLETE"
echo "=========================================="
echo "Shards merged: $merged_count"
echo "Files merged: $file_count"
- name: Show changes
run: |
@@ -301,7 +294,7 @@ jobs:
echo "✓ Commit and push successful"
- name: Add Done Reaction
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9
uses: peter-evans/create-or-update-comment@e8674b075228eee787fea43ef493e45ece1004c9 # v5.0.0
if: github.event_name == 'issue_comment' && steps.commit.outputs.has-changes == 'true'
with:
comment-id: ${{ needs.setup.outputs.comment-id }}

View File

@@ -20,13 +20,13 @@ jobs:
dist_tag: ${{ steps.dist.outputs.dist_tag }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
persist-credentials: false
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: '24.x'
@@ -71,7 +71,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 2

View File

@@ -77,19 +77,19 @@ jobs:
fi
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ steps.resolve_ref.outputs.ref }}
fetch-depth: 1
persist-credentials: false
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: '24.x'
cache: 'pnpm'

View File

@@ -61,13 +61,13 @@ jobs:
steps:
- name: Checkout ComfyUI_frontend
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
path: frontend
- name: Checkout ComfyUI (sparse)
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: Comfy-Org/ComfyUI
sparse-checkout: |
@@ -75,12 +75,12 @@ jobs:
path: comfyui
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: lts/*
@@ -169,7 +169,7 @@ jobs:
steps:
- name: Checkout ComfyUI fork
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
repository: ${{ inputs.comfyui_fork || 'Comfy-Org/ComfyUI' }}
token: ${{ secrets.PR_GH_TOKEN }}

View File

@@ -18,13 +18,13 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
token: ${{ secrets.PR_GH_TOKEN || secrets.GITHUB_TOKEN }}
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: 'lts/*'

View File

@@ -19,12 +19,12 @@ jobs:
is_prerelease: ${{ steps.check_prerelease.outputs.is_prerelease }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 'lts/*'
cache: 'pnpm'
@@ -50,37 +50,43 @@ jobs:
ALGOLIA_API_KEY: ${{ secrets.ALGOLIA_API_KEY }}
ENABLE_MINIFY: 'true'
USE_PROD_CONFIG: 'true'
IS_NIGHTLY: ${{ case(github.ref == 'refs/heads/main', 'true', 'false') }}
run: |
pnpm install --frozen-lockfile
pnpm build
# Desktop-specific release artifact with desktop distribution flags.
DISTRIBUTION=desktop pnpm build
pnpm zipdist ./dist ./dist-desktop.zip
# Default release artifact for core/PyPI.
NX_SKIP_NX_CACHE=true pnpm build
pnpm zipdist
- name: Upload dist artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: dist-files
path: |
dist/
dist.zip
dist-desktop.zip
draft_release:
needs: build
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Download dist artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: dist-files
- name: Create release
id: create_release
uses: >-
softprops/action-gh-release@da05d552573ad5aba039eaac05058a918a7bf631
uses: softprops/action-gh-release@a06a81a03ee405af7f2048a818ed3f03bbf83c7b # v2.5.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: |
dist.zip
dist-desktop.zip
tag_name: v${{ needs.build.outputs.version }}
target_commitish: ${{ github.event.pull_request.base.ref }}
make_latest: >-
@@ -98,13 +104,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download dist artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: dist-files
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Install build dependencies
@@ -119,8 +125,7 @@ jobs:
env:
COMFYUI_FRONTEND_VERSION: ${{ needs.build.outputs.version }}
- name: Publish pypi package
uses: >-
pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
password: ${{ secrets.PYPI_TOKEN }}
packages-dir: comfyui_frontend_package/dist
@@ -147,7 +152,7 @@ jobs:
pull-requests: write
steps:
- name: Checkout merge commit
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.pull_request.merge_commit_sha }}
fetch-depth: 2

View File

@@ -69,18 +69,18 @@ jobs:
fi
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ steps.resolve_ref.outputs.ref }}
fetch-depth: 1
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: 'lts/*'
cache: 'pnpm'

View File

@@ -15,12 +15,12 @@ jobs:
version: ${{ steps.current_version.outputs.version }}
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- uses: actions/setup-node@v4
- uses: actions/setup-node@v6
with:
node-version: 'lts/*'
cache: 'pnpm'
@@ -40,7 +40,7 @@ jobs:
pnpm build
pnpm zipdist
- name: Upload dist artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: dist-files
path: |
@@ -52,13 +52,13 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
uses: actions/checkout@v6
- name: Download dist artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v7
with:
name: dist-files
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v6
with:
python-version: '3.x'
- name: Install build dependencies
@@ -73,7 +73,7 @@ jobs:
env:
COMFYUI_FRONTEND_VERSION: ${{ format('{0}.dev{1}', needs.build.outputs.version, inputs.devVersion) }}
- name: Publish pypi package
uses: pypa/gh-action-pypi-publish@76f52bc884231f62b9a034ebfe128415bbaabdfc
uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0
with:
password: ${{ secrets.PYPI_TOKEN }}
packages-dir: comfyui_frontend_package/dist

View File

@@ -65,7 +65,7 @@ jobs:
- name: Close stale nightly version bump PRs
if: github.event_name == 'schedule'
uses: actions/github-script@v7
uses: actions/github-script@v8
with:
github-token: ${{ github.token }}
script: |
@@ -118,7 +118,7 @@ jobs:
core.info(`Closed ${closed.length} stale PR(s).`)
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ steps.prepared-inputs.outputs.branch }}
fetch-depth: 0
@@ -142,12 +142,12 @@ jobs:
echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: lts/*
@@ -180,7 +180,7 @@ jobs:
echo "capitalised=${CAPITALISED_TYPE@u}" >> "$GITHUB_OUTPUT"
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[release] Increment version to ${{ steps.bump-version.outputs.NEW_VERSION }}'

View File

@@ -29,7 +29,7 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
ref: ${{ github.event.inputs.branch }}
fetch-depth: 0
@@ -51,12 +51,12 @@ jobs:
echo "✅ Branch '$BRANCH' exists"
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v5
uses: actions/setup-node@v6
with:
node-version: '24.x'
cache: 'pnpm'
@@ -79,7 +79,7 @@ jobs:
echo "capitalised=${VERSION_TYPE@u}" >> $GITHUB_OUTPUT
- name: Create Pull Request
uses: peter-evans/create-pull-request@271a8d0340265f705b14b6d32b9829c1cb33d45e
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: '[release] Increment desktop-ui to ${{ steps.bump-version.outputs.NEW_VERSION }}'

View File

@@ -22,18 +22,18 @@ jobs:
timeout-minutes: 45
steps:
- name: Checkout repository
uses: actions/checkout@v5
uses: actions/checkout@v6
with:
fetch-depth: 0
fetch-depth: 50
ref: main
- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@41ff72655975bd51cab0327fa583b6e92b6d3061 # v4.2.0
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v6
with:
node-version: '20'
cache: 'pnpm'
@@ -49,7 +49,7 @@ jobs:
fi
- name: Run Claude Documentation Review
uses: anthropics/claude-code-action@v1.0.6
uses: anthropics/claude-code-action@ff34ce0ff04a470bd3fa56c1ef391c8f1c19f8e9 # v1.0.38
with:
prompt: |
Is all documentation still 100% accurate?
@@ -130,7 +130,7 @@ jobs:
- name: Create or Update Pull Request
if: steps.check_changes.outputs.has_changes == 'true'
uses: peter-evans/create-pull-request@v7
uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8.1.0
with:
token: ${{ secrets.PR_GH_TOKEN }}
commit-message: 'docs: weekly documentation accuracy update'

3
.gitignore vendored
View File

@@ -64,6 +64,7 @@ browser_tests/local/
dist.zip
/temp/
/tmp/
# Generated JSON Schemas
/schemas/
@@ -96,3 +97,5 @@ vitest.config.*.timestamp*
# Weekly docs check output
/output.txt
.amp

View File

@@ -35,7 +35,7 @@
}
],
"no-control-regex": "off",
"no-eval": "off",
"no-eval": "error",
"no-redeclare": "error",
"no-restricted-imports": [
"error",
@@ -60,11 +60,6 @@
{
"name": "primevue/sidebar",
"message": "Sidebar is deprecated in PrimeVue 4+. Use Drawer instead: import Drawer from 'primevue/drawer'"
},
{
"name": "@/i18n--to-enable",
"importNames": ["st", "t", "te", "d"],
"message": "Don't import `@/i18n` directly, prefer `useI18n()`"
}
]
}
@@ -101,6 +96,7 @@
"typescript/restrict-template-expressions": "off",
"typescript/unbound-method": "off",
"typescript/no-floating-promises": "error",
"typescript/no-explicit-any": "error",
"vue/no-import-compiler-macros": "error",
"vue/no-dupe-keys": "error"
},
@@ -110,6 +106,17 @@
"rules": {
"no-console": "allow"
}
},
{
"files": ["browser_tests/**/*.ts"],
"rules": {
"typescript/no-explicit-any": "error",
"no-async-promise-executor": "error",
"no-control-regex": "error",
"no-useless-rename": "error",
"no-unused-private-class-members": "error",
"unicorn/no-empty-file": "error"
}
}
]
}

24
.pinact.yaml Normal file
View File

@@ -0,0 +1,24 @@
# pinact configuration
# https://github.com/suzuki-shunsuke/pinact
version: 3
files:
- pattern: .github/workflows/*.yaml
- pattern: .github/actions/**/*.yaml
# Actions that don't need SHA pinning (official GitHub actions are trusted)
ignore_actions:
- name: actions/cache
ref: v5
- name: actions/checkout
ref: v6
- name: actions/setup-node
ref: v6
- name: actions/setup-python
ref: v6
- name: actions/upload-artifact
ref: v6
- name: actions/download-artifact
ref: v7
- name: actions/github-script
ref: v8

View File

@@ -98,12 +98,10 @@ const config: StorybookConfig = {
},
build: {
rolldownOptions: {
experimental: {
strictExecutionOrder: true
},
treeshake: false,
output: {
keepNames: true
keepNames: true,
strictExecutionOrder: true
},
onwarn: (warning, warn) => {
// Suppress specific warnings

View File

@@ -90,7 +90,6 @@ const preview: Preview = {
{ value: 'light', icon: 'sun', title: 'Light' },
{ value: 'dark', icon: 'moon', title: 'Dark' }
],
showName: true,
dynamicTitle: true
}
}

View File

@@ -40,26 +40,27 @@
"block-no-empty": true,
"no-descending-specificity": null,
"no-duplicate-at-import-rules": true,
"at-rule-disallowed-list": ["apply"],
"at-rule-no-unknown": [
true,
{
"ignoreAtRules": [
"tailwind",
"apply",
"layer",
"config",
"theme",
"reference",
"plugin",
"custom-variant",
"utility"
"utility",
"source"
]
}
],
"function-no-unknown": [
true,
{
"ignoreFunctions": ["theme", "v-bind"]
"ignoreFunctions": ["theme", "v-bind", "from-folder", "from-json"]
}
]
},

View File

@@ -8,3 +8,6 @@ rules:
line-length: disable
document-start: disable
truthy: disable
comments:
min-spaces-from-content: 1

View File

@@ -21,7 +21,7 @@ See @docs/guidance/\*.md for file-type-specific conventions (auto-loaded by glob
- i18n: `src/i18n.ts`,
- Entry Point: `src/main.ts`.
- Tests:
- unit/component in `tests-ui/` and `src/**/*.test.ts`
- unit/component in `src/**/*.test.ts`
- E2E (Playwright) in `browser_tests/**/*.spec.ts`
- Public assets: `public/`
- Build output: `dist/`
@@ -37,6 +37,10 @@ See @docs/guidance/\*.md for file-type-specific conventions (auto-loaded by glob
The project uses **Nx** for build orchestration and task management
## Package Manager
This project uses **pnpm**. Always prefer scripts defined in `package.json` (e.g., `pnpm test:unit`, `pnpm lint`). To run arbitrary packages not in scripts, use `pnpx` or `pnpm dlx` — never `npx`.
## Build, Test, and Development Commands
- `pnpm dev`: Start Vite dev server.
@@ -44,7 +48,7 @@ The project uses **Nx** for build orchestration and task management
- `pnpm build`: Type-check then production build to `dist/`
- `pnpm preview`: Preview the production build locally
- `pnpm test:unit`: Run Vitest unit tests
- `pnpm test:browser`: Run Playwright E2E tests (`browser_tests/`)
- `pnpm test:browser:local`: Run Playwright E2E tests (`browser_tests/`)
- `pnpm lint` / `pnpm lint:fix`: Lint (ESLint)
- `pnpm format` / `pnpm format:check`: oxfmt
- `pnpm typecheck`: Vue TSC type checking
@@ -264,7 +268,7 @@ A particular type of complexity is over-engineering, where developers have made
## Repository Navigation
- Check README files in key folders (tests-ui, browser_tests, composables, etc.)
- Check README files in key folders (browser_tests, composables, etc.)
- Prefer running single tests for performance
- Use --help for unfamiliar CLI tools

View File

@@ -2,57 +2,57 @@
* @Comfy-org/comfy_frontend_devs
# Desktop/Electron
/apps/desktop-ui/ @benceruleanlu
/src/stores/electronDownloadStore.ts @benceruleanlu
/src/extensions/core/electronAdapter.ts @benceruleanlu
/vite.electron.config.mts @benceruleanlu
/apps/desktop-ui/ @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/stores/electronDownloadStore.ts @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/extensions/core/electronAdapter.ts @benceruleanlu @Comfy-org/comfy_frontend_devs
/vite.electron.config.mts @benceruleanlu @Comfy-org/comfy_frontend_devs
# Common UI Components
/src/components/chip/ @viva-jinyi
/src/components/card/ @viva-jinyi
/src/components/button/ @viva-jinyi
/src/components/input/ @viva-jinyi
/src/components/chip/ @viva-jinyi @Comfy-org/comfy_frontend_devs
/src/components/card/ @viva-jinyi @Comfy-org/comfy_frontend_devs
/src/components/button/ @viva-jinyi @Comfy-org/comfy_frontend_devs
/src/components/input/ @viva-jinyi @Comfy-org/comfy_frontend_devs
# Topbar
/src/components/topbar/ @pythongosssss
/src/components/topbar/ @pythongosssss @Comfy-org/comfy_frontend_devs
# Thumbnail
/src/renderer/core/thumbnail/ @pythongosssss
/src/renderer/core/thumbnail/ @pythongosssss @Comfy-org/comfy_frontend_devs
# Legacy UI
/scripts/ui/ @pythongosssss
/scripts/ui/ @pythongosssss @Comfy-org/comfy_frontend_devs
# Link rendering
/src/renderer/core/canvas/links/ @benceruleanlu
/src/renderer/core/canvas/links/ @benceruleanlu @Comfy-org/comfy_frontend_devs
# Partner Nodes
/src/composables/node/useNodePricing.ts @jojodecayz @bigcat88
/src/composables/node/useNodePricing.ts @jojodecayz @bigcat88 @Comfy-org/comfy_frontend_devs
# Node help system
/src/utils/nodeHelpUtil.ts @benceruleanlu
/src/stores/workspace/nodeHelpStore.ts @benceruleanlu
/src/services/nodeHelpService.ts @benceruleanlu
/src/utils/nodeHelpUtil.ts @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/stores/workspace/nodeHelpStore.ts @benceruleanlu @Comfy-org/comfy_frontend_devs
/src/services/nodeHelpService.ts @benceruleanlu @Comfy-org/comfy_frontend_devs
# Selection toolbox
/src/components/graph/selectionToolbox/ @Myestery
/src/components/graph/selectionToolbox/ @Myestery @Comfy-org/comfy_frontend_devs
# Minimap
/src/renderer/extensions/minimap/ @jtydhr88 @Myestery
/src/renderer/extensions/minimap/ @jtydhr88 @Myestery @Comfy-org/comfy_frontend_devs
# Workflow Templates
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki
/src/components/templates/ @Myestery @christian-byrne @comfyui-wiki
/src/platform/workflow/templates/ @Myestery @christian-byrne @comfyui-wiki @Comfy-org/comfy_frontend_devs
/src/components/templates/ @Myestery @christian-byrne @comfyui-wiki @Comfy-org/comfy_frontend_devs
# Mask Editor
/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp
/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp
/src/extensions/core/maskeditor.ts @trsommer @brucew4yn3rp @Comfy-org/comfy_frontend_devs
/src/extensions/core/maskEditorLayerFilenames.ts @trsommer @brucew4yn3rp @Comfy-org/comfy_frontend_devs
# 3D
/src/extensions/core/load3d.ts @jtydhr88
/src/components/load3d/ @jtydhr88
/src/extensions/core/load3d.ts @jtydhr88 @Comfy-org/comfy_frontend_devs
/src/components/load3d/ @jtydhr88 @Comfy-org/comfy_frontend_devs
# Manager
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata
/src/workbench/extensions/manager/ @viva-jinyi @christian-byrne @ltdrdata @Comfy-org/comfy_frontend_devs
# Translations
/src/locales/ @Comfy-Org/comfy_maintainer @Comfy-org/comfy_frontend_devs

View File

@@ -201,7 +201,7 @@ The project supports three types of icons, all with automatic imports (no manual
2. **Iconify Icons** - 200,000+ icons from various libraries: `<i class="icon-[lucide--settings]" />`, `<i class="icon-[mdi--folder]" />`
3. **Custom Icons** - Your own SVG icons: `<i-comfy:workflow />`
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Custom icons are stored in `packages/design-system/src/icons/` and processed by `packages/design-system/src/iconCollection.ts` with automatic validation.
Icons are powered by the unplugin-icons system, which automatically discovers and imports icons as Vue components. Tailwind CSS icon classes (`icon-[comfy--template]`) are provided by `@iconify/tailwind4`, configured in `packages/design-system/src/css/style.css`. Custom icons are stored in `packages/design-system/src/icons/` and loaded via `from-folder` at build time.
For detailed instructions and code examples, see [packages/design-system/src/icons/README.md](packages/design-system/src/icons/README.md).

View File

@@ -61,8 +61,7 @@
"^build"
],
"options": {
"cwd": "apps/desktop-ui",
"command": "vite build --config vite.config.mts"
"command": "vite build --config apps/desktop-ui/vite.config.mts"
},
"outputs": [
"{projectRoot}/dist"

View File

@@ -4,3 +4,39 @@
position: absolute;
inset: 0;
}
.p-button-secondary {
border: none;
background-color: var(--color-neutral-600);
color: var(--color-white);
}
.p-button-secondary:hover {
background-color: var(--color-neutral-550);
}
.p-button-secondary:active {
background-color: var(--color-neutral-500);
}
.p-button-danger {
background-color: var(--color-coral-red-600);
}
.p-button-danger:hover {
background-color: var(--color-coral-red-500);
}
.p-button-danger:active {
background-color: var(--color-coral-red-400);
}
.task-div .p-card {
transition: opacity var(--default-transition-duration);
--p-card-background: var(--p-button-secondary-background);
opacity: 0.9;
}
.task-div .p-card:hover {
opacity: 1;
}

View File

@@ -101,13 +101,15 @@ onUnmounted(() => {
</script>
<style scoped>
@reference '../../../../assets/css/style.css';
/* xterm renders its internal DOM outside Vue templates, so :deep selectors are
* required to style those generated nodes.
*/
:deep(.p-terminal) .xterm {
@apply overflow-hidden;
overflow: hidden;
}
:deep(.p-terminal) .xterm-screen {
@apply bg-neutral-900 overflow-hidden;
overflow: hidden;
background-color: var(--color-neutral-900);
}
</style>

View File

@@ -7,7 +7,7 @@
option-value="value"
:disabled="isSwitching"
:pt="dropdownPt"
:size="props.size"
:size="size"
class="language-selector"
@change="onLocaleChange"
>
@@ -36,16 +36,10 @@ import { i18n, loadLocale, st } from '@/i18n'
type VariantKey = 'dark' | 'light'
type SizeKey = 'small' | 'large'
const props = withDefaults(
defineProps<{
variant?: VariantKey
size?: SizeKey
}>(),
{
variant: 'dark',
size: 'small'
}
)
const { variant = 'dark', size = 'small' } = defineProps<{
variant?: VariantKey
size?: SizeKey
}>()
const dropdownId = `language-select-${Math.random().toString(36).slice(2)}`
@@ -104,10 +98,8 @@ const VARIANT_PRESETS = {
const selectedLocale = ref<string>(i18n.global.locale.value)
const isSwitching = ref(false)
const sizePreset = computed(() => SIZE_PRESETS[props.size as SizeKey])
const variantPreset = computed(
() => VARIANT_PRESETS[props.variant as VariantKey]
)
const sizePreset = computed(() => SIZE_PRESETS[size])
const variantPreset = computed(() => VARIANT_PRESETS[variant])
const dropdownPt = computed(() => ({
root: {
@@ -195,13 +187,17 @@ async function onLocaleChange(event: SelectChangeEvent) {
</script>
<style scoped>
@reference '../../assets/css/style.css';
:deep(.p-dropdown-panel .p-dropdown-item) {
@apply transition-colors;
transition-property: color, background-color, border-color;
transition-duration: var(--default-transition-duration);
}
:deep(.p-dropdown) {
@apply focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-yellow/60 focus-visible:ring-offset-2;
&:focus-visible {
outline: none;
box-shadow:
0 0 0 2px var(--color-neutral-900),
0 0 0 4px color-mix(in srgb, var(--color-brand-yellow) 60%, transparent);
}
}
</style>

View File

@@ -269,26 +269,43 @@ const onFocus = async () => {
</script>
<style scoped>
@reference '../../assets/css/style.css';
:deep(.location-picker-accordion) {
@apply px-12;
padding-inline: calc(var(--spacing) * 12);
.p-accordionpanel {
@apply border-0 bg-transparent;
border: 0;
background-color: transparent;
}
.p-accordionheader {
@apply bg-neutral-800/50 border-0 rounded-xl mt-2 hover:bg-neutral-700/50;
margin-top: calc(var(--spacing) * 2);
border: 0;
border-radius: var(--radius-xl);
background-color: color-mix(
in srgb,
var(--color-neutral-800) 50%,
transparent
);
transition:
background-color 0.2s ease,
border-radius 0.5s ease;
&:hover {
background-color: color-mix(
in srgb,
var(--color-neutral-700) 50%,
transparent
);
}
}
/* When panel is expanded, adjust header border radius */
.p-accordionpanel-active {
.p-accordionheader {
@apply rounded-t-xl rounded-b-none;
border-top-left-radius: var(--radius-xl);
border-top-right-radius: var(--radius-xl);
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.p-accordionheader-toggle-icon {
@@ -299,11 +316,24 @@ const onFocus = async () => {
}
.p-accordioncontent {
@apply bg-neutral-800/50 border-0 rounded-b-xl rounded-t-none;
border: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
border-bottom-right-radius: var(--radius-xl);
border-bottom-left-radius: var(--radius-xl);
background-color: color-mix(
in srgb,
var(--color-neutral-800) 50%,
transparent
);
}
.p-accordioncontent-content {
@apply bg-transparent pt-3 pr-5 pb-5 pl-5;
background-color: transparent;
padding-top: calc(var(--spacing) * 3);
padding-right: calc(var(--spacing) * 5);
padding-bottom: calc(var(--spacing) * 5);
padding-left: calc(var(--spacing) * 5);
}
/* Override default chevron icons to use up/down */

View File

@@ -1,11 +1,20 @@
<template>
<div
class="task-div relative grid min-h-52 max-w-48"
:class="{ 'opacity-75': isLoading }"
:class="
cn(
'task-div group/task-card relative grid min-h-52 max-w-48',
isLoading && 'opacity-75'
)
"
>
<Card
class="relative h-full max-w-48 overflow-hidden"
:class="{ 'opacity-65': runner.state !== 'error' }"
:class="
cn(
'relative h-full max-w-48 overflow-hidden',
runner.state !== 'error' && 'opacity-65'
)
"
:pt="cardPt"
v-bind="(({ onClick, ...rest }) => rest)($attrs)"
>
<template #header>
@@ -43,7 +52,7 @@
<i
v-if="!isLoading && runner.state === 'OK'"
class="task-card-ok pi pi-check"
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]"
/>
</div>
</template>
@@ -55,6 +64,7 @@ import { computed } from 'vue'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import type { MaintenanceTask } from '@/types/desktop/maintenanceTypes'
import { cn } from '@/utils/tailwindUtil'
import { useMinLoadingDurationRef } from '@/utils/refUtil'
const taskStore = useMaintenanceTaskStore()
@@ -83,51 +93,9 @@ const reactiveExecuting = computed(() => !!runner.value.executing)
const isLoading = useMinLoadingDurationRef(reactiveLoading, 250)
const isExecuting = useMinLoadingDurationRef(reactiveExecuting, 250)
const cardPt = {
header: { class: 'z-0' },
body: { class: 'z-[1] grow justify-between' }
}
</script>
<style scoped>
@reference '../../assets/css/style.css';
.task-card-ok {
@apply text-green-500 absolute -right-4 -bottom-4 opacity-100 row-span-full col-span-full transition-opacity;
font-size: 4rem;
text-shadow: 0.25rem 0 0.5rem black;
z-index: 10;
}
.p-card {
@apply transition-opacity;
--p-card-background: var(--p-button-secondary-background);
opacity: 0.9;
&.opacity-65 {
opacity: 0.4;
}
&:hover {
opacity: 1;
}
}
:deep(.p-card-header) {
z-index: 0;
}
:deep(.p-card-body) {
z-index: 1;
flex-grow: 1;
justify-content: space-between;
}
.task-div {
> i {
pointer-events: none;
}
&:hover > i {
opacity: 0.2;
}
}
</style>

View File

@@ -1,10 +1,10 @@
<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">
{{ t(`desktopDialogs.${id}.title`, title) }}
{{ $t(`desktopDialogs.${id}.title`, title) }}
</h1>
<p class="whitespace-pre-wrap">
{{ t(`desktopDialogs.${id}.message`, message) }}
{{ $t(`desktopDialogs.${id}.message`, message) }}
</p>
<div class="flex w-full gap-2">
<Button
@@ -12,7 +12,7 @@
:key="button.label"
class="rounded-lg first:mr-auto"
:label="
t(
$t(
`desktopDialogs.${id}.buttons.${normalizeI18nKey(button.label)}`,
button.label
)
@@ -31,7 +31,6 @@ import { useRoute } from 'vue-router'
import { getDialog } from '@/constants/desktopDialogs'
import type { DialogAction } from '@/constants/desktopDialogs'
import { t } from '@/i18n'
import { electronAPI } from '@/utils/envUtil'
const route = useRoute()
@@ -41,31 +40,3 @@ const handleButtonClick = async (button: DialogAction) => {
await electronAPI().Dialog.clickButton(button.returnValue)
}
</script>
<style scoped>
@reference '../assets/css/style.css';
.p-button-secondary {
@apply text-white border-none bg-neutral-600;
}
.p-button-secondary:hover {
@apply bg-neutral-550;
}
.p-button-secondary:active {
@apply bg-neutral-500;
}
.p-button-danger {
@apply bg-coral-red-600;
}
.p-button-danger:hover {
@apply bg-coral-red-500;
}
.p-button-danger:active {
@apply bg-coral-red-400;
}
</style>

View File

@@ -6,11 +6,11 @@
<div class="relative m-8 text-center">
<!-- Header -->
<h1 class="download-bg pi-download text-4xl font-bold">
{{ t('desktopUpdate.title') }}
{{ $t('desktopUpdate.title') }}
</h1>
<div class="m-8">
<span>{{ t('desktopUpdate.description') }}</span>
<span>{{ $t('desktopUpdate.description') }}</span>
</div>
<ProgressSpinner class="m-8 w-48 h-48" />
@@ -19,7 +19,7 @@
<Button
style="transform: translateX(-50%)"
class="fixed bottom-0 left-1/2 my-8"
:label="t('maintenance.consoleLogs')"
:label="$t('maintenance.consoleLogs')"
icon="pi pi-desktop"
icon-pos="left"
severity="secondary"
@@ -28,8 +28,8 @@
<TerminalOutputDrawer
v-model="terminalVisible"
:header="t('g.terminal')"
:default-message="t('desktopUpdate.terminalDefaultMessage')"
:header="$t('g.terminal')"
:default-message="$t('desktopUpdate.terminalDefaultMessage')"
/>
</div>
</div>
@@ -44,7 +44,6 @@ import Toast from 'primevue/toast'
import { onUnmounted, ref } from 'vue'
import TerminalOutputDrawer from '@/components/maintenance/TerminalOutputDrawer.vue'
import { t } from '@/i18n'
import { electronAPI } from '@/utils/envUtil'
import BaseViewTemplate from './templates/BaseViewTemplate.vue'
@@ -61,10 +60,10 @@ onUnmounted(() => electron.Validation.dispose())
</script>
<style scoped>
@reference '../assets/css/style.css';
.download-bg::before {
@apply m-0 absolute text-muted;
position: absolute;
margin: 0;
color: var(--muted-foreground);
font-family: 'primeicons', sans-serif;
top: -2rem;
right: 2rem;

View File

@@ -1,6 +1,8 @@
// eslint-disable-next-line storybook/no-renderer-packages
import type { Meta, StoryObj } from '@storybook/vue3'
import type { ElectronAPI } from '@comfyorg/comfyui-electron-types'
import { nextTick, provide } from 'vue'
import type { ElectronWindow } from '@/utils/envUtil'
import { createMemoryHistory, createRouter } from 'vue-router'
import InstallView from './InstallView.vue'
@@ -42,16 +44,21 @@ const meta: Meta<typeof InstallView> = {
const router = createMockRouter()
// Mock electron API
;(window as any).electronAPI = {
;(window as ElectronWindow).electronAPI = {
getPlatform: () => 'darwin',
Config: {
getDetectedGpu: () => Promise.resolve('mps')
},
Events: {
trackEvent: (_eventName: string, _data?: any) => {}
trackEvent: (
_eventName: string,
_data?: Record<string, unknown>
) => {}
},
installComfyUI: (_options: any) => {},
changeTheme: (_theme: any) => {},
installComfyUI: (
_options: Parameters<ElectronAPI['installComfyUI']>[0]
) => {},
changeTheme: (_theme: Parameters<ElectronAPI['changeTheme']>[0]) => {},
getSystemPaths: () =>
Promise.resolve({
defaultInstallPath: '/Users/username/ComfyUI'
@@ -240,8 +247,8 @@ export const DesktopSettings: Story = {
export const WindowsPlatform: Story = {
render: () => {
// Override the platform to Windows
;(window as any).electronAPI.getPlatform = () => 'win32'
;(window as any).electronAPI.Config.getDetectedGpu = () =>
;(window as ElectronWindow).electronAPI.getPlatform = () => 'win32'
;(window as ElectronWindow).electronAPI.Config.getDetectedGpu = () =>
Promise.resolve('nvidia')
return {
@@ -259,8 +266,8 @@ export const MacOSPlatform: Story = {
name: 'macOS Platform',
render: () => {
// Override the platform to macOS
;(window as any).electronAPI.getPlatform = () => 'darwin'
;(window as any).electronAPI.Config.getDetectedGpu = () =>
;(window as ElectronWindow).electronAPI.getPlatform = () => 'darwin'
;(window as ElectronWindow).electronAPI.Config.getDetectedGpu = () =>
Promise.resolve('mps')
return {
@@ -327,7 +334,7 @@ export const ManualInstall: Story = {
export const ErrorState: Story = {
render: () => {
// Override validation to return an error
;(window as any).electronAPI.validateInstallPath = () =>
;(window as ElectronWindow).electronAPI.validateInstallPath = () =>
Promise.resolve({
isValid: false,
exists: false,
@@ -375,7 +382,7 @@ export const ErrorState: Story = {
export const WarningState: Story = {
render: () => {
// Override validation to return a warning about non-default drive
;(window as any).electronAPI.validateInstallPath = () =>
;(window as ElectronWindow).electronAPI.validateInstallPath = () =>
Promise.resolve({
isValid: true,
exists: false,

View File

@@ -183,33 +183,37 @@ onMounted(async () => {
</script>
<style scoped>
@reference '../assets/css/style.css';
:deep(.p-steppanel) {
@apply mt-8 flex justify-center bg-transparent;
margin-top: calc(var(--spacing) * 8);
display: flex;
justify-content: center;
background-color: transparent;
}
/* Remove default padding/margin from StepPanels to make scrollbar flush */
:deep(.p-steppanels) {
@apply p-0 m-0;
margin: 0;
padding: 0;
}
/* Ensure StepPanel content container has no top/bottom padding */
:deep(.p-steppanel-content) {
@apply p-0;
padding: 0;
}
/* Custom overlay scrollbar for WebKit browsers (Electron, Chrome) */
:deep(.p-steppanels::-webkit-scrollbar) {
@apply w-4;
width: calc(var(--spacing) * 4);
}
:deep(.p-steppanels::-webkit-scrollbar-track) {
@apply bg-transparent;
background-color: transparent;
}
:deep(.p-steppanels::-webkit-scrollbar-thumb) {
@apply bg-white/20 rounded-lg border-[4px] border-transparent;
border: 4px solid transparent;
border-radius: var(--radius-lg);
background-color: color-mix(in srgb, var(--color-white) 20%, transparent);
background-clip: content-box;
}
</style>

View File

@@ -77,7 +77,7 @@ const createMockElectronAPI = () => {
}
const ensureElectronAPI = () => {
const globalWindow = window as unknown as { electronAPI?: unknown }
const globalWindow = window as { electronAPI?: unknown }
if (!globalWindow.electronAPI) {
globalWindow.electronAPI = createMockElectronAPI()
}

View File

@@ -114,12 +114,12 @@ import Tag from 'primevue/tag'
import Toast from 'primevue/toast'
import { useToast } from 'primevue/usetoast'
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import RefreshButton from '@/components/common/RefreshButton.vue'
import StatusTag from '@/components/maintenance/StatusTag.vue'
import TaskListPanel from '@/components/maintenance/TaskListPanel.vue'
import TerminalOutputDrawer from '@/components/maintenance/TerminalOutputDrawer.vue'
import { t } from '@/i18n'
import { useMaintenanceTaskStore } from '@/stores/maintenanceTaskStore'
import type { MaintenanceFilter } from '@/types/desktop/maintenanceTypes'
import { electronAPI } from '@/utils/envUtil'
@@ -129,6 +129,7 @@ import BaseViewTemplate from './templates/BaseViewTemplate.vue'
const electron = electronAPI()
const toast = useToast()
const { t } = useI18n()
const taskStore = useMaintenanceTaskStore()
const { clearResolved, processUpdate, refreshDesktopTasks } = taskStore
@@ -220,14 +221,14 @@ onUnmounted(() => electron.Validation.dispose())
</script>
<style scoped>
@reference '../assets/css/style.css';
:deep(.p-tag) {
--p-tag-gap: 0.375rem;
}
.backspan::before {
@apply m-0 absolute text-muted;
position: absolute;
margin: 0;
color: var(--muted-foreground);
font-family: 'primeicons', sans-serif;
top: -2rem;
right: -2rem;

View File

@@ -1,6 +1,6 @@
<template>
<BaseViewTemplate>
<div class="sad-container">
<div class="sad-container grid items-center justify-evenly">
<!-- Right side image -->
<img
class="sad-girl"
@@ -79,10 +79,7 @@ const continueToInstall = async () => {
</script>
<style scoped>
@reference '../assets/css/style.css';
.sad-container {
@apply grid items-center justify-evenly;
grid-template-columns: 25rem 1fr;
& > * {

View File

@@ -232,8 +232,6 @@ onUnmounted(() => {
</script>
<style scoped>
@reference '../assets/css/style.css';
/* Hide the xterm scrollbar completely */
:deep(.p-terminal) .xterm-viewport {
overflow: hidden !important;

View File

@@ -4,5 +4,40 @@ See `@docs/guidance/playwright.md` for Playwright best practices (auto-loaded fo
## Directory Structure
- `assets/` - Test data (JSON workflows, fixtures)
- Tests use premade JSON workflows to load desired graph state
```text
browser_tests/
├── assets/ - Test data (JSON workflows, images)
├── fixtures/
│ ├── ComfyPage.ts - Main fixture (delegates to helpers)
│ ├── ComfyMouse.ts - Mouse interaction helper
│ ├── VueNodeHelpers.ts - Vue Nodes 2.0 helpers
│ ├── selectors.ts - Centralized TestIds
│ ├── components/ - Page object components
│ │ ├── ContextMenu.ts
│ │ ├── SettingDialog.ts
│ │ ├── SidebarTab.ts
│ │ └── Topbar.ts
│ ├── helpers/ - Focused helper classes
│ │ ├── CanvasHelper.ts
│ │ ├── CommandHelper.ts
│ │ ├── KeyboardHelper.ts
│ │ ├── NodeOperationsHelper.ts
│ │ ├── SettingsHelper.ts
│ │ ├── WorkflowHelper.ts
│ │ └── ...
│ └── utils/ - Utility functions
├── helpers/ - Test-specific utilities
└── tests/ - Test files (*.spec.ts)
```
## After Making Changes
- Run `pnpm typecheck:browser` after modifying TypeScript files in this directory
- Run `pnpm exec eslint browser_tests/path/to/file.ts` to lint specific files
- Run `pnpm exec oxlint browser_tests/path/to/file.ts` to check with oxlint
## Skill Documentation
A Playwright test-writing skill exists at `.claude/skills/writing-playwright-tests/SKILL.md`.
The skill documents **meta-level guidance only** (gotchas, anti-patterns, decision guides). It does **not** duplicate fixture APIs - agents should read the fixture code directly in `browser_tests/fixtures/`.

View File

@@ -0,0 +1,205 @@
{
"last_node_id": 7,
"last_link_id": 5,
"nodes": [
{
"id": 1,
"type": "T2IAdapterLoader",
"pos": [100, 100],
"size": [300, 80],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "CONTROL_NET",
"type": "CONTROL_NET",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "T2IAdapterLoader"
},
"widgets_values": ["t2iadapter_model.safetensors"]
},
{
"id": 2,
"type": "CheckpointLoaderSimple",
"pos": [100, 300],
"size": [315, 98],
"flags": {},
"order": 1,
"mode": 0,
"outputs": [
{
"name": "MODEL",
"type": "MODEL",
"links": [],
"slot_index": 0
},
{
"name": "CLIP",
"type": "CLIP",
"links": [],
"slot_index": 1
},
{
"name": "VAE",
"type": "VAE",
"links": [],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "CheckpointLoaderSimple"
},
"widgets_values": ["v1-5-pruned-emaonly-fp16.safetensors"]
},
{
"id": 3,
"type": "ResizeImagesByLongerEdge",
"pos": [500, 100],
"size": [300, 80],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [1],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ResizeImagesByLongerEdge"
},
"widgets_values": [1024]
},
{
"id": 4,
"type": "ImageScaleBy",
"pos": [500, 280],
"size": [300, 80],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "image",
"type": "IMAGE",
"link": 1
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [2, 3],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ImageScaleBy"
},
"widgets_values": ["lanczos", 1.5]
},
{
"id": 5,
"type": "ImageBatch",
"pos": [900, 100],
"size": [300, 80],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "image1",
"type": "IMAGE",
"link": 2
},
{
"name": "image2",
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [4],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ImageBatch"
},
"widgets_values": []
},
{
"id": 6,
"type": "SaveImage",
"pos": [900, 300],
"size": [300, 80],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 3
}
],
"properties": {
"Node name for S&R": "SaveImage"
},
"widgets_values": ["ComfyUI"]
},
{
"id": 7,
"type": "PreviewImage",
"pos": [1250, 100],
"size": [300, 250],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 4
}
],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
}
],
"links": [
[1, 3, 0, 4, 0, "IMAGE"],
[2, 4, 0, 5, 0, "IMAGE"],
[3, 4, 0, 6, 0, "IMAGE"],
[4, 5, 0, 7, 0, "IMAGE"]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [0, 0]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,186 @@
{
"last_node_id": 5,
"last_link_id": 2,
"nodes": [
{
"id": 1,
"type": "Load3DAnimation",
"pos": [100, 100],
"size": [300, 100],
"flags": {},
"order": 0,
"mode": 0,
"outputs": [
{
"name": "MESH",
"type": "MESH",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "Load3DAnimation"
},
"widgets_values": ["model.glb"]
},
{
"id": 2,
"type": "Preview3DAnimation",
"pos": [450, 100],
"size": [300, 100],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "mesh",
"type": "MESH",
"link": null
}
],
"properties": {
"Node name for S&R": "Preview3DAnimation"
},
"widgets_values": []
},
{
"id": 3,
"type": "ConditioningAverage ",
"pos": [100, 300],
"size": [300, 100],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "conditioning_to",
"type": "CONDITIONING",
"link": null
},
{
"name": "conditioning_from",
"type": "CONDITIONING",
"link": null
}
],
"outputs": [
{
"name": "CONDITIONING",
"type": "CONDITIONING",
"links": [1],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "ConditioningAverage "
},
"widgets_values": [1]
},
{
"id": 4,
"type": "SDV_img2vid_Conditioning",
"pos": [450, 300],
"size": [300, 150],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "clip_vision",
"type": "CLIP_VISION",
"link": null
},
{
"name": "init_image",
"type": "IMAGE",
"link": null
},
{
"name": "vae",
"type": "VAE",
"link": null
}
],
"outputs": [
{
"name": "positive",
"type": "CONDITIONING",
"links": [],
"slot_index": 0
},
{
"name": "negative",
"type": "CONDITIONING",
"links": [],
"slot_index": 1
},
{
"name": "latent",
"type": "LATENT",
"links": [2],
"slot_index": 2
}
],
"properties": {
"Node name for S&R": "SDV_img2vid_Conditioning"
},
"widgets_values": [1024, 576, 14, 127, 25, 0.02]
},
{
"id": 5,
"type": "KSampler",
"pos": [800, 300],
"size": [300, 262],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null
},
{
"name": "positive",
"type": "CONDITIONING",
"link": 1
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"name": "latent_image",
"type": "LATENT",
"link": 2
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [],
"slot_index": 0
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [42, "fixed", 20, 8, "euler", "normal", 1]
}
],
"links": [
[1, 3, 0, 5, 1, "CONDITIONING"],
[2, 4, 2, 5, 3, "LATENT"]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [0, 0]
}
},
"version": 0.4
}

View File

@@ -0,0 +1,183 @@
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"revision": 0,
"last_node_id": 2,
"last_link_id": 0,
"nodes": [
{
"id": 2,
"type": "e5fb1765-aaaa-bbbb-cccc-ddddeeee0001",
"pos": [600, 400],
"size": [200, 100],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": null
}
],
"properties": {},
"widgets_values": []
}
],
"links": [],
"groups": [],
"definitions": {
"subgraphs": [
{
"id": "e5fb1765-aaaa-bbbb-cccc-ddddeeee0001",
"version": 1,
"state": {
"lastGroupId": 0,
"lastNodeId": 2,
"lastLinkId": 5,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "Subgraph With Duplicate Links",
"inputNode": {
"id": -10,
"bounding": [200, 400, 120, 60]
},
"outputNode": {
"id": -20,
"bounding": [900, 400, 120, 60]
},
"inputs": [],
"outputs": [
{
"id": "out-latent-1",
"name": "LATENT",
"type": "LATENT",
"linkIds": [2],
"pos": [920, 420]
}
],
"widgets": [],
"nodes": [
{
"id": 1,
"type": "KSampler",
"pos": [400, 100],
"size": [270, 262],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "model",
"type": "MODEL",
"link": null
},
{
"name": "positive",
"type": "CONDITIONING",
"link": null
},
{
"name": "negative",
"type": "CONDITIONING",
"link": null
},
{
"name": "latent_image",
"type": "LATENT",
"link": 1
}
],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [2]
}
],
"properties": {
"Node name for S&R": "KSampler"
},
"widgets_values": [0, "randomize", 20, 8, "euler", "simple", 1]
},
{
"id": 2,
"type": "EmptyLatentImage",
"pos": [100, 200],
"size": [200, 106],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "LATENT",
"type": "LATENT",
"links": [1, 3, 4, 5]
}
],
"properties": {
"Node name for S&R": "EmptyLatentImage"
},
"widgets_values": [512, 512, 1]
}
],
"groups": [],
"links": [
{
"id": 1,
"origin_id": 2,
"origin_slot": 0,
"target_id": 1,
"target_slot": 3,
"type": "LATENT"
},
{
"id": 2,
"origin_id": 1,
"origin_slot": 0,
"target_id": -20,
"target_slot": 0,
"type": "LATENT"
},
{
"id": 3,
"origin_id": 2,
"origin_slot": 0,
"target_id": 1,
"target_slot": 3,
"type": "LATENT"
},
{
"id": 4,
"origin_id": 2,
"origin_slot": 0,
"target_id": 1,
"target_slot": 3,
"type": "LATENT"
},
{
"id": 5,
"origin_id": 2,
"origin_slot": 0,
"target_id": 1,
"target_slot": 3,
"type": "LATENT"
}
],
"extra": {}
}
]
},
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [0, 0]
},
"frontendVersion": "1.38.14"
},
"version": 0.4
}

View File

@@ -0,0 +1,599 @@
{
"id": "9a37f747-e96b-4304-9212-7abcaad7bdac",
"revision": 0,
"last_node_id": 5,
"last_link_id": 5,
"nodes": [
{
"id": 5,
"type": "1e38d8ea-45e1-48a5-aa20-966584201867",
"pos": [788, 433.5],
"size": [210, 108],
"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": [["-1", "string_a"]]
},
"widgets_values": [""]
},
{
"id": 2,
"type": "PreviewAny",
"pos": [1135, 429],
"size": [250, 145.5],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "source",
"type": "*",
"link": 5
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewAny"
},
"widgets_values": [null, null, false]
},
{
"id": 1,
"type": "PrimitiveStringMultiline",
"pos": [456, 450],
"size": [225, 121.5],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "STRING",
"type": "STRING",
"links": [4]
}
],
"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": 6,
"lastLinkId": 9,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "New Subgraph",
"inputNode": {
"id": -10,
"bounding": [351, 432.5, 120, 60]
},
"outputNode": {
"id": -20,
"bounding": [1315, 432.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]
}
],
"outputs": [
{
"id": "fbe975ba-d7c2-471e-a99a-a1e2c6ab466d",
"name": "STRING",
"type": "STRING",
"linkIds": [9],
"localized_name": "STRING",
"pos": [1335, 452.5]
}
],
"widgets": [],
"nodes": [
{
"id": 3,
"type": "StringConcatenate",
"pos": [815, 373],
"size": [347, 231],
"flags": {},
"order": 2,
"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": [955, 775],
"size": [210, 88],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "string_a",
"name": "string_a",
"type": "STRING",
"widget": {
"name": "string_a"
},
"link": 7
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [9]
}
],
"properties": {
"proxyWidgets": [["-1", "string_a"]]
},
"widgets_values": [""]
},
{
"id": 4,
"type": "PrimitiveStringMultiline",
"pos": [313, 685],
"size": [325, 109],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [2]
}
],
"properties": {
"Node name for S&R": "PrimitiveStringMultiline"
},
"widgets_values": ["Inner 1\n"]
}
],
"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"
}
],
"extra": {}
},
{
"id": "9be42452-056b-4c99-9f9f-7381d11c4454",
"version": 1,
"state": {
"lastGroupId": 0,
"lastNodeId": 9,
"lastLinkId": 12,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "New Subgraph",
"inputNode": {
"id": -10,
"bounding": [680, 774, 120, 60]
},
"outputNode": {
"id": -20,
"bounding": [1320, 774, 120, 60]
},
"inputs": [
{
"id": "01c05c51-86b5-4bad-b32f-9c911683a13d",
"name": "string_a",
"type": "STRING",
"linkIds": [4],
"localized_name": "string_a",
"pos": [780, 794]
}
],
"outputs": [
{
"id": "a8bcf3bf-a66a-4c71-8d92-17a2a4d03686",
"name": "STRING",
"type": "STRING",
"linkIds": [12],
"localized_name": "STRING",
"pos": [1340, 794]
}
],
"widgets": [],
"nodes": [
{
"id": 5,
"type": "StringConcatenate",
"pos": [860, 719],
"size": [400, 200],
"flags": {},
"order": 2,
"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": 6,
"type": "PrimitiveStringMultiline",
"pos": [401, 973],
"size": [400, 200],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [7]
}
],
"properties": {
"Node name for S&R": "PrimitiveStringMultiline"
},
"widgets_values": ["Inner 2\n"]
},
{
"id": 9,
"type": "7c2915a5-5eb8-4958-a8fd-4beb30f370ce",
"pos": [1046, 985],
"size": [210, 88],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "string_a",
"name": "string_a",
"type": "STRING",
"widget": {
"name": "string_a"
},
"link": 11
}
],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [12]
}
],
"properties": {
"proxyWidgets": [["-1", "string_a"]]
},
"widgets_values": [""]
}
],
"groups": [],
"links": [
{
"id": 4,
"origin_id": -10,
"origin_slot": 0,
"target_id": 5,
"target_slot": 0,
"type": "STRING"
},
{
"id": 7,
"origin_id": 6,
"origin_slot": 0,
"target_id": 5,
"target_slot": 1,
"type": "STRING"
},
{
"id": 11,
"origin_id": 5,
"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"
}
],
"extra": {}
},
{
"id": "7c2915a5-5eb8-4958-a8fd-4beb30f370ce",
"version": 1,
"state": {
"lastGroupId": 0,
"lastNodeId": 8,
"lastLinkId": 10,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "New Subgraph",
"inputNode": {
"id": -10,
"bounding": [262, 1222, 120, 60]
},
"outputNode": {
"id": -20,
"bounding": [1330, 1222, 120, 60]
},
"inputs": [
{
"id": "934a8baa-d79c-428c-8ec9-814ad437d7c7",
"name": "string_a",
"type": "STRING",
"linkIds": [9],
"localized_name": "string_a",
"pos": [362, 1242]
}
],
"outputs": [
{
"id": "4c3d243b-9ff6-4dcd-9dbf-e4ec8e1fc879",
"name": "STRING",
"type": "STRING",
"linkIds": [10],
"localized_name": "STRING",
"pos": [1350, 1242]
}
],
"widgets": [],
"nodes": [
{
"id": 7,
"type": "StringConcatenate",
"pos": [870, 1038],
"size": [400, 200],
"flags": {},
"order": 1,
"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": ["", "", ""]
},
{
"id": 8,
"type": "PrimitiveStringMultiline",
"pos": [442, 1296],
"size": [400, 200],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"localized_name": "STRING",
"name": "STRING",
"type": "STRING",
"links": [8]
}
],
"properties": {
"Node name for S&R": "PrimitiveStringMultiline"
},
"widgets_values": ["Inner 3\n"]
}
],
"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"
}
],
"extra": {}
}
]
},
"config": {},
"extra": {
"ds": {
"scale": 1,
"offset": [-7, 144]
},
"frontendVersion": "1.38.13"
},
"version": 0.4
}

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

@@ -0,0 +1,394 @@
{
"id": "43e9499c-2512-43b5-a5a1-2485eb65da32",
"revision": 0,
"last_node_id": 8,
"last_link_id": 10,
"nodes": [
{
"id": 1,
"type": "LoadImage",
"pos": [170.55728894250745, 515.6401487466619],
"size": [282.8166809082031, 363.8333435058594],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [7, 9]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": ["example.png", "image"]
},
{
"id": 7,
"type": "21dea088-e1b4-47a4-a01f-3d1bf4504001",
"pos": [500.2639113468392, 519.9960755960157],
"size": [464.95001220703125, 615.8333129882812],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 7
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [10]
}
],
"properties": {
"proxyWidgets": [
["2", "$$canvas-image-preview"],
["4", "$$canvas-image-preview"]
]
},
"widgets_values": []
},
{
"id": 8,
"type": "a7a0350a-af99-4d26-9391-450b4f726206",
"pos": [1000.5293620197185, 499.9253405678786],
"size": [225, 359.8333435058594],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "image1",
"type": "IMAGE",
"link": 9
},
{
"name": "image2",
"type": "IMAGE",
"link": 10
}
],
"outputs": [],
"properties": {
"proxyWidgets": [["6", "$$canvas-image-preview"]]
},
"widgets_values": []
}
],
"links": [
[7, 1, 0, 7, 0, "IMAGE"],
[9, 1, 0, 8, 0, "IMAGE"],
[10, 7, 0, 8, 1, "IMAGE"]
],
"groups": [],
"definitions": {
"subgraphs": [
{
"id": "21dea088-e1b4-47a4-a01f-3d1bf4504001",
"version": 1,
"state": {
"lastGroupId": 0,
"lastNodeId": 8,
"lastLinkId": 10,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "New Subgraph",
"inputNode": {
"id": -10,
"bounding": [297.7833638107301, 502.6302057820892, 120, 60]
},
"outputNode": {
"id": -20,
"bounding": [1052.8175480718996, 502.6302057820892, 120, 60]
},
"inputs": [
{
"id": "afc8dbc3-12e6-4b3c-9840-9b398d06e6bd",
"name": "images",
"type": "IMAGE",
"linkIds": [1, 2],
"localized_name": "images",
"pos": [397.7833638107301, 522.6302057820892]
}
],
"outputs": [
{
"id": "d0a84974-5f4d-4f4b-b23a-2e7288a9689d",
"name": "IMAGE",
"type": "IMAGE",
"linkIds": [5],
"localized_name": "IMAGE",
"pos": [1072.8175480718996, 522.6302057820892]
}
],
"widgets": [],
"nodes": [
{
"id": 4,
"type": "PreviewImage",
"pos": [767.8225773415076, 602.8695134060456],
"size": [225, 303.8333435058594],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"localized_name": "images",
"name": "images",
"type": "IMAGE",
"link": 3
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 2,
"type": "PreviewImage",
"pos": [754.9358989867657, 188.55375831225257],
"size": [225, 303.8333435058594],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"localized_name": "images",
"name": "images",
"type": "IMAGE",
"link": 1
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 3,
"type": "ImageInvert",
"pos": [477.783932416778, 542.2440719627998],
"size": [225, 71.83333587646484],
"flags": {
"collapsed": false
},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "image",
"name": "image",
"type": "IMAGE",
"link": 2
}
],
"outputs": [
{
"localized_name": "IMAGE",
"name": "IMAGE",
"type": "IMAGE",
"links": [3, 5]
}
],
"properties": {
"Node name for S&R": "ImageInvert"
},
"widgets_values": []
}
],
"groups": [],
"links": [
{
"id": 3,
"origin_id": 3,
"origin_slot": 0,
"target_id": 4,
"target_slot": 0,
"type": "IMAGE"
},
{
"id": 1,
"origin_id": -10,
"origin_slot": 0,
"target_id": 2,
"target_slot": 0,
"type": "IMAGE"
},
{
"id": 2,
"origin_id": -10,
"origin_slot": 0,
"target_id": 3,
"target_slot": 0,
"type": "IMAGE"
},
{
"id": 5,
"origin_id": 3,
"origin_slot": 0,
"target_id": -20,
"target_slot": 0,
"type": "IMAGE"
}
],
"extra": {}
},
{
"id": "a7a0350a-af99-4d26-9391-450b4f726206",
"version": 1,
"state": {
"lastGroupId": 0,
"lastNodeId": 8,
"lastLinkId": 10,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "New Subgraph",
"inputNode": {
"id": -10,
"bounding": [973.7423316105073, 561.9744246746379, 120, 80]
},
"outputNode": {
"id": -20,
"bounding": [1905.487372786412, 581.9744246746379, 120, 40]
},
"inputs": [
{
"id": "20ac4159-6814-4d40-a217-ea260152b689",
"name": "image1",
"type": "IMAGE",
"linkIds": [4],
"localized_name": "image1",
"pos": [1073.7423316105073, 581.9744246746379]
},
{
"id": "c3759a8c-914e-4450-bc41-ca683ffce96b",
"name": "image2",
"type": "IMAGE",
"linkIds": [8],
"localized_name": "image2",
"shape": 7,
"pos": [1073.7423316105073, 601.9744246746379]
}
],
"outputs": [],
"widgets": [],
"nodes": [
{
"id": 5,
"type": "ImageStitch",
"pos": [1153.7423085222254, 396.2033931749105],
"size": [270, 225.1666717529297],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"localized_name": "image1",
"name": "image1",
"type": "IMAGE",
"link": 4
},
{
"localized_name": "image2",
"name": "image2",
"shape": 7,
"type": "IMAGE",
"link": 8
}
],
"outputs": [
{
"localized_name": "IMAGE",
"name": "IMAGE",
"type": "IMAGE",
"links": [6]
}
],
"properties": {
"Node name for S&R": "ImageStitch"
},
"widgets_values": ["right", true, 0, "white"]
},
{
"id": 6,
"type": "PreviewImage",
"pos": [1620.4874189629757, 529.9122050216333],
"size": [225, 307.8333435058594],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"localized_name": "images",
"name": "images",
"type": "IMAGE",
"link": 6
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
}
],
"groups": [],
"links": [
{
"id": 6,
"origin_id": 5,
"origin_slot": 0,
"target_id": 6,
"target_slot": 0,
"type": "IMAGE"
},
{
"id": 4,
"origin_id": -10,
"origin_slot": 0,
"target_id": 5,
"target_slot": 0,
"type": "IMAGE"
},
{
"id": 8,
"origin_id": -10,
"origin_slot": 1,
"target_id": 5,
"target_slot": 1,
"type": "IMAGE"
}
],
"extra": {}
}
]
},
"config": {},
"extra": {
"ds": {
"scale": 0.7269777827561446,
"offset": [-35.273237658266034, -55.17394203309256]
},
"frontendVersion": "1.40.8"
},
"version": 0.4
}

View File

@@ -0,0 +1,170 @@
{
"id": "preview-subgraph-test-001",
"revision": 0,
"last_node_id": 11,
"last_link_id": 2,
"nodes": [
{
"id": 5,
"type": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"pos": [318.6320139868054, 212.9091015141833],
"size": [225, 368],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 2
}
],
"outputs": [],
"properties": {
"proxyWidgets": [
["10", "filename_prefix"],
["10", "$$canvas-image-preview"]
],
"cnr_id": "comfy-core",
"ver": "0.13.0",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.6.2",
"input_ue_unconnectable": {}
}
},
"widgets_values": []
},
{
"id": 11,
"type": "LoadImage",
"pos": [-0.5080003681592018, 211.3051121416672],
"size": [282.8333435058594, 364],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [2]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"ue_properties": {
"widget_ue_connectable": {},
"input_ue_unconnectable": {}
},
"cnr_id": "comfy-core",
"ver": "0.13.0",
"Node name for S&R": "LoadImage"
},
"widgets_values": ["example.png", "image"]
}
],
"links": [[2, 11, 0, 5, 0, "IMAGE"]],
"groups": [],
"definitions": {
"subgraphs": [
{
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"version": 1,
"state": {
"lastGroupId": 0,
"lastNodeId": 11,
"lastLinkId": 2,
"lastRerouteId": 0
},
"revision": 0,
"config": {},
"name": "New Subgraph",
"inputNode": {
"id": -10,
"bounding": [300, 350, 120, 60]
},
"outputNode": {
"id": -20,
"bounding": [900, 350, 120, 40]
},
"inputs": [
{
"id": "img-slot-001",
"name": "images",
"type": "IMAGE",
"linkIds": [1],
"pos": [400, 370]
}
],
"outputs": [],
"widgets": [],
"nodes": [
{
"id": 10,
"type": "SaveImage",
"pos": [500.0046924937855, 300.0146992076527],
"size": [315, 340],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [
{
"localized_name": "images",
"name": "images",
"type": "IMAGE",
"link": 1
}
],
"outputs": [],
"properties": {
"cnr_id": "comfy-core",
"ver": "0.13.0",
"Node name for S&R": "SaveImage",
"ue_properties": {
"widget_ue_connectable": {},
"version": "7.6.2",
"input_ue_unconnectable": {}
}
},
"widgets_values": ["ComfyUI"]
}
],
"groups": [],
"links": [
{
"id": 1,
"origin_id": -10,
"origin_slot": 0,
"target_id": 10,
"target_slot": 0,
"type": "IMAGE"
}
],
"extra": {
"ue_links": [],
"links_added_by_ue": []
}
}
]
},
"config": {},
"extra": {
"ds": {
"scale": 1.1819400303977265,
"offset": [81.66005130613983, -19.028558221588725]
},
"frontendVersion": "1.40.3",
"ue_links": [],
"links_added_by_ue": [],
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true
},
"version": 0.4
}

View File

@@ -0,0 +1,86 @@
{
"id": "save-image-and-webm-test",
"revision": 0,
"last_node_id": 12,
"last_link_id": 2,
"nodes": [
{
"id": 10,
"type": "LoadImage",
"pos": [50, 100],
"size": [315, 314],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [1, 2]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": ["example.png", "image"]
},
{
"id": 11,
"type": "SaveImage",
"pos": [450, 100],
"size": [210, 270],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 1
}
],
"outputs": [],
"properties": {},
"widgets_values": ["ComfyUI"]
},
{
"id": 12,
"type": "SaveWEBM",
"pos": [450, 450],
"size": [210, 368],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 2
}
],
"outputs": [],
"properties": {},
"widgets_values": ["ComfyUI", "vp9", 6, 32]
}
],
"links": [
[1, 10, 0, 11, 0, "IMAGE"],
[2, 10, 0, 12, 0, "IMAGE"]
],
"groups": [],
"config": {},
"extra": {
"frontendVersion": "1.17.0",
"ds": {
"offset": [0, 0],
"scale": 1
}
},
"version": 0.4
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 517 B

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,7 @@
*/
import type { Locator, Page } from '@playwright/test'
import { TestIds } from './selectors'
import { VueNodeFixture } from './utils/vueNodeFixtures'
export class VueNodeHelpers {
@@ -148,9 +149,9 @@ export class VueNodeHelpers {
* Get a specific widget by node title and widget name
*/
getWidgetByName(nodeTitle: string, widgetName: string): Locator {
return this.getNodeByTitle(nodeTitle).locator(
`_vue=[widget.name="${widgetName}"]`
)
return this.getNodeByTitle(nodeTitle).getByLabel(widgetName, {
exact: true
})
}
/**
@@ -159,8 +160,8 @@ export class VueNodeHelpers {
getInputNumberControls(widget: Locator) {
return {
input: widget.locator('input'),
decrementButton: widget.getByTestId('decrement'),
incrementButton: widget.getByTestId('increment')
decrementButton: widget.getByTestId(TestIds.widgets.decrement),
incrementButton: widget.getByTestId(TestIds.widgets.increment)
}
}
@@ -170,7 +171,7 @@ export class VueNodeHelpers {
*/
async enterSubgraph(nodeId?: string): Promise<void> {
const locator = nodeId ? this.getNodeLocator(nodeId) : this.page
const editButton = locator.getByTestId('subgraph-enter-button')
const editButton = locator.getByTestId(TestIds.widgets.subgraphEnterButton)
await editButton.click()
}
}

View File

@@ -0,0 +1,31 @@
import type { Locator, Page } from '@playwright/test'
export class BaseDialog {
readonly root: Locator
readonly closeButton: Locator
constructor(
public readonly page: Page,
testId?: string
) {
this.root = testId ? page.getByTestId(testId) : page.locator('.p-dialog')
this.closeButton = this.root.getByRole('button', { name: 'Close' })
}
async isVisible(): Promise<boolean> {
return this.root.isVisible()
}
async waitForVisible(): Promise<void> {
await this.root.waitFor({ state: 'visible' })
}
async waitForHidden(): Promise<void> {
await this.root.waitFor({ state: 'hidden' })
}
async close(): Promise<void> {
await this.closeButton.click({ force: true })
await this.waitForHidden()
}
}

View File

@@ -0,0 +1,35 @@
import type { Locator, Page } from '@playwright/test'
class ShortcutsTab {
readonly essentialsTab: Locator
readonly viewControlsTab: Locator
readonly manageButton: Locator
readonly keyBadges: Locator
readonly subcategoryTitles: Locator
constructor(readonly page: Page) {
this.essentialsTab = page.getByRole('tab', { name: /Essential/i })
this.viewControlsTab = page.getByRole('tab', { name: /View Controls/i })
this.manageButton = page.getByRole('button', { name: /Manage Shortcuts/i })
this.keyBadges = page.locator('.key-badge')
this.subcategoryTitles = page.locator('.subcategory-title')
}
}
export class BottomPanel {
readonly root: Locator
readonly keyboardShortcutsButton: Locator
readonly toggleButton: Locator
readonly shortcuts: ShortcutsTab
constructor(readonly page: Page) {
this.root = page.locator('.bottom-panel')
this.keyboardShortcutsButton = page.getByRole('button', {
name: /Keyboard Shortcuts/i
})
this.toggleButton = page.getByRole('button', {
name: /Toggle Bottom Panel/i
})
this.shortcuts = new ShortcutsTab(page)
}
}

View File

@@ -30,7 +30,7 @@ export class ComfyNodeSearchFilterSelectionPanel {
async addFilter(filterValue: string, filterType: string) {
await this.selectFilterType(filterType)
await this.selectFilterValue(filterValue)
await this.page.locator('button:has-text("Add")').click()
await this.page.getByRole('button', { name: 'Add', exact: true }).click()
}
}
@@ -80,4 +80,11 @@ export class ComfyNodeSearchBox {
async removeFilter(index: number) {
await this.filterChips.nth(index).locator('.p-chip-remove-icon').click()
}
/**
* Returns a locator for a search result containing the specified text.
*/
findResult(text: string): Locator {
return this.dropdown.locator('li').filter({ hasText: text })
}
}

View File

@@ -0,0 +1,29 @@
import type { Locator, Page } from '@playwright/test'
import type { ComfyPage } from '../ComfyPage'
export class ComfyNodeSearchBoxV2 {
readonly dialog: Locator
readonly input: Locator
readonly results: Locator
readonly filterOptions: Locator
constructor(readonly page: Page) {
this.dialog = page.getByRole('search')
this.input = this.dialog.locator('input[type="text"]')
this.results = this.dialog.getByTestId('result-item')
this.filterOptions = this.dialog.getByTestId('filter-option')
}
categoryButton(categoryId: string): Locator {
return this.dialog.getByTestId(`category-${categoryId}`)
}
filterBarButton(name: string): Locator {
return this.dialog.getByRole('button', { name })
}
async reload(comfyPage: ComfyPage) {
await comfyPage.settings.setSetting('Comfy.NodeSearchBoxImpl', 'default')
}
}

View File

@@ -0,0 +1,54 @@
import type { Locator, Page } from '@playwright/test'
export class ContextMenu {
constructor(public readonly page: Page) {}
get primeVueMenu() {
return this.page.locator('.p-contextmenu, .p-menu')
}
get litegraphMenu() {
return this.page.locator('.litemenu')
}
get menuItems() {
return this.page.locator('.p-menuitem, .litemenu-entry')
}
async clickMenuItem(name: string): Promise<void> {
await this.page.getByRole('menuitem', { name }).click()
}
async clickLitegraphMenuItem(name: string): Promise<void> {
await this.page.locator(`.litemenu-entry:has-text("${name}")`).click()
}
async isVisible(): Promise<boolean> {
const primeVueVisible = await this.primeVueMenu
.isVisible()
.catch(() => false)
const litegraphVisible = await this.litegraphMenu
.isVisible()
.catch(() => false)
return primeVueVisible || litegraphVisible
}
async waitForHidden(): Promise<void> {
const waitIfExists = async (locator: Locator, menuName: string) => {
const count = await locator.count()
if (count > 0) {
await locator.waitFor({ state: 'hidden' }).catch((error: Error) => {
console.warn(
`[waitForHidden] ${menuName} waitFor failed:`,
error.message
)
})
}
}
await Promise.all([
waitIfExists(this.primeVueMenu, 'primeVueMenu'),
waitIfExists(this.litegraphMenu, 'litegraphMenu')
])
}
}

View File

@@ -1,20 +1,20 @@
import type { Page } from '@playwright/test'
import type { ComfyPage } from '../ComfyPage'
import { TestIds } from '../selectors'
import { BaseDialog } from './BaseDialog'
export class SettingDialog {
export class SettingDialog extends BaseDialog {
constructor(
public readonly page: Page,
page: Page,
public readonly comfyPage: ComfyPage
) {}
get root() {
return this.page.locator('div.settings-container')
) {
super(page, TestIds.dialogs.settings)
}
async open() {
await this.comfyPage.executeCommand('Comfy.ShowSettingsDialog')
await this.page.waitForSelector('div.settings-container')
await this.comfyPage.command.executeCommand('Comfy.ShowSettingsDialog')
await this.waitForVisible()
}
/**
@@ -23,9 +23,7 @@ export class SettingDialog {
* @param value - The value to set
*/
async setStringSetting(id: string, value: string) {
const settingInputDiv = this.page.locator(
`div.settings-container div[id="${id}"]`
)
const settingInputDiv = this.root.locator(`div[id="${id}"]`)
await settingInputDiv.locator('input').fill(value)
}
@@ -34,15 +32,31 @@ export class SettingDialog {
* @param id - The id of the setting
*/
async toggleBooleanSetting(id: string) {
const settingInputDiv = this.page.locator(
`div.settings-container div[id="${id}"]`
)
const settingInputDiv = this.root.locator(`div[id="${id}"]`)
await settingInputDiv.locator('input').click()
}
get searchBox() {
return this.root.getByPlaceholder(/Search/)
}
get categories() {
return this.root.locator('nav').getByRole('button')
}
category(name: string) {
return this.root.locator('nav').getByRole('button', { name })
}
get contentArea() {
return this.root.getByRole('main')
}
async goToAboutPanel() {
const aboutButton = this.page.locator('li[aria-label="About"]')
const aboutButton = this.root.locator('nav').getByRole('button', {
name: 'About'
})
await aboutButton.click()
await this.page.waitForSelector('div.about-container')
await this.page.waitForSelector('.about-container')
}
}

View File

@@ -1,5 +1,8 @@
import type { Locator, Page } from '@playwright/test'
import type { WorkspaceStore } from '../../types/globals'
import { TestIds } from '../selectors'
class SidebarTab {
constructor(
public readonly page: Page,
@@ -31,16 +34,16 @@ class SidebarTab {
}
export class NodeLibrarySidebarTab extends SidebarTab {
constructor(public readonly page: Page) {
constructor(public override readonly page: Page) {
super(page, 'node-library')
}
get nodeLibrarySearchBoxInput() {
return this.page.locator('.node-lib-search-box input[type="text"]')
return this.page.getByPlaceholder('Search Nodes...')
}
get nodeLibraryTree() {
return this.page.locator('.node-lib-tree-explorer')
return this.page.getByTestId(TestIds.sidebar.nodeLibrary)
}
get nodePreview() {
@@ -55,12 +58,12 @@ export class NodeLibrarySidebarTab extends SidebarTab {
return this.tabContainer.locator('.new-folder-button')
}
async open() {
override async open() {
await super.open()
await this.nodeLibraryTree.waitFor({ state: 'visible' })
}
async close() {
override async close() {
if (!this.tabButton.isVisible()) {
return
}
@@ -69,30 +72,40 @@ export class NodeLibrarySidebarTab extends SidebarTab {
await this.nodeLibraryTree.waitFor({ state: 'hidden' })
}
folderSelector(folderName: string) {
return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-folder .node-label:has-text("${folderName}")))`
}
getFolder(folderName: string) {
return this.page.locator(this.folderSelector(folderName))
}
nodeSelector(nodeName: string) {
return `.p-tree-node-content:has(> .tree-explorer-node-label:has(.tree-leaf .node-label:has-text("${nodeName}")))`
return this.page.locator(
`[data-testid="node-tree-folder"][data-folder-name="${folderName}"]`
)
}
getNode(nodeName: string) {
return this.page.locator(this.nodeSelector(nodeName))
return this.page.locator(
`[data-testid="node-tree-leaf"][data-node-name="${nodeName}"]`
)
}
nodeSelector(nodeName: string): string {
return `[data-testid="node-tree-leaf"][data-node-name="${nodeName}"]`
}
folderSelector(folderName: string): string {
return `[data-testid="node-tree-folder"][data-folder-name="${folderName}"]`
}
getNodeInFolder(nodeName: string, folderName: string) {
return this.getFolder(folderName)
.locator('xpath=ancestor::li')
.locator(`[data-testid="node-tree-leaf"][data-node-name="${nodeName}"]`)
}
}
export class WorkflowsSidebarTab extends SidebarTab {
constructor(public readonly page: Page) {
constructor(public override readonly page: Page) {
super(page, 'workflows')
}
get root() {
return this.page.locator('.workflows-sidebar-tab')
return this.page.getByTestId(TestIds.sidebar.workflows)
}
async getOpenedWorkflowNames() {
@@ -140,7 +153,9 @@ export class WorkflowsSidebarTab extends SidebarTab {
// Wait for workflow service to finish renaming
await this.page.waitForFunction(
() => !window['app']?.extensionManager?.workflow?.isBusy,
() =>
!(window.app?.extensionManager as WorkspaceStore | undefined)?.workflow
?.isBusy,
undefined,
{ timeout: 3000 }
)

View File

@@ -1,5 +1,6 @@
import type { Locator, Page } from '@playwright/test'
import { expect } from '@playwright/test'
import type { WorkspaceStore } from '../../types/globals'
export class Topbar {
private readonly menuLocator: Locator
@@ -57,7 +58,7 @@ export class Topbar {
async closeWorkflowTab(tabName: string) {
const tab = this.getWorkflowTab(tabName)
await tab.locator('.close-button').click({ force: true })
await tab.getByRole('button', { name: 'Close' }).click({ force: true })
}
getSaveDialog(): Locator {
@@ -86,7 +87,7 @@ export class Topbar {
// Wait for workflow service to finish saving
await this.page.waitForFunction(
() => !window['app'].extensionManager.workflow.isBusy,
() => !(window.app!.extensionManager as WorkspaceStore).workflow.isBusy,
undefined,
{ timeout: 3000 }
)
@@ -122,7 +123,7 @@ export class Topbar {
*/
async closeTopbarMenu() {
await this.page.locator('body').click({ position: { x: 300, y: 10 } })
await expect(this.menuLocator).not.toBeVisible()
await this.menuLocator.waitFor({ state: 'hidden' })
}
/**

View File

@@ -0,0 +1,51 @@
import type { Position } from './types'
/**
* Hardcoded positions for the default graph loaded in tests.
* These coordinates are specific to the default workflow viewport.
*/
export const DefaultGraphPositions = {
// Node click positions
textEncodeNode1: { x: 618, y: 191 },
textEncodeNode2: { x: 622, y: 400 },
textEncodeNodeToggler: { x: 430, y: 171 },
emptySpaceClick: { x: 35, y: 31 },
// Slot positions
clipTextEncodeNode1InputSlot: { x: 427, y: 198 },
clipTextEncodeNode2InputSlot: { x: 422, y: 402 },
clipTextEncodeNode2InputLinkPath: { x: 395, y: 422 },
loadCheckpointNodeClipOutputSlot: { x: 332, y: 509 },
emptySpace: { x: 427, y: 98 },
// Widget positions
emptyLatentWidgetClick: { x: 724, y: 645 },
// Node positions and sizes for resize operations
ksampler: {
pos: { x: 863, y: 156 },
size: { width: 315, height: 292 }
},
loadCheckpoint: {
pos: { x: 26, y: 444 },
size: { width: 315, height: 127 }
},
emptyLatent: {
pos: { x: 473, y: 579 },
size: { width: 315, height: 136 }
}
} as const satisfies {
textEncodeNode1: Position
textEncodeNode2: Position
textEncodeNodeToggler: Position
emptySpaceClick: Position
clipTextEncodeNode1InputSlot: Position
clipTextEncodeNode2InputSlot: Position
clipTextEncodeNode2InputLinkPath: Position
loadCheckpointNodeClipOutputSlot: Position
emptySpace: Position
emptyLatentWidgetClick: Position
ksampler: { pos: Position; size: { width: number; height: number } }
loadCheckpoint: { pos: Position; size: { width: number; height: number } }
emptyLatent: { pos: Position; size: { width: number; height: number } }
}

View File

@@ -0,0 +1,9 @@
export interface Position {
x: number
y: number
}
export interface Size {
width: number
height: number
}

View File

@@ -0,0 +1,184 @@
import type { Locator, Page } from '@playwright/test'
import { DefaultGraphPositions } from '../constants/defaultGraphPositions'
import type { Position } from '../types'
export class CanvasHelper {
constructor(
private page: Page,
private canvas: Locator,
private resetViewButton: Locator
) {}
private async nextFrame(): Promise<void> {
await this.page.evaluate(() => {
return new Promise<number>(requestAnimationFrame)
})
}
async resetView(): Promise<void> {
if (await this.resetViewButton.isVisible()) {
await this.resetViewButton.click()
}
await this.page.mouse.move(10, 10)
await this.nextFrame()
}
async zoom(deltaY: number, steps: number = 1): Promise<void> {
await this.page.mouse.move(10, 10)
for (let i = 0; i < steps; i++) {
await this.page.mouse.wheel(0, deltaY)
}
await this.nextFrame()
}
async pan(offset: Position, safeSpot?: Position): Promise<void> {
safeSpot = safeSpot || { x: 10, y: 10 }
await this.page.mouse.move(safeSpot.x, safeSpot.y)
await this.page.mouse.down()
await this.page.mouse.move(offset.x + safeSpot.x, offset.y + safeSpot.y)
await this.page.mouse.up()
await this.nextFrame()
}
async panWithTouch(offset: Position, safeSpot?: Position): Promise<void> {
safeSpot = safeSpot || { x: 10, y: 10 }
const client = await this.page.context().newCDPSession(this.page)
await client.send('Input.dispatchTouchEvent', {
type: 'touchStart',
touchPoints: [safeSpot]
})
await client.send('Input.dispatchTouchEvent', {
type: 'touchMove',
touchPoints: [{ x: offset.x + safeSpot.x, y: offset.y + safeSpot.y }]
})
await client.send('Input.dispatchTouchEvent', {
type: 'touchEnd',
touchPoints: []
})
await this.nextFrame()
}
async rightClick(x: number = 10, y: number = 10): Promise<void> {
await this.page.mouse.click(x, y, { button: 'right' })
await this.nextFrame()
}
async doubleClick(): Promise<void> {
await this.page.mouse.dblclick(10, 10, { delay: 5 })
await this.nextFrame()
}
async click(position: Position): Promise<void> {
await this.canvas.click({ position })
await this.nextFrame()
}
async clickEmptySpace(): Promise<void> {
await this.canvas.click({ position: DefaultGraphPositions.emptySpaceClick })
await this.nextFrame()
}
async dragAndDrop(source: Position, target: Position): Promise<void> {
await this.page.mouse.move(source.x, source.y)
await this.page.mouse.down()
await this.page.mouse.move(target.x, target.y, { steps: 100 })
await this.page.mouse.up()
await this.nextFrame()
}
async moveMouseToEmptyArea(): Promise<void> {
await this.page.mouse.move(10, 10)
}
async getScale(): Promise<number> {
return this.page.evaluate(() => {
return window.app!.canvas.ds.scale
})
}
async setScale(scale: number): Promise<void> {
await this.page.evaluate((s) => {
window.app!.canvas.ds.scale = s
}, scale)
await this.nextFrame()
}
async convertOffsetToCanvas(
pos: [number, number]
): Promise<[number, number]> {
return this.page.evaluate((pos) => {
return window.app!.canvas.ds.convertOffsetToCanvas(pos)
}, pos)
}
async getNodeCenterByTitle(title: string): Promise<Position | null> {
return this.page.evaluate((title) => {
const app = window.app!
const node = app.graph.nodes.find(
(n: { title: string }) => n.title === title
)
if (!node) return null
const centerX = node.pos[0] + node.size[0] / 2
const centerY = node.pos[1] + node.size[1] / 2
const [clientX, clientY] = app.canvasPosToClientPos([centerX, centerY])
return { x: clientX, y: clientY }
}, title)
}
async getGroupPosition(title: string): Promise<Position> {
const pos = await this.page.evaluate((title) => {
const groups = window.app!.graph.groups
const group = groups.find((g: { title: string }) => g.title === title)
if (!group) return null
return { x: group.pos[0], y: group.pos[1] }
}, title)
if (!pos) throw new Error(`Group "${title}" not found`)
return pos
}
async dragGroup(options: {
name: string
deltaX: number
deltaY: number
}): Promise<void> {
const { name, deltaX, deltaY } = options
const screenPos = await this.page.evaluate((title) => {
const app = window.app!
const groups = app.graph.groups
const group = groups.find((g: { title: string }) => g.title === title)
if (!group) return null
const clientPos = app.canvasPosToClientPos([
group.pos[0] + 50,
group.pos[1] + 15
])
return { x: clientPos[0], y: clientPos[1] }
}, name)
if (!screenPos) throw new Error(`Group "${name}" not found`)
await this.dragAndDrop(screenPos, {
x: screenPos.x + deltaX,
y: screenPos.y + deltaY
})
}
async disconnectEdge(): Promise<void> {
await this.dragAndDrop(
DefaultGraphPositions.clipTextEncodeNode1InputSlot,
DefaultGraphPositions.emptySpace
)
}
async connectEdge(options: { reverse?: boolean } = {}): Promise<void> {
const { reverse = false } = options
const start = reverse
? DefaultGraphPositions.clipTextEncodeNode1InputSlot
: DefaultGraphPositions.loadCheckpointNodeClipOutputSlot
const end = reverse
? DefaultGraphPositions.loadCheckpointNodeClipOutputSlot
: DefaultGraphPositions.clipTextEncodeNode1InputSlot
await this.dragAndDrop(start, end)
}
}

View File

@@ -0,0 +1,15 @@
import type { Locator } from '@playwright/test'
import type { KeyboardHelper } from './KeyboardHelper'
export class ClipboardHelper {
constructor(private readonly keyboard: KeyboardHelper) {}
async copy(locator?: Locator | null): Promise<void> {
await this.keyboard.ctrlSend('KeyC', locator ?? null)
}
async paste(locator?: Locator | null): Promise<void> {
await this.keyboard.ctrlSend('KeyV', locator ?? null)
}
}

View File

@@ -0,0 +1,87 @@
import type { Page } from '@playwright/test'
import type { KeyCombo } from '../../../src/platform/keybindings/types'
export class CommandHelper {
constructor(private readonly page: Page) {}
async executeCommand(
commandId: string,
metadata?: Record<string, unknown>
): Promise<void> {
await this.page.evaluate(
({ commandId, metadata }) => {
const app = window.app
if (!app) throw new Error('window.app is not available')
return app.extensionManager.command.execute(commandId, {
metadata
})
},
{ commandId, metadata }
)
}
async registerCommand(
commandId: string,
command: (() => void) | (() => Promise<void>)
): Promise<void> {
// SECURITY: eval() is intentionally used here to deserialize/execute functions
// passed from controlled test code across the Node/Playwright browser boundary.
// Execution happens in isolated Playwright browser contexts with test-only data.
// This pattern is unsafe for production and must not be copied elsewhere.
await this.page.evaluate(
({ commandId, commandStr }) => {
const app = window.app!
const randomSuffix = Math.random().toString(36).substring(2, 8)
const extensionName = `TestExtension_${randomSuffix}`
app.registerExtension({
name: extensionName,
commands: [
{
id: commandId,
function: eval(commandStr)
}
]
})
},
{ commandId, commandStr: command.toString() }
)
}
async registerKeybinding(
keyCombo: KeyCombo,
command: () => void
): Promise<void> {
// SECURITY: eval() is intentionally used here to deserialize/execute functions
// passed from controlled test code across the Node/Playwright browser boundary.
// Execution happens in isolated Playwright browser contexts with test-only data.
// This pattern is unsafe for production and must not be copied elsewhere.
await this.page.evaluate(
({ keyCombo, commandStr }) => {
const app = window.app!
const randomSuffix = Math.random().toString(36).substring(2, 8)
const extensionName = `TestExtension_${randomSuffix}`
const commandId = `TestCommand_${randomSuffix}`
app.registerExtension({
name: extensionName,
keybindings: [
{
combo: keyCombo,
commandId: commandId
}
],
commands: [
{
id: commandId,
function: eval(commandStr)
}
]
})
},
{ keyCombo, commandStr: command.toString() }
)
}
}

View File

@@ -0,0 +1,171 @@
import { readFileSync } from 'fs'
import type { Page } from '@playwright/test'
import type { Position } from '../types'
export class DragDropHelper {
constructor(
private readonly page: Page,
private readonly assetPath: (fileName: string) => string
) {}
private async nextFrame(): Promise<void> {
await this.page.evaluate(() => {
return new Promise<void>((resolve) => {
requestAnimationFrame(() => resolve())
})
})
}
async dragAndDropExternalResource(
options: {
fileName?: string
url?: string
dropPosition?: Position
waitForUpload?: boolean
} = {}
): Promise<void> {
const {
dropPosition = { x: 100, y: 100 },
fileName,
url,
waitForUpload = false
} = options
if (!fileName && !url)
throw new Error('Must provide either fileName or url')
const evaluateParams: {
dropPosition: Position
fileName?: string
fileType?: string
buffer?: Uint8Array | number[]
url?: string
} = { dropPosition }
if (fileName) {
const filePath = this.assetPath(fileName)
const buffer = readFileSync(filePath)
const getFileType = (fileName: string) => {
if (fileName.endsWith('.png')) return 'image/png'
if (fileName.endsWith('.svg')) return 'image/svg+xml'
if (fileName.endsWith('.webp')) return 'image/webp'
if (fileName.endsWith('.webm')) return 'video/webm'
if (fileName.endsWith('.json')) return 'application/json'
if (fileName.endsWith('.glb')) return 'model/gltf-binary'
if (fileName.endsWith('.avif')) return 'image/avif'
return 'application/octet-stream'
}
evaluateParams.fileName = fileName
evaluateParams.fileType = getFileType(fileName)
evaluateParams.buffer = [...new Uint8Array(buffer)]
}
if (url) evaluateParams.url = url
const uploadResponsePromise = waitForUpload
? this.page.waitForResponse(
(resp) => resp.url().includes('/upload/') && resp.status() === 200,
{ timeout: 10000 }
)
: null
await this.page.evaluate(async (params) => {
const dataTransfer = new DataTransfer()
if (params.buffer && params.fileName && params.fileType) {
const file = new File(
[new Uint8Array(params.buffer)],
params.fileName,
{
type: params.fileType
}
)
dataTransfer.items.add(file)
}
if (params.url) {
dataTransfer.setData('text/uri-list', params.url)
dataTransfer.setData('text/x-moz-url', params.url)
}
const targetElement = document.elementFromPoint(
params.dropPosition.x,
params.dropPosition.y
)
if (!targetElement) {
throw new Error(
`No element found at drop position: (${params.dropPosition.x}, ${params.dropPosition.y}). ` +
`document.elementFromPoint returned null. Ensure the target is visible and not obscured.`
)
}
const eventOptions = {
bubbles: true,
cancelable: true,
dataTransfer,
clientX: params.dropPosition.x,
clientY: params.dropPosition.y
}
const dragOverEvent = new DragEvent('dragover', eventOptions)
const dropEvent = new DragEvent('drop', eventOptions)
const graphCanvasElement = document.querySelector('#graph-canvas')
// Keep Litegraph's drag-over node tracking in sync when the drop target is a
// Vue node DOM overlay outside of the graph canvas element.
if (graphCanvasElement && !graphCanvasElement.contains(targetElement)) {
graphCanvasElement.dispatchEvent(
new DragEvent('dragover', eventOptions)
)
}
Object.defineProperty(dropEvent, 'preventDefault', {
value: () => {},
writable: false
})
Object.defineProperty(dropEvent, 'stopPropagation', {
value: () => {},
writable: false
})
targetElement.dispatchEvent(dragOverEvent)
targetElement.dispatchEvent(dropEvent)
return {
success: true,
targetInfo: {
tagName: targetElement.tagName,
id: targetElement.id,
classList: Array.from(targetElement.classList)
}
}
}, evaluateParams)
if (uploadResponsePromise) {
await uploadResponsePromise
}
await this.nextFrame()
}
async dragAndDropFile(
fileName: string,
options: { dropPosition?: Position; waitForUpload?: boolean } = {}
): Promise<void> {
return this.dragAndDropExternalResource({ fileName, ...options })
}
async dragAndDropURL(
url: string,
options: { dropPosition?: Position } = {}
): Promise<void> {
return this.dragAndDropExternalResource({ url, ...options })
}
}

View File

@@ -0,0 +1,45 @@
import type { Locator, Page } from '@playwright/test'
export class KeyboardHelper {
constructor(
private readonly page: Page,
private readonly canvas: Locator
) {}
private async nextFrame(): Promise<void> {
await this.page.evaluate(() => new Promise<number>(requestAnimationFrame))
}
async ctrlSend(
keyToPress: string,
locator: Locator | null = this.canvas
): Promise<void> {
const target = locator ?? this.page.keyboard
await target.press(`Control+${keyToPress}`)
await this.nextFrame()
}
async selectAll(locator?: Locator | null): Promise<void> {
await this.ctrlSend('KeyA', locator)
}
async bypass(locator?: Locator | null): Promise<void> {
await this.ctrlSend('KeyB', locator)
}
async undo(locator?: Locator | null): Promise<void> {
await this.ctrlSend('KeyZ', locator)
}
async redo(locator?: Locator | null): Promise<void> {
await this.ctrlSend('KeyY', locator)
}
async moveUp(locator?: Locator | null): Promise<void> {
await this.ctrlSend('ArrowUp', locator)
}
async moveDown(locator?: Locator | null): Promise<void> {
await this.ctrlSend('ArrowDown', locator)
}
}

View File

@@ -0,0 +1,186 @@
import type { Locator } from '@playwright/test'
import type {
LGraph,
LGraphNode
} from '../../../src/lib/litegraph/src/litegraph'
import type { NodeId } from '../../../src/platform/workflow/validation/schemas/workflowSchema'
import type { ComfyPage } from '../ComfyPage'
import { DefaultGraphPositions } from '../constants/defaultGraphPositions'
import type { Position, Size } from '../types'
import { NodeReference } from '../utils/litegraphUtils'
export class NodeOperationsHelper {
constructor(private comfyPage: ComfyPage) {}
private get page() {
return this.comfyPage.page
}
async getGraphNodesCount(): Promise<number> {
return await this.page.evaluate(() => {
return window.app?.graph?.nodes?.length || 0
})
}
async getSelectedGraphNodesCount(): Promise<number> {
return await this.page.evaluate(() => {
return (
window.app?.graph?.nodes?.filter(
(node: LGraphNode) => node.is_selected === true
).length || 0
)
})
}
async getNodeCount(): Promise<number> {
return await this.page.evaluate(() => window.app!.graph.nodes.length)
}
async getNodes(): Promise<LGraphNode[]> {
return await this.page.evaluate(() => {
return window.app!.graph.nodes
})
}
async waitForGraphNodes(count: number): Promise<void> {
await this.page.waitForFunction((count) => {
return window.app?.canvas.graph?.nodes?.length === count
}, count)
}
async getFirstNodeRef(): Promise<NodeReference | null> {
const id = await this.page.evaluate(() => {
return window.app!.graph.nodes[0]?.id
})
if (!id) return null
return this.getNodeRefById(id)
}
async getNodeRefById(id: NodeId): Promise<NodeReference> {
return new NodeReference(id, this.comfyPage)
}
async getNodeRefsByType(
type: string,
includeSubgraph: boolean = false
): Promise<NodeReference[]> {
return Promise.all(
(
await this.page.evaluate(
({ type, includeSubgraph }) => {
const graph = (
includeSubgraph ? window.app!.canvas.graph : window.app!.graph
) as LGraph
const nodes = graph.nodes
return nodes
.filter((n: LGraphNode) => n.type === type)
.map((n: LGraphNode) => n.id)
},
{ type, includeSubgraph }
)
).map((id: NodeId) => this.getNodeRefById(id))
)
}
async getNodeRefsByTitle(title: string): Promise<NodeReference[]> {
return Promise.all(
(
await this.page.evaluate((title) => {
return window
.app!.graph.nodes.filter((n: LGraphNode) => n.title === title)
.map((n: LGraphNode) => n.id)
}, title)
).map((id: NodeId) => this.getNodeRefById(id))
)
}
async selectNodes(nodeTitles: string[]): Promise<void> {
await this.page.keyboard.down('Control')
try {
for (const nodeTitle of nodeTitles) {
const nodes = await this.getNodeRefsByTitle(nodeTitle)
for (const node of nodes) {
await node.click('title')
}
}
} finally {
await this.page.keyboard.up('Control')
await this.comfyPage.nextFrame()
}
}
async resizeNode(
nodePos: Position,
nodeSize: Size,
ratioX: number,
ratioY: number,
revertAfter: boolean = false
): Promise<void> {
const bottomRight = {
x: nodePos.x + nodeSize.width,
y: nodePos.y + nodeSize.height
}
const target = {
x: nodePos.x + nodeSize.width * ratioX,
y: nodePos.y + nodeSize.height * ratioY
}
// -1 to be inside the node. -2 because nodes currently get an arbitrary +1 to width.
await this.comfyPage.canvasOps.dragAndDrop(
{ x: bottomRight.x - 2, y: bottomRight.y - 1 },
target
)
await this.comfyPage.nextFrame()
if (revertAfter) {
await this.comfyPage.canvasOps.dragAndDrop(
{ x: target.x - 2, y: target.y - 1 },
bottomRight
)
await this.comfyPage.nextFrame()
}
}
async convertAllNodesToGroupNode(groupNodeName: string): Promise<void> {
await this.comfyPage.canvas.press('Control+a')
const node = await this.getFirstNodeRef()
if (!node) {
throw new Error('No nodes found to convert')
}
await node.clickContextMenuOption('Convert to Group Node')
await this.fillPromptDialog(groupNodeName)
await this.comfyPage.nextFrame()
}
get promptDialogInput(): Locator {
return this.page.locator('.p-dialog-content input[type="text"]')
}
async fillPromptDialog(value: string): Promise<void> {
await this.promptDialogInput.fill(value)
await this.page.keyboard.press('Enter')
await this.promptDialogInput.waitFor({ state: 'hidden' })
await this.comfyPage.nextFrame()
}
async dragTextEncodeNode2(): Promise<void> {
await this.comfyPage.canvasOps.dragAndDrop(
DefaultGraphPositions.textEncodeNode2,
{
x: DefaultGraphPositions.textEncodeNode2.x,
y: 300
}
)
await this.comfyPage.nextFrame()
}
async adjustEmptyLatentWidth(): Promise<void> {
await this.page.locator('#graph-canvas').click({
position: DefaultGraphPositions.emptyLatentWidgetClick
})
const dialogInput = this.page.locator('.graphdialog input[type="text"]')
await dialogInput.click()
await dialogInput.fill('128')
await dialogInput.press('Enter')
await this.comfyPage.nextFrame()
}
}

View File

@@ -0,0 +1,96 @@
import type { CDPSession, Page } from '@playwright/test'
interface PerfSnapshot {
RecalcStyleCount: number
RecalcStyleDuration: number
LayoutCount: number
LayoutDuration: number
TaskDuration: number
JSHeapUsedSize: number
Timestamp: number
}
export interface PerfMeasurement {
name: string
durationMs: number
styleRecalcs: number
styleRecalcDurationMs: number
layouts: number
layoutDurationMs: number
taskDurationMs: number
heapDeltaBytes: number
}
export class PerformanceHelper {
private cdp: CDPSession | null = null
private snapshot: PerfSnapshot | null = null
constructor(private readonly page: Page) {}
async init(): Promise<void> {
this.cdp = await this.page.context().newCDPSession(this.page)
await this.cdp.send('Performance.enable')
}
async dispose(): Promise<void> {
this.snapshot = null
if (this.cdp) {
try {
await this.cdp.send('Performance.disable')
} finally {
await this.cdp.detach()
this.cdp = null
}
}
}
private async getSnapshot(): Promise<PerfSnapshot> {
if (!this.cdp) throw new Error('PerformanceHelper not initialized')
const { metrics } = (await this.cdp.send('Performance.getMetrics')) as {
metrics: { name: string; value: number }[]
}
function get(name: string): number {
return metrics.find((m) => m.name === name)?.value ?? 0
}
return {
RecalcStyleCount: get('RecalcStyleCount'),
RecalcStyleDuration: get('RecalcStyleDuration'),
LayoutCount: get('LayoutCount'),
LayoutDuration: get('LayoutDuration'),
TaskDuration: get('TaskDuration'),
JSHeapUsedSize: get('JSHeapUsedSize'),
Timestamp: get('Timestamp')
}
}
async startMeasuring(): Promise<void> {
if (this.snapshot) {
throw new Error(
'Measurement already in progress — call stopMeasuring() first'
)
}
this.snapshot = await this.getSnapshot()
}
async stopMeasuring(name: string): Promise<PerfMeasurement> {
if (!this.snapshot) throw new Error('Call startMeasuring() first')
const after = await this.getSnapshot()
const before = this.snapshot
this.snapshot = null
function delta(key: keyof PerfSnapshot): number {
return after[key] - before[key]
}
return {
name,
durationMs: delta('Timestamp') * 1000,
styleRecalcs: delta('RecalcStyleCount'),
styleRecalcDurationMs: delta('RecalcStyleDuration') * 1000,
layouts: delta('LayoutCount'),
layoutDurationMs: delta('LayoutDuration') * 1000,
taskDurationMs: delta('TaskDuration') * 1000,
heapDeltaBytes: delta('JSHeapUsedSize')
}
}
}

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