## Summary
Allow staging api/platform base URLs to be overridden by env vars so
non-cloud builds can target an alternate backend without source edits.
## Changes
- **What**: `BUILD_TIME_API_BASE_URL` / `BUILD_TIME_PLATFORM_BASE_URL`
in `src/config/comfyApi.ts` now read
`import.meta.env.VITE_STAGING_API_BASE_URL` /
`VITE_STAGING_PLATFORM_BASE_URL` first, falling back to the existing
`stagingapi.comfy.org` / `stagingplatform.comfy.org` constants. Vars
typed in `src/vite-env.d.ts` and documented in `.env_example`.
- **Breaking**: None. Defaults unchanged. The cloud-runtime override
path via the features endpoint (`comfy_api_base_url`,
`comfy_platform_base_url` in `RemoteConfig`) is untouched.
## Review Focus
Override only applies to the non-prod branch of the build-time ternary,
so prod builds (`USE_PROD_CONFIG=true`) cannot be redirected. Cloud
builds continue to resolve URLs at runtime via `remoteConfig` regardless
of these env vars.
## Note
Pre-commit `pnpm typecheck` fails on `origin/main` independently of this
change (`src/utils/nodeDefUtil.ts` and
`src/workbench/utils/nodeHelpUtil.ts` import non-existent exports from
`@/schemas/nodeDefSchema` / `@/types/nodeSource`). Verified by stashing
this PR's diff and re-running. Committed with `--no-verify`; please
address the underlying breakage separately.
This is a targeted small scope change to improve the availability for
converting group nodes into a subgraph.
The prior implementation would only apply on the litegraph context menu
option for converting a node to a subgraph. It failed to apply on any of
the other more common methods. The code for unpacking group nodes has
been moved directly into the setup for converting a group of nodes into
a subgraph and drastically simplified.
Of note, several other long lived bugs were found while working on this
fix, but they are out of scope for this targeted PR.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12356-On-subgraph-conversion-always-unpack-group-nodes-3666d73d365081d09774c00a851b8198)
by [Unito](https://www.unito.io)
## Summary
Import published media assets for shared workflows before loading the
graph so the first missing-media scan sees the user's newly imported
references instead of surfacing a false missing asset error. cc FE-773
## Changes
- **What**: Moves the shared workflow import step ahead of
`loadGraphData` for the copy-and-open flow, while still allowing the
workflow to open with a warning path if asset import fails.
- **What**: Clears the shared workflow URL intent consistently on
failure paths, including graph load failure after an import attempt, so
reloads do not repeatedly replay the same shared workflow side effects.
- **What**: Invalidates the input asset cache after published asset
import so graph loading and missing-media resolution can observe the
refreshed media state.
- **What**: Adds a global loading spinner while shared workflow asset
import and graph load are in progress, with `role="status"`,
`aria-live`, reduced-motion-safe animation, and body teleporting so it
stays visible above blocking UI.
- **What**: Adds stable TestIds for the shared workflow dialog and
updates existing shared workflow E2E selectors away from copy-dependent
role text.
- **What**: Adds a cloud E2E regression fixture and spec covering the
critical flow: shared URL opens the dialog, the user confirms asset
import, published media is imported before the public-inclusive input
asset scan, the workflow loads, the share query is removed, and missing
media UI is not surfaced.
- **Breaking**: None.
- **Dependencies**: None.
## Root Cause
Shared workflow graph loading triggered the missing-media pipeline
before the user-selected published media import had completed. Because
`include_public=true` does not include published assets, the pre-import
scan could classify shared media as missing even when the user was about
to import those assets into their own library.
## Review Focus
- The ordering in `useSharedWorkflowUrlLoader`: import published assets
first, then load the graph, while keeping import failure non-fatal for
workflow opening.
- The failure cleanup behavior: the shared URL/preserved query intent is
now cleared for graph load failures too, avoiding repeated
reload-triggered imports.
- The spinner behavior in `App.vue`: it uses the existing
`workspaceStore.spinner` boolean and intentionally keeps broader
ref-counted spinner ownership as follow-up work.
- The E2E sentinel in `sharedWorkflowMissingMedia.spec.ts`: it asserts
no public-inclusive input asset scan occurs before `/api/assets/import`,
then waits for a settling window to ensure the missing-media overlay
does not appear.
## Validation
- `pnpm format`
- `pnpm lint` (passed with existing unrelated warnings only)
- `pnpm typecheck`
- `pnpm test:unit`
- Commit hook: lint-staged formatting/linting, `pnpm typecheck`, `pnpm
typecheck:browser`
- Push hook: `pnpm knip --cache` (passed with existing tag hint only)
## Follow-Up
- Consider a ref-counted or scoped global spinner API so long-running
flows do not directly toggle `workspaceStore.spinner`.
- Consider separating shared workflow load status into orthogonal result
fields instead of encoding partial success in a single string union.
- Consider moving published asset import/cache invalidation behind an
asset-service-owned API boundary.
- Backend follow-up remains needed for `include_public=true` not
including published assets; this PR only removes the frontend false
positive when the user explicitly imports the shared media.
## Screenshots
Before
https://github.com/user-attachments/assets/dc790046-237c-4dd8-b773-2507f9a66650
After
https://github.com/user-attachments/assets/6517cd38-2c3d-4bfe-a990-35892b7e50aehttps://github.com/user-attachments/assets/d89dc3d3-75d9-4251-998b-0c354414e25b
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12333-fix-avoid-false-missing-media-errors-after-importing-shared-workflow-assets-3656d73d365081b38634dcb7625cfc32)
by [Unito](https://www.unito.io)
## Summary
Two fixes for the cloud LoadImage form dropdown:
1. **Cloud root-cause fix** — outputs now come from a single
`getAssetsByTag('output')` call instead of walking the jobs API and
per-job `resolveOutputAssetItems` detail fetches. Per Christian's [Slack
feedback](https://comfy-organization.slack.com/archives/C0A4XMHANP3/p1778051260476369?thread_ts=1776716352.588229&cid=C0A4XMHANP3):
*"on cloud, we can just grab the assets with a single GET, filtering
with input or output tag."* Sidebar's job-stack UX is untouched.
2. **Local / defense-in-depth** — even when the watch+expansion path is
in play (still used by local), batch all in-flight
`resolveOutputAssetItems` for the current `media` snapshot via
`Promise.all`, committing once into `resolvedByJobId`. This kills the
progressive head-shift symptom even on the legacy path.
The first attempt at (1) (`6a1a083c9`, reverted in `c175962e8`) broke
select+load on cloud prod because the dropdown wrote `asset.name` (human
filename) into the widget value, but cloud's `/api/view` resolves output
files by **`asset_hash`** (the blake3-keyed filename). Verified against
cloud prod that every output row carries `asset_hash` and that cloud's
own `preview_url` is hash-keyed, not name-keyed. Re-introduced in
`d7693377` with the dropdown value derived from `asset.asset_hash ||
asset.name`, with the human filename retained as the display label.
- Fixes FE-227
## Cloud / local divergence — what this PR clarifies
| | input | output (this PR) |
| ------------ | ---------------------------------------------- |
-------------------------------------------------------- |
| **cloud** | `getAssetsByTag('input')` (already correct) |
**`getAssetsByTag('output')` (new)** |
| **local** | `/files/input` (FS-listing) | `getHistory` + per-job
expansion (unchanged) |
Both directions are now symmetric on cloud: tag-based listing,
hash-keyed values. Local stays on the legacy path because core ComfyUI
doesn't have the assets/tags model — that's the deeper convergence
Jacob/Luke flagged in the FE-556 thread (now BE-757), which is BE/Core
work and not this PR.
## Red-Green verification
| Commit | CI: Tests Unit | Purpose |
|--------|----------------|---------|
| `3e8d42e7` test | 🔴 [failure
(25413987208)](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25413987208)
| Asserts the head of the list does not shift while one of two
multi-output jobs is still resolving. |
| `fe2608d4` fix (atomic batch) | 🟢 [success
(25414246791)](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/25414246791)
| Resolutions awaited via `Promise.all` and merged in one
`resolvedByJobId` update. |
| `6a1a083c` simplification (broken) | — | First attempt — used
`asset.name`, broke select+load on cloud prod. |
| `c175962e` revert | — | Rolled back the broken simplification while
diagnosis was in flight. |
| `d7693377` simplification (fixed) | pending | Re-introduces
`useFlatOutputAssets` and uses `asset.asset_hash` for the dropdown
value. Adds 7 unit tests covering hash-as-value, name-fallback,
pagination, dedupe, and error path. |
## screenshot/ video
### before
https://github.com/user-attachments/assets/239aa447-a260-4713-926c-04dd80a30408
### after
https://github.com/user-attachments/assets/d68228c6-33f5-4bf0-ad24-bb83c876fdc2
## Test plan
- [x] New `useFlatOutputAssets.test.ts` — 7 tests for tag-based
fetching, pagination, dedupe, error path.
- [x] `useWidgetSelectItems.test.ts` — atomic-batching regression test +
new tests asserting hash-as-value and local name-fallback. 35 tests
pass.
- [x] `WidgetSelectDropdown.test.ts` — 5 tests pass with the new
conditional source.
- [x] CI red on test-only commit, CI green on first fix commit.
- [ ] CI green on the simplification (re-introduce) commit.
- [ ] Manual verification on cloud build: open LoadImage → switch to
Outputs → scroll → list head stays stable; select an output → LoadImage
preview loads (was broken in `6a1a083c`, restored in `d7693377`).
## Summary
Avoid a second `comfyPage.setup()` in the job history browser tests by
registering the initial jobs route mocks before the normal page boot.
## Changes
- **What**: Adds an `initialJobsScenario` Playwright option with an auto
fixture for the job-history spec, moves QPOV2 setup into
`initialSettings`, and keeps the sidebar helper focused on UI
navigation.
- **Dependencies**: None
## Review Focus
Confirm the auto fixture ordering matches the intended browser-test
setup: initial `/api/jobs` mocks should be installed before the
`comfyPage` fixture performs its normal setup.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12324-test-avoid-job-history-double-setup-3656d73d365081778e24c11d3b65cbef)
by [Unito](https://www.unito.io)
## Summary
Classify `.ply` as 3D media so PLY outputs are surfaced by queue/assets
preview flows.
## Changes
- **What**: adds `.ply` to shared 3D extension detection and falls back
to the asset `/view` URL when opening 3D assets without `preview_url`.
- **Breaking**: none.
- **Dependencies**: none.
## Review Focus
- This is the tactical FE fix for FE-129; it intentionally does not
solve the broader 3D media vs load3d-loadable split.
- Assets sidebar 3D viewer still prefers `preview_url`, but now has a
usable fallback for assets that only have the normal asset URL.
FE-129
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12319-fix-classify-PLY-assets-as-3D-media-3646d73d365081218f0bde401b1601bd)
by [Unito](https://www.unito.io)
## Summary
Stops the Shape submenu (and any other PrimeVue nested submenu) from
being clipped behind the node context menu when the menu fits in the
viewport.
## Changes
- **What**: `constrainMenuHeight` in `NodeContextMenu.vue` now applies
`max-height` + `overflow-y: auto` to the root `<ul>` only when
`scrollHeight > availableHeight`. The common case keeps `overflow:
visible`.
- Added `browser_tests/tests/nodeContextMenuShapeSubmenu.spec.ts`
regression spec.
## Review Focus
Root cause: setting only `overflow-y: auto` on a `<ul>` coerces
`overflow-x` to a non-visible value per CSS spec (`If one of
overflow-x/overflow-y is visible and the other isn't, the visible value
is computed as auto`). PrimeVue `ContextMenuSub` renders submenus
in-tree as a nested `<ul>` with `position: absolute; left: 100%`, so the
implicit horizontal clip hides them entirely.
The pre-existing overflow scenario (#10824 / #10854) is unchanged — when
the menu actually overflows, the clamp still applies and
`nodeContextMenuOverflow.spec.ts` continues to verify scroll. Submenu
clipping in that overflow case is a known limitation, not introduced by
this PR.
Fixes FE-570
## screenshot
### AS IS
<img width="788" height="505" alt="Screenshot 2026-05-07 at 12 43 26 PM"
src="https://github.com/user-attachments/assets/36d34070-0c57-4385-a130-0394f22f282e"
/>
### TO BE
<img width="779" height="627" alt="Screenshot 2026-05-07 at 12 42 44 PM"
src="https://github.com/user-attachments/assets/00956729-763b-4787-822f-209e8ea42331"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12035-fix-keep-node-context-menu-overflow-visible-when-content-fits-3586d73d365081ad9aaec82f220d401c)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
*PR Created by the Glary-Bot Agent*
---
## Summary
The comfy.org favicon was reported as illegible in Google search
results. The current `logomark.svg` is a transparent yellow "C" — when
Google (or any client) composites it onto a white surface (search
results, light-theme tab strips), the yellow disappears into the
background.
Fix: ship a dedicated `/favicon.svg` that wraps the existing yellow
logomark in a solid black square, and point `<link rel="icon">` at it.
The in-page nav logo, `Organization.logo` Schema.org URL, and any other
consumer of `logomark.svg` are left untouched, so transparent-composite
contexts (knowledge panels, dark nav) continue to render cleanly.
## Changes
- `apps/website/public/favicon.svg` *(new)* — 48×48 SVG: black square +
scaled-down original logomark path. Existing path geometry is reused
verbatim inside a `<g transform>` so the C glyph is byte-identical to
the source.
- `apps/website/src/layouts/BaseLayout.astro` — `<link rel="icon"
href="/icons/logomark.svg">` → `<link rel="icon" href="/favicon.svg">`.
One-line change.
## Why a new file (vs editing `logomark.svg` in place)
`logomark.svg` is also used by `SiteNav.vue` (in-page header on the dark
`--color-primary-comfy-ink` background) and by the JSON-LD
`Organization.logo` URL. Both consumers want the transparent version.
Editing it in place would draw an ugly black square in the site's own
header.
## User report
> "just google searched comfyui and logo isnt legible. We should
update.."
## Verification
**Built site**
- `pnpm typecheck` (astro check): 0 errors, 0 warnings
- `pnpm build` (astro build): 280 pages built, exit 0
- Built `dist/index.html` contains exactly one `<link rel="icon"
href="/favicon.svg">` and zero references to the old icon path in
`<head>`
- `oxlint` on changed `.astro` file: 0 warnings, 0 errors
**Visual (Playwright on local astro dev server)**
- New favicon renders correctly at 16/32/64 px — yellow C centered on
black square, no clipping.
- In-page nav logo unchanged (yellow C floats cleanly on the dark
`--color-primary-comfy-ink` nav background, no black wrapper visible).
- Mock of Google search-result row shows the new favicon is
high-contrast inside Google's white circular wrapper; the old one is
nearly invisible.
## Screenshots
### Google-style search result simulation (before / after)

### Favicon at native sizes + Google circular wrapper

### In-page nav header (unchanged after the fix)

## Notes for reviewers
- The change deliberately uses pure black `#000` (matching the user's
literal request "make the white background, black") rather than
`--color-primary-comfy-ink` (`#211927`). Either would work; happy to
switch if brand preference is the ink color.
- Search-engine cached favicons can take days/weeks to refresh on
Google's side after the new file is deployed.
## Screenshots



┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12285-fix-website-add-dark-background-favicon-for-legibility-in-search-results-3616d73d365081babbcbedf0b86d3d67)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
When the built-in logs terminal stayed open during a backend restart,
the buffer froze on pre-restart entries and live log streaming silently
stopped — only closing and reopening the panel resynced. Listen for the
api `reconnected` event and rebuild the terminal contents the same way a
fresh open would.
## Changes
- **What**:
- Extract `useLogsTerminal` composable. The SFC is now a thin shell
holding `terminal: shallowRef<Terminal>` and forwarding to the
composable, so `onMounted`/`onScopeDispose` no longer rely on the
child's emit callback timing.
- Subscribe to `api`'s `reconnected` event via `useEventListener`,
registered synchronously before any awaits. On reconnect:
`terminal.reset()` → refetch raw logs → `scrollToBottom()` →
`subscribeLogs(true)` (the backend loses the per-client subscription on
restart, so re-subscribe is required for live streaming to resume).
- Wrap in-flight resync/mount fetches in AbortControllers. Overlapping
reconnects abort the prior resync, and unmount mid-fetch suppresses
writes to the disposed xterm.
- Hide BaseTerminal whenever `errorMessage` is set so the error layout
doesn't expose an empty xterm container behind the message;
`loading=false` after both load failure and resync success so a later
successful reconnect can clear a stuck spinner.
- Migrate the load/resync error strings to vue-i18n
(`logsTerminal.loadError`, `logsTerminal.resyncError`).
## Review Focus
- **Re-subscribe is the non-obvious half of the fix** — without it, even
after the WebSocket reconnects the backend never resumes streaming logs
to this client because its subscription state was wiped on restart. The
visible "stale buffer" is only one symptom; the silent "no new logs"
symptom needed the explicit `subscribeLogs(true)` re-call in resync.
- `terminal.reset()` lives after a successful raw-logs fetch (not
before) so a failed resync leaves the prior buffer visible instead of
blanking it; resync errors surface via the same inline error message the
mount path uses.
- 8 unit tests around the composable: mount + subscribe, resync ordering
(reset → write → scroll → subscribe via `invocationCallOrder`),
in-flight resync abort on double reconnect, resync error surfacing,
mount-failure-then-recovery, unmount-mid-fetch terminal-write
suppression, listener cleanup on unmount.
- 2 E2E tests using `ws.close()` on the proxied WebSocket as the
reconnect trigger and `subscribeLogs` HTTP fetch count as the sync point
(same pattern as `wsReconnectStaleJob.spec.ts`). Red-checked: disabling
the `reconnected` listener fails exactly the two new tests, all 8
pre-existing tests stay green.
Fixes FE-712
## Screenshots
Before - (After rebooting, the console window does not update from its
state before the reboot must remount the console window for it to
resync.)
https://github.com/user-attachments/assets/b1e49c2c-89a4-4a4a-82b4-064412acee12
After - (The console window syncs automatically after a reboot.)
https://github.com/user-attachments/assets/54b582c5-ad42-41c0-9886-18f4495859da
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12270-fix-terminal-resync-logs-console-on-backend-reconnect-3606d73d3650812fb13fd1934c632344)
by [Unito](https://www.unito.io)
## Summary
Add the first product-area browser coverage on top of the merged typed
route mock foundation: the docked job history sidebar.
## Changes
- **What**: Adds `browser_tests/tests/sidebar/jobHistory.spec.ts` using
`jobsRouteFixture`.
- **What**: Covers direct sidebar entry, docked QPO history entry,
terminal history jobs, active queue jobs, tab filtering, search, clear
queue, and clear history.
- **What**: Adds typed `POST /api/queue` and `POST /api/history` route
helpers that validate request bodies with generated zod schemas.
- **What**: Adds stable test ids for the job history sidebar and queue
progress overlay so tests avoid structural CSS selectors.
- **Dependencies**: Builds on the typed route mock foundation merged in
#12267.
## Review Focus
Review the product assertions and whether this is the right first
coverage slice on top of the typed route mock foundation. This PR
intentionally avoids asset sidebar and floating QPO lifecycle coverage;
those should remain follow-up PRs.
## Screenshots (if applicable)
Not applicable.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12272-test-cover-job-history-sidebar-with-typed-route-mocks-3606d73d3650817481d5f9fac4bfc93c)
by [Unito](https://www.unito.io)
## Summary
https://linear.app/comfyorg/issue/FE-727/refactor-usemediaasset
`useMediaAssets` was a factory wrapper that branched on `isCloud` and
returned one of two near-identical implementations (`useAssetsApi` /
`useInternalFilesApi`). Both implementations delegated to the same
`assetsStore` actions (`updateInputs` / `updateHistory` /
`loadMoreHistory`) — the real cloud/local fork lives inside
`assetsStore.fetchInputFiles` (line 121), not at the composable layer.
Collapse the wrapper:
1. Delete `useInternalFilesApi` (identical to `useAssetsApi`).
2. Delete `useMediaAssets` (now a pass-through).
3. Switch the four callers to `useAssetsApi` directly.
## Changes
- **What**:
- Delete `useInternalFilesApi.ts` and `useMediaAssets.ts`.
- `AssetsSidebarTab.vue`, `WidgetSelectDropdown.vue`,
`useOutputHistory.ts`: call `useAssetsApi` directly.
- `useWidgetSelectItems.ts`: narrow `outputMediaAssets` prop type to
`IAssetsProvider` (the interface `useOutputHistory` already implements
directly).
- Test mocks repointed from `useMediaAssets` → `useAssetsApi`.
- Two unrelated tailwind class-order lint errors auto-fixed by `pnpm
lint:fix` to keep CI green.
- **Breaking**: None — no behavior change.
## Review Focus
The real cloud/local fork remains at `assetsStore.ts:121`
(`fetchInputFiles = isCloud ? fetchInputFilesFromCloud :
fetchInputFilesFromAPI`). That branch is M1-strict and clears once
BE-933 lands. This PR only collapses the dead wrapper layer above it.
`IAssetsProvider` is intentionally kept — `useOutputHistory.ts:83`
directly implements it for a different use case, so the interface still
has more than one consumer.
Surfaced while working on the FE-678 cloud/local asset branching survey.
## Screenshots (if applicable)
N/A — no UI change.
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
`getSurveyCompletedStatus` (auth.ts) now resolves ambiguous responses to
"completed" instead of "not completed", so transient backend errors no
longer bounce working users to `/cloud/survey`.
| Backend response | Old behavior | New behavior |
|---|---|---|
| 200 with non-empty `value` | `true` (completed) | `true` (completed) |
| 200 with empty `value` | `false` (not completed) | `false` (not
completed) |
| 404 | `false` → bounced | `true` (treat as completed) |
| 5xx | `false` → bounced | `true` (treat as completed) |
| 401 / 403 | `false` → bounced | `true` (treat as completed) |
| Network error | `false` → bounced | `true` (treat as completed) |
Only a definitive `200` with empty `value` is treated as "not
completed". Everything else fails open. The dedicated auth layer handles
re-authentication on the next API call, so 401/403 doesn't need a
separate branch here.
## Why
User reports from team-plan customers: _"I was working in a workflow,
hit run, and then got logged out and redirected to a survey screen."_
Datadog shows ~7,000 distinct users/day hitting the `setting key
onboarding_survey not found` path on prod ingest. With
`onboarding_survey_enabled: true` in prod dynamic config and the
catch-all `!response.ok` returning `false`, any mid-session reload
tripped a redirect to `/cloud/survey`.
User-validated requirement: rather miss showing the survey to a few
users than show it duplicately or interrupt working customers.
## Trade-off worth product review
A genuinely brand-new user whose `User.Settings` JSON is empty also
returns 404 from `/api/settings/onboarding_survey` — the backend doesn't
distinguish "key absent for existing user" from "user has no settings
yet". With this change, that 404 is treated as "completed", so the
survey gate does not fire on the strict 404 path. New users will still
see the survey if signup pre-populates the `onboarding_survey` key with
an empty object (`200` with empty `value`); if not, the survey is missed
on initial signup.
We picked this trade-off per the product call that false positives
(bouncing paying customers) are strictly worse than false negatives
(occasionally missing a new user).
The clean fix to recover the new-user signal is a backend change: return
`200` with `value: null` when the `User` row exists but the key is
absent — distinguishing "no survey saved" from "user not found". Out of
scope for this PR; filing as follow-up if accepted.
## Test plan
- [ ] Logged-in user with completed survey navigates around — no
redirect
- [ ] Logged-in user with no survey, fresh tab — redirected to
`/cloud/survey` (gate still works for new sessions)
- [ ] Logged-in user with no survey, after submitting — no redirect on
next nav
- [ ] Simulate transient 5xx on `/api/settings/onboarding_survey`
(DevTools blocking) — user stays on current page, no redirect
Unit coverage in `auth.test.ts` locks the resolution table above against
drift (one test per branch, 8 total).
## Companion PRs
None — frontend only.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-12301-fix-cloud-stop-bouncing-working-users-to-cloud-survey-mid-session-3616d73d365081128ba7e266ad7ccff9)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
*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)
## 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>
Replace the pre-rendered `.webm` video in the homepage hero section with
an interactive Three.js 3D logo.
## Changes
- Add `three` dependency to the website package and pnpm catalog
- Add `useHeroLogo` composable that sets up the Three.js scene:
- Extruded Comfy "C" logo with stencil-masked image sequence slideshow
- Auto-rotation with drag interaction and cursor tilt
- Graceful degradation if some textures fail to load
- Update `HeroSection.vue` to use the composable instead of a `<video>`
element
- Upload 16 image sequence frames to
`gs://comfy-org-videos/website/homepage/hero-logo-seq/`
<img width="1000" height="648" alt="Kapture 2026-05-05 at 20 54 05"
src="https://github.com/user-attachments/assets/7a7b1634-2da3-4aa2-871a-f64d4d337b39"
/>
@coderabbitai approve
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11964-feat-replace-spin-logo-video-with-Three-js-instance-in-homepage-HeroSection-3576d73d365081bbab0ed19dd121830c)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
`assetsStore.updateAssetTags` issues `removeAssetTags` and
`addAssetTags` as two separate network calls. When the remove succeeds
server-side but the subsequent add rejects, the cache rolls back to the
original tags while the server has already dropped the removed tags —
cache and backend diverge until the next refetch.
This adds a compensating action: if remove succeeded and add then fails,
attempt to re-add the just-removed tags so the server returns to its
prior state. If the compensating add also fails, invalidate the category
cache so the next access refetches fresh state.
- Fixes#11694
- Fixes
[FE-473](https://linear.app/comfyorg/issue/FE-473/fix-converge-asset-tag-cache-and-server-on-partial-failure)
## Changes
- `src/stores/assetsStore.ts`: track which tags were already removed
server-side; on add-failure, re-add them; if compensation fails,
invalidate the resolved category cache via `resolveCategory` +
`invalidateCategory`.
- `src/stores/assetsStore.test.ts`: extend the cloud asset-service mock
with `addAssetTags` / `removeAssetTags` and add a `updateAssetTags
partial-failure compensation` describe block:
- re-adds removed tags when add fails so cache and server converge
- invalidates the category cache when compensation also fails
- does not attempt compensation when only the add was attempted (no
remove ran)
## Test plan
- [x] `pnpm exec vitest run src/stores/assetsStore.test.ts` — 42 passed
(39 prior + 3 new)
- [x] `pnpm typecheck`
- [x] `pnpm lint`
- [x] `pnpm knip`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11695-fix-converge-asset-tag-cache-and-server-on-partial-failure-34f6d73d365081149900f95b6ee4bfa9)
by [Unito](https://www.unito.io)
Primitive nodes do not create their widgets until their
`onAfterGraphConfigured` method is called. Previously, when a
subgraphNode contains a linked widget, any proxyWidgets that do not
resolve to a real widget are pruned at time of configure. Since this
occurs prior to initialization of the primitive, the primitive value
would be de-promoted before the widget could initialize
This is resolved by the minimally disruptive change of allowing proxied
promotions to primitive nodes to be kept so long as the node itself can
be found.
*PR Created by the Glary-Bot Agent*
---
## Summary
The left side of `CloudBannerSection` on `/download` showed an
unintended fade-out: the bottom-left of the banner appeared darker than
the rest of the bar.
## Root cause
`product/local/HeroSection.vue` renders an SVG illustration whose
container has `lg:z-1` and whose SVG element has `overflow-visible`. The
SVG contains a left-edge fade `<rect x="300" y="150" width="250"
height="900" fill="url(#localHeroFadeLeft)" />` that paints outside the
SVG's `viewBox` (`400 200 550 800`) — including upward into the area
occupied by the preceding `CloudBannerSection`.
Because `CloudBannerSection` had `position: static` and `z-auto`, the
positively-stacked illustration painted over the banner's bottom-left,
producing the visible darkening.
## Fix
Establish a stacking context on `CloudBannerSection` (`relative z-20`)
so it always renders above the hero illustration's overflow on every
page that includes the banner (download, api, cloud/enterprise, and
zh-CN equivalents).
This is a minimal, isolated change to the shared component — no logic or
markup structure changes.
## Verification
- Reproduced visually at `lg` breakpoint and confirmed the fade is gone
after the fix.
- Verified `/download`, `/api`, `/cloud/enterprise`, and
`/zh-CN/download` render correctly.
- `pnpm typecheck` and `pnpm typecheck:website` pass (run automatically
by pre-commit hook).
- `oxfmt`, `oxlint`, `eslint`, `stylelint` all pass.
### Before

### After

## Follow-up
Consider adding a Playwright visual regression test for the banner/hero
seam on `/download` to catch future stacking regressions (called out by
review).
## Screenshots


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11974-fix-website-prevent-HeroSection-fade-from-bleeding-into-CloudBannerSection-on-downloa-3576d73d3650813d8924fb54d5f78cee)
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
Aligns the `OpenAIGPTImage1` node display name in all 11 non-English
`nodeDefs.json` locale files with the English source-of-truth, which was
already updated to `OpenAI GPT Image 2`.
## Changes
- **What**: Updates `display_name`, the description string, and the
prompt tooltip in `ja`, `ru`, `zh`, `zh-TW`, `ar`, `pt-BR`, `ko`, `fr`,
`es`, `fa`, and `tr` locales from `GPT Image 1` to `GPT Image 2` (and
`GPT Görüntü 1` → `GPT Görüntü 2` in Turkish, `GPT صورة 1` → `GPT صورة
2` in Arabic). Other tooltips already referenced `GPT Image 2` and are
unchanged.
- **Breaking**: None — the registry node id `OpenAIGPTImage1` is
preserved (it is an internal identifier, not user-facing).
## Review Focus
- Translations were updated mechanically — please confirm the
version-number change is acceptable as-is for non-Latin scripts (Arabic,
Persian, Korean, Japanese, Chinese) where the version number was kept as
`2` per the existing pattern.
- The English locale already used `OpenAI GPT Image 2`, so this PR
brings the other locales into sync; no English copy was changed.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11968-fix-i18n-rename-OpenAI-GPT-Image-1-to-GPT-Image-2-across-locales-3576d73d365081bfa204cbf528d84bf3)
by [Unito](https://www.unito.io)
Co-authored-by: Marwan Ahmed <marwan@Marwans-MacBook-Pro.local>
## Summary
Restores required-input validation highlighting on Vue node input slots.
## Changes
- **What**: Passes validation error state from `NodeSlots` to
`InputSlot` using node locator IDs, including subgraph and nested
subgraph execution IDs.
- **What**: Adds unit coverage for root, one-level subgraph, and nested
subgraph slot error mapping.
- **What**: Adds a Vue Nodes screenshot regression test that asserts the
missing required input slot itself receives the error highlight.
- **Dependencies**: None.
## Review Focus
- Required input errors on Vue-rendered node's slots.
- The new Playwright screenshot expectation will need the `New Browser
Test Expectation` label for Linux baseline generation.
## Screenshots (if applicable)
Before
<img width="499" height="324" alt="스크린샷 2026-05-05 오후 3 00 44"
src="https://github.com/user-attachments/assets/285fdf91-6d7e-480b-99b9-715705f78914"
/>
After
<img width="482" height="356" alt="스크린샷 2026-05-05 오후 3 01 11"
src="https://github.com/user-attachments/assets/51b8db49-eb9c-4155-8aa5-109c0bd7699b"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11950-fix-highlight-missing-input-slots-on-Vue-nodes-3576d73d365081bd85bfd1ea149d45c5)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Fixes the Cloud-only nested subgraph missing-model false positive
covered by the stacked regression test in #11907.
When returning from an outer subgraph to the root graph, the Vue graph
node manager replays `onNodeAdded` for existing graph nodes. The
realtime error-clearing hook handled a subgraph container by recursively
scanning all interior nodes. For nested subgraphs, that also scanned the
nested subgraph container itself.
Nested subgraph container widgets are promoted synthetic views of
interior widgets. Scanning them as real model-loader nodes is wrong: the
container node type is the subgraph UUID, not `UNETLoader`, so Cloud
asset resolution can classify an installed promoted model as missing.
## Changes
- Skip nested subgraph container nodes during parent subgraph replay
scans.
- Keep scanning real active interior leaf nodes.
- Add unit coverage proving the replay scan visits the `UNETLoader` leaf
but not the nested subgraph container.
- Remove the `test.fail()` annotation from the Cloud E2E regression test
added in #11907.
## Stacked PR
This PR is stacked on #11907. After #11907 lands, this branch should be
rebased or retargeted onto `main`.
## Verification
- `pnpm exec vitest run
src/composables/graph/useErrorClearingHooks.test.ts -t "skips nested
subgraph containers during parent subgraph replay scan"`
- `pnpm exec oxfmt --check
src/composables/graph/useErrorClearingHooks.ts
src/composables/graph/useErrorClearingHooks.test.ts
browser_tests/tests/propertiesPanel/errorsTabCloudMissingModels.spec.ts`
- `pnpm exec eslint src/composables/graph/useErrorClearingHooks.ts
src/composables/graph/useErrorClearingHooks.test.ts
browser_tests/tests/propertiesPanel/errorsTabCloudMissingModels.spec.ts`
- `pnpm exec oxlint src/composables/graph/useErrorClearingHooks.ts
src/composables/graph/useErrorClearingHooks.test.ts
browser_tests/tests/propertiesPanel/errorsTabCloudMissingModels.spec.ts
--type-aware`
- `pnpm typecheck`
- `pnpm typecheck:browser`
- `pnpm build:cloud`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188 pnpm
exec playwright test
browser_tests/tests/propertiesPanel/errorsTabCloudMissingModels.spec.ts
--project=cloud`
- commit hook: `pnpm typecheck`, `pnpm typecheck:browser`
- push hook: `pnpm knip`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11908-fix-skip-nested-subgraph-containers-in-replay-scan-3566d73d3650819c8687d6ab74add1b9)
by [Unito](https://www.unito.io)
## Summary
Adds a Cloud Playwright regression test for the nested subgraph case
where an installed Lotus diffusion model is incorrectly surfaced as
missing after returning to the root graph.
The fixture keeps the reproduction small: root graph -> subgraph node ->
nested subgraph node -> `UNETLoader` using
`lotus-depth-d-v1-1.safetensors`. The test stubs `/api/assets` through
the shared asset API fixture so that model is explicitly present as a
`diffusion_models` asset.
This test is intentionally written as an XFAIL regression guard. Its
setup and precondition checks are outside the XFAIL section: initial
workflow load must not show the error overlay, the Errors tab must
initially stay hidden, subgraph entry must succeed, root return must
succeed, and the replay scan must run. Only the final `Errors` tab
visibility assertion is expected to fail on current Cloud behavior.
## What a green run means
A green CI run for this PR means the Cloud-only bug was reproduced at
the intended point. The test reaches the root-return replay scan,
verifies that the replay scan ran, and then current Cloud behavior makes
the Errors tab visible even though the Lotus model exists in
`/api/assets`.
If any earlier setup or navigation step fails, or if the root-return
replay scan does not run, the test fails normally because those checks
happen before `test.fail()` is applied.
Locally, removing `test.fail()` produces the expected red result after
the replay-scan precondition passes, with `panel-tab-errors` visible.
The intended post-fix contract is that the replay scan still runs, but
the Errors tab remains hidden.
## Why this is XFAIL
This PR intentionally ships only the regression test, not the production
fix. The final behavioral assertion is annotated with `test.fail()`
because the current Cloud replay path still treats the nested subgraph
promoted model widget as missing.
When the follow-up fix lands, Playwright will report this test as an
unexpected pass until the `test.fail()` annotation is removed. That is
the handoff point for converting this regression guard into a normal
passing E2E test.
## Follow-up
The stacked fix PR is #11908. It updates the replay scan so nested
subgraph container nodes are skipped, then removes the `test.fail()`
annotation from this test.
## Verification
- `pnpm exec oxfmt --check browser_tests/fixtures/assetApiFixture.ts
browser_tests/tests/cloud-asset-default.spec.ts
browser_tests/tests/propertiesPanel/errorsTabCloudMissingModels.spec.ts`
- `pnpm exec oxlint browser_tests/fixtures/assetApiFixture.ts
browser_tests/tests/cloud-asset-default.spec.ts
browser_tests/tests/propertiesPanel/errorsTabCloudMissingModels.spec.ts
--type-aware`
- `pnpm exec eslint browser_tests/fixtures/assetApiFixture.ts
browser_tests/tests/cloud-asset-default.spec.ts
browser_tests/tests/propertiesPanel/errorsTabCloudMissingModels.spec.ts`
- `pnpm typecheck:browser`
- `pnpm typecheck`
- `pnpm lint`
- `PLAYWRIGHT_LOCAL=1 PLAYWRIGHT_TEST_URL=http://localhost:8188 pnpm
exec playwright test
browser_tests/tests/propertiesPanel/errorsTabCloudMissingModels.spec.ts
browser_tests/tests/cloud-asset-default.spec.ts --project=cloud`
- Temporarily removed `test.fail()` locally and verified the test fails
only after the replay-scan precondition passes, with `panel-tab-errors`
visible
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11907-test-add-Playwright-regression-test-for-nested-subgraph-Cloud-missing-model-3566d73d3650810b86d4de916c2852f9)
by [Unito](https://www.unito.io)
*PR Created by the Glary-Bot Agent*
---
## Summary
Adds "comfyui app" / "comfyui web app" / "comfy ui application" keywords
to the titles and meta descriptions of the home, download, and Comfy
Cloud pages (and zh-CN equivalents) to recover organic traffic for those
queries.
## Context
Organic traffic for the query **"comfyui app"** dropped after
`https://docs.comfy.org/interface/app-mode` started outranking the
product/landing pages. The docs page about app-mode converts worse than
the product pages, so we want Google to prefer comfy.org product pages
for that query. The cleanest, lowest-risk lever is on-page SEO metadata.
## Changes
- **What**:
- `apps/website/src/pages/index.astro` → title `ComfyUI App —
Professional Control of Visual AI` + product-focused description.
- `apps/website/src/pages/download.astro` → title `Download the ComfyUI
App — Run Visual AI Locally` + desktop-app description.
- `apps/website/src/pages/cloud/index.astro` → title `Comfy Cloud — The
ComfyUI Web App` + web-app description.
- `apps/website/src/pages/zh-CN/{index,download,cloud/index}.astro` →
localised Chinese titles and descriptions so the zh-CN product pages no
longer fall back to the English `BaseLayout` default.
- `apps/website/src/layouts/BaseLayout.astro` → unchanged net-net
(touched then reverted to neutral copy after review feedback so
non-product / non-localised pages keep their existing, generic
fallback).
- **Breaking**: none. Visual content, routing, and components are
untouched — only `<title>` and `<meta>` tags change.
## Review Focus
- The keyword copy reads naturally (no stuffing) and stays under typical
SERP truncation limits (≤ ~165 chars).
- zh-CN pages get Chinese descriptions — they intentionally don't repeat
the English keywords, since "comfyui app" is an English-language query.
- Pre-existing behaviour preserved: zh-CN pages **without** an explicit
description still inherit the English `BaseLayout` default. Fixing that
fallback for the whole zh-CN tree is out of scope for this PR — happy to
follow up if desired.
## Verification
- `pnpm typecheck` — 0 errors
- `pnpm build` — 39 pages built clean
- `pnpm test:unit` — 23/23 pass
- `pnpm format:check apps/website/src` — clean
- Manually verified rendered `<title>` and `<meta name="description">`
via Playwright on `/`, `/download`, `/cloud`, and the zh-CN equivalents.
## Screenshots
Home page rendered with the new title (visible in browser tab / SERP
preview); visual content unchanged.
## Screenshots

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11834-feat-website-add-comfyui-app-SEO-keywords-to-product-pages-3546d73d3650819da11bd665c2fcfb88)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
Add a typed Playwright jobs API mock and migrate the floating queue
overlay browser spec onto it.
## Changes
- replace the backend/seed terminology with `JobsApiMock`,
`jobsApiMockFixture`, `mockJobs()`, and `MockJobRecord`
- keep the mock at the network boundary with `page.route()` for
`/api/jobs`, `/api/jobs/{id}`, and `/api/history`
- remove backend-like query behavior that these browser tests do not
use, including sort handling, workflow filtering, and strict limit
validation
## Why
This keeps jobs coverage fast and profile-independent while avoiding
backend architecture changes for test setup. The fixture now serves only
the response shapes the UI consumes instead of pretending to be a
general in-memory backend.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11280-test-add-in-memory-jobs-backend-fixture-3436d73d365081bb87e8c9771654496c)
by [Unito](https://www.unito.io)
*PR Created by the Glary-Bot Agent*
---
Removes the placeholder "GROOVE JONES CONTRIBUTORS" author card from the
Groove Jones customer story (`/customers/groove-jones`). The card was
rendering with `TBD` / `待补充` values for the contributor name and role.
## Change
Deletes 3 i18n keys from `apps/website/src/i18n/translations.ts`:
- `customers.detail.groove-jones.topic-10.block.2.label`
- `customers.detail.groove-jones.topic-10.block.2.name`
- `customers.detail.groove-jones.topic-10.block.2.role`
Block types in `ContentSection.vue` are inferred from the presence of
suffix keys (`.role` → `author` block) via `deriveSections` in
`apps/website/src/config/contentSections.ts`. Removing the keys causes
the author card to drop out of the rendered output entirely. The other
two blocks in topic-10 (intro paragraph + Dale Carman blockquote) remain
unchanged.
## Verification
- `pnpm typecheck` — passes
- `pnpm lint` — 0 errors (1 pre-existing warning, unrelated)
- `pnpm format` — applied
- `pnpm knip` — clean (1 pre-existing warning, unrelated)
- Manual: ran `pnpm dev` for the website app, navigated to
`/customers/groove-jones`, confirmed the conclusion section ends
naturally — no `TBD` text, no orphan `CONTRIBUTORS` label, no broken
card.
Code review (Oracle): 0 critical / 0 warning / 0 suggestion.
## Screenshots

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11937-fix-website-remove-placeholder-author-info-for-groove-jones-customer-story-3576d73d36508193b1a0c0c3cd887686)
by [Unito](https://www.unito.io)
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
Use resized preview URLs for floating QPO row thumbnails so the expanded
overlay does not load full-size image assets while the canvas is being
navigated.
Linear: FE-538
## Changes
- **What**: Pass `ResultItemImpl.previewUrl` into `AssetsListItem` for
completed image/video job rows.
- **Dependencies**: None.
## Review Focus
Confirm this only changes the row thumbnail source; full asset viewing
still flows through the existing job/task preview output.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11946-fix-use-resized-QPO-thumbnails-3576d73d365081b68682d1b7b109af30)
by [Unito](https://www.unito.io)
## Summary
Use ShadCN-style Reka popover primitives for the live queue job list
after the unused legacy queue row implementation is removed in #11621.
This is the first step in migrating popovers toward the ShadCN UI
pattern: local design-system wrappers over Reka UI, rather than ad hoc
direct Reka or PrimeVue popovers at each call site.
## Changes
- **What**: Added the minimal ShadCN-style popover primitives needed by
this fix: `Popover`, `PopoverAnchor`, and `PopoverContent`.
- **What**: Migrated `JobAssetsList` job details from manual fixed
positioning to these popover primitives with viewport collision
handling.
- **What**: Removed the obsolete manual hover-position helper after
`JobAssetsList` stopped using it.
- **Dependencies**: No new dependencies; the primitives wrap the
existing `reka-ui` package.
- Added browser coverage for bottom-row job details clipping in the
queue overlay.
## Review Focus
- This PR is stacked on #11621.
- The live queue surfaces are `JobAssetsList` consumers: expanded queue
progress overlay and job history sidebar.
- The new `src/components/ui/popover` files intentionally seed the
ShadCN-style migration path, but only include the pieces used here to
keep the first PR small.
- Follow-up PRs can add `PopoverTrigger` and migrate existing
PrimeVue/direct-Reka popovers once there is an actual caller.
## Summary
A follow-up PR of
https://github.com/Comfy-Org/ComfyUI_frontend/issues/11107.
## Changes
Add unit test to `editAttention.ts`
- [x] `Extract pure functions to module level`: **Moved**
`incrementWeight`, `findNearestEnclosure`, and `addWeightToParentheses`
out of the `init()` closure and **promoted** them to module-level
functions with `export` to allow for independent testing.
- [x] `Add unit tests for incrementWeight`: **Added** 6 tests covering
edge cases such as normal increment/decrement, NaN input, negative
weights, and floating-point precision.
- [x] `Add unit tests for findNearestEnclosure`: **Added** 7 tests
covering edge cases including simple brackets, no brackets, cursor
outside, nested brackets (inner/outer), empty strings, and missing
closing brackets.
- [x] `Add unit tests for addWeightToParentheses`: **Added** 6 tests
covering scenarios like adding a default 1.0 weight, retaining existing
weights, no changes when brackets are absent, scientific notation
weights, negative weights, and multi-word tokens.
- [x] `Mock app module`: **Used** `vi.mock('@/scripts/app')` to
intercept side effects from `app.registerExtension`, **preventing** the
triggering of ComfyUI extension registration logic during module import.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Adjusts token selection/weight-detection logic used during
Ctrl/Cmd+Arrow editing, which could subtly change how prompts are
rewritten in edge cases (nested parens, scientific notation, time-like
text). Adds tests that should reduce regression risk but behavior
changes still warrant verification in the UI.
>
> **Overview**
> Adds a new `vitest` unit test suite for `editAttention` by mocking
`app.registerExtension` side effects and validating `incrementWeight`,
`findNearestEnclosure`, and `addWeightToParentheses` across common and
edge cases.
>
> Refactors those helpers to exported module-level functions and
tightens parsing/selection behavior: `findNearestEnclosure` now handles
the cursor being on an opening `(`, `addWeightToParentheses` improves
trailing weight detection (supports scientific notation/negatives and
avoids misclassifying time-like `12:30`), and the weight-rewrite regex
now matches exponent forms.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
df20340b49. 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-11301-Test-edit-attention-unit-tests-3446d73d365081f29e8dfceefc06f5d3)
by [Unito](https://www.unito.io)
## Summary
Adds 11 tests for \`src/renderer/core/layout/store/layoutStore.ts\`
covering paths previously uncovered by the existing 17-test suite.
Targets the customRef setter machinery, reactive queries, and
link-layout updates that are reachable through the public API.
## Test Coverage
\`getNodeLayoutRef\` setter:
- Setter on a fresh ref triggers \`createNode\`.
- Position-only change triggers \`moveNode\`.
- Size-only change triggers \`resizeNode\`.
- zIndex-only change triggers \`setNodeZIndex\`.
- Setting to \`null\` triggers \`deleteNode\`.
Queries:
- \`getNodesInBounds\` returns reactive node IDs intersecting the
bounds.
- \`queryNodeAtPoint\` returns the top-zIndex node containing the point.
- \`queryNodeAtPoint\` returns \`null\` when no node contains the point.
Link layouts:
- \`updateLinkLayout\` short-circuits when bounds and centerPos
unchanged but still updates the path.
- \`updateLinkLayout\` replaces stored layout when bounds change.
- \`deleteLinkLayout\` removes the link and its segment layouts.
## Testing
\`\`\`bash
pnpm vitest run src/renderer/core/layout/store/layoutStore.test.ts
\`\`\`
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11747-test-add-unit-tests-for-layoutStore-setter-and-query-paths-3516d73d365081d9bc1de336ff7258ea)
by [Unito](https://www.unito.io)
… (issue #11092 phase 4b)
## Summary
Part of #11092 — Phase 4b: remove 2 `@ts-expect-error` suppressions from
`CustomizationDialog.vue`.
## Changes
`selectedIcon` ref initialisation and `resetCustomization` assignment
both suppressed a type error on `Array.find()` returning `T |
undefined`.
**Why**
`Array.find()` has no way to statically guarantee a match, so its return
type is always `T | undefined`. Both usages were searching `iconOptions`
— a literal array of 8 entries declared in the same scope — and
TypeScript could not prove that the searched value would always be
found, even though at runtime it always is (the default icon value is
defined from `iconOptions[0]`).
**How**
Added `iconOptions[0]` as a final fallback via `??` in both places.
Because `iconOptions` is a non-empty literal array, `iconOptions[0]` is
provably non-null to TypeScript, which makes the overall expression type
`T` and satisfies the assignment. The explicit generic on `ref<{ name:
string; value: string }>` was also dropped — TypeScript infers the type
correctly from the non-nullable initialiser. In `resetCustomization`,
`||` was replaced with `??` since the values being null-coalesced are
objects (never falsy), making `??` the semantically precise operator for
this case.
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Low risk: UI-only refactor that adds explicit fallbacks for
`Array.find()` results and introduces a small unit test suite; behavior
should remain the same except for safer handling of unexpected icon
values.
>
> **Overview**
> Removes two `@ts-expect-error` suppressions in
`CustomizationDialog.vue` by making `selectedIcon` initialization and
`resetCustomization` use a guaranteed fallback (`iconOptions[0]`) via
`??`, ensuring the selected icon is never `undefined`.
>
> Adds `CustomizationDialog.test.ts` to verify `confirm` emits the
expected icon/color for default, provided initial values, and an invalid
`initialIcon` fallback.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
f77addf713. 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-11339-refactor-remove-ts-expect-error-suppressions-in-CustomizationDialog-3456d73d36508165865ac569e047db2e)
by [Unito](https://www.unito.io)
## Summary
Phase D of the **useBrushDrawing-refactor plan.md**. Extract `WebGPU`
state management from `useBrushDrawing` into a dedicated
`useGPUResources` composable, reducing `useBrushDrawing` from ~1,160
lines to ~230. This is Phase D of the ongoing `useBrushDrawing`
decomposition (Phases A–C landed in previous PRs).
## Changes
- **What**: Split `useBrushDrawing` along a clean boundary — GPU
device/texture lifecycle moves to `useGPUResources`, stroke
orchestration stays in `useBrushDrawing`. Shared reactive state
(`dirtyRect`, `isSavingHistory`, `previewCanvas`) is now owned by
`useGPUResources` and exposed as refs. A pure
`clampDirtyRect` helper is extracted to `gpuUtils.ts`.
- **Dependencies**: No new dependencies
## Tests
Local test - pass
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Refactors WebGPU initialization, texture management, and readback
paths used during drawing; regressions could affect stroke rendering,
canvas visibility, and undo/redo GPU sync.
>
> **Overview**
> Extracts WebGPU device/texture/renderer lifecycle, watchers (clear,
undo/redo sync, texture recreation), and readback logic out of
`useBrushDrawing` into a new `useGPUResources` composable, with shared
refs (`dirtyRect`, `isSavingHistory`, `previewCanvas`, `hasRenderer`)
now owned by that module.
>
> Updates `useBrushDrawing` to delegate GPU-specific operations
(prepare/render/draw point/composite/readback/cleanup) to
`useGPUResources` while keeping CPU drawing + stroke orchestration, and
adds new pure helpers in `gpuUtils` (`clampDirtyRect`,
`buildStrokePoints`) to centralize dirty-rect clamping and stroke point
resampling.
>
> Adds Vitest coverage for the new helpers, `useGPUResources`
no-op/error behavior when GPU isn’t available, and `useBrushDrawing`
interactions with the extracted GPU API (composition mode selection,
shift-line, history save, and canvas/preview opacity restoration).
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
a9fcd80ab5. 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://app.notion.com/p/PR-11784-refactor-extract-GPU-lifecycle-into-useGPUResources-phase-D-3526d73d365081108a97c164a0bfa13e)
by [Unito](https://www.unito.io)
## Summary
Render keybinding badges in sentence case (`Ctrl + Shift + A`) instead
of UPPERCASE (`CTRL + SHIFT + A`) by swapping the `uppercase` Tailwind
class for `capitalize` in `KeyComboDisplay.vue`.
`KeyComboImpl.getKeySequences()` already returns labels in their
canonical form (`Ctrl`, `Alt`, `Shift`, plus the raw key). The badge
styling was forcing them all to UPPER, which is what FE-524 calls out.
`text-transform: capitalize` cleanly handles every case: lower modifier,
upper modifier, and single character keys.
- Fixes FE-524
## Before / After
| Before (`uppercase`) | After (`capitalize`) |
| --- | --- |
| <img
src="https://raw.githubusercontent.com/Comfy-Org/ComfyUI_frontend/c6bb96fce/docs/screenshots/fe-524/before.png"
width="480"> | <img
src="https://raw.githubusercontent.com/Comfy-Org/ComfyUI_frontend/c6bb96fce/docs/screenshots/fe-524/after.png"
width="480"> |
## Test plan
- [ ] Open Settings → Keybinding panel and confirm modifier badges
render as `Ctrl`, `Alt`, `Shift` instead of `CTRL`, `ALT`, `SHIFT`
- [ ] Confirm single-letter keys (e.g. `A`, `S`) still render uppercase
- [ ] Edit a keybinding and verify the live preview badges in the dialog
also render in sentence case
## Summary
Use spread pattern when writing `nodeValue.properties['Model Config']`
so future ModelConfig fields are preserved across viewer dialog
cancel/apply.
## Changes
- **What**: Spread existing `Model Config` before applying known keys in
both `restoreInitialState()` and `applyChanges()` in
[useLoad3dViewer.ts](src/composables/useLoad3dViewer.ts). Removes the
hard-coded `showSkeleton: false` override from `applyChanges()` so it
falls through from the existing config.
## Review Focus
The change is intentionally minimal and matches the suggestion in the
upstream issue. Two regression tests added (one each for restore/apply)
verify that an unknown future field on Model Config survives both code
paths.
Fixes#11346
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11838-fix-load3d-preserve-unknown-Model-Config-fields-with-spread-3546d73d3650819686efc4e1a9799ad9)
by [Unito](https://www.unito.io)
## Summary
`fitToViewer()` in `SceneModelManager` resets the model rotation to
`(0,0,0)` as part of its normalize-and-scale flow, but does not reapply
`currentUpDirection` afterward. This causes a state/view mismatch when
the user has previously selected a non-default up-axis (e.g. `+x`,
`-z`): the model snaps back to its raw orientation while the Up
Direction control still shows the previously selected value.
## Changes
- In `fitToViewer()`, clear `originalRotation` (to avoid compounding
with the stale pre-fit base) then reapply `currentUpDirection` if it is
not `'original'`
- Add 5 unit tests covering: no-op when no model, reapplication of
direction, no rotation compounding on repeated calls, zero rotation for
`'original'` direction, and stale `originalRotation` guard
## Testing
### Automated
- `src/extensions/core/load3d/SceneModelManager.test.ts` — 5 new tests
in `describe('fitToViewer')`
- All 48 unit tests pass
### E2E Verification Steps
1. Open the Load3D viewer with any 3D model
2. Change **Up Direction** to any non-default value (e.g. `+x` or `-z`)
— observe model rotates correctly
3. Click **Fit to Viewer** — model should remain in the chosen
up-direction orientation, not snap back to raw orientation
4. Click **Fit to Viewer** again — rotation should remain stable (no
compounding)
5. Change Up Direction back to `original` then click **Fit to Viewer** —
model should return to neutral orientation `(0,0,0)`
## Review Focus
The key invariant: `fitToViewer()` resets `rotation.set(0,0,0)`
explicitly, so we must clear `originalRotation = null` before calling
`setUpDirection`. Otherwise `setUpDirection` restores the stale pre-fit
rotation as a base and then adds the direction offset on top,
compounding incorrectly.
Fixes#11347
<!-- Pipeline-Ticket: pick-issue-3414 -->
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11826-fix-load3d-reapply-up-direction-after-fitToViewer-transform-reset-3546d73d36508166b9bcecc9949c4952)
by [Unito](https://www.unito.io)
## Summary
Fixes a bug where models reloaded in wireframe/normal/depth modes would
not restore to their original materials correctly, because the material
snapshot was being taken *after* the mode was applied.
## Changes
- Move `setupModelMaterials(model)` to immediately after
`scene.add(model)` and before `setMaterialMode()` / `setUpDirection()`
in `SceneModelManager.setupModel()`
- Save `materialMode` into `pendingMaterialMode` before calling
`setupModelMaterials()`, since the latter internally calls
`setMaterialMode('original')` which resets `this.materialMode` —
preserving the value ensures the subsequent reapplication guard works
correctly
- Update stale test assertion that reflected the old (incorrect) call
order
- Add regression test: verifies that `originalMaterials` captures the
pre-mutation material and that restoring to `'original'` after a
non-original load gives back the true original mesh material
## Testing
### Automated
- `src/extensions/core/load3d/SceneModelManager.test.ts` — 44 tests, all
pass
- Full load3d test suite — 401 tests, all pass
### E2E Verification Steps
1. Open ComfyUI with a Load3D node
2. Load any GLB/OBJ model
3. Switch Material Mode to **Wireframe** (or Normal/Depth)
4. Load a different model (or reload the same one)
5. Switch Material Mode back to **Original**
6. Verify the model renders with its original diffuse/PBR materials (not
wireframe)
## Review Focus
The key invariant: `setupModelMaterials()` must snapshot mesh materials
in their *unmodified* state. It must run before any `setMaterialMode()`
call that mutates them. The `pendingMaterialMode` variable is needed
because `setupModelMaterials()` internally calls
`setMaterialMode('original')`, which updates `this.materialMode`, making
the subsequent guard `if (this.materialMode !== 'original')` silently
skip reapplication without it.
Fixes#11344
<!-- Pipeline-Ticket: pick-issue-3417 -->
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11825-fix-load3d-snapshot-original-materials-before-reapplying-materialMode-3546d73d3650818b9c35fa60c15f17a3)
by [Unito](https://www.unito.io)
## Summary
Preview 3D and Animation nodes were stuck at the LOD from initial page
load because CSS `scale3d` transforms don't affect
`clientWidth`/`clientHeight` — `handleResize()` always received
layout-space dimensions regardless of zoom level. This fix passes
`ds.scale` as the renderer pixel ratio so the 3D scene renders at the
correct visual resolution when the graph is zoomed in or out.
## Changes
- **What**: In `Load3d.handleResize()`, call
`renderer.setPixelRatio(ds.scale)` before `setSize` so pixel density
scales with canvas zoom. A `getZoomScale` callback is threaded through
`Load3DOptions` → `Load3d` constructor → `handleResize`. In `useLoad3d`,
a watcher on `canvasStore.appScalePercentage` triggers `handleResize`
whenever the zoom level changes.
- **What**: Fix `SceneManager.captureScene()` to save and restore the
renderer's logical size and pixel ratio around capture, so exact-pixel
output is unaffected by the current zoom state.
## Review Focus
- `handleResize` now calls `setPixelRatio` before `setSize`. Three.js
renders at `logicalWidth × pixelRatio` physical pixels while CSS
displays it at `logicalWidth` CSS pixels — this is the standard pattern
for HiDPI but here used to match the visual zoom level.
- `captureScene` must reset `pixelRatio` to 1 so `setSize(w, h)`
produces exactly `w×h` pixel output. It saves and restores both logical
size and pixel ratio via `renderer.getSize()` /
`renderer.getPixelRatio()`.
- The zoom watcher is guarded with `getActivePinia()` to avoid errors in
unit tests and non-Pinia contexts.
## Test
before
https://github.com/user-attachments/assets/9778ad54-7cb2-4fdc-b200-65a683ee8e4d
after
https://github.com/user-attachments/assets/acfaaf7a-43c7-495f-b352-5dd2cdaa94db
## Analysis Report
https://linear.app/comfyorg/issue/FE-401/bug-preview-3d-and-animation-nodes-lod-stuck-at-initial-page-load
## More
- Add `debounce` and pixel ratio limit
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Medium risk because it changes core `Load3d.handleResize()` rendering
behavior (pixel ratio/LOD) and adds a debounced zoom-driven resize
watcher, which could affect performance or visual output across all
Load3D nodes. Capture logic is also refactored to manipulate renderer
size/pixel ratio and camera params, so regressions would show up in
thumbnails/exports.
>
> **Overview**
> Fixes Load3D LOD/render sharpness when the graph canvas is zoomed by
threading a new `getZoomScale` option from `useLoad3d` into `Load3d` and
using it to call `renderer.setPixelRatio()` (clamped) during
`handleResize()`.
>
> Adds a debounced watcher on `canvasStore.appScalePercentage` to
trigger `handleResize()` on zoom changes, and updates
`SceneManager.captureScene()` to temporarily force pixel ratio 1 and
restore renderer size/pixel ratio and camera settings after capture.
Coverage is expanded with new Playwright smoke coverage plus unit tests
for zoom propagation, debouncing, pixel ratio behavior, and capture
state restoration.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
261940d111. 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-11734-fix-load3d-update-renderer-pixel-ratio-on-canvas-zoom-to-fix-LOD-resolution-3516d73d365081e6b3d4cdd05f516489)
by [Unito](https://www.unito.io)
## Summary
Fixes several issues in both Simplified Chinese (zh) and Traditional
Chinese (zh-TW) locale files that were identified through systematic
comparison against the English source.
### Changes
**nodeDefs.json (zh + zh-TW):**
- **CLIPLoader.description**: Added missing model recipes (lumina2, wan,
hidream, omnigen2) to match English source
- **ByteDanceSeedreamNode.display_name**: Updated version from "Seedream
4" to "Seedream 4.5 and 5.0" to match English
- **BriaImageEditNode.display_name**: Added missing "FIBO" model name
**nodeDefs.json (zh only):**
- **APG.inputs.eta.name**: Fixed incorrect translation "预计到达时间" (ETA) ->
kept as "eta" (technical parameter name)
**commands.json (zh + zh-TW):**
- **Comfy_ToggleLinear**: Updated label to match English "Toggle App
Mode"
- **Experimental_ToggleVueNodes**: Rebranded "Vue 节点"/"Vue 節點" to "Nodes
2.0" to match English
**settings.json (zh + zh-TW):**
- **Comfy_VueNodes_Enabled / Comfy_VueNodes_AutoScaleLayout**: Rebranded
"Vue 节点"/"Vue 節點" to "Nodes 2.0"
**main.json (zh + zh-TW):**
- **errorDialog.accessRestrictedMessage / accessRestrictedTitle**: Added
missing Chinese translations
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11930-fix-i18n-correct-zh-and-zh-TW-translations-3566d73d365081ff9b0beb1c1fc7ef1a)
by [Unito](https://www.unito.io)
---------
Co-authored-by: LifetimeVip <lifetimevip@users.noreply.github.com>
## Summary
Add 27 unit tests for MembersPanelContent component covering workspace
views, member management, and billing states.
## Changes
- **What**: New test file for MembersPanelContent with 27 tests across 8
describe blocks (personal workspace, owner view, member view, sorting,
search filtering, pending invites, single seat plan, member count
display)
## Review Focus
- Uses `@testing-library/vue` + `@testing-library/user-event` per
project conventions
- Component stubs (Button, SearchInput, Menu, UserAvatar) for isolation
- Reactive mock refs in `vi.hoisted()` shared across `vi.mock()` calls
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11402-test-add-MembersPanelContent-unit-tests-3476d73d36508107abcbce95b72b3fb7)
by [Unito](https://www.unito.io)
## Summary
Add Playwright e2e tests for the node replacement feature (swap nodes UI
in the errors tab).
## Changes
- **What**: 6 e2e test cases across two describe blocks covering single
and multi-type node replacement flows. Tests verify swap nodes group
visibility, in-place replacement, widget value preservation, Replace All
across multiple types, output connection preservation, and success toast
display. Includes typed mock data for `/api/node_replacements` and two
workflow fixture files with fake missing node types mapped to real core
nodes.
## Review Focus
- Mock setup pattern in `setupNodeReplacement` — enables feature flag
via `page.evaluate` and routes the API endpoint
- Workflow fixture design — uses fake node types (E2E_OldSampler,
E2E_OldUpscaler) that map to real registered types (KSampler,
ImageScaleBy)
- Assertion coverage for link preservation after replacement
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11242-test-add-e2e-tests-for-node-replacement-flows-3426d73d3650811e87d7f0d96fd66433)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Connor Byrne <c.byrne@comfy.org>
## Summary
The Vue node model picker mixes output assets into the dropdown with a
trailing ' [output]' suffix on the value. Forwarding that string to
loadModel as a literal filename under the configured input folder caused
a 404 and the model never rendered. Strip the trailing folder annotation
per call and use the matching folder so picking an output asset loads
correctly while plain values keep the configured folder.
Output assets stored under a subfolder (e.g. 3d/) were exposed in the
Vue node dropdown as just '<filename> [output]', so selection produced
an /api/view URL with empty subfolder and a 404. Read the subfolder from
the asset's OutputAssetMetadata and prefix it onto the annotated path so
the downstream load handler can split it back out and target the correct
folder.
## Screenshots (if applicable)
https://github.com/user-attachments/assets/463d1071-efdc-46a4-b101-8e1c012d19c7
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11805-fix-load3d-parse-output-input-temp-annotation-when-loading-3536d73d365081a8ac9cf75d14ae29e6)
by [Unito](https://www.unito.io)
## Summary
Fixed two minor documentation inaccuracies found during comprehensive
documentation audit:
- Corrected outdated "Lodash" reference to "Utility Functions" in unit
testing guide
- Updated package manager command from `npx` to `pnpm dlx` in Playwright
skill documentation
## Changes Made
### Documentation Fixes
#### docs/testing/unit-testing.md:150
- **Before**: `## Mocking Lodash Functions`
- **After**: `## Mocking Utility Functions`
- **Reason**: The section describes mocking `es-toolkit/compat`
functions, not Lodash. The project uses es-toolkit as stated in
AGENTS.md line 158 and docs/guidance/typescript.md line 60.
#### .claude/skills/writing-playwright-tests/SKILL.md:117
- **Before**: `npx playwright show-trace trace.zip`
- **After**: `pnpm dlx playwright show-trace trace.zip`
- **Reason**: Project standardizes on pnpm, explicitly avoiding npx per
AGENTS.md line 42: "use `pnpx` or `pnpm dlx` — never `npx`"
## Audit Summary
Comprehensive audit verified accuracy of:
- ✅ Core documentation (CLAUDE.md, AGENTS.md, README.md)
- ✅ All docs/**/*.md files (40+ files including ADRs, testing guides,
architecture docs)
- ✅ All README files throughout repository (21 files)
- ✅ All .claude/commands/*.md files (8 files)
- ✅ Code examples and API references
- ✅ File structure references (verified src/router.ts, src/i18n.ts,
src/main.ts, config files exist)
- ✅ Package dependencies (es-toolkit ^1.39.9 confirmed)
- ✅ Script commands (pnpm test:unit, pnpm test:browser:local, etc.)
- ✅ External resource links
- ✅ ADR index and dates
All other documentation remains accurate and up-to-date as of
2026-05-04.
## Review Notes
This PR contains only two trivial corrections to terminology/commands.
No functional changes, no code changes, no breaking changes. The
documentation audit found the codebase documentation to be in excellent
condition overall.
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
## Summary
The existing \`MixpanelTelemetryProvider.test.ts\` was misnamed: it only
tested \`getExecutionContext\` from \`../../utils/getExecutionContext\`,
never the provider class itself — provider coverage sat at **0%**
despite a 239-line test file living next to it.
This PR:
1. **Renames** the existing test file to
\`src/platform/telemetry/utils/getExecutionContext.test.ts\` (co-located
with the source it actually tests). Updates its relative import to
\`./getExecutionContext\`.
2. **Adds** a fresh
\`src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.test.ts\`
covering the provider class.
Lifts provider coverage from **0% → 81.1%** lines (functions 73.5%,
branches 88.5%).
## Test Coverage (new tests)
Constructor / initialization:
- Without \`mixpanel_token\`, warns and disables itself; subsequent
\`trackXxx\` calls are no-ops.
- With \`mixpanel_token\`, dynamically imports mixpanel-browser, calls
\`init\`, and after \`loaded()\` fires identifies users via
\`onUserResolved\`.
Queueing semantics:
- Events fired before \`loaded()\` are queued and flushed in order once
Mixpanel reports ready.
Filtering:
- Events listed in the default \`disabledEvents\` set (e.g.
\`workflow_opened\`) are suppressed.
Direct dispatchers (parameterized \`it.each\`):
- 16 \`trackXxx\` methods covered: signup/auth/login, subscription
lifecycle, credit topup events, template lifecycle, workflow
imported/saved, default-view, enter-linear, share-flow, execution
success/error.
- \`trackApiCreditTopupButtonPurchaseClicked\` payload includes
\`credit_amount\`.
- \`trackEmailVerification\` dispatches the matching
\`USER_EMAIL_VERIFY_*\` event for each stage.
- \`trackSubscription\` maps \`'modal_opened'\` and
\`'subscribe_clicked'\` to their distinct events.
- \`trackRunButton\` populates \`RunButtonProperties\` from the
execution context.
- \`trackWorkflowExecution\` consumes the latest \`trigger_source\` from
\`trackRunButton\`, then resets it to \`'unknown'\`.
Survey:
- On \`'submitted'\`, normalized properties are written to
\`Mixpanel.people\`.
- On \`'opened'\`, \`Mixpanel.people\` is not touched.
Topup delegation:
- \`startTopupTracking\`, \`clearTopupTracking\`,
\`checkForCompletedTopup\` all forward to the \`topupTracker\` utility.
## Testing
\`\`\`bash
pnpm vitest run
src/platform/telemetry/providers/cloud/MixpanelTelemetryProvider.test.ts
pnpm vitest run src/platform/telemetry/utils/getExecutionContext.test.ts
\`\`\`
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11749-test-rename-misnamed-Mixpanel-test-and-cover-the-actual-provider-class-3516d73d365081609c54f34bd2d8b00d)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
## Automated Ingest API Type Update
This PR updates the Ingest API TypeScript types and Zod schemas from the
latest cloud OpenAPI specification.
- Cloud commit: 758732f
- 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: skishore23 <178779+skishore23@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
Nodes in the 'essentials' category do not have type 'core'. The check
has been updated to instead use the dedicated `isCoreNode` prop.
No tests currently. The existing tests for this code section all mock
out the relevant code path and properly writing a test for this would
take far more time than I can allocate right now.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11809-Fix-core-node-detection-for-missing-nodes-3536d73d3650815aabb2deb54c4ecec4)
by [Unito](https://www.unito.io)
## Summary
Extends the new unsafe Error assertion lint rule from #11845 to also
reject angle-bracket assertions.
## Changes
- **What**: Adds a `TSTypeAssertion` selector alongside the existing
`TSAsExpression` selector and broadens the lint message to cover Error
type assertions generally.
## Review Focus
This is stacked on #11845 and only addresses the lint-rule bypass for
`<Error>value` syntax.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11909-fix-catch-angle-bracket-Error-assertions-3566d73d365081f58ecfecfa2e948c33)
by [Unito](https://www.unito.io)
---------
Co-authored-by: bymyself <cbyrne@comfy.org>
*PR Created by the Glary-Bot Agent*
---
## Summary
Updates the "Install via GitHub" CTA buttons on the `/download` page to
deep-link to the install instructions section of the ComfyUI README
(`#installing`) instead of the repo root, so users land directly on
setup steps.
## Changes
- `apps/website/src/config/routes.ts`: add `externalLinks.githubInstall
= 'https://github.com/Comfy-Org/ComfyUI#installing'` (separate from
`externalLinks.github`, which is still used by the navbar/footer/star
badge for the generic repo link).
- `apps/website/src/components/product/local/HeroSection.vue`: switch
the secondary CTA next to "Download Local" from `externalLinks.github`
to `externalLinks.githubInstall`.
- `apps/website/src/components/product/local/EcoSystemSection.vue`: same
swap on the ecosystem-section CTA.
The platform-aware `Download Local` button (Windows/macOS installers via
`useDownloadUrl`) and the generic GitHub social/repo links elsewhere on
the site are unchanged.
## Verification
- `pnpm --filter @comfyorg/website typecheck` — 0 errors
- `pnpm --filter @comfyorg/website test:unit` — 23/23 passing
- `pnpm exec eslint` on changed files — clean
- `pnpm exec oxfmt --check` — clean
- Manual via `pnpm dev` + Playwright DOM assertion on `/download`:
- Hero "INSTALL FROM GITHUB" →
`https://github.com/Comfy-Org/ComfyUI#installing` ✓
- EcoSystem "INSTALL FROM GITHUB" →
`https://github.com/Comfy-Org/ComfyUI#installing` ✓
- Other "GitHub" links (nav, footer, star badge) → unchanged at
`https://github.com/Comfy-Org/ComfyUI` ✓
Per request from #website-and-docs: the local download surfaces should
at least link to the ComfyUI install instructions on GitHub. Companion
change to comfy-org/website#227.
## Screenshots

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11852-fix-website-point-Install-via-GitHub-buttons-to-install-docs-anchor-3546d73d365081fe8370cd675ae8f896)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
*PR Created by the Glary-Bot Agent*
---
Synthesizes lessons from the recent `v1.43.16` backport session (PRs
#11856–#11862) into the `.claude/skills/backport-management/` skill.
**Documentation only — no code, no workflow, no automation changes.**
## What's new
### `SKILL.md`
- **Quick Start** expanded from 7 to 12 steps, surfacing the pre-filter
and target-file-existence check that should run BEFORE per-PR triage
- **New gotcha**: *Cherry-Picked Tests Can Reference Files Added By
Earlier Unbackported PRs* — drop the test, document why
- **New gotcha**: *Backport-Only Compatibility Shims* — when a
refactor-style fix's mechanism relies on changes that aren't on the
older branch, a literal cherry-pick can recreate the bug for extensions
still using the old contract. Real example: #11541 + `LGraphNode.vue`
`handleDrop`
- **New section**: *Path Pre-Filter* under Auto-Skip Categories —
auto-skip PRs touching only `apps/website/`, `browser_tests/`,
`.github/`, `packages/design-system/`, generated types, `.claude/`,
`docs/`, or `*.stories.ts`. Removes 30–50% of candidates without reading
their bodies
### `reference/analysis.md`
- **New subsection**: *Verify Target File Existence* — `git cat-file -e`
before cherry-pick to skip PRs targeting features the branch doesn't
ship, faster than letting cherry-pick fail with modify/delete
- **New section**: *Tiered Triage* — **Tier 1** (core editor
must-haves), **Tier 2** (cloud-distribution only), **Tier 3** (skip)
before per-PR Y/N. Surfaces release-engineering decisions a flat
MUST/SHOULD list obscures
### `reference/discovery.md`
- Reconciliation workflow combining Slack bot list + git gap,
subtracting already-backported PRs (extracted via `Backport of #` grep
on the branch)
- Clarifies that no single source is authoritative
### `reference/execution.md`
- **New Step 0**: *Test-Then-Resolve Pre-Pass* — moved the dry-run loop
to the top of the workflow so you classify clean vs conflict before
triggering automation
- **New inline guard**: *Public-API conflict review* in the manual
cherry-pick loop — consult oracle BEFORE pushing if resolution touches
LiteGraph callbacks, `node.*` methods, or extension-API surfaces
- **Per-PR validation block** (typecheck + targeted unit tests + ESLint
+ oxfmt) before push, in addition to wave verification
- **Three new lessons learned** (19–21) covering the above
- **New PR Body Template** for manual cherry-picks — non-negotiable
conflict-resolution section so reviewers don't have to re-derive
resolution logic from the diff six months later
## Why
1. **Path pre-filtering** caught 35 of 61 candidates as `skip` before
any per-PR analysis was needed during the `v1.43.16` session
(`apps/website/`, CI, tests, design-system). The previous flat
MUST/SHOULD/SKIP rubric meant reading every PR.
2. **Oracle review on PR #11856** (#11541 backport) caught a regression
in `LGraphNode.vue:823` where the upstream PR's removal of the legacy
`handled === true` sync-return path would silently break custom nodes
still using the old `onDragDrop` contract — recreating the very
duplicate-node bug the PR was fixing. The skill had no guidance for this
class of issue.
3. **Two separate backports** (#11180, #11541) hit the same
modify/delete conflict pattern: a test file added on `main` by an
earlier unbackported PR. The skill's *Conflict Triage* table covered
modify/delete generally but didn't surface this specific anti-pattern of
smuggling in test scaffolding without its prerequisites.
4. **Discovery sources** — the skill assumed Slack bot + git gap as
inputs but didn't show how to reconcile them with already-backported
PRs, leading to potential double-cherry-picking.
## Verification
- No code, no workflow, no automation changes — skill documentation only
- `git diff --check` clean
- All four edited files render as valid markdown
- Cross-references between SKILL.md and `reference/*.md` files validated
by line-count check
## Out of scope (intentionally not changed)
- The `pr-backport.yaml` GitHub Action — the skill describes this but
doesn't own it
- The Slack bot — described as *Source 1*, not modified
- The PR title convention `[backport TARGET] ...` — kept as-is
(CodeRabbit's auto-skip filter relies on it)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11868-docs-skill-improve-backport-management-with-tiered-triage-path-pre-filter-and-public-3556d73d3650814faec3df795567bee3)
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>
*PR Created by the Glary-Bot Agent*
---
## Step 2 of 2 — GitHub squash-commit incident recovery for #11460
This is the companion to #11629. After #11629 (the revert) merges, this
PR re-applies the original PR #11460 contents on top of the post-revert
`main`, restoring the intended state of the codebase.
### ⚠️ Sequencing — must merge after #11629
Until #11629 is merged, this PR's diff against `main` is empty (because
`main` currently still contains the squashed bug-dump-ingest commit that
#11629 will revert). After #11629 is merged into `main`, GitHub will
recompute the diff and this PR will show a clean re-add of the same 5
`.claude/skills/bug-dump-ingest/` files.
### Verification
The branch was constructed by:
1. Branching from `glary/revert-pr-11460` (the revert branch in #11629).
2. `git checkout FETCH_HEAD -- .claude/skills/bug-dump-ingest/` from
`refs/pull/11460/head` to restore the exact files.
3. Committing them as a single squash-style commit.
I verified that all 5 file blobs are byte-for-byte identical to the
original squash commit `559922eaa5c129767c22275c206c6877931ac15c` by
comparing git object SHAs:
| File | Object SHA |
|---|---|
| `.claude/skills/bug-dump-ingest/SKILL.md` |
`413737835fa1c996291019483effdd39e6da33e5` |
| `.claude/skills/bug-dump-ingest/reference/examples.md` |
`4fc54a4f14b1359de63f558acca0de48c1b65c57` |
| `.claude/skills/bug-dump-ingest/reference/linear-api.md` |
`57986740df2ee02c19b81059f0f1e00e54c2a042` |
| `.claude/skills/bug-dump-ingest/reference/schema.md` |
`84db1a5818c04ee53a94167092ec76dd814992d4` |
| `.claude/skills/bug-dump-ingest/reference/verify-commands.md` |
`a2c99a43a030ccc3769692d3c09be74132645bb4` |
### Notes
- One commit on `refs/pull/11460/head` (an automated lint commit
`76ca1598e`) modified
`src/renderer/extensions/vueNodes/widgets/components/WidgetChart.test.ts`
to remove an `eslint-disable` directive. That change was **not** in the
original squash commit on `main` (verified via `git show --stat`), and
the directive has separately been removed from `main` by an unrelated
commit (#11550), so re-applying that change would now be a no-op. This
repair PR therefore intentionally restores **only the 5 bug-dump-ingest
files**, matching the original squash commit exactly.
- Branch name is `glary/repair-pr-11460` (the `glary/` prefix is
required by the tooling; otherwise equivalent to GitHub's suggested
`repair-pr-11460`).
Refs: #11460, #11629
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11630-Repair-re-add-bug-dump-ingest-skill-11460-GitHub-squash-commit-incident-recovery--34d6d73d365081acbd54c19316561fa9)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
*PR Created by the Glary-Bot Agent*
---
## Step 1 of 2 — GitHub squash-commit incident recovery for #11460
GitHub flagged the squash commit for #11460
(`559922eaa5c129767c22275c206c6877931ac15c`) as affected by the
squash-commit incident that produced non-deterministic merges. This PR
is **step 1 of 2** in the recovery procedure GitHub asked us to follow.
### What this PR does
Reverts `559922eaa5c129767c22275c206c6877931ac15c` (the affected squash
commit for #11460, "feat: add bug-dump-ingest skill") on top of current
`main`.
### Diff
5 files removed, all under `.claude/skills/bug-dump-ingest/`:
- `.claude/skills/bug-dump-ingest/SKILL.md`
- `.claude/skills/bug-dump-ingest/reference/examples.md`
- `.claude/skills/bug-dump-ingest/reference/linear-api.md`
- `.claude/skills/bug-dump-ingest/reference/schema.md`
- `.claude/skills/bug-dump-ingest/reference/verify-commands.md`
`git revert` applied cleanly with no conflicts.
### Recovery procedure
Per GitHub's instructions:
1. **This PR (step 1)** — Revert the affected squash commit. Removes
both the original changes and any unintended changes that the incident
may have introduced.
2. **Next PR (step 2)** — Re-apply the original PR's changes from
`refs/pull/11460/head`, rebased onto post-revert `main`. Will be opened
as a separate "repair" PR.
### Review notes
- This is a pure revert; please confirm the diff is exactly the inverse
of #11460.
- After this PR is merged, the companion repair PR will land the
original changes back on `main` from a clean source ref, restoring the
intended state of the codebase.
- Branch name is `glary/revert-pr-11460` (the `glary/` prefix is
required by the tool that opened this PR — it's otherwise equivalent to
GitHub's suggested `revert-pr-11460`).
- Code review: Oracle reviewed and found 0 issues
(critical/warning/suggestion all 0). Ready to merge.
Refs: #11460
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11629-Revert-feat-add-bug-dump-ingest-skill-11460-GitHub-squash-commit-incident-recove-34d6d73d3650810f9ed3f6068e4f1511)
by [Unito](https://www.unito.io)
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
Per product feedback, scroll-jacking the page to step through
'Industries that create with ComfyUI' categories feels bad on desktop.
This PR removes the desktop scroll override and switches to hover-driven
imagery on lg+, while preserving the existing pin/scrub interaction on
mobile/touch breakpoints.
## Changes
- **What**:
- Gate `usePinScrub` setup to `(max-width: 1023px)` so the pin/scrub
only runs on touch breakpoints; desktop never engages it.
- Wire `@mouseenter` and `@focus` on each category button to update the
active category on desktop. Click still works on both modes via the
existing `scrollToIndex` (which falls through to a direct ref set when
no ScrollTrigger instance is present).
- Pass the same `(max-width: 1023px)` to `useParallax` via its existing
`mediaQuery` option so parallax doesn't run against the no-longer-pinned
section on desktop.
- Apply the section's `lg:h-[calc(100vh+60px)]` unconditionally on lg+
since pin no longer drives it; mobile height is still managed
dynamically by `usePinScrub`'s `cacheLayout`.
- **Breaking**: None.
- **Dependencies**: None.
## Review Focus
- Mobile path inside `usePinScrub.onMounted` is byte-identical to main —
only the early-return condition gained one extra clause
(`!window.matchMedia('(max-width: 1023px)').matches`), which mobile
evaluates to `false` so it falls through to the existing setup.
- `onCategoryHover` early-returns when `isEnabled` is true, making it a
no-op on mobile (where pin is engaged), so a tap doesn't accidentally
fight the scrub.
- `@focus` is wired alongside `@mouseenter` so keyboard tab navigation
also previews the imagery.
- The previous Lenis-on-macOS workaround from this branch is reverted —
it was only needed because the scroll override existed.
## Screenshots (if applicable)
N/A — interaction change. Test on desktop (≥1024px) by hovering category
labels — imagery should swap with no scroll-jacking. Test on mobile
(<1024px) by scrolling the section; pin/scrub should engage as before.
---------
Co-authored-by: Marwan Ahmed <marwan@Marwans-MacBook-Pro.local>
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Refactor E2E tests added in #11210 that repeated full prior-test bodies
as setup, combining duplicate pairs into single tests with named
`test.step()` blocks.
## Changes
- **What**: In
[`browser_tests/tests/keyboardShortcutActions.spec.ts`](../blob/batch-dispatch/cr-11556/browser_tests/tests/keyboardShortcutActions.spec.ts):
- Merge `Ctrl+Z undoes` + `Ctrl+Shift+Z redoes` → single test with two
`test.step()` blocks.
- Merge `Ctrl+, opens settings dialog` + `Escape closes settings dialog`
→ single test with two `test.step()` blocks.
- **What**: In
[`browser_tests/tests/topbarMenuCommands.spec.ts`](../blob/batch-dispatch/cr-11556/browser_tests/tests/topbarMenuCommands.spec.ts):
- Merge `Edit > Undo` + `Edit > Redo` → single test with two
`test.step()` blocks.
The redo step now reuses the post-undo state from its preceding step
instead of re-creating and re-undoing the node, removing the duplicated
setup the reviewer flagged.
## Review Focus
- Naming of combined tests and `test.step()` labels.
- Note: per @AustinMroz's [comment
thread](https://github.com/Comfy-Org/ComfyUI_frontend/pull/11210#discussion_r3113526265),
location 2 in the issue refers to the `Escape closes settings dialog`
test (which duplicated the `Ctrl+,` test body), not the `Delete` test
(which has unique logic). Treated accordingly.
Fixes#11556
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11835-test-combine-duplicated-undo-redo-and-settings-dialog-E2E-tests-with-test-step-3546d73d365081689df3c56bfbb6f4e4)
by [Unito](https://www.unito.io)
## Summary
Replaces all 7 production `as Error` type assertions with proper
`instanceof Error` narrowing or a new `toError()` helper, and adds an
ESLint rule to prevent new ones. First slice of #11429 (the `as Error`
category — 9 total occurrences, 7 production + 2 in a test file left
untouched).
## Changes
- **What**:
- New `src/utils/errorUtil.ts` exporting `toError(value: unknown):
Error` and `getErrorMessage(value: unknown): string | undefined`.
`toError` returns the value unchanged if already an `Error`, otherwise
wraps it (handles strings, `undefined`, JSON-serializable objects, and
circular refs via `String()` fallback).
- Refactored 7 production call sites:
- `src/services/gateway/registrySearchGateway.ts` — `toError(error)` for
`lastError` assignment in fallback loop
- `src/platform/cloud/onboarding/auth.ts` (×2) — `toError(error)` for
`captureApiError` Sentry calls
-
`src/renderer/extensions/vueNodes/widgets/composables/audio/useAudioRecorder.ts`
— `toError(err)` before forwarding to `options.onError`
- `src/extensions/core/load3d/LoaderManager.ts` — replaced `error as
Error & { response?: ... }` cast inside `isNotFoundError` with
`'response' in error` + nested narrowing
- `apps/desktop-ui/src/stores/maintenanceTaskStore.ts` — inline `error
instanceof Error ? error.message : String(error)`
- `apps/desktop-ui/src/components/maintenance/TaskListPanel.vue` —
inline `error instanceof Error ? error.message : undefined`
- New ESLint rule (`no-restricted-syntax` block named
`comfy/no-unsafe-error-assertion`) banning `TSAsExpression
TSTypeReference[typeName.name='Error']` in `src/**` and `apps/*/src/**`,
with test files (`*.test.ts`, `*.spec.ts`) excluded.
- 12 unit tests for the new helpers in `src/utils/errorUtil.test.ts`.
- **Breaking**: none
- **Dependencies**: none
## Review Focus
- The lint rule is scoped to non-test source files. Test files retain
freedom to use `as Error` for fixture construction; only 2 occurrences
exist (in `teamWorkspaceStore.test.ts` and `errorDialog.spec.ts`) and
they're intentional.
- `toError` is duplicated as inline `instanceof` narrowing in
`apps/desktop-ui/` rather than imported, since the desktop-ui workspace
doesn't share `@/utils/` with the main app and adding a path mapping for
one helper felt heavier than two inline guards.
- Remaining `as`-on-DOM categories (HTMLElement ×133, HTMLInputElement
×55, HTMLCanvasElement ×36, KeyboardEvent ×7, Element ×3, MouseEvent ×2,
Event ×2) are intentionally left for follow-up PRs to keep this one
reviewable.
Refs #11429
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11845-refactor-replace-unsafe-as-Error-assertions-with-type-guards-3546d73d36508137a015c4f9e8708f23)
by [Unito](https://www.unito.io)
*PR Created by the Glary-Bot Agent*
---
Removes the "coming soon" badge from the Parallel Job Execution feature
card on the cloud pricing page (`comfy.org/cloud/pricing`).
## Changes
- `apps/website/src/components/pricing/WhatsIncludedSection.vue`: drop
`isComingSoon: true` from feature11 so it renders with the standard
check icon and no badge.
The `isComingSoon` mechanism (clock icon + yellow badge) is preserved in
the component for future use on other features.
## Note
The FAQ copy elsewhere on the site (`cloud.faq.9.a`) still references
"one active job at a time" and "parallel runs soon". That copy will be
updated separately.
## Verification
- `pnpm typecheck` (website): 0 errors
- `pnpm lint`: clean (1 pre-existing warning unrelated to this change)
- `pnpm format:check`: clean
- `pnpm test:unit` (website): 20 passed
- Visual check via Playwright on local dev server (see screenshot)
## Screenshots

┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11819-fix-remove-coming-soon-badge-from-parallel-job-execution-3546d73d365081d19060f976095d03ac)
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
Registers a new nightly feature survey for the Queue Progress Overlay
using the existing feature-survey registry (same pattern as the merged
node-search survey, PRs #8175/#8355/#9934).
- New registry entry `queue-progress-overlay` → Typeform `HZ5saxry`,
threshold **16**, 5s display delay.
- `trackFeatureUsed()` wired at the major user-initiated handlers inside
the overlay so the survey triggers regardless of panel location
(floating-right v1 or docked-left v2).
- Run button and other ActionBar items that the overlay pops over from
are deliberately **not** tracked — tracking is scoped to interactions
that originate inside the job panel / queue progress overlay itself.
## Tracked interactions
Both variants share most sub-components, so tracking is instrumented
once at each logical surface:
- **`QueueProgressOverlay.vue`** (v1 container): `viewAllJobs`,
`interruptAll`, `cancelQueuedWorkflows`, `onClearHistoryFromMenu`,
`toggleAssetsSidebar`, `onCancelItem`, `onDeleteItem`, `inspectJobAsset`
- **`QueueOverlayExpanded.vue`**: job tab switches
- **`JobHistorySidebarTab.vue`** (v2 docked): job tab switches,
`clearQueuedWorkflows`, `onClearHistory`, `onCancelItem`,
`onDeleteItem`, `onViewItem`
- **`JobFilterActions.vue`** (shared): workflow filter + sort mode
selections
- **`JobHistoryActionsMenu.vue`** (shared): docked-history toggle +
run-progress-bar toggle
Deliberately **not tracked** to keep the signal clean:
- Hover handlers (ambient preview behaviour)
- Search-box keystrokes (debounced typing)
- Context menu open and menu-item dispatch — menu actions either bubble
through already-tracked terminal handlers (e.g. inspect-asset →
`onViewItem`) or are secondary operations (copy-id, open-workflow,
download). Avoids double-counting per code review feedback.
## How it works (inherits from existing infrastructure)
1. `surveyRegistry.ts` drives `NightlySurveyController` →
`NightlySurveyPopover`, which handles the Typeform embed.
2. Eligibility already gated on `isNightly && !isCloud && !isDesktop`,
once-per-user, 4-day global cooldown across all surveys, and opt-out.
3. Typeform response routing to #C0ALLT6Q3SQ is handled on the Typeform
side.
## Verification
- `pnpm typecheck` ✅
- `pnpm lint` ✅ (no new warnings)
- `pnpm knip` ✅
- `pnpm test:unit` on `src/components/queue`,
`src/components/sidebar/tabs/JobHistorySidebarTab`,
`src/platform/surveys` → **123/123 passing**
- Pre-commit hooks (stylelint, oxfmt, oxlint, eslint, typecheck) all
pass
- Manual: dev server + backend boot cleanly, app loads without new
runtime errors, `localStorage['Comfy.FeatureUsage']` layout verified to
match what `useFeatureUsageTracker` writes
## Notes
- Survey key `queue-progress-overlay` covers both v1 (floating-right)
and v2 (docked-sidebar) per product guidance: _"This should trigger
regardless of the location of the panel (docked from left or floating on
right)."_ Both surfaces are the same product feature — the survey is
intentionally scoped to the whole job-panel experience.
## Screenshots

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11560-feat-add-queue-progress-overlay-feature-survey-34b6d73d3650819a9a50fd67fd9b5941)
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
Adds the building blocks for a responsive media system on
`apps/website`, motivated by the gallery video blurriness raised in
Slack. Three independent pieces:
1. **`<SiteVideo>` Vue component + URL helper** — emits a `<video>` with
multiple `<source>` tags, designed to pair with assets named
`${name}-${width}.${format}` on `media.comfy.org`.
2. **`scripts/process-videos.sh`** — local-developer `ffmpeg` helper
that produces VP9/WebM + H.264/MP4 variants and a poster JPG. Not wired
into CI; the team uploads to `media.comfy.org` out-of-band.
3. **Marketing image conventions** — shared `MARKETING_FORMATS` /
`MARKETING_WIDTHS` constants and a README documenting how to render
local marketing images via Astro's built-in `<Picture>` from
`astro:assets`.
This PR is **infrastructure only** — no existing pages are modified.
Adoption (e.g. converting `HeroSection`, gallery videos) is a follow-up.
The new files are added to knip's ignore list with the existing "pending
stacked PR" pattern.
## Why this shape
- **No custom `<Picture>` wrapper.** Astro 5 already ships a
`ResponsiveImage` component (name conflict), and Astro's
`LocalImageProps | RemoteImageProps` discriminated union does not
survive a thin wrapper without unsafe `as` casts. Shared constants give
the consistency benefit at lower cost.
- **No CI media-upload step.** The `Release: Website` workflow currently
only refreshes the Ashby snapshot; wiring GCS uploads into it would
require new secrets and team coordination beyond this PR's scope. The
script runs locally and outputs are uploaded to `media.comfy.org` the
same way as today.
- **Single resolution per `<video>`.** `<source media="...">` inside
`<video>` is unreliable across browsers (Safari ignores it). The script
generates multiple widths so callers can pick one per page; JS-based
selection can be layered on later if metrics demand it.
## What's verified
- `pnpm --filter @comfyorg/website test:unit` — 30 pass (7 new for
`buildVideoSources` / `videoKey`)
- `pnpm --filter @comfyorg/website typecheck` — clean
- `pnpm --filter @comfyorg/website build` — 41 pages built clean
- `pnpm knip` — exit 0
- `oxfmt --check` and `oxlint` clean on all changed files
- `bash -n` on `process-videos.sh` clean; usage and missing-deps paths
exercised manually
- Manual: home page and `/gallery` rendered via `astro dev` — both
unchanged with zero console errors (screenshots attached)
## Review feedback addressed
After Oracle review, three follow-up commits land:
- **`SiteVideo` reactivity** — `sources` is now `computed`; the
`<video>` is keyed on the joined source URLs so it remounts when the
source set changes (browsers don't reload on `<source>` mutation).
- **`SiteVideo` accessibility** — `aria-hidden="true"` only when truly
decorative (no `alt` and no `controls`).
- **Shell script robustness** — probes duration with `ffprobe` and falls
back to `t=0` for clips shorter than 1s; enables `nocaseglob` so
`CLIP.MP4` is picked up.
- **Docs** — clarifies when to use `<SiteVideo>` (lightweight
multi-source) vs `<VideoPlayer>` (captions, controls, scrubber).
## Out of scope (follow-ups)
- Converting existing pages (`HeroSection`, customer detail heros,
gallery) to use the new components. Most current images are CDN-hosted
and migrating them is a separate decision.
- Re-encoding the gallery videos at a higher source width to actually
fix the blurriness — that requires the team to run `process-videos.sh`
against the source clips and re-upload.
- Combining `<SiteVideo>`'s multi-source support with `<VideoPlayer>`'s
rich chrome.
## Screenshots


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11869-feat-website-add-responsive-media-tooling-for-marketing-assets-3556d73d3650818899c7f9ed3204c9a5)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
## Summary
Use exact BLAKE3 hash lookups first for missing model/media detection,
and add a separate public-inclusive input asset cache so public input
assets are considered missing-detection candidates without changing the
user-only input assets shown in the UI.
## Changes
- **What**:
- Added `assetService.checkAssetHash()` for `HEAD
/api/assets/hash/{hash}` status-only existence checks.
- Added strict BLAKE3 hash helpers so only `blake3:<64 hex>` media
values and raw 64-hex BLAKE3 model metadata are sent to the hash
endpoint.
- Updated missing media detection to group BLAKE3 candidates by hash,
resolve them through the hash endpoint, and fall back to the legacy
asset list path for invalid/unverifiable/non-hash values.
- Updated missing model detection to use hash lookup for BLAKE3-backed
asset-supported candidates before falling back to the existing node-type
asset matching path.
- Added `assetService.getInputAssetsIncludingPublic()` backed by a
dedicated cache that fetches input assets with `include_public=true` for
missing media fallback checks.
- Kept `assetsStore.inputAssets` user-only for widget/UI display, while
invalidating the public-inclusive missing-detection cache when input
assets may change.
- Added abort handling for paginated asset fetches and shared
public-input cache callers so one aborted caller does not cancel the
shared fetch for other callers.
- Added regression coverage for hash lookup, fallback behavior, abort
paths, public input fallback detection, and cache invalidation.
- **Dependencies**: None.
- **Change size**:
- Production code: 4 files, 400 insertions, 24 deletions, net +376.
- Test code: 4 files, 806 insertions, 59 deletions, net +747.
- Total: 8 files, 1206 insertions, 83 deletions, net +1123.
## Review Focus
- The public-inclusive input asset cache is intentionally separate from
`assetsStore.inputAssets`. The existing store data is user-only and
drives the asset widgets/sidebar, so using it for missing input
detection misses public assets. Making that store public-inclusive would
change UI data semantics; this PR instead keeps the UI dataset unchanged
and adds a missing-detection-specific cache in `assetService`.
- Hash lookup is only used when the workflow exposes a valid BLAKE3
hash. Filename-like values and invalid hash values still use the legacy
fallback path.
- Missing model detection keeps the existing fallback behavior for
non-hash candidates and for hash checks that are invalid or fail
transiently.
- Async model download cache refresh behavior is left unchanged; this PR
avoids coupling model download completion to input asset cache
invalidation.
- No browser/e2e test was added because this changes the missing asset
detection data path, not UI interaction or rendering. The behavioral
coverage is in unit tests for the asset service and the missing
media/model scanners.
## Follow-up Items
- Fix `assetsStore.updateAssetTags()` partial-failure recovery. If
`removeAssetTags()` succeeds and `addAssetTags()` fails, the local model
asset cache can roll back to tags that the backend has already removed;
this should be handled in a focused model asset cache PR.
- Consider extracting shared hash-verification flow used by missing
media and missing model scans after this behavior stabilizes.
- Consider adding a concurrency cap or short-lived request cache for
large workflows with many unique hash lookups.
- Consider splitting `assetService.ts` further, e.g. hash helpers, abort
utilities, and the public-inclusive input asset cache.
- Consider tightening the asset hash service API shape so callers do not
directly depend on HTTP-oriented statuses such as `invalid`.
- Consider adding broader mutation-path coverage for public-inclusive
input cache invalidation once the cache has more consumers.
Linear: FE-534
## Screenshots (if applicable)
Before <false positive / missing image / public asset>
https://github.com/user-attachments/assets/db7ce2a9-b169-4fae-bf9f-98bb93d3ee6d
After
https://github.com/user-attachments/assets/29af9f9e-b536-4fcd-a426-3add40bcb165
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11873-Use-hash-lookup-for-missing-asset-detection-3556d73d36508165babafb16614be0d8)
by [Unito](https://www.unito.io)
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 `25260868155`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11851-chore-website-refresh-Ashby-roles-snapshot-3546d73d365081579f98f13f7b58e611)
by [Unito](https://www.unito.io)
Co-authored-by: christian-byrne <72887196+christian-byrne@users.noreply.github.com>
## Summary
Adds tests for metadata parsers
## Changes
- **What**:
- add test file generation script
- identified & fixed bug in webp exif parsing over-reading
- identified & fix bug in mp3/ogg parser where it would read from a
fixed position instead of relative, causing incorrect reads throwing
RangeError
- added catch in latent + json parsing to resolve errors
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11307-test-add-metadata-parser-coverage-3446d73d36508108ac36dddcec0a54d4)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Add a new customer story for Groove Jones — Crocs x NFL FOOH holiday
campaign for Dick's Sporting Goods, delivered with Comfy.
## Changes
- **What**:
- New entry in
[`apps/website/src/config/customerStories.ts`](https://github.com/Comfy-Org/ComfyUI_frontend/blob/feat/groove-jones-customer-story/apps/website/src/config/customerStories.ts)
registering slug `groove-jones` with cover image hosted on
`media.comfy.org/website/customers/groove-jones/`.
- Added `customers.story.groove-jones.{category,title,body}` and
`customers.detail.groove-jones.topic-1` … `topic-10` translations (en +
zh-CN) in
[`apps/website/src/i18n/translations.ts`](https://github.com/Comfy-Org/ComfyUI_frontend/blob/feat/groove-jones-customer-story/apps/website/src/i18n/translations.ts).
10 sections matching design sidebar: INTRO, THE OUTPUT, THE PROBLEM, HOW
COMFY SOLVED THE PROBLEM, BRAND-TRAINED LORAS, MULTI-MODEL
ORCHESTRATION, THE PIPELINE, VERSION CONTROL, FINISHING IN NUKE, THE
TAKEAWAY.
- Includes 2 pull quotes (Doug Hogan, Dale Carman), 1 final blockquote +
author card, and 3 inline images.
- Routes `/customers/groove-jones` and `/zh-CN/customers/groove-jones`
are auto-generated by `[slug].astro`.
## Review Focus
- Contributors author card uses `TBD` placeholder names/roles — to be
filled in.
- No `readMoreHref` set yet (no public blog post URL).
- All 4 images uploaded to
`gs://comfy-org-videos/website/customers/groove-jones/` and served via
`media.comfy.org`.
<img width="1000" height="535" alt="Kapture 2026-05-02 at 23 17 04"
src="https://github.com/user-attachments/assets/28654d24-0d49-4303-82ac-b6923cd6bc93"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11849-feat-add-Groove-Jones-customer-story-3546d73d36508128a64bd6809ad77447)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
Fixes#11345
## Summary
`clearModel()` in `SceneModelManager` only traversed and disposed
`THREE.Mesh` instances, leaving `THREE.Points` objects (created by
`handlePLYModeSwitch()` for point-cloud mode) leaking GPU geometry and
material memory on repeated point-cloud loads/clears.
## Changes
- `SceneModelManager.ts`: extend the dispose traversal in `clearModel()`
to also handle `THREE.Points`, mirroring the pattern already used by
`removeAllMainModelsFromScene()`.
- `SceneModelManager.test.ts`: add regression test verifying
`geometry.dispose()` and `material.dispose()` are called for
`THREE.Points` children on `clearModel()`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11836-fix-load3d-dispose-THREE-Points-GPU-resources-in-clearModel-3546d73d365081718338e824bc3e737d)
by [Unito](https://www.unito.io)
## Summary
Adds `/llms.txt` at the apex following the [llms.txt
standard](https://llmstxt.org) — a curated, link-based markdown file
that tells LLM-based search agents (ChatGPT search, Perplexity, Claude
search, Google AI Overviews, etc.) what's most important on the site. It
complements `robots.txt` (crawler permissions) and `sitemap-index.xml`
(URL inventory) by giving AI agents a short, prose-friendly index they
can ingest into a context window.
## What's in the file
28 links across 6 sections:
- **Product** (6) — homepage, Local download, Cloud, Cloud pricing, API,
Enterprise
- **Workflows and Gallery** (2) — gallery + community workflows site
- **Customers and Case Studies** (5) — customers index + 4 named studios
(Series Entertainment, Moment Factory, Ubisoft Chord, Open Story
Movement)
- **Developers and Documentation** (4) — docs.comfy.org, ComfyUI repo,
Comfy-Org GitHub org, registry.comfy.org
- **Company** (6) — about, careers, contact, blog, privacy, terms
- **Optional** (5) — `zh-CN` locale variant, long-form enterprise case
studies, blog posts (de-prioritized per spec — agents can skip if
context-limited)
The intro paragraph names the four product surfaces (Local, Cloud, API,
Enterprise), the named customers, and the use-case industries (VFX &
animation, advertising, gaming, eCommerce/fashion) — so an agent that
ingests only the prose still gets the elevator pitch.
## Verification
- All 28 URLs verified live (`HTTP 200`) before commit.
- File is plain markdown — no build step. Astro/Vercel will serve it
from `apps/website/public/llms.txt` exactly as it serves `robots.txt`
(which lives in the same directory and ships at
`https://comfy.org/robots.txt`).
- Will verify on the Vercel preview deploy after this PR opens that
`curl -sI https://<preview>/llms.txt` returns `200` with a sensible
`content-type`. (`robots.txt` currently serves as `text/plain;
charset=utf-8` — `.txt` will likely do the same; that's fine for AI
agents.)
## Decisions
- **No `llms-full.txt` yet.** That variant inlines full prose of key
pages and requires curating substantive content. Deferred to a follow-up
— the marketing-site pages are mostly Vue-rendered hero/feature blocks
rather than long-form prose, so a meaningful `llms-full.txt` would need
either dedicated copy or a build step that flattens i18n strings +
section text. Tracking separately.
- **No comment line in `robots.txt`.** I considered adding a `# AI
agents: see /llms.txt` comment above the `Sitemap:` directive, but
decided against it: (a) the convention is to probe the well-known path
`/llms.txt` directly, not to discover it via robots.txt; (b)
`robots.txt` was just polished in #11823 with a deliberate compact
design and adding a non-standard comment would muddy that; (c) zero
implementations I checked actually parse robots.txt for llms.txt hints.
Easy to add later if needed.
## Context
Third of three follow-ups from the SEO/GEO sweep on 2026-05-02:
1. ~~Comfy-Router: add `X-Content-Type-Options: nosniff` to apex
security headers~~ (separate PR on `Comfy-Org/comfy-router`)
2. ~~Cloudflare: enable "Always Use HTTPS"~~ (dashboard toggle, no PR)
3. **This PR** — add `llms.txt` for GEO discovery
## Testing
- [x] All linked URLs return 200
- [x] File parses as valid markdown
- [ ] Preview deploy serves `/llms.txt` (will verify once preview is up)
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11830-feat-website-add-llms-txt-for-GEO-discovery-by-AI-search-agents-3546d73d365081a98c6bfc5301699f64)
by [Unito](https://www.unito.io)
## P0 SEO fix — entire marketing site is currently `noindex`
A deep-audit sweep on **2026-05-02** confirmed that every page on
`comfy.org` (home, `/about`, `/pricing`, `/customers`, `/careers`, …) is
being served with:
```
x-robots-tag: noindex
```
This hides the entire marketing site from Google and other search
engines.
### Root cause
`comfy.org` reaches the Astro marketing site via a Cloudflare Worker
reverse proxy
([`comfy-router`](https://github.com/Comfy-Org/comfy-router)) that does:
```js
fetch('https://website-frontend-comfyui.vercel.app/...')
```
Per Vercel's KB article [Avoiding duplicate-content with vercel.app
URLs](https://vercel.com/kb/guide/avoiding-duplicate-content-with-vercel-app-urls):
> By default, Vercel adds an `X-Robots-Tag: noindex` HTTP header to all
deployments hosted on `vercel.app` URLs. […] This header tells search
engines like Google not to index these deployment URLs.
Because `website-frontend-comfyui.vercel.app` is **not** registered as a
Production custom domain on the Vercel project, Vercel applies that
header — and our Worker faithfully forwards it back to `comfy.org`
clients (and Googlebot).
### Fix
Vercel's documented workaround for proxied `*.vercel.app` setups is to
override the header via `vercel.json` ([blog post by Dan
Denney](https://www.dandenney.com/posts/front-end-dev/fixing-x-robots-nofollow-with-vercel/)):
```json
"headers": [
{
"source": "/(.*)",
"has": [
{ "type": "host", "value": "website-frontend-comfyui.vercel.app" }
],
"headers": [
{ "key": "X-Robots-Tag", "value": "index, follow" }
]
}
]
```
### Why the `has` host filter
Critical scoping detail: without the `has` filter, the override would
also apply to PR-preview deployments at
`comfy-website-preview-pr-N.vercel.app`. We want previews to **stay**
`noindex` to avoid duplicate-content competition with production.
The filter pins the override to the production deployment hostname only.
The hostname matches `WEBSITE_ORIGIN` in `comfy-router/src/index.js`:
```js
const WEBSITE_ORIGIN = 'https://website-frontend-comfyui.vercel.app';
```
### Defense in depth
A parallel PR on `comfy-router` (TBD) will also strip `X-Robots-Tag:
noindex` at the Worker layer, so the public `comfy.org` response is
correct even if a future Vercel project change re-introduces the
upstream header.
### Verification (after merge + Vercel production deploy)
```bash
# Production should no longer be noindex
curl -sI https://comfy.org/ | grep -i x-robots-tag
curl -sI https://comfy.org/about | grep -i x-robots-tag
curl -sI https://comfy.org/pricing | grep -i x-robots-tag
# Expect: empty output, OR "x-robots-tag: index, follow"
# Direct Vercel production hostname
curl -sI https://website-frontend-comfyui.vercel.app/ | grep -i x-robots-tag
# Expect: "x-robots-tag: index, follow"
# Preview deploys must stay noindex (proves the host filter works)
curl -sI https://comfy-website-preview-pr-<N>.vercel.app/ | grep -i x-robots-tag
# Expect: "x-robots-tag: noindex"
```
### Pre-merge state (for the record)
```
$ curl -sI https://comfy.org/ | grep -iE 'x-robots-tag|x-served-by'
x-served-by: vercel-website
x-robots-tag: noindex
```
### Scope
Minimum delta — only `apps/website/vercel.json`. No other files touched.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11831-fix-website-override-X-Robots-Tag-to-index-follow-on-production-deployment-3546d73d365081489b62e6633d25dfe5)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Christian Byrne <christian@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
*PR Created by the Glary-Bot Agent*
---
## Summary
- Adds `browser_tests/tests/queueNotificationBanners.spec.ts` covering
`useQueueNotificationBanners` composable E2E behavior
- Adds `data-testid="queue-notification-banner"` to
`QueueNotificationBannerHost.vue` for stable test targeting
- Registers the new test ID in `TestIds.queue.notificationBanner`
### Test coverage added (7 tests)
| Group | Tests | Behavior |
|---|---|---|
| Queuing lifecycle | 4 | `promptQueueing` → banner appears,
`promptQueued` upgrades to queued, batch plural text, requestId mismatch
doesn't upgrade |
| Auto-dismiss | 1 | Banner disappears after 4s timeout |
| FIFO queue | 1 | Second notification shows after first auto-dismisses
|
| Direct queued | 1 | `promptQueued` without prior `promptQueueing`
shows banner directly |
### Approach
Tests dispatch `promptQueueing`/`promptQueued` custom events directly
via `window.app.api.dispatchCustomEvent()` inside `page.evaluate()`,
matching how `app.ts` triggers these events during real queue
operations. This avoids needing a running execution pipeline while
exercising the full composable → component → DOM rendering chain.
### Verification
- TypeScript: zero errors
- ESLint: clean
- oxlint: clean
- oxfmt: formatted
- Playwright execution requires running ComfyUI backend (not available
in sandbox)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11366-test-add-queue-notification-banners-lifecycle-browser-tests-3466d73d36508172a7ffd3fe3b4fd925)
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
Adds a unified manual-trigger release flow for the `apps/website`
package
(careers/marketing site at comfy.org), mirroring how main-app releases
work.
**User-facing flow:**
```
workflow_dispatch ──► fresh Ashby pull ──► auto-PR with snapshot bump
│
▼
existing CI / Vercel preview deploy
│
▼
human merges ──► auto prod deploy
```
The careers data on comfy.org comes from Ashby and is fetched at build
time by
`apps/website/src/utils/ashby.ts`. Without `WEBSITE_ASHBY_API_KEY`, the
build
falls back to a committed snapshot at
`apps/website/src/data/ashby-roles.snapshot.json`. That snapshot has
been
going stale because no CI workflow was passing the API key. This PR
fixes
both: a manual refresh workflow + day-to-day secrets wiring.
## Files
**Added**
- `.github/actions/ashby-pull/action.yaml` — composite action that runs
`pnpm --filter @comfyorg/website ashby:refresh-snapshot` with the Ashby
secrets piped in. Uses the existing `setup-frontend` composite for
pnpm/Node setup.
- `.github/workflows/release-website.yaml` — `workflow_dispatch`-only
release workflow. Checks out `main`, refreshes the snapshot via the
composite action, opens a PR labelled `Release:Website` via
`peter-evans/create-pull-request@c0f553fe…` (the same SHA pin used by
`release-version-bump.yaml`).
**Modified**
- `.github/workflows/ci-website-build.yaml` — adds
`WEBSITE_ASHBY_API_KEY`
and `WEBSITE_ASHBY_JOB_BOARD_NAME` env to the `Build website` step.
- `.github/workflows/ci-vercel-website-preview.yaml` — adds the same env
to both `vercel build` steps (preview + production).
## Snapshot fallback preserved
`apps/website/src/utils/ashby.ts` keeps using the committed snapshot
when
the API key is absent (e.g. fork PRs, secret rotation). Verified
locally:
```
$ pnpm --filter @comfyorg/website ashby:refresh-snapshot
Snapshot refresh aborted. Outcome: stale; reason: missing WEBSITE_ASHBY_API_KEY...
```
The release workflow surfaces this as a job failure, which is the
desired
behavior for a manual release trigger.
## Validation
- `yamllint --config-file .yamllint` on all changed YAML — clean
- `./scripts/cicd/check-yaml.sh` — clean
- `pinact run --check` on new files — clean (matches `.pinact.yaml`
policy)
- `pnpm install --frozen-lockfile` — works with `.nvmrc` Node 24
- Husky pre-commit hooks (eslint + typecheck + lint-staged) passed
## Caveats
- **Cannot fully end-to-end test until merged.** `workflow_dispatch`
workflows only run from branches that exist on `main`. The first
manual run can only happen after this PR lands. The pieces that
*can* be tested pre-merge — yamllint, pinact pin validation, and
the existing `CI: Website Build` / `CI: Vercel Website Preview`
workflows now picking up the secret — will run on this PR.
- **`Release:Website` label needs to be created** in the repo before
the auto-PR step will successfully apply it.
`peter-evans/create-pull-request`
will warn but not fail if the label doesn't exist. Suggested color:
`#4f6ef5` (matches `cloud/*` family in `release-branch-create.yaml`).
- The release workflow uses `secrets.PR_GH_TOKEN` (matching
`release-version-bump.yaml`) so the auto-PR can be authored by a
PAT and trigger downstream CI workflows. Without `PR_GH_TOKEN` it
will fall back behavior is up to GitHub Actions defaults — confirm
the secret exists before the first run.
## Context
Came out of work on `comfy-router#22` + `ComfyUI_frontend#11823`
(comfy.org/countdown subpage / website refresh). Discovered the
8+-day-stale snapshot while auditing the website build path.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11829-feat-ci-add-Release-Website-workflow-to-refresh-Ashby-snapshot-3546d73d3650811eb300d8bcb593c652)
by [Unito](https://www.unito.io)
## Summary
Once
[comfy-router#22](https://github.com/Comfy-Org/comfy-router/pull/22)
ships, `comfy.org/sitemap-index.xml` will return a unified index
aggregating both the website (38 URLs) and workflow-templates sitemaps.
This PR:
1. Reverts `Sitemap:` back to `/sitemap-index.xml` (was `/sitemap-0.xml`
in #11802 as a workaround for the 404).
2. Adds explicit allow records for 21 search and AI/LLM crawlers
(GPTBot, ChatGPT-User, OAI-SearchBot, Google-Extended, ClaudeBot,
Claude-Web, anthropic-ai, PerplexityBot, Perplexity-User,
Applebot-Extended, Bytespider, Amazonbot, CCBot, Meta-ExternalAgent,
Meta-ExternalFetcher, Diffbot, etc.).
3. Adds `Disallow:` for `/_astro/`, `/_website/`, `/_vercel/` — Vercel
build artifacts that aren't useful to crawl.
## Why granular UAs
Stacked `User-agent:` records (per [RFC 9309
§2.2](https://datatracker.ietf.org/doc/html/rfc9309#section-2.2)) share
one rule block. Listing each bot explicitly:
- Signals intent to AI bots that look for their UA in robots.txt before
crawling more aggressively.
- Surfaces our crawl policy clearly to anyone inspecting the file.
- Lets us add per-bot Disallows in future without restructuring.
## Merge order
⚠️ **Do NOT merge until comfy-router#22 is deployed to production.**
Until then, `/sitemap-index.xml` returns 404 and this PR would re-break
the issue PR #11802 patched. Verification:
```bash
curl -sI https://comfy.org/sitemap-index.xml
# expect: HTTP/2 200, x-served-by: worker-sitemap-index
```
Once that returns 200, this is safe to merge.
## Verification (after merge + deploy)
```bash
# robots.txt is served and points at the unified index
curl -s https://comfy.org/robots.txt | grep '^Sitemap:'
# → Sitemap: https://comfy.org/sitemap-index.xml
# Each AI crawler can fetch it
for ua in 'GPTBot/1.0' 'ClaudeBot/1.0' 'PerplexityBot/1.0' 'Google-Extended' 'Applebot-Extended'; do
curl -s -o /dev/null -w "$ua → %{http_code}\n" -A "$ua" https://comfy.org/robots.txt
done
# Sitemap is reachable from robots.txt
SITEMAP=$(curl -s https://comfy.org/robots.txt | awk -F': ' '/^Sitemap:/ {print $2}')
curl -s "$SITEMAP" | xmllint --noout - && echo "valid XML"
```
## Linear / closes
- Closes FE-437 (AI crawler rules)
- Updates FE-432 — the robots.txt change in #11802 was a workaround
that's no longer needed once #22 ships
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11823-feat-website-point-robots-txt-at-sitemap-index-xml-AI-crawler-rules-3546d73d3650811dbceedd06c00db444)
by [Unito](https://www.unito.io)
## Summary
Replaces the 4-step Cloud onboarding survey with a 7-step flow that
captures both ICP attributes and user persona dimensions. The survey
questions are now populated dynamically from remoteConfig.
## Changes
- **What**: New survey questions — Usage, Familiarity, Role, Team size,
Industry, Making, Source. Role / Team size / Industry are gated to
"Work" usage; Education users see a Student / Educator short list for
Role. Most option lists are randomized per visit (familiarity and team
size stay ordered as ordinals). \`SurveyResponses\` extended with
optional \`usage\`, \`role\`, \`teamSize\`, \`source\` fields.
- **Breaking**: None — \`useCase\` and \`workflowRelationship\` remain
optional in the type and existing telemetry normalization keeps working
unchanged.
## Review Focus
- The \`role\` step has a function-form \`options\` so the list can swap
based on \`usage\`. \`steps\` is a computed that filters by
\`showWhen()\` and resolves the option function — verify reactivity when
\`usage\` changes.
- Changing \`usage\` clears the previously-picked \`role\` via a watcher
to prevent a stale value from carrying over between Work / Education
modes.
- Per-visit shuffle is stable: option lists are passed through
\`randomize()\` once at module load, not on every render.
## Screenshots
https://github.com/user-attachments/assets/3602a388-50dc-401e-ada9-ea9016c5052d
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11628-feat-redesign-cloud-onboarding-survey-for-ICP-and-persona-signal-34d6d73d365081f4a792cfe76a987ffb)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Dante <bunggl@naver.com>
## Summary
- Adds `silentOnNotFound` option to `LoadModelOptions` interface,
threaded through `Load3d.loadModel` → `LoaderManager.loadModel`
- 404 errors (detected via message text or `response.status`) are
silently swallowed when `silentOnNotFound: true`; all other errors still
surface a toast
- Sets `silentOnNotFound: true` for output-folder loads in `load3d.ts`
and `saveMesh.ts` — covers shared workflows opened on a machine that
never ran them
## Test plan
- [x] `LoaderManager.test.ts` — 40 unit tests covering 404 suppression,
non-404 still toasts, stale load handling
- [x] `Load3DConfiguration.test.ts` — 4 unit tests verifying
`silentOnNotFound` propagates correctly through `configureForSaveMesh`
and `configure`
- [x] `load3d.spec.ts` — 2 E2E tests: 404 → no toast, 500 → toast
appears
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Changes error-handling behavior in the 3D model loading pipeline and
extends method signatures/options; risk is mainly missed call sites or
incorrectly classifying non-404 errors as 404 and hiding real failures.
>
> **Overview**
> Prevents noisy user-facing toasts when an *output* 3D model referenced
by `Preview3D`/`SaveGLB` is missing locally by adding a
`silentOnNotFound` flag and suppressing the "Error loading model" toast
specifically for HTTP 404 failures.
>
> Threads the new `LoadModelOptions` through `Load3d.loadModel` →
`LoaderManager.loadModel` and updates `Load3DConfiguration`/callers to
opt in for output-folder loads, with new unit + Playwright coverage (404
stays silent, non-404 still toasts).
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
049f75ef60. 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://app.notion.com/p/PR-11807-fix-load3d-suppress-error-toast-on-404-when-loading-output-model-file-3536d73d36508129ac0de1d5b081dcf0)
by [Unito](https://www.unito.io)
## Summary
<!-- One sentence describing what changed and why. -->
robots.txt at [comfy.org](https://comfy.org/) references
/sitemap-index.xml which returns 404. The actual working sitemap is at
/sitemap-0.xml (200, 38 URLs). This blocks search engines from
discovering the sitemap.
## Changes
- **What**: <!-- Core functionality added/modified -->
- Update robots.txt to point to /sitemap-0.xml, OR
## Review Focus
<!-- Critical design decisions or edge cases that need attention -->
<!-- If this PR fixes an issue, uncomment and update the line below -->
<!-- Fixes #ISSUE_NUMBER -->
## Screenshots (if applicable)
<!-- Add screenshots or video recording to help explain your changes -->
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11802-update-robots-txt-to-point-to-sitemap-0-xml-3536d73d365081bb9545eb96dd1e8025)
by [Unito](https://www.unito.io)
## Summary
Extract a `.github/actions/changes-filter` composite action and adopt it
across path-gated CI workflows, fixing the docs-only PR stall and
removing duplicated `paths:` / `paths-ignore:` filtering across 8
workflows.
## Background
Docs-only PRs stalled on required status checks because workflows using
`paths-ignore: ['**/*.md']` never created a check run, while branch
protection still required it. Observed on #11776 (the `test` check from
`ci-tests-unit.yaml` never appeared).
The fix pattern: keep the workflow triggered, gate downstream jobs on a
`changes` job whose outputs are computed from a path filter. Skipped
jobs count as passing under branch protection.
## What the action emits
| Output | Meaning |
|---|---|
| `should-run` | Any file outside `apps/`, `docs/`, `.storybook/`,
`**/*.md` changed. |
| `app-website-changes` | Shared deps or `apps/website/**` changed. |
| `app-desktop-changes` | Shared deps or `apps/desktop-ui/**` changed. |
| `app-frontend-changes` | Shared deps or `src/**` changed. |
| `packages-changes` | Shared deps or `packages/**` changed. |
| `storybook-changes` | Shared deps or `.storybook/**` changed. |
| `docs-changes` | `docs/**` or any `**/*.md` changed (deps NOT folded
in). |
| `dependency-changes` | Root `package.json`, `pnpm-lock.yaml`, or
`pnpm-workspace.yaml` changed. |
Shared deps are folded into every `app-*`, `packages-changes`, and
`storybook-changes` output so a lockfile bump correctly invalidates each
granular gate. Outputs default to `'true'` for non-`pull_request` events
to avoid the silent-skip footgun on push / merge_group.
## Workflows migrated
| Workflow | Gate | Notes |
|---|---|---|
| `ci-tests-unit.yaml` | `should-run` | Required check (`test`). Fixes
the original stall. |
| `ci-tests-e2e.yaml` | `should-run` | Required check (`e2e-status`).
Replaces inline filter. |
| `ci-perf-report.yaml` | `should-run` | Removes `paths-ignore`. |
| `ci-website-build.yaml` | `app-website-changes \|\| packages-changes`
| Refactor — not a required check, but moves to job-level gating. Filter
scope broadens from `packages/{design-system}` to all `packages/**`
(strictly safer). |
| `ci-website-e2e.yaml` | `app-website-changes \|\| packages-changes` |
Same restructure; `post-starting-comment` also gated to avoid spurious
"tests are running" when E2E is skipped. |
| `ci-dist-telemetry-scan.yaml` | `should-run` | New gate; was
previously running on every PR including docs-only. |
| `ci-oss-assets-validation.yaml` | `should-run` | Same. |
| `ci-size-data.yaml` | `should-run` | Preserves existing repository
guard on the new `changes` job. |
| `ci-tests-storybook.yaml` | `storybook-changes \|\|
app-frontend-changes \|\| packages-changes` | Gates 4 of 6 jobs.
`deploy-production` (push to main) left ungated;
`update-comment-with-chromatic` cascades naturally. |
## Branch protection (verified)
Required status checks on `main` and `core/**`/`cloud/**`: `test`,
`lint-and-format`, `e2e-status`. Only `test` and `e2e-status` use the
composite — `lint-and-format` correctly stays unfiltered (must run on
docs/apps too). The other 6 migrations are refactor wins (less wasted CI
on docs/apps-only PRs), not stall fixes.
## Changes
- **What**: New `.github/actions/changes-filter` composite + 8 workflow
migrations to consume it.
- **Breaking**: None.
- **Dependencies**: New pin on `dorny/paths-filter@de90cc6` — already
covered by `ci-validate-action-pins`.
## Review Focus
- The `should-run` filter excludes `.storybook/**` (granular
`storybook-changes` covers it instead). Storybook's gate combines all
three: `storybook-changes || app-frontend-changes || packages-changes`.
- Two `dorny/paths-filter` steps inside the composite —
`predicate-quantifier=every` is required for the negated globs in
`should-run` but breaks the multi-pattern OR filters.
- The website filter scope intentionally broadens from
`packages/{design-system,tailwind-utils}/**` to all `packages/**` for
consistency and safety.
Fixes#11776
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11785-ci-extract-changes-filter-composite-action-fix-docs-only-PR-stall-3526d73d36508172a1d7fe8c30fa6453)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
ZIP export toast now reports the total number of files instead of the
number of selected jobs when any selected job has multiple outputs.
## Changes
- **What**: In `downloadMultipleAssetsAsZip`
(`src/platform/assets/composables/useMediaAssetActions.ts`), compute
`fileCount` by summing each asset's `outputCount` metadata (fallback 1)
and pass it to `mediaAsset.selection.exportStarted` instead of
`assets.length`. The existing i18n string already handles `file`/`files`
plural.
- **Tests**: 3 new unit tests in `useMediaAssetActions.test.ts` covering
multi-output, single-output fallback, and mixed selections. The
`useToast` and `useI18n` mocks were lifted to hoisted refs so toast call
args are assertable.
## Review Focus
- Reduce uses `count > 1 ? count : 1`, mirroring the
`hasMultiOutputJobs` gate above so a known `outputCount === 1` is still
counted as 1 file (no double-counting).
- Only `downloadMultipleAssetsAsZip` is touched; OSS individual-download
path and direct-download path are unchanged.
Fixes#11736
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11737-fix-report-total-file-count-not-job-count-in-ZIP-export-toast-3516d73d3650811ba78dfdb0a0ae8ea1)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Fixes two visual bugs in the Desktop app at small window sizes: the
search bar getting pushed/clipped in modal headers, and autocomplete
suggestion dropdowns being cut off by `overflow-hidden` ancestors.
## Changes
- **`SearchAutocomplete.vue`**: Wrap `ComboboxContent` in
`ComboboxPortal` so the suggestions dropdown teleports to `<body>`,
escaping `overflow-hidden` ancestors (fixes z-index clipping in Manager
dialog and other modals using `BaseModalLayout`)
- **`BaseModalLayout.vue`**: Replace `shrink-0` with `min-w-0` on the
header content container so the search bar can shrink at narrow window
sizes instead of overflowing and being clipped by the modal root's
`overflow-hidden`
- **`GraphCanvas.vue`**: Fix dead code where the native drag
(`app-drag`) div was nested inside a `v-if="workflowTabsPosition ===
'Topbar'"` block with its own mutually exclusive condition — move it
before the block and add `pointer-events-auto` so Desktop window
dragging works when tabs are in Sidebar position
## Why no E2E tests
- **`SearchAutocomplete` portal**: The fix is structural (teleport to
`<body>`). A meaningful regression test would require opening the
Manager dialog with a real or mocked extension list — that is a
substantial standalone effort tracked in #11714.
- **`BaseModalLayout` header shrink**: A viewport-resize assertion would
test CSS layout behaviour, not application logic; it would be fragile
and low-value.
- **`GraphCanvas` app-drag**: Desktop/Electron-only.
`-webkit-app-region: drag` cannot be exercised in headless Chromium.
Unit tests for `SearchAutocomplete` cover the new code paths (portal
rendering, suggestion display, item selection).
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Low risk UI-only changes: adjusts layout CSS and combobox rendering
via `ComboboxPortal`, plus adds unit tests; no business logic or data
flow changes.
>
> **Overview**
> Fixes small-window Desktop UI issues where modal-header search inputs
could be clipped and autocomplete dropdowns could be cut off by
`overflow-hidden` ancestors.
>
> `SearchAutocomplete` now renders its suggestions list inside a
`ComboboxPortal` (teleporting the popper content outside clipping
containers) and adds a focused unit test suite covering empty/non-empty
suggestions, selection behavior, and `optionLabel` handling.
>
> `BaseModalLayout` tweaks header flexbox constraints (`min-w-0` on the
header content container) to allow the search bar to shrink instead of
overflowing.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
fd32d960f9. Bugbot is set up for automated
code reviews on this repo. Configure
[here](https://www.cursor.com/dashboard/bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
---------
Co-authored-by: Benjamin Lu <benjaminlu1107@gmail.com>
## Summary
Reverts #10849 (per-instance promoted widget value storage) and its
companion test-pinning PR #11697. The fix in #10849 caused regressions
in promoted-widget serialization (notably the Z-Image-Turbo template,
see #10146 follow-up). A replacement fix is being developed on
`fix/subgraph-promoted-widget-inline-state` and will land separately.
## Changes
- **Revert #11697** — drops the `it.fails`-marked tests that pin the
#10849 corruption symptom. With #10849 reverted, those markers would
falsely flip to passing.
- **Revert #10849** — removes per-instance `_instanceWidgetValues` map,
`_pendingWidgetsValues` configure-time hydration, the `widgets_values`
write path in `SubgraphNode.serialize()`, the `sourceSerialize` field on
`PromotedWidgetView`, the multi-instance Vitest suite, and the
multi-instance E2E test + asset.
- **Conflict resolution** in
`browser_tests/tests/subgraph/subgraphSerialization.spec.ts`: kept the
restored test coverage from #11579 (which is post-#10849 on main) and
removed only the now-unreachable multi-instance test, its helper, and
its workflow constant. Auto-merge with #11698 (`incrementVersion`) and
#11699 (ID type aliases) was clean.
## Review Focus
- Confirm no other on-main code path has come to depend on
`PromotedWidgetView.sourceSerialize` or
`SubgraphNode._instanceWidgetValues` since #10849 (grep is clean
locally).
- Confirm we want to land this revert before the replacement fix on
`fix/subgraph-promoted-widget-inline-state` is ready — this leaves the
original #10146 (multi-instance widget value collision) unfixed in the
meantime.
- The retained #11579 test coverage now exercises pre-#10849 behavior;
some of those assertions were written expecting the #10849 code path. CI
will surface any that need adjustment.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11790-revert-roll-back-10849-11697-per-instance-promoted-widget-values-3536d73d3650814094abd58b6b712d8d)
by [Unito](https://www.unito.io)
## Summary
A follow-up PR of
https://github.com/Comfy-Org/ComfyUI_frontend/issues/11106.
This PR only focus on `layoutMutations.ts`.
`layoutMutations.ts` is the central API for all node layout mutations in
the Vue renderer. It previously had zero test coverage despite
containing non-trivial logic such as guard clauses, ID normalization,
and z-index scanning. This PR addresses issue #11106 to prevent silent
regressions in node positioning and lifecycle.
## What was tested and how
All tests use the real `layoutStore` singleton (no mocks).
`initializeFromLiteGraph` resets node state before each test, and
results are verified via `getNodeLayoutRef().value`.
| Method | Tests | Logic covered |
| :--- | :--- | :--- |
| `moveNode` | 3 | **Guard** (missing node -> no-op); position written
to store; numeric ID coerced to string |
| `resizeNode` | 2 | **Guard**; size written to store |
| `setNodeZIndex` | 2 | **Guard**; zIndex written to store |
| `createNode` | 1 | Node becomes readable with the provided position
and size |
| `deleteNode` | 2 | **Guard**; node removed from store |
| `batchMoveNodes` | 4 | Empty array -> no-op; multiple nodes updated
**atomically**; existing size preserved; missing nodes skipped while
valid ones still update |
| `bringNodeToFront` | 1 | Target node ends up with a **higher zIndex**
than all other nodes |
## What was not tested and why
| Method | Reason skipped |
| :--- | :--- |
| `createLink` / `deleteLink` | `layoutStore` exposes no public API to
query link existence by ID; methods contain no logic beyond a straight
`applyOperation` call. |
| `createReroute` / `deleteReroute` / `moveReroute` | Same reason as
above. |
| `setSource` / `setActor` | Single-line delegation to `layoutStore`; no
logic to test. |
| Default value behavior in `createNode` | Avoids "change-detector"
tests; asserting hardcoded defaults adds no regression value and would
block valid product changes. |
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Low Risk**
> Test-only changes; no production logic is modified. Main risk is
potential flakiness due to reliance on singleton store state across
tests.
>
> **Overview**
> Adds a new `layoutMutations.test.ts` Vitest suite that exercises
`useLayoutMutations` against the real `layoutStore` singleton
initialized from LiteGraph data.
>
> Tests cover no-op guard clauses for missing nodes, node ID
normalization, position/size/z-index updates, node create/delete
behavior, `batchMoveNodes` semantics (empty input, skipping missing
nodes, preserving size), and `bringNodeToFront` z-index promotion
relative to other nodes.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
bb92de0b5b. 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-11313-test-add-unit-tests-for-useLayoutMutations-3446d73d36508134a289d6147244e710)
by [Unito](https://www.unito.io)
## Summary
Fix part of the
#https://github.com/Comfy-Org/ComfyUI_frontend/issues/11092
A total of 4 `// @ts-expect-error` directives were removed across 3
files — all caused by PrimeVue's legacy `$el` access pattern (`const
inputElement = inputRef.value.$el`) — by replacing PrimeVue with
Reka-based UI components. 1 corresponding unit test file was added.
## Changes
### `src/components/ui/input/Input.vue`
- **Exposed APIs**: Extended `defineExpose` to include `blur()` and
`setSelectionRange()`. This allows parent components to programmatically
control input behavior without direct DOM manipulation.
### `src/components/ui/textarea/Textarea.vue`
- **Exposed APIs**: Added `focus()` via `defineExpose`.
- **Cleanup**: Removed redundant attribute spreading (`...restAttrs`) to
lean on Vue’s default `$attrs` inheritance, making the component more
predictable.
---
## Refactored Feature Components
### `WidgetMarkdown.vue` (Note/Markdown Widgets)
- **Dependency Swap**: Replaced `primevue/textarea` with local
`Textarea.vue`.
- **Logic Simplification**: Simplified focus logic from
`textareaRef.value?.$el?.focus()` to a typed
`textareaRef.value?.focus()`.
- **Code Style**: Converted arrow functions to function declarations and
removed redundant section comments.
### `PromptDialogContent.vue` (Generic Prompt Dialogs)
- **Component Update**: Replaced PrimeVue `FloatLabel` and `InputText`
with a native `<label>` and local `Input.vue`.
- **Vue 3.5 Adoption**: Implemented **Reactive Destructuring** for
props.
- **Conflict Resolution**: Renamed internal `onConfirm` handler to
`handleConfirm` to prevent collision with destructured props.
### `EditableText.vue` (Node Titles & Sidebar Items)
- **Style Modernization**: Removed `<style scoped>` block in favor of
**Tailwind CSS** utility classes (e.g., `inline`, `w-full`).
- **Clean Implementation**: Replaced PrimeVue PassThrough (`:pt`) logic
with standard `@blur` and `v-bind` attributes.
---
## Testing & Quality Assurance
### Updated Tests
- **Redundancy Removal**: Cleaned up `EditableText.test.ts` and
`WidgetMarkdown.test.ts` by removing unused PrimeVue global
registrations. All 34 existing behavioral tests remain passing.
### New Coverage
- **`PromptDialogContent.test.ts`**: Added 3 new tests to verify:
1. Correct initialization with `defaultValue`.
2. Value persistence when clicking the Confirm button.
3. Form submission via the `Enter` key.
---
## Manual Test Screenshot
All functions have passed testing.
<img width="594" height="530" alt="test5"
src="https://github.com/user-attachments/assets/46a6b3b2-1855-414e-ac78-65668052ce50"
/>
<img width="1190" height="1074" alt="test4"
src="https://github.com/user-attachments/assets/89aa61ab-9401-44c2-9eae-9ca8761df675"
/>
<img width="1154" height="1028" alt="test3"
src="https://github.com/user-attachments/assets/3f63cfdf-8fbd-4dd3-9e42-dbebe4d8d421"
/>
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Moderate risk because it swaps underlying input/textarea components
and ref handling (focus/blur/selection) in interactive UI paths
(editable labels, prompt dialogs, markdown editor), which could subtly
change keyboard/blur behavior.
>
> **Overview**
> Refactors several Vue components to stop using PrimeVue
`InputText`/`Textarea` (and `$el` access) in favor of the project’s
`Input`/`Textarea` components, updating bindings/events and Tailwind
classes accordingly.
>
> Extends the shared `Input` to expose `blur`, `setSelectionRange`, and
`selectAll`, and updates `Textarea` to expose `focus`, enabling callers
to manage focus/selection without DOM internals.
>
> Adds a new unit test suite for `PromptDialogContent` and simplifies
existing tests by removing PrimeVue plugin/component setup; the groups
e2e test replaces a screenshot assertion with a functional visibility
check for the new title input.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
9c97314d59. 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-11324-refactor-replace-PrimeVue-InputText-Textarea-with-project-UI-components-3456d73d36508109a18bc97a7d0487a7)
by [Unito](https://www.unito.io)
## Summary
Replace `vi.mock('vue-i18n')` stub with a real `createI18n` plugin
instance in `useReconnectingNotification` tests.
## Changes
- **What**: Add `setupComposable()` helper that renders a wrapper
component via `@testing-library/vue` with a real `createI18n` plugin.
Assertions now check translated values
(`'Reconnecting'`/`'Reconnected'`) instead of raw i18n keys. Removes the
brittle `vi.mock('vue-i18n')` stub.
## Review Focus
Straightforward test-only change — the composable requires a component
setup context for `useI18n()`, so we render a thin wrapper via
`@testing-library/vue` with the i18n plugin installed.
Fixes#11153
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11386-test-use-real-vue-i18n-plugin-in-useReconnectingNotification-tests-3476d73d3650814ba70eea6df91c8bbe)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Extracts the missing-model pipeline orchestration out of `ComfyApp` and
into an app-independent platform module, while tightening the
workflow-flattening type boundary that refresh needs when rescanning the
live LiteGraph graph.
This PR is intentionally refactor-heavy. It is the follow-up to the
earlier missing-model refresh work: instead of keeping refresh-specific
candidate recheck logic beside the UI, this change makes the refresh
path reuse the existing missing-model pipeline and removes the direct
dependency on private `ComfyApp` pipeline methods.
Linear: FE-499
Issues covered by this PR:
- Fixes#11678
- Fixes#11680
- Partially addresses #11679 by removing the missing-model refresh
path's unsafe `graph.serialize() as unknown as ComfyWorkflowJSON` cast
and replacing it with the narrower flattenable workflow contract.
Broader workflow serialization/type-boundary cleanup outside this
missing-model refresh path remains deferred.
## Changes
- **What**:
- Added `src/platform/missingModel/missingModelPipeline.ts` as the
orchestration module for missing-model detection/verification.
- `runMissingModelPipeline(...)` now owns the pipeline previously
embedded in `ComfyApp`:
- candidate scan and enrichment
- active ancestor filtering for muted/bypassed subgraph containers
- pending warning cache updates
- OSS folder path and file-size follow-up work
- cloud asset verification follow-up work
- surfaced missing-model errors via the existing execution error store
- `refreshMissingModelPipeline(...)` handles the refresh-specific flow:
- calls the injected `reloadNodeDefs()` first
- serializes the current live graph
- preserves model metadata by preferring active workflow `models`, then
falling back to current missing-model candidate metadata
- delegates back into the same pipeline used during workflow load
- Kept `ComfyApp` as the compatibility caller instead of the owner of
the pipeline.
- `loadGraphData(...)` now calls `runMissingModelPipeline(...)` with
`graph`, `graphData`, `missingNodeTypes`, and `silent` options.
- `refreshMissingModels(...)` is now a thin wrapper around
`refreshMissingModelPipeline(...)` and keeps the existing default
`silent: true` refresh behavior.
- The new pipeline module does not import `@/scripts/app`; app-owned
data/actions are passed in as inputs.
- Moved the workflow node-flattening helpers out of `workflowSchema.ts`
and into `src/platform/workflow/core/utils/workflowFlattening.ts`.
- This includes `flattenWorkflowNodes`, `buildSubgraphExecutionPaths`,
and `isSubgraphDefinition`.
- The move is intentional: these helpers are not zod schema definitions
or workflow validation logic. They are core workflow traversal utilities
used to flatten root workflow nodes plus nested subgraph definition
nodes into the execution-shaped node list needed by missing-model
scanning.
- The refresh path receives data from `LGraph.serialize()`, whose return
type is serialized LiteGraph data rather than validated
`ComfyWorkflowJSON`. Previously this forced unsafe typing like
`graph.serialize() as unknown as ComfyWorkflowJSON`.
- The new `FlattenableWorkflowGraph` / `FlattenableWorkflowNode`
structural contract describes only what flattening actually needs:
`nodes`, `definitions.subgraphs`, node `id`, `type`, `mode`,
`widgets_values`, and `properties`.
- This lets both normal workflow-load data (`ComfyWorkflowJSON`) and
refresh-time live graph serialization (`LGraph.serialize()`) flow into
the same scan/enrichment path without pretending serialized LiteGraph
output is a fully validated workflow schema document.
- Updated `missingModelScan.ts` to consume that minimal flattenable
workflow shape via `MissingModelWorkflowData`.
- `MissingModelWorkflowData` extends the flattenable workflow contract
with optional workflow-level `models` metadata.
- Removed now-unnecessary casts around execution IDs, flattened nodes,
and `widgets_values` object access.
- Updated `getSelectedModelsMetadata(...)` to accept readonly widget
value arrays so flattened workflow data can stay read-only.
- Reduced the exported surface of the new pipeline module after `knip`
flagged unused exported internal option/store interfaces.
- Kept `workflowSchema.ts` focused on validation schemas. The flattening
helpers are not re-exported from the schema module because they are
internal workflow core utilities, not public schema API.
- **Breaking**: None intended.
- Internal imports were updated to the new core utility path.
- This repo is not exposing these flattening helpers as a public package
API, so the old schema-local helper location is treated as an internal
implementation detail.
- **Dependencies**: None.
## Review Focus
- **Pipeline extraction / dependency direction**:
- Please verify that `missingModelPipeline.ts` stays independent from
`@/scripts/app`.
- `ComfyApp` should remain the caller/adapter, not the owner of
missing-model pipeline orchestration.
- **Workflow flattening type boundary**:
- The main type-cleanup goal is removing the refresh-time
`graph.serialize() as unknown as ComfyWorkflowJSON` lie.
- `LGraph.serialize()` and validated workflow JSON are not the same
contract. The new flattenable workflow contract is deliberately smaller
and structural because the missing-model enrichment path only needs
enough data to flatten nodes and read embedded model metadata.
- This is why the flattening helpers moved from `workflowSchema.ts` to
`workflow/core/utils`: the logic is reusable workflow traversal, not
validation schema.
- **Behavior preservation**:
- The PR is intended to preserve existing user-facing missing-model
behavior while moving ownership out of `app.ts`.
- Existing async follow-up behavior remains intentionally
fire-and-forget:
- cloud asset verification still surfaces after verification completes
- OSS folder paths still update asynchronously before surfacing
confirmed missing models
- file-size metadata fetching remains asynchronous
- More invasive behavior changes, such as adding non-cloud post-fetch
`isMissingCandidateActive(...)` re-verification or redesigning the
fire-and-forget result contract, are intentionally left for follow-up
work because they are not pure extraction.
- **Downloadable model metadata**:
- `missingModels` returned for download metadata now requires both `url`
and `directory`.
- Candidates without a directory still remain in `confirmedCandidates`,
but they are not exposed as downloadable model metadata. This keeps the
returned downloadable list aligned with what the download flow can
actually use.
- **Test ownership**:
- Complex missing-model pipeline behavior tests moved out of
`src/scripts/app.test.ts` and into
`src/platform/missingModel/missingModelPipeline.test.ts`.
- `app.test.ts` now only covers thin delegation for
`app.refreshMissingModels(...)`.
- Workflow flattening tests moved with the helper from schema tests into
`src/platform/workflow/core/utils/workflowFlattening.test.ts`.
- **Deferred follow-ups**:
- Broader function decomposition for cognitive complexity.
- Wider dependency-injection/port cleanup for stores and services beyond
the app boundary.
- Cloud-specific pipeline unit tests, which need a separate `isCloud`
mocking strategy.
- Additional E2E coverage expansion beyond the existing OSS refresh
path.
- More general workflow serialization/type-boundary cleanup outside the
missing-model refresh path.
## Validation
- `pnpm format`
- `pnpm lint`
- Passed. Existing lint output included a pre-existing
`no-misused-spread` warning and icon-name logs, but the command exited
successfully.
- `pnpm typecheck`
- `pnpm test:unit`
- `714 passed`, `9514 passed | 8 skipped`
- Pre-push `pnpm knip`
- Passed after reducing the exported surface of the new pipeline module.
## Screenshots (if applicable)
Not applicable. This PR is a pipeline/type-boundary refactor with no UI
changes.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11751-refactor-extract-missing-model-refresh-pipeline-3516d73d3650816d9245d4b1324b71c9)
by [Unito](https://www.unito.io)
---------
Co-authored-by: DrJKL <DrJKL0424@gmail.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
*PR Created by the Glary-Bot Agent*
---
## Summary
Adds new `plum` and `ink` color scales for Comfy Hub branding and
standardizes existing tokens to align with current Figma design system.
### Changes
**Phase 1 — New primitives** (`_palette.css`)
- Added `plum-300/400/500/600` and `ink-100` through `ink-900`
**Phase 2 — Token cleanup** (`style.css`)
- Removed deprecated `slate-100/200/300` primitives (cool blue-grey,
removed from Figma)
- Removed duplicate `graphite-400` (identical hex to slate-100)
- Dark mode: migrated 6 slate/graphite references to muted-foreground,
smoke-700, smoke-800, charcoal-200
- Light mode: replaced 3 `ash-500` references with `smoke-800` per
designer alignment
### Token migration detail
| Dark mode token | Old value | New value | Rationale |
|---|---|---|---|
| `--node-component-header-icon` | slate-300 (#5b5e7d) |
muted-foreground (smoke-800) | Figma `node/foreground-secondary` |
| `--node-component-slot-text` | slate-200 (#9fa2bd) | smoke-700
(#a0a0a0) | Lighter neutral for text contrast |
| `--node-component-surface-highlight` | slate-100 (#9c9eab) | smoke-800
(#8a8a8a) | Neutral grey highlight |
| `--node-component-tooltip-border` | slate-300 (#5b5e7d) | charcoal-200
(#494a50) | Consistent with dark border tokens |
| `--text-secondary` | slate-100 (#9c9eab) | smoke-700 (#a0a0a0) |
Adequate contrast on dark surfaces |
| `--widget-background-highlighted` | graphite-400 (#9c9eab) | smoke-800
(#8a8a8a) | Removed duplicate, neutral replacement |
### Visual note
These changes shift some dark mode colors from cool blue-grey to neutral
grey. This is intentional per the design team. The
`--node-component-surface-highlight` and
`--node-component-tooltip-border` tokens should be QA'd as the designer
noted.
### Not included (Phase 3)
Hub Dark overlay theme will ship separately once the Hub UI work is
ready to validate against.
## Screenshots


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11139-feat-add-plum-ink-color-primitives-and-standardize-design-tokens-33e6d73d365081418e13e0efe6161fb5)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Glary-Bot <glary-bot@users.noreply.github.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Adds `.claude/skills/reviewing-unit-tests/SKILL.md`, a project-scoped
agent skill that gives reviewing agents a concrete rubric for evaluating
Vitest unit-test diffs.
## Changes
- **What**: New skill at `.claude/skills/reviewing-unit-tests/SKILL.md`.
Mirrors the structural template of the peer skill
`.claude/skills/writing-playwright-tests/SKILL.md` (frontmatter shape,
table-driven sections, terse prescriptive prose). Description triggers
on PR review of `*.test.ts` files. Codifies: alias-by-renaming
detection, `vi.mocked()` scope rule, unnecessary-cast detection,
divergence checks against `docs/testing/unit-testing.md` /
`store-testing.md` / `component-testing.md` / `vitest-patterns.md`,
redundant `mockClear()` flag, `vue-i18n`-must-not-be-mocked rule (with
`testI18n` reference), bugfix regression-validity check, leading-close
framing flag, testing-library requirement for new component tests.
## Review Focus
- **Retrospective validation against PR #11737** (head SHA
`4573d62450fd12ac6f06e5e491f8af84ccbd27de`, file
`src/platform/assets/composables/useMediaAssetActions.test.ts`).
Mentally running the skill's checklist against that diff flags:
1. ✅ `getToastAddMock` / `getI18nTMock` module-level helpers — caught by
the "Renaming ≠ Restructuring" section + Mocking Smells row
"Module-level helper functions wrapping mocked composable returns".
2. ✅ `as ReturnType<typeof vi.fn>` casts in those helpers — caught by
the "`vi.mocked()` Scope" section + Mocking Smells row on stray casts.
3. ✅ `vi.mock('vue-i18n', ...)` — caught by Mocking Smells row directing
to mount real `createI18n` per `docs/testing/vitest-patterns.md` and the
shared `testI18n` in
`src/components/searchbox/v2/__test__/testUtils.ts`.
4. ✅ `vi.mock('primevue/usetoast', ...)` — explicitly **not** flagged.
The "Distinguish" paragraph after the Mocking Smells table marks
trivially-shaped third-party hooks as acceptable to mock; "Don't Mock
What You Don't Own" applies to behavior-rich APIs, not single-method
composables.
- **Reference style**: links to `docs/testing/*.md` and
`docs/guidance/typescript.md` instead of restating their content, per
the FE-511 acceptance criterion.
- **Open question**: scope. The repo-local skill is the FE-511
deliverable; equivalent prescriptive rules already live in the user's
global `~/.config/amp/AGENTS.md` "Code Review Rigor" section as a
separate artifact. No duplication intended — the skill is the
project-scoped surface a reviewing agent loads when entering this repo.
Closes FE-511
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11777-docs-add-reviewing-unit-tests-skill-3526d73d365081848759e6a8fab942f0)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Document that `vue-i18n` should not be mocked in tests — mount with a
real `createI18n` plugin instance instead.
## Changes
- **What**: Expanded `docs/testing/vitest-patterns.md` "Don't Mock
`vue-i18n`" section with a concrete code example (covering both
component and composable tests), guidance for asserting on translation
keys with empty messages, and a real-example link to
`src/components/searchbox/v2/__test__/testUtils.ts`. Added a callout at
the top of `docs/testing/unit-testing.md` "Mocking Composables with
Reactive State" cross-linking the new section, since that section
applies to *owned* composables.
## Review Focus
- The previous `vitest-patterns.md` paragraph pointed at a non-existent
`SearchBox.test.ts`; the new link points to the actual shared `testI18n`
helper.
- The "Mocking Composables with Reactive State" pattern should not be
applied to third-party composables like `useI18n` — the callout makes
that explicit.
Surfaced during review of #11737, where the test file mocked `vue-i18n`
and then accumulated structural workarounds (hoisted aliases, helper
functions, type casts) to interact with the mocked `t`. A real
`createI18n` instance avoids the entire ceremony.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11776-docs-prefer-real-createI18n-over-mocking-vue-i18n-in-tests-3526d73d365081d4bc39fbf3c2502e49)
by [Unito](https://www.unito.io)
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Adds PostHog page analytics to the marketing website (`apps/website/`).
## Changes
- **What**: New `posthog.ts` script with
`initPostHog`/`capturePageview`. Wired into `BaseLayout.astro` behind
`import.meta.env.PROD` (mirroring the GTM gate). Pageviews are captured
on every `astro:page-load` so ClientRouter view-transition navigations
are tracked, not just hard reloads.
- **Dependencies**: `posthog-js` (already in the workspace catalog at
`^1.358.1`; previously used by the workbench telemetry provider).
## Review Focus
- API host is set to `https://t.comfy.org` to match
`src/platform/telemetry/providers/cloud/PostHogTelemetryProvider.ts` —
confirm that proxy is OK to share with the website surface.
- Project token is hard-coded inline. It is a public `phc_…` frontend
token (designed to ship to clients); this matches the pattern used for
`gtmId` in the same file. Happy to switch to a `PUBLIC_POSTHOG_KEY` env
var if preferred.
- `person_profiles: 'identified_only'` to avoid creating profiles for
every anonymous visitor — flag if you want full anonymous tracking
instead.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11735-feat-website-add-PostHog-analytics-3516d73d3650811189c6d64c3af4ded9)
by [Unito](https://www.unito.io)
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:
1.**Discover** — Collect candidates from Slack bot + git log gap, then **reconcile both lists** (`reference/discovery.md`)
2.**Pre-filter by path** — Auto-skip PRs whose changed files are entirely under `apps/website/`, `browser_tests/`, `.github/`, `packages/design-system/`, `packages/{cloud,registry}-types/`, `.claude/`, `docs/`. Don't read PR bodies for these — they don't ship to core ComfyUI users (`reference/analysis.md`)
3.**Verify target file existence** — For each surviving candidate, run `git cat-file -e origin/$TARGET:$path` for primary changed files. If they don't exist on the target, auto-mark SKIP with reason `feature-not-on-branch`
6.**Human Review** — Present candidates in batches for interactive approval, with tier context attached (see Interactive Approval Flow)
7.**Plan** — Order by dependency (leaf fixes first), group into waves per branch
8.**Test-then-resolve dry-run** — Classify clean vs conflict before committing time (`reference/execution.md`)
9.**Execute** — Label-driven automation for clean PRs → worktree fallback for conflicts (`reference/execution.md`)
10.**Public-API conflict review** — If conflict resolution touches a public LiteGraph callback, extension API, or `node.*` method, consult oracle for compat-regression review BEFORE pushing (`reference/execution.md`)
11.**Verify** — Per-PR validation (typecheck + targeted tests + lint on changed files) AND per-wave verification (full typecheck + test:unit on branch HEAD)
@@ -107,6 +112,35 @@ Husky hooks fail in worktrees (can't find lint-staged config). Always use `git p
In the 2026-04-06 session: core/1.42 got 18/26 auto-PRs, cloud/1.42 got only 1/25. The cloud branch has more divergence. **Always plan for manual fallback** — don't assume automation will handle most PRs.
### Cherry-Picked Tests Can Reference Files Added By Earlier Unbackported PRs
A common conflict: PR A on main modifies a test file that was _added_ on main by an earlier PR B (not backported to the target). The cherry-pick of A reports "modify/delete" on B's test file because the file doesn't exist on the target. Adding the new file would smuggle in B's test scaffolding without B's runtime changes.
**Detection:** Conflict says `deleted in HEAD and modified in <PR>`. Verify with:
If the introducing commit is **not** on the target branch, the test file isn't a real prerequisite for the runtime fix.
**Fix:**`git rm` the test file (drop it from the backport). Document in the commit body which PR introduced it on main and why dropping it is safe. The runtime fix itself usually doesn't depend on these tests — coverage exists at the integration layer.
### Backport-Only Compatibility Shims
When a PR's _mechanism_ relies on changes upstream that aren't on the older branch, a literal cherry-pick can recreate the original bug for any consumer still using the old contract. This is most dangerous for **public LiteGraph callbacks, extension APIs, and `node.*` methods** that custom-node packages depend on.
**Real example (#11541, core/1.43 backport):** The PR removed `LGraphNode.vue`'s legacy `handled === true` sync-return check from `handleDrop`, replacing it with `await node.onDragDrop(event, true)`. Safe on `main` because all in-repo `onDragDrop` handlers had migrated to participate in the new `claimEvent` flag. On `core/1.43`, `onDragDrop` is a public callback — custom-node packages with synchronous `onDragDrop` returning `true` would no longer have their event claimed, recreating the duplicate-node-creation bug the PR was fixing.
**Detection:** The PR's diff modifies a file that is part of a public extension API surface. Look for:
-`node.onXxx` callback assignments
- Methods on `LGraphNode`, `LGraphCanvas`, `LGraph`, `Subgraph`
- Public exports from `src/lib/litegraph/`
- Type changes affecting `litegraph-augmentation.d.ts`
**Fix:** Add a backport-only compatibility shim that preserves the old contract while keeping the new fix. Document it explicitly in the commit body under a `## Backport-only compatibility fix` heading. Consult oracle for review before pushing — a bad shim is worse than no fix.
## Conflict Triage
**Always categorize before deciding to skip. High conflict count ≠ hard conflicts.**
@@ -147,6 +181,26 @@ Skip these without discussion:
- **Features not on target branch** — e.g., Painter, GLSLShader, appModeStore on core/1.40
- **Cloud-only PRs on core/\* branches** — Team workspaces, cloud queue, cloud-only login. (Note: app mode and Firebase auth are NOT cloud-only — see Branch Scope Rules)
### Path Pre-Filter (run BEFORE reading PR bodies)
For 50+ candidate PRs, classify by changed paths first to skip the unproductive ones without spending time on triage. Run `git show --stat $SHA` (or `gh pr view --json files`) and bucket:
| `src/` (core editor) | KEEP — analyze further | Runtime/editor code that requires full triage |
A PR touches multiple paths? Keep it if **any** changed file is under `src/` (or other core paths) and run normal analysis. Auto-skip is conservative — only skip when _all_ paths match the SKIP buckets.
This filter alone removes ~30-50% of candidates in a typical session, leaving only the PRs that need real triage.
## Wave Verification
After merging each wave of PRs to a target branch, verify branch integrity before proceeding:
@@ -39,6 +39,89 @@ Check before backporting — these don't exist on older branches:
- **App builder** — check per branch
- **appModeStore.ts** — not on core/1.40
### Verify Target File Existence (Run Before Cherry-Pick)
Before cherry-picking any PR, confirm the files it modifies actually exist on the target branch. If they don't, the PR's runtime fix is for a feature that hasn't been added yet — skip cleanly without attempting cherry-pick:
```bash
# For each file the PR changes
for f in $(gh pr view $PR --json files --jq '.files[].path'| grep -v "^browser_tests/\|\.test\.");do
if ! git cat-file -e origin/$TARGET:$f 2>/dev/null;then
echo"MISSING on $TARGET: $f"
fi
done
```
If the _primary_ changed files (the runtime ones, not tests) are missing, mark the PR `SKIP / feature-not-on-branch`. This is faster than letting cherry-pick fail with modify/delete conflicts and gives a clean signal.
This check is the first thing that runs after the path pre-filter and BEFORE you spend time reading PR descriptions.
## Tiered Triage (Recommended for 30+ Candidates)
Before the interactive Y/N approval flow, bucket all surviving candidates into three tiers. This surfaces release-engineering decisions that a flat MUST/SHOULD list obscures:
### Tier 1 — Core Editor Must-Haves
User-facing bugs, crashes, data corruption, or security issues in code paths that exist on the target branch. These are the strongest backport candidates.
Indicators:
-`fix:` prefix and the bug is reproducible on the target branch
Bugs that only manifest on cloud-hosted distributions (Secrets panel, subscription flows, cloud signup, workspace tracking, etc.). Whether to backport depends on whether cloud ships from the target `core/*` branch in your release matrix.
Indicators:
- Files under `src/platform/secrets/`, `src/platform/subscription/`, signup flows
- PR description mentions cloud staging issues
- Fix gated behind cloud feature flags
Default: ask the cloud release rotation owner. If unsure, defer.
### Tier 3 — Skip
Path pre-filter caught most of these. The rest are PRs where the diff _touches_`src/` but the practical impact is non-user-facing or scoped to features the target doesn't ship.
Indicators:
- All changes in test files even if the PR touched `src/` test files
- Storybook stories only
- Lint config / lint rule additions
- Documentation comments
- Internal refactors with no behavior change
### Presentation Format
When showing tier results to the user, format as:
```text
Tier 1 (N PRs) — strong backport candidates
- #11541 fix: stop duplicate node creation when dropping image on Vue nodes
Why: Vue Nodes 2.0 regression — async onDragDrop bypassed handled-check, drops bubble to document, spawns extra LoadImage nodes
- #10849 fix: store promoted widget values per SubgraphNode instance
Why: Multiple instances overwriting each other's promoted widget values — data loss
Tier 2 (N PRs) — cloud-distribution release rotation should decide
- #11636 fix: enable Chrome password autofill on signup form
- ...
Tier 3 (N PRs) — skip recommended
- #11586 fix: website polish (apps/website/ only)
- ...
```
Then run interactive Y/N over Tier 1 and Tier 2; Tier 3 gets confirmed-skip without per-PR review.
## Dep Refresh PRs
Always SKIP on stable branches. Risk of transitive dependency regressions outweighs audit cleanup benefit. If a specific CVE fix is needed, cherry-pick that individual fix instead.
Before triggering label-driven automation, run a dry-run cherry-pick loop to classify candidates. This is much faster than discovering conflicts after-the-fact across automation, manual cherry-picks, and CI failures.
16.**Use `--no-verify` in worktrees** — husky hooks fail in `/tmp/` worktrees. Always push/commit with `--no-verify`.
17.**Automation success varies by branch** — core/1.42 got 18/26 auto-PRs (69%), cloud/1.42 got 1/25 (4%). Cloud branches diverge more. Plan for manual fallback.
18.**Test-then-resolve pattern** — for branches with low automation success, run a dry-run loop to classify clean vs conflict PRs before processing. This is much faster than resolving conflicts serially.
19.**Public-API conflict resolutions need oracle review** — when a conflict touches `node.onXxx` callbacks, `LGraphNode`/`LGraphCanvas`/`LGraph`/`Subgraph` methods, or types in `litegraph-augmentation.d.ts`, consult oracle BEFORE pushing. Custom-node packages depend on these contracts. A literal cherry-pick of a refactor-style fix can silently break extensions still using the old contract — sometimes recreating the very bug the PR was fixing. Document any backport-only compatibility shim explicitly in the commit body.
20.**Cherry-picked tests can require unbackported test scaffolding** — when a PR modifies a test file that was _added_ on main by an earlier unbackported PR, the cherry-pick reports modify/delete on that file. Drop it from the backport (`git rm`) and document which PR introduced it. Don't smuggle in test infrastructure without its runtime prerequisites.
21.**Per-PR validation catches issues earlier than wave verification** — for high-stakes branches, run `pnpm typecheck && pnpm exec eslint <changed files> && pnpm exec oxfmt --check` per PR before pushing. Wave verification still matters (it catches cross-PR interactions), but per-PR makes attribution trivial when something fails.
## CI Failure Triage
@@ -268,3 +341,40 @@ Common failure categories:
| Type error | Interface changed on main but not branch | May need manual adaptation |
**Never assume a failure is safe to skip.** Present all failures to the user with analysis.
## PR Body Template (Manual Cherry-Picks)
Manual cherry-pick PRs need detail beyond the automation's terse default. Use this template — reviewers will look here before re-deriving conflict-resolution logic from the diff.
```markdown
Manual backport of #ORIG to `TARGET` for inclusion in `vX.Y.Z`.
Cherry-picked from upstream merge commit `SHORT_SHA`.
## Why
[1-2 sentences from the original PR's "Summary" — what bug, what fix mechanism]
## Conflict resolution
- **`path/to/file`** — [what conflicted on this branch] → [resolution chosen + why]
- **`path/to/dropped-test.test.ts`** — added on main by unrelated PR #XXXX (not backported). Dropped from this backport; runtime fix intact.
[If you added a shim that wasn't in the upstream PR, document it here — what extension surface, what contract, what the shim preserves, why the upstream version would have regressed it]
## Validation
-`pnpm typecheck` ✅
-`pnpm test:unit -- run <targeted suites>` ✅ (N/N passing)
-`pnpm exec eslint <changed files>` ✅ (0 errors)
-`pnpm exec oxfmt --check` ✅ (clean)
[If manual e2e was skipped, explain why — e.g., requires live backend, headless not feasible. State that source is byte-identical to upstream + how long it's been baking on main.]
Original PR: #ORIG / Original commit: `FULL_SHA`
```
The conflict-resolution section is non-negotiable — every conflict you resolved by hand needs a one-liner. This makes archaeology trivial six months later when someone asks "why does this look slightly different from main?"
description: 'Syncs the #bug-dump Slack channel into Linear as the system of record AND auto-fixes verified real bugs via red-green-fix. Every Linear operation (create, search, link, label) is performed by posting an @Linear mention in the bug-dump thread — no Linear MCP, no API key. Flow: fetch → mandatory dedupe gate (@Linear search + gh PR search) → false-defect verification → post @Linear create in thread (tool call) → parse bot card for FE-NNNN + URL → post :white_check_mark: confirmation reply → if candidate is a verified real bug with no dedupe hit and no open PR, invoke red-green-fix automatically to produce failing test + fix + PR. Respects team emoji scheme (:white_check_mark: ticket created, :pr-open: PR open, :question: needs context, :repeat: duplicate). Use when asked to sync #bug-dump to Linear, triage slack bugs, run a bug-dump sweep, or ingest bug reports. Triggers on: bug-dump, sync bug-dump, ingest bugs, triage slack bugs, bug sweep.'
---
# Bug Dump Ingest
**Primary job: sync `#bug-dump` (Slack: `C0A4XMHANP3`) into Linear as the source of truth, then auto-fix the verified real bugs.** Linear is where status, labels, and follow-up triage happen — this skill gets every bug into Linear with enough context that a downstream agent or human can work from Linear alone. **Every Linear action is performed by mentioning `@Linear` in the bug-dump thread**; there is no Linear MCP and no API key path. When pre-flight verification confirms a candidate is a real bug (not dedupe, not already in a PR, not out of scope), the skill then invokes `red-green-fix` automatically.
→ POST "@Linear create ..." thread reply via slack_send_message (mandatory tool call)
→ poll slack_read_thread → parse Linear bot card for FE-NNNN + URL
→ POST :white_check_mark: confirmation thread reply via slack_send_message
→ if verification = "real bug" AND no dedupe AND no open PR:
invoke Skill(skill="red-green-fix") → POST :pr-open: thread reply
```
### Non-negotiable rules
1.**Linear actions are Slack tool calls.** The skill MUST drive Linear by calling `mcp__plugin_slack_slack__slack_send_message` with `thread_ts` set and text that mentions `@Linear`. There is no MCP-direct path and no API-key path. Printing `@Linear create ...` into the Claude CLI response is NOT a substitute — the Slack thread reply is what triggers the Linear bot, and its card is the canonical receipt.
2.**Dedupe is a gate, not a suggestion.** No candidate is proposed for creation until `@Linear search` AND `gh pr` search have been run and recorded. A hit short-circuits creation to `L` (link) or `pr-open`.
3.**Auto-fix real bugs.** When the dedupe gate is clean AND false-defect verification is clean AND the candidate isn't on the handoff-exclusion list (see § Handoff conditions), after Linear creation the skill invokes `red-green-fix` via the `Skill` tool — without waiting for an extra human prompt.
### What the skill cannot do
The Slack MCP exposes no `reactions.add` tool, so the skill cannot put a `:white_check_mark:` reaction on the parent message. The thread reply with the leading `:white_check_mark:` emoji is the skill's canonical marker; a human can additionally add the parent reaction for channel visibility (see § Parent reaction — optional visibility nudge). Both are respected by Processed Detection.
## Team emoji scheme
| Emoji | Meaning | Who adds it | Skill behavior |
| `:white_check_mark:` | Ticket created | Human on parent (after skill files); also in bot reply | Skip in future sweeps |
| `:pr-open:` | PR open | Human | Skip creation; include PR link in approval row |
| `:question:` | Needs more context | Human | Skip creation; agent may ask for clarification |
| `:repeat:` | Duplicate | Human | Skip creation; link existing Linear issue |
## Design Priority
Optimize for **coverage, label quality, and proven fixes** over fix-path cleverness. Linear is the downstream triage surface — once every bug is there with status, labels, and context, agents and humans can work from Linear alone. A Linear ticket with a wrong severity is cheap to fix; a Slack-only bug is invisible to downstream tooling; a "filed but not fixed" real regression wastes a human turn that the skill could have spent on a red-green PR.
## Quick Start
1.**Scope** — default window: messages in the last 48h. Override with `--since YYYY-MM-DD` or a Slack permalink list.
2.**Fetch** — `slack_read_channel` for `C0A4XMHANP3`; `slack_read_thread` per message with replies.
3.**Filter** — drop already-processed (see Processed Detection).
4.**Classify** — bug / discussion / meta (see Classification Rules).
5.**Pre-flight dedupe gate (MANDATORY)** — for every bug candidate, run `@Linear search` AND `gh pr` search BEFORE proposing (see § Pre-flight Dedupe Gate). A hit means the candidate goes into the batch as `L` (link) or `pr-open`, not as a new create.
6.**Verify false defects** — per candidate, run quick checks before proposing (see False-Defect Verification).
7.**Extract** — normalize to ticket schema (see Ticket Schema).
8.**Human approval** — batch table, collect Y/N/?/S/L/R per candidate (see Interactive Approval). Default recommendation for clean candidates is `Y` (file + auto-fix).
9.**Post `@Linear create` thread reply — MANDATORY TOOL CALL** — for each approved `Y`/`L` row, call `mcp__plugin_slack_slack__slack_send_message` with `channel_id=C0A4XMHANP3`, `thread_ts=<parent-ts>`, and text starting with `@Linear create` (see § Linear Slack Bot Integration). Do NOT print the command into chat as a substitute.
10.**Capture the Linear bot card** — poll `slack_read_thread` up to 3× with ~3s spacing, parse the first Linear-app reply for the `FE-NNNN` identifier and `https://linear.app/...` URL. No URL = not ingested; never fabricate one.
11.**Post `:white_check_mark:` confirmation reply — MANDATORY TOOL CALL** — call `slack_send_message` again with text starting with `:white_check_mark: Filed to Linear: <URL>` so future sweeps can detect the marker via `has::white_check_mark: from:me`. Record both `ts` values in the session log.
12.**Auto-fix (clean candidates only)** — if dedupe gate is clean AND false-defect verification is clean AND the candidate isn't on the Handoff-Exclusion list, immediately invoke the `red-green-fix` skill via the `Skill` tool. See § Fix Workflow for the exact call contract.
13.**Log** — append to session log; update `processed.json`.
| Drafts (failure) | `~/temp/bug-dump-ingest/drafts/*.md` — written only when `@Linear` never replies, so the human can retry manually |
## Label Taxonomy
Every created Linear issue MUST get the following labels, passed as a comma-separated list in the `Labels:` line of the `@Linear create` message. The Linear Slack app creates missing labels on first use:
| `reporter:` | `reporter:<slack-handle>` (kebab-case) | From message author |
| Status flags | `needs-repro`, `needs-backend`, `regression`, `pr-open` | When applicable |
Label rules:
- Always include `source:bug-dump`, exactly one `area:`, at least one `env:` (or `env:unknown`), exactly one `severity:`, exactly one `reporter:`.
-`needs-repro` — set when repro steps were ambiguous; signals "human should confirm before fix".
-`needs-backend` — set when fix is clearly in ComfyUI backend, not this frontend repo.
-`regression` — set when the bug mentions a version/upgrade correlation.
-`pr-open` — set instead of creating a fresh ticket when a fix PR already exists; the Linear issue becomes a tracker.
Labels are the primary affordance for downstream triage — invest in getting them right, not just in the title.
## Processed Detection
A top-level message is considered already-handled (skip creation) if ANY of:
- Its timestamp appears in `processed.json`.
- It carries a `:white_check_mark:` reaction on the parent — ticket already created.
- It carries a `:pr-open:` reaction — fix PR is open; skill records the PR link in the session log rather than creating a fresh Linear issue.
- It carries a `:repeat:` reaction — duplicate; skill attempts to find the original Linear issue and link it in the session log.
- It carries a `:question:` reaction — needs more context; skill skips creation and records for follow-up.
- Its thread contains a reply with a `https://linear.app/` URL (fetch via `slack_read_thread`).
- Its thread contains a reply starting with `:white_check_mark:` from the skill's bot user.
- It is a system/meta message (`has joined the channel`, bot-only message).
- Its thread already contains resolution confirmation (`"solved"`, `"resolved"`, `:done:` reaction from the reporter) AND has no fix PR referenced — treat as "resolved without ticket, skip".
Never re-ingest a message already marked in any of the above ways.
Before a candidate hits the approval batch, run cheap checks to demote obvious non-bugs. Goal: keep the approval table high-signal. This is not a full repro — just fast heuristics that catch the top false-positive classes.
| Reporter self-resolved in same msg | "no action needed", "solved", "nvm", "fixed it" | `resolved` |
| Reporter self-resolved in thread | `slack_read_thread` → reporter's last reply contains "solved" | `resolved` |
| Fix PR merged on main | `gh search prs "in:title <keyword>" --state merged --limit 3` | `fixed` |
| Fix PR open (already-filed) | `gh search prs "<keyword>" --state open --limit 3` | `pr-open` |
| Linear issue exists (open) | Linear `searchIssues` on title keywords → any open match | `dedupe` |
| Behavior is documented / intended | grep `docs/` and `src/locales/en/*.json` for the feature | `expected` |
| Not reproducible — feature doesn't exist | grep `src/` for mentioned component/feature → 0 hits | `stale` |
| Env drift only (local setup issue) | Thread contains "my machine", "my setup", "proxy" without others | `env` |
For each demoted candidate, record the demotion reason in the approval table as `Verify: <tag>` so the human can override if they disagree. Never hard-skip based on verification alone — always show the row with the demotion.
### Recommended verify commands
```bash
# 1. Search recent PRs for the feature in question
# 3. Check if it's a known i18n / documented setting
rg "<setting-key>" src/locales/en/ docs/
```
Keep verification under ~30s per candidate. If it takes longer, propose a ticket and let the human decide — don't let verification become the bottleneck.
Keep descriptions copy-paste friendly: lead with repro bullets, then env, then "See Slack: <permalink>". Attach thread summary only if it adds context beyond the top-level message.
### Severity Heuristics
- **high** — crash, data loss, blocks a template or core feature, affects paying users broadly (e.g. "job ends in 30m on Pro", "widget values reset").
- **medium** — visible regression, template error, wrong pricing, broken UX on a common path.
- **low** — cosmetic, single-template edge case, minor tooltip/boundary issue.
When unsure, mark `medium` and flag for human in the approval batch.
Before any candidate enters the approval table, run BOTH checks below and record the result in the row's `Dedup` and `PR` columns. This is a hard gate — no candidate may be proposed for creation without a verdict.
### Check 1 — Open Linear issues (via `@Linear search`)
Extract 3-5 keyword terms from the proposed title (strip stopwords). Post a search command to the bug-dump thread — use a scratch thread if no parent `ts` is available yet, but prefer the candidate's own parent thread so the search card becomes part of that thread's audit trail:
Poll `slack_read_thread` for up to 10s; parse the Linear app's card reply for `FE-NNNN` identifiers and URLs. Run the search twice with different keyword subsets if the first returns zero hits — reworded titles are the top false-negative class.
If `@Linear search` is not supported by the workspace's Linear app version, fall back to a Slack search for prior `@Linear` card replies in the channel:
This scans past Linear bot replies in the channel — any reply containing a matching `FE-NNNN` URL is a candidate duplicate. Record which dedupe path was used in the session log.
Treat a hit as a duplicate if any of:
- Title overlap ≥ 80% (after lowercasing + stopword removal)
- Same reporter + same component reference in description
- Same stack trace or error code
**Verdict:** set `Dedup: FE-NNNN` and default recommendation to `L` (link, don't create). The human may still override to `Y` to file a separate ticket.
### Check 2 — Open or merged fix PRs on GitHub
```bash
# Open PRs matching title keywords
gh pr list --repo Comfy-Org/ComfyUI_frontend --state open \
Treat a hit as a match if the PR title/body mentions the same component or bug phrase and the PR is unmerged or merged within the window covering the reporter's observation.
**Verdict:**
- Open PR match → set `PR: #NNNN (open)`, recommendation `pr-open` (file Linear with `pr-open` label linking the PR, skip auto-fix).
- Merged PR match → set `PR: #NNNN (merged)`, recommendation `fixed` (demote in verify, usually skip; human can override if the reporter claims the fix didn't land).
### Failure handling
If either check errors (Linear Slack app silent or not in channel, `gh` auth expired), DO NOT proceed to proposal — stop the sweep, report the failure to the user, and let them decide whether to re-run or manually dedupe. A silent skip of dedupe is never acceptable; it's the single biggest source of duplicate tickets.
Log each dedupe query + top hits in `~/temp/bug-dump-ingest/session-YYYY-MM-DD.md` under a per-candidate `Dedup trace:` block so the human can audit.
## Interactive Approval
Present candidates in batches of 5-10. Table format (10 columns):
```text
# | Slack (author, time) | Proposed title | Env | Sev | Area | Dedup | PR | Verify | Rec
Each row MUST show: Slack author + date, proposed title, env tags, severity, area, **dedupe status from the Pre-flight Dedupe Gate**, **open/merged PR hit from the Pre-flight Dedupe Gate**, verify tag (from False-Defect Verification), and agent recommendation.
### Default recommendation logic
The skill computes `Rec` deterministically from the gate results:
-`L` — Dedupe hit on open Linear issue.
-`pr-open` — Open GitHub PR hit.
-`fixed` — Merged PR hit within the reporter's observation window.
-`N` — Verify tag is `resolved`, `expected`, `stale`, or `env` only.
-`?` — Repro incomplete or classification ambiguous.
-`Y` — Everything clean AND candidate is not on the § Handoff-Exclusion list. This is the "file + auto-fix" path.
-`Y (file-only)` — Clean but on the handoff-exclusion list (e.g. touches LGraphNode, needs backend). File Linear, skip auto-fix.
### Response format
-`Y` — default path: create Linear ticket, post `:white_check_mark:` thread reply, AND if the candidate is eligible (dedupe clean, verify clean, not on handoff-exclusion list), immediately invoke `red-green-fix` via the `Skill` tool. See § Fix Workflow.
-`S` — **skip auto-fix** for this row: create Linear ticket + thread reply only, do NOT run red-green-fix. Use when the human knows a specific person is already investigating or wants to batch fixes.
-`N` — skip entirely (log reason in session file).
-`?` — mark as needs-context; skill posts a thread reply asking for repro details and prompts the human to add `:question:` to the parent.
-`L` — link to existing Linear issue instead of creating (skill asks which one if the Pre-flight Dedupe Gate didn't return an exact match).
-`R` — duplicate of another bug-dump message; skill links the two and prompts the human for `:repeat:` on the parent.
-`E` — edit proposed title/description before creating (skill shows draft for inline tweaks).
- Bulk responses accepted: `1 N, 2 Y, 3 L FE-4521, 4 pr-open #11402, 5 ?` — any row omitted from the response is treated as its computed `Rec` default.
Do not post any `@Linear create` messages until all candidates in the batch have a terminal decision. Auto-fix invocations run sequentially AFTER every `@Linear create` has produced a parsed `FE-NNNN`, so every `red-green-fix` call has a `Fixes FE-NNNN` to put in the PR body.
## Linear Slack Bot Integration (@Linear)
Every Linear action — create, search, link, label, status change — is performed by posting a message to the candidate's thread in `#bug-dump` that mentions `@Linear`. The Linear Slack app parses the mention and responds with a card in the same thread. There is no Linear MCP path and no `LINEAR_API_KEY` path; see `reference/linear-api.md` § "Why no direct API path" for the rationale.
### Prerequisites
- The Comfy Slack workspace already has the Linear Slack app installed (this is how humans add `@Linear` mentions today).
- Channel `C0A4XMHANP3` is connected to the `Frontend Engineering` Linear team.
- No per-machine setup. If a `@Linear` invocation produces no bot reply, the app is not in the channel — surface to the human, do NOT retry silently.
- First line MUST be `@Linear create` — this is the command token.
-`Team: Frontend Engineering` is required on every create — without it the bot falls back to the workspace default, which may route to a different team.
-`Status: Triage` pins the initial state (per § System Context).
-`Labels:` — comma-separated, full `source:bug-dump, area:*, env:*, sev:*, reporter:*` set per § Label Taxonomy. Missing labels are auto-created by the Linear Slack app on first use.
- Description body is markdown — see `reference/linear-api.md` § "Description body template" and `reference/schema.md` for per-field extraction.
- Use real newlines (not literal `\n`) when constructing the text.
After the tool call returns, poll `slack_read_thread` for the Linear app's reply card (up to 3× with ~3s spacing). Parse the card for:
- An `FE-NNNN` identifier
- A `https://linear.app/<org>/issue/FE-NNNN` URL
The URL is the ingested receipt. The skill then posts the `:white_check_mark:` confirmation reply (§ Slack Thread Reply).
### Search (dedupe)
See § Pre-flight Dedupe Gate § Check 1 for the search command shape and handling of the bot's reply. The search is a tool call in the candidate's thread — not a chat aside.
### Link an existing issue (`L` response)
When the human picks `L FE-4521` for a row, do NOT post `@Linear create`. Instead:
```text
mcp__plugin_slack_slack__slack_send_message({
channel_id: "C0A4XMHANP3",
thread_ts: "<parent-ts>",
text: "@Linear link FE-4521"
})
```
The bot replies with the linked issue card. Then post the `:white_check_mark:` confirmation reply (adjusted to say `Linked to Linear:` rather than `Filed to Linear:`) so Processed Detection still matches.
### Label / status updates
When a later sweep needs to flip a ticket (e.g. a PR opened after initial ingest, so add `pr-open` and link):
```text
mcp__plugin_slack_slack__slack_send_message({
channel_id: "C0A4XMHANP3",
thread_ts: "<parent-ts>",
text: "@Linear FE-4521 add-labels pr-open"
})
```
Status changes are rarely driven by this skill directly — Linear auto-moves issues to `In Review` when a PR with `Fixes FE-NNNN` is opened, and the `red-green-fix` skill handles that PR body.
### Captured fields per create
Every successful create must produce, via the Linear bot's reply card:
-`identifier` — e.g. `FE-4710`, used in `Fixes <LIN-ID>` references and session log
-`url` — `https://linear.app/.../issue/FE-4710`, included verbatim in the `:white_check_mark:` reply
-`ts` of the Linear bot's card reply — recorded in session log for audit
If the card is missing the URL or identifier, fall through to the failure path below — do NOT fabricate either value.
### Failure path
If the Linear bot does not reply within the poll window, OR replies with a parse error (`couldn't parse`, `no team matched`, `failed`):
1. Write a draft markdown file to `~/temp/bug-dump-ingest/drafts/NN-short-slug.md` containing the full `@Linear create` text that was sent plus any partial bot reply.
2. Post a thread reply that is explicit about the failure — do NOT include `:white_check_mark:` or a fake Linear URL:
```text
:warning: bug-dump-ingest: @Linear did not respond. Drafted at ~/temp/bug-dump-ingest/drafts/<slug>.md — please file manually and reply with the FE-NNNN.
```
3. Skip auto-fix for this candidate (no Linear ID = no `Fixes` reference).
4. Log the failure in the session log.
Never invent a Linear URL. Never post `:white_check_mark: Filed to Linear: ...` without a real URL parsed from a real Linear bot card.
Every approved candidate produces **two** mandatory `slack_send_message` calls in the parent thread:
1. The `@Linear create` (or `@Linear link`) command — see § Linear Slack Bot Integration.
2. The `:white_check_mark:` confirmation reply described below, posted after a real `FE-NNNN` + URL have been parsed from the Linear bot's card.
The second reply is what future sweeps grep for via `has::white_check_mark: from:me`. Even though the Linear bot's own card already contains the URL, the `:white_check_mark:` prefix is the canonical Processed Detection marker — without it, a future sweep may re-ingest the same bug.
The skill is not done with a candidate until BOTH calls have succeeded. If either fails, do not claim the candidate is ingested.
### Required call shape
```text
mcp__plugin_slack_slack__slack_send_message({
channel_id: "C0A4XMHANP3",
thread_ts: "<parent-message-ts>", // dotted form, e.g. "1776714531.990509"
- `thread_ts` MUST be the parent message ts — never the channel ts, never omitted. An omitted `thread_ts` posts at channel level, which pollutes `#bug-dump` and breaks Processed Detection.
- The text MUST start with `:white_check_mark:` followed by a space and `Filed to Linear:`. This exact prefix is what future sweeps grep for via `has::white_check_mark: from:me`.
- The Linear URL MUST be present. No URL = not ingested; future sweeps will re-file the same bug.
- Plain text only — no markdown tables, no bold, no code fences. Slack renders the emoji shortcode into a real `:white_check_mark:` only when the message is plain text.
- Capture the returned `ts` and record it in the session log for audit.
### NEVER-do list (common failure mode)
- **Do NOT** print `@Linear create ...` or `:white_check_mark: Filed to Linear: <URL>` into the Claude CLI chat response as a substitute for calling `slack_send_message`. The CLI output is not seen by Slack. If you find yourself typing either into a plain assistant message, stop and issue the tool call instead.
- **Do NOT** claim the thread reply was posted until the `slack_send_message` tool call has returned a success with a `ts`. If the tool call errors, surface the error and halt the batch — do not fabricate a reply.
- **Do NOT** use any other tool (e.g. `slack_schedule_message`, `slack_send_message_draft`) as a substitute. Only an immediate `slack_send_message` with `thread_ts` set counts — the Linear Slack app does not trigger on scheduled/draft messages.
- **Do NOT** substitute any direct Linear API call (MCP, GraphQL, curl) for the `@Linear` mention. The Slack thread is intentionally the single audit trail.
### Fix-path reply (after red-green-fix opens a PR)
When `red-green-fix` returns a PR URL for an auto-fixed candidate, the skill MUST post a second thread reply on the same parent — again via `slack_send_message`:
```text
mcp__plugin_slack_slack__slack_send_message({
channel_id: "C0A4XMHANP3",
thread_ts: "<same parent ts>",
text: ":pr-open: Fix PR: <PR_URL>\nRed-green verified: <unit|e2e> test proves the regression.\nFixes <LIN-ID>"
The Slack MCP does not expose `reactions.add`, so the skill cannot set a `:white_check_mark:` reaction on the parent. The thread reply above is sufficient for Processed Detection; the parent reaction is a human-only "visible in channel" nudge. At the end of the run, the skill MAY print a compact list for the human:
```text
Optional: add :white_check_mark: to parent messages for in-channel visibility.
LIN-4710 → <permalink>
LIN-4711 → <permalink>
```
This is a convenience, not a deliverable — a missing parent reaction does not cause re-ingestion.
## Fix Workflow (auto-invoke red-green-fix)
For every `Y` row whose `Rec` resolved to auto-fix (dedupe clean, verify clean, not on handoff-exclusion list), the skill MUST — after Linear creation and the `:white_check_mark:` thread reply — invoke the `red-green-fix` skill via the `Skill` tool. This is a real tool call, not a narrative handoff.
### Required Skill tool call
```text
Skill({
skill: "red-green-fix",
args: "<composed prompt — see below>"
})
```
Compose `args` as a single self-contained prompt so the sub-invocation has everything it needs without re-reading the Linear issue:
Follow the red-green-fix two-commit sequence exactly. Do NOT skip the red commit.
```
The skill MUST wait for `red-green-fix` to return before moving to the next candidate. Process one auto-fix at a time so branch state is deterministic.
### Verifying the invocation ran
After the `Skill` call returns, the skill MUST confirm at least one of:
1. A new git branch named `fix/<lin-id>-*` exists (`git branch --list "fix/<lin-id>-*"`).
2. A PR URL is present in `red-green-fix`'s return payload.
If neither is true, the invocation silently no-op'd. Log the failure to the session log as `auto-fix skipped: invocation returned without branch or PR` and continue — do NOT post the `:pr-open:` thread reply.
### Inputs summary
- **Bug description** — the Linear description (includes repro, env, source permalink).
- **Linear ID** — inserted into the PR body as `Fixes <LIN-ID>`.
- `cloud` → unit if client-side logic, otherwise flag "backend — out of scope for this repo"
### Handoff-Exclusion list (do NOT auto-invoke red-green-fix)
These rows still get a Linear ticket + `:white_check_mark:` thread reply, but the skill MUST skip the `Skill(skill="red-green-fix")` call and instead post a thread nudge explaining why:
- Repro steps are incomplete (no clear numbered steps, no env) — reply in thread: "Need clearer repro before I can write a failing test. What's the shortest path to reproduce?"
- Linear ticket was dedupe-linked rather than newly created — existing owner may already be fixing.
- Severity is cosmetic AND reporter hasn't asked for a fix — file ticket only.
- Fix would touch `LGraphNode`, `LGraphCanvas`, `LGraph`, or `Subgraph` god-objects (ADR-0003/0008 — always human decision).
- Pre-flight Dedupe Gate found an open PR (`pr-open`) or a matching merged PR (`fixed`).
When a row is excluded, record the reason in the session log under `auto-fix excluded: <reason>`.
### Test authoring rules
Both tests MUST be written in the "red" commit BEFORE any fix code (per red-green-fix). Rules specific to bug-dump ingestion:
- **Unit test (Vitest)** — colocated next to the implementation, `<file>.test.ts`. Exercise the specific logic path reproduced by the reporter. One `describe` block named after the Linear ID:
it('includes the currently-selected model in the list even when not in available models', () => {
// ...
})
})
```
- **E2E test (Playwright)** — under `browser_tests/tests/`, follow `writing-playwright-tests` skill. Tag with `@regression` and include the Linear ID in the test title:
```typescript
test.describe(
'LIN-4710 unet dropdown regression',
{ tag: ['@regression'] },
() => {
test('keeps selected model visible in the dropdown', async ({
comfyPage
}) => {
// ...
})
}
)
```
- **Mock data types** — follow `docs/guidance/playwright.md`: mock responses typed from `packages/ingest-types`, `packages/registry-types`, `src/schemas/` — never `as any`.
(The Handoff-Exclusion list above governs when `red-green-fix` is NOT invoked.)
### PR body template
The red-green-fix skill's PR template is extended with a `Source` line:
| `:eyes:` | Someone is triaging | Still ingestable |
| `:done:` | Reporter resolved | Demote to `resolved` in verify, but still show row |
| `:+1:` | Acknowledged | Ignore |
Approval-table response code `R` (new) corresponds to `:repeat:` — if you pick `R`, the skill treats it as duplicate and asks for the target Linear ID.
## Session Log
Append to `~/temp/bug-dump-ingest/session-YYYY-MM-DD.md`:
```text
Bug Dump Ingest Session -- 2026-04-20 11:40 KST
Window: 2026-04-18 00:00 — 2026-04-20 12:00 KST
Scanned: 28 top-level messages
Skipped (meta/discussion/processed): 14
Proposed: 14
Approved: 11
Created in Linear: 10
Draft-only (creation failed): 1
Linked-only (dedupe): 1
Thread replies posted: 11
Created:
- LIN-4710 Unet model dropdown missing selected model -- wavey -- low/ui
- LIN-4711 Pro plan jobs end at 30 minutes -- Denys -- high/cloud
- ...
Skipped with reason:
- 1776592837.616399 -- design discussion in thread, not a bug
- ...
```
## Gotchas
### Thread summaries, not raw dumps
Pulling the full thread often adds noise. Summarize replies to: (a) confirmed reproductions by other users, (b) env/version details added in replies, (c) links to related PRs/commits. Drop emojis-only replies, joined-channel notifications, and off-topic chatter.
### Cross-posts are not bugs
When the top-level message is just a link to a Slack message in another channel (e.g. "X posting" with a URL and nothing else), follow the link to the original source and ingest from there — do NOT create a ticket from the cross-post itself.
### Resolved-in-thread messages
If the reporter replies `"No action needed, this is solved"` (see wavey 2026-04-20 08:06), mark the ticket for SKIP in the approval table, not auto-skip. The human may still want a regression test ticket.
Slack file IDs (e.g. `F0AT...`) are private. Do NOT link them directly in Linear. Instead, list the filename and type in the Linear description and include the Slack permalink — anyone with Slack access can see the attachments from the thread.
### No auto-create without approval
Never create Linear issues without a human `Y`. This is a hard rule — the skill exists to reduce human toil, not to replace triage judgment.
- `reference/schema.md` — full ticket schema with field-by-field extraction notes.
- `reference/examples.md` — worked examples drawn from real #bug-dump messages.
- `reference/verify-commands.md` — cookbook of false-defect verification commands per bug class.
## Related Skills
- `red-green-fix` — auto-invoked via the `Skill` tool for every eligible `Y` candidate to produce a failing test + fix + PR with the red-green CI proof.
- `writing-playwright-tests` — used by red-green-fix when an e2e test is needed.
- `hardening-flaky-e2e-tests` — if the e2e test added in the fix PR starts flaking, jump to this skill.
> With Nodes 2.0, large workflows (hundreds of nodes) make the canvas extremely laggy and unusable for actual work — switching tabs takes several seconds or more. Switching back to Litegraph, performance is significantly better.
**Dedupe**: Post `@Linear search nodes 2.0 performance canvas lag` (Team: Frontend Engineering, Status: open) in the candidate's thread. Likely matches exist — flag `Dedup? ?` and ask human which ticket to link to.
---
## Example 6 — Reporter says it's a question, not a report
**Source message** (Luke, 2026-04-17 08:27):
> Is NodeInfo supposed to show information or docs about the node? It just brings up the node sidebar
**Classification**: question → ambiguous. Read thread. If replies confirm "that's unexpected, should show docs", upgrade to bug. If "yes that's intended", skip.
Default recommendation in the approval batch: `?` (needs expansion).
---
## Example 7 — Bug with PR already in flight
**Source message** (Pablo, 2026-04-17 08:52):
> when deleting multiple assets on cloud -> the confirmation popup still has the assets hashes as names instead of the display name
**Reaction**: `pr-open (1)` — someone's opened a PR.
**Classification**: `already-filed` branch. Skip creation; in the session log, note "PR already open". If the human wants a tracking Linear ticket anyway, still fileable with a link to the PR.
The skill drives Linear exclusively through the Linear Slack app (`@Linear`). **There is no Linear MCP, no `LINEAR_API_KEY`, no GraphQL.** Every Linear read/write happens as a Slack message that mentions `@Linear` in the `#bug-dump` thread, and the Linear Slack app performs the action and posts a reply card containing the issue URL.
## Why Slack-only
- The `#bug-dump` thread is already the source of truth; keeping the entire lifecycle (report → ticket → PR → resolution) in one thread means Processed Detection can grep the thread instead of a separate registry.
- No API key rotation, no MCP server install, no OAuth browser flow — works on any machine that already has the Slack MCP configured.
- The Linear Slack app's reply card (with issue URL, title, status, and assignee) IS the canonical receipt; the skill records its `ts` in the session log.
## Prerequisites (one-time, per workspace)
The Comfy Slack workspace must already have the Linear Slack app installed (it is — that's how humans use `@Linear` reactions today) and `#bug-dump` (channel `C0A4XMHANP3`) must have Linear enabled for the `Frontend Engineering` team. Nothing else to configure. If a `@Linear` invocation silently does nothing, the bot isn't present in the channel — surface that to the human rather than re-trying.
## Supported operations
Every operation is a `mcp__plugin_slack_slack__slack_send_message` call with `channel_id=C0A4XMHANP3` and `thread_ts=<parent-ts>`. The `text` is a natural-language instruction to the Linear bot. Keep the text concise — Linear parses the first line as the command intent.
- Start with `@Linear create` on its own line — this is the command token the bot keys on.
- Always specify `Team: Frontend Engineering`. Without it, the bot falls back to the Slack workspace's default team, which may not be FE.
-`Status: Triage` pins the initial workflow state.
-`Labels:` — comma-separated. If a label doesn't exist yet in Linear, the bot creates it on first use (verified in Linear workspace settings). Keep the taxonomy exactly as SKILL.md § Label Taxonomy.
-`<description body>` — markdown per `reference/schema.md` Description Template. Use real newlines, not literal `\n`.
- End with `Source: <slack-permalink>` so the Linear issue body links back even if the auto-attachment of the parent message fails.
The Linear bot replies in the same thread with a card that contains:
- The Linear URL (`https://linear.app/comfy-org/issue/FE-NNNN`)
- Status, assignee (initially unassigned), and applied labels
- A "View in Linear" button
Parse the URL out of the bot's reply text (or attachments). If no card reply appears within ~10s of polling `slack_read_thread`, treat it as a creation failure — do NOT proceed to the `:white_check_mark:` confirmation reply.
The bot replies with a card listing up to ~5 matching open issues. Parse identifier (`FE-NNNN`) and URL per row. Treat a hit as a duplicate per SKILL.md § Pre-flight Dedupe Gate § Check 1.
If `@Linear search` is not supported in the installed Slack app version, fall back to Slack-native search across the `#bug-dump` thread replies (previous `@Linear` cards contain title + URL — grep those for the same keywords). Record which path was used in the session log so the human can see dedupe coverage.
### 3. Link an existing issue (dedupe: `L` response)
```text
mcp__plugin_slack_slack__slack_send_message({
channel_id: "C0A4XMHANP3",
thread_ts: "<parent-ts>",
text: "@Linear link FE-4521"
})
```
The bot replies with the linked issue card. The skill then posts its own `:white_check_mark: Linked to Linear: <URL>` confirmation reply (see SKILL.md § Slack Thread Reply).
### 4. Add labels to an existing issue
```text
mcp__plugin_slack_slack__slack_send_message({
channel_id: "C0A4XMHANP3",
thread_ts: "<parent-ts>",
text: "@Linear FE-4521 add-labels pr-open"
})
```
Used when an open PR is discovered after ticket creation and the Linear issue should flip to `pr-open`.
### 5. Change status
```text
mcp__plugin_slack_slack__slack_send_message({
channel_id: "C0A4XMHANP3",
thread_ts: "<parent-ts>",
text: "@Linear FE-4521 status In Progress"
})
```
Rarely used by the skill directly — usually status changes come from the `red-green-fix` PR lifecycle (Linear auto-moves to `In Review` when a PR references `Fixes FE-4521`).
## Description body template
The text that follows the command headers is rendered verbatim as the Linear issue description (markdown). Use this template — see `reference/schema.md` for field-by-field extraction notes:
Thread summary: <1-3 bullets if thread adds context>
```
The Slack permalink is load-bearing — it's the canonical route to attachments, reporter, and any follow-up discussion. Do NOT embed Slack file IDs (`F0AT...`) directly; they're permissioned.
## Parsing the bot's reply
After each `slack_send_message` that mentions `@Linear`, poll `slack_read_thread` (with `channel_id=C0A4XMHANP3`, `thread_ts=<parent-ts>`) up to 3 times, ~3s apart. Scan replies authored by the Linear Slack app user for:
- Any `https://linear.app/<org>/issue/FE-\d+` URL → capture as the issue URL.
- The `FE-NNNN` identifier pattern → capture as the issue identifier.
- An error phrase (`couldn't`, `failed`, `not found`, `no team matched`) → treat as failure; surface the full bot text to the human.
Record the bot reply's `ts` alongside the captured URL and identifier in the session log.
| No bot reply within 10s | Linear app not in channel, or bot outage | Halt the batch, surface to human, do NOT fabricate a Linear URL. Remaining approved candidates stay queued for re-run. |
| Bot replies with "no team matched" | Team name typo or Linear workspace drift | Re-send with the exact team name from the Linear workspace (default: `Frontend Engineering`). If it still fails, ask the human to verify. |
| Bot replies with "couldn't parse labels" | One of the labels has syntax the bot rejects | Drop the offending label, re-send; log the partial-label failure so the human can patch after. |
| Bot creates the issue but reply lacks the URL | Rare bot format change | Re-fetch the thread after ~5s; if URL still absent, open Linear search via `@Linear search <title>` and recover the identifier + URL. |
| Multiple `@Linear` replies match (duplicate card) | The skill retried without polling first | Keep the earliest card's URL; log the extras. Never re-issue `@Linear create` for the same candidate without confirming the first card failed. |
Never retry `@Linear create` without first running `@Linear search` for the same title keywords — a duplicate card is worse than an initial failure because the human has to close one of them manually.
## Why no direct API path
- The Linear MCP (official or community) would require either OAuth setup or `LINEAR_API_KEY` in env — both are per-machine hurdles the skill should not depend on.
- Direct GraphQL against `api.linear.app` has the same key-management cost and bypasses the Slack thread as the audit trail.
- Routing every action through `@Linear` in the thread gives humans full visibility in the channel (the bot's card is the receipt) and Processed Detection becomes a simple Slack thread read.
If a future need requires capabilities the `@Linear` Slack app doesn't expose (bulk operations, private field edits, webhooks), stop and surface the limitation to the human rather than quietly adding an API-key path — the "Slack-only" constraint is intentional.
Field-by-field guidance for normalizing a Slack #bug-dump message into a ticket.
## `slack_ts`
The top-level message timestamp from `slack_read_channel` response (`Message TS:` field). Always store the dotted form (`1776510375.473579`). This is the ingestion identity used in `processed.json`.
| "unet model dropdown doesnt display all available models..." | Unet dropdown missing selected model |
| "Dates are broken on Settings -> Secrets. Cloud Prod" | Settings → Secrets dates broken on cloud prod |
| "LTX-2: Audio to VIdeo template results in the "RuntimeError..." error" | LTX-2 Audio-to-Video template RuntimeError on cloud |
## `description`
Structure — see `linear-api.md` § "Description body template". Key rules:
- Lead with **Repro** numbered list. Extract from the message body; if no steps are given, write "Repro: [Slack message body quoted verbatim]" and flag for human in approval.
- Preserve the reporter's own words in the Repro section when they include "step 1 / step 2" markers.
- Collapse multi-paragraph asides into "Notes" at the end.
A message can have multiple env tags. If none are detectable, set `env: []` and flag "env unclear" in the approval row.
## `severity`
Heuristics in SKILL.md. When uncertain, mark `medium` and note in approval table: `Sev: medium (flag)`.
## `area`
Single tag. Use the one that best fits; tiebreak toward the more actionable team:
-`cloud` > `workflow` when the reported behavior is specific to cloud-hosted features (billing, queue, jobs)
-`node-system` > `ui` when the defect is canvas interaction, not just visual
-`templates` only when a named template is the subject
## `attachments`
From `slack_read_channel` message `Files:` field. Parse name, ID, type. Never include the Slack file ID in the Linear description — those are permissioned — just the filename and type.
Verify tag: `dedupe` with the `FE-NNNN` identifier in the approval row. See `reference/linear-api.md` § "Search existing open issues (dedupe)" for full handling.
If the original is already ingestable, ingest from the original's permalink. If it's a GitHub issue, prefer linking that GitHub issue to the Linear ticket instead of creating two entries.
Verify tag: `cross-post` with the resolved source permalink.
description: Use when reviewing Vitest unit-test diffs in ComfyUI_frontend, especially new mocks, store tests, component tests, or bugfix regression tests.
---
# Reviewing Unit Tests for ComfyUI_frontend
## Overview
Review for behavior and current repo rules, not motion. Compare to authoritative rules, not prior diffs or legacy snippets.
## Review Workflow
1. Identify the test type: component, store, composable, util, or bugfix regression.
2. Name the behavior the test proves. If you cannot say it in one sentence, request changes.
3. Open the authoritative doc section before judging structure.
4. Scan the red flags below.
5. State the verdict first. Name the failure mode. Cite the doc or rule.
## Source of Truth / Precedence
When docs and examples conflict, use this order:
1. Explicit repo rules, lint rules, and note blocks.
3. Rule sections in [`docs/testing/unit-testing.md`](../../../docs/testing/unit-testing.md), [`docs/testing/store-testing.md`](../../../docs/testing/store-testing.md), and [`docs/testing/component-testing.md`](../../../docs/testing/component-testing.md)
4. Example snippets
5. Prior diffs
Apply these repo-specific clarifications:
- [`docs/testing/component-testing.md`](../../../docs/testing/component-testing.md) starts with the authoritative rule: new component tests use `@testing-library/vue` with `@testing-library/user-event`. The `@vue/test-utils` snippets below it are legacy examples.
- [`docs/testing/store-testing.md`](../../../docs/testing/store-testing.md) still contains `as any` examples. Treat them as legacy snippets, not approval for new or edited test code.
- If docs conflict, prefer the stricter newer rule and call out the doc ambiguity. Do not approve through it.
| "I restructured the mocks" | If the indirection stayed, nothing improved. Flag `alias-by-renaming`. |
| "The docs do it" | Rule, note, and lint beat legacy snippet. Compare to the current rule, not the nearest example. |
| "TypeScript required the cast" | `vi.mocked()` usually narrows mock methods. Assertion-only references need no cast. |
| "Putting it in `beforeEach` is DRY" | Recreating module mock state in hooks hides singleton behavior and drifts from the documented pattern. |
| "It is only a nit" | Explicit repo-rule violations are never nits. |
| "No behavior changed, just cleanup" | Motion != fix. Ask what behavior got stronger. |
| "Mental revert is enough" | For bugfix tests, establish red on pre-fix code or ask the author to show it. |
## Mocking Rules
- Fail helpers that do not remove repeated setup, encode domain meaning, or simplify assertions. Barely earning the abstraction is not enough.
- For composables with reactive or singleton state, define stable mock state inside the `vi.mock()` factory. Access it per test via the composable itself. See [`docs/testing/unit-testing.md`](../../../docs/testing/unit-testing.md) "Mocking Composables with Reactive State".
- This does not ban local test data builders or per-test `vi.spyOn(...)`.
- Mock seams, not the project-owned module you are trying to exercise. For store tests, prefer real Pinia plus `createTestingPinia({ stubActions: false })` per [`docs/testing/vitest-patterns.md`](../../../docs/testing/vitest-patterns.md) and [`docs/testing/store-testing.md`](../../../docs/testing/store-testing.md).
### Alias-by-Renaming
```ts
// Before
constmockAdd=vi.hoisted(()=>vi.fn())
// After: same indirection, new name
functiongetToastAddMock() {
returnuseToast().add
}
```
If the wrapper only renames or relays a mocked value, fail it. Inline the lookup at the call site or fetch the singleton mock via the documented pattern.
- Flag casts whenever `vi.mocked()` would narrow correctly.
- Do not add `vi.mocked()` around assertion-only references just for style.
### Reset Hygiene
- Flag per-mock `mockClear()` or `mockReset()` when `vi.clearAllMocks()` or `vi.resetAllMocks()` already runs in the relevant hook chain.
- Review for redundancy or broken state management. Do not bikeshed `clearAllMocks` vs `resetAllMocks` unless behavior depends on it.
### Third-Party Seams
- Distinguish trivial hooks from behavior-rich APIs.
- Mocking single-method third-party hooks like `primevue/usetoast` is usually acceptable.
- That exception does not justify mocking behavior-rich third-party modules.
### `vue-i18n`
- Never mock `vue-i18n` in component tests.
- Use real `createI18n` per [`docs/testing/vitest-patterns.md`](../../../docs/testing/vitest-patterns.md) and the shared [`testI18n`](../../../src/components/searchbox/v2/__test__/testUtils.ts) setup.
| Mock-only assertion | Accept collaborator-call assertions only when the call is the meaningful external effect and the test also exercises the triggering behavior. |
| Non-behavioral assertion | Reject tests that only check classes, utility hooks, or styling internals. |
| New component test using `@vue/test-utils` | Request changes. Use `@testing-library/vue` plus `@testing-library/user-event`. |
| `any`, `as any`, or `@ts-expect-error` in new or edited test code | Request changes unless the author proves no safer type exists. Legacy doc snippets do not authorize it. |
## Bugfix Regression Proof
For `fix:` PRs or bugfix diffs:
1. Identify the production change that fixes the bug.
2. Verify the new test fails on pre-fix code, or ask the author to show it.
3. If the test passes on broken code, request changes.
A regression test that never proves red does not pin the bug.
## Review Output Rules
- State verdict before procedural questions.
- Do not lead with approval language like `LGTM, just one nit` or `approve and move on?`.
- Name the failure mode directly: `alias-by-renaming`, `unnecessary cast`, `mocked i18n`, `mock-only assertion`, `unproven regression`.
- Link the authoritative doc section in the review comment.
- If an explicit repo rule, lint rule, or authoritative doc note is violated, do not downgrade it to "minor deviation" or "nit".
| New `vi.mock(...)` for a composable | [`docs/testing/unit-testing.md`](../../../docs/testing/unit-testing.md) -> "Mocking Composables with Reactive State" |
| New store test or store mock | [`docs/testing/vitest-patterns.md`](../../../docs/testing/vitest-patterns.md) setup + [`docs/testing/store-testing.md`](../../../docs/testing/store-testing.md) |
| New component test | Top note in [`docs/testing/component-testing.md`](../../../docs/testing/component-testing.md) |
| `vue-i18n` in a component test | [`docs/testing/vitest-patterns.md`](../../../docs/testing/vitest-patterns.md) + [`src/components/searchbox/v2/__test__/testUtils.ts`](../../../src/components/searchbox/v2/__test__/testUtils.ts) |
| Cast around a mock | [`docs/guidance/typescript.md`](../../../docs/guidance/typescript.md) -> "Type Assertion Hierarchy" |
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
> Comfy is the AI creation engine for visual professionals who demand control over every model, every parameter, and every output. Built around ComfyUI — the open-source node-graph runtime with 60,000+ community nodes and thousands of shared workflows — Comfy ships as a free local app, a managed cloud, an API, and an enterprise platform.
The Comfy ecosystem spans four surfaces:
- **ComfyUI (local)** — the open-source node-graph runtime that runs models on your own hardware.
- **Comfy Cloud** — managed ComfyUI in the browser, with hosted models and storage.
- **Comfy API** — a REST API for triggering workflows from your own apps and pipelines.
- **Comfy Enterprise** — single-tenant deployments, BYO keys, data ownership, and orchestration for teams.
Studios building with Comfy include Series Entertainment, Moment Factory, Open Story Movement, and Ubisoft (La Forge). Use cases concentrate in VFX & animation, advertising & creative studios, gaming, and eCommerce/fashion.
## Product
- [Homepage](https://comfy.org/): Overview of Comfy and the four product surfaces (Local, Cloud, API, Enterprise).
- [Download Comfy (Local)](https://comfy.org/download/): Free desktop app for macOS, Windows, and Linux — runs ComfyUI on your own GPU.
- [Comfy Cloud](https://comfy.org/cloud/): Managed ComfyUI in the browser with hosted models and storage; no local install required.
- [Comfy Cloud Pricing](https://comfy.org/cloud/pricing/): Plans and per-credit pricing for individuals and teams using Comfy Cloud.
- [Comfy API](https://comfy.org/api/): REST API for triggering ComfyUI workflows programmatically from external apps.
- [Comfy Enterprise](https://comfy.org/cloud/enterprise/): Single-tenant ComfyUI deployments with BYO keys, orchestration, and data-ownership guarantees.
## Workflows and Gallery
- [Workflow Gallery](https://comfy.org/gallery/): Curated showcase of ComfyUI outputs — images, video, and 3D — produced by the community.
- [Community Workflows](https://www.comfy.org/workflows/): Browseable library of community-shared ComfyUI workflows you can load and remix.
## Customers and Case Studies
- [Customer Stories](https://comfy.org/customers/): Index of named customers and how they use ComfyUI in production.
- [Series Entertainment](https://comfy.org/customers/series-entertainment/): How Series Entertainment rebuilt game and video production around ComfyUI.
- [Moment Factory](https://comfy.org/customers/moment-factory/): Architectural-scale 3D projection mapping reimagined with ComfyUI at Moment Factory.
- [Ubisoft — Chord](https://comfy.org/customers/ubisoft-chord/): Ubisoft La Forge open-sourcing the Chord model and its ComfyUI integration.
- [Open Story Movement](https://comfy.org/customers/open-story-movement/): How an open-source movement around AI storytelling builds on ComfyUI.
## Developers and Documentation
- [ComfyUI Docs](https://docs.comfy.org/): Official documentation for installing, configuring, and extending ComfyUI.
- [ComfyUI on GitHub](https://github.com/comfyanonymous/ComfyUI): Source repository for the open-source ComfyUI runtime.
- [Comfy-Org on GitHub](https://github.com/Comfy-Org): Organization-wide repositories — frontend, registry, manager, docs, and tooling.
- [Comfy Registry](https://registry.comfy.org/): Public registry of ComfyUI custom nodes and extensions, with versioning and search.
## Company
- [About Comfy](https://comfy.org/about/): Company background, mission, and the team behind ComfyUI.
- [Careers](https://comfy.org/careers/): Open roles across engineering, design, product, and go-to-market.
- [Contact](https://comfy.org/contact/): Sales, partnership, and general contact form.
- [Blog](https://blog.comfy.org/): Product announcements, technical deep-dives, and customer stories.
- [Privacy Policy](https://comfy.org/privacy-policy/): How Comfy collects, uses, and protects personal information.
- [Terms of Service](https://comfy.org/terms-of-service/): Terms governing use of ComfyUI and related Comfy services.
## Optional
- [简体中文 / Chinese homepage](https://comfy.org/zh-CN/): Simplified Chinese localization of the main site.
- [Series Entertainment — long-form case study](https://comfy.org/cloud/enterprise-case-studies/how-series-entertainment-rebuilt-game-and-video-production-with-comfyui): Extended write-up of the Series Entertainment deployment.
- [Moment Factory — long-form case study](https://comfy.org/cloud/enterprise-case-studies/comfyui-at-architectural-scale-how-moment-factory-reimagined-3d-projection-mapping): Extended write-up of Moment Factory's projection-mapping pipeline.
- [Ubisoft Chord announcement (blog)](https://blog.comfy.org/p/ubisoft-open-sources-the-chord-model): Original blog post announcing Ubisoft's open-source Chord model.
- [Open-source storytelling (blog)](https://blog.comfy.org/p/how-open-source-is-fueling-the-open): Blog post on how open source is fueling the Open Story Movement.
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.