Commit Graph

1147 Commits

Author SHA1 Message Date
jaeone94
82bea29dda fix: defer node auto-pan until drag starts (#12654)
## Summary

Fix a Vue node drag edge case where holding the partially off-screen
Advanced inputs button could continuously auto-pan the canvas even
though the pointer had not moved into an actual drag.

Linear:
[FE-938](https://linear.app/comfyorg/issue/FE-938/holding-partially-off-screen-advanced-inputs-causes-continuous)

## Changes

- **What**: Move Vue node auto-pan initialization from `startDrag()` to
`handleDrag()`, so auto-pan starts only after the pointer interaction
has become a real drag.
- **What**: Keep the existing auto-pan behavior during active drags by
creating the controller on the first `handleDrag()` call, updating its
pointer position on later drag frames, and preserving the existing
`onPan` position adjustments.
- **What**: Add unit coverage for the important drag lifecycle
invariants: no auto-pan on pointerdown/startDrag, auto-pan starts on
handleDrag, the same controller is reused across handleDrag calls, and
cleanup still stops auto-pan on endDrag.
- **What**: Add a Playwright regression that places the Advanced inputs
button partially beyond the visible canvas edge, holds the pointer down
without moving, and verifies the canvas offset stays stable.
- **What**: Add `data-testid="advanced-inputs-button"` to the Advanced
inputs footer button variants so the regression test does not depend on
translated button text.
- **Breaking**: None.
- **Dependencies**: None.

## Root Cause

`useNodeDrag.startDrag()` created and started `AutoPanController`
immediately on pointerdown. When the Advanced inputs button was partly
outside the canvas bounds, a stationary pointer near the visible canvas
edge was enough for auto-pan to begin, even before any drag movement
occurred.

The pointer interaction layer already distinguishes press/hold from real
dragging before calling `handleDrag()`. Deferring auto-pan to
`handleDrag()` aligns auto-pan startup with that drag threshold and
prevents a plain hold from panning the canvas.

## Review Focus

- Auto-pan should not start from `startDrag()`/pointerdown alone.
- Auto-pan should still start promptly once `handleDrag()` runs for an
actual drag.
- Repeated `handleDrag()` calls should reuse the existing controller
rather than recreate it.
- Existing `onPan` behavior should continue to update drag start
positions, selected node start positions, selected groups, and node
positions during active drags.
- The E2E intentionally asserts the canvas offset, not node bounds,
because the reported bug is unintended canvas auto-pan while the pointer
is stationary.

## Red-Green Verification

- `a00b5d2fb test: add failing advanced button hold pan regression`:
adds the Playwright regression and test id plumbing. This was verified
red against the pre-fix production code.
- `5c207ae28 fix: defer node auto-pan until drag starts`: adds the
production fix and unit coverage. The same regression is verified green
with the fix.

## Validation

- `pnpm format`
- `pnpm lint`
- `pnpm typecheck`
- `pnpm typecheck:browser`
- `pnpm test:unit`
- `pnpm test:unit
src/renderer/extensions/vueNodes/layout/useNodeDrag.test.ts`
- `PLAYWRIGHT_SETUP_API_URL=http://127.0.0.1:8188 pnpm
test:browser:local
browser_tests/tests/vueNodes/interactions/node/move.spec.ts --grep
"should not pan while holding the Advanced button without dragging"`
- Pre-push hook: `pnpm knip --cache`

## Screenshots (Before / After)

Before 


https://github.com/user-attachments/assets/6080de2d-e2da-4b38-a1ed-1f1f88548c2d

After 


https://github.com/user-attachments/assets/f331271a-9ea1-41ec-92cb-974bc57be56b
2026-06-05 05:26:18 +00:00
jaeone94
d129f757c0 fix: keep connected advanced inputs visible (#12652)
## Summary

Keep connected advanced widget inputs visible on Vue-rendered nodes when
advanced inputs are collapsed.

This fixes Linear
[FE-924](https://linear.app/comfyorg/issue/FE-924/bug-connected-advanced-input-parameters-become-hidden-when-advanced),
where a user could connect a noodle to an advanced input, collapse
advanced inputs, and then lose visual access to the connected parameter
even though it was actively used by the workflow.

## Changes

- **What**: Treat a widget-backed input as visible when its slot is
linked, even if the widget is advanced and the node-level advanced
section is collapsed.
- **What**: Move Vue node widget rendering to use the processed
`widget.visible` value instead of reimplementing visibility in
`NodeWidgets.vue`.
- **What**: Keep the visibility decision as a single source of truth
during widget processing, including the existing deduplication path.
- **What**: Add unit coverage for the new linked-widget visibility
behavior and the precedence rule that explicit hidden state still wins.
- **What**: Add an E2E regression that connects a `PrimitiveFloat` to
the advanced `max_shift` input on `ModelSamplingFlux`, collapses
advanced inputs, and verifies the connected input remains visible while
an unconnected advanced input remains hidden.
- **Breaking**: None.
- **Dependencies**: None.

## Review Focus

The key behavior is that linked advanced widgets should be promoted into
the visible widget set only while they are connected. Explicitly hidden
widgets must remain hidden even when linked.

The fix uses existing slot metadata from `useGraphNodeManager`; no new
graph state is introduced. This keeps the change scoped to Vue node
widget processing and rendering.

## Red-Green Verification

| Commit | Purpose | Local result |
| --- | --- | --- |
| `4fa5932c6` | Adds the E2E regression only | Red: `max_shift` was not
found after collapsing advanced inputs |
| `e5d1ee06a` | Adds the production fix and focused unit coverage |
Green: targeted E2E passed |

## Test Plan

- `pnpm test:unit
src/renderer/extensions/vueNodes/composables/useProcessedWidgets.test.ts`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://127.0.0.1:5175
PLAYWRIGHT_SETUP_API_URL=http://127.0.0.1:8188 pnpm test:browser -g
"should keep connected advanced widgets visible when advanced inputs are
hidden" browser_tests/tests/vueNodes/widgets/advancedWidgets.spec.ts`
- `pnpm typecheck && pnpm typecheck:browser`

## Screenshots (Before / After)
Before 


https://github.com/user-attachments/assets/bf1e88f3-2983-4bef-9cef-48ffe6dbfd6d



After 


https://github.com/user-attachments/assets/4dee7766-0252-478f-9b1c-4b801fc20eb2
2026-06-05 05:26:01 +00:00
jaeone94
874b486640 fix: resolve missing resource error messages (#12646)
## Summary

Resolve missing resource error groups through the error catalog so
missing nodes, replaceable nodes, missing models, and missing media use
consistent panel and single-error overlay copy.

## Changes

- **What**: Adds missing-resource resolvers for `missing_node`,
`swap_nodes`, `missing_model`, and `missing_media` that provide
`displayMessage`, `toastTitle`, and `toastMessage` alongside the
existing group titles. The Errors tab now renders a group-level
`displayMessage` under non-execution group headers, which gives grouped
missing-resource cards the same explanatory message path used by
validation/runtime errors without adding per-row detail fields that
these grouped cards do not need.
- **What**: Moves missing node and swap node explanatory copy out of
card-local hardcoded text and into `errorCatalog.missingErrors.*` keys.
`MissingNodeCard` and `SwapNodesCard` now focus on rendering their
grouped rows and actions, while the shared error group header owns the
explanatory copy.
- **What**: Adds environment-aware copy for missing node packs and
missing models. Cloud messages explain unsupported resources and
replacement/import paths without suggesting local execution, while OSS
messages point users toward installing or downloading the missing
resources.
- **What**: Adds single-error overlay/toast copy for missing resources.
Missing media uses a concise input-focused title/message, missing models
distinguish Cloud unsupported models from OSS missing files, and missing
nodes/swap nodes use node-type-aware copy.
- **What**: Deduplicates missing node and swap node toast decisions by
distinct node type so repeated instances of the same missing/replaceable
node do not accidentally switch the single-error copy to plural copy.
- **What**: Preserves representative missing media candidate metadata so
missing media toast copy can use a human-readable node name such as
`Load Image is missing a required media file.`
- **What**: Removes unused missing-resource resolver fields such as
grouped `displayDetails`, grouped `displayItemLabel`, and the unused
`mediaTypes` source parameter after deciding those fields do not fit
grouped missing-resource cards.
- **Breaking**: None.
- **Dependencies**: None.

## Review Focus

- Missing resource groups are still grouped cards. This PR intentionally
gives them group-level display and toast copy, but does not split
missing resources into one error item per underlying candidate.
- Missing resource count semantics are intentionally not normalized
here. Error overlay totals, store counts, and grouped card counts still
follow the existing behavior; a follow-up PR can define those count
units separately.
- The Cloud/OSS message variants remain explicit in the resolver instead
of being abstracted into a generic variant helper. That keeps this PR
focused on the messaging behavior and avoids a broader resolver
refactor.
- Only `src/locales/en/main.json` is updated directly. Other locales
should be synced by the existing localization flow.

## Screenshots (if applicable)

<img width="668" height="245" alt="스크린샷 2026-06-05 오전 3 16 49"
src="https://github.com/user-attachments/assets/98b50ac3-67e1-438d-8c37-e06c7bf465ee"
/>
<img width="666" height="195" alt="스크린샷 2026-06-05 오전 3 16 58"
src="https://github.com/user-attachments/assets/92da95b1-03d6-4739-97e6-c573982bfec9"
/>
<img width="505" height="358" alt="스크린샷 2026-06-05 오전 3 17 27"
src="https://github.com/user-attachments/assets/4d0e1a6e-13b9-4097-9fb5-19fe0c5331dc"
/>
<img width="507" height="324" alt="스크린샷 2026-06-05 오전 3 17 44"
src="https://github.com/user-attachments/assets/054e42f8-0d0c-44b5-8a67-e467fc04f1fc"
/>


## Validation

- `pnpm format`
- `pnpm lint`
- `pnpm test:unit src/platform/errorCatalog/errorMessageResolver.test.ts
src/components/rightSidePanel/errors/useErrorGroups.test.ts
src/components/rightSidePanel/errors/TabErrors.test.ts`
- `pnpm typecheck`
- push hook: `knip --cache`
2026-06-05 05:25:46 +00:00
Matt Miller
8a819fa2be refactor(assets): read content hash from the canonical hash field (#12638)
## Summary
The assets API exposes an asset's content hash as `hash`. An older
`asset_hash` field was a deprecated alias carrying the same value. This
PR moves the frontend fully onto `hash` and removes `asset_hash` from
the frontend entirely.

## Changes
- Read `asset.hash` (no `?? asset_hash` fallback) across the asset
consumers:
- `useMediaAssetActions` — widget-value variants + cloud-mode
stored-filename resolution
  - `assetsStore` — input-asset-by-filename map
  - `assetMetadataUtils.getAssetUrlFilename`
  - `missingMedia` resolver/scan and `missingModel` scan hash matching
  - `useComboWidget` / `useWidgetSelectItems`
- `assetPreviewUtil.findOutputAsset` now queries `/assets?hash=` instead
of the deprecated `?asset_hash=` param and matches on `a.hash`.
- Removed `asset_hash` from the zod asset schema and the local
`AssetRecord` type. Responses that still include the alias parse cleanly
— zod strips unknown keys — so the declared field protected nothing once
the reads were gone.
- Purged `asset_hash` from all test fixtures/mocks; tests key on the
canonical `hash`.

## Safety / rollout
The API currently emits **both** `hash` and `asset_hash` with identical
values, so reading `hash` is safe today. This is the frontend half of
retiring the alias; the backend stops emitting `asset_hash` only after
this ships and old bundles age out, so there is no window where the
field the UI reads is absent.

## Verification
- `pnpm typecheck`: clean.
- Affected unit tests pass (asset utils, store, media/model scans,
widget composables).
- `grep -rn asset_hash src/`: zero matches.
2026-06-04 18:18:12 +00:00
jaeone94
f9cbaf750f fix: simplify error overlay messaging (#12598)
## Summary

Simplifies the error overlay so it presents one clear title, one clear
message, and one stable details action instead of rendering a list of
per-error messages.

## Changes

- **What**: Extracts the error overlay view model into
`useErrorOverlayState`, adds focused unit coverage for the overlay copy
resolution rules, and updates the overlay E2E coverage to match the new
behavior.
- **Breaking**: None.
- **Dependencies**: None.

### Behavior changes

- The overlay body no longer renders a `<ul>` of individual error
messages. It now always renders a single paragraph message.
- Single-error overlays now prefer toast-specific copy when it exists.
For execution errors, the overlay resolves the message in this order:
`toastMessage`, `displayMessage`, raw `message`, group `displayMessage`,
then group `displayTitle`. The title resolves from `toastTitle`, then
`displayTitle`, then the group title.
- Single non-execution groups use group-level toast/display copy. This
lets grouped error types supply overlay-friendly copy without the
overlay needing to understand each card implementation.
- Multiple-error overlays now ignore individual error item copy in the
overlay itself. The header becomes the pluralized count title, for
example `7 errors found`, and the body becomes the fixed guidance
message: `Resolve them before running the workflow.`
- The overlay is hidden if the store reports an error count but no
resolved overlay message exists. This avoids rendering a visible shell
with an empty body.
- The action button no longer varies by error type in normal app mode.
Missing nodes, missing models, missing media, swap nodes, validation
errors, and runtime errors all use `View details` instead of labels like
`Show missing nodes`, `Show missing models`, `Show missing inputs`, or
`See Errors`.
- App mode keeps its existing `Show errors in graph` action label.
- The overlay width now keeps the previous width as its minimum and
allows a wider maximum, reducing avoidable wrapping in longer error
headers.
- The live region was softened from an assertive alert-style
announcement to `role="status"` with `aria-live="polite"` so updates
such as count changes are less disruptive.

### Tests

- Adds component coverage for the rendered overlay shape and app-mode
action label.
- Adds composable coverage for single execution errors, runtime errors,
grouped missing media errors, multiple-error aggregate copy, hidden
empty-message state, and display-copy fallback behavior.
- Updates `errorOverlay.spec.ts` so the E2E suite checks the new
single-message overlay, the stable `View details` action, and the fixed
multiple-error body guidance.
- Removes the old type-specific button-label E2E expectations because
that branch no longer exists in product behavior.

### Follow-up PR

A follow-up PR is stacked on top of this one:
`jaeone/fe-816-missing-resource-error-messaging`.

That follow-up will wire missing resource error resolvers into the copy
model consumed here. It covers missing node packs, missing models,
missing media, and swap-node groups, including the group-level
`toastTitle`, `toastMessage`, `displayMessage`, `displayDetails`, and
item label copy those cards need. This PR intentionally keeps the
overlay behavior separate so it can merge first without depending on the
missing-resource resolver copy.

## Review Focus

- Please check the single-error versus multiple-error overlay behavior,
especially the fallback order for execution error copy.
- Please check that the `View details` action is now intentionally
error-type agnostic in normal app mode while app mode keeps `Show errors
in graph`.
- Please check the empty-message guard and the requirement that a
single-error overlay only resolves a single group when the total error
count and group list agree.
- Please check the E2E reduction: the old type-specific action-label
assertions were removed because the UI branch they tested was removed.

## Screenshots (if applicable)

N/A
2026-06-04 06:28:14 +00:00
jaeone94
4bfb0c36be Fix Cloud media input defaults (#12562)
## Summary

Fix Cloud media loader widgets so `LoadImage`, `LoadVideo`, and
`LoadAudio` resolve their default values from Cloud input assets instead
of blindly accepting backend `object_info` combo options. When no
matching Cloud input asset exists, the widgets now start empty instead
of selecting a server-only value that immediately trips missing-input
detection.

## Changes

- **What**: Cloud media input widgets now derive their available values
from `assetsStore.inputAssets`, filtered to the node's media type and to
assets with a valid `asset_hash`.
- **What**: Cloud media defaults now prefer an explicit default only
when it matches an available Cloud input asset hash or name, then fall
back to the first matching Cloud input asset, and otherwise use an empty
value.
- **What**: The media path keeps the existing model-widget
implementation style by resolving Cloud asset state through the store
internally, while preserving media-specific hash/name matching because
media widgets submit Cloud `asset_hash` values.
- **What**: Added regression coverage for adding empty Cloud
`LoadImage`, `LoadVideo`, and `LoadAudio` nodes when backend
`object_info` advertises server-only media options.
- **What**: Expanded media widget unit coverage for image/video/audio
inputs, empty defaults, first-asset fallback, default-by-hash matching,
default-by-name matching, hashless asset filtering, unrelated media
filtering, dynamic values, option labels, lazy input loading, and
`control_after_generate` wiring.
- **What**: Kept existing OSS and Cloud runtime missing-media E2E
coverage, with Cloud fixtures consistently using the local setup backend
for `object_info` so tests do not depend on live Cloud backend startup
details.
- **Breaking**: None.
- **Dependencies**: None.

## Review Focus

- Cloud media loader defaults should no longer be sourced from backend
file lists unless the value also corresponds to a Cloud input asset.
- Empty Cloud input-asset libraries should produce empty media widget
values, not missing-input errors at node creation time.
- Model asset-browser behavior is intentionally unchanged. The media
path mirrors the store-access style, but media defaults still resolve to
asset hashes because those are the values submitted by Cloud media
widgets.
- The Cloud E2E fixture stubs bootstrap endpoints and routes
`object_info` through the local setup backend. This keeps the test
focused on frontend behavior while still using realistic node
definitions.

## Testing

- `pnpm exec oxfmt
browser_tests/tests/propertiesPanel/errorsTabMissingMediaRuntime.spec.ts
src/renderer/extensions/vueNodes/widgets/composables/useComboWidget.test.ts
src/renderer/extensions/vueNodes/widgets/composables/useComboWidget.ts`
- `pnpm lint`
- `pnpm typecheck`
- `pnpm typecheck:browser`
- `pnpm exec vitest run
src/renderer/extensions/vueNodes/widgets/composables/useComboWidget.test.ts`
- `PLAYWRIGHT_TEST_URL=http://127.0.0.1:5174
PLAYWRIGHT_SETUP_API_URL=http://127.0.0.1:8188 pnpm exec playwright test
browser_tests/tests/propertiesPanel/errorsTabMissingMediaRuntime.spec.ts
--project=chromium --workers=1`
- `PLAYWRIGHT_TEST_URL=http://127.0.0.1:5175
PLAYWRIGHT_SETUP_API_URL=http://127.0.0.1:8188 pnpm exec playwright test
browser_tests/tests/propertiesPanel/errorsTabMissingMediaRuntime.spec.ts
--project=cloud --workers=1`
- `git diff --check`
- `pnpm knip`
- `.claude/skills/reviewing-unit-tests/SKILL.md` red-flag review

## Screenshots
Before


https://github.com/user-attachments/assets/5df04036-d15c-4f94-bdcd-df8b26a29329

After 


https://github.com/user-attachments/assets/abe7caf5-a83b-4960-aa6f-65a377424a85
2026-06-04 06:27:42 +00:00
Dante
f86ffbb05f fix(assets): dedupe outputs by composite key to prevent media asset panel scroll-duplication (#11716)
## Summary

When the cloud `getJobDetail` returns two output records that resolve to
the same composite key `${nodeId}-${subfolder}-${filename}`,
`mapOutputsToAssetItems` in
`src/platform/assets/utils/outputAssetUtil.ts` produces two `AssetItem`s
with the same synthetic id. The Vue `v-for :key="item.key"` in
`src/components/common/VirtualGrid.vue:10` collides, Vue reuses one DOM
node for the colliding rows, and the user sees one asset visibly
duplicate and progressively replace its neighbours while scrolling
through an expanded large job in the media asset panel — symptom matches
FE-297 in both list and grid views (both views derive from the same
`displayAssets` populated by `resolveOutputAssetItems`).

Fix tracks composite keys per resolved job and skips subsequent records
that collide on the same key. Treats the composite key as the canonical
identity of an output, so each rendered row is unique in both views
without changing public id semantics for non-colliding inputs.

- Fixes FE-297
- Source: [Slack
#bug-dump](https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1777047001770899)

## Red-Green Verification (unit)

| Commit | Purpose | CI |
| --- | --- | --- |
|
[`b263af29b`](https://github.com/Comfy-Org/ComfyUI_frontend/pull/11716/commits/b263af29b)
test: FE-297 add failing test for asset id collision on duplicate output
key | Proves the test catches the regression | 🔴 [Red run
25038514049](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25038514049)
— `FE-297: deduplicates outputs that share the same composite output
key` failed with `expected length 2 but got 3` |
|
[`af38cad1d`](https://github.com/Comfy-Org/ComfyUI_frontend/pull/11716/commits/af38cad1d)
fix(assets): dedupe outputs by composite key to prevent visual collapse
on scroll | Proves the fix resolves it | 🟢 [Green run
25038832343](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25038832343)
— all unit tests pass |

Local verification (`pnpm vitest run
src/platform/assets/utils/outputAssetUtil.test.ts`):
- On red commit: `FE-297: deduplicates outputs that share the same
composite output key` fails with `expected length 2 but got 3`.
- On green commit: all 8 tests pass; broader `pnpm vitest run
src/platform/assets/` reports 390/390 pass.

## Browser-level Verification (e2e)


[`c5f80e225`](https://github.com/Comfy-Org/ComfyUI_frontend/pull/11716/commits/c5f80e225)
adds `browser_tests/tests/sidebar/assets-fe297-dedupe.spec.ts`. The spec
mocks `/api/jobs` + `/api/jobs/{jobId}` so the assets sidebar receives a
5-output stack job whose detail payload contains two records sharing
`9--duplicate_00002_.png`. It expands the stack into folder view and
reads the underlying `VirtualGrid` row total from the top/bottom spacer
heights so the assertion does not depend on viewport size, scroll
virtualization, or Vue's same-key DOM reuse.

Locally validated red→green (`pnpm dev` + local ComfyUI on :8188, `pnpm
exec playwright test … --project=cloud`):

| Source | `totalRows` | rendered tile labels | result |
| --- | --- | --- | --- |
| Red (dedup removed from `mapOutputsToAssetItems`) | **5** |
`[duplicate_00002_.png, duplicate_00002_.png, distinct_00004_.png]` (two
adjacent collisions) | 🔴 `Expected: 4 / Received: 5` |
| Green (HEAD) | **4** | `[duplicate_00002_.png, distinct_00004_.png,
distinct_00003_.png]` (no collisions) | 🟢 `1 passed` |

Manual repro of the same flow against the running dev server (Chrome
DevTools, runtime `fetch` interceptor for `/api/jobs/<id>`) reproduces
the FE-297 symptom directly: with the fix removed, the folder view
renders **two consecutive cards both labelled `duplicate_00002_.png`**;
with the fix applied that second slot is replaced by the next distinct
file.

## Cloud Prod Verification (unmocked)

Verified against a real cloud prod job
(`22dda683-1634-4120-8a7d-233cff28e07e`) whose `/api/jobs/{id}` payload
organically contains the FE-297 trigger condition.

Raw cloud response analysis: **35 output records across 27 nodes, 34
unique composite keys, 1 colliding key**
(`103--cbbce08934d17e85987b09824ae519822e4462b4294fc52fc40dca8d5b096323.png`
appears twice — same nodeId/subfolder/filename emitted from two distinct
output records).

Expanded folder view on local FE running this branch against cloud prod
backend:

| Measurement | Value | Expectation |
| --- | --- | --- |
| `outputCount` (backend) | 35 | — |
| Unique composite keys (backend) | 34 | (35 - 1 collision) |
| VirtualGrid rendered cells (`totalRows * colCount` via spacer height)
| **34** | matches dedupe target |
| Simultaneously-visible duplicate labels | 0 | no Vue `:key` collision
|

This is the first visual confirmation of FE-297 against actual prod data
rather than synthetic/mocked payloads. The block had been pending on
FE-844 — cloud `/api/jobs/{id}` returns `text: [null]` for empty
subgraph promoted text outputs, which the FE Zod schema rejects, causing
`fetchJobDetail` to return `undefined` and the folder view to collapse
to the cover preview only. With [PR
#12449](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12449)
(FE-844) applied locally, the job-detail response reaches
`resolveOutputAssetItems`, FE-297's dedupe runs, and the 35→34 collapse
is observable end-to-end.

## Test Plan

- [x] New unit regression covering composite-key collision in
`mapOutputsToAssetItems`
- [x] Existing `outputAssetUtil` tests still pass (`job-1-1-sub-a.png`
etc. id format unchanged for non-colliding inputs)
- [x] Broader asset platform suite (`src/platform/assets/`) passes
- [x] CI red-green sequence captured (links above)
- [x] Browser-level e2e (`assets-fe297-dedupe.spec.ts`) red→green
validated locally; will run in CI on this commit
- [x] Manual repro on dev server confirms the duplicate-card symptom
disappears with the fix
- [x] Unmocked cloud-prod repro: real 35-output job with 1 organic
composite-key collision renders 34 cells, 0 duplicate labels
simultaneously visible (requires
[#12449](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12449) for
job-detail fetch to succeed)
2026-06-04 01:28:02 +00:00
Alexander Brown
488bc33288 refactor: drop primevue/colorpicker from settings form and customization selector (FE-804) (#12391)
*PR Created by the Glary-Bot Agent*

---

## Summary

The node-canvas COLOR widget (`WidgetColorPicker.vue`) already migrated
off PrimeVue; this PR finishes FE-804 by porting the two remaining
`primevue/colorpicker` consumers — `FormColorPicker.vue` (settings form
`type: 'color'`) and `ColorCustomizationSelector.vue`
(folder-customization dialog) — to the in-house Reka-UI based
`ColorPicker`. The now-dead PrimeVue overlay workaround in
`CustomizationDialog.vue` is removed.

After this lands there are **zero `primevue/colorpicker` imports left**
in `src/`.

## Changes

- **What**: `FormColorPicker.vue` swaps `primevue/colorpicker` +
`primevue/inputtext` for the in-house `ColorPicker` + `Input`. The
legacy "hex without `#`" storage contract (e.g. `load3d`'s
`BackgroundColor` default `'282828'`) is preserved on read and on write.
- **What**: The text input now uses a separate draft value and only
commits on blur / Enter when the input is a complete 6- or 8-digit hex.
This fixes the "type `#f` and watch it snap to black" regression that a
naive shared-`v-model` implementation re-introduces.
- **What**: `disabled`, `id`, and `aria-labelledby` are now explicit
props on `FormColorPicker` and are forwarded to both children. The
custom `ColorPicker` learned a `disabled` prop that propagates to its
`<PopoverTrigger>` button.
- **What**: `ColorCustomizationSelector.vue` swaps
`primevue/colorpicker` for the in-house `ColorPicker` (still uses
`primevue/selectbutton` — intentionally out of scope per FE-804's title;
`SelectButton` migration is a separate effort).
- **What**: `CustomizationDialog.vue` drops the `.p-colorpicker-panel,
.p-overlay, .p-overlay-mask` `pointer-down-outside` guard. With PrimeVue
ColorPicker gone, no descendant of this dialog teleports an overlay to
`<body>` anymore.
- **What**: Updates two affected browser tests — `extensionAPI.spec.ts`
(the `disabled` attr smoke check) and `sidebar/nodeLibrary.spec.ts` (the
bookmark color customization flow) — to target the new picker via stable
accessible names (`role="slider"` + i18n aria-label `Color saturation
and brightness`) and the `.color-picker-wrapper > button` trigger. The
disabled-attr eval helper now handles `HTMLButtonElement` in addition to
`HTMLInputElement`.
- **What**: Adds `FormColorPicker.test.ts` with focused regression
coverage for the manual-entry contract: legacy no-`#` storage
round-trip, no commit on partial hex, revert on partial-then-Enter,
8-digit alpha hex, and `disabled` propagation.
- **Dependencies**: none added; removes two PrimeVue imports.
- **Breaking**: no breaking change to the documented FormItem `'color'`
setting contract. Manual-entry semantics change: typing partial hex no
longer immediately writes mangled state — it commits on blur or Enter
when the value fully parses. Existing settings values are unaffected.

## Verification

- `pnpm typecheck` clean
- `pnpm typecheck:browser` clean
- `pnpm exec eslint` on every touched file clean
- `pnpm test:unit` over the affected directories — **216 passed**
- Manual QA via Playwright against the running dev server:
- Registered a test extension with `type: 'color'` + a `disabled: true`
variant
- Confirmed the new picker renders, opens its Reka popover, and the
disabled row has `button.disabled === true` and `input.disabled ===
true`
  - Confirmed partial hex (`#ab`) does **not** clobber the swatch
- Confirmed `#1133aa` + blur commits and round-trips through the picker

## Review focus

1. The manual-entry commit gate in `FormColorPicker.vue`
(`commitDraft()` + `FULL_HEX`) — is the regex strict enough? Should
3/4-digit shorthand hex be accepted on commit too? PrimeVue accepted
3-digit shorthand; the existing `toHexFromFormat()` already does, so
adding `|[0-9a-f]{3}|[0-9a-f]{4}` is a one-line change if reviewers want
parity.
2. Disabled-attr E2E selector swap (`.p-colorpicker-preview` →
`.color-picker-wrapper > button`) + the eval-helper update that now
handles `HTMLButtonElement` in addition to `HTMLInputElement`. The
structural selector matches what PrimeVue had; happy to add a
`data-testid` if reviewers prefer.
3. `ColorPicker.vue` gained a `disabled` prop — kept explicit (peer of
`class`) to match the existing prop shape rather than forwarding through
`$attrs`.

## Follow-up (NOT in this PR)

Discussed in-thread — the **Reka-UI `ColorField` migration** (full
picker rebuild) and the **Kijai regression suite** for alpha-disabled +
manual-entry on the node-canvas COLOR widget belong in a separate,
scoped PR alongside the `ColorInputSpec` schema additions (`hasAlpha`,
`format`). The custom picker also has a known lossy HSV-percent
quantization (e.g. `#1133aa` round-trips to `#1033a9`) that pre-dates
this PR and would be addressed by the Reka primitives.

- Fixes FE-804


## Screenshots

![Migrated FormColorPicker: orange swatch with hex label next to a text
input showing legacy no-# value, plus a disabled row visibly
muted](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/83c8829f71d395971cd24bb8b016314e129aa2c3a1e431eec372e88bbce09aa5/pr-images/1779319155370-5029e334-00fb-4610-8aae-7d7436069473.png)

![Color picker popover open: saturation/value panel, hue slider, alpha
slider, hex/rgba dropdown, hosted by Reka-UI
Popover](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/83c8829f71d395971cd24bb8b016314e129aa2c3a1e431eec372e88bbce09aa5/pr-images/1779319155802-900f903d-0e4b-4a9a-9d23-da7dd0e02358.png)

![Partial hex entry: text input shows '#ab' while the picker swatch
remains orange - no clobber while
typing](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/83c8829f71d395971cd24bb8b016314e129aa2c3a1e431eec372e88bbce09aa5/pr-images/1779319156247-cc507939-6102-45e5-ba13-1c16fbe1fb53.png)

![After blur the text input committed and the swatch turned blue; value
stored in legacy no-#
format](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/83c8829f71d395971cd24bb8b016314e129aa2c3a1e431eec372e88bbce09aa5/pr-images/1779319156568-f758dbdc-62fa-41dd-a5fc-fa503360c332.png)

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
2026-06-03 20:51:23 +00:00
Dante
60a4dc3001 fix: dedupe Bypass context-menu items via state-aware legacy label (FE-720) (#12500)
## Summary

Right-clicking a bypassed node showed two bypass-related items in the
Vue "More Options" context menu (FE-720):

- Plain `Bypass` from the legacy LiteGraph `getExtraMenuOptions` hook in
`litegraphService.ts`
- `Remove Bypass` (with `Ctrl+B` and an icon) from the Vue
`getBypassOption` composable

The Vue menu's exact-label deduplicator in `contextMenuConverter.ts`
collapsed the unbypassed case (both emit `Bypass` → Vue source wins) but
not the bypassed case (`Bypass` vs `Remove Bypass`), so the duplicate
leaked through whenever the node was bypassed.

### before
<img width="1920" height="958" alt="fe-720-before"
src="https://github.com/user-attachments/assets/ef001aca-d70e-4798-ac61-01cc34c31e44"
/>

### after
<img width="1920" height="958" alt="fe-720-after"
src="https://github.com/user-attachments/assets/d6d2bf4b-cb98-4b30-9dac-9bd4b68a7e36"
/>

#### single active node (KSampler)
<img width="1920" height="958" alt="fe-720-1-unbypassed-node-menu"
src="https://github.com/user-attachments/assets/bec9cd47-2f2d-4adb-b95b-266e7969a36c"
/>

#### single bypassed node (Load Checkpoint)
<img width="1920" height="958" alt="fe-720-2-bypassed-node-menu"
src="https://github.com/user-attachments/assets/91f80157-836d-4fce-adad-474f31baff04"
/>

#### KSampler + bypassed Load Checkpoint
<img width="1920" height="958" alt="fe-720-3-mixed-selection-menu"
src="https://github.com/user-attachments/assets/e4780b16-08e5-4f87-80e9-3ff65a5acdae"
/>

## Root cause

`src/services/litegraphService.ts` pushes a `Bypass` entry from its
legacy `getExtraMenuOptions` hook in addition to the Vue
`getBypassOption`. In Vue-menu mode both reach the menu; the exact-label
dedup in `contextMenuConverter.ts` only collapses them when the labels
match, which fails once the node is bypassed and the Vue side switches
to `Remove Bypass`.

## Fix

Add `Bypass` and `Remove Bypass` to the `HARD_BLACKLIST` in
`contextMenuConverter.ts`. The blacklist filters the legacy emission out
of the Vue conversion pipeline (`convertContextMenuToOptions`) before it
is ever merged, so Vue's `getBypassOption` is the single source of the
bypass item in every node state — no duplicate is created in the first
place. This is the established convention for legacy items that the Vue
menu replaces (`Properties`, `Colors`, `Shapes`, `Title`, `Mode`,
`Properties Panel`, `Copy (Clipspace)`); Bypass is the same category.

`litegraphService.ts` reverts to a plain `content: 'Bypass'` and no
longer imports `areAllSelectedNodesInMode` or i18n keys for this entry.

The Vue `getBypassOption` label is still derived from the same
selection-aware predicate (`areAllSelectedNodesInMode`) that
`toggleSelectedNodesMode` uses, so on mixed selections the label stays
in sync with the action — it shows `Bypass` when clicking would bypass
the rest, rather than `Remove Bypass`.

**Trade-off:** the classic LiteGraph canvas menu
(`Comfy.VueNodes.Enabled: false`) renders `litegraphService`'s options
directly without going through `convertContextMenuToOptions`, so it
shows a plain `Bypass` regardless of node state. This matches the pre-PR
behavior (the legacy push was already a hardcoded `Bypass`), so it is
not a regression.

## Considered and rejected

- **`equivalents` map** (`bypass: ['bypass', 'remove bypass']`) — would
collapse `Bypass` and `Remove Bypass` as synonyms, which is semantically
wrong: they are distinct actions that must stay distinguishable, and the
rule would also misfire on the unbypassed case. A converter test locks
in that they are not treated as equivalents.
- **State-aware label on the legacy push** (matching the Vue label so
the exact-label dedup collapses them) — works, and additionally gives
the classic canvas menu a state-aware label, but it couples
`litegraphService` to the selection predicate and i18n keys solely to
keep a downstream dedup load-bearing. `HARD_BLACKLIST` removes the
duplicate at the source instead of creating, converting, then collapsing
it. The only thing lost is the classic-menu state-aware label, which was
never present pre-PR.
- **Gating the legacy push on `Comfy.UseNewMenu === 'Disabled'`** — the
setting that selects the legacy vs Vue context menu is
`Comfy.VueNodes.Enabled`, not `Comfy.UseNewMenu` (an unrelated
top-menu-bar toggle). Gating on `UseNewMenu` would drop the Bypass entry
from the legacy canvas menu for the OSS default (`VueNodes.Enabled:
false` + `UseNewMenu: 'Top'`).
- **Suppressing the legacy callback via
`SUPPRESSED_LITEGRAPH_CALLBACKS`** — matches by callback identity and
adds cross-file coupling for what is a simple label-based filter that
`HARD_BLACKLIST` already expresses.

## Cleanups (review feedback)

- Removed the now-dead `NodeSelectionState.bypassed` field and its
producer (no consumers after the label switch).
- Replaced the `vue-i18n` mock in `useNodeMenuOptions.test.ts` with a
real `createI18n` instance per `docs/testing/vitest-patterns.md`;
removed a `ts-expect-error` via a typed hoisted `app` mock.
- Simplified `getSelectedNodeArray` to
`Object.values(app.canvas.selected_nodes ?? {})`.

## Tests

- `useSelectedLiteGraphItems.test.ts` — `areAllSelectedNodesInMode`:
all-bypassed → true, mixed → false, empty → false.
- `useNodeMenuOptions.test.ts` — Vue label is `Bypass` (active / mixed)
and `Remove Bypass` (all bypassed).
- `contextMenuConverter.test.ts` — the legacy `Bypass` push is filtered
by `HARD_BLACKLIST` so the Vue item is the only bypass entry (keeps
shortcut/source); `Bypass` and `Remove Bypass` are not treated as label
equivalents.
- `browser_tests/tests/vueNodes/interactions/node/contextMenu.spec.ts` —
e2e regression: exactly one bypass-family item per node state.

Verified live on a bypassed Load Checkpoint: single `Remove Bypass` →
toggle un-bypasses → single `Bypass`; no duplicate, rest of the menu
intact.

- Fixes FE-720

---------

Co-authored-by: Alexander Brown <drjkl@comfy.org>
2026-06-02 01:12:55 +00:00
Alexander Brown
c4c1dfa58a Remove drag node test from interaction.spec.ts (#12579)
It's flaky.
2026-06-02 01:05:48 +00:00
AustinMroz
1938ba809b Track undo state on subgraph conversion (#12575)
When converting to subgraph, `beforeChange` and `afterChange` were being
called, but these functions exclusively called vestigial change handlers
that don't actually affect change tracking.

Consequentially, if you made a change to the graph (updating a widget),
converted a node to a subgraph using the selectionToolbox, and then
pushed Ctrl+Z before performing any other canvas interaction, it would
incorrectly undo the prior widget edit as well.

This is resolved by calling the important handlers directly. Adding them
to `beforeChange`/`afterChange` was considered, but caused breakage in
other functions (`connect`) which failed to even attempt symmetric calls
of the function.
2026-06-02 00:36:43 +00:00
Dante
e16a0bfe82 fix(knip): narrow Playwright entrypoints so browser-test dead exports are reported (FE-717) (#12496)
## Summary

Narrow Knip's Playwright `entry` to actual spec files so dead exports in
browser-test fixtures are reported instead of being hidden by treating
every helper as an entrypoint.

## Changes

- **What**:
- `knip.config.ts`: Playwright `entry` changed from the broad
`['**/*.@(spec|test)…', 'browser_tests/**/*.ts']` to
`['browser_tests/**/*.@(spec|test).?(c|m)[jt]s?(x)']`.
`globalSetup`/`globalTeardown` stay covered via Knip's playwright config
resolution; fixtures remain in the project graph so their unused exports
surface.
- Resolved the 54 dead findings this exposed: over-exported symbols used
only within their own module are now module-private (dropped `export`,
no behavioral change); genuinely unreferenced fixtures were deleted
(asset/template `ALL_*` aggregators + orphaned `STABLE_*` data,
`TemplateHelper` distribution helpers + `generateTemplates`, dead
types/utils, and the unused `nodeDefinitions.ts` module).
- **Breaking**: none — test-only changes.

## Review Focus

- Deletions are limited to fixtures with zero importers on `main`
(verified via `pnpm knip`); the bulk of the diff is `export`-keyword
removal.
- Verified: `pnpm knip` (browser_tests clean), `pnpm typecheck`, `pnpm
typecheck:browser`, oxfmt/oxlint/eslint all pass.

Linear: FE-717
2026-06-01 07:53:29 +00:00
Dante
3a8ddfb6f1 fix: wrap long workflow name in Open shared workflow dialog (FE-828) (#12540)
## Summary

The "Open shared workflow" dialog rendered the workflow name in an
`<h2>` with no
wrapping control. A long, space-free name (e.g. a content-hash filename)
is a
single unbreakable "word", so with the default `overflow-wrap: normal`
it could
not wrap. It overflowed its box and, because PrimeVue's
`.p-dialog-content` is
`overflow-x: auto`, the dialog scrolled horizontally instead of
wrapping.

CDP measurement on the unfixed build (96-char name): dialog content
`scrollWidth 1336` vs `clientWidth 702` -> horizontal scroll. After
adding
`wrap-anywhere` to the heading: `scrollWidth 702 == clientWidth 702`,
name wraps
to multiple lines, full name still in the DOM.

### before
<img width="704" height="295" alt="before-dialog"
src="https://github.com/user-attachments/assets/ea05ab32-a80d-4210-951c-f43d595bd6eb"
/>

### after
<img width="704" height="359" alt="after-dialog"
src="https://github.com/user-attachments/assets/cbf3019e-5e71-4dba-a1fd-ea3586dd995a"
/>

## Changes

- `OpenSharedWorkflowDialogContent.vue`: add `wrap-anywhere` to the
workflow-name
`<h2>` so a long unbreakable name wraps within the dialog bounds instead
of
  forcing horizontal scroll. The parent already has `min-w-0`.
- Breaking: none

## Red-Green Verification

| Commit | CI | Purpose |
|--------|-----|---------|
| [`test:`
d9158d1](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/26678596015)
| 🔴 Red (failure) | Proves the test catches the bug — cloud
e2e failed at `expect(scrollWidth).toBeLessThanOrEqual(clientWidth +
1)`: received 1332, expected <= 703 |
| [`fix:`
08e75a1](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/26678974321)
| 🟢 Green (success) | Proves the fix resolves it |

Fixes FE-828

## Test Plan

- [x] CI red on test-only commit
- [x] CI green on fix commit
- [x] E2E regression
`browser_tests/tests/dialogs/openSharedWorkflowDialog.spec.ts` (@cloud):
mocks a long-named shared workflow, asserts the dialog does not overflow
horizontally
- [x] Manual CDP verification (before/after above)
2026-06-01 07:53:16 +00:00
AustinMroz
e97c4b6ab9 Remove flake screenshot (#12529) 2026-05-29 21:08:46 +00:00
jaeone94
fb58a76a53 fix: preserve validation errors on execution start (#12493)
## Summary

Preserve validation node errors and their overlay when a valid active
root starts execution, so partial workflow runs no longer hide
validation failures.

## Changes

- **What**: Split execution-start clearing from full error clearing;
`execution_start` now clears transient execution/prompt state without
clearing validation `lastNodeErrors`.
- **What**: Keep the ErrorOverlay open when validation errors are still
present, and show it for successful prompt responses that include
`node_errors`.
- **Dependencies**: None.

## Review Focus

Please check the error-clearing boundary between prompt
submission/workflow changes and WebSocket `execution_start`. Full
clearing still happens through `clearAllErrors`; execution start now
uses the narrower clearing path and only dismisses the overlay when
there are no validation node errors to show.

Linear: FE-851

## Red-Green Verification

- Red: `76bcf34c4 test: add failing validation error preservation e2e`
- Green: `9766172ea fix: preserve validation errors on execution start`
- Follow-up: `321c95aba fix: keep validation error overlay during
execution start`
- Coverage: `7b5fab577 test: cover prompt node error overlay`

## Test Plan

- `pnpm exec vitest run src/scripts/app.test.ts`
- `pnpm exec vitest run src/stores/executionStore.test.ts`
- `pnpm exec vitest run src/scripts/app.test.ts
src/stores/executionStore.test.ts --coverage`
- `pnpm format:check -- src/stores/executionErrorStore.ts
src/stores/executionStore.ts src/stores/executionStore.test.ts
src/scripts/app.ts src/scripts/app.test.ts
browser_tests/fixtures/helpers/ExecutionHelper.ts
browser_tests/tests/execution.spec.ts`
- `pnpm exec oxlint src/stores/executionErrorStore.ts
src/stores/executionStore.ts src/stores/executionStore.test.ts
src/scripts/app.ts src/scripts/app.test.ts
browser_tests/tests/execution.spec.ts --type-aware`
- `pnpm typecheck`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://127.0.0.1:5175 pnpm
exec playwright test browser_tests/tests/execution.spec.ts:132`

## Screenshots (Before/After)
Before


https://github.com/user-attachments/assets/04a212b6-66f9-4c77-9056-58bdc642d96e

After


https://github.com/user-attachments/assets/db7813c7-bf8a-4e19-9b66-7f49fd01c305
2026-05-29 17:20:54 +00:00
AustinMroz
b7990f7645 Fix ghost links on IO remove slot (#12473)
Context menu operations on subgraph IO slots only set the foreground
canvas as dirty, so links would visually persist until a different
operation caused a background draw.
2026-05-29 16:57:52 +00:00
Dante
c57944f315 fix: hide duplicate LiteGraph Resize/Collapse/Expand entries from Vue node menu (FE-867) (#12487)
## Summary

https://linear.app/comfyorg/issue/FE-867/bug-node-expand-menu-doesnt-work-nodes-immediately-collapse-after
Recreates #12175 on a fresh `main` base (original branch's CI failed
only because its `frontend-dist` artifact had expired — not a code
issue). Original work by @christian-byrne / Glary-Bot, cherry-picked
here so it can land while he's offline.

The Vue right-click "More Options" node menu shows duplicates for
collapse/expand functionality:

- **Vue source**: `Minimize Node` / `Expand Node` (works)
- **LiteGraph source**: `Resize`, `Collapse`, `Expand` (silently no-op
in this menu — the converter wrapper invokes
`LGraphCanvas.onMenuNodeCollapse` without the `node` arg it expects)

Suppress the LiteGraph duplicates in `convertContextMenuToOptions` by
matching the built-in **callback identity**
(`LGraphCanvas.onMenuResizeNode`, `LGraphCanvas.onMenuNodeCollapse`),
not the raw label. Matching by identity avoids accidentally hiding
extension-provided items that share those labels.

Also align `CORE_MENU_ITEMS` / `MENU_ORDER` on the Vue label `Expand
Node` so the toggled Minimize/Expand pair sorts correctly.

## Scope of suppression

Only the Vue node menu (via `convertContextMenuToOptions`) is affected.
The raw `LGraphCanvas.getNodeMenuOptions` output is untouched, so:

- The legacy right-click menu (`Comfy.UseNewMenu` disabled) still has
`Collapse` / `Resize`.
- `useLoad3d.ts`, which calls `new
LiteGraph.ContextMenu(app.canvas.getNodeMenuOptions(node), ...)`, is
unaffected.
- Extensions that monkey-patch `getNodeMenuOptions` continue to receive
the full option list.

## Tests

- `contextMenuConverter.test.ts`: covers both that built-in entries are
dropped by identity AND that extension-provided items with the same
labels survive.
- E2E `selectionToolboxMoreActions.spec.ts`: asserts the Vue "More
Options" menu shows `Minimize Node` but no `Resize`/`Collapse`/`Expand`.
- `pnpm typecheck` clean.

Supersedes #12175.

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
2026-05-29 22:28:08 +09:00
jaeone94
d86483a6af refactor: consolidate middle-button pan handling (#12491)
## Summary

Refactors middle mouse button pan handling around the intent of #11409,
dropping the outdated implementation details from that PR and aligning
the core behavior with the current main branch.

## Changes

- **What**: Centralized phase-specific middle mouse button handling in
`src/base/pointerUtils.ts`, added a shared Vue widget forwarding helper,
and updated canvas, LiteGraph, Vue node, and mask editor call sites to
use the same semantics.
- **Breaking**: None expected. This keeps existing middle-click pan
behavior while making pointerdown, pointermove, pointerup, and auxclick
checks explicit for their event phases.
- **Dependencies**: None.

## Review Focus

This PR is intentionally narrower than #11409. That PR had the right
goal, but its implementation became outdated against main: mask editor
tests now have helper coverage on main, Vue node/widget code has
shifted, and a blanket replacement with `isMiddlePointerInput` would
lose the bitmask behavior needed during pointermove drags.

The core difference is that this PR preserves the useful part of #11409,
namely removing scattered ad-hoc MMB checks, while avoiding stale
changes that no longer fit the current codebase.

Key behavior changes:

- `isMiddlePointerInput` is the conservative pointerdown-style check:
changed middle button or strict middle-only `buttons === 4`.
- `isMiddleButtonHeld` handles pointermove-style held-button bitmasks so
chorded drags with the middle button still pan.
- `isMiddleButtonEvent` handles pointerup/auxclick-style changed-button
events.
- Call sites now choose the phase-specific helper directly instead of
routing through an event-type dispatcher.
- String and markdown widgets now share
`forwardMiddleButtonToCanvas(...)` instead of duplicating three pointer
listeners each.
- The widget helper intentionally keeps the existing
`app.canvas.processMouseDown/Move/Up` forwarding route and only
centralizes the duplicated listener logic.
- Mask editor pan handling, Vue node pointer forwarding, graph canvas
pan forwarding, LiteGraph middle-click checks, input indicators, and
transform settling now use the centralized helpers.

Coverage added or updated:

- Unit coverage for middle-button helper semantics, including chorded
pointermove drags and pointercancel held-bit behavior.
- Unit coverage for widget forwarding helper down/move/up routing.
- Regression coverage for canvas, mask editor, Vue node media preview,
and transform-settling pointer handling.
- Browser coverage for middle-click drag panning on a Vue node, a
multiline string widget, and the mask editor canvas.

Validation run:

- `pnpm format`
- `pnpm lint`
- `pnpm typecheck`
- `pnpm test:unit src/base/pointerUtils.test.ts
src/renderer/extensions/vueNodes/widgets/utils/forwardMiddleButtonToCanvas.test.ts
src/renderer/extensions/vueNodes/widgets/composables/useStringWidget.test.ts
src/renderer/extensions/vueNodes/widgets/composables/useMarkdownWidget.test.ts
src/renderer/core/canvas/useCanvasInteractions.test.ts
src/composables/maskeditor/useToolManager.test.ts
src/renderer/core/layout/transform/useTransformSettling.test.ts
src/composables/node/useNodeImage.test.ts
src/composables/node/useNodeAnimatedImage.test.ts
src/components/graph/SelectionToolbox.test.ts
src/lib/litegraph/src/LGraphCanvas.slotHitDetection.test.ts`
- `pnpm typecheck:browser`
- `pnpm test:browser:local
browser_tests/tests/vueNodes/interactions/canvas/pan.spec.ts
browser_tests/tests/vueNodes/widgets/text/multilineStringWidget.spec.ts
browser_tests/tests/maskEditor.spec.ts --project chromium --grep
"Middle-click drag"`
- Commit hook: staged file format/lint, `pnpm typecheck`

## Screenshots (if applicable)

Not applicable; this is interaction behavior covered by unit and browser
tests.
2026-05-29 16:12:12 +09:00
Alexis Rolland
dc1bc4c9f8 Update utils category to utilities (#12498)
## Summary

Update frontend only nodes categories to consolidate utility nodes into
a `utilities` category (instead of utils). Paired with changes done in
the core repo here: https://github.com/Comfy-Org/ComfyUI/pull/14145

## Changes

- **What**:
  - Rename frontend only nodes category from `utils` to `utilities`
- Move frontend only Primitive node from `utils` to
`utilities/primitive`

## Screenshots

<img width="563" height="352" alt="image"
src="https://github.com/user-attachments/assets/a768ec48-fb87-4fa3-934a-bd593bb35f3d"
/>

<img width="1181" height="773" alt="image"
src="https://github.com/user-attachments/assets/a3e09e25-3412-4d23-abe8-220948b87258"
/>
2026-05-29 02:03:44 +00:00
AustinMroz
767bd17077 Fix "open tutorial button" not working in templates (#12511)
The "open tutorial" button only existed in the DOM when the template
card as actively hovered. For reasons I can not comprehend (probably
overzealous pointer handlers somewhere), the act of clicking on the
button would fire a mouseleave event. This caused the button to
disappear for the exact moment it was clicked alike to a mischievous
dondurma vendor.

This is resolved by keeping the button always in DOM, but making it
invisible when the card isn't hovered.

The PR also removes a deeply nested `v-bind='$attrs'`. I'm assuming it
must be a mistake that attributes applied to the entire template
selector dialogue would be bound to every deeply nested tutorial button
on individual workflow cards.
2026-05-28 23:38:15 +00:00
jaeone94
dc8471c6d3 fix: show workflow refresh loading state (#12509)
## Summary

Adds visible loading feedback to the Workflows sidebar refresh button so
users can tell when a workflow sync request is in flight.

## Changes

- **What**: Exposes `isSyncLoading` from the workflow store and binds
the Workflows sidebar refresh button to disabled, `aria-busy`, and
spinning icon states while sync is pending.
- **What**: Adds stable E2E selectors for the workflows refresh button
and covers the loading state with unit and browser tests.
- **Dependencies**: None.

## Review Focus

Please verify the refresh control behavior while
`/api/userdata?dir=workflows` is pending, especially that the button is
disabled, exposes busy state, and returns to idle after sync completes.

## Validation

- `pnpm format`
- `pnpm test:unit
src/components/sidebar/tabs/BaseWorkflowsSidebarTab.test.ts`
- `pnpm test:browser:local browser_tests/tests/sidebar/workflows.spec.ts
-g "Shows loading state while refreshing workflows"`
- `pnpm lint`
- Commit hooks: `oxfmt`, `oxlint`, `eslint`, `typecheck`,
`typecheck:browser`

## Screenshots (if applicable)


https://github.com/user-attachments/assets/e8b893ae-a91d-45c9-81ea-adaf164de227
2026-05-28 17:31:42 +00:00
Christian Byrne
8206022982 fix(subgraph): validate URL hash and redirect to root when subgraph missing (#12169)
*PR Created by the Glary-Bot Agent*

---

## Summary

Fix FE-559: browser forward/back to a deleted subgraph used to leave the
canvas on stale state (and sometimes triggered unrelated tab navigation)
because the subgraph id in the URL hash was looked up with no validation
or fallback.

## Changes

- **What**:
- Added `src/schemas/subgraphIdSchema.ts` — `zSubgraphId =
z.string().uuid()` + `isValidSubgraphId(value)` type guard, matching how
subgraph ids are persisted in `workflowSchema.ts` and generated by
`createUuidv4()`.
- `subgraphNavigationStore.navigateToHash()` now (a) validates the hash
with `isValidSubgraphId` before any lookup, (b) redirects to the root
graph (`router.replace('#' + root.id)` + `canvas.setGraph(root)`) when
the locator is malformed, missing from `root.subgraphs`, or still
unresolved after a workflow-load attempt.
- Replaced the `console.error('subgraph poofed after load?')` dead-end
with the same redirect helper.
- Re-ordered the "already on this graph" short-circuit so a stale canvas
reference to a now-deleted subgraph doesn't suppress the redirect.

## Review Focus

- TDD: 6 new tests in `subgraphNavigationStore.navigateToHash.test.ts`
cover valid navigation, deleted-subgraph hash, malformed (non-UUID)
hash, no-op when target equals current, empty-hash root case, and
stale-canvas recovery. 15 new tests in `subgraphIdSchema.test.ts` lock
down the validator.
- `redirectToRoot()` toggles `blockHashUpdate` while calling
`router.replace`, so the new redirect doesn't re-trigger `updateHash()`
and clobber the canvas state.
- Generalized validation: the new schema lives in `src/schemas/` and can
be reused anywhere a subgraph id crosses an untrusted boundary (URL,
IPC, etc.).

## Manual Verification

Ran ComfyUI backend (`--cpu --port 8188`) + frontend dev server, then
drove Playwright through three scenarios:

| Input hash | Result | Console |
|---|---|---|
| `#11111111-2222-4333-8444-555555555555` (UUID-shaped, non-existent) |
URL replaced with `#<root-id>` | `[subgraphNavigation] subgraph not
found: 11111111-…; redirecting to root graph` |
| `#not-a-valid-uuid` (malformed) | URL replaced with `#<root-id>` |
`[subgraphNavigation] invalid subgraph id in hash: not-a-valid-uuid;
redirecting to root graph` |
| `#aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee` (UUID-shaped, non-existent) |
URL replaced with `#<root-id>` | (same redirect message) |

Screenshot below shows the redirected viewport.

Fixes FE-559

## Screenshots

![ComfyUI canvas after a hash referencing a deleted subgraph was
rewritten to the root graph
hash](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/fe7f8846b3efdc95461cd63995dd10808073dd86c561eff9d8816742eb892687/pr-images/1778562546959-43f5ead4-3e13-45de-a0ac-988c3424368b.png)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12169-fix-subgraph-validate-URL-hash-and-redirect-to-root-when-subgraph-missing-35e6d73d3650819f840af1475b9f44d4)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: jaeone94 <89377375+jaeone94@users.noreply.github.com>
2026-05-28 00:34:42 +00:00
Dante
a931acadd3 feat(dialog): migrate Settings dialog to Reka-UI (Phase 3) (#12182)
## Summary

Phase 3 of the dialog migration. Closes the parity gaps in the Reka
renderer (maximize affordance, headless layout mode, overlay-class
plumbing), then flips `useSettingsDialog` onto the Reka path. Public API
of `useDialogService` / `dialogStore` is unchanged.

Parent:
[FE-571](https://linear.app/comfyorg/issue/FE-571/dialog-system-migration-primevue-reka-ui-parent)
This phase:
[FE-575](https://linear.app/comfyorg/issue/FE-575/phase-3-migrate-settings-dialog-workspace-non-workspace-designer)
Predecessors: #11719 (Phase 0, merged), #12041 (Phase 1, merged), #12109
(Phase 2, **stacked PR base**)

> **Stacked on Phase 2**: this PR targets
`jaewon/dialog-reka-migration-phase-2`. Rebase onto `main` after #12109
lands.

## Changes

### Reka primitives — parity gaps closed

| File | Change |
| --- | --- |
| `src/components/ui/dialog/dialog.variants.ts` | New `maximized`
variant. `false` keeps the centered/sized layout; `true` switches to
`inset-2 top-2 left-2 size-auto max-h-none max-w-none sm:max-w-none` for
full-screen mode |
| `src/components/ui/dialog/DialogContent.vue` | Accepts `maximized`
prop, forwards to variants |
| `src/components/ui/dialog/DialogMaximize.vue` **(new)** | Icon-only
button toggling `lucide--maximize-2` / `lucide--minimize-2`; emits
`toggle`; uses `g.maximizeDialog` / `g.restoreDialog` i18n |
| `src/stores/dialogStore.ts` | Adds `overlayClass?:
HTMLAttributes['class']` to `CustomDialogComponentProps` (Reka-only;
PrimeVue path uses `pt.mask`) |
| `src/components/dialog/GlobalDialog.vue` | (a) Forwards `overlayClass`
to `DialogOverlay`; (b) passes `:maximized` to `DialogContent`; (c)
renders `DialogMaximize` in the header when `maximizable`, wired to a
local `toggleMaximize`; (d) when `headless: true`, skips the inner
`flex-1 overflow-auto px-4 py-2` wrapper so layout dialogs control their
own chrome |

### Settings flip

| File | Change |
| --- | --- |
| `src/platform/settings/composables/useSettingsDialog.ts` | Adds
`dialogComponentProps: { renderer: 'reka', size: 'full', contentClass:
'\<...\>', overlayClass }`. `contentClass` is `w-[90vw] max-w-[960px]
sm:max-w-[960px] h-[80vh] max-h-none rounded-2xl overflow-hidden` —
matches the previous `BaseModalLayout size="sm"` (960px × 80vh).
`overlayClass: 'p-8'` only when `isCloud && teamWorkspacesEnabled`
(preserves the workspace breathing-room contract) |
| `src/components/dialog/GlobalDialog.vue` | Drops the now-dead
`getDialogPt` workspace special case and the orphan
`.settings-dialog-workspace` CSS. Removes unused imports (`merge`,
`computed`, `useFeatureFlags`, `isCloud`, `DialogPassThroughOptions`) |

### Tests

- `src/platform/settings/composables/useSettingsDialog.test.ts`
**(new)** — 5 tests: renderer flip + sizing, workspace `overlayClass`
toggle, panel forwarding, `showAbout()`

## Quality gates

- [x] `pnpm typecheck` — clean
- [x] `pnpm lint` — 0 errors (3 pre-existing warnings unrelated to this
PR)
- [x] `pnpm format` — applied
- [x] `pnpm test:unit` (touched + adjacent areas):
  - `useSettingsDialog.test.ts` — 5/5
  - `dialogService.renderer.test.ts` — 5/5
  - `GlobalDialog.test.ts` — 9/9
  - All `src/components/dialog/` — 73/73
  - All `src/platform/settings/` — 75/75
  - `CustomizationDialog.test.ts` — 4/4
- [ ] CI Playwright matrix
- [ ] Manual verification on a backend

## Screenshots

End-to-end verification of the Reka flip on a local dev server:

| | |
| --- | --- |
| Settings dialog rendered via Reka (non-modal, focus stays in dialog
body) |
![Settings](https://raw.githubusercontent.com/Comfy-Org/ComfyUI_frontend/c454af1888d5d8d88092475b498cff7d2adac1a1/temp/summaries/settings-dialog-reka.png)
|
| Keybinding panel inside the Reka Settings dialog |
![Keybinding](https://raw.githubusercontent.com/Comfy-Org/ComfyUI_frontend/c454af1888d5d8d88092475b498cff7d2adac1a1/temp/summaries/keybinding-panel.png)
|
| Nested PrimeVue **Modify keybinding** dialog stacked on top —
`document.activeElement` is the `<input autofocus>`, proving the
focus-trap fix | ![Modify
keybinding](https://raw.githubusercontent.com/Comfy-Org/ComfyUI_frontend/c454af1888d5d8d88092475b498cff7d2adac1a1/temp/summaries/nested-modify-keybinding.png)
|


## Public API impact

None. `useSettingsDialog().show()` keeps the same signature. Reka
primitives gain optional `maximized` prop and `overlayClass` field —
additive, non-breaking.

## Out of scope (later phases)

- Manager dialog — Phase 4 (FE-576) — will consume the new `maximizable`
affordance
- `ConfirmDialog` callers — Phase 5 (FE-577)
- Removing PrimeVue `Dialog`/`<style>` overrides in `GlobalDialog.vue` —
Phase 6 (FE-578)

## Review focus

1. **Sizing strategy** — `contentClass` overrides Reka's default content
sizing (matching the existing `BaseModalLayout size="sm"` of 960 ×
80vh). Worth a designer pass per FE-575's acceptance criteria.
2. **`overlayClass: 'p-8'` workspace mode** — Reka's `DialogContent` is
positioned with viewport coordinates, so overlay padding does not
constrain it the way the old PrimeVue `mask.p-8` did. Cosmetic gutter
only. If designer flags missing breathing room, follow-up by shrinking
`contentClass` in workspace mode.
3. **`headless: true` semantics for Reka** — now skips the inner padding
wrapper. Existing migrated dialogs (Phases 1–2) all set a header, so no
visible impact. The Reka-headless path is new with this PR.
4. **Maximize wiring** — `toggleMaximize` mutates
`item.dialogComponentProps.maximized` directly (Pinia deep-reactive
proxy). The store's `onMaximize` / `onUnmaximize` callbacks are still
wired for the PrimeVue path; not double-fired.

## Test plan

- [x] Unit: 102/102 across touched + adjacent areas
- [ ] CI: full Vitest + Playwright matrix
- [ ] Manual on a backend:
- Open Settings via gear icon / keyboard shortcut → renders through
Reka, search works, panel navigation works, ESC closes
- Open Settings → trigger a reset confirmation (stacked confirm) →
confirm renders above Settings, ESC closes only the confirm
- Cloud workspace mode: Settings opens with workspace panel;
`overlayClass` applied
- Cloud non-workspace mode: Settings opens without workspace panel; no
`overlayClass`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12182-feat-dialog-migrate-Settings-dialog-to-Reka-UI-Phase-3-35e6d73d36508144bb4af88f83c5ab20)
by [Unito](https://www.unito.io)

---------

Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
2026-05-27 22:08:30 +00:00
AustinMroz
b89940134f Better preview grid tiling (#12463)
The previous image preview tiling code was less than ideal. It had fixed
breakpoints based on the number of images. Outputs with many images
would become comically long.

This PR instead tiles images to fill the available space.
| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/e793ce65-8efc-44ca-b049-98f066a65b7d"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/ca891ce2-335f-42ce-aeec-a99579f669c8"
/>|
2026-05-27 20:26:44 +00:00
Christian Byrne
7ac1cbbd53 test: add E2E coverage for NE, SW, NW corner node resizing (#11408)
*PR Created by the Glary-Bot Agent*

---

## Summary

- Adds parameterized Playwright E2E tests covering all non-SE resize
corners (NE, SW, NW), closing the coverage gap in the `useNodeResize.ts`
switch statement
- Adds `resizeFromCorner()` and `getResizeHandle()` to `VueNodeFixture`
for reuse across tests
- Test cases are derived from the production `RESIZE_HANDLES` config so
they stay in sync with the actual handle definitions

## Test Groups (8 new tests)

| Group | Tests | Coverage |
|-------|-------|----------|
| Corner resize directions | NE, SW, NW — size increases and correct
edges shift | Lines 110-124, 184 |
| Opposite edge anchoring | NE, SW, NW — opposite corner stays fixed |
Position compensation end-to-end |
| Minimum size enforcement | SW width clamp (≥ MIN_NODE_WIDTH), NE
height clamp | Lines 162-176 |

## Design Decisions

**Locator-based handle discovery**: `resizeFromCorner()` finds handles
via `getByRole('button', { name: ariaLabel })` instead of coordinate
offsets. The resize handles have `opacity-0 pointer-events-auto`,
meaning they're always interactive even when visually transparent —
Playwright considers elements with `opacity: 0` as visible (it only
gates on `visibility: hidden` / `display: none` / zero-size bounding
box). If this approach turns out to be flaky in CI due to handle
discoverability, we can fall back to coordinate-based targeting
(computing offsets from the node's bounding box corners), which is what
the original SE-corner test uses.

**Parameterization from production config**: Tests import
`RESIZE_HANDLES` from `resizeHandleConfig.ts` and derive test case data
(drag direction, which axes move) from the corner name. An upfront guard
throws if any expected corner is missing from the config, preventing
silent coverage loss.

**Aria-label coupling**: `RESIZE_HANDLE_LABELS` in `VueNodeFixture`
hardcodes the English aria-label strings. This is intentional — tests
run in English locale, and aria-labels are the accessibility interface
contract. If a more stable hook is needed (e.g., `data-testid` per
handle), that can be added to `LGraphNode.vue` in a follow-up.

**Frame settlement**: `resizeFromCorner()` calls `nextFrame()` after the
mouse-up to ensure layout settles before assertions run, per
`FLAKE_PREVENTION_RULES.md`.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11408-test-add-E2E-coverage-for-NE-SW-NW-corner-node-resizing-3476d73d3650818d8a5ce5d6d535b38c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: jaeone94 <89377375+jaeone94@users.noreply.github.com>
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
2026-05-27 17:26:59 +00:00
Alexander Brown
0157b47024 feat(subgraph): Subgraph Link Only Promotion (ADR 0009) + migration/store hygiene (#12197)
## Summary

Introduces **Subgraph Link Only Promotion** (ADR 0009) — a new model for
surfacing inner subgraph widgets on the parent SubgraphNode by
*promoting through links* rather than by duplicating widget state on the
host. Ships with the hygiene/refactor pass on the migration, store, and
event layers that the new model depends on.

## What changes

### Subgraph Link Only Promotion (ADR 0009)

Promoted widgets are defined by the link from a SubgraphNode input to
the interior node, not by a duplicated widget instance on the host.
Consequences:

- A SubgraphNode renders inner widgets purely as a **projection** of the
interior widgets and links — no host-side state to drift.
- **Per-host independence**: multiple instances of the same SubgraphNode
render and edit their own values without cross-talk.
- **Reversible promote/demote**: structural link operation, so demote
preserves host slots and external connections (#12278).

### Supporting refactors

- **Migration** — Planner/classifier/repair/quarantine helpers collapsed
into a single `proxyWidgetMigration` entry point with black-box
round-trip coverage. Honors the source-node-id disambiguator on
`proxyWidgets`, so deduplicated names (e.g. `text`, `text_1`) resolve to
the right interior widget.
- **Widget identity** — `appMode` unified on `WidgetEntityId`; promoted
widget state is keyed by entityId across the store, DOM, and migration
paths.
- **SubgraphNode** — 3-key promoted-view cache replaced with a single
version counter + explicit `invalidatePromotedViews()` at mutation
sites; `id === -1` sentinel removed.
- **Events** — `LGraph.trigger()` now dispatches node trigger payloads
through `this.events`, replacing a leaky `onTrigger` monkey-patch.
`SubgraphEditor` reactivity is driven from subgraph events instead of
imperative refresh.
- **Stores** — `appModeStore` migration helpers collapsed into
`upgradeAndValidateInput`; `nodeOutputStore.*ByExecutionId` derived from
the locator index; `previewExposureStore` cleanup and cycle-detection
double-warn fix.
- **Misc** — `Outcome` types consolidated; mutable accumulators replaced
with `flatMap`; new ESLint rule forbids litegraph imports under
`src/world/`.

### Tests

- Browser tests for promoted widgets retagged `@vue-nodes` and rewritten
to assert against the rendered Vue node DOM (via `getNodeLocator` /
`getByRole('textbox')` / `enterSubgraph`) instead of `page.evaluate`
graph introspection.
- Per-host widget independence asserted via DOM.
- Migration coverage moved to black-box round-trip tests.
- Added coverage for duplicate-named promoted widget identity (ADR 0009)
and the per-parent demote branch in `WidgetActions`.

## Review focus

- ADR 0009 conformance of the link-only promotion model.
- Disambiguator resolution path in `proxyWidgetMigration`.
- Single-version-counter promoted-view cache and its
`invalidatePromotedViews()` call sites.
- `LGraph.trigger()` event dispatch and the `AppModeWidgetList.vue`
migration off `onTrigger` (FE-667 tracks the remaining
`useGraphNodeManager` conversion).

## Breaking changes

None for users. Internal subgraph promotion APIs changed — see ADR 0009.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12197-feat-subgraph-link-only-widget-promotion-migration-store-hygiene-35e6d73d365081fd882cf3a69bc09956)
by [Unito](https://www.unito.io)

---------

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: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: AustinMroz <austin@comfy.org>
2026-05-27 00:29:11 -07:00
AustinMroz
876ed502c9 Mock sign-in request in test (#12482)
Mock the sign in request in e2e tests to ensure tests success isn't tied
to external variables.
2026-05-27 05:05:26 +00:00
AustinMroz
c638ad194b Fix restoring values to dynamic combos (#12211)
`DynamicCombo`s redefined `widget.value` without going through the
store. This would result in desync of state. Most noticeably, swapping
to and from a workflow would break vue reactivity and cause the default
option to display visually

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12211-Fix-restoring-values-to-dynamic-combos-35f6d73d3650814ba12ccda42615239a)
by [Unito](https://www.unito.io)
2026-05-26 21:19:59 +00:00
AustinMroz
bbaaa82125 Fix missing value control on 'Primitive Int' (#12431)
#8505 added support for specifying default values for
`control_after_generate`. Unbeknown to me, this exact same format of
assigning `control_after_generate` to a string in the schema already
served a function of renaming the control widget. As a result, control
widgets with a default value set would use a different internal name,
but due to other overlapping systems, would either have a label of
`control_after_generate` or `control_before_generate`.

The fix here, is incredibly simple and low scope. Instead of trying to
filter control widgets by name, the dedicated `IS_CONTROL_WIDGET` symbol
is used.

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/5917e093-124a-4923-80ff-321fc0a94ef3"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/c6d95b5a-2764-4e71-a09f-dcae5ddcfdbb"
/>|
2026-05-26 01:49:44 +00:00
Alexander Brown
8d1a170136 feat: remove ability to create Group Nodes (#12347)
*PR Created by the Glary-Bot Agent*

---

Group Nodes are a legacy feature superseded by Subgraphs. This PR
removes every UI entry point for *creating* a new Group Node while
keeping the loading, ungrouping, and management code intact so existing
workflows that contain Group Nodes continue to load and can still be
unpacked or managed.

## Removed creation entry points

- `Comfy.GroupNode.ConvertSelectedNodesToGroupNode` command
- `Alt+G` keybinding
- "Convert to Group Node (Deprecated)" canvas and node right-click menu
items (`groupNode.ts` `getCanvasMenuItems` / `getNodeMenuItems`)
- "Convert to Group Node" entry in the Vue selection menu
(`useSelectionMenuOptions.ts`)
- Associated `MENU_ORDER` entry in `contextMenuConverter.ts`
- `convertSelectedNodesToGroupNode` / `convertDisabled` helpers in
`groupNode.ts`
- `BadgeVariant.DEPRECATED` enum member (no remaining consumers;
knip-clean)
- Matching `en` locale strings in `main.json` (`contextMenu.Convert to
Group Node`, `commands.Convert selected nodes to group node`) and
`commands.json` (`Comfy_GroupNode_ConvertSelectedNodesToGroupNode`)
- Browser-test helpers `convertToGroupNode` /
`convertAllNodesToGroupNode` and the three tests that exercised the
creation flow

## Preserved (intentionally)

- `GroupNodeHandler`, `GroupNodeConfig`, `GroupNodeBuilder`,
`ManageGroupDialog`
- `beforeConfigureGraph` / `nodeCreated` hooks that load and initialize
Group Nodes from saved workflows
- "Manage Group Nodes" canvas menu item, the
`Comfy.GroupNode.ManageGroupNodes` command, and the per-node "Manage
Group Node" / "Convert to nodes" options on existing group node
instances
- "Ungroup selected group nodes" command + `Alt+Shift+G` keybinding so
users can disassemble existing group nodes in legacy workflows
- Reduced `browser_tests/tests/groupNode.spec.ts` covering surviving
behaviors: workflow loading (legacy `/` separator, hidden-input config,
v1.3.3 fixture), copy/paste of already-loaded group nodes across
workflows, and opening the Manage Group Node dialog

## Verification

- `pnpm typecheck` clean
- `pnpm typecheck:browser` clean
- `pnpm format` clean
- `pnpm knip` clean (no new findings; pre-existing flac.ts tag warning
unchanged)
- `pnpm test:unit` — 796 files, 10,789 tests pass (8 pre-existing
skipped); includes a regression test in
`useSelectionMenuOptions.test.ts` asserting the Vue selection menu no
longer offers a Convert to Group Node option
- Pre-commit hooks (oxfmt, oxlint, eslint, typecheck, typecheck:browser)
passed
- Manual verification against a live dev server: programmatically
inspecting the GroupNode extension showed `getCanvasMenuItems` returns
only `[Manage Group Nodes]`, `getNodeMenuItems` returns `[]`, and the
`ConvertSelectedNodesToGroupNode` command + Alt+G keybinding are absent
from the registries. Visually captured the node right-click menu
(attached screenshot) — "Convert to Subgraph" remains, no "Convert to
Group Node" entry
- Browser E2E suite not executed locally (sandbox has no GPU and
Playwright requires a full backend; the reduced spec will run in CI)
- Non-English locales not modified — per `src/locales/CONTRIBUTING.md`
they are regenerated by CI

## Notes for reviewers

- This is a surgical removal of creation only; loading any older
workflow that already contains group nodes will continue to work.
- If you'd like to also remove the management UI (`Manage Group Nodes`
command/menu/dialog) or the ungroup command in a follow-up, happy to
open a separate PR.

## Screenshots

![Node right-click context menu after the change: lists Convert to
Subgraph and standard node options, with no Convert to Group Node
entry](https://pub-1fd11710d4c8405b948c9edc4287a3f2.r2.dev/sessions/51318b42bce8def1eb9f98252fbfc2bf097a0c68f001ae6f7a15f1344abf3b91/pr-images/1779234588218-3d0e305a-0167-44c4-8ae3-ea6e785ca418.png)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12347-feat-remove-ability-to-create-Group-Nodes-3656d73d365081d488bfd98ffd7545c0)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Amp <amp@ampcode.com>
2026-05-26 00:47:30 +00:00
AustinMroz
fb5b4a62ba Fix mask editor sometimes showing wrong image (#12413)
Mask editor checks `node.images` to determine the image which is edited.
If the user generates an output image in litegraph mode, swaps to vue
mode, then generates a new image, the mask editor will incorrectly
display the image last shown in litegraph mode.

This is resolved by having `syncLegacyNodeImgs` also synchronize node
outputs to `node.images`.
2026-05-25 18:37:10 +00:00
Dante
d405002127 fix(widgets): collapse duplicate COLOR widget rendering on Color to RGB Int (FE-842) (#12447)
## Summary

Fix the duplicate \`<WidgetColorPicker>\` rendering on the \`Color to
RGB Int\` node (and any other COLOR-using V3 node that the runtime
double-registers a widget for).
<img width="480" alt="after-fix-dedupe-proof"
src="https://github.com/user-attachments/assets/5c801806-ed5d-493f-92b6-e0b99dd8e408"
/>

## Changes

- **What**:
- \`useProcessedWidgets.getWidgetIdentity\`: fall back to the host
\`nodeId\` parameter for the dedupe identity root when neither
\`storeNodeId/widget.nodeId\` nor \`sourceExecutionId\` is set. Normal
root-graph widgets now dedupe identically to promoted/execution-scoped
widgets, so any duplicate same-name+same-type widget collapses to one
render. \`sourceExecutionId\` precedence is preserved.
- \`useColorWidget\`: read top-level \`default\` from the V2 spec (falls
back to nested \`options.default\` for hand-authored V2 specs), and
short-circuit if a same-name color widget already exists on
\`node.widgets\` so a second \`addWidget('color', …)\` call from
upstream hooks (or a \`configure\` round-trip) no longer duplicates the
row.
- **Tests**:
- New \`useColorWidget.test.ts\` covers top-level default,
nested-options fallback, no-default fallback, and the idempotency guard.
- \`useProcessedWidgets.test.ts\` gets a regression case for two
identical color widgets on the same node collapsing to one render, plus
an updated \`getWidgetIdentity\` case for the host-nodeId fallback.

## Review Focus

- \`getWidgetIdentity\` precedence change. The fallback only fires when
none of \`storeNodeId\`, \`widget.nodeId\`, or \`sourceExecutionId\` are
present, so promoted/exec-scoped widgets (incl. the \"unresolved
same-name promoted entries distinct by source execution identity\"
\`NodeWidgets\` test) are unaffected.
- \`useColorWidget\` idempotency guard is defensive — the root cause of
the second \`addWidget\` call (cloud-only hook or persisted
\`info.widgets\` configure round-trip) is not in this diff; that's
tracked separately.

Fixes
[FE-842](https://linear.app/comfyorg/issue/FE-842/color-to-rgb-int-node-shows-duplicate-color-widgets)
2026-05-25 11:33:58 +00:00
pythongosssss
abd233d10d feat: default search to essentials when graph is empty (#12377)
## Summary

Currently, when opening node search on an empty graph, the default view
shows "Most Relevant" nodes, which includes nodes like CLIP and VAE. For
users building from scratch, these nodes are not necessarily the most
helpful starting point.

## Changes

- **What**: 
- Update default mode to Essentials when graph is empty

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12377-feat-default-search-to-essentials-when-graph-is-empty-3666d73d3650816d9d5ae3ed602a30ec)
by [Unito](https://www.unito.io)
2026-05-25 09:35:52 +00:00
jaeone94
91d2df45a1 Fix V2 draft lifecycle persistence (#12269)
## Summary

This PR fixes the remaining FE-367 workflow persistence gap by moving
the workflow draft lifecycle callers from the legacy V1 draft store to
`workflowDraftStoreV2`, following the core design from #10367 while
omitting unrelated changes.

It keeps the change focused on saved workflow tab restore and V2 draft
lifecycle behavior:

- save active workflow drafts through V2 before loading a new graph
- load, save, save-as, close, rename, and delete workflows against V2
draft storage
- prefer a fresh V2 draft when loading a saved workflow, and discard
stale drafts when the remote workflow is newer
- restore saved open tabs from persisted tab state instead of letting
stale active-path state win
- preserve V2 draft payload timestamps when moving or refreshing draft
recency
- remove the now-unused V1 draft store/cache implementation instead of
suppressing knip; the raw V1 on-disk migration path remains for existing
users

Co-authored-by: xmarre <xmarre@users.noreply.github.com>

## Test coverage

Added unit coverage for V2 draft load, stale draft discard, rename/close
lifecycle cleanup, tab restore ordering, metadata-load waiting/fallback,
draft recency updates, quota eviction retry, and persistence-disabled
reset behavior.

Updated the workflow persistence composable tests to use a real
`vue-i18n` plugin host instead of mocking `vue-i18n`.

Added an E2E regression test that saves two workflows, edits an inactive
saved tab draft, makes the active-path pointer stale, reloads, and
verifies the saved tab order, active tab, and inactive draft
restoration.

## Validation

- `pnpm format`
- `pnpm lint`
- `pnpm typecheck`
- `pnpm test:unit`
- pre-push `pnpm knip` (passes with the existing flac tag hint)

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12269-Fix-V2-draft-lifecycle-persistence-3606d73d365081b4a84feb1696ed88bb)
by [Unito](https://www.unito.io)

---------

Co-authored-by: xmarre <xmarre@users.noreply.github.com>
2026-05-22 15:24:31 +00:00
AustinMroz
ee286291d4 Fix reactivity on matchType output slots (#12397)
Relevant: #9935. A PR claimed to solve the same issue (and was approved
by me), but the issue persists. Even when checking out that exact
commit, the issue does not appear affected.

This PR is somewhat heavier. It converts the outputs into
shallowReactive. Since there is no individual moment of registration for
outputs, this conversion happens on type change and leverages that
calling `shallowReactive` on a shallow reactive is low cost and
reflexive. It also adds a test to ensure that regression can not happen
in the future.

| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/3e4f4a0a-906f-4539-95b6-b2e80de7ceff"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/1a29ac66-ed5e-4874-82dc-ce9f6135dea5"
/>|
2026-05-22 02:56:02 +00:00
pythongosssss
b3ba6c9344 fix: select node after adding from library (#12404)
## Summary

When adding a node from the library sidebar, the node was not correctly
selected upon placing it. This was due to the canvas capturing the node
under the cursor on mouse down, however the node had not yet been
comitted to the graph at that point, and so selection was then cleared
on mouse up.

## Changes

- **What**: 
- add `blockCommitPointerDown` so if the cursor is over the canvas stop
propagation to prevent LiteGraph adding the mouse handler to clear the
selection

## Review Focus

Alternative approaches considered were blocking the event in endDrag
however this then required manual cleanup of LiteGraph handlers or
overriding the `pointer.onClick` function to force selection of our
node, both felt worse than this approach.

## Screenshots (if applicable)


https://github.com/user-attachments/assets/a2eb154e-5178-4a1e-b5c7-884efd7a10c6
2026-05-21 19:52:56 +00:00
AustinMroz
a50b3d16da Persist splash until graph load completes (#12387)
When an app mode workflow is opened on fresh page load, either from a
template url, or a persisted in browser cache, the UI would briefly
display the graph view prior to swapping to app mode. This is fixed by
continuing to display the splash screen until workflow state has loaded.

Share by url brings unique difficulties. The function call does not
return until a user has responded to a dialogue. If the splash screen
were blocked by this, the user would never be able to see the dialogue.
Consequentially, this change is not applied to shared workflow urls and
the (very unlikely) url including both a template url and a share url
will now prioritize the template url.

A best effort e2e test is included, but is a little clunky.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12387-Persist-splash-until-graph-load-completes-3666d73d3650813495e4ccad6052c1e4)
by [Unito](https://www.unito.io)
2026-05-21 19:44:12 +00:00
AustinMroz
3ce0c07af2 Use utility function to add node with V2 search (#12382)
Default search box settings are a little inconvenient to work with. This
PR introduces a new `addNode` utility function to the V2 search fixture
that handles all the steps of opening search and adding a node that a
user would perform.

It then migrates several PRs I have recently written to use this new
fixture.

See also #12029

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12382-Use-utility-function-to-add-node-with-V2-search-3666d73d3650817c8c73c9104b1113bf)
by [Unito](https://www.unito.io)
2026-05-21 19:25:26 +00:00
jaeone94
f1f65cff61 feat: select top asset widget FormDropdown result on Enter (#12209)
## Summary

Allow asset/media FormDropdown searches to select the top filtered
result when the user presses Enter. This covers image, video, audio,
mesh, model-like asset selects, and other `WidgetSelectDropdown`-backed
media widgets.

## Implementation Scope

This PR implements a **top-result Enter shortcut** for the custom
asset/media dropdown path only:

- In scope: `WidgetSelectDropdown` -> `FormDropdown` asset/media
widgets.
- In scope: while the dropdown is open, single-select, and the search
text is non-empty, the first current search result becomes the Enter
candidate.
- In scope: pressing Enter in the search input selects that
candidate/top result through the existing selection path.
- In scope: candidate feedback for this shortcut, including visual
candidate styling and a polite screen-reader announcement for the
current top result.
- In scope: stale async search protection, empty-query/no-result no-op
behavior, multi-select guard behavior, and focus return to the trigger
after Enter selection closes the menu.
- Out of scope: plain combo widgets (`WidgetSelectDefault` /
`SelectPlus`). That path is PrimeVue-based and should be handled
separately from this focused asset-widget PR.
- Out of scope: full combobox/listbox keyboard navigation, including
Tab-to-list focus, ArrowUp/ArrowDown candidate movement, Home/End
behavior, scroll-to-active-item behavior, and a full ARIA
combobox/listbox refactor.

Follow-up arrow-key navigation should validate the interaction model
separately. This PR keeps the candidate state narrow and localized so
that future work can either extend it into movable active-item state or
replace it as part of a fuller combobox/listbox implementation.

## Changes

- **What**: Added an explicit Enter event from `FormSearchInput`, routed
it through the FormDropdown menu actions, and selected the current top
search result in `FormDropdown`.
- **What**: Kept the existing `computedAsync` + debounced filtering path
for normal typing, while Enter performs a one-off search against the
latest input before selecting. Stale async Enter results are ignored if
the query or item source changes before resolution.
- **What**: Prevented closed FormDropdown state from treating the full
unfiltered list as current search results, limited Enter-to-select to
single-select dropdowns, and made empty search Enter a no-op.
- **What**: Returned focus to the dropdown trigger after single-select
selection closes the menu.
- **What**: Added candidate styling for the first current FormDropdown
result while a search query is active so the Enter target is visible to
users.
- **What**: Added a polite screen-reader announcement for the current
top result candidate.
- **What**: Fixed the FormDropdownMenuActions `baseModelSelected` model
default to use a `Set` factory instead of a shared instance.
- **What**: Added unit coverage for the search Enter event, FormDropdown
selection behavior, focus return, debounce/Enter behavior, stale async
Enter protection, empty-query no-op behavior, closed-state stale result
protection, multi-select guard behavior, and candidate announcement
behavior. Added App Mode E2E coverage for asset FormDropdown Enter
selection.
- **What**: Extracted reusable app-mode dropdown fixture helpers and
updated the existing FormDropdown clipping test to use the shared
helper.

## Review Focus

Please focus review on the asset/media FormDropdown path, especially
`getTopSearchResult()`, the single-select/empty-query guards, stale
async search protection, trigger focus return after selection, and
candidate feedback in grid/list layouts.

The plain combo path and full arrow-key navigation are intentionally
left for separate follow-up work.

## Screenshots (if applicable)



https://github.com/user-attachments/assets/3eb3456d-93a3-4959-91a3-188f8116ccc9



Validation performed:

- Latest final-commit validation:
- `pnpm test:unit
src/renderer/extensions/vueNodes/widgets/components/form/FormSearchInput.test.ts
src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdown.test.ts
src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenuActions.test.ts
src/renderer/extensions/vueNodes/widgets/components/form/dropdown/FormDropdownMenu.test.ts`
- Commit hook: `pnpm exec stylelint ...`, `pnpm exec oxfmt --write ...`,
`pnpm exec oxlint --type-aware --fix ...`, `pnpm exec eslint --cache
--fix ...`, `pnpm typecheck`
  - Push hook: `pnpm knip --cache`
  - `git diff --check`
- Earlier branch validation for this flow:
  - `pnpm install`
  - `pnpm typecheck:browser`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173
PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm test:browser --
--project=chromium browser_tests/tests/appMode.spec.ts -g "Drag and
Drop|FormDropdown search Enter selects the top filtered item"
--reporter=list`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173
PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm test:browser --
--project=chromium browser_tests/tests/appMode.spec.ts -g "FormDropdown
search Enter selects the top filtered item" --reporter=list`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:5173
PLAYWRIGHT_SETUP_API_URL=http://localhost:8188 pnpm test:browser --
--project=chromium browser_tests/tests/appModeDropdownClipping.spec.ts
-g "FormDropdown popup is not clipped" --reporter=list`
2026-05-21 02:26:26 +00:00
Benjamin Lu
d472ca783b test: cover FE-130 assets sidebar route mocks (#12332)
## Summary

Adds a focused FE-130 assets sidebar browser-test slice without
extending the stateful asset helper path.

## Changes

- **What**: Extends `jobsRouteFixture` with job-detail and
history-delete helpers for generated asset flows.
- **What**: Adds an assets sidebar tab spec covering generated/imported
rendering, preview opening, generated selection footer actions, and
explicit delete refresh behavior.

## Review Focus

The delete test keeps backend state explicit: it captures the
`/api/history` request, then replaces the `/api/jobs` history mock with
the post-delete response. The imported-file and `/api/view` mocks stay
local to this focused spec instead of growing `AssetHelper` or adding a
sidebar-specific fixture.
2026-05-20 17:01:51 -07:00
AustinMroz
2717d59451 Fix reactivity of vue subgraph price badges (#12029)
When a subgraph contains partner nodes with price badges, those badges
are also displayed on the subgraphNode. The reactivity here was spotty:
The price badges would fail to display unless the user had navigated
into the subgraph on the current page load. Fixing this is performed in
2 steps:
- Firing a `node:property:changed` event when the badges contained in a
subgraph are updated
- Extending the reactivity updates so that badges update in vue mode
despite using the litegraph badge getter.

This PR also includes a minor styling tweak to fix text alignment on
price badges
| Before | After |
| ------ | ----- |
| <img width="360" alt="before"
src="https://github.com/user-attachments/assets/56a95cbe-12c9-43b0-8664-34e52b6415ac"
/> | <img width="360" alt="after"
src="https://github.com/user-attachments/assets/bf4a0d81-21e4-4afc-946e-eba5967f1715"
/>|

Resolves FE-346

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12029-Fix-reactivity-of-vue-subgraph-price-badges-3586d73d3650813cb12fe265090940e4)
by [Unito](https://www.unito.io)
2026-05-20 11:22:42 -07:00
AustinMroz
d63b0f05bf Subgraph io fixes (#12281)
Fixes 3 different bugs when making links to and from subgraph IO from
vue nodes
- When dragging a link from a node to a subgraph IO, there is no
feedback if a slot is not a valid connection target or if a slot is
actively hovered
- When a link is made from a subgraph IO to a node, the reactivity is
not triggered on the node to indicate a change of link state.
- When dragging a link from a subgraph IO to a node, the link would not
snap to the valid connection targets on nodes
- The fix for this one is not as thorough as I would like. It only
allows connections to the slot, not connections to the hovered widget.
We have two deeply disconnected linking systems and properly reconciling
them would be a multi-week project.

Resolves FE-561

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12281-Subgraph-io-fixes-3606d73d365081089f7ef19331c6d70a)
by [Unito](https://www.unito.io)
2026-05-20 10:26:47 -07:00
AustinMroz
a95e53bf6d On subgraph conversion, always unpack group nodes (#12356)
This is a targeted small scope change to improve the availability for
converting group nodes into a subgraph.

The prior implementation would only apply on the litegraph context menu
option for converting a node to a subgraph. It failed to apply on any of
the other more common methods. The code for unpacking group nodes has
been moved directly into the setup for converting a group of nodes into
a subgraph and drastically simplified.

Of note, several other long lived bugs were found while working on this
fix, but they are out of scope for this targeted PR.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12356-On-subgraph-conversion-always-unpack-group-nodes-3666d73d365081d09774c00a851b8198)
by [Unito](https://www.unito.io)
2026-05-20 05:23:46 +00:00
jaeone94
98a8a614e8 fix: avoid false missing media errors after importing shared workflow assets (#12333)
## Summary

Import published media assets for shared workflows before loading the
graph so the first missing-media scan sees the user's newly imported
references instead of surfacing a false missing asset error. cc FE-773

## Changes

- **What**: Moves the shared workflow import step ahead of
`loadGraphData` for the copy-and-open flow, while still allowing the
workflow to open with a warning path if asset import fails.
- **What**: Clears the shared workflow URL intent consistently on
failure paths, including graph load failure after an import attempt, so
reloads do not repeatedly replay the same shared workflow side effects.
- **What**: Invalidates the input asset cache after published asset
import so graph loading and missing-media resolution can observe the
refreshed media state.
- **What**: Adds a global loading spinner while shared workflow asset
import and graph load are in progress, with `role="status"`,
`aria-live`, reduced-motion-safe animation, and body teleporting so it
stays visible above blocking UI.
- **What**: Adds stable TestIds for the shared workflow dialog and
updates existing shared workflow E2E selectors away from copy-dependent
role text.
- **What**: Adds a cloud E2E regression fixture and spec covering the
critical flow: shared URL opens the dialog, the user confirms asset
import, published media is imported before the public-inclusive input
asset scan, the workflow loads, the share query is removed, and missing
media UI is not surfaced.
- **Breaking**: None.
- **Dependencies**: None.

## Root Cause

Shared workflow graph loading triggered the missing-media pipeline
before the user-selected published media import had completed. Because
`include_public=true` does not include published assets, the pre-import
scan could classify shared media as missing even when the user was about
to import those assets into their own library.

## Review Focus

- The ordering in `useSharedWorkflowUrlLoader`: import published assets
first, then load the graph, while keeping import failure non-fatal for
workflow opening.
- The failure cleanup behavior: the shared URL/preserved query intent is
now cleared for graph load failures too, avoiding repeated
reload-triggered imports.
- The spinner behavior in `App.vue`: it uses the existing
`workspaceStore.spinner` boolean and intentionally keeps broader
ref-counted spinner ownership as follow-up work.
- The E2E sentinel in `sharedWorkflowMissingMedia.spec.ts`: it asserts
no public-inclusive input asset scan occurs before `/api/assets/import`,
then waits for a settling window to ensure the missing-media overlay
does not appear.

## Validation

- `pnpm format`
- `pnpm lint` (passed with existing unrelated warnings only)
- `pnpm typecheck`
- `pnpm test:unit`
- Commit hook: lint-staged formatting/linting, `pnpm typecheck`, `pnpm
typecheck:browser`
- Push hook: `pnpm knip --cache` (passed with existing tag hint only)

## Follow-Up

- Consider a ref-counted or scoped global spinner API so long-running
flows do not directly toggle `workspaceStore.spinner`.
- Consider separating shared workflow load status into orthogonal result
fields instead of encoding partial success in a single string union.
- Consider moving published asset import/cache invalidation behind an
asset-service-owned API boundary.
- Backend follow-up remains needed for `include_public=true` not
including published assets; this PR only removes the frontend false
positive when the user explicitly imports the shared media.

## Screenshots

Before 


https://github.com/user-attachments/assets/dc790046-237c-4dd8-b773-2507f9a66650

After 


https://github.com/user-attachments/assets/6517cd38-2c3d-4bfe-a990-35892b7e50ae



https://github.com/user-attachments/assets/d89dc3d3-75d9-4251-998b-0c354414e25b




┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12333-fix-avoid-false-missing-media-errors-after-importing-shared-workflow-assets-3656d73d365081b38634dcb7625cfc32)
by [Unito](https://www.unito.io)
2026-05-20 02:59:44 +00:00
Benjamin Lu
64c75bfce5 test: avoid job history double setup (#12324)
## Summary

Avoid a second `comfyPage.setup()` in the job history browser tests by
registering the initial jobs route mocks before the normal page boot.

## Changes

- **What**: Adds an `initialJobsScenario` Playwright option with an auto
fixture for the job-history spec, moves QPOV2 setup into
`initialSettings`, and keeps the sidebar helper focused on UI
navigation.
- **Dependencies**: None

## Review Focus

Confirm the auto fixture ordering matches the intended browser-test
setup: initial `/api/jobs` mocks should be installed before the
`comfyPage` fixture performs its normal setup.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12324-test-avoid-job-history-double-setup-3656d73d365081778e24c11d3b65cbef)
by [Unito](https://www.unito.io)
2026-05-19 20:09:01 +00:00
Dante
3b37488eee fix: keep node context menu overflow visible when content fits (#12035)
## Summary

Stops the Shape submenu (and any other PrimeVue nested submenu) from
being clipped behind the node context menu when the menu fits in the
viewport.

## Changes

- **What**: `constrainMenuHeight` in `NodeContextMenu.vue` now applies
`max-height` + `overflow-y: auto` to the root `<ul>` only when
`scrollHeight > availableHeight`. The common case keeps `overflow:
visible`.
- Added `browser_tests/tests/nodeContextMenuShapeSubmenu.spec.ts`
regression spec.

## Review Focus

Root cause: setting only `overflow-y: auto` on a `<ul>` coerces
`overflow-x` to a non-visible value per CSS spec (`If one of
overflow-x/overflow-y is visible and the other isn't, the visible value
is computed as auto`). PrimeVue `ContextMenuSub` renders submenus
in-tree as a nested `<ul>` with `position: absolute; left: 100%`, so the
implicit horizontal clip hides them entirely.

The pre-existing overflow scenario (#10824 / #10854) is unchanged — when
the menu actually overflows, the clamp still applies and
`nodeContextMenuOverflow.spec.ts` continues to verify scroll. Submenu
clipping in that overflow case is a known limitation, not introduced by
this PR.

Fixes FE-570


## screenshot

### AS IS
<img width="788" height="505" alt="Screenshot 2026-05-07 at 12 43 26 PM"
src="https://github.com/user-attachments/assets/36d34070-0c57-4385-a130-0394f22f282e"
/>


### TO BE

<img width="779" height="627" alt="Screenshot 2026-05-07 at 12 42 44 PM"
src="https://github.com/user-attachments/assets/00956729-763b-4787-822f-209e8ea42331"
/>


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12035-fix-keep-node-context-menu-overflow-visible-when-content-fits-3586d73d365081ad9aaec82f220d401c)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
2026-05-19 10:56:41 +00:00
jaeone94
fc7e6a0935 fix(terminal): resync logs console on backend reconnect (#12270)
## Summary

When the built-in logs terminal stayed open during a backend restart,
the buffer froze on pre-restart entries and live log streaming silently
stopped — only closing and reopening the panel resynced. Listen for the
api `reconnected` event and rebuild the terminal contents the same way a
fresh open would.

## Changes

- **What**:
- Extract `useLogsTerminal` composable. The SFC is now a thin shell
holding `terminal: shallowRef<Terminal>` and forwarding to the
composable, so `onMounted`/`onScopeDispose` no longer rely on the
child's emit callback timing.
- Subscribe to `api`'s `reconnected` event via `useEventListener`,
registered synchronously before any awaits. On reconnect:
`terminal.reset()` → refetch raw logs → `scrollToBottom()` →
`subscribeLogs(true)` (the backend loses the per-client subscription on
restart, so re-subscribe is required for live streaming to resume).
- Wrap in-flight resync/mount fetches in AbortControllers. Overlapping
reconnects abort the prior resync, and unmount mid-fetch suppresses
writes to the disposed xterm.
- Hide BaseTerminal whenever `errorMessage` is set so the error layout
doesn't expose an empty xterm container behind the message;
`loading=false` after both load failure and resync success so a later
successful reconnect can clear a stuck spinner.
- Migrate the load/resync error strings to vue-i18n
(`logsTerminal.loadError`, `logsTerminal.resyncError`).

## Review Focus

- **Re-subscribe is the non-obvious half of the fix** — without it, even
after the WebSocket reconnects the backend never resumes streaming logs
to this client because its subscription state was wiped on restart. The
visible "stale buffer" is only one symptom; the silent "no new logs"
symptom needed the explicit `subscribeLogs(true)` re-call in resync.
- `terminal.reset()` lives after a successful raw-logs fetch (not
before) so a failed resync leaves the prior buffer visible instead of
blanking it; resync errors surface via the same inline error message the
mount path uses.
- 8 unit tests around the composable: mount + subscribe, resync ordering
(reset → write → scroll → subscribe via `invocationCallOrder`),
in-flight resync abort on double reconnect, resync error surfacing,
mount-failure-then-recovery, unmount-mid-fetch terminal-write
suppression, listener cleanup on unmount.
- 2 E2E tests using `ws.close()` on the proxied WebSocket as the
reconnect trigger and `subscribeLogs` HTTP fetch count as the sync point
(same pattern as `wsReconnectStaleJob.spec.ts`). Red-checked: disabling
the `reconnected` listener fails exactly the two new tests, all 8
pre-existing tests stay green.

Fixes FE-712

## Screenshots

Before - (After rebooting, the console window does not update from its
state before the reboot must remount the console window for it to
resync.)


https://github.com/user-attachments/assets/b1e49c2c-89a4-4a4a-82b4-064412acee12

After - (The console window syncs automatically after a reboot.)


https://github.com/user-attachments/assets/54b582c5-ad42-41c0-9886-18f4495859da




┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12270-fix-terminal-resync-logs-console-on-backend-reconnect-3606d73d3650812fb13fd1934c632344)
by [Unito](https://www.unito.io)
2026-05-19 01:55:11 +00:00
Benjamin Lu
a97f46b497 test: cover job history sidebar with typed route mocks (#12272)
## Summary

Add the first product-area browser coverage on top of the merged typed
route mock foundation: the docked job history sidebar.

## Changes

- **What**: Adds `browser_tests/tests/sidebar/jobHistory.spec.ts` using
`jobsRouteFixture`.
- **What**: Covers direct sidebar entry, docked QPO history entry,
terminal history jobs, active queue jobs, tab filtering, search, clear
queue, and clear history.
- **What**: Adds typed `POST /api/queue` and `POST /api/history` route
helpers that validate request bodies with generated zod schemas.
- **What**: Adds stable test ids for the job history sidebar and queue
progress overlay so tests avoid structural CSS selectors.
- **Dependencies**: Builds on the typed route mock foundation merged in
#12267.

## Review Focus

Review the product assertions and whether this is the right first
coverage slice on top of the typed route mock foundation. This PR
intentionally avoids asset sidebar and floating QPO lifecycle coverage;
those should remain follow-up PRs.

## Screenshots (if applicable)

Not applicable.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12272-test-cover-job-history-sidebar-with-typed-route-mocks-3606d73d3650817481d5f9fac4bfc93c)
by [Unito](https://www.unito.io)
2026-05-18 18:34:56 -07:00
Benjamin Lu
3e31de5bbb test: migrate MaxHistoryItems browser coverage (#12298)
## Summary

Migrates the MaxHistoryItems browser coverage to the accepted jobs route
fixture pattern.

## Changes

- **What**: Composes `jobsRouteFixture` into the queue settings spec and
removes the old `AssetsHelper` route setup.
- **What**: Adds a `responseLimit` option to `jobsRouteFixture` so tests
can match a requested history limit while intentionally returning more
jobs.
- **Dependencies**: None.

## Review Focus

The key behavior is preserving both FE-501 acceptance cases: `/api/jobs`
still receives the configured `limit`, and the queue panel still caps
rendered history even if the mocked backend returns more rows than
requested.

Fixes FE-501

## Screenshots (if applicable)

Not applicable.

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12298-test-migrate-MaxHistoryItems-browser-coverage-3616d73d365081d6bf77fb205fcd51d4)
by [Unito](https://www.unito.io)
2026-05-18 18:59:20 +00:00
jaeone94
0558740c78 refactor: migrate default combo widget select to Reka (#12288)
## Summary

Migrate the default combo widget select from the PrimeVue `SelectPlus`
wrapper to a Reka `Combobox` implementation while preserving the
existing Comfy combo widget contract and the node-canvas dropdown
behavior.

## Changes

- **What**: Rewrites `WidgetSelectDefault.vue` on top of Reka
`ComboboxRoot`, `ComboboxTrigger`, `ComboboxInput`, `ComboboxContent`,
and `ComboboxItem`.
- **What**: Preserves the default combo widget surface: `v-model`,
`widget` prop, `aria-label` from the widget name/label,
`data-capture-wheel`, disabled state, placeholder/filter placeholder,
default slot controls, invalid current value display, array values,
dynamic/factory values, and `getOptionLabel` fallback behavior.
- **What**: Keeps dynamic `values` compatibility by refreshing
function-backed options when the dropdown opens, without re-evaluating
the factory on every search keystroke.
- **What**: Deletes the now-unused PrimeVue `SelectPlus.vue` wrapper and
removes the PrimeVue test plugin/stub path from the default widget
select tests.
- **What**: Updates App Mode dropdown clipping coverage and combo-widget
browser coverage to target the new Reka overlay/viewport structure.
- **Breaking**: No breaking change is intended for the documented Comfy
combo widget contract. This migration does not preserve incidental
PrimeVue `Select` prop pass-through from `widget.options`; that was a
side effect of wrapping PrimeVue rather than a stable widget API.
- **Dependencies**: No new dependencies.

## Review Focus

### Compatibility choices

The goal of this PR is a migration PR, not a broad behavior redesign.
The new implementation keeps the Comfy-specific combo contract rather
than attempting to emulate PrimeVue internals. In particular:

- `values` still accepts arrays and functions, and function values are
re-read on open to support dynamic/custom node option sources.
- `getOptionLabel(value) || value` is intentionally preserved to match
the sibling dropdown path and avoid turning an empty-string label into a
blank rendered option.
- Invalid/current values that are not present in the option list are
still rendered in the trigger instead of disappearing.
- `WidgetWithControl` continues to render its default slot in the
control area, with trigger text truncation preserved.
- App Mode `OverlayAppendToKey='body'` continues to map to a body portal
to avoid panel clipping.

### Visual alignment and screenshot updates

The previous PrimeVue implementation passed `size="small"`, which
injected internal `.p-select-sm .p-select-label` styling. That internal
PrimeVue style used its own small-select font size and padding,
overriding the surrounding widget sizing intent and making the select
trigger subtly taller with slightly larger text than nearby inline node
widget controls.

The Reka implementation intentionally keeps the normal widget styling
path instead of recreating that PrimeVue-specific internal override.
This means the trigger follows the same inline widget sizing direction
as neighboring controls rather than preserving the incidental PrimeVue
height/text-size delta. Because this is an expected visual difference
from the migration, the affected E2E screenshots should be recaptured
instead of treating the old PrimeVue select height as the target.

### Scrollbar and focus behavior

Reka provides the combobox/listbox semantics we want, including search,
arrow navigation, highlighted items, and Enter selection. The tricky
part is the canvas dropdown scrollbar behavior. The native Reka viewport
path hides/owns scrollbar behavior in a way that made it hard to
preserve the previous widget dropdown affordances, especially visible
scrollbars and mouse wheel capture over the node canvas.

To keep the previous behavior, this PR renders a dedicated scrollable
viewport inside `ComboboxContent` with the project scrollbar utilities
(`scrollbar-thin`, stable gutter, transparent track). That preserves
visible scroll affordance and allows wheel events over the dropdown to
scroll the list instead of zooming the canvas.

There was one Reka interaction to account for: pressing the native
scrollbar can be treated as a focus-outside event from the search input,
which previously closed the dropdown on mouse down or caused subsequent
wheel events to leak back to the canvas. The new
`useRestoreFocusOnViewportPointer` composable handles only that short
pointer gesture:

- viewport pointerdown marks a short-lived scrollbar/viewport
interaction,
- the next focus-outside event is prevented only if the search input can
be restored,
- the guard is cleared by `pointerup`, `pointercancel`, and a timeout so
normal outside clicks still close the dropdown.

### Tests and regression coverage

Unit coverage was updated around the new Reka implementation:

- option sources from arrays and functions,
- dynamic values refreshed on open but not on each search keystroke,
- selection updates and blank/undefined Reka emissions being ignored,
- search filtering and Reka keyboard selection behavior,
- disabled state, invalid current values, `getOptionLabel`, empty
results status, and WidgetWithControl slot preservation,
- composable coverage for pointerup, pointercancel, repeated pointerdown
listener cleanup, and no-input/no-op behavior.

Browser regression coverage now checks the canvas-specific interaction
surface:

- opening and selecting default combo widget options,
- wheel over the dropdown scrolls the list instead of zooming the
canvas,
- pressing the scrollbar does not close the dropdown,
- wheel capture still works after pressing the scrollbar,
- opening another node widget closes the previous dropdown,
- switching between node widgets preserves dropdown scroll capture,
- serialize/reload retains selected combo values.

## Screenshots (if applicable)
New
<img width="527" height="753" alt="스크린샷 2026-05-18 오전 1 36 27"
src="https://github.com/user-attachments/assets/2293d510-6965-4b84-9b12-b8528f8a734f"
/>

Old 
<img width="496" height="473" alt="스크린샷 2026-05-18 오전 1 35 57"
src="https://github.com/user-attachments/assets/47c0e28a-27df-44a6-81a8-14fcc1f3bd8f"
/>

Reka Supports Auto highlight top item on search (Search -> Enter ->
Select 👍)


https://github.com/user-attachments/assets/9d633dfc-c23a-4e7a-8d39-b044c219f1f3

The default combo widget trigger has a small intentional visual delta
from the old PrimeVue path because the Reka implementation does not
recreate PrimeVue's internal `size="small"` label override.


https://github.com/user-attachments/assets/a9053a14-e39e-4d5e-a846-dcf9aeb0caed



## Validation

- `pnpm format`
- `pnpm lint` (passes; existing warning-only lint output remains in
unrelated tests)
- `pnpm typecheck`
- `pnpm typecheck:browser`
- `pnpm test:unit`

- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://127.0.0.1:5174 pnpm
exec playwright test
browser_tests/tests/vueNodes/widgets/combo/comboWidget.spec.ts
--project=chromium`

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12288-refactor-migrate-default-combo-widget-select-to-Reka-3616d73d365081fd8742c038a7dc7851)
by [Unito](https://www.unito.io)

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
2026-05-18 02:58:37 +00:00