*PR Created by the Glary-Bot Agent*
---
## Summary
Replaces the `tos.*` i18n keys in
`apps/website/src/i18n/translations.ts` with the legal-approved Terms of
Service copy from `Comfy - Terms of Service (GP 5.12.26).docx` and
surfaces an effective date below the hero on `/terms-of-service`.
- Restructures the ToS into 14 sections (intro + 13 numbered sections)
to match the new legal-approved structure.
- Adds two new keys, `tos.effectiveDateLabel` and `tos.effectiveDate`,
rendered as a centered `Effective Date: May 13, 2026` line between the
hero and the content (matches the pattern used on the Affiliate Program
Terms page).
- Subsection labels (*Right to Access and Use Comfy Products.*,
*Customer Data.*, etc.) render as h3 headings via the existing
`block.N.heading` shape — no changes to `ContentSection.vue` or
`contentSections.ts`.
- English page meta description tightened to reflect the new scope
(Comfy Products: Cloud, API, Enterprise — explicitly excluding Comfy
OSS).
## Verbatim legal copy
Per request, the copy is **verbatim from the legal-approved `.docx`**,
including:
- `[URL]` placeholder in §2.7 (Data Retention) where the legal doc has a
placeholder pending the real docs page.
- `[Address]` placeholder in §12.8 (Notices) where the legal doc has a
placeholder pending the finalized mailing address.
- Mixed casing in §8 (Disclaimer) and §9 (Limitation of Liability) —
e.g. `THE Comfy Products AND OUTPUT…`, `…TOTAL LIABILITY OF Comfy…` —
preserved exactly as the legal doc presents it.
- §11(c) cross-reference left as written.
These are intentional and flagged for follow-up with legal/docs before
publishing. I have **not** silently substituted real values for the
placeholders or normalized casing — that would be editing legal-approved
text.
## Chinese (zh-CN) handling
The legal-approved copy was provided in English only. To avoid serving
English text under a Chinese page shell:
- `apps/website/src/pages/zh-CN/terms-of-service.astro` is **removed**.
- `getRoutes()` in `apps/website/src/config/routes.ts` treats
`termsOfService` as locale-invariant, so the Chinese footer link emits
`/terms-of-service` directly — no redirect hop.
- `astro.config.ts` adds a redirect from `/zh-CN/terms-of-service` →
`/terms-of-service` as a safety net for any stale external/cached links.
- All `zh-CN` values on the new `tos.*` keys are filler (mirrored from
English) so the `Record<Locale, string>` type contract holds; they are
never served.
## Files changed
- `apps/website/src/i18n/translations.ts` — 73 old `tos.*` keys removed,
136 new keys added matching the .docx structure.
- `apps/website/src/pages/terms-of-service.astro` — imports `t`, renders
effective date, updates meta description.
- `apps/website/src/pages/zh-CN/terms-of-service.astro` — **removed**.
- `apps/website/astro.config.ts` — adds `/zh-CN/terms-of-service` →
`/terms-of-service` redirect.
- `apps/website/src/config/routes.ts` — `termsOfService` route stays
un-prefixed in non-English locales.
## Verification
- `pnpm --filter=@comfyorg/website typecheck` — 0 errors (2 pre-existing
hints in unrelated files).
- `pnpm --filter=@comfyorg/website build` — 279 pages built,
`/terms-of-service/` (English page) and `/zh-CN/terms-of-service/`
(redirect stub with `noindex` + `canonical`) both emitted.
- Pre-commit lint-staged ran `oxfmt`, `oxlint --type-aware`, `eslint
--fix`, and `pnpm typecheck` on every commit — all green.
- Rendered HTML spot-checked: English `/terms-of-service` contains the
new content with verbatim `[URL]` and `[Address]` placeholders; zh-CN
homepage footer now links directly to `/terms-of-service` (no redirect
hop); `/zh-CN/privacy-policy` and other locale routes still correctly
emit `/zh-CN/…` prefixes.
- Manual visual check via `astro preview` + Playwright — sidebar nav, h2
section titles, h3 subsection headings, paragraph wrapping, and inline
mailto/href anchors all render correctly. Screenshots attached.
## Code-review follow-ups addressed
- **zh-CN regression** — Page removed, route override added, redirect
kept as safety net.
- **Page description mismatch** — Updated meta description to reflect
new scope.
- **`docs.comfy.org/data-retention` 404** — Now matches the docx
placeholder `[URL]`; flagged to legal/docs.
- **Disclaimer / Liability casing** — Restored to match docx verbatim.
- **Mailing address** — Now matches the docx placeholder `[Address]`;
flagged to legal.
- **Section 11(c) cross-reference** — Left verbatim per legal doc.
## Scope notes
- English-only legal update per request — no Chinese rewrite, no schema
changes, no acceptance-tracking infrastructure.
- The signup-flow link on `platform.comfy.org` (`website` repo) already
points at `https://www.comfy.org/terms-of-service` and renders the new
copy at the same URL — no change needed there.
## Screenshots


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12286-feat-website-update-Terms-of-Service-to-legal-approved-2026-05-13-copy-3616d73d3650815b9262f84d12655dfa)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## 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)
## 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>
## Summary
When a workflow queues a 3D job (Preview3D / Load3D / SaveGLB) and the
user switches to another tab before the job completes, the WS `executed`
payload was silently dropped: `getNodeByExecutionId(app.rootGraph, …)`
resolves against the currently-visible workflow's graph, so the node
isn't found, `onExecuted` never fires, and the workflow JSON never
learns the path of the asset the backend just saved.
Worse, `setNodeOutputsByExecutionId` for top-level execution ids returns
the id verbatim without consulting rootGraph, so the old handler also
wrote the payload into the *active* (wrong) workflow's
`app.nodeOutputs`, polluting it and getting mis-attributed by
`ChangeTracker` snapshot/restore on subsequent tab switches.
This change introduces a single routing point — no new public API, no
new in-memory cache layer — that re-uses three pieces of infrastructure
that already exist independently:
1. `executionStore.jobIdToSessionWorkflowPath` already records which
workflow queued each prompt_id.
2. Each `ComfyWorkflow.ChangeTracker` already snapshots `nodeOutputs` on
`deactivate()` and replays it through the `app.nodeOutputs = …` setter
on `restore()`, which fires `onNodeOutputsUpdated` for every extension.
3. Audio nodes already implement `onNodeOutputsUpdated` to rehydrate
their widget from `nodeOutputs`. They have always been silently broken
in this same scenario (no one noticed because missing audio is less
visible than a missing 3D model); the fix here repairs audio for free as
a side effect.
The new behaviour:
- `app.ts` looks up the node against the active rootGraph *first*. Only
when found do we touch `nodeOutputStore` and call `node.onExecuted`
(existing path, unchanged). Moving the `nodeOutputStore` write inside
`if (node)` is the part that eliminates the cross-workflow pollution
above.
- When the node is not in the active rootGraph, the new
`routeExecutedToInactiveOwner` helper finds the owner workflow via
`jobIdToSessionWorkflowPath`, then writes the payload directly into
`owner.changeTracker.nodeOutputs` — the same field that `restore()`
already drains on tab switch back.
- Preview3D / SaveGLB add `onNodeOutputsUpdated` (mirroring the pattern
audio uses), normalise the path, write `node.properties['Last Time Model
File']` (already workflow-JSON-serialised), and apply it to the viewer.
The legacy `node.onExecuted` chain stays intact for live active-workflow
executions — both paths are idempotent.
## Screenshots (if applicable)
Before
https://github.com/user-attachments/assets/3803af1f-2eb6-41af-87ed-ac885a2eaad6
After
https://github.com/user-attachments/assets/72e1bed9-5f94-414d-ac31-fc925651d11b
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12295-FE-416-route-3D-node-outputs-to-owner-workflow-on-tab-switch-3616d73d3650817b908de48a32b1d6bd)
by [Unito](https://www.unito.io)
*PR Created by the Glary-Bot Agent*
---
## Summary
The
[`/cloud/supported-nodes`](https://comfy-website-preview-pr-12271.vercel.app/cloud/supported-nodes)
page was rendering packs without descriptions, icons, or download counts
(PR #12271 preview, [`Release: Website`
run](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25866708684/job/76010642686)).
The registry enrichment in
`apps/website/src/utils/cloudNodes.registry.ts` was silently failing for
**two** reasons:
1. **Missing `limit` query parameter.** `api.comfy.org /nodes` applies a
default page size of `10` when no `limit` is sent. Each batch of up to
50 `node_id` filters was therefore truncated to 10 results, dropping
metadata for every pack past the first ten.
2. **Schema rejected `null` for optional arrays.** The registry
serializes empty server-side slices as JSON `null`, so any pack with
`supported_os: null` or `supported_accelerators: null` failed Zod
validation — and because parse failure is not retryable, the **entire
batch** got `null` enrichment.
Both bugs produce the same user-visible symptom ("packs fetched, but no
metadata"), so they were entangled.
## Changes (2 files)
- `cloudNodes.registry.ts`: send `?limit=<batch length>` on every batch
and accept `null` for all optional registry fields. The schema
normalizes `null → undefined` at the parse boundary via `.transform()`,
so the parsed shape continues to match the generated OpenAPI `Node` type
contract; downstream code (`toDomainPack`, the rendered `Pack`) is
unchanged.
- `cloudNodes.registry.test.ts`: two new regression tests:
- Server-side default page size: simulates the pre-fix behavior (default
`limit=10` truncates) and asserts all 30 batched IDs are enriched.
- `null` registry fields: asserts `null` values are normalized to
`undefined` on the parsed pack.
## Verification
End-to-end fetch against the live registry on this branch (14 packs from
the current snapshot):
```
Requested: 14, enriched: 14
comfyui-kjnodes downloads=3,404,416
rgthree-comfy downloads=3,105,034
comfyui-easy-use downloads=2,829,702
comfyui-impact-pack downloads=2,680,589
comfyui_essentials downloads=2,418,367 (supported_os null → undefined)
ComfyUI-Crystools downloads=1,729,087
comfyui_layerstyle downloads=1,696,809
comfyui_ultimatesdupscale downloads=1,478,763
comfyui_ipadapter_plus downloads=1,236,442 (supported_os null → undefined)
was-node-suite-comfyui downloads=993,960 (supported_os null → undefined)
comfyui-advanced-controlnet downloads=600,849
comfyui-animatediff-evolved downloads=503,831
comfyui-cogvideoxwrapper downloads=121,716 (supported_os null → undefined)
comfyui_steudio downloads=58,470 (supported_os null → undefined)
```
5 of 14 packs returned `null` arrays — all parse cleanly now.
Sort-by-downloads (already implemented in `useFilteredPacks.ts`) becomes
meaningful again once `downloads` is populated.
Quality gates:
- `pnpm --filter @comfyorg/website test:unit` → 77/77 pass (includes 2
new regression tests)
- `pnpm --filter @comfyorg/website typecheck` → 0 errors, 0 warnings on
changed files
- `pnpm exec eslint` on changed files → clean
- `pnpm exec oxfmt --check` on changed files → clean
## Follow-ups (separate tickets)
- "New" badge + `dateAdded` field for newly added packs.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12307-fix-website-restore-registry-metadata-for-cloud-nodes-catalog-3626d73d365081288a2cfc30003160cf)
by [Unito](https://www.unito.io)
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
Model Library sidebar now refreshes automatically when an upload
completes, and the `r` keybinding refreshes the library in addition to
refreshing combo widgets inside graph nodes.
## Changes
- **What**:
- `modelStore`: new `refreshModelFolder(name)` for surgical reset+reload
of one folder, and `refresh()` that re-loads any folders that had been
loaded
- `ModelLibrarySidebarTab.vue`: watches
`assetDownloadStore.lastCompletedDownload` and refreshes the affected
folder; the in-panel refresh button now routes through `refresh()`
- `Comfy.RefreshNodeDefinitions` (`r` key): also calls
`modelStore.refresh()` so the keyboard fallback actually refreshes the
Model Library list
## Review Focus
- Both `modelStore` and `assetsStore` exist; the upload wizard was only
refreshing the latter, which is what caused the bug. Confirm the new
watcher path is the right hook (rather than wiring it inside the wizard)
— chose this so it also covers completions that happen after the wizard
has been closed.
- `refreshModelFolder` falls back to `refresh()` (not raw
`loadModelFolders()`) for unknown folder types, to avoid dropping other
folders' loaded contents.
- Generated tab half of the ticket is intentionally **deferred** until
BE-885 (cursor pagination on `GET /api/jobs`) lands — AC items around
"no duplicates" and "cursor state maintained" depend on it.
Fixes FE-695 (Model Library half).
## Screenshots (if applicable)
N/A — behavior change verified by unit tests.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12257-fix-model-library-auto-refresh-after-upload-plus-r-key-fallback-3606d73d3650811a8895ef6e3ef2b4b8)
by [Unito](https://www.unito.io)
## Summary
When a Preview3D / Load3D / SaveGLB node lives inside a subgraph, the 3D
viewer correctly displays the model the first time you enter the
subgraph but is blank after exiting and re-entering — even though
`node.properties['Last Time Model File']` is still populated and the
underlying file is on disk.
Fix: introduce a persistent companion to `waitForLoad3d` in
`useLoad3d.ts`:
- `onLoad3dReady(callback)` — registers a callback that fires on *every*
(re-)initialization of the `Load3d` instance for a given node, not just
the first one. Cleared automatically when the node is removed from the
graph (chained into `node.onRemoved` alongside the existing
`pendingCallbacks` cleanup).
- `waitForLoad3d` keeps its original one-shot semantics so callbacks
that install per-node side effects (e.g. wrapping `node.onExecuted`,
setting `sceneWidget.serializeValue`) do not chain on remount.
- When `onLoad3dReady` is registered after a `Load3d` instance already
exists, the callback fires synchronously as well, so the same code path
covers both initial setup and subsequent rehydrations.
Preview3D / Load3D / SaveGLB move the "reapply state from
`node.properties` / `model_file` widget to the Load3d viewer" block from
`waitForLoad3d` to `onLoad3dReady`.
First mount and every subsequent remount now run identical rehydration
code, with `node.properties['Last Time Model File']` (already
workflow-JSON-serialised) as the single source of truth.
## Screenshots (if applicable)
before
https://github.com/user-attachments/assets/e4b0fe6f-c898-4210-b545-7ad6883ed722
after
https://github.com/user-attachments/assets/a4a28490-071d-4694-87a8-5eaa501ac168
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12294-FE-702-rehydrate-3D-viewer-on-subgraph-re-entry-via-persistent-ready-hook-3616d73d3650811e93e7dedb32762711)
by [Unito](https://www.unito.io)
## Summary
Resolve the queue progress node label from queued prompt metadata so
subgraph execution IDs show the correct node name without depending on
the live canvas.
## Changes
- **What**: Store a prompt-scoped `executionId -> { title, type }`
lookup from `p.output` when queueing a job, and use that lookup for the
active job's executing node label.
- **What**: Reuse the same job-scoped node info for the browser tab
title so it stays aligned with the queue overlay.
- **What**: Add unit coverage for root and subgraph execution IDs, and
merge the branch forward to current `main`.
## Review Focus
This keeps the fix scoped to the existing singular `activeJobId` path.
It fixes subgraph labels and avoids the workflow-switching regression
from resolving against `app.rootGraph`, but it does not redesign
concurrent multi-job selection yet.
Longer term, the cleaner solution is still prompt-scoped execution
metadata from the backend rather than frontend reconstruction.
## Screenshots (if applicable)
N/A
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
## Summary
On macOS trackpads, several browser default gestures were leaking
through and breaking the workflow:
- **Pinch-zoom on a focused textarea widget** triggered page-level zoom,
pushing `position: fixed` UI (notably `ComfyActionbar`) off-screen until
a reload.
- **Horizontal swipe on a focused textarea widget** triggered browser
back/forward, leaving the workflow.
- **Pinch / horizontal swipe inside the image picker dropdown** had the
same two issues, because PrimeVue `Popover` teleports content to
`document.body` and the `LGraphNode` wheel handler never sees the
events.
Fixes FE-292.
## Why
- **`overscroll-behavior: none` on `html, body`** — horizontal swipe to
back/forward is decided by the browser at gesture start; JS
preventDefault can't reliably beat it. `overscroll-behavior` is the
standards-track signal for opting out, and ComfyUI is a full-screen
editor that never benefits from native overscroll.
- **`useCanvasInteractions` now treats pinch-zoom and
horizontal-dominant wheel as canvas gestures** that override widget
wheel consumption, so the gesture pans/zooms the canvas instead of
falling through to destructive browser defaults. The check is exported
as `isCanvasGestureWheel` for reuse.
- **`FormDropdownMenu` has its own minimal `onWheel`** that only
preventDefaults destructive gestures and deliberately does not forward
to the canvas. The dropdown is its own scroll container and shouldn't
leak interactions into the editor; vertical scrolling stays native.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12052-fix-stop-trackpad-pinch-swipe-gestures-from-breaking-the-UI-3596d73d3650810aac3fcd8a71b29f9e)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Centralize template route mocking by mirroring the existing
`AssetHelper` fixture pattern, so tests no longer hand-roll
`page.route('**/templates/index.json', ...)` and
`page.route('**/templates/**.webp', ...)` blocks.
## Changes
- **What**:
- Expand `browser_tests/fixtures/data/templateFixtures.ts` with stable
distribution exports (`STABLE_CLOUD_TEMPLATE`,
`STABLE_DESKTOP_TEMPLATE`, `STABLE_LOCAL_TEMPLATE`,
`STABLE_UNRESTRICTED_TEMPLATE`), an `ALL_DISTRIBUTION_TEMPLATES` set,
and a `generateTemplates(count, distribution?)` bulk generator.
`makeTemplate` and `mockTemplateIndex` are preserved.
- Add `browser_tests/fixtures/helpers/TemplateHelper.ts` modeled on
`AssetHelper`: an immutable `TemplateConfig` driven by composable
operators (`withTemplates`, `withTemplate`, `withCloudTemplates`,
`withDesktopTemplates`, `withLocalTemplates`,
`withUnrestrictedTemplates`, `withRawIndex`), a `TemplateHelper` class
with `mock()` / `mockIndex()` / `mockThumbnails()` / `clearMocks()`, and
a `createTemplateHelper(page, ...operators)` factory.
- Add `browser_tests/fixtures/templateApiFixture.ts` exposing
`templateApi: TemplateHelper` as a Playwright fixture with automatic
`clearMocks()` teardown (mirrors `assetApiFixture`).
- Migrate `browser_tests/tests/templateFilteringCount.spec.ts` to
`mergeTests(comfyPageFixture, templateApiFixture)` and
`templateApi.configure(withTemplates(...))` + `templateApi.mockIndex()`.
The webp thumbnail mock moves into the helper.
- **Breaking**: None. `makeTemplate` and `mockTemplateIndex` exports
remain.
## Review Focus
- `TemplateHelper.mock()` is split into `mockIndex()` and
`mockThumbnails()` so the spec can install the thumbnail handler in
`beforeEach` (where it has no per-test data) and the index handler
per-test (after `configure(...)`). Both still register through
`routeHandlers` so `clearMocks()` unroutes everything.
- Operators are pure (`(config) => config`) and the helper deep-copies
the resulting `templates` array, matching the AssetHelper immutability
pattern.
- The thumbnail route is `**/templates/**.webp` and serves
`browser_tests/assets/example.webp` — identical to the previous inline
behavior.
Fixes#11431
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11837-refactor-browser-tests-centralize-template-mocking-via-TemplateHelper-3546d73d365081beac00d80c2ab5ea1c)
by [Unito](https://www.unito.io)
## Summary
Follow-up to #12183: move the debounced, searcher-driven search input
out of `src/renderer/...` and into the shared primitives folder, so both
the graph (form dropdown node widget) and the shell UI (templates
dialog, right side panel tabs) can use it without crossing the renderer
layer.
## Changes
- **What**: Renamed and relocated `FormSearchInput` → `AsyncSearchInput`
at `src/components/ui/search-input/AsyncSearchInput.vue`, joining the
existing `SearchInput` / `SearchAutocomplete` siblings.
- **What**: Updated all 9 callers (graph form dropdown, right side panel
tabs, templates dialog) to import from the new path/name. Test file
moved alongside the component.
- **Breaking**: None — pure rename + relocate, behavior is identical.
## Review Focus
- New name reflects the component's distinguishing feature (the async
`searcher` lifecycle: debounce + cancellation + loading state); see
Slack thread.
- Slack thread captured the discussion — splitting from #12183 so the
perf fix can backport cleanly to the release line.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12185-refactor-promote-FormSearchInput-to-shared-ui-as-AsyncSearchInput-35e6d73d365081c585d8d421ea4353fa)
by [Unito](https://www.unito.io)
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
*PR Created by the Glary-Bot Agent*
---
## Summary
Bump `tailwindcss` and `@tailwindcss/vite` from `^4.2.0` to `^4.3.0` in
the workspace catalog so the new first-class scrollbar utilities
(notably `scrollbar-gutter-stable`) are available — the missing utility
behind the FE-697 workaround. Release notes:
https://tailwindcss.com/blog/tailwindcss-v4-3.
## Changes
- **What**:
- `pnpm-workspace.yaml` catalog: `tailwindcss` and `@tailwindcss/vite` →
`^4.3.0` (lockfile resolves to `4.3.0` for `tailwindcss`,
`@tailwindcss/vite`, `@tailwindcss/node`, and all `@tailwindcss/oxide-*`
native packages).
- Auto-fix 21 lint errors that `eslint-plugin-better-tailwindcss` now
reports because the new utilities have canonical forms:
- `VirtualGrid.vue`: `[scrollbar-gutter:stable]` →
`scrollbar-gutter-stable` (the FE-697 case)
- `VirtualGrid.vue` + `RightSidePanel.vue`: `scrollbar-thin` class-order
fix
- `TopBarHeader.vue`: `h-6.25 w-6.25` → `size-6.25` (6 button cells)
- `Dialogue.vue`: `translate-x-[-50%] translate-y-[-50%]` →
`translate-[-50%]`
- Minor `-mx-[…]`, `-inset-[…]`, `-ml-[…]` arbitrary-value reorderings
in `NodeSearchFilterBar.vue`, `LGraphNode.vue`,
`CloudNotificationContent.vue`
- All 21 are auto-fixes; all existed on `main` and would have started
failing CI on next merge.
- **Breaking**: None. v4.3 is purely additive; existing config
(CSS-first `@theme`/`@utility`/`@plugin`, custom `lucideStrokePlugin`,
`tailwindcss-primeui`, `@iconify/tailwind4`, `tw-animate-css`) is
unchanged.
## Review Focus
- Confirmed `scrollbar-gutter: stable` compiles into
`dist/assets/index-*.css` from `VirtualGrid.vue` after the
canonicalization.
- Runtime probe via Playwright (preview build) confirmed
`scrollbar-gutter-stable`, `scrollbar-thin`, `tab-4`, and `zoom-100` all
apply.
- `pnpm typecheck`, `pnpm lint`, `pnpm build`, `pnpm knip` all pass.
- `pnpm test:unit`: 10687/10696 pass. 1 failure in `GraphView.test.ts`
("reconnect wiring") confirmed to fail identically on `main` (flaky
`toHaveBeenCalledTimes(1)` getting `3`/`4`) — unrelated to this change.
- Oracle code review: 0 findings.
Refs FE-697.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12275-chore-upgrade-tailwindcss-to-4-3-3606d73d3650813185cece1c7315e1c2)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
- Add `unmapped` to genhtml `--ignore-errors` flag to fix GH Pages
deploy failure
- Remove unreachable placeholder block (dead code cleanup)
## Problem
PR #11381 added `--ignore-errors source` but genhtml is now failing with
a different error:
```
genhtml: ERROR: no data for line:4291, TLA:GNC, file:src/lib/litegraph/src/LGraphNode.ts
(use "genhtml --ignore-errors unmapped ..." to bypass this error)
```
This happens when LCOV data references lines that don't map to source
(from V8 coverage instrumentation).
## Changes
1. **Add `unmapped` to ignore-errors** — `--ignore-errors
source,unmapped` handles both missing source files and unmapped line
data
2. **Remove unreachable placeholder block** — The `if [ ! -s
coverage/playwright/coverage.lcov ]` check is dead code because the step
is already gated on `has-coverage == 'true'`, which only triggers when
the merged LCOV exists and is non-empty
## Test plan
- [ ] Verify workflow completes successfully on next push to main
- [ ] Verify https://comfy-org.github.io/ComfyUI_frontend/ returns 200
Fixes#12229🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12273-fix-ci-add-unmapped-to-genhtml-ignore-errors-remove-unreachable-placeholder-3606d73d36508198a4ffddfce3354d4a)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Replace the merged stateful jobs API browser mock fixture with a small
declarative typed route-mock foundation.
## Changes
- **What**: Removes `JobsApiMock`, `jobsApiMockFixture`, and the old
shared `jobFixtures` helper.
- **What**: Adds a generic `RouteMocker` primitive for explicit typed
JSON route responses.
- **What**: Adds `jobsRouteFixture`, which registers explicit
`/api/jobs` list/detail responses without filtering, mutation handling,
or hidden in-memory backend behavior.
- **What**: Migrates the current queue overlay and missing-media runtime
specs onto the new jobs route fixture.
- **What**: Keeps `./browser_tests/tsconfig.json` in the ESLint
TypeScript resolver config.
- **Dependencies**: None.
## Review Focus
This is intended to be the foundation PR for the test-strategy reset:
old stateful helper out, typed declarative route mocks in. It
intentionally does not add the full asset sidebar, job history sidebar,
or floating QPO coverage suite; those should stack on top after this
fixture shape is accepted.
The boundary this PR is trying to preserve: route mocks may describe
frontend-visible API responses, but should not implement Core
queue/history mutation semantics.
Context:
https://www.notion.so/comfy-org/E2E-Test-Strategy-for-Assets-Job-History-and-Queue-Progress-Overlay-35f6d73d365081209bc5f10e6c7eb8de
## Screenshots (if applicable)
Not applicable.
*PR Created by the Glary-Bot Agent*
---
## Summary
Click vs drag is disambiguated by two thresholds: distance
(`Comfy.Pointer.ClickDrift`, 6px) and time
(`Comfy.Pointer.ClickBufferTime`). Distance does the real work — any
intentional drag immediately exceeds 6px on the first move. The time
threshold only matters in the corner case where the pointer is held
still then released without crossing the distance threshold.
The previous 150ms default forces every pointerdown to wait up to 150ms
before drag begins, even when the user is clearly dragging. This is
visible as lag when click+dragging an unselected node, where the wait
stacks on top of selection-state work that runs at drag start
([FE-558](https://linear.app/comfyorg/issue/FE-558/bug-delay-when-clickdragging-an-unselected-node)).
32ms (~2 frames at 60fps) is well below human-perceptible click latency
and still safely above incidental jitter on pointerdown.
## Changes
- `src/platform/settings/constants/coreSettings.ts` —
`Comfy.Pointer.ClickBufferTime`:
- `defaultValue: 150` → `32`
- `versionAdded: '1.4.3'` retained, add `versionModified: '1.44.19'`
- Slider `step: 25` → `1` (the old step made `32` unrepresentable on the
slider)
- Tooltip extended with rationale (why distance is the real
disambiguator, why this should be small)
- `src/lib/litegraph/src/CanvasPointer.ts` — Static `bufferTime = 150` →
`32` for consistency with the user-setting default; the runtime
`watchEffect` in `useLitegraphSettings.ts` continues to override at
boot. JSDoc explains the rationale and that the user setting overrides
at runtime.
## Verification
- `pnpm typecheck` clean.
- Litegraph + settings test suites: 998/998 pass.
- `pnpm format` clean (lint-staged enforced on commit).
## Why not just leave it user-overridable?
It already is. But the default is what every user — including ones who
never open settings — experiences. 150ms was never a justified value (no
comment explains it, no benchmark, and 6px distance covers the
disambiguation). Defaults should reflect best UX.
Refs FE-558.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12032-feat-settings-lower-Comfy-Pointer-ClickBufferTime-default-from-150ms-to-32ms-3586d73d365081f5a29cdc84b4b736e3)
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>
*PR Created by the Glary-Bot Agent*
---
## Summary
- Fixes app mode bug where custom combo inputs (and other widget inputs)
unselect and show "Widget not visible" after refreshing or reopening a
saved app workflow
- Root cause: `resetSelectedToWorkflow()` loads from
`changeTracker.activeState` which may not have linearData yet after
refresh, and `pruneLinearData()` prunes valid entries during graph
loading when nodes aren't yet in the graph
- Two defensive guards: fallback to `initialState` for authoritative
data, and skip pruning during graph loading
## Changes
- `appModeStore.ts`: `resetSelectedToWorkflow()` now falls back to
`initialState.extra.linearData` when `activeState` has none
- `appModeStore.ts`: `pruneLinearData()` skips node-existence checks
when `ChangeTracker.isLoadingGraph` is true
- Unit tests: 4 new tests covering both fix paths (pruning during
loading, fallback to initialState)
- E2E test: Save-as → close → reopen → verify all inputs persist with no
"Widget not visible"
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11422-fix-preserve-app-builder-inputs-through-graph-reconfiguration-3476d73d36508166a563f7df3967665c)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
*PR Created by the Glary-Bot Agent*
---
## Summary
`Release: Website` only refreshed the Ashby snapshot, so the cloud-nodes
snapshot (`apps/website/src/data/cloud-nodes.snapshot.json`) was stale
on every release. `loadPacksForBuild()` then silently fell back to that
snapshot because `WEBSITE_CLOUD_API_KEY` was never plumbed through CI or
Vercel, leaving production at `/cloud/supported-nodes` with placeholder
data (e.g. `rgthree-comfy` listed as supported when it isn't — visible
at line 104 of the committed snapshot, last fetched 2026-05-04).
## Changes
- **New composite action `.github/actions/cloud-nodes-pull`** mirroring
`ashby-pull`: runs `pnpm --filter @comfyorg/website
cloud-nodes:refresh-snapshot` with `WEBSITE_CLOUD_API_KEY`. The script
already `process.exit(1)`s on any non-`fresh` outcome, so refresh
failures are loud.
- **`release-website.yaml`** now runs both refreshes and opens a single
PR with both updated snapshots. Renamed the job to `refresh-snapshots`,
updated branch/commit/title/body for the wider scope, and kept the
existing `Release:Website` label so downstream automation is unaffected.
- **`cloudNodes.build.ts`** throws when the outcome is `'stale'` **and**
`VERCEL_ENV === 'production'`. Preview / local builds keep the snapshot
fallback so contributors without key access are unaffected. The CI
reporter still runs first so the GitHub annotation explaining *why* it's
stale is visible in the failed job.
- **`ci-vercel-website-preview.yaml`**: passes `WEBSITE_CLOUD_API_KEY`
to `vercel build` in both preview and production jobs, and adds a
preflight step on `deploy-production` that hard-fails before `vercel
build --prod` if the secret is missing — surfacing config drift with a
maintainer-friendly error annotation instead of mid-build.
- **`apps/website/README.md`**: documents the production-strictness
behavior, the new required secret (GitHub Actions + Vercel env), and the
manual refresh path.
- **New unit tests** in `cloudNodes.build.test.ts` (6 cases): fresh,
stale-no-VERCEL_ENV, stale-on-preview, stale-on-production,
failed-regardless, and "still reports on stale-in-production before
throwing".
## Manual / one-time steps required before merging
This PR cannot finish the job alone. A maintainer must also:
1. Add `WEBSITE_CLOUD_API_KEY` as a **GitHub Actions repo secret** in
`Comfy-Org/ComfyUI_frontend`.
2. Add `WEBSITE_CLOUD_API_KEY` to the **Vercel project environment**
(`production` env at minimum; `preview` recommended).
3. Investigate why `rgthree-comfy` is in the current snapshot — either
the Cloud API was actually returning it on 2026-05-04, the snapshot was
generated against a non-production environment, or it was hand-edited.
The first manual run of `Release: Website` after this PR merges will
confirm.
Without step 1, the new `Release: Website` job will fail loudly (the
refresh script exits 1 with `missing WEBSITE_CLOUD_API_KEY`). Without
step 2, the new preflight will fail the production deploy with a clear
error annotation pointing at `apps/website/README.md`. Both failure
modes are intentional — they replace today's silent stale snapshot.
## Related (out of scope for this PR)
The other half of the original report — production 404s on
`/p/supported-models/*`, `/cloud/supported-nodes/*`,
`/demos/community-workflows` from PRs #11892 / #11903 / #11942 — is a
`comfy-router` allow-list gap (those paths exist in the Vercel build as
pre-rendered static HTML). That fix needs to land in
`Comfy-Org/comfy-router` and is being handled separately since glary
doesn't have access to that repo.
## Verification
- `pnpm --filter @comfyorg/website test:unit` — 75/75 pass (6 new in
`cloudNodes.build.test.ts`)
- `pnpm --filter @comfyorg/website typecheck` — 0 errors, 0 warnings (2
pre-existing hints unrelated to this PR)
- `pnpm format` + `pnpm exec eslint` on changed files — clean
- `js-yaml` validates `release-website.yaml`,
`cloud-nodes-pull/action.yaml`, `ci-vercel-website-preview.yaml`
- Oracle code review (round 1) raised 1 warning + 1 suggestion; both
addressed in commit 2.
**Manual verification not applicable**: the runtime changes are GitHub
Actions workflows and a Vercel-env-gated branch in a build-time module —
they cannot meaningfully run outside of GitHub Actions / Vercel, and the
strict-on-stale path is exhaustively covered by the 6 unit tests
(including the exact assertions a manual run would check: throws on
`VERCEL_ENV=production` + stale, passes on preview, reports
observability annotation before throwing). The end-to-end behavior will
be verified by the first `Release: Website` dispatch and the next
production deploy after the maintainer adds the secret.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12219-fix-website-refresh-cloud-nodes-snapshot-in-release-workflow-strict-production-build-35f6d73d3650816d8f32d403cb39d733)
by [Unito](https://www.unito.io)
---------
Co-authored-by: glary <bot@glary.dev>
*PR Created by the Glary-Bot Agent*
---
## Summary
Extend the existing frontend version-mismatch warning UI to consume the
new `comfy_package_versions` array now exposed by ComfyUI's
`/system_stats` endpoint. The backend ships installed/required versions
for every `comfy*` package pinned in `requirements.txt` (frontend,
workflow-templates, embedded-docs, kitchen, aimdo, …); the frontend now
surfaces one toast per outdated package, reusing the existing N=1
frontend-version warning shape.
Backend PR: https://github.com/Comfy-Org/ComfyUI/pull/13875
## Changes
- `src/schemas/apiSchema.ts` — add `comfy_package_versions: Array<{name,
installed, required}>` to the `SystemStats` schema and export a
`ComfyPackageVersion` type. The field is `.optional()` so older backends
remain compatible.
- `src/platform/updates/common/versionCompatibilityStore.ts` — new
`outdatedComfyPackages` / `packageWarningMessages` computeds that mirror
the existing semver `gt` comparison (same `valid` guard). Skip
`comfyui-frontend-package` because the dedicated frontend warning above
already covers it (and uses the running bundle's version rather than the
installed pip version). Outdated packages are sorted by
`name`/`installed`/`required` before being folded into the dismissal
storage key so the key is stable across response orderings — a fresh
package bump re-shows the warning, but the same outdated set in a
different order does not.
- `src/platform/updates/common/useFrontendVersionMismatchWarning.ts` —
emit one toast per outdated package in addition to the existing frontend
toast, reusing the same i18n wrapper and the existing `hasShownWarning`
once-per-session guard.
- `src/locales/en/main.json` — new `g.comfyPackageOutdated` string.
- Unit tests — added coverage for outdated/skip/invalid-version paths,
dismissal-key inclusion, and stable ordering. Existing 21 store + 8
composable tests untouched and still passing.
CI suppression is unchanged: warnings still gate on
`versionCompatibilityStore.shouldShowWarning` (which respects the
`Comfy.VersionCompatibility.DisableWarnings` setting and the 7-day
dismissal cache), and unit tests continue to mock the store the same
way.
## Verification
- `pnpm vitest run` for the version-warning module: **31/31** passing.
- Targeted sweep across `src/platform/updates`,
`src/stores/systemStatsStore.test.ts`, `src/schemas`: **160/160**
passing.
- `pnpm typecheck`: clean.
- `pnpm lint`: 0 errors (3 pre-existing warnings in unrelated 3D test
files).
- `pnpm format`: applied, no incidental changes.
- **Manual Playwright run** against the real dev server with
`/api/system_stats` intercepted to return outdated package data —
produced exactly the expected toasts and correctly skipped
`comfyui-frontend-package` and the up-to-date `comfy-kitchen` entry.
Same run with all-up-to-date data produced zero toasts.
### Toasts produced (manual verification)
The fixture used: `required_frontend_version=99.99.99`, plus
`comfy_package_versions=[frontend-package (outdated, skipped),
workflow-templates 0.9.0→0.9.5 (outdated), embedded-docs 0.4.0→0.5.0
(outdated), comfy-kitchen 0.2.8→0.2.8 (up to date)]`.
## Screenshots


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12167-feat-extend-version-warning-to-all-comfy_package_versions-entries-35e6d73d365081e7b993d0f06c9e5c98)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
*PR Created by the Glary-Bot Agent*
---
## Summary
The in-app cloud paywall modal shown on first install of ComfyUI Desktop
1.0 advertised "400 Free Credits Monthly" as a benefit, but the free
tier is currently disabled, making that copy misleading.
Per the thread direction, this skips the feature-flag plumbing (which
isn't available on Desktop anyway) and simply removes the line.
## Changes
- `CloudNotificationContent.vue`: change the feature loop from `n in 4`
to `n in [2, 3, 4]` so feature1 is skipped.
- `src/locales/*/main.json` (12 locales): remove the now-unused
`cloudNotification.feature1Title` key.
- `browser_tests/tests/dialog.spec.ts`: add a regression assertion that
"400" / "Free Credits" copy is no longer present in the modal.
## Test plan
- `pnpm typecheck` — passes
- `pnpm typecheck:browser` — passes
- `pnpm exec vitest run
src/platform/cloud/notification/components/DesktopCloudNotificationController.test.ts`
— 3/3 pass
- Pre-commit hooks (oxfmt, oxlint, eslint, stylelint, typecheck) — all
pass
- Manual: triggered `showCloudNotification()` against the dev server and
confirmed only 3 benefit rows render (screenshot attached); the "400
Free Credits Monthly" line is gone.
- Fixes DESK2-90
## Screenshots

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12251-fix-remove-400-Free-Credits-Monthly-line-from-cloud-paywall-modal-3606d73d365081eaa572e6cd995278d8)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
*PR Created by the Glary-Bot Agent*
https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1778150003248989
FE-603
---
## Summary
Send `share_id` alongside `published_asset_ids` from
`workflowShareService.importPublishedAssets`, and type / zod-validate
the body against `ImportPublishedAssetsRequest` +
`zImportPublishedAssetsRequest` from `@comfyorg/ingest-types`.
This is a **parameter-schema change**, not a live bug fix. The original
`BAD_REQUEST: share_id is required` failure is **no longer reproducible
in production** — the backend rolled back the required-field enforcement
in [BE-855](https://linear.app/comfyorg/issue/BE-855) (cloud
[#3587](https://github.com/Comfy-Org/cloud/pull/3587) /
[#3588](https://github.com/Comfy-Org/cloud/pull/3588)). Today the import
succeeds without `share_id`.
- Closes FE-603
## Why we still ship this
Frontend goes first so the backend can later re-tighten the contract
without breaking shared-workflow imports a second time. Agreed sequence
in
[Slack](https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1778535349236419)
— BE-6 was split into two sub-issues to make the chain explicit:
1. **[BE-898](https://linear.app/comfyorg/issue/BE-898)** (blocks
FE-603) — BE makes `share_id` **optional**, validating when present.
[cloud PR #3633](https://github.com/Comfy-Org/cloud/pull/3633), In
Review.
2. **FE-603** (this PR) — FE sends `share_id`.
3. **[BE-899](https://linear.app/comfyorg/issue/BE-899)** (blocked by
FE-603) — BE flips `share_id` back to **required** after FE-603 deploys.
Steps 1 and 2 ship in order: BE-898 → FE-603 → BE-899.
## Changes
- `workflowShareService.importPublishedAssets` now takes `shareId:
string` and types the request body with `ImportPublishedAssetsRequest`.
The body is parsed through `zImportPublishedAssetsRequest` before the
network call so future contract drift surfaces as a typecheck or zod
parse failure rather than a silent runtime 400.
- `useSharedWorkflowUrlLoader.ts` threads `payload.shareId` (already in
scope from `SharedWorkflowPayload`) into the import call.
- Unit tests assert `share_id` is sent and that an empty `share_id` is
rejected before fetch.
## Verification
- `pnpm typecheck` — clean
- `pnpm lint` — 0 errors / 0 warnings on changed files (3 pre-existing
warnings in unrelated files)
- `pnpm format` — applied
- `pnpm vitest run` on both affected files — 34/34 passing
### Live in-browser smoke test (Vite dev server)
Loaded the built module in the browser, intercepted `fetch`, and
exercised the new code path. The new `importPublishedAssets` produces
the correct wire format and zod rejects bad input before fetch:
```json
// Happy path — sent to /api/assets/import (POST)
{
"published_asset_ids": ["pa-1", "pa-2", "pa-3"],
"share_id": "share-abc"
}
```
```text
// Empty share_id — zod rejects, fetch is never called
[ { code: "too_small", minimum: 1, path: ["share_id"], message: "String must contain at least 1 character(s)" } ]
fetchCallsBetweenAttempts: 0
```
## Why typecheck didn't catch the original miss
`api.fetchApi(route: string, options?: RequestInit)` accepts the
standard DOM `RequestInit`, so `body` is just `BodyInit | null`. Once
the call site does `JSON.stringify({ ... })`, the inline object is
erased into a string and TS has no schema to enforce. There was no
`@ts-ignore` or `as any` — the generated types were simply never
imported. This PR plugs that one call site; the same pattern should be
applied wherever the frontend hits ingest endpoints (the broader hey-zod
migration gap mentioned in #bug-dump).
Reported in #bug-dump.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12055-fix-include-share_id-when-importing-published-assets-3596d73d365081c0a1c7e69102f5cfcc)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Add a shared `assert(condition, message)` utility in `src/base/` that
centralizes DEV-throw / Desktop-Sentry / nightly-toast / `console.error`
policy for invariant reporting across the codebase.
## Changes
- **`src/base/assert.ts`**: New `assert()` utility with
`setAssertReporter()` registration pattern
- `console.error` always fires on failure
- Throws `Error` in DEV mode (surfaces bugs immediately)
- Delegates to registered reporter otherwise (Sentry, toast, etc.)
- No imports from `platform/` — respects layer architecture (`base →
platform → workbench → renderer`)
- **`src/main.ts`**: Registers Sentry + nightly-toast reporter after
`Sentry.init()`
- **`src/scripts/changeTracker.ts`**: Migrates
`reportInactiveTrackerCall()` to use `assert()`, removing inline
`Sentry.captureMessage` + `console.warn` calls
- **`src/scripts/changeTracker.test.ts`**: Mocks `@/base/assert` to
prevent DEV-mode throws in existing no-op tests
## Testing
### Automated
- `src/base/assert.test.ts` — 6 tests covering: no-op on true,
console.error on false, DEV throw, non-DEV no-throw, reporter
invocation, reporter not called on true
- `src/scripts/changeTracker.test.ts` — 16 tests all pass (pre-existing)
- Coverage: 100% for assert.ts
### E2E Verification Steps
1. Run `pnpm test:unit` — all tests pass
2. Build the app and open browser devtools
3. In DEV mode: trigger a lifecycle violation (call an inactive tracker
method) — should see error thrown in console
4. In production build: same trigger — should see `console.error` only,
no throw
## Review Focus
- `setAssertReporter()` is called in `main.ts` once at startup —
appropriate for a singleton reporter. In tests that import `assert`, the
reporter is reset to a no-op in `afterEach`.
- Layer architecture respected: `base/assert.ts` has zero imports, upper
layers wire in side effects via `setAssertReporter()`.
Fixes#11373
<!-- Pipeline-Ticket: pick-issue-3410 -->
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11824-feat-3410-add-centralized-assert-utility-in-src-base-3546d73d3650819d96afdf4018161c26)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
## Summary
`openJobWorkflow` (Job Queue → "Open Workflow in New Tab") had no error
handling around `workflowService.openWorkflow`. When workflow JSON
contained nodes that fail `LiteGraph.configure()` (e.g. rgthree
`DisplayAny`, stale `GetNode/SetNode aux_id`), the menu action appeared
to do nothing — either an early return after a generic "Load Workflow
Error" dialog (`app.ts:1340-1347`) or a context-less toast from the
surrounding `wrapWithErrorHandlingAsync`.
This PR catches the error inside `openJobWorkflow` and routes it to
`dialogService.showErrorDialog` with a Job-Queue-specific `reportType`
so users get a clear, actionable message tied to the action they
invoked.
- Fixes#8841
- Linear:
[FE-215](https://linear.app/comfyorg/issue/FE-215/open-workflow-from-frontend-not-working)
## Red-Green Verification
| Commit | CI Status | Run |
|--------|-----------|-----|
| `test: add failing test for openJobWorkflow swallowing load errors`
(a422b392d) | 🔴 failure |
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25531063427 |
| `fix: surface error dialog when Open Workflow from Job Queue fails`
(86b2a3a9) | 🟢 success |
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25531374096 |
## Test Plan
- [x] CI red on test-only commit
- [x] CI green on fix commit
- [ ] Manual: load a workflow containing rgthree `DisplayAny` from Job
Queue → "Open Workflow in New Tab" → confirm error dialog appears with
Job-Queue context instead of nothing
<img width="972" height="662" alt="Screenshot 2026-05-13 at 5 58 31 PM"
src="https://github.com/user-attachments/assets/bf1d8d96-85b7-47a8-89d8-b3bb69a526cb"
/>
## Summary
Replaces #12164.
Right-clicking a Vue node, using the selection toolbox More Options
menu, or clicking the selection toolbox Node Info button now opens the
right-side Info tab only when the new-menu UI makes that panel
available. Legacy-menu contexts hide the no-op action even when the
legacy node library design is selected; node-library help remains
isolated to the node library itself. The existing
`selection_toolbox_node_info_opened` telemetry fires only after the
toolbox button successfully opens node info. No new context-menu
telemetry event is added in this PR.
## Changes
- **What**: Share the node-info availability/action path across the
context menu and selection toolbox, keep legacy-menu state out of the
right-side panel public store API, tighten node-info settings tests, and
add unit plus E2E regression coverage for new-menu and legacy-menu
modes.
- **Dependencies**: None
## Review Focus
Confirm the node context menu, selection toolbox direct Info button, and
selection toolbox More Options entry all respect right-side panel
availability, including legacy menu + legacy node library mode, while
node-library help behavior remains isolated to the node library.
## Validation
- Self-review: checked production path, unit mocks, and Playwright
coverage; only gap found was weak E2E coverage for the toolbox direct
Info path, now strengthened.
- `pnpm test:unit -- src/composables/graph/useSelectionState.test.ts
src/components/graph/SelectionToolbox.test.ts
src/components/graph/selectionToolbox/InfoButton.test.ts`
- `pnpm test:browser:local -- --project=chromium
browser_tests/tests/selectionToolboxActions.spec.ts
browser_tests/tests/selectionToolboxSubmenus.spec.ts
browser_tests/tests/vueNodes/interactions/node/contextMenu.spec.ts
--grep "info button opens the right-side info tab|info button is
hidden|hides Node Info|should open node info"`
- `pnpm typecheck:browser`
- `pnpm exec oxlint --type-aware
browser_tests/tests/selectionToolboxActions.spec.ts`
- `pnpm exec eslint --cache --no-warn-ignored
browser_tests/tests/selectionToolboxActions.spec.ts`
- `pnpm exec oxfmt --check
browser_tests/tests/selectionToolboxActions.spec.ts`
- `git diff --check`
- Commit hooks: lint-staged + `pnpm typecheck` + `pnpm
typecheck:browser`
- Push hook: `knip --cache` (existing tag hint only)
## Screenshots (if applicable)
Before
https://github.com/user-attachments/assets/4b1f6ddb-a01c-4958-81ab-36167f434e59https://github.com/user-attachments/assets/83433f0d-24f1-46b7-a81d-f0f065812496
After
https://github.com/user-attachments/assets/30bd61e5-f8d4-48b7-97e0-26c93e3cb362https://github.com/user-attachments/assets/afce9f51-a43d-434f-a006-6b357a61ac8f
---------
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Clear missing media validation errors after paste/drop media uploads by
emitting the existing widget-change event path.
## Changes
- **What**: Emit `node.onWidgetChanged` after image/video upload
completion updates the file combo widget.
- **What**: Emit the same widget-change path after Load Audio upload
completion.
- **What**: Add unit coverage for upload completion emitting
`onWidgetChanged` and for missing media clearing through that existing
hook path.
- **What**: Add E2E coverage for Load Image drag/drop and paste clearing
validation rings, with red/green verified from a fresh `main` base.
- **Dependencies**: None.
## Review Focus
Please check that paste/drop upload paths now reuse the existing
widget-change error-clearing path instead of expanding `widget.callback`
patching.
Also check the Load Image E2E helper path for synthetic paste/drop
behavior.
Supersedes #12207.
Ref: FE-687
## Screenshots
Before
https://github.com/user-attachments/assets/2cee52bc-b1c8-4dff-8a02-5b18a69ae639
After
https://github.com/user-attachments/assets/e1ecd147-1d8a-470e-b77d-13345d473ef3
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12212-fix-clear-media-upload-errors-via-widget-change-35f6d73d365081bcb1a0dfc042d417eb)
by [Unito](https://www.unito.io)
## Problem
The GH Pages coverage deploy has been failing since #11291 merged —
every `CI: E2E Coverage` workflow run errors out and
https://comfy-org.github.io/ComfyUI_frontend/ returns 404.
Additionally, two correctness/security issues were identified in the
workflow (filed as #11374 and #11375).
## Changes
1. **`--ignore-errors source` on genhtml** — merged LCOV data includes
paths like `localhost-8188/assets/main-BRkC1B8m.js` from Playwright V8
coverage instrumented runtime bundles that don't exist as source files
in CI, causing genhtml to error out
2. **Pin checkout to `workflow_run.head_sha`** — in `workflow_run`
context, the default checkout ref points to the default branch, not the
commit that triggered the upstream run; genhtml could annotate against
wrong source files (#11375)
3. **Gate deploy on `event == 'push'`** — a fork branch named `main`
could satisfy the branch check and overwrite production coverage; adding
the event guard prevents this (#11375)
4. **Include workflow run link in placeholder HTML** — when no coverage
data is available, the placeholder page now links back to the workflow
run for debugging (#11374)
## Fixes
- Fixes the GH Pages 404 caused by #11291
- Fixes#11374
- Fixes#11375
Implements the AUDIT-LG verdicts (#12223, #12224, #12225) as a single
deletion PR off main.
> **DRAFT — sequencing.** Per the AUDIT-LG framing doc, deletions land
**after Phase B ECS migration** (Alex's #11939 + #11811). Open early to
capture the diff and let CI cascade-flag any unused-export fallout. Flip
to ready-for-review post-Alex-rebase.
## Status
| commit | scope | status |
|---|---|---|
| 1 | Delete 6 LGraph stepping hooks (`onAfterStep`, `onBeforeStep`,
`onPlayEvent`, `onStopEvent`, `onAfterExecute`, `onExecuteStep`) + their
dispatch sites in `start()`/`stop()`/`runStep()` | ✅ landed (this
commit) |
| 2 | Delete the rest of the dead executor cluster
(`start()`/`stop()`/`runStep()`/`sendEventToAllNodes()` + state fields +
`STATUS_*` constants) | follow-up |
| 3 | Delete `LGraphNode` dead event hooks (~22 fields per #12224) |
follow-up |
| 4 | Delete trigger/action subsystem (~22 symbols, #12223) | follow-up
|
| 5 | `ON_EVENT` deprecation cycle, release N (#12225) | follow-up |
## Verification (commit 1)
```
$ pnpm lint && pnpm format:check && pnpm knip
✓ format: All matched files use the correct format
✓ lint: pre-existing icon-name warnings only
✓ knip: no new unused exports flagged
```
## Verdicts the deletion is grounded in
- AUDIT-LG.7 master verdict table (146 surfaces classified DELETE-NOW /
DEPRECATE / KEEP)
- AUDIT-LG.9 per-symbol attribution sweep (confirmed zero functional
callers for the trigger cluster + the dead hooks)
For each surface in this PR:
- **`internal_count` from rg over `src/`, `browser_tests/`, `packages/`
excluding `lib/litegraph/`:** 0
- **External use from touch-points DB:** 0 (per AUDIT-LG.3 + AUDIT-LG.9
per-symbol attribution)
- **Host methods (`start()`/`stop()`/`runStep()`):** `@deprecated 'Will
be removed in 0.9'` already
The dispatch sites (`this.onAfterStep?.()` etc.) are inside the
deprecated host methods — removing the dispatchers does not change
observable behaviour because no listener is attached to begin with.
## Why batched, why draft
Per AUDIT-LG framing, the deletion sequences behind Alex's PR #11939
(ECS world-combo). Opening as draft lets CI run early and flags any
unused-export cascades the audit script missed. Each follow-up commit
will be its own atomic deletion (one feature per commit) so any single
one can be reverted in isolation if needed.
cc @drjkl @christian-byrne
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12228-refactor-litegraph-prune-dead-surfaces-AUDIT-LG-implementation-draft-35f6d73d365081e0b72cd292228e2ca6)
by [Unito](https://www.unito.io)
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
## Summary
Adds browser coverage for the missing-media runtime paths introduced by
#12069 and #12111:
- OSS: annotated `[output]` media is resolved from job history.
- Cloud: compact `[output]` media is resolved from output assets.
- OSS and Cloud: dropped video uploads do not surface a missing-media
error while the upload is still in progress.
This PR is now rebased directly onto `main`; the parent fix PRs have
been squash-merged, so this branch only contains the E2E coverage
commit.
## Test Fixtures
- Adds workflow fixtures for OSS spaced output annotations and Cloud
compact output annotations.
- Adds a small plain MP4 fixture for video drag/drop upload coverage.
## Validation
- `pnpm exec oxfmt --check
browser_tests/tests/propertiesPanel/errorsTabMissingMediaRuntime.spec.ts
browser_tests/assets/missing/missing_media_cloud_output_annotation.json
browser_tests/assets/missing/missing_media_output_annotations.json`
- `pnpm typecheck:browser`
- `pnpm exec oxlint
browser_tests/tests/propertiesPanel/errorsTabMissingMediaRuntime.spec.ts
--type-aware`
- `git diff --check origin/main..HEAD`
Note: before the rebase, the Cloud project target for this spec passed
locally. Local OSS project execution against the currently running Cloud
dist did not reach the test body because `ComfyPage.waitForAppReady`
timed out in `beforeEach`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12126-test-cover-missing-media-runtime-sources-35d6d73d365081f0a981c02f33c0ff84)
by [Unito](https://www.unito.io)
- Adds functions to SubgraphHelper to perform widget promotion by
standard user means
- Right Click -> Promote
- Properties Panel
- Adds new slot fixture code that works with simple `locator.dragTo`
operations.
- Adds multiple subgraph tests with a focus on historically difficult
operations.
- Fixes a bug where the litegraph `node.selected` state would not be
unset when switching graphs. This made it so 'Selecting a node ->
leaving subgraph -> re-enter subgraph -> right click on node' would fail
to select the node because it is marked as already selected.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11806-Add-helper-functions-for-widget-promotion-3536d73d365081f58dd9cd730c1a91a9)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
API and other legacy JSON generated by python `json.dumps` can contain
`NaN` and `Infinity` which cannot be parsed with JS `JSON.parse`. This
adds regex to replace these invalid tokens with `null`.
## Changes
- **What**:
- add regex replace on bare NaN/infinity tokens after JSON.parse fails
- update call sites
- tests
## Review Focus
- The regex should only rewrite bare NaN/-Infinity/Infinity and not
touch string values or other invalid tokens.
- A small regex was chosen over JSON5 due to package size (30.3kB
Minified, 9kB Minified + Gzipped) or a manual parser due to the
unnecessarily complexity vs a single regex replace.
- The happy path is run first, the safe parse is only executed if that
failed, meaning no overhead the vast majority of the time and no
possiblity of corrupting valid workflows due to a bug in the fallback
parser
- Multiple call sites had to be updated due to pre-existing architecture
of the various parsers, an issue for unifying these is logged for future
cleanup
- New binary fixtures added for validating e2e import using real files
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12217-fix-add-support-for-parsing-python-generated-json-with-NaN-infinite-35f6d73d365081889fc7f4af823f29c1)
by [Unito](https://www.unito.io)
## Summary
The position of a link relative to its slot was able to drift on load,
due to widgets inside a node being able to resize without triggering an
node-level resize event (min-height node with space at the bottom could
have widgets expand into free space, causing misalignment).
Recreation:
1. Add KSampler
2. Add Float
3. Connect Float to KSamper.denoise
4. Reload workflow (F5)
5. Observe misalignment
## Changes
- **What**:
- track widget grid element as signal only that triggers resync
- node bound calculations skipped for widget signals
- prevent setDirty on non-graph nodes (e.g. LGraphNodePreview)
- tests
## Review Focus
This is a small focused approach to fix the reported issue - it does not
address the underlying issue of the layout not being a SSOT. This fix is
a small bandaid and investigation into resolving the layout SOT issue is
not impacted by this.
## Screenshots (if applicable)
Before:
<img width="673" height="374" alt="image"
src="https://github.com/user-attachments/assets/2d34b8e3-0731-4fd2-8553-4dd429010ced"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12214-fix-resolve-widget-input-link-position-drift-on-reload-35f6d73d3650814eb31bebb3042ff58b)
by [Unito](https://www.unito.io)
## Summary
When a user who has not used the app before first loads up, they are
presented with the template selection dialog. This conflicts when the
first-time user visits the app via a share link - both the share &
template dialog are triggered.
## Changes
- **What**:
- Skip the templates browser when share param is in URL
- Tests
- Add `url` to `setup`/`goto` to allow specifying the `share` parameter
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12024-fix-prevent-first-user-template-popup-when-following-shared-link-3586d73d365081cbbcecdba45a1ad1ea)
by [Unito](https://www.unito.io)
*PR Created by the Glary-Bot Agent*
---
## Summary
Adds the Anthropic logo to the partner-node icon set so nodes whose
category ends in `Anthropic` (e.g. the Claude node added in
Comfy-Org/ComfyUI#13867 with `category="api node/text/Anthropic"`)
render the correct provider badge in the node library.
## Changes
- `packages/design-system/src/icons/anthropic.svg` — new auto-discovered
partner icon (Anthropic A glyph, sourced from
[lobehub/lobe-icons](https://github.com/lobehub/lobe-icons), uses
`fill="currentColor"` for theme adaptation)
- `src/utils/categoryUtil.ts` — register Anthropic's brand coral
`#D97757` as the badge border color
- `packages/design-system/src/css/style.css` — add `anthropic` to the
dynamic comfy-icon safelist so Tailwind/Iconify emits CSS for
`icon-[comfy--anthropic]` in production builds
- `src/utils/categoryUtil.test.ts` — regression tests for
`getProviderIcon('Anthropic')` and `getProviderBorderStyle('Anthropic')`
## Verification
- `pnpm typecheck` ✓
- `pnpm lint` ✓ (0 errors; 3 pre-existing warnings in unrelated files)
- `pnpm format:check` ✓
- `pnpm test:unit -- src/utils/categoryUtil.test.ts` ✓ (13/13)
- `pnpm build` ✓ — confirmed `comfy--anthropic` class is emitted into
`dist/assets/index-*.css`
- Manual visual check via Playwright against `pnpm dev`: injected `<i
class="icon-[comfy--anthropic]">` elements at badge size (10px) and 48px
alongside the existing OpenAI and BFL icons and confirmed the Anthropic
"A" glyph renders correctly in coral. See screenshot.
End-to-end visual verification of the live badge in the node library
requires Comfy-Org/ComfyUI#13867 to land first (the Claude node is what
produces the `Anthropic` category that triggers the icon lookup).
Related: Comfy-Org/ComfyUI#13867
## Screenshots

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12216-feat-add-Anthropic-partner-icon-35f6d73d36508133a134fcafaf72f4f8)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
Rework the painter always hands the backend a valid asset reference:
- Drop the `hasStrokes` flag and the `isCanvasEmpty` check.
- `serializeValue` falls back to the existing `modelValue` when the
canvas element is transiently unmounted, reuses the cached upload when
not dirty and a value is present, and otherwise uploads the current
canvas (a fully transparent PNG is a valid no-op mask, Painter's Python
`execute()` treats painter_alpha=0 the same as "no mask painted").
- `handleClear` now also clears `modelValue` so a user-initiated clear
doesn't resurrect a stale upload on the next serialize.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12196-FE-566-fix-Painter-mask-submission-edge-cases-on-cloud-35e6d73d365081dd8856ddb785952526)
by [Unito](https://www.unito.io)
*PR Created by the Glary-Bot Agent*
---
## Summary
Disables the Free tier on the public marketing website
(`comfy.org/cloud/pricing` and `comfy.org/cloud`) behind a single
boolean flag so re-enabling is a one-line change.
The Free tier was already removed from the Comfy Cloud sign-up flow;
this PR removes the matching promotional surfaces on the marketing site
so users hit the paywall directly.
## Changes
- **New** `apps/website/src/config/features.ts` — exports
`SHOW_FREE_TIER` (currently `false`). Flip to `true` to restore the
previous UX.
- **`apps/website/src/components/pricing/PriceSection.vue`** — when
`SHOW_FREE_TIER` is `false`:
- drops the Free plan card from the pricing array
- desktop grid collapses from `lg:grid-cols-4` to `lg:grid-cols-3`
- Standard plan's "Everything in Free, plus:" intro is replaced with an
aria-hidden spacer so the three remaining cards stay vertically aligned
- **`apps/website/src/components/product/cloud/PricingSection.vue`** —
hides the "Start free. Upgrade when you're ready." tagline on the
`/cloud` pricing teaser.
- **New** `apps/website/e2e/pricing.spec.ts` — three @smoke tests
asserting the paid tiers + Enterprise are visible and that all Free-tier
surfaces are absent.
All translation strings (`pricing.plan.free.*`, `cloud.pricing.tagline`)
are retained so re-enabling requires no copy work.
## Verification
- `pnpm typecheck` — clean (0 errors, 0 warnings; pre-existing hint
unrelated)
- `pnpm lint` / `oxfmt` — clean
- `pnpm test:unit` — 30/30 passing
- Playwright e2e (desktop project) — `pricing.spec.ts` 3/3 passing,
`cloud.spec.ts` 13/13 still passing
- Visual: desktop and mobile pricing pages render with balanced 3-column
layout; `/cloud` teaser card still proportional.
Screenshot below.
## Screenshots

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12165-feat-website-hide-Free-tier-behind-SHOW_FREE_TIER-flag-35e6d73d36508164b4dfcfe9fee6b5e7)
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>
## Automated Ingest API Type Update
This PR updates the Ingest API TypeScript types and Zod schemas from the
latest cloud OpenAPI specification.
- Cloud commit: 9619326
- Generated using @hey-api/openapi-ts with Zod plugin
These types cover cloud-only endpoints (workspaces, billing, secrets,
assets, tasks, etc.).
Overlapping endpoints shared with the local ComfyUI Python backend are
excluded.
---------
Co-authored-by: MillerMedia <7741082+MillerMedia@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
- Extract `isCivitaiHost` private helper from `isCivitaiModelUrl` in
`formatUtil.ts` for DRY hostname checking
- Add `isCivitaiUrl` exported function for hostname-only Civitai URL
detection (distinct from `isCivitaiModelUrl` which also validates the
path format)
- Refactor `getSourceName` in `assetMetadataUtils.ts` to use the shared
`isCivitaiUrl` instead of inline duplicate hostname checks
- Add tests for `isCivitaiUrl` covering `.com`, `.red`, subdomain, and
invalid URL cases
## Changes
- `packages/shared-frontend-utils/src/formatUtil.ts` — add
`isCivitaiHost` private helper + export `isCivitaiUrl`; refactor
`isCivitaiModelUrl` to use helper
- `packages/shared-frontend-utils/src/formatUtil.test.ts` — add
`isCivitaiUrl` test suite
- `src/platform/assets/utils/assetMetadataUtils.ts` — import
`isCivitaiUrl` from `@/utils/formatUtil`; remove inline hostname logic
from `getSourceName`
## Testing
### Automated
- Added `isCivitaiUrl` test suite (6 cases: `.com`, `.red`, subdomains,
non-Civitai, invalid URL)
- All 71 existing `formatUtil` tests pass
- All 53 existing `assetMetadataUtils` tests pass (behavior preserved)
- TypeScript typecheck passes
### E2E Verification Steps
1. Run unit tests: `npx vitest run
packages/shared-frontend-utils/src/formatUtil.test.ts
src/platform/assets/utils/assetMetadataUtils.test.ts`
2. Expected: all tests pass
3. Verify `getSourceName('https://civitai.red/models/123')` returns
`'Civitai'`
4. Verify `isCivitaiUrl('https://civitai.com/models/any-path')` returns
`true`
5. Verify `isCivitaiModelUrl` still rejects non-API paths while
`isCivitaiUrl` accepts them
## Review Focus
`isCivitaiUrl` (new, hostname-only) vs `isCivitaiModelUrl` (existing,
hostname+path format): `getSourceName` needs to recognize ANY Civitai
URL as a source, so using `isCivitaiModelUrl` directly would incorrectly
reject valid browse URLs like `civitai.com/models/123`.
Closes#11357
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11822-refactor-deduplicate-Civitai-hostname-logic-in-getSourceName-3546d73d36508110974ccc3b7384d82b)
by [Unito](https://www.unito.io)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Adds the `--type-aware` so that the typechecks performed by precommit
hooks have parity with the results output by a full `pnpm lint`
Most notably, unawaited promises would not be caught by the precommit
hooks prior to this PR.
```
× typescript-eslint(no-floating-promises): Promises must be awaited, add void operator to ignore.
╭─[browser_tests/fixtures/utils/vueNodeFixtures.ts:45:5]
44 │ async select() {
45 │ this.header.click()
· ───────────────────
46 │ }
╰────
```
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12203-Fix-pre-commit-linter-skipping-type-checks-35e6d73d365081a4adade833294df7ed)
by [Unito](https://www.unito.io)
*PR Created by the Glary-Bot Agent*
---
## Summary
The careers page at comfy.org/careers was linking every role to its
Ashby application form (`.../{id}/application`) instead of the job
description page (`.../{id}/`). Users expect to first read the role
description, not land on the submit-resume page.
Ashby's job board API returns both `jobUrl` (description) and `applyUrl`
(application form). `toDomainRole` was preferring `applyUrl`; this PR
switches to `jobUrl` and renames the `Role` field accordingly so the
field name matches its meaning.
## Changes
- `apps/website/src/utils/ashby.ts`: use `job.jobUrl` directly instead
of `job.applyUrl ?? job.jobUrl`.
- `apps/website/src/data/roles.ts`: rename `Role.applyUrl` →
`Role.jobUrl`.
- `apps/website/src/components/careers/RolesSection.vue`: update the `<a
:href>` binding.
- `apps/website/src/data/ashby-roles.snapshot.json`: regenerated
fallback snapshot — URLs stripped of `/application`, `id`s recomputed
from the new URLs.
- Unit + E2E tests updated; new E2E assertion that links do not end in
`/application` prevents regressions.
The Ashby schema (`ashby.schema.ts`) still accepts `applyUrl` since the
API returns it — we just no longer consume it.
## Verification
- `pnpm test:unit` — 70/70 pass
- `pnpm typecheck` — 0 errors
- `pnpm build` — succeeds; inspected `dist/careers/index.html`, all 19
Ashby links now point to description URLs and zero contain
`/application`
- Oracle code review — 0 issues
Fixes user report in #hiring-ideas (Slack).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12200-fix-website-link-careers-page-to-Ashby-job-description-not-application-form-35e6d73d3650815cbedadf974f7d3364)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Automated refresh of `apps/website/src/data/ashby-roles.snapshot.json`
from the Ashby job board API.
**Flow:**
1. `Release: Website` workflow ran (manual trigger).
2. This PR opens with the regenerated snapshot.
3. `CI: Vercel Website Preview` deploys a preview for review.
4. Merging to `main` triggers the production Vercel deploy.
The snapshot fallback in `apps/website/src/utils/ashby.ts` remains
intact: builds without `WEBSITE_ASHBY_API_KEY` continue to use the
committed snapshot.
Triggered by workflow run `25746888214`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12191-chore-website-refresh-Ashby-roles-snapshot-35e6d73d365081f4b2e1d802dd412a72)
by [Unito](https://www.unito.io)
Co-authored-by: Yourz <8287689+Yourz@users.noreply.github.com>
*PR Created by the Glary-Bot Agent*
---
## Summary
Adds a comfy.org page that lists every custom-node pack supported on
Comfy Cloud, with per-pack detail subpages. Data is fetched at build
time from `cloud.comfy.org/api/object_info` (gated by
`WEBSITE_CLOUD_API_KEY`), sanitized of user content, joined with public
registry metadata from `api.comfy.org/nodes`, and falls back to a
committed snapshot — mirroring the existing Ashby careers integration
pattern.
- Index: `/cloud/supported-nodes` (en) and
`/zh-CN/cloud/supported-nodes` (zh-CN)
- Detail: `/cloud/supported-nodes/[pack]` and
`/zh-CN/cloud/supported-nodes/[pack]`, generated via `getStaticPaths()`
from the same fetcher as the index so the two routes can never diverge.
## What's new
**Shared package (extracted)**
- `@comfyorg/object-info-parser` — Zod schemas (`zComfyNodeDef`,
`validateComfyNodeDef`), node-source classifier (`getNodeSource`,
`isCustomNode`, `CORE_NODE_MODULES`), and helpers (`groupNodesByPack`,
`sanitizeUserContent`). `src/schemas/nodeDefSchema.ts` and
`src/types/nodeSource.ts` become 1-line re-export shims; existing
imports keep compiling.
**Build-time pipeline**
- `apps/website/src/utils/cloudNodes.ts` — Ashby-style fetcher:
retry/backoff `[1s, 2s, 4s]`, 10 s timeout via AbortController, Zod
envelope + per-node validation, snapshot fallback, memoized via
module-level `inflight` promise.
- `apps/website/src/utils/cloudNodes.registry.ts` — Public registry
enrichment (no auth, batches of 50, single retry, soft-fail).
- `apps/website/src/utils/cloudNodes.ci.ts` — GitHub Actions annotations
+ step summary mirroring the Ashby reporter.
- `apps/website/src/utils/cloudNodes.build.ts` — Single
`loadPacksForBuild()` consumed by both index and detail pages so they
share one source of truth.
- `apps/website/scripts/refresh-cloud-nodes-snapshot.ts` — atomic-rename
refresh CLI that walks pack/node string fields with a user-content
extension regex *before* renaming the snapshot into place.
- Mandatory user-content sanitization strips uploaded filenames from
combo lists (`LoadImage`, `LoadImageMask`, `LoadImageOutput`,
`LoadVideo`, `LoadAudio` zeroed; any combo value matching
`/\.(png|jpe?g|webp|gif|mp4|mov|webm|wav|mp3|flac|ogg|safetensors|ckpt|pt)$/i`
filtered).
**Page + components**
- `apps/website/src/pages/cloud/supported-nodes.astro` (en) + zh-CN
twin.
- `apps/website/src/pages/cloud/supported-nodes/[pack].astro` detail
(en) + zh-CN twin, async `getStaticPaths` driven by
`loadPacksForBuild()`.
-
`apps/website/src/components/cloud-nodes/{HeroSection,PackGridSection,PackCard,PackBanner,NodeList,PackDetail}.vue`
— Vue 3.5 destructured props, `cn()` from `@comfyorg/tailwind-utils`,
design-system tokens only, no PrimeVue.
- Pack card name links to its detail page; banner uses the shared
`fallback-gradient-avatar.svg` asset (copied into
`apps/website/public/assets/images/`) when `banner_url` and `icon` are
missing.
- 25 new `cloudNodes.*` i18n keys in `en` + `zh-CN`.
**Tests**
- 33 unit tests in `@comfyorg/object-info-parser` (schemas, classifier,
sanitizer, grouping).
- 19 new website unit tests covering fetcher (10), CI reporter (6),
registry enrichment (3) — Ashby patterns mirrored.
- E2E: index smoke + search + banner + detail click-through + direct
visit + zh-CN parity.
## Required maintainer follow-up
GitHub Apps cannot push `.github/workflows/*` changes (push was rejected
with `refusing to allow a GitHub App to create or update workflow …
without workflows permission`), so the workflow edits prepared in this
branch were reverted in commit `9be2abce8`. The intended diffs are
documented as copy-paste-ready snippets in `apps/website/README.md`
under the new "Cloud nodes integration → CI wiring" section.
A maintainer must:
1. Provision `WEBSITE_CLOUD_API_KEY` in the repo secrets and the Vercel
project env.
2. Apply the `ci-website-build.yaml` and
`ci-vercel-website-preview.yaml` diffs documented in the README directly
to `main` (or as a follow-up commit on this branch with a maintainer
account).
The committed snapshot lets builds succeed without the secret while the
maintainer step is pending — pages render from
`apps/website/src/data/cloud-nodes.snapshot.json`.
## Self-review (Oracle)
Two warnings caught and fixed in commits `deba5ab02` and `99dfc3381`:
- Index/detail pages now share a single source of truth
(`loadPacksForBuild`), so a fresh fetch can't expose packs whose detail
routes weren't generated.
- Refresh script validates parsed snapshot fields *before* the atomic
rename, instead of regex-scanning the serialized JSON after the file is
already in place.
## Quality gates (local)
```
pnpm --filter @comfyorg/object-info-parser test → 33 passed
pnpm --filter @comfyorg/website test:unit → 42 passed
pnpm --filter @comfyorg/website typecheck → 0 errors
pnpm --filter @comfyorg/website build → 47 pages built (incl. 6 cloud-nodes routes)
pnpm lint → 0 errors (1 pre-existing warning in unrelated test file)
pnpm knip → 0 errors (1 pre-existing tag hint in unrelated file)
```
E2E (`pnpm --filter @comfyorg/website test:e2e`) is intended to be run
by the Vercel/CI pipelines.
## Manual verification
Built `dist/`, served locally on port 4321, drove with Playwright:
- `/cloud/supported-nodes` renders both pack cards, search input, sort
dropdown
- `/cloud/supported-nodes/comfyui-impact-pack` renders the metadata grid
(publisher, downloads, stars, version, license, last updated) and 3
categorized node sections with 5 nodes total
- `/zh-CN/cloud/supported-nodes` localizes hero (`Comfy Cloud 上的自定义节点`),
label (`云端节点目录`), search placeholder (`搜索节点包或节点名称`), sort
- `/zh-CN/cloud/supported-nodes/comfyui-controlnet-aux` localizes every
metadata label (`查看仓库`, `发布者`, `下载量`, `GitHub 星标`, `最新版本`, `许可证`,
`最后更新`) and renders dates with `Intl.DateTimeFormat('zh-CN')`
(`2026年4月27日`)
- Search input narrows pack count from 2 to 1 when typing `impact`
(verified via DOM count)
Banners render the shared `fallback-gradient-avatar.svg` when the
snapshot's image URL doesn't resolve — expected in the local sandbox.
## Preview URL (after CI completes)
`https://comfy-website-preview-pr-{N}.vercel.app/cloud/supported-nodes`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11903-feat-cloud-nodes-catalog-at-cloud-supported-nodes-3566d73d36508194afdec5f389897585)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
- Prevent missing media detection from scanning media loader nodes while
a drag/drop, paste, or file-select upload is still in progress.
- Align LoadAudio with the existing media upload lifecycle by setting
`node.isUploading`, blocking concurrent uploads, and clearing the flag
after upload completion.
- Keep added-node model and missing-node scans on the original
one-microtask path, while deferring added-node media scanning by one
extra microtask so upload handlers can mark transient upload state
before the scan reads widget values.
## Why
Drag/drop and paste can create media loader nodes before the backing
upload has settled. During that short window, the widget may contain a
local filename that is not yet backend-resolvable, so missing media
detection can surface a false missing asset. Refreshing works because
the upload has completed by then.
## Follow-up
- E2E coverage for this upload race will be handled in a follow-up PR
together with E2E coverage for the annotated output-media path changes
from #12069.
## Validation
- `pnpm format`
- `pnpm lint`
- `pnpm typecheck`
- `pnpm vitest run src/composables/graph/useErrorClearingHooks.test.ts
src/platform/missingMedia/missingMediaScan.test.ts
src/extensions/core/uploadAudio.test.ts
src/composables/node/useNodeImageUpload.test.ts`
- Re-ran `pnpm typecheck` after rebasing onto latest `main`
- Pre-push `knip` hook passed
Fixes FE-620
## Screenshots
Before
https://github.com/user-attachments/assets/db7891de-a4b5-4cde-aa76-6340e6cdf7b2
After
https://github.com/user-attachments/assets/9b99bb13-0d5b-4ff7-8f52-66eea6e417ec
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12111-fix-suppress-missing-media-scan-during-uploads-35b6d73d365081f3b54eed02874ccaa4)
by [Unito](https://www.unito.io)
## Summary
Route the templates dialog search through `FormSearchInput`'s debounced
searcher so per-keystroke work no longer trips the heavy filter/render
path.
## Changes
- **What**: `WorkflowTemplateSelectorDialog` now uses `FormSearchInput`
instead of `SearchInput`. The raw input is bound to a local ref; the
actual `searchQuery` consumed by `useTemplateFiltering` is only written
after the input debounce settles, so dependent computeds (notably
`shouldUsePagination`, which used to flip on every keystroke and force a
full grid rebuild) stay stable while typing.
- **What**: `FormSearchInput` gains optional `debounceMs` (default
`250`) and `debounceMaxWaitMs` (default `1000`) props. Existing callers
are unchanged; the templates dialog passes `400` / `4000` to match the
feel tuned in this PR.
## Review Focus
- Reset path: `searchQuery` is still owned by `useTemplateFiltering` and
cleared by `resetFilters`; a watch syncs the visible input back to empty
when that happens.
- `FormSearchInput` is currently under `src/renderer/...` but already
imported by workbench-level components (rightSidePanel tabs). This PR
follows that existing precedent rather than relocating the component.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12183-perf-debounce-template-search-input-to-keep-typing-responsive-35e6d73d365081b7a11ec4a84323095f)
by [Unito](https://www.unito.io)
*PR Created by the Glary-Bot Agent*
---
Enables the oxlint rule `vitest/consistent-each-for` (configured to
prefer `.for` for `test`, `it`, `describe`, and `suite`) and migrates
every `.each` parameterized test in the repo to `.for`. Using `.for`
avoids accidentally splatting tuple elements into separate callback
arguments and exposes `TestContext` as the second callback argument.
The first commit covers the 38 lint-detected files (88 callsites):
renames `.each` → `.for` and updates callback signatures to destructure
when the data is an array of tuples (objects/primitives already work
unchanged with `.for`).
The follow-up commit addresses code review feedback: oxlint's rule does
not recognize `test.each` on extended test bases
(`baseTest.extend(...)`) and skips files in `ignorePatterns`
(`src/extensions/core/*`). These were converted manually so the policy
is uniform across the codebase.
## Verification
- `node_modules/.bin/oxlint src` — 0 errors, 0 `consistent-each-for`
violations
- `pnpm typecheck` — passes
- `pnpm test:unit` — all modified test files pass; pre-existing
environmental flakes (`GraphView.test.ts`, `ColorWidget.test.ts`, etc.,
unchanged here and flaky on `main` in this sandbox) are unrelated
- `pnpm lint` / `pnpm knip` — clean
- Manual verification: 362 tests across 6 representative converted
suites re-run in an interactive shell — all passing
Manual UI verification (Playwright/screenshots) is not applicable:
changes are test-file-only refactors with no production runtime or UI
behavior change.
## Notes on `.for` semantics
- Array-of-tuples (`[[a, b], ...]`) passes the tuple as a single arg, so
callbacks were changed from `(a, b) => …` to `([a, b]) => …`.
- Array-of-objects (`[{a}, …]`) already used destructuring — unchanged.
- Array-of-primitives (`['a', …]`) — callback signature unchanged.
- A handful of complex cases use a small `type Case = [...]` alias plus
`it.for<Case>([...])` to preserve tuple inference where TS narrowed
unions otherwise broke parameter types.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12161-test-enable-vitest-consistent-each-for-and-migrate-each-for-35e6d73d3650810c9417e07bdd9f27a2)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
Sidebar buttons rendered literal i18n keys (e.g.
`sideToolbar.labels.assets`) on a fresh install when the user's
`navigator.language` base tag wasn't one of the 12 shipped locales —
German/Italian/Polish/Dutch/Brazilian-Portuguese users among others.
## Changes
- **What**: Add `resolveSupportedLocale()` that tries the full BCP-47
tag first (preserves `zh-TW`, `pt-BR`), then the base tag, then `'en'`.
Wire through both entry points (`createI18n`'s initial locale,
`Comfy.Locale`'s `defaultValue`) and clamp inside `loadLocale`,
propagating the resolved tag to `GraphView` so a stale stored
`Comfy.Locale='de'` from older builds also recovers.
- **Side benefit**: Brazilian Portuguese users were previously falling
through `pt-BR` → `pt` (unshipped) → broken. The full-tag-first lookup
now correctly lands them on the `pt-BR` bundle.
- **Breaking**: None.
- **Dependencies**: None.
## Root Cause
Three-link chain:
1. `Comfy.Locale`'s default was `() => navigator.language.split('-')[0]
|| 'en'`. German → `'de'` (unshipped).
2. `loadLocale('de')` silently `console.warn`'d and returned without
throwing.
3. `GraphView` then ran `i18n.global.locale.value = 'de'` anyway.
4. `st(key, fallback) = te(key) ? t(key) : fallback`. vue-i18n's `te()`
checks **only** the current locale and ignores `fallbackLocale` — every
key missed → `st()` returned the literal key string.
Two pathways reached the broken state (defaultValue path, and
unset-setting path through `createI18n`'s own `navigator.language`
snapshot); the new helper closes both.
## Review Focus
- `loadLocale` now returns `SupportedLocale` (was `void`). Old `void`
callers continue to compile; the only change is `GraphView` consuming
the return value.
- Unit-tested in `src/i18n.test.ts` (added `resolveSupportedLocale`
block + updated the `loadLocale` unsupported-locale case from "warn" to
"clamp to en").
- Self-reproduced via Playwright with `navigator.language='de-DE'` +
fresh-install state on both `main` (shows the bug) and this branch
(shows the fix). Spec saved at
`temp/scripts/issue-10563-locale-bug.spec.ts`.
Fixes#10563
FE-480 — https://linear.app/comfyorg/issue/FE-480
## Screenshots
**Before** (from #10563, on `main`):
<img width="258" height="399" alt="Sidebar with literal i18n keys"
src="https://github.com/user-attachments/assets/098d1d76-8e89-4237-813f-5f030b34e51e"
/>
**After** (this branch, same `navigator.language='de-DE'`):
<img width="367" height="793" alt="Screenshot 2026-04-28 at 2 07 38 PM"
src="https://github.com/user-attachments/assets/9d279de3-50a8-4774-999f-ab4c3018a9ef"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11712-fix-i18n-clamp-unsupported-browser-locales-to-a-shipped-tag-3506d73d3650812f89d2f0fe3199de3a)
by [Unito](https://www.unito.io)
## Summary
Add entries to `MODEL_NODE_MAPPINGS` so the model browser's "Use" button
correctly creates a loader node for five model directories that
currently have no mapping.
## Changes
- **What**: 5 new entries in
`src/platform/assets/mappings/modelNodeMappings.ts`:
- `background_removal` → `LoadBackgroundRemovalModel` /
`bg_removal_name` (ComfyUI v0.21+ core)
- `frame_interpolation` → `FrameInterpolationModelLoader` / `model_name`
(ComfyUI v0.21+ core)
- `film` → `FILM VFI` / `ckpt_name` (ComfyUI-Frame-Interpolation)
- `ultralytics/bbox` → `UltralyticsDetectorProvider` / `model_name`
(ComfyUI-Impact-Pack)
- `ultralytics/segm` → `UltralyticsDetectorProvider` / `model_name`
(ComfyUI-Impact-Pack)
- **Breaking**: none
## Review Focus
- Node class names and input keys were cross-checked against the ComfyUI
v0.21.0 source and the published Impact-Pack / Frame-Interpolation node
definitions
- Both `ultralytics/bbox` and `ultralytics/segm` map to the same node
(`UltralyticsDetectorProvider`); its `model_name` combo accepts values
from both subdirectories (`bbox/...` and `segm/...`)
- `film` and `frame_interpolation` are separate directories serving
different node packs — keeping them as distinct entries rather than
collapsing under a parent
## Test plan
- [ ] In the model browser, clicking "Use" on `birefnet.safetensors`
creates a `LoadBackgroundRemovalModel` node with the model preselected
- [ ] Same for one model in each of: `frame_interpolation`, `film`,
`ultralytics/bbox`, `ultralytics/segm`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12151-feat-add-model-to-node-mappings-for-new-model-directories-35d6d73d365081ff834bf6eb610da160)
by [Unito](https://www.unito.io)
Adds an E2E test that loads a Primitive wired to KSampler.sampler_name,
presses 'r' (Refresh Node Definitions), and asserts the primitive's
combo options stay a non-empty array containing the connected input's
values. Reproduces the regression this PR fixes — before the fix, the
options array was clobbered with undefined.
## Problem
`checkIsNewUser()` in `useNewUserService` only checked legacy pre-V1
localStorage keys (`workflow`, `Comfy.PreviousWorkflow`) to determine if
a user had prior workflow history. A returning user who had only ever
used the V1 or V2 draft persistence system would have neither of those
keys set, causing `isNewUser()` to return `true` and the getting-started
tab to appear in the workflow templates dialog after a settings reset.
## Solution
Extend the check to also cover:
- **V1 draft store keys**: `Comfy.Workflow.Drafts`,
`Comfy.Workflow.DraftOrder`
- **V2 draft index key**: `Comfy.Workflow.DraftIndex.v2:personal`
The `personal` scope is hardcoded for the V2 check because at the time
`checkIsNewUser()` runs, the cloud workspace ID (stored in
sessionStorage) may not be set yet. This is fine — any genuine new user
will have no personal workspace index regardless.
The original legacy keys are preserved for users who may still have them
from older installs.
## Tests
Added three new test cases covering V1 draft store keys, V1 draft order
key, and V2 draft index key.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11728-fix-detect-V1-V2-draft-storage-keys-in-new-user-check-3506d73d3650819ca4cfc8e83d95c258)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
*PR Created by the Glary-Bot Agent*
---
Adds a new interactive demo page at
`comfy.org/demos/community-workflows` for the [Explore and Use a
Community Workflow from the
Hub](https://app.arcade.software/flows/mqZh17oWDuWIyhK0xwEV/view) Arcade
walkthrough.
Built on top of the demo infrastructure merged in #11436.
## Changes
- `apps/website/src/config/demos.ts` — register the new demo
- `apps/website/src/i18n/translations.ts` — add en + zh-CN strings
(title, description, transcript)
- `apps/website/public/images/demos/community-workflows-og.png` —
1200×630 OG image so email/social previews render correctly
- `apps/website/public/images/demos/community-workflows-thumb.webp` —
1280×720 WebP thumbnail
- `apps/website/e2e/demos.spec.ts` — refactored to iterate `demos` from
config so every demo (current + future) is exercised in both en and
zh-CN, and the iframe `src` is asserted to contain the correct Arcade ID
Adding a new demo only requires editing `demos.ts` + `translations.ts`
going forward; the e2e refactor is a one-time generalization that gives
future demos coverage automatically.
## Verification
- `pnpm typecheck:website`: 0 errors, 0 warnings, 0 hints
- Pre-commit hook ran `pnpm typecheck`, `oxfmt`, `oxlint`, `eslint` —
all clean on staged files
- `npx astro build`: 53 pages built; `/demos/community-workflows/` and
`/zh-CN/demos/community-workflows/` generated and present in
`sitemap-0.xml`
- Page rendered in Playwright preview: hero (title, GETTING STARTED,
BEGINNER, ~2 min), Arcade embed loads, transcript section present,
"What's Next" links to `image-to-video`
- zh-CN page shows localized title (探索并使用社区工作流), description, badges,
and "What's Next" heading
- OG meta tag references the new 1200×630 PNG
## Screenshots


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11942-feat-website-add-community-workflows-demo-page-3576d73d36508139b647c774b1d39323)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Automated Ingest API Type Update
This PR updates the Ingest API TypeScript types and Zod schemas from the
latest cloud OpenAPI specification.
- Cloud commit: 0a03f3a
- Generated using @hey-api/openapi-ts with Zod plugin
These types cover cloud-only endpoints (workspaces, billing, secrets,
assets, tasks, etc.).
Overlapping endpoints shared with the local ComfyUI Python backend are
excluded.
---------
Co-authored-by: MillerMedia <7741082+MillerMedia@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
*PR Created by the Glary-Bot Agent*
---
Converts the `CategoryNav` in the careers `RolesSection` from a
click-to-filter button into a scroll-spy section locator, matching the
pattern already used by `ContentSection.vue` (customer story details,
TOS, privacy policy).
## Changes
- **`apps/website/src/components/careers/RolesSection.vue`**
- Replaced category-based filtering with anchor navigation: clicking a
department in the sidebar smooth-scrolls (via existing Lenis/GSAP
`scrollTo` helper) to that department's section with a `-144px` header
offset.
- Removed the `ALL` button — every department is always rendered as its
own scroll target with `id="careers-dept-{key}"`.
- Added `useIntersectionObserver` (rootMargin `-20% 0px -60% 0px`) that
updates the active nav item as the user scrolls. An `isScrolling` guard
prevents the observer from fighting click-jumps mid-animation.
- Added a viewport-entry fade/slide-up animation on each department
section, gated by `motion-safe:` so users with `prefers-reduced-motion`
see content immediately. The reveal state is sticky (one-way) so
sections don't disappear once revealed.
- Active state is driven by raw department keys; both the nav model and
the observer's id-to-key mapping use a single consistent identifier.
- **`apps/website/e2e/careers.spec.ts`**
- Replaced the obsolete "ENGINEERING filter narrows the list" test with
one that validates locator behavior: clicking the department button
scrolls the section into the viewport, sets `aria-pressed="true"`, and
keeps the full role list rendered.
## Verification
- `pnpm --filter @comfyorg/website typecheck` — clean.
- `pnpm exec oxfmt` / `pnpm exec eslint` / `pnpm exec oxlint` — clean.
- Pre-commit lint-staged hooks (stylelint, oxfmt, oxlint, eslint,
typecheck) — passing.
- Manual smoke test via Playwright on `astro dev`: careers page renders
all departments stacked vertically, active department in the sidebar
highlights based on viewport position (DESIGN active on initial scroll),
nav items reflect each department instead of including an `ALL` button.
## Screenshots

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12110-feat-convert-careers-CategoryNav-to-scroll-spy-locator-35b6d73d3650818a9226e5dcb1244756)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary <glary@bot.local>
## Summary
`.m4v` and `.mkv` files render as a generic file icon in the assets
sidebar instead of a video preview because `VIDEO_EXTENSIONS` doesn't
list them, even though both formats are widely produced by ComfyUI
custom nodes and are browser-playable when written with common codecs.
## Changes
- **What**: Add `m4v` and `mkv` to `VIDEO_EXTENSIONS` in
`packages/shared-frontend-utils/src/formatUtil.ts` and extend the
existing test cases. Aligns with ComfyUI core's canonical video
extension list
(`tests-unit/folder_paths_test/filter_by_content_types_test.py:13`),
which includes both. The frontend's format registry at
`src/platform/workflow/core/types/formats.ts` also lists `.m4v` with
mime `video/x-m4v` — `formatUtil.ts` was the inconsistent surface.
- **Breaking**: None.
- **Dependencies**: None.
## Review Focus
`m4v` is Apple's MP4 container variant; `mkv` is the Matroska container.
ComfyUI custom nodes most commonly produce both with H.264/VP9 codecs,
which play natively in modern browsers via `<video>`. Adding the
extensions routes those files through the existing `MediaVideoTop`
component without any new rendering logic.
If a user's `.mkv` happens to use an exotic codec the browser can't play
(e.g. H.265/HEVC in Chrome), they get the same controllable failure mode
as today's `.avi` entries — a `<video>` element with the browser's
native unsupported-source UI. That is no worse than the current "show a
generic file icon" behavior, and strictly better in the common case.
## Screenshots (if applicable)
> **Note**: Screenshots taken from the OSS *input* assets sidebar with
[#12086](https://github.com/Comfy-Org/ComfyUI_frontend/pull/12086) also
applied locally. That PR fixes a separate regression where OSS input
filenames carry a `[input]` annotation suffix that breaks all
extension-based media detection — without it, `m4v`/`mkv` files (and
every other file in that sidebar) still render as the generic icon. This
PR alone is sufficient for cloud assets and OSS output history; the
input-sidebar previews require both PRs.
Before:
<img width="1197" height="714" alt="2026-05-09-031123_hyprshot"
src="https://github.com/user-attachments/assets/5c6ebc2d-aac2-411f-a2e4-51a111033184"
/>
After:
<img width="1042" height="723" alt="2026-05-09-031005_hyprshot"
src="https://github.com/user-attachments/assets/f0acc2cf-8571-4fd0-b0cd-2b8b87ff9b74"
/>
*PR Created by the Glary-Bot Agent*
---
## Summary
Fixes the bug where the last badge in `ContentSection`'s sticky sidebar
nav stays unhighlighted when the user scrolls to the very bottom of the
page on tall viewports (reported on a 14" MacBook M4 Pro at 3024×1964 /
2016×1310 logical, both Chrome and Safari).
## Root cause
The scroll-spy uses an IntersectionObserver with `rootMargin: '-20% 0px
-60% 0px'`, which makes only a 20%–40% horizontal band from the viewport
top "active". When multiple intersecting entries are reported, the
callback picks the one whose `boundingClientRect.top` is smallest
(highest up on screen).
On tall viewports, when the page is scrolled to the absolute bottom, the
last *and* the second-to-last sections frequently both sit inside that
20%–40% band at the same time. The "smallest top" tiebreak then selects
the second-to-last section, leaving the last badge inactive even though
the user has reached the end of the page.
## Fix
`apps/website/src/components/common/ContentSection.vue`:
1. Add `isAtBottom()` — true when the viewport bottom has reached the
document bottom (within 4px to absorb sub-pixel rounding).
2. The IntersectionObserver callback bails out when `isAtBottom()` so it
cannot overwrite the choice below.
3. A passive `scroll` listener (and a one-shot `onMounted` call) sets
`activeSection` to the last section whenever the page is at the bottom —
including when the component mounts already at the bottom (e.g. hash
navigation to a trailing anchor, restored scroll position, or a page
shorter than the viewport).
4. Both the scroll handler and the IO callback honor the existing
`isScrolling` flag, so click-driven smooth scroll-to-section behavior is
unchanged.
## Verification
Reproduced the bug at viewport 2016×1310 (14" M4 Pro "More Space" mode)
on `/privacy-policy`:
- Before fix: at absolute bottom, IntersectionObserver picks
`australian-privacy` (second-to-last) — bug confirmed via DOM inspection
that showed multiple sections intersecting the active band, with the
second-to-last winning the "smallest top" tiebreak.
- After fix:
- Scrolled to bottom → last badge `CONTACT` is active.
- Scrolled to top → first badge `INTRO` is active.
- Scrolled mid-page → correct mid-section is active.
- Click on a badge → smooth scrolls and that badge becomes active.
- Initial render at bottom (loaded `/privacy-policy#contact`, browser
scrolls to the bottom on mount) → `CONTACT` active immediately.
`pnpm typecheck` and `pnpm typecheck:website` pass; `pnpm lint` reports
0 errors; existing website unit tests pass.
Note: The website app currently has no Vue component test setup
(`vitest.config.ts` is configured for `node` env, no DOM). Adding
component tests for this scroll-spy interaction would require setting up
`happy-dom` and `@testing-library/vue` for the website app, which is out
of scope for this bug fix.
Fixes FE-604
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12057-FE-604-fix-website-activate-last-section-badge-when-scrolled-to-bottom-3596d73d365081faa243f4dd8e6ee54a)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
When an image fails to load in the image preview, the context buttons
are still visible - clicking these does not work (Mask editor opens and
closes, download does nothing) - this hides the buttons if load fails.
## Changes
- **What**:
- hide buttons if load failed
- tests
## Review Focus
<!-- Critical design decisions or edge cases that need attention -->
<!-- If this PR fixes an issue, uncomment and update the line below -->
<!-- Fixes #ISSUE_NUMBER -->
## Screenshots (if applicable)
Current:
<img width="622" height="857" alt="image"
src="https://github.com/user-attachments/assets/26e391a0-5538-4c6c-ac8a-b6f2b6acabae"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12136-fix-hide-image-buttons-if-load-failed-35d6d73d365081579c71f1849b9ab1bd)
by [Unito](https://www.unito.io)
## Summary
After deleting an asset, the Load Image node kept displaying the deleted
thumbnail — both in the node body and in the picker dropdown (All /
Imported / Generated tabs), even after a workflow reload.
- Fixes FE-230
- Source: Slack
https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1776715727656809
## Root Cause
Three distinct paths kept the deleted asset visible:
1. **Node-body preview cache** — `useMediaAssetActions.deleteAssets`
never cleared `node.imgs` / `node.videoContainer` / the
`nodeOutputStore` Vue ref, so the canvas renderer kept its cached frame.
2. **Live-delete dropdown gap** — the picker reads from
`outputMediaAssets.media` (the asset list) and from
`missingMediaStore.missingMediaCandidates` (verified-missing names). On
live delete, neither was updated for the deleted asset, so the dropdown
filter had nothing to drop.
3. **Synthetic "selected" placeholder** —
`useWidgetSelectItems.missingValueItem` rebuilt any orphaned
`modelValue` as a fake item with a `/api/view?filename=...` preview URL.
Browsers had cached that URL pre-delete, so the deleted thumbnail still
rendered with a blue checkmark even after the filter dropped the real
asset entry.
A subtler issue compounded #2/#3: candidate names stored in
`missingMediaStore` are raw widget values (e.g. `sub/foo.png [output]`),
but the dropdown computed comparison keys differently per source (asset
list uses bare `asset.name`, widget option list uses bare filename).
Names with a subfolder prefix slipped through the filter.
## Fix
- **`clearNodePreviewCacheForFilenames`** (existing helper, refactored):
exports `findNodesReferencingFilenames` +
`extractFilenameFromWidgetValue`. Uses
`nodeOutputStore.removeNodeOutputs` so the **reactive** Pinia ref
updates, not just the legacy `app.nodeOutputs` mirror. Also clears
`node.videoContainer` for Load Video.
- **`markDeletedAssetsAsMissingMedia`** (new): on successful deletion,
surfaces the affected widgets through `missingMediaStore` immediately so
the dropdown filter has something to drop without waiting for
verification.
- **`useMissingMediaPreviewSync`** (new): watches `missingMediaStore`
and clears `node.imgs` / `node.videoContainer` / Vue preview source for
nodes referencing confirmed-missing media on workflow load — covers the
post-reload case.
- **`useWidgetSelectItems`**: normalizes both sides of the missing-media
filter via `extractFilenameFromWidgetValue` (strips
`[input|output|temp]` annotation + subfolder prefix), and suppresses
`missingValueItem` when the value is in the missing-media store so the
cached-thumbnail "selected" placeholder doesn't appear.
## Red-Green Verification
| Commit | CI Status | Run |
|--------|-----------|-----|
| `test: FE-230 add failing test for Load Image preview cache clearing`
| 🔴 Failure — test caught the bug |
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/24700188700 |
| `fix: FE-230 clear Load Image preview cache when asset is deleted` |
🟢 Success |
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/24700265884 |
## Test Plan
- [x] Unit coverage: 78 tests across 5 files (preview-cache helper,
mark-deleted-as-missing, missing-media-preview-sync, widget-select-items
missing-media filter incl. subfolder-prefix case, useMediaAssetActions
integration)
- [x] Live delete: Load Image node preview clears, dropdown drops the
asset across All / Imported / Generated, no synthetic "selected"
placeholder
- [x] Post-reload: missing-media verification →
`useMissingMediaPreviewSync` clears the preview, dropdown drops the
asset
- [x] Linear FE-230 auto-links via the Source line
## Scope note
In-session and session-restore are both covered. If the backend/CDN
continues serving the deleted `filename`/`asset_hash` after deletion, a
cross-session reopen may still render stale bytes from cache — that's a
backend/CDN concern tracked separately.
## demo
### before
https://github.com/user-attachments/assets/e4d3a40e-0d46-43ad-985c-22ce7e0d3faf
### after
https://github.com/user-attachments/assets/fcac9387-4c07-4be2-bcdd-d1a6192fe962
## Summary
Remove the module-level `reportedInactiveCalls: Set<string>` and the
early-return dedup check from `reportInactiveTrackerCall()` in
`src/scripts/changeTracker.ts`. Every invocation now emits
`console.warn` and (on Desktop) `Sentry.captureMessage` unconditionally.
## Why
The dedup was added in #11328 but is unnecessary:
- Every first-party call site already goes through the
`activeWorkflow?.changeTracker` guard, so flooding from in-repo code is
unlikely.
- Repeated identical alerts may actually provide more diagnostic signal
than the first-only approach suppresses.
## Changes
- Drop `reportedInactiveCalls` Set
- Drop the per-`(method, workflowPath)` early-return
- Trim the JSDoc accordingly
No behavior change for callers (`deactivate`, `captureCanvasState`);
only the reporting frequency increases.
## Verification
- `pnpm test:unit -- src/scripts/changeTracker.test.ts` — 16/16 passing
- `pnpm typecheck` — clean
- ESLint / oxfmt — clean
- Fixes#11372
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11833-refactor-remove-dedup-complexity-from-reportInactiveTrackerCall-3546d73d365081fabf57cbf1fa17051f)
by [Unito](https://www.unito.io)
## Summary
Replace `nodeId: string` with canonical `NodeId` type in right-side
panel parameters tab components, eliminating redundant `String()`
conversions at call sites.
## Changes
- `TabNodes.vue`: `isSectionCollapsed` and `setSectionCollapsed` now
accept `NodeId` instead of `string`; callers updated to pass `node.id`
directly (removing `String()` wrapping)
- `TabNormalInputs.vue`: same pattern
## Notes
The other 6 files listed in the issue use `nodeId` parameters that carry
execution IDs (`NodeExecutionId = string`), not graph node IDs (`NodeId
= number | string`). Changing those to `NodeId` would be semantically
incorrect. The two files changed here are the clear-cut cases where
`node.id` (a `NodeId`) was being unnecessarily stringified before being
passed.
## Testing
### Automated
- `pnpm typecheck` — passes
- `pnpm lint` — passes (0 warnings, 0 errors)
- `pnpm format:check` — passes
### E2E Verification Steps
1. Open ComfyUI frontend
2. Load a workflow with multiple nodes
3. Open the right side panel (Parameters tab)
4. Verify node sections collapse/expand correctly per node
5. Verify "Collapse All" / "Expand All" toggle works correctly
6. Repeat with both TabNodes and TabNormalInputs views
Fixes#11843
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12014-chore-11843-replace-bare-string-NodeId-typings-in-parameters-tab-components-3586d73d365081ed84caf560277f0553)
by [Unito](https://www.unito.io)
## Summary
This PR fixes missing-media false positives for annotated media widget
values such as:
```txt
photo.png [output]
clip.mp4 [input]
147257c95a3e957e0deee73a077cfec89da2d906dd086ca70a2b0c897a9591d6e.png [output]
clip.mp4[input] // Cloud compact form
```
The change is intentionally scoped to the missing-media detection
pipeline for:
- `LoadImage`
- `LoadImageMask`
- `LoadVideo`
- `LoadAudio`
It preserves the raw widget value on `MissingMediaCandidate.name` for UI
display, grouping, replacement, and user-facing missing-media rows.
Normalized values are used only as comparison keys during verification.
## Diff Size
`main...HEAD` line diff is currently:
- Production/runtime code: `+478 / -37` (`515` changed lines)
- Unit test code: `+960 / -47` (`1,007` changed lines)
- Total: `+1,438 / -84` (`1,522` changed lines)
The PR looks large mostly because it locks both Cloud and OSS/Core
runtime paths with unit coverage; the production/runtime change is about
one third of the total diff.
## What Changed
- Added missing-media-scoped annotation helpers for detection-only path
normalization.
- Core/OSS recognizes spaced suffixes like `file.png [output]`.
- Cloud also recognizes compact suffixes like `file.png[output]`.
- User-selectable trailing `input` and `output` annotations are
normalized for matching.
- Unknown annotations and middle-of-filename annotations are left
unchanged.
- Added shared file-path helpers in `formatUtil`:
- `joinFilePath(subfolder, filename)`
- `getFilePathSeparatorVariants(filepath)`
- Updated media verification to compare candidates against both raw and
normalized match keys.
- Kept input candidates and generated output candidates in separate
identifier sets so an input asset cannot accidentally satisfy an output
reference with the same name.
- Moved missing-media source loading into `missingMediaAssetResolver` so
`missingMediaScan` remains focused on scan/verification orchestration.
- Updated Cloud generated-media verification to use the Cloud assets API
instead of job history:
- Cloud input candidates use input/public assets.
- Cloud output candidates use `output` tagged assets.
- Kept OSS/Core generated-media verification history-based, matching the
current generated-picker/widget availability model.
## Runtime Verification Paths
### Cloud
Cloud stores generated outputs as asset records. For an annotated output
value, this PR verifies against the `output` asset tag rather than job
history.
```txt
Widget value
"147257...d6e.png [output]"
|
v
Detection keys
"147257...d6e.png [output]"
"147257...d6e.png"
|
v
Cloud asset sources
input candidates -> /api/assets?include_tags=input&include_public=true
output candidates -> /api/assets?include_tags=output&include_public=true
|
v
Match against
asset.name
asset.asset_hash
subfolder/asset.name
subfolder/asset.asset_hash
slash and backslash separator variants
```
Example:
```ts
candidate.name = 'abc123.png [output]'
asset.name = 'ComfyUI_00001_.png'
asset.asset_hash = 'abc123.png'
asset.tags = ['output']
// Result: not missing
```
### OSS / Core
Core widget options for the normal loader nodes are input-folder based.
Annotated output values are resolved by Core through
`folder_paths.get_annotated_filepath()`, but the current generated
picker path is history-backed. This PR keeps OSS generated verification
aligned with that widget availability model instead of treating the full
output folder as the source of truth.
```txt
Widget value
"subfolder/photo.png [output]"
|
v
Detection keys
"subfolder/photo.png [output]"
"subfolder/photo.png"
|
v
OSS generated source
fetchHistoryPage(...)
|
v
History preview_output
filename: "photo.png"
subfolder: "subfolder"
|
v
Generated match keys
"subfolder/photo.png"
"subfolder\\photo.png"
```
This means OSS/Core verification is about whether the generated media is
currently available through the same generated/history-backed path the
widget uses, not a full disk-level executability check across the entire
output directory.
## Why Not Consolidate All Annotated Path Parsers
There are existing annotated-path parsers in image widget, Load3D, and
path creation code. This PR does not replace them.
The helper added here is detection-only: it strips annotations to build
comparison keys for missing-media verification. Parser consolidation
across widget implementations is intentionally left out of scope to keep
this fix narrow.
## Known Follow-Ups / Out Of Scope
- FE-620 tracks the separate video drag-and-drop upload race between
upload completion and missing-media detection.
- Published/shared workflow assets are still not fully represented by
`/api/assets?include_public=true`; that remains a backend/API contract
issue.
- A future backend/API contract that answers “is this workflow media
executable?” would be preferable to stitching together runtime-specific
FE sources.
- OSS/Core full output-folder scanning via `/internal/files/output` was
considered, but that endpoint is internal, shallow (`os.scandir`), and
not the same source currently used by the generated picker flow.
## Validation
- `pnpm test:unit -- missingMediaAssetResolver missingMediaScan
mediaPathDetectionUtil formatUtil`
- touched files `oxfmt`
- touched files `oxlint --fix`
- touched files `eslint --cache --fix --no-warn-ignored`
- `pnpm typecheck`
- pre-commit `pnpm knip --cache`
- pre-push `pnpm knip --cache`
`knip` passes with the existing tag hint:
```txt
Unused tag in src/scripts/metadata/flac.ts: getFromFlacBuffer → @knipIgnoreUnusedButUsedByCustomNodes
```
## Screenshots
Before
https://github.com/user-attachments/assets/50eab565-3160-4a57-a758-87ec2c09071e
After
https://github.com/user-attachments/assets/08adcbbd-c3fc-43f9-b86c-327e4eb5abd8
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12069-fix-handle-annotated-output-media-paths-in-missing-media-scan-3596d73d365081f4afa3d4dd45cad3da)
by [Unito](https://www.unito.io)
*PR Created by the Glary-Bot Agent*
---
## Summary
Sort the canvas right-click "Add Node" context menu by display name
(case-insensitive, natural numeric). Previously, both category submenus
and leaf nodes appeared in node-registration order, making the menu hard
to scan for users browsing for nodes.
This change is scoped specifically to the **smaller right-click
contextual menu**. It does NOT affect the double-click search menu or
the left-side Nodes panel.
## Changes
- `src/lib/litegraph/src/LGraphCanvas.ts` — In `onMenuAdd` →
`inner_onMenuAdded`, sort the deduplicated category submenu entries and
the leaf-node entries by `content` using `localeCompare` with `{
numeric: true, sensitivity: 'base' }`. Categories still appear before
leaf nodes within a level (preserves existing UX).
- `src/lib/litegraph/src/LGraphCanvas.onMenuAdd.test.ts` — New unit
tests that mock `LiteGraph.ContextMenu` and assert: case-insensitive
sort, natural numeric ordering (`Cat1`, `Cat2`, `Cat10`), leaf-node
sorting inside a category, and category-before-leaf placement.
## Verification
- `pnpm vitest run src/lib/litegraph/src/LGraphCanvas.onMenuAdd.test.ts`
— 4/4 pass
- `pnpm typecheck` — clean (ran via pre-commit hook on initial commit)
- `oxfmt` / `oxlint` / `eslint` — clean
- Oracle review against `main` returned 0 critical / 1 warning (test
coverage) / 1 suggestion (numeric sort) — both addressed in this PR.
## Notes
- The sort is applied at the menu-build site rather than inside
`LiteGraphGlobal.getNodeTypesCategories`/`getNodeTypesInCategory` to
keep the change scoped to the menu UX and avoid changing the iteration
order seen by extensions that consume those public methods.
- Per user request, this is opening as a draft PR for self-review +
CodeRabbit feedback in a single follow-up pass; manual browser
verification (right-click screenshots) was deferred to that pass.
- Slack thread context: user reported the contextual menu is "a mess"
for discovering native nodes; alphabetical sorting addresses the
discoverability problem without touching the search-oriented menus.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12039-feat-sort-right-click-context-menu-categories-alphabetically-3596d73d36508107a87ffec1c353994e)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: Alexis Rolland <alexis@comfy.org>
## Summary
Imported assets render as a generic check-check icon instead of a
thumbnail because the OSS `/internal/files/{type}` endpoint returns
annotated filenames (`photo.png [input]`) that the assets-sidebar mapper
passes through verbatim, which breaks extension-based media-type
detection.
## Changes
- **What**: Strip ComfyUI's trailing directory-type annotation (`
[input]`, ` [output]`, `[temp]`) in `mapInputFileToAssetItem` so `name`,
`id`, and the generated `/view?filename=…` URL all use the canonical
on-disk filename. Adds a focused unit test.
- **Breaking**: None.
- **Dependencies**: None.
## Review Focus
### Root cause
ComfyUI core PR
[comfyanonymous/ComfyUI#13078](https://github.com/comfyanonymous/ComfyUI/pull/13078)
(April 2026) changed `/internal/files/{type}` to append the directory
type to each entry:
```python
# api_server/routes/internal/internal_routes.py
return web.json_response(
[f"{entry.name} [{directory_type}]" for entry in sorted_files], status=200
)
```
The annotation is the wire format `LoadImage`-style widgets expect, so
the backend change is correct. The assets-sidebar mapper treated the
response strings as raw filenames. After
[#8914](https://github.com/Comfy-Org/ComfyUI_frontend/pull/8914) changed
`getMediaTypeFromFilename` to default unknown extensions to `'other'`,
every input asset now routes to `MediaOtherTop` and renders as
`icon-[lucide--check-check]`:
```
getMediaTypeFromFilename("photo.png [input]").split('.').pop() === "png [input]" → 'other'
```
The strip happens at data ingestion so every consumer of
`AssetItem.name` (sidebar grid, list, filter, gallery, drag-drop,
delete) gets the canonical filename automatically. OSS-only — Cloud
paths get clean names from the cloud API and are unaffected.
Reproduces locally on stock OSS ComfyUI on `main` of both repos; no
public issue tracker entry.
## Screenshots (if applicable)
Before:
<img width="1091" height="718" alt="image"
src="https://github.com/user-attachments/assets/ff1f070d-da39-4e5a-bc6d-99b7214f7da8"
/>
After:
<img width="1089" height="716" alt="image"
src="https://github.com/user-attachments/assets/7123d9bf-f7dd-4430-b6f7-f6702b70baaa"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12086-fix-assets-strip-directory-annotation-from-input-filenames-35a6d73d365081e9b9eed7d8630d6f0b)
by [Unito](https://www.unito.io)
*PR Created by the Glary-Bot Agent*
---
## Summary
Disable the `UltralyticsDetectorProvider` model-to-node mapping so the
node falls back to the static combo populated from `/api/object_info`,
restoring pre-#8468 behavior on cloud.
## Why
PR #8468 opted `UltralyticsDetectorProvider` into the cloud asset-widget
path, which exposed a latent mismatch in cloud asset metadata for
nested-directory model folders. The bug has two independent halves, and
a fix that addresses only one will still leave the workflow broken at
execution time:
- **Tag lookup mismatch.** Cloud stores tags as combined values like
`ultralytics/bbox`, while the asset query asks for split tags (`models`
+ `ultralytics`) with exact-match filtering — so the dropdown returns no
results.
- **Submitted value mismatch.** Cloud stores filenames as basenames, but
the node expects subdirectory-prefixed values (e.g.
`bbox/face_yolov8m.pt`) that the static combo path normally produces.
Both halves require cloud-side fixes (asset ingestion + metadata) before
the asset-browser registration can be safely re-enabled. Until then,
removing the registration restores the working static-combo behavior so
users are unblocked.
## Changes
- `src/platform/assets/mappings/modelNodeMappings.ts`: comment out the
`['ultralytics', 'UltralyticsDetectorProvider', 'model_name']` entry
with a note pointing at BE-689 and the re-enablement criteria.
- `src/stores/modelToNodeStore.test.ts`: drop the now-stale ultralytics
expectations from `EXPECTED_DEFAULT_TYPES`, `MOCK_NODE_NAMES`, and the
hierarchical-fallback `it.each` cases.
## Verification
Local quality gates:
- `pnpm typecheck` — clean
- `pnpm lint` — clean (3 pre-existing warnings, 0 errors)
- `pnpm format:check` — clean
- `pnpm knip` — clean (1 pre-existing warning unrelated to this change)
- `pnpm test:unit -- src/stores/modelToNodeStore.test.ts` — 51/51
passing
Manual runtime verification (dev server + Playwright against the live
module):
- `MODEL_NODE_MAPPINGS` no longer contains any entry where `[0] ===
'ultralytics'` or `[1] === 'UltralyticsDetectorProvider'` (84 entries
total, 0 ultralytics).
- `useModelToNodeStore().getNodeProvider('ultralytics')` returns `null`
after `registerDefaults()`, so the asset-widget path is no longer
triggered for this node.
- `getNodeProvider('ultralytics/bbox')` also returns `null`, confirming
hierarchical fallback no longer resolves to the disabled mapping.
- `getNodeProvider('checkpoints')` still resolves to
`CheckpointLoaderSimple`, confirming unrelated mappings are intact.
End-to-end cloud verification (actually exercising the asset-browser
path against cloud-seeded ultralytics metadata) is not possible in the
local sandbox, since the regression depends on the cloud's asset
ingestion data shape. The change is a single mapping-table removal that
reverts to the well-exercised static-combo path covered by the updated
unit tests.
Long-term cloud-side fix is tracked in BE-689.
- Fixes BE-689
## Screenshots

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12075-fix-disable-ultralytics-asset-browser-registration-35a6d73d36508179b394f0915e69742e)
by [Unito](https://www.unito.io)
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
The help icon (lucide circle-help) next to the credits balance in the
cloud user popover was a bare `<i>` with `v-tooltip` and `cursor-help`.
PrimeVue tooltip on a bare `<i>` did not fire reliably and the icon had
no focus/keyboard semantics, so users saw "no hover action and not
clickable".
Wrap the icon in `<Button variant="muted-textonly" size="icon-sm">`,
matching the existing pattern in `InfoButton.vue` and
`MissingPackGroupRow.vue`. Same change applied to
`CurrentUserPopoverLegacy.vue` and `CurrentUserPopoverWorkspace.vue`,
which shared the broken pattern.
- Fixes FE-617
-
https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1778191473621829
## Red-Green CI Verification
The branch was force-pushed back to the test-only commit so CI could run
against it, then restored to the fix commit.
| Commit | CI: Tests Unit | Outcome |
|--------|---------------|---------|
| `test:` (e7c83abd0) — adds the regression test only |
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25532935842 |
failed — `Unable to find an element by:
[data-testid="credits-info-button"]` |
| `fix:` (64ec4cda4) — wraps the icon in `<Button>` |
https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25533224195 |
passed |
<img width="434" height="364" alt="Screenshot 2026-05-08 at 5 32 47 PM"
src="https://github.com/user-attachments/assets/d3088b90-813f-4a0f-ba35-0f040fc79a6a"
/>
## Test Plan
- [x] Component test asserts the icon renders as an interactive
`<button>` with the unified-credits tooltip text as `aria-label`
- [x] Red CI failed with the expected error on the test-only commit
- [x] Green CI passed on the fix commit
- [ ] Manual verification on `pnpm dev:cloud` — hover the help icon next
to the credits balance and confirm the unification tooltip appears
## Summary
The nightly `Release Draft Create` -> `publish_types / Build types` job
has been failing on every run since 1.45.1 ([failed
run](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25531376650)).
Reproduced locally on `main` (`c8c0e5386`).
## Root cause
`pnpm build:types` ends with `FATAL ERROR: Reached heap limit Allocation
failed - JavaScript heap out of memory`. The `vite-plugin-dts` rolled-up
type generation now exceeds Node's default ~4GB heap on the GH runner.
The `TS2742`/`TS4082` warnings printed earlier are non-fatal diagnostics
from the plugin pre-pass — the api-extractor rollup itself completes
once the heap is large enough.
## Fix
Set `NODE_OPTIONS='--max-old-space-size=8192'` in `build:types`,
matching the existing pattern already used by `build` and `build:cloud`
(only one-line change).
## Verification
- `pnpm build:types` exits 0 locally with the change (built in ~40s).
- `dist/index.d.ts` (1.9MB) emitted with the public types intact:
`ComfyExtension`, `ComfyApi`, `ComfyApp`, `ComfyNodeDef`, `InputSpec`,
`DOMWidget`, etc.
- `dist/package.json` correctly produced by `scripts/prepare-types.js`.
- Reverting the change reproduces the OOM crash.
## Test plan
- [ ] Trigger `Release NPM Types` workflow manually (or wait for next
nightly) and confirm `Build types` step succeeds.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12073-fix-restore-nightly-publish_types-build-35a6d73d3650819e95aecfbd8a66847c)
by [Unito](https://www.unito.io)
## Summary
This PR removes the `/api/assets/hash/:hash` verification path from
missing media/model detection.
I decided to remove this path for two reasons:
1. The Cloud runtime implementation and the OpenAPI/generated FE
contract do not agree on the hash format that this endpoint represents.
In current Cloud data, the dominant asset_hash shape is
<64-hex>.<extension> (for example, abc123....png), while the
OpenAPI/generated FE contract expects a blake3:<hash> style value. That
makes this path either dead code that should never be reached, or, when
it is reached, a request that always returns 400 and only adds
unnecessary noise.
2. Even if the format is reconciled, the Cloud implementation is a
global deduplication-oriented lookup, not an access-aware check for
whether the current workflow can use a resource. In theory, it can
return success for another user's personal asset, so it is the wrong
primitive for missing asset detection.
Because of that, this PR makes the existing asset list/store based
checks the primary verification path and removes the hash-specific
helpers, service method, and tests.
## Known follow-ups
These are known issues that are intentionally not solved in this PR:
1. Published assets are not exposed through
`/api/assets?...include_public=true`. This is a backend issue and can
still cause mismatch between missing-asset detection and resources that
preview/run successfully.
2. Shared workflow import has an ordering issue. The API contract issue
is being hotfixed separately under FE-603.
3. Annotated media paths can still be detected incorrectly.
I will prepare follow-up PRs for these, starting with the annotated
media path issue because that is the most critical frontend-side gap.
## Validation
- `pnpm exec vitest run
src/platform/assets/services/assetService.test.ts
src/platform/missingMedia/missingMediaScan.test.ts
src/platform/missingModel/missingModelScan.test.ts`
- `pnpm lint:unstaged`
- `pnpm typecheck`
- `pnpm knip`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12061-fix-remove-asset-hash-verification-3596d73d365081a088f8dfc874724c1d)
by [Unito](https://www.unito.io)
*PR Created by the Glary-Bot Agent*
---
## Summary
The horizontal progress bar in `FeedbackSection.vue` lagged behind the
carousel scroll position because the bar's width was driven reactively
by `useScroll`, but the `transition-all duration-200` utility animated
each width change over 200ms. As scroll continuously emits new target
widths, the bar visibly trailed the scroll position.
Removing the transition makes the bar track scroll synchronously.
## Verification
- Reproduced the lag locally on `/customers`.
- Verified post-fix that `bar.style.width` updates in the same frame as
`scrollLeft` changes (samples at scrollLeft 0 / 944 / 1600 → width 0% /
59% / 100%, with `transitionDuration: 0s`).
- `pnpm exec eslint`, `pnpm exec oxfmt --check`, `pnpm nx typecheck
website`, and `pnpm test:unit` all pass.
## Notes
No regression test added — the customers section components have no
existing unit/E2E coverage in this repo, and standing up a new test
harness for a one-line CSS fix would be disproportionate. Worth
following up on broader carousel coverage as a separate task.
## Screenshot
After fix, scrolled to second slide — progress bar tracks scroll
position synchronously.
## Screenshots

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12059-fix-remove-transition-delay-from-FeedbackSection-progress-bar-3596d73d36508107bc80dc38ea7ab79e)
by [Unito](https://www.unito.io)
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
*PR Created by the Glary-Bot Agent*
---
## Summary
Fix the video position glitch on `/customers` caused by
`c-projection.webp` loading.
## Root cause
In `HeroSection.vue` the hero `<img>` for `c-projection.webp` had no
`width`/`height` attributes, so the browser reserved no space for it.
When the image finished loading, the layout shifted ~192px, pushing the
`VideoPlayer` below it. `useHeroAnimation` registers a GSAP
`ScrollTrigger` parallax against the video in `onMounted` (before the
image is loaded), so the cached scroll geometry then went stale and the
video visibly glitched.
## Fix
- Add explicit `width="1568"` / `height="1763"` to the `<img>` (the
image's native size) so the browser reserves the correct aspect-ratio'd
space upfront.
- Add `h-auto` so the height attribute doesn't override the responsive
layout.
- Refresh `ScrollTrigger` on the image's `@load` (with `refresh(true)`
so measurements happen after layout has settled) as a defensive measure
for any sub-pixel adjustments.
## Test coverage
Added `apps/website/e2e/customers.spec.ts` with a regression guard that:
1. Asserts the hero `<img>` declares numeric `width`/`height`
attributes.
2. Asserts the unloaded image still reserves vertical space (>100px),
which is the exact property that prevents the video from jumping when
the image finishes loading.
Verified the test fails when the `width`/`height` attributes are removed
and passes with the fix applied.
## Verification
- `pnpm typecheck` clean.
- `pnpm test:unit` (website app) — 30/30 pass.
- `pnpm test:e2e customers.spec.ts --project=desktop` — passes.
- ESLint + oxfmt clean on changed files.
- Manual Playwright verification confirmed the video bounding rect stays
at `top=785, height=628` through the full image load lifecycle.
Reverting the fix in DevTools and re-loading the image reproduces the
original ~192px shift.
## Out of scope
`apps/website/src/components/contact/FormSection.vue` uses the same
`c-projection.webp` pattern (also without `width`/`height`). It runs
`useHeroAnimation` with `parallax: false`, so the symptom is much
smaller — leaving as a follow-up to keep this PR minimal.
- Fixes
[FE-607](https://linear.app/comfyorg/issue/FE-607/bug-video-on-customers-shifts-position-when-c-projectionwebp-finishes)
## Screenshots

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12060-fix-website-prevent-video-glitch-when-c-projection-webp-loads-on-customers-3596d73d365081ebbcb8db25aaa5c451)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
Unblock CI due to missing function in mock
```
⎯⎯⎯⎯ Unhandled Rejection ⎯⎯⎯⎯⎯
TypeError: this.load3d.emitModelReady is not a function
❯ src/extensions/core/load3d/Load3DConfiguration.ts:313:19
311| }
312|
313| this.load3d.emitModelReady()
| ^
314| }
315| }
This error originated in "src/extensions/core/load3d/Load3DConfiguration.test.ts" test file. It doesn't mean the error was thrown inside the file itself, but while it was running.
The latest test that might've caused the error is "prefers persisted Scene/Camera/Light Config over settings". It might mean one of the following:
- The error was thrown, while Vitest was running this test.
- If the error occurred after the test had been completed, this was the last documented test before it was thrown.
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
Test Files 773 passed (773)
Tests 10417 passed | 8 skipped (10425)
Errors 2 errors
Start at 12:06:06
Duration 446.79s (transform 32.89s, setup 113.26s, import 589.18s, tests 109.19s, environment 228.45s)
```
## Changes
- **What**:
- add `emitModelReady` fn to mock
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12056-test-add-missing-emitModelReady-to-mock-object-3596d73d36508130a087f5c7713e932f)
by [Unito](https://www.unito.io)
## Summary
Follow-up to #11568 and #11785. Keeps the E2E coverage workflow clean
when `CI: Tests E2E` is intentionally skipped and no coverage shard
artifacts are produced.
## Changes
- Detect whether downloaded E2E coverage shard artifacts contain any
`coverage.lcov` files.
- Treat missing coverage shards as an intentionally skipped coverage run
instead of running lcov, Codecov, or Pages deployment on missing files.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11575-ci-handle-skipped-e2e-workflow-consumers-34b6d73d3650813f92e1d7d6af0bf958)
by [Unito](https://www.unito.io)
## Summary
Align the asset list pagination schema with generated ingest-types
metadata and remove the now-unneeded missing `has_more` fallback branch.
## Changes
- **What**: Reuse `zListAssetsResponse` for `total` and `has_more`, keep
the local loose `AssetItem` shape, and simplify `getAllAssetsByTag()` to
trust the required `has_more` contract.
- **Breaking**: None.
- **Dependencies**: None.
## Review Focus
This is PR 1 of 4 in the missing asset follow-up stack:
1. This PR - Asset schema / pagination cleanup
2. #11900 - Missing asset hash verification utility cleanup
3. #11901 - Browser regression coverage for public input assets
4. #11902 - TanStack Query public-input cache replacement
The key decision is intentionally narrow: pagination metadata now comes
from generated ingest-types, but asset item validation remains locally
loose to avoid changing UI/store synthetic asset shapes in this PR.
`asset_hash` nullability remains unchanged because absent-vs-null hash
semantics are still a backend/API contract follow-up.
Addresses #11894
## Screenshots (if applicable)
N/A
*PR Created by the Glary-Bot Agent*
---
Adds an entry for the new `SaveImageAdvanced` node to
`public/assets/sorted-custom-node-map.json` with the same frequency stat
(1762) as the existing `SaveImage` node, so the new Save Image node
ranks at the top of search results when typing "save" — matching the
original node's behavior.
Context: the new Save Image node ([Notion
spec](https://www.notion.so/comfy-org/Save-Image-94a77c506ce145fc9b8c477c52091a04))
replaces/deprecates the original `SaveImage`. Search ranking uses the
static node frequency map; the new node had no entry and was therefore
ranked at frequency 0. Mirroring the original's stat is the manual-boost
approach discussed in the thread.
## Changes
- `public/assets/sorted-custom-node-map.json`: add `"SaveImageAdvanced":
1762` directly after `"SaveImage": 1762` to preserve descending sort
order.
## Verification
- `pnpm typecheck`, `pnpm lint`, and `pnpm format` all pass via
lint-staged on commit.
- JSON validated and entry placement confirmed (position 4, between
`SaveImage` and `VAEDecode`).
- Review (oracle) ran clean: 0 critical / 0 warning / 0 suggestion.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11853-feat-boost-SaveImageAdvanced-node-frequency-for-search-ranking-3546d73d36508168b058d9d750fc3c56)
by [Unito](https://www.unito.io)
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
*PR Created by the Glary-Bot Agent*
---
## Summary
Replaces `right1.webp` with `right1.webm` in the VFX panel of
`UseCaseSection`. `BlobMedia` already auto-detects `.webm` URLs and
mounts a `<video>` element (with the `.webp` as poster), so this single
URL swap is the only change required — matching the pattern used by the
other 4 use-case panels.
## Files changed
- `apps/website/src/components/home/UseCaseSection.vue` — swap
`right1.webp` → `right1.webm`.
## Verification
- `pnpm exec nx run website:typecheck` — clean
- `pnpm exec eslint` on changed file — clean
- `pnpm exec oxfmt --check` — clean
- pre-commit lint-staged hooks — passed
## Reviewer note (Oracle finding)
VFX is the default active panel, so the homepage's initial right-rail
asset moves from ~131 KB `.webp` to ~4.1 MB `.webm`. Behaviorally
consistent with the other 4 panels (which already use `.webm`), but
worth confirming whether `right1.webm` should be re-encoded smaller on
the CDN before promoting this PR out of draft.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12040-fix-use-webm-video-for-VFX-use-case-right-asset-3596d73d365081829976f37b733840f1)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: Amp <amp@ampcode.com>
## Summary
The dirtyClose modal had three buttons (`Cancel | No | Save`) and the
sign-out flow collapsed two distinct outcomes (deny vs. dismiss) into a
single early return — so today clicking "No" *cancels* sign-out instead
of signing out without saving, and clicking "Save" never actually saves
before logging out. This PR drops `Cancel` for `dirtyClose`, gives each
caller a context-specific deny label, and fixes the sign-out 3-state
handling.
- Fixes
[FE-419](https://linear.app/comfyorg/issue/FE-419/unsaved-changes-modal-uses-confusing-button-labels)
## Changes
- **What**:
- `ConfirmationDialogContent.vue`: hide `Cancel` for
`type='dirtyClose'`; add `denyLabel?: string` prop; autofocus `Save`
(preserves work on Enter).
- `dialogService.confirm()`: accept and forward `denyLabel`.
- `useAuthActions.logout`: handle `null` (cancel) / `false` (sign out
anyway, no save) / `true` (save each modified workflow, then logout)
distinctly. Pass `denyLabel: 'Sign out anyway'`.
- `workflowService.closeWorkflow`: pass `denyLabel: 'Close anyway'`.
- i18n: add `auth.signOut.signOutAnyway` and
`sideToolbar.workflowTab.closeAnyway`.
- **Breaking**: none. The `denyLabel` prop is optional and falls back to
`g.no`.
## Review Focus
- The "Save" branch in `useAuthActions.logout` now iterates
`workflowStore.modifiedWorkflows` and awaits
`useWorkflowService().saveWorkflow(workflow)` for each before calling
`authStore.logout()`. The close-tab path
(`workflowService.closeWorkflow`) was already correct — only the
sign-out path needed the same shape.
- `ConfirmationDialogContent` autofocus moves from `Cancel` (gone for
`dirtyClose`) to `Save`. The dialog is still dismissable via ESC /
outside-click, which routes through `dialogComponentProps.onClose →
resolve(null)` — sign-out and close-tab both treat `null` as cancel.
- Out of scope: the native browser `beforeunload` warning
(`UnloadWindowConfirmDialog.vue`) is a separate flow and never reaches
the in-app modal.
## Tests
- Unit (`useAuthActions.test.ts`, new): logout handles `null` / `false`
/ `true` / no-modified-workflows; saves *every* modified workflow before
`authStore.logout`; passes `denyLabel='Sign out anyway'`.
- Unit (`ConfirmationDialogContent.test.ts`): Cancel hidden for
`dirtyClose`; custom `denyLabel` rendered; falls back to `g.no` when
omitted.
- E2E (`workflowTabs.spec.ts`): modified-tab close shows `Close anyway`
(not `No`) and no `Cancel`; clicking `Close anyway` removes the tab; ESC
keeps the tab.
## screenshot
### AS IS
<img width="816" height="379" alt="Screenshot 2026-04-27 at 5 40 19 PM"
src="https://github.com/user-attachments/assets/a8e39403-bf72-455a-8d86-6ceb1f94ac85"
/>
<img width="923" height="396" alt="Screenshot 2026-04-27 at 5 40 38 PM"
src="https://github.com/user-attachments/assets/08031c7c-b3a6-45d7-a4dc-5dcb4e63cfa0"
/>
### TO BE
<img width="1661" height="872" alt="Screenshot 2026-04-27 at 5 43 40 PM"
src="https://github.com/user-attachments/assets/b89d160b-be66-450e-981e-32b1591f6841"
/>
<img width="1488" height="584" alt="Screenshot 2026-04-27 at 5 44 21 PM"
src="https://github.com/user-attachments/assets/b3a141a7-1f3b-4f25-85a9-49529229c28b"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11669-fix-clarify-unsaved-changes-modal-buttons-and-fix-sign-out-3-state-34f6d73d365081bf8afad8e146b3b990)
by [Unito](https://www.unito.io)
*PR Created by the Glary-Bot Agent*
---
## Summary
PR #10890 routed the legacy action bar feedback button and the Help
Center feedback item to the nightly Typeform survey, but the **default
topbar feedback button** in `WorkflowTabs.vue` still called
`buildFeedbackUrl()` and opened Zendesk. Since `Comfy.UI.TabBarLayout`
defaults to `Default` (not `Legacy`), most Cloud/Nightly users were
clicking the WorkflowTabs button and never reaching the Typeform survey
— explaining the lack of survey responses.
## Changes
- Added a shared `buildFeedbackTypeformUrl(source)` helper in
`platform/support/config.ts` that tags the survey URL with:
- `distribution`: `ccloud` / `oss-nightly` / `oss` (preserves the
build-tagging the old `buildFeedbackUrl()` sent to Zendesk so responses
stay segmented)
- `source`: `topbar` / `action-bar` / `help-center` (identifies which UI
entry point launched the survey)
Tags are passed via the URL fragment (Typeform's hidden-field
convention), so they reach the survey but are never sent to the server
in the request line.
- `WorkflowTabs.vue`: replaced `buildFeedbackUrl()` with
`buildFeedbackTypeformUrl('topbar')`.
- `cloudFeedbackTopbarButton.ts` and `HelpCenterMenuContent.vue`: use
the shared builder with their respective source labels instead of inline
URL literals.
- Removed the now-unused `buildFeedbackUrl()` and
`ZENDESK_FEEDBACK_FORM_ID` (knip-clean). `buildSupportUrl()` is
preserved — `Comfy.ContactSupport` (the Help Center "Help" item) still
routes to Zendesk as before.
- Added unit tests for the builder, the WorkflowTabs feedback button,
the legacy action bar button, and the Help Center feedback item
(covering both the Cloud/Nightly Typeform path and the OSS
`Comfy.ContactSupport` fallback).
## Verification
- `pnpm format`, `pnpm lint`, `pnpm typecheck`, `pnpm knip`: clean (one
pre-existing unrelated lint warning in `useWorkspaceBilling.test.ts`)
- `pnpm test:unit` (impacted scope): 506/506 passing, including 13 new
tests
## Review Focus
- Cloud/Nightly gating in `WorkflowTabs.vue` (`v-if="isCloud ||
isNightly"`) is unchanged and matches PR #10890's gating philosophy.
- The Help Center "Help" item and `Comfy.ContactSupport` command
intentionally still route to Zendesk — feedback ≠ support.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11863-fix-route-default-topbar-feedback-button-to-Typeform-3556d73d3650815fb446dac33095d4be)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
Replace `useMouseInElement` in `CompareSliderThumbnail` with a local
`@mousemove` handler to eliminate ~1s of forced layout per keystroke in
the templates dialog search.
## Changes
- **What**: Profiling the templates dialog search (4× CPU throttle,
cloud distribution) showed a single `getClientRects` block at 977ms
inside Vue's `flushPostFlushCbs → update` path on every keystroke. The
call traces back to VueUse's `useMouseInElement`, which (a) shares a
global `mousemove` listener via `useMouse`, and (b) runs
`el.getBoundingClientRect()` inside a `watch([targetRef, x, y], …, {
immediate: true })`. Every template card with `thumbnailVariant ===
'compareSlider'` mounts one instance — when search filters change and
the page's compareSlider cards re-mount, each instance's `immediate`
watch fires a rect read against freshly-inserted DOM, forcing
synchronous layout of the entire new subtree. With many cards on screen
the costs stack into the ~977ms block. Replaced with a native
`@mousemove` listener bound directly to the slider container; the rect
is read from `event.currentTarget` only when the mouse is actually over
that one card, so the work no longer scales with mounted instance count
and is gated by real pointer activity.
- **Breaking**: None
- **Dependencies**: None
## Review Focus
- UX is unchanged: `sliderPosition` still follows the mouse during hover
and keeps its last value on mouseleave (matches previous behaviour where
`if (!isHovered) return` simply stopped updates without resetting).
- The same `useMouseInElement` pattern still exists in
`src/platform/workflow/sharing/composables/useSliderFromMouse.ts` (used
by `ComfyHubThumbnailStep` in the publish dialog). That path is
single-instance and off the templates hot path, so it's left untouched
to keep this PR scoped — happy to fix it in a follow-up.
- New tests use `userEvent.pointer({ coords })` with a stubbed
`getBoundingClientRect` to lock in the native mousemove path and the
zero-width guard.
## E2E coverage
No `browser_tests/` changes. The `fix:` commit is a defensive clamp
inside `updateSliderPosition`; the overshoot it guards against comes
from subpixel rounding and stale rects observed during hover-in, neither
of which can be deterministically reproduced under Playwright. The perf
change itself is verified via the DevTools profiler (forced-layout block
disappears) — also not assertable as a stable e2e signal across CI
hardware. Both the mousemove-driven slider behavior and the clamp guard
are covered by unit tests in
`src/components/templates/thumbnails/CompareSliderThumbnail.test.ts` (8
cases, including out-of-range pointer coordinates and zero-width
containers).
## Summary
Temporarily patch FE-569 by keeping the affected portaled Reka dropdowns
and menus above their containing PrimeVue dialogs when PrimeVue auto
z-index state has been elevated.
## Changes
- **What**: Added a small compatibility helper,
`usePrimeVueOverlayChildStyle`, that returns an anchor ref plus a
computed inline style for child popover content. The helper finds the
nearest PrimeVue dialog mask (`.p-dialog-mask` / `.p-overlay-mask`) from
the parent surface and, only when found, applies `parent z-index + 1` to
the affected Reka overlay content.
- **What**: Applied that helper at the exact PrimeVue parent surfaces
where the issue was found. This PR does not add a global overlay policy
and does not change every Reka select/dropdown in the app.
- **What**: Added optional `contentStyle`/`selectContentStyle` plumbing
only where needed so the style reaches the actual portaled Reka overlay
root.
- **What**: Added focused unit coverage for the helper contract: no
PrimeVue parent preserves existing stacking, PrimeVue dialog/overlay
masks render child content above the parent, low parent z-index values
respect the Reka floor, and invalid z-index values do not inject an
inline override.
- **Approach**: This is intentionally a minimal, parent-scoped band-aid.
It avoids a global PrimeVue overlay scanner because global sampling can
be polluted by unrelated persistent PrimeVue roots such as Toast and
would turn this fix into a broader layering policy.
- **Approach**: The patch targets the confirmed failure mode: a Reka
child overlay rendering below its owning PrimeVue dialog after PrimeVue
autoZIndex has been elevated. It does not attempt to solve PrimeVue
z-index globally.
- **Lifecycle**: This is temporary migration compatibility. PrimeVue
dialogs and controls are being incrementally migrated to Reka UI, so
`usePrimeVueOverlayChildStyle` and the optional style props added for
FE-569 should be removed once the affected parent surfaces move to Reka.
- **Breaking**: None. New props are optional and no public API contract
is changed.
- **Dependencies**: None.
## Patched Entry Points
This PR pinpoints the six affected user-facing surfaces below. Each
patch is applied from the PrimeVue dialog parent and passed only to the
Reka child overlay content that can render underneath that parent.
https://github.com/user-attachments/assets/d0d1522a-ffc7-4934-9e7a-06b83e20f809
1. **Workflow Template Library filters**
- **How to enter**: click the Templates button in the left sidebar, or
open the Comfy menu and choose **Browse Templates**.
- **Affected elements**: the template filter popovers in
`WorkflowTemplateSelectorDialog`: **Model**, **Use case**, **Runs on**,
and **Sort by**.
- **Patch point**: `WorkflowTemplateSelectorDialog.vue` anchors to the
template dialog content filter area and passes `selectContentStyle` to
the affected `MultiSelect` / `SingleSelect` controls.
https://github.com/user-attachments/assets/3641fa24-da51-4392-a904-9085f8a5a2f4
2. **Manager dialog header controls**
- **How to enter**: open Manager from the top/menu Manager entry when
the new Manager UI is available.
- **Affected elements**: the Manager header controls in `ManagerDialog`:
search mode `SingleSelect`, search autocomplete suggestions, and
**Sort** `SingleSelect`.
- **Patch point**: `ManagerDialog.vue` anchors to the dialog header and
passes `selectContentStyle` to those three Reka overlays.
https://github.com/user-attachments/assets/cf25cc06-f851-48ef-9d9c-9ec2da8afc06
3. **Asset Browser filter bar**
- **How to enter**: open the Asset Browser from an eligible model widget
browse action, the Model Library flow, or another
`useAssetBrowserDialog` caller.
- **Affected elements**: `AssetFilterBar` controls: **File formats**,
**Base models**, **Ownership**, and **Sort by**.
- **Patch point**: `AssetBrowserModal.vue` anchors to the PrimeVue
dialog header and passes the style through `AssetFilterBar` to its
`MultiSelect` / `SingleSelect` controls.
https://github.com/user-attachments/assets/e27bd805-10c0-4b3b-97f3-9e11faa47021
4. **Asset Browser model info panel**
- **How to enter**: open Asset Browser, select an asset, then use the
right-side model info panel.
- **Affected element**: the **Model type** select in `ModelInfoPanel`.
- **Patch point**: `AssetBrowserModal.vue` reuses the same parent-scoped
style and passes it to `ModelInfoPanel` as `selectContentStyle`.
https://github.com/user-attachments/assets/5e9f7ef0-ebd7-4987-ba1b-2137c034086f
5. **Upload Model confirmation step**
- **How to enter**: open Asset Browser, click **Upload**, enter/fetch
model metadata, then proceed to the confirmation step.
- **Affected element**: the **Model type** `SingleSelect` in
`UploadModelConfirmation`.
- **Patch point**: `UploadModelConfirmation.vue` anchors within the
upload dialog content and passes `selectContentStyle` to the model type
selector.
https://github.com/user-attachments/assets/ec145f26-8621-455b-915e-bedee47e1cbd
6. **Settings > Keybinding panel controls**
- **How to enter**: open Settings from the sidebar/menu, then select the
**Keybinding** panel.
- **Affected elements**: the keybinding preset select, the preset
overflow dropdown menu, and the row context menu inside
`KeybindingPanel`.
- **Patch point**: `KeybindingPanel.vue` anchors to the settings dialog
panel and passes `keybindingOverlayContentStyle` only to those Reka
overlay roots.
## Review Focus
- Confirm the patch stays narrowly scoped to the six known PrimeVue
parent + Reka child overlay surfaces above.
- Confirm `contentStyle` reaches the actual portaled Reka overlay
content in each patched path.
- Confirm the fallback behavior preserves existing stacking when no
PrimeVue parent overlay is found; in that case the helper returns an
empty style object and leaves existing Tailwind z-index classes alone.
- Please avoid expanding this into a larger overlay refactor. The goal
is a clean, backport-friendly compatibility patch while the Reka
migration continues.
Validation performed:
- `pnpm exec vitest run src/composables/usePopoverSizing.test.ts`
- `pnpm typecheck`
- `pnpm lint` (passes with existing unrelated warnings only)
- `pnpm format:check`
- commit hook lint-staged checks (`oxfmt`, `stylelint`, `oxlint`,
`eslint --fix`, `pnpm typecheck`)
- pre-push `pnpm knip`
Linear: FE-569
## Bug Screenshots
https://github.com/user-attachments/assets/e73761af-9867-4c50-ab0d-4e32e59011e1https://github.com/user-attachments/assets/145daf4d-3268-428b-9987-1e1afd0b866f
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12038-fix-keep-Reka-overlays-above-PrimeVue-dialogs-3596d73d365081e7af49dbc4d3905962)
by [Unito](https://www.unito.io)
## Root cause
`useAssetBrowser`'s `filteredAssets` computed re-ran
`.map(transformAssetForDisplay)` over the full result set on every tab
switch. `transformAssetForDisplay` allocates fresh `badges`/`stats`
objects, walks `tags`, calls `getAssetBaseModels`, and runs i18n date
formatting per asset — none of which were memoized. Switching All /
Inputs / Outputs forced N transforms per click and produced brand-new
`AssetDisplayItem` references, which also defeated `:key`-based diffing
in `AssetGrid` / `VirtualGrid` and re-rendered every visible card.
## Fix
Memoize `transformAssetForDisplay` at module scope with a
`WeakMap<AssetItem, AssetDisplayItem>`. Unchanged assets reuse the same
display item across tab switches; the GC reclaims entries when assets
are released.
## Before / after (n=200 assets, 6 tab switches: inputs → outputs → all
→ inputs → outputs → all)
| Metric | Before | After |
| ------------------------------- | -----: | ----: |
| `transformAssetForDisplay` runs | 800 | 0 |
| Wall time (Vitest harness) | 13.2 ms | 8.1 ms |
| Reused `AssetDisplayItem` refs | 0 | 200 |
Measured via
`src/platform/assets/composables/useAssetBrowser.perf.test.ts` plus a
temporary `process.stderr.write` harness.
## Red / green
| Commit | Purpose |
| --------- | ------- |
| 7367fdd60 | test: failing perf assertions (transform budget + reused
refs) |
| 021b98ac0 | perf: WeakMap memoization of `transformAssetForDisplay` |
## Test plan
- [x] `pnpm exec vitest run
src/platform/assets/composables/useAssetBrowser` — 42/42 pass (including
2 new perf assertions)
- [x] `pnpm typecheck`
- [x] `pnpm exec eslint` on touched files
- [x] `pnpm exec oxfmt --check` on touched files
Fixes [FE-229
](https://linear.app/comfyorg/issue/FE-229/asset-browser-switching-all-inputs-outputs-tabs-is-slow)Source:
https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1776716352588229
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11491-perf-memoize-asset-display-transform-across-filter-tab-switches-3496d73d36508112822dd6e7b58040fe)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
*PR Created by the Glary-Bot Agent*
---
Raises `Comfy.QueueButton.BatchCountLimit` on cloud from `32` to `100`
to match the server-side `MaxQueuedJobsPerUser` cap
(`cloud/infrastructure/dynamicconfig/prod/config.json:3`). The desktop
default was already `100` and is unchanged — collapsing both branches to
the same constant.
Addresses Discord feature request: [Increase queue batch limit from
200](https://discord.com/channels/1218270712402415686/1243609826299220039/1499104231381012641).
## Change
```diff
- defaultValue: isCloud ? 32 : 100,
+ defaultValue: 100,
```
The setting is read dynamically by all batch count UIs
(`BatchCountEdit.vue`, `LinearControls.vue`).
## Why 100 (not 512)
Original ask was 200→512. Investigation showed:
- The actual previous default was `100` (desktop) / `32` (cloud), not
200.
- Cloud enforces `MaxQueuedJobsPerUser = 100` per workspace server-side.
A higher frontend cap can't unlock more queued work — extra prompts just
get rejected with `QUEUE_LIMIT`.
- Frontend submits prompts as N sequential `POST /prompt` calls (no
batched-prompt endpoint), so the UI cap is purely about how many clicks
it takes — not throughput.
- Going from 32 → 100 lets cloud users match the server cap in one click
instead of 4. No new behavior is unlocked.
## Known limitation (pre-existing, not introduced here)
The new max equals the absolute server cap, not the user's remaining
capacity. A user with already-queued work can hit `QUEUE_LIMIT`
mid-batch. The pre-existing 32 limit had the same shape (just at a
smaller scale); deriving the UI max from `cap - outstanding` would
require polling and reactive state and is out of scope for a one-line
setting bump.
## Verification
- `pnpm typecheck` — passes
- `pnpm lint` — 0 errors (1 pre-existing warning in unrelated test file)
- `pnpm test:unit` — `BatchCountEdit.test.ts` (3 tests) +
`src/platform/settings/**` (70 tests) all pass
- **Manual (Playwright)**:
- `settingStore.get('Comfy.QueueButton.BatchCountLimit')` returns `100`
at runtime
- Typing `999` into the batch count widget clamps to `100`
- Increment button is disabled at `100` (max reached)
## Screenshots

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11876-feat-align-cloud-batch-count-limit-with-server-side-queue-cap-3566d73d3650819b8d01dbf83d1a8e49)
by [Unito](https://www.unito.io)
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Replace PrimeVue components in 3D node viewer controls with the
project's Reka UI equivalents across 7 files.
## Changes
| File | Replaced |
|------|---------|
| `AnimationControls.vue` | `Select` × 2 (speed + animation) |
| `ViewerModelControls.vue` | `Select` × 2 (up direction + material
mode) |
| `ViewerCameraControls.vue` | `Select` + `Slider` (camera type + FOV) |
| `ViewerExportControls.vue` | `Select` (export format) |
| `PopupSlider.vue` | `Slider` |
| `ViewerLightControls.vue` | `Slider` |
| `ViewerSceneControls.vue` | `Checkbox` → native `<input
type="checkbox">` |
## Implementation notes
- `Select` uses `@/components/ui/select/*` compound components. Numeric
model values (animation speed index) are stringified at the binding
boundary and converted back on update, matching Reka `SelectRoot`'s
`string`-only `modelValue` contract.
- `Slider` uses `@/components/ui/slider/Slider.vue`. Single-number
`defineModel` values are wrapped in a `computed` array and unwrapped in
the update handler, following the pattern established in
`LightControls.vue`.
- No new Reka UI wrapper components were created — existing ui/select
and ui/slider primitives were used directly.
## Test
https://github.com/user-attachments/assets/afca0fc8-a7b6-49ee-b221-ee5725bd127e
1. AnimationControls.vue
- **Add Load3D node** → Upload an animated GLB file (e.g., a character
model).
- **Node preview top bar:** Play/Pause button, speed dropdown, animation
name dropdown, and progress bar.
2. PopupSlider.vue
- **Hover over Load3D preview:** Icon buttons appear in the left
toolbar.
- **"Light Intensity" button (bulb icon)** → Slider pops up on the
right.
- **"FOV" button (view icon)** → Slider pops up on the right.
3. ViewerCameraControls.vue
- **Load3D node** → Settings panel (top-right) → **"Camera"** tab.
- **Features:** Camera type dropdown (Perspective / Orthographic), FOV
slider (visible in Perspective mode).
4. ViewerExportControls.vue
- **Settings panel** → **"Export"** tab.
- **Features:** Format dropdown (GLB / OBJ / STL), Export button.
5. ViewerLightControls.vue
- **Settings panel** → **"Light"** tab.
- **Features:** Light intensity slider.
6. ViewerModelControls.vue
- **Settings panel** → **"Model"** tab.
- **Features:** "Up direction" dropdown, Material mode dropdown
(Wireframe / Normal, etc.).
7. ViewerSceneControls.vue
- **Settings panel** → **"Scene"** tab.
- **Features:** Background color picker, "Show grid" checkbox, upload
background image button.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> UI component swap touches multiple interactive viewer controls
(selects/sliders/checkbox), so small binding/typing differences (string
vs number, array slider values) could cause subtle regressions despite
test updates.
>
> **Overview**
> Replaces PrimeVue `Select`, `Slider`, and `Checkbox` usages across
Load3D viewer controls with the project’s Reka UI-based primitives
(`@/components/ui/select/*`, `@/components/ui/slider/Slider.vue`) and a
native checkbox.
>
> Updates v-model wiring to match the new components’ contracts: selects
now bind via string `modelValue` with explicit number casting where
needed, and sliders now wrap single numeric values into `[number]`
arrays with corresponding update handlers. Unit tests are updated to
mock the new UI components and their updated event/value shapes.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
46f99db256. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12020-refactor-load3d-replace-PrimeVue-Select-Slider-Checkbox-with-Reka-UI-3586d73d365081f58601d93031016afd)
by [Unito](https://www.unito.io)
## Summary
Reuse the actionbar queue-button page object in the queue mode E2E tests
so dropdown selectors live in one helper.
## Changes
- **What**: Adds queue mode menu/item helpers to
`ComfyActionbar.queueButton` and updates `queueButtonModes.spec.ts` to
use them.
- **Dependencies**: None.
## Review Focus
This is stacked on #11209 and should be reviewed as a test-infra cleanup
only; the behavior covered by the spec is unchanged.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11927-test-reuse-queue-button-page-object-in-e2e-3566d73d365081918d59c2d587c4c94a)
by [Unito](https://www.unito.io)
## Summary
Make `userStore.initialize()` idempotent and concurrency-safe so the
bootstrap, router-guard, and UserSelectView callers share a single
getUserConfig fetch instead of racing/duplicaitng calls.
## Changes
- **What**:
- cache initialize in a promise so callers all re-use the same result
- remove now redundant is initialized guard
- tests
## Review Focus
- Current user switch/logout uses `window.location.reload()`, no callers
intentionally call initialize to reinit. In future if this changes we
may want to add a parameter to skip the cache or a separate function.
- Failed initializes are not cached to allow callers to retry
- Not practical for e2e tests, the unit tests prove that the requests
are deduped. All a e2e test would do is mock/spy on the network requests
to show multiple requests do not happen - which the unit tests do a
better job of.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11959-fix-add-guard-to-prevent-user-store-re-initialization-3576d73d3650817db7b0e52cc25f9b7b)
by [Unito](https://www.unito.io)
## Summary
- Defer thumbnail capture until camera state is restored via new
modelReady event so captureThumbnail no longer races with the saved
view, fixing the "snap back to default on hover" regression.
- Repaint the live scene at the end of captureThumbnail so the canvas is
not left with the offscreen mask/normal pass when the render loop is
gated.
- Persist post-fitToViewer model.scale + model.position into the
existing modelConfig.gizmo slot so a refresh reapplies them via the
existing applyGizmoConfigToLoad3d path; rotation stays owned by
upDirection.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11944-FE-537-fix-load3d-preserve-camera-view-fit-transform-and-first-frame-paint-after-re-3576d73d365081429653ea4740612617)
by [Unito](https://www.unito.io)
Adds targeted behavioral tests for the slimmed `useBrushDrawing`
orchestration composable (Phase E of the brush-drawing refactor).
## Changes
- 5 new tests covering previously untested branches:
- `compositeStroke` receives `isRgb=true` when active layer is `rgb`
- `compositeStroke` receives `isErasing=true` when tool is `eraser`
- Mask canvas opacity is restored after drawing on the mask layer
- `globalCompositeOperation` is set to `destination-out` during
`handleDrawing` when tool is eraser
- `globalCompositeOperation` is set to `destination-out` during
`handleDrawing` when right mouse button is held
## Coverage (useBrushDrawing.ts)
| Metric | Before | After |
|--------|--------|-------|
| Statements | 86.33% | 87.05% |
| Branches | 68.75% | 70.00% |
| Functions | 90.00% | 90.00% |
| Lines | 89.23% | 90.00% |
All 18 tests pass. GPU paths remain `/* c8 ignore */` excluded
(untestable without WebGL).
- Fixes#0
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Low risk: changes are limited to unit tests, adding coverage for
eraser/right-click composition and `drawEnd` GPU compositing/opacity
restoration paths without altering production logic.
>
> **Overview**
> Adds new `useBrushDrawing` test cases to cover previously untested
branches: setting `globalCompositeOperation` to `destination-out` during
`handleDrawing` when erasing (tool or right-click), and verifying
`drawEnd` passes correct `isRgb`/`isErasing` flags to
`gpu.compositeStroke`.
>
> Also asserts mask-layer opacity is restored after `drawEnd`,
increasing behavioral coverage around stroke completion and canvas
visibility cleanup.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
20c411e6ce. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12001-test-maskeditor-expand-useBrushDrawing-behavioral-coverage-3586d73d365081388ebcef91c2172c0a)
by [Unito](https://www.unito.io)
## Summary
Add a `@perf` test measuring the cost of entering a subgraph containing
80 interior nodes. Establishes a CI baseline for the synchronous
mount/unmount bottleneck.
## Changes
- **What**: Add `subgraph transition (enter and exit)` perf test to
`performance.spec.ts` and a test workflow asset
(`large-subgraph-80-nodes.json`) with a single subgraph node containing
80 Note nodes.
## Review Focus
This is PR 1 of 2. The test establishes a baseline on main so the
optimization PR (PR 2) can show a CI-proven delta for `taskDurationMs`
and `totalBlockingTimeMs`.
The test:
1. Loads the 80-node subgraph workflow
2. Enters and exits once to warm up
3. Measures a fresh enter transition (start → 80 nodes mounted → layout
settled)
4. Records `taskDurationMs`, `layouts`, and `TBT`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10480-test-add-perf-test-for-subgraph-transition-bottleneck-32d6d73d3650811b9b6eec03a9591f82)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
## Summary
Preview as Text (`PreviewAny`) nodes were re-executing on every prompt
submission because the rendered preview text was being echoed back to
the backend as input values, mutating the cache signature.
## Changes
- **What**: Set `widget.options.serialize = false` on the three widgets
the `Comfy.PreviewAny` extension adds (`preview_markdown`,
`preview_text`, `previewMode`) so they are excluded from the API prompt
sent to the backend.
## Root cause
The extension was setting `widget.serialize = false`, which only
controls **workflow JSON** persistence (checked in
`LGraphNode.serialize`). The **API prompt** serializer in
`executionUtil.graphToPrompt` checks `widget.options.serialize` instead
— a distinct property documented in litegraph's `WIDGET_SERIALIZATION`
convention.
After `onExecuted` writes the rendered text into the widget value, the
next `graphToPrompt` call serialized that text into
`inputs.preview_text` / `inputs.preview_markdown`. The backend cache
signature in `comfy_execution/caching.py` hashes all keys in
`node["inputs"]`, so the changing text invalidated the cache and forced
a redundant execution every time.
## Review Focus
Two commits, red-green TDD:
1. `test:` failing unit + e2e tests asserting the desired behavior.
2. `fix:` adds `options.serialize = false` to make them pass.
Tests verify the widgets are excluded from the API prompt; e2e
additionally simulates a prior execution populating widget values to
mirror the real bug condition.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12010-fix-stop-PreviewAny-widgets-from-triggering-re-execution-3586d73d3650810585cdd077f3ac64f5)
by [Unito](https://www.unito.io)
## Changes
Add Playwright E2E tests for queue button modes in the topbar (6 tests):
- Run button visibility in topbar
- Queue mode trigger menu visibility
- Opening the mode menu via trigger click
- Verifying available modes are shown as menu item radios
- Menu closes after selecting a mode
- Run button sends prompt when clicked (via intercepted `/api/prompt`
route)
## Test
```bash
pnpm test:browser:local -- browser_tests/tests/queueButtonModes.spec.ts
```
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11209-test-add-E2E-tests-for-queue-button-modes-3416d73d365081a5bf10f8b9c6bdc2a7)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
*PR Created by the Glary-Bot Agent*
---
## Summary
Adds 22 Playwright E2E tests for the keybinding settings panel
(`KeybindingPanel.vue`), covering all 15 previously-untested functions
identified via coverage analysis.
## Test Groups
| Group | Tests | Functions Covered |
|---|---|---|
| Row Expansion | 2 | `handleRowClick`, `toggleExpanded`, `expandedRows`
|
| Double-Click | 2 | `handleRowDblClick`, `addKeybinding`,
`editKeybinding` |
| Context Menu | 7 | `handleRowContextMenu`, `clearContextMenuTarget`,
`ctxChangeKeybinding`, `ctxAddKeybinding`, `ctxResetToDefault`,
`ctxRemoveKeybinding` |
| Action Buttons | 7 | `editKeybinding`, `resetKeybinding`,
`removeSingleKeybinding`, `handleRemoveAllKeybindings`,
`handleRemoveKeybindingFromMenu` |
| Expanded Row Actions | 2 | `editKeybinding` (expansion),
`removeSingleKeybinding` (expansion) |
| Reset All | 2 | `resetAllKeybindings` (confirm + cancel) |
| Search Filter | 1 | `watch(filters, ...)` clears expansion |
## Flake Prevention Measures
- Deterministic test command
(`TestCommand.KeybindingPanelE2E.NoBinding`) registered via
`app.registerExtension()` for 0-binding scenarios — avoids flaky
pagination-dependent row scanning
- `pressComboOnInput()` asserts input focus before pressing key combos
in the edit dialog
- `saveAndCloseKeybindingDialog()` waits for dialog teardown to complete
before proceeding
- `openContextMenu()` waits for Reka UI menu items to be visible before
interacting
- Resets `Comfy.Keybinding.NewBindings` and
`Comfy.Keybinding.UnsetBindings` in `afterEach`
## Note on Selectors
Some locators use Tailwind utility classes (`.pl-4`,
`.icon-[lucide--chevron-right]`) because the expansion template and
chevron icon in `KeybindingPanel.vue` lack `data-testid` attributes.
Adding test IDs would be a follow-up to this test-only PR.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11455-test-add-E2E-tests-for-keybinding-settings-panel-coverage-3486d73d365081d7a902fc68091552f2)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
- Distinguish 'no result' (undefined) from 'empty options' ([]) so a
legitimate empty combo list propagates to the widget instead of being
treated as a no-op.
- Fall back to input.name when the connected target's input has no
widget locator, mirroring _onFirstConnection.
- Add regression tests for V2 combo specs, empty option propagation,
and the widgetless-input fallback path.
Pressing 'r' (Refresh Node Definitions) calls refreshComboInNode on every
node. For primitives, the previous override re-derived combo values from
this.outputs[0].widget[GET_CONFIG]()[0] without null checks. When that
returned undefined (stale slot widget reference after subgraph traversal
rebuilds widgets), widget.options.values was clobbered with undefined
and the dropdown went blank.
Now the override:
- Uses the freshly passed defs to resolve combo values from the connected
node + input, matching what reloadNodeDefs does for normal combo widgets.
- Falls back to outputs[0].widget[GET_CONFIG]() when defs are unavailable
or do not include the target type.
- Preserves existing options if both lookups fail rather than overwriting
with undefined.
Add regression tests covering the defs-driven refresh, the slot-config
fallback, the missing-config preservation case, value reset when the
current value is no longer valid, no-op when it is still valid, and
non-combo widgets being skipped.
2026-04-29 05:00:37 +00:00
613 changed files with 44780 additions and 4951 deletions
description: 'add, update, or remove a model page entry on the comfy org website. creates a PR to Comfy-Org/ComfyUI_frontend apps/website folder with the change and posts a Vercel preview link back to Slack.'
---
# add-model-page
add, update, or remove model pages in the ComfyUI website.
## Trigger phrases
-`Add a model page for <model-name>`
-`Update the model page for <model-name>`
-`Remove <model-name> from model pages`
## Phase 1 — Parse the request
Extract:
- **action**: `add` | `update` | `remove`
- **model-name**: raw string (e.g. `flux1-schnell`, `flux1_dev.safetensors`)
Normalize to a slug: lowercase, replace `_` and `.` with `-`, strip file extensions.
Example: `flux1_dev.safetensors` → `flux1-dev`
## Architecture overview
Models come from two sources merged at build time:
Use only PR metadata already available in the review context: the PR title, commit subjects in this PR, the files changed in this PR relative to the PR base (equivalent to `base...head`), and the PR description.
Do not rely on shell commands. Do not inspect reverse diffs, files changed only on the base branch, or files outside this PR. If the changed-file list or commit subjects are unavailable, mark the check inconclusive instead of guessing.
Use only PR metadata already available in the review context:
- the PR title
- commit subjects in this PR
- The files changed in this PR relative to the PR base (equivalent to `base...head`)
- the PR description.
Do not rely on shell commands.
Do not inspect reverse diffs, files changed only on the base branch, or files outside this PR.
If the changed-file list or commit subjects are unavailable, mark the check inconclusive instead of guessing.
Pass if at least one of the following is true:
1. Neither the PR title nor any commit subject in the PR uses bug-fix language such as `fix`, `fixed`, `fixes`, `fixing`, `bugfix`, or `hotfix`.
2. The PR changes at least one file under `browser_tests/`.
3. The PR description includes a concrete, non-placeholder explanation of why an end-to-end regression test was not added.
Fail if all of the following are true:
1. The PR title and/or any commit subject in the PR uses bug-fix language such as `fix`, `fixed`, `fixes`, `fixing`, `bugfix`, or `hotfix`.
2. The PR changes files under `src/` or `packages/` related to the main frontend application but the PR does not change at least one file under `browser_tests/`.
3. The PR description lacks a concrete explanation of why an end-to-end regression test was not added.
Do not fail if the changes are exclusively in `apps/website`, just documentation changes, or changes related to CI processes.
The goal is to make sure that fixes include End-to-End regression tests. Do not insist on tests when the PR is not fixing a bug.
Pass otherwise.
When failing, mention which bug-fix signal you found and ask the author to either add or update a Playwright regression test under `browser_tests/` or add a concrete explanation in the PR description of why an end-to-end regression test is not practical.
Fail otherwise. When failing, mention which bug-fix signal you found and ask the author to either add or update a Playwright regression test under `browser_tests/` or add a concrete explanation in the PR description of why an end-to-end regression test is not practical.
- name:ADR compliance for entity/litegraph changes
echo "::error title=Missing WEBSITE_CLOUD_API_KEY::Production builds require WEBSITE_CLOUD_API_KEY so /cloud/supported-nodes is generated from fresh Cloud API data. Add it as a GitHub Actions repo secret and to the Vercel project environment. See apps/website/README.md."
The script exits non-zero on any non-fresh outcome so stale/empty
snapshots can't be accidentally committed.
## Cloud nodes integration
`/cloud/supported-nodes` (and `/zh-CN/`) lists custom-node packs preinstalled on Comfy Cloud, joined with public metadata from the [ComfyUI Custom Node Registry](https://registry.comfy.org) ([`api.comfy.org`](https://api.comfy.org)). See [`src/pages/cloud/supported-nodes/AGENTS.md`](src/pages/cloud/supported-nodes/AGENTS.md) for the build pipeline, source-file map, and key invariants.
Build-time env var: `WEBSITE_CLOUD_API_KEY` (Cloud `/api/object_info` auth; the build falls back to the committed snapshot when unset). Must also be set in the Vercel project environment.
### Production strictness
`src/utils/cloudNodes.build.ts` throws when `fetchCloudNodesForBuild()` returns
`{ status: 'stale' }`**and**`process.env.VERCEL_ENV === 'production'`. This
prevents the production deploy from silently shipping an out-of-date snapshot
when the Cloud API is unreachable or `WEBSITE_CLOUD_API_KEY` is missing. Preview
and local builds continue to use the committed snapshot with a warning
"description":"Production-grade detailer, detector, and SEG (segmentation) tooling. The most-used pack for face restoration, region-based refinement, and iterative upscaling on Comfy Cloud.",
"description":"Detect and refine faces with iterative passes."
},
{
"name":"DetailerForEach",
"displayName":"DetailerForEach",
"category":"ImpactPack/Detailer",
"description":"Run iterative detail refinement over detected SEG regions."
},
{
"name":"UltralyticsDetectorProvider",
"displayName":"UltralyticsDetectorProvider",
"category":"ImpactPack/Detector",
"description":"Provide detector models powered by Ultralytics YOLO."
},
{
"name":"SAMLoader",
"displayName":"SAMLoader",
"category":"ImpactPack/Detector",
"description":"Load Segment Anything models for high-fidelity masking."
},
{
"name":"MaskToSEGS",
"displayName":"MaskToSEGS",
"category":"ImpactPack/Operation",
"description":"Convert binary masks into SEGS regions for the detailer pipeline."
}
]
},
{
"id":"ComfyUI-Crystools",
"registryId":"ComfyUI-Crystools",
"displayName":"ComfyUI-Crystools",
"description":"Live system monitoring (GPU, RAM, disk) and rich image inspection inside your workflow. The most-installed quality-of-life pack on the registry.",
"description":"Display GPU, RAM and disk usage live in the workflow."
},
{
"name":"CCrystools_Show_Image",
"displayName":"CCrystools_Show_Image",
"category":"crystools/show",
"description":"Inspect images at full resolution with metadata overlays."
},
{
"name":"CCrystools_Json",
"displayName":"CCrystools_Json",
"category":"crystools/json",
"description":"Compose and parse JSON inline for advanced workflows."
},
{
"name":"CCrystools_Pipe_To_Any",
"displayName":"CCrystools_Pipe_To_Any",
"category":"crystools/pipe",
"description":"Convert a pipe bus into individual outputs."
},
{
"name":"CCrystools_Save_Metadata",
"displayName":"CCrystools_Save_Metadata",
"category":"crystools/save",
"description":"Save images with workflow metadata embedded."
}
]
},
{
"id":"rgthree-comfy",
"registryId":"rgthree-comfy",
"displayName":"rgthree-comfy",
"description":"Quality-of-life nodes that make complex workflows readable: Power Lora Loader, group bypassers, smarter reroutes, and inline debug widgets.",
"description":"Match the colors of one image to another using statistics."
},
{
"name":"ImageResizeKJ",
"displayName":"ImageResizeKJ",
"category":"KJNodes/image",
"description":"Resize images with intuitive size and divisibility controls."
},
{
"name":"StringConstantMultiline",
"displayName":"StringConstantMultiline",
"category":"KJNodes/string",
"description":"A multi-line string constant suitable for prompts."
},
{
"name":"EmptyLatentImagePresets",
"displayName":"EmptyLatentImagePresets",
"category":"KJNodes/latent",
"description":"Quickly create empty latents at common resolutions."
},
{
"name":"GetImageSizeAndCount",
"displayName":"GetImageSizeAndCount",
"category":"KJNodes/image",
"description":"Read width, height and batch size from an image input."
}
]
},
{
"id":"comfyui-easy-use",
"registryId":"comfyui-easy-use",
"displayName":"ComfyUI-Easy-Use",
"description":"Simplified, opinionated nodes that bundle common patterns into single drop-ins — full loader, pre-sampling, easy KSampler, and XY plotting.",
"description":"Combined checkpoint, VAE and CLIP loader with sensible defaults."
},
{
"name":"easy preSampling",
"displayName":"easy preSampling",
"category":"EasyUse/PreSampling",
"description":"A unified pre-sampling node bundling common settings."
},
{
"name":"easy kSampler",
"displayName":"easy kSampler",
"category":"EasyUse/KSampler",
"description":"A simplified KSampler with extra quality-of-life options."
},
{
"name":"easy showAnything",
"displayName":"easy showAnything",
"category":"EasyUse/Util",
"description":"Display any value inline for debugging."
},
{
"name":"easy XYPlot",
"displayName":"easy XYPlot",
"category":"EasyUse/XYPlot",
"description":"Compose XY plots over arbitrary parameters."
}
]
},
{
"id":"comfyui-advanced-controlnet",
"registryId":"comfyui-advanced-controlnet",
"displayName":"ComfyUI-Advanced-ControlNet",
"description":"ControlNet with timestep keyframes, per-frame masks, and advanced strength scheduling — essential for animation and batched-latent workflows.",
"description":"Apply ControlNet with timestep keyframes and per-frame masks."
},
{
"name":"ControlNetLoaderAdvanced",
"displayName":"ControlNetLoaderAdvanced",
"category":"Adv-ControlNet/loaders",
"description":"Load ControlNet models with the advanced wrapper."
},
{
"name":"Latent Keyframe Group",
"displayName":"Latent Keyframe Group",
"category":"Adv-ControlNet/keyframes",
"description":"Schedule ControlNet strength over a batch of latents."
},
{
"name":"Timestep Keyframe",
"displayName":"Timestep Keyframe",
"category":"Adv-ControlNet/keyframes",
"description":"Set ControlNet strength at a specific timestep."
},
{
"name":"Scaled Soft Mask",
"displayName":"Scaled Soft Mask",
"category":"Adv-ControlNet/masks",
"description":"Apply a soft attention mask to ControlNet conditioning."
}
]
},
{
"id":"was-node-suite-comfyui",
"registryId":"was-node-suite-comfyui",
"displayName":"WAS Node Suite",
"description":"A broad utility suite covering image adjustments, compositing, text, math, and I/O — the original \"kitchen sink\" pack still relied on by thousands of workflows.",
"description":"Adjust brightness, contrast, saturation and more."
},
{
"name":"Image Blending Mode",
"displayName":"Image Blending Mode",
"category":"WAS Suite/Image/Compositing",
"description":"Composite two images with Photoshop-style blend modes."
},
{
"name":"Text String",
"displayName":"Text String",
"category":"WAS Suite/Text",
"description":"A reusable text constant suitable for prompts."
},
{
"name":"Number to Float",
"displayName":"Number to Float",
"category":"WAS Suite/Number",
"description":"Cast integer or string values to a float."
},
{
"name":"Image Save",
"displayName":"Image Save",
"category":"WAS Suite/IO",
"description":"Save an image to disk with rich filename templating."
}
]
},
{
"id":"comfyui_ipadapter_plus",
"registryId":"comfyui_ipadapter_plus",
"displayName":"ComfyUI_IPAdapter_plus",
"description":"Reference-image conditioning with IPAdapter — style transfer, Face ID, and multi-image embeddings. The most-installed conditioning pack on the registry, used in countless portrait, product, and animation workflows.",
Build-time catalog of custom-node packs preinstalled on Comfy Cloud. Index at `/cloud/supported-nodes`, per-pack details at `/cloud/supported-nodes/[pack]`, both also under `/zh-CN/`.
## Sources
- **Cloud `/api/object_info`** — authoritative list of nodes available on Comfy Cloud (auth: `WEBSITE_CLOUD_API_KEY`)
- **ComfyUI Custom Node Registry** ([dashboard](https://registry.comfy.org), API at `https://api.comfy.org/nodes`) — public pack metadata (banner, icon, description, downloads, stars, license, version, repo, publisher)
The registry is the same one the in-app Manager dialog reads from. For reference and additional reading, see the existing client wrappers in `src/`:
- [`src/services/comfyRegistryService.ts`](../../../../../../src/services/comfyRegistryService.ts) — typed wrappers around `/nodes`, `/nodes/search`, `/nodes/{id}`, `/nodes/{id}/versions/{version}/comfy-nodes`, etc.
- [`src/stores/comfyRegistryStore.ts`](../../../../../../src/stores/comfyRegistryStore.ts) — cached store + `getPacksByIds` batch helper
- **Sanitization is mandatory.** The cloud `/api/object_info` endpoint mixes the calling user's uploaded files into combo input lists; `sanitizeUserContent()` strips them before the snapshot is written. Tests in [`packages/object-info-parser/src/__tests__/sanitizeUserContent.test.ts`](../../../../../../packages/object-info-parser/src/__tests__/sanitizeUserContent.test.ts).
- **All remote data is Zod-validated.** `cloudNodes.schema.ts` validates the cloud envelope; `@comfyorg/object-info-parser` validates each node def; `cloudNodes.registry.ts` validates the registry response shape with a passthrough Zod schema (defense-in-depth on top of the generated OpenAPI types). The fetcher never trusts a network response.
- **JSON-LD must be escaped.** Pack metadata is registry-controlled; route every `<script type="application/ld+json">` payload through `escapeJsonLd()`.
- **Index and detail share one fetch.** Both call `loadPacksForBuild()` so the static routes can never diverge from the rendered list.
- **No `PUBLIC_` prefix on env vars.** Astro inlines `PUBLIC_*` into the client bundle; build-time secrets never start with that prefix.
## Why `Pack` is a domain projection, not the raw registry `Node`
`apps/website/src/data/cloudNodes.ts` defines its own `Pack` and `PackNode` types instead of re-exporting `components['schemas']['Node']` from `@comfyorg/registry-types`. That is intentional:
- A `Pack` joins **two upstream sources** — the cloud `object_info` shape (which has `python_module`, `category`, `display_name`, etc.) and the registry `Node` shape (which has `banner_url`, `downloads`, `github_stars`, etc.). Neither shape on its own captures both.
- A `Pack` is **post-sanitization**. Combo input lists, user-uploaded filenames, and any other build-time-stripped data are guaranteed gone by the time the type appears. The raw `Node` carries fields we never expose.
- A `Pack` adds **safe-URL invariants** (banner / icon / repo all flow through `safeExternalUrl()` before becoming part of the type), which the registry's optional `string` fields don't encode.
The `Pack`/`PackNode` types live next to the snapshot they describe; the validated build-time projections feed Astro's static routes directly. The OpenAPI-generated `Node` type is still consumed inside `cloudNodes.registry.ts` as `RegistryPack` for the upstream fetch.
const canonicalURL = new URL(`/p/supported-models/${model.slug}`, Astro.site)
const dirDescriptions: Record<string, string> = {
diffusion_models: 'a diffusion model that generates images or video from text and image prompts',
checkpoints: 'an all-in-one checkpoint model that bundles a diffusion model, text encoder, and VAE',
loras: 'a LoRA (Low-Rank Adaptation) that fine-tunes an existing model for a specific style or subject',
controlnet: 'a ControlNet that steers image generation using structural guides like depth maps, edges, or poses',
clip_vision: 'a CLIP Vision encoder that converts images into embeddings for conditioning or style transfer',
vae: 'a VAE (Variational Autoencoder) that encodes and decodes latent representations',
text_encoders: 'a text encoder that converts prompts into embeddings used to guide generation',
audio_encoders: 'an audio encoder that converts audio into embeddings for audio-conditioned generation',
upscale_models: 'an upscale model that increases image resolution while preserving or enhancing detail',
latent_upscale_models: 'a latent upscale model that refines latents at higher resolution before decoding',
style_models: 'a style model that transfers artistic style onto generated images',
model_patches: 'a model patch that modifies or extends the behavior of an existing base model',
partner_nodes: 'a cloud API model accessible through ComfyUI partner nodes without local hardware requirements'
}
const dirDesc = dirDescriptions[model.directory] ?? 'an AI model'
const whatIsDescription = `${displayName} is ${dirDesc}. You can run it locally in ComfyUI with full control over every parameter, or access it through Comfy Cloud. ComfyUI's node-based workflow editor lets you connect ${displayName} with ControlNets, LoRAs, upscalers, and custom nodes to build any pipeline you need. There are ${model.workflowCount} community workflow templates using ${displayName} on Comfy Hub, ready to load and customize.`
const softwareAppJsonLd = {
'@context': 'https://schema.org',
'@type': 'SoftwareApplication',
name: displayName,
applicationCategory: 'MultimediaApplication',
operatingSystem: 'Any',
url: canonicalURL.href,
author: {
'@type': 'Organization',
name: 'Comfy Org',
url: 'https://comfy.org'
}
}
const breadcrumbJsonLd = {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: [
{
'@type': 'ListItem',
position: 1,
name: t('models.breadcrumb.home'),
item: 'https://comfy.org'
},
{
'@type': 'ListItem',
position: 2,
name: t('models.breadcrumb.models'),
item: 'https://comfy.org/p/supported-models'
},
{
'@type': 'ListItem',
position: 3,
name: displayName
}
]
}
const faqJsonLd = {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: [
{
'@type': 'Question',
name: `What is ${displayName}?`,
acceptedAnswer: {
'@type': 'Answer',
text: whatIsDescription
}
},
{
'@type': 'Question',
name: `How do I use ${displayName} in ComfyUI?`,
acceptedAnswer: {
'@type': 'Answer',
text: model.docsUrl
? `Follow the step-by-step tutorial at ${model.docsUrl}. You can also load any of the ${model.workflowCount} community workflow templates that use ${displayName} directly in ComfyUI.`
: `Open ComfyUI and browse the ${model.workflowCount} community workflow templates that use ${displayName}. Load one as a starting point, then customize the nodes and parameters to fit your use case.`
}
},
{
'@type': 'Question',
name: `How many ComfyUI workflows use ${displayName}?`,
acceptedAnswer: {
'@type': 'Answer',
text: `There are ${model.workflowCount} community workflow templates that use ${displayName} on Comfy Hub. Each template is ready to run in ComfyUI and can be customized to suit your project.`
}
},
{
'@type': 'Question',
name: `Is ${displayName} free to use in ComfyUI?`,
acceptedAnswer: {
'@type': 'Answer',
text: `ComfyUI is free and open source. ${model.huggingFaceUrl ? `${displayName} weights are available to download from Hugging Face.` : `${displayName} is available as a cloud API through Comfy Cloud.`} You only pay for compute when running on Comfy Cloud; local inference on your own hardware is always free.`
}
}
]
}
const pageTitle = `${displayName} in ComfyUI`
const pageDescription = `Run ${displayName} in ComfyUI with full parameter control. ${model.workflowCount} community workflow templates, step-by-step tutorials, and free local inference.`
`::warning title=Cloud nodes: dropped ${outcome.droppedCount} invalid ${nodeCount}::Dropped nodes:%0A${drops}%0A%0AAction items:%0A 1. Verify node definitions returned by cloud /api/object_info.%0A 2. If a valid node shape changed, update @comfyorg/object-info-parser/src/schemas/nodeDefSchema.ts and add tests.%0A 3. Dropped nodes are not shown on /cloud/supported-nodes until fixed.`
]
}
if(outcome.status==='stale'){
return[staleAnnotation(outcome.reason)]
}
return[
`::error title=Cloud nodes fetch failed and no snapshot is available::Cannot build cloud nodes page without data.%0A%0AReason: ${escapeAnnotation(outcome.reason)}%0A%0AAction items:%0A 1. Run \`pnpm --filter @comfyorg/website cloud-nodes:refresh-snapshot\` locally with a valid WEBSITE_CLOUD_API_KEY.%0A 2. Commit apps/website/src/data/cloud-nodes.snapshot.json.%0A 3. Push and re-run CI.`
]
}
functionstaleAnnotation(reason: string):string{
constescaped=escapeAnnotation(reason)
if(reason.startsWith('missing ')){
return`::warning title=Cloud nodes integration::${escaped}. Falling back to committed snapshot.%0A%0AAction items:%0A 1. If you're a contributor without key access, this is expected. The snapshot will be used.%0A 2. If this is CI, check that the \`WEBSITE_CLOUD_API_KEY\` secret exists in the repo and is referenced in .github/workflows/ci-website-build.yaml.`
return`::error title=Cloud nodes authentication failed::${escaped}. The WEBSITE_CLOUD_API_KEY is missing, invalid, or revoked. Build continues with the last-known-good snapshot.%0A%0AAction items:%0A 1. Verify the Cloud API key is active and scoped for /api/object_info.%0A 2. Update the \`WEBSITE_CLOUD_API_KEY\` secret in GitHub Actions and Vercel.%0A 3. Re-run this workflow.`
}
if(reason.startsWith('envelope')){
return`::error title=Cloud nodes schema mismatch::${escaped}. The Cloud API contract likely changed. Build continues with the snapshot, but future updates will fail until schema is fixed.%0A%0AAction items:%0A 1. Check cloud/services/ingest/openapi.yaml for /api/object_info changes.%0A 2. Update apps/website/src/utils/cloudNodes.schema.ts and @comfyorg/object-info-parser schema as needed.`
}
return`::warning title=Cloud nodes API unavailable::${escaped}. Using last-known-good snapshot.%0A%0AAction items:%0A 1. Check cloud service health.%0A 2. Re-run this workflow once cloud.comfy.org is healthy.`
Some files were not shown because too many files have changed in this diff
Show More
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.