CodeRabbit raised two valid concerns:
1. If processSelect throws (e.g. through a node/extension callback), the
plain assign-and-restore pattern leaves onSelectionChange unset and
silently drops every later selection change on this canvas.
2. The unconditional RAF schedules a pointless store sync on the
already-selected sticky-resselect path where processSelect early-returns
without notifying.
Both fixes are the same shape: replace the temporary undefined with a
sentinel that records whether processSelect actually attempted a
notification, wrap processSelect in try/finally so the original listener
is always restored, then schedule the RAF only if the sentinel fired.
Adds two tests covering each guarantee.
When click-dragging an unselected node, processSelect() runs synchronously
at drag start to update selection state, then fires onSelectionChange,
which downstream triggers Vue store reactivity (canvasStore.updateSelectedItems).
That work blocks the first paint of the dragged node, producing visible lag
before the node starts following the pointer (FE-558).
Selection state must be updated synchronously (used by drag math), but the
notification can wait one frame: the user sees the node move immediately,
and Vue store updates settle by the next frame — far less perceptible than
a dragged node lagging the cursor.
Suppress onSelectionChange across the processSelect call (avoiding the
nested notification fired by deselectAll), then schedule a single RAF
notification with the post-processSelect selection set. Already-selected
node drags are unaffected (sticky=true short-circuits processSelect).
## 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)