- Split retryAndDiagnostics.test.ts into retry.test.ts + diagnostics.test.ts so each test file mirrors its source file
- Add explicit ERR_CANCELED test case in retry.test.ts
- Drop UseRemoteComboResult interface — useRemoteCombo has a single caller, inferring the return shape removes redundant type definition (per @christian-byrne)
Pre-refactor getAuthScope() used four buckets including a literal 'apikey' / 'anon' split. The atomized scope dropped this distinction — both api-key and anonymous sessions ended up with {userId: null, workspaceId: null}, sharing a query key.
Because the QueryClient persists across api-key set/clear transitions (no eviction path runs without a router-driven WorkspaceAuthGate unmount), this caused cross-bucket cache reuse: an anonymous fetch could be served to an authenticated api-key reader within staleTime, or vice versa.
Add an opaque apiKeyBucket: 'apikey' | 'anon' | null field to RemoteAuthScope, include it in remoteOptionKeys.byRoute, and populate it from useApiKeyAuthStore().getApiKey() in both useRemoteOptions (Vue path) and useRemoteWidget (Litegraph path). Only the bucket literal lives in the query key — never the api-key value itself — so devtools/Sentry don't see secrets.
Adds a regression test asserting that anon and apikey scopes with identical userId/workspaceId produce distinct query keys.
reka-ui's ComboboxRoot defaults resetSearchTermOnBlur=true. After ComboboxItem.onSelect, the root closes and ~1ms later triggers ComboboxInput's resetSearchTerm, which without an explicit displayValue prop writes rootModelValue.toString() — i.e. the selected item's id — into the input. Our v-model on the input propagates that into ctx.searchQuery, so reopening the dropdown shows the id in the search box and a single-item filtered list.
Pass displayValue={() => ''} on ComboboxInput so reset always writes '' instead. Smallest surface, contract sits right next to the v-model it's correcting.
Some remote assets (e.g. /proxy/seedance/assets) return name='' for items the user never titled. Trigger and list rows rendered blank because nullish coalescing (??) only catches null/undefined, not empty strings.
Add displayName(item) helper in base/remote/itemSchema.ts using logical-or fallback (matches the FormDropdownInput pattern in PR #11310) and use it in Trigger.vue's selected-label computed and Item.vue's name span, img alt, and video aria-label so the accessibility names also fall back instead of going empty.
The atom family computed previewType but never used it: Item.vue's default slot only rendered the text. Wire previewType through RemoteComboContext, compute it in useRemoteCombo from item_schema.preview_type (default 'image'), and render <img> / <video preload=metadata muted playsinline> / audio toggle button based on the type when item.preview_url is set.
Audio toggle button uses @click.stop / @pointerdown.stop to prevent triggering item selection, and the existing widgets.remoteCombo.playAudioPreview / pauseAudioPreview i18n keys for the aria-label.
Drops the void previewType.value / void RemoteComboKey markers in RichComboWidget.vue (they were placeholders for this unfinished wiring) and the now-unused itemSchema computed.
Adds Item.preview.test.ts covering image/audio/no-preview branches.
Without v-model on ComboboxInput, the Root's controlled :search-term binding didn't receive keystrokes back through the input — typing in the search bar did nothing. Wire v-model directly to ctx.searchQuery.value so the input updates the shared search state.
Previously showRefreshButton returned true whenever refresh_button wasn't explicitly false, including when remoteConfig itself was absent. Gate visibility on remoteConfig presence first so the refresh control only renders for combos that actually have a remote_combo config.
Only fetch and attach the platform auth header when descriptor.client === 'comfyApi'. The RemoteRequestClient union currently only contains 'comfyApi', but this guard prevents future additions from accidentally leaking platform credentials to external/non-platform routes.
- itemSchema: case-insensitive data:/blob: scheme check
- retry: don't retry ERR_CANCELED axios errors
- useRemoteOptions: return refetch promise instead of dropping it
- useRemoteWidget: recompute queryKey from current auth state per call
(prevents stale cache partition across login/logout)
- useRemoteWidget: sanitize error log (avoid leaking axios request metadata)
- RichComboWidget.test: assert loading text + aria-disabled, not just trigger
- RemoteCombo.stories: drop redundant unused QueryClient/i18n decorator and
the provide override that broke useI18n in child atoms
Nits:
- comboAdapter.test: drop unnecessary 'as never' casts (Partial<ComboInputSpec>
is already correctly typed)
- useRemoteOptions.test: withSetup returns cleanup that unmounts the test app
- mapToDropdownItem accepts an optional previewBaseUrl and resolves
relative preview paths against it (preserves absolute / data: / blob:
/ protocol-relative URLs unchanged); useRemoteCombo passes the
comfy-api base URL so previews render correctly when item_schema
preview_url_field returns a relative path
- Forward widget.options.disabled through RichComboWidget into the
RemoteCombo Root, Trigger, and Refresh atoms so a disabled remote
combo is non-interactive (matches WidgetSelectDefault precedent)
- Tests for both fixes (preview URL normalization edge cases + disabled
forwarding to trigger and refresh button)
Implements the master plan from PR #11955 on top of PR #11310:
- Phase 1: Add @tanstack/vue-query, wire VueQueryPlugin in main.ts with
bounded gcTime + retry policy. Module-level singleton via
getAppQueryClient() so non-Vue contexts (legacy useRemoteWidget) can
reuse the same cache.
- Phase 2: Move pure helpers to base/remote/ — itemSchema.ts (getByPath,
resolveLabel, mapToDropdownItem, extractItems, buildSearchText),
retry.ts (getBackoff, isRetriableError), diagnostics.ts (summarizeError,
summarizePayload). Delete fetchRemoteRoute (auth headers now injected
inline in useRemoteOptions per existing API-client pattern).
- Phase 3: platform/remote/composables/useRemoteOptions.ts wraps
TanStack Query with a typed RequestDescriptor and a key factory keyed
by client/route/params/{userId, workspaceId} for defense-in-depth
partitioning (auth-teardown invariant covers the cache lifecycle).
- Phase 4: RemoteCombo/ atom family (Root/Trigger/Content/Search/List/
Item/Empty/Loading/Error/Refresh/LayoutSwitcher) over reka-ui's
Combobox primitives. CVA variants in remoteCombo.variants.ts mirror
Button.vue conventions (size/variant/border axes). Reka data-attr
styling for hover/highlighted/checked. Adapter pattern for spec→prop
extraction (specAdapter.ts + comboAdapter.ts).
- Phase 5: useRemoteCombo (view layer: schema mapping, search index,
auto_select). useRemoteWidget rewritten on getAppQueryClient() —
preserves the IWidget mutation contract: first-load defaulting,
control_after_refresh override, execution_success auto-refresh toggle.
- Phase 6: zComboInputOptionsValidated enforces remote XOR remote_combo
to match backend XOR validation.
- Phase 7: Tests for new modules (comboAdapter, useRemoteOptions key
factory, RichComboWidget atom-level flows, fast-check property test
on mapToDropdownItem, XOR schema validation). Pure-helper tests
relocated to base/remote/.
- Phase 8: A11y minimums on every atom (aria-label/aria-live/aria-busy/
aria-disabled/aria-pressed; sr-only error/empty announcements).
- Phase 9: Storybook stories (Default / Loading / Error / Empty /
WithSelection / KeyboardA11y) for the atom family. Token-aligned to
the design system per master plan §11.2.b.
Removed:
- src/renderer/extensions/vueNodes/widgets/utils/fetchRemoteRoute.{ts,test.ts}
- src/renderer/extensions/vueNodes/widgets/utils/itemSchemaUtils.{ts,test.ts}
- src/renderer/extensions/vueNodes/widgets/utils/richComboHelpers.{ts,test.ts}
- The auth-scoped Cache API persistence layer in RichComboWidget.vue
- The legacy in-memory cacheEntry map in useRemoteWidget.ts
Quality gates: pnpm lint, typecheck, knip, build, test:unit (754 files,
10053 tests) all pass.
*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>
## 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)
Adds a Vue-native renderer for combo inputs that declare `remote_combo=`
(RemoteComboOptions on the backend). Wired through WidgetSelect; runs in
parallel to the existing useRemoteWidget composable, which continues to
handle plain `remote=` combos.
The widget fetches a single items array from a relative `/proxy/...`
route — the frontend always prepends the comfy-api base URL and injects
auth headers (no opt-out flag while the feature is partner-node-only).
Items are mapped via the per-node `item_schema`, with image/video/audio
previews, search across multiple fields, optional auto-select first/last,
and a refresh button.
Caching: browser Cache API with TTL from `refresh`, partitioned by full
auth scope (workspace / firebase uid / api-key / anon). Refresh button
sequences cache delete before refetch to avoid the fast-response race.
Logging: auth headers and response bodies are redacted from error logs.
Also adds an audio preview branch to FormDropdownMenuItem — used by the
new widget when `preview_type='audio'`.
Tests cover: single-shot fetch, error classification, retry exhaustion,
refresh, deselect, stale-id preservation, cache-key partitioning,
route resolution, item-schema mapping, and Zod relative-route
validation.
## 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>
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)
*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
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
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
Part of the `useBrushDrawing` decomposition plan (PR C). Extracts brush
size/hardness adjustment logic (Alt+drag interaction) into a dedicated
`useBrushAdjustment` composable. No runtime behavior is changed — pure
structural refactor.
## Changes
- **New** `src/composables/maskeditor/useBrushAdjustment.ts` —
encapsulates `startBrushAdjustment` and `handleBrushAdjustment`,
including dead zone filtering, dominant axis suppression, and
size/hardness clamping
- **New** `src/composables/maskeditor/useBrushAdjustment.test.ts` — unit
tests covering: no-op before start, dead zone suppression, size increase
on drag, size/hardness clamping, dominant axis lock
- **Updated** `src/composables/maskeditor/useBrushDrawing.ts` — removes
inlined adjustment state and functions, delegates to
`useBrushAdjustment(initialSettings)`
## Test Functionality
Open ComfyUI and enter the MaskEditor of any image node. On the canvas,
Alt + Right-click Drag:
- Drag Right → Increase brush size - pass
- Drag Left → Decrease brush size - pass
- Drag Up → Increase hardness - pass
- Drag Down → Decrease hardness - pass
https://github.com/user-attachments/assets/273e8383-dab5-4c82-ac7b-0a1534dfd770
<!-- CURSOR_SUMMARY -->
---
> [!NOTE]
> **Medium Risk**
> Touches core pointer-interaction logic for brush tuning and changes
adjustment behavior (removes delta saturation and uses initial values),
which could subtly affect UX even though scope is localized to the mask
editor.
>
> **Overview**
> Extracts the Alt-drag brush size/hardness adjustment logic out of
`useBrushDrawing` into a new `useBrushAdjustment` composable, and wires
`useBrushDrawing` to delegate to it.
>
> The extracted logic now bases adjustments off the captured initial
brush size/hardness and removes prior delta capping (no ±100px
saturation), which changes how large/continuous drags affect the final
values. Adds a Vitest suite covering dead-zone behavior, dominant-axis
suppression, clamping, and the no-op-before-start contract.
>
> <sup>Reviewed by [Cursor Bugbot](https://cursor.com/bugbot) for commit
b2f0ce22d3. 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-11544-refactor-extract-useBrushAdjustment-from-useBrushDrawing-34a6d73d365081a48897dd77b04ef56b)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Add `name` attributes to the signup form's email, password, and
confirm-password inputs so Chrome's password manager recognizes the form
and offers autofill/save.
## Changes
- **What**: Pass `name` through to the underlying `<input>` on the email
field (via `pt:root:name`) and on both password fields (via
`pt:pc-input-text:root:name`). Without `name`, Chrome can't pair the
email with the password and won't surface the save-password /
suggest-strong-password prompts.
## Review Focus
- The PrimeVue passthrough syntax (`pt:root:*` for `InputText`,
`pt:pc-input-text:root:*` for `Password`) lands the attribute on the
actual `<input>` element — verified in DevTools.
- `confirm-password` is not a standard `autocomplete` token; we keep
`autocomplete="new-password"` on both password fields and only
differentiate via `name`.
## Screenshots (if applicable)
https://github.com/user-attachments/assets/e1e25ab5-8496-4c84-b5f1-630f31956c80
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11636-fix-enable-Chrome-password-autofill-on-signup-form-34e6d73d36508180882cc9ebafb58417)
by [Unito](https://www.unito.io)
## Summary
- Tighten the new `useCoreCommands` help command tests to assert the
exact external URL opened for GitHub issues and Discord.
## Testing
```bash
pnpm test:unit -- src/composables/useCoreCommands.test.ts
pnpm format:check src/composables/useCoreCommands.test.ts
```
Also passed pre-commit `pnpm typecheck` and push hook `pnpm knip`.
Stacked on #11748.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11768-test-assert-core-command-help-urls-3516d73d365081de99d0c71f707d0fb4)
by [Unito](https://www.unito.io)
---------
Co-authored-by: dante01yoon <bunggl@naver.com>
## Summary
Add 35 unit tests for `SceneManager`, the largest remaining gap in the
load3d core (452 LOC). Targets only the logic-bearing methods
(background mode dispatch, render-mode switching, aspect-ratio scaling,
capture pipeline, dispose). Renderer-passthrough internals are
intentionally left to E2E. Follow-up to Tier 1 (#11733), Tier 2, Tier
3a, and Tier 3b.
## Changes
- **What**: 35 new tests covering construction (main scene + background
scene + grid + tiled mesh + default color mode), `toggleGrid`,
`setBackgroundColor` (color update, scene-bg cleanup, panorama-demote,
prior-texture dispose), `setBackgroundImage` (empty-path fallback,
loading-start emit, temp/output subfolder rewrite, /api prefix,
tiled-mesh material swap, panorama scene-background promotion,
prior-texture dispose, error-path fallback), `removeBackgroundImage`,
`setBackgroundRenderMode` (no-op same-mode, color-only emit, image
panorama-promote, image tiled-demote), `updateBackgroundSize`
(no-texture/no-mesh/no-map guards, wide vs. tall image scaling),
`handleResize` (image-bg active vs. color-only),
`getCurrentBackgroundInfo`, `captureScene` (returns 3 data URLs +
restores renderer state, restores grid visibility, propagates errors),
and `dispose` (resource cleanup + scene-background null).
## Review Focus
- **Coverage**: `SceneManager.ts` 89.5% lines / 74.2% branches / 89.5%
funcs. Uncovered lines are concentrated in `renderBackground` and the
deep mesh-traversal loop inside `captureScene` — exactly the
renderer-passthrough territory deferred per the Tier 3c plan.
- **`THREE.Material.needsUpdate` is a write-only setter** in THREE.js —
reading returns `undefined`. The "demote panorama → tiled" test asserts
`mat.map === texture` instead of `mat.needsUpdate === true`, with a
comment explaining why.
- **happy-dom canvas `clientWidth`/`clientHeight` default to 0** —
`makeRenderer()` overrides them via `Object.defineProperty` so
production code reading `renderer.domElement.clientWidth` gets the test
value.
- **`THREE.TextureLoader` is mocked via `vi.mock('three', ...)` with
`importOriginal`**, matching the pattern in `RecordingManager.test.ts`
and `HDRIManager.test.ts`. `mockTextureLoad` is hoisted so each test can
resolve/reject the load callback independently.
- **`vi.spyOn(manager, 'setBackgroundColor')` in three places** to
assert internal delegation (empty-path fallback, error fallback,
`removeBackgroundImage`). Defensible because the delegation IS the
documented contract for these methods.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11762-test-load3d-add-unit-tests-for-SceneManager-3516d73d365081628ff4c146defebac1)
by [Unito](https://www.unito.io)
## Summary
Add 19 unit tests for `Load3dService.copyLoad3dState` (the one method
intentionally deferred from Tier 1). Brings `load3dService.ts` from
54.5% to 100% line coverage. Follow-up to Tier 1 (#11733), Tier 2, and
Tier 3a.
## Changes
- **What**: 19 new tests covering every branch of `copyLoad3dState`:
no-source-model fast path, splat fast path (with and without
`originalURL`), mesh path (existing-target-model removal, SkeletonUtils
clone, originalModel/material/upDirection/texture copy, initial
transform on clone, gizmo transform application, gizmo enable/disable
across both source and target prior states, animation copy when
present/absent), background-image vs. background-color dispatch,
light-intensity falsy fallback, perspective-vs-orthographic FOV gating,
and the always-detach + setupForModel gizmo contract.
## Review Focus
- **Coverage**: `load3dService.ts` lines 54.5% → **100%**, branches 50%
→ **90.9%**, funcs 88.9% → **100%**. Remaining uncovered lines are minor
(`loadSkeletonUtils` cache-hit path, a couple of null-map early
returns).
- **Test fixtures use real `THREE.Object3D` and `THREE.Scene`** so
production code's `.position.set(...)`, `.rotation.set(...)`,
`scene.add/remove` calls work without further stubbing.
- **`makeTarget` memoizes the gizmo manager** (`getGizmoManager: () =>
gizmoManager` rather than returning a fresh literal each call).
Production code calls `getGizmoManager()` multiple times; without
memoization, the `detach` and `setupForModel` mocks would be
unobservable from tests.
- **`state` return on `makeTarget`** exposes mutable `modelManager`,
captured `gizmoManager`/`animationManager`, and
`sceneAdded`/`sceneRemoved` arrays so tests can assert post-state
directly without casts through the production-typed `Load3d` interface.
- **Background-image test uses `createMockLGraphNode({ id, properties
})` overrides** rather than mid-test property mutation.
- **Destructuring-default gotcha**: `const { lightsIntensity = 0.8 } =
overrides` applies the default even when `undefined` is passed
explicitly. The "fallback to setLightIntensity(1)" test passes `0`
instead — production code's `intensity || 1` short-circuits the same
way.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11761-test-load3d-add-unit-tests-for-copyLoad3dState-in-load3dService-3516d73d36508142bc72d97b27b0a36b)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for the three remaining small/wrapper modules in the
load3d domain (`EventManager` pub/sub, `ViewHelperManager` ViewHelper
wrapper, and `load3dLazy` lazy-extension loader). Follow-up to Tier 1
(#11733) and Tier 2.
## Changes
- **What**: 28 new unit tests across 3 files covering EventManager
add/remove/emit semantics, ViewHelperManager container DOM creation +
pointer-event interception + animation-finished camera-state emission +
perspective/orthographic zoom snapshotting + dispose ordering, and
load3dLazy extension registration + 3D-node-type recognition +
Load3D-specific `mesh_upload` injection + `beforeRegisterNodeDef` hook
replay for newly registered extensions.
## Review Focus
- **Coverage** (lines/branches/funcs): EventManager 100% / 100% / 100%,
ViewHelperManager 100% / 83.3% / 72.7%, load3dLazy 95.8% / 80% / 100%.
Aggregate: **98.6% / 85.3% / 85.7%**.
- **`vi.mock` factory side-effects only fire once per test file** —
`load3dLazy.test.ts` originally tried to count dynamic imports of
`./load3d` and `./saveMesh` via spies inside the mock factory, but
factories aren't re-invoked across `vi.resetModules()`. Switched to
verifying observable side effects (`enabledExtensions` getter call
counts, `beforeRegisterNodeDef` replay invocations).
- **Snapshot-vs-diff `enabledExtensions` queue** in
`load3dLazy.test.ts`: production code does `before = new
Set(enabledExtensions); await imports; diff =
enabledExtensions.filter(!before.has)`. To exercise the replay branch,
the mock returns `[]` first (for `before`) and `[newExtension]` second
(for the post-import snapshot) via `mockReturnValueOnce` queueing.
- **`MockViewHelper` is defined inside the `vi.mock()` factory** rather
than `vi.hoisted()`, matching the `GizmoManager.test.ts:15-41`
convention (hoisted handles only, classes inside the factory).
- **PointerEvent propagation tests** require the production code's
`event.stopPropagation()` to actually keep events from bubbling to the
parent in happy-dom; the parent listener gets attached and
asserted-not-called.
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11760-test-load3d-add-unit-tests-for-EventManager-ViewHelperManager-and-load3dLazy-3516d73d3650814bb2dac3360ab0d2a1)
by [Unito](https://www.unito.io)
## Summary
Add unit tests for the four Tier 2 untested logic modules in the load3d
domain (`LightingManager`, `ControlsManager`, `exportMenuHelper`, and
`ModelExporter`). Follow-up to the Tier 1 PR (#11733).
## Changes
- **What**: 43 new unit tests across 4 files covering light
setup/intensity scaling/HDRI mode/disposal, OrbitControls construction
(including DOM-parent fallback) and camera-state event emission, the
export submenu builder (item structure, submenu opening, format
dispatch, success/error toasts), and the static `ModelExporter` (URL
parsing, direct-URL fast paths for matching extensions, GLB/OBJ/STL
serialization branches, error toast paths).
## Review Focus
- **Coverage** (lines/branches/funcs): LightingManager 100% / 50% /
90.9%, ControlsManager 100% / 100% / 87.5%, exportMenuHelper 100% / 100%
/ 100%, ModelExporter 98.4% / 95.7% / 100%. Aggregate: **99.2% / 93.5% /
95.1%**.
- **`vi.mock(import('@/lib/litegraph/src/litegraph'), ...)` in
`exportMenuHelper.test.ts`** uses the dynamic-import form so
`importOriginal()` is auto-typed. Required because `apiSchema.ts`
transitively imports `LinkMarkerShape` from the same module — replacing
the whole module breaks the build. The mock replaces only
`LiteGraph.ContextMenu` in-place on the real singleton.
- **`MockContextMenu` must be a class**, not an arrow function —
production code does `new LiteGraph.ContextMenu(...)`. Initial
arrow-function mock failed with "is not a constructor".
- **Fake-timer rejection pattern in `ModelExporter.test.ts`**: rejection
assertions are attached *before* `vi.runAllTimersAsync()` (`const
assertion = expect(p).rejects.toThrow(...); await drain; await
assertion`) to avoid unhandled-rejection warnings.
- **Surprising `detectFormatFromURL` behavior**:
`detectFormatFromURL('?filename=cube')` returns `'cube'`, not `null`,
because `'cube'.split('.').pop()` returns the whole basename when no dot
is present. Test documents this rather than asserting an incorrect
expectation.
- **Two unreachable lines left uncovered**: `LightingManager:65` (`?? 1`
fallback in the `setLightIntensity` multiplier lookup — every light is
registered in the map at construction, so the fallback is dead) and
`ModelExporter:21` (a `try/catch` around `URLSearchParams` whose
constructor cannot throw on the inputs the production code passes).
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11758-test-load3d-add-unit-tests-for-LightingManager-ControlsManager-exportMenuHelper-and-3516d73d365081cb96fff33672503822)
by [Unito](https://www.unito.io)
## Summary
Adds 8 tests across three new describe blocks for
\`src/composables/useCoreCommands.ts\`:
- **Canvas view**: \`Comfy.Canvas.ResetView\`, \`Comfy.Canvas.ZoomIn\`,
\`Comfy.Canvas.ZoomOut\`.
- **Workflow lifecycle**: \`Comfy.OpenClipspace\`,
\`Comfy.RefreshNodeDefinitions\`.
- **Help**: \`Comfy.Help.OpenComfyUIIssues\`,
\`Comfy.Help.OpenComfyOrgDiscord\`, \`Comfy.Help.AboutComfyUI\`.
Adds \`vi.hoisted\` mocks for \`useTelemetry\`, \`useSettingsDialog\`,
and \`useLitegraphService.resetView\` so they remain isolated from the
existing 15-test suite.
## Why this slice
\`useCoreCommands.ts\` exports 118 distinct command callbacks (1356
LOC). A single coverage-backfill PR for the whole file would be unwieldy
and risk merge conflicts (this file is touched frequently). This PR
covers a coherent slice — view/lifecycle/help commands — and follow-up
PRs can pick off remaining clusters.
## Testing
\`\`\`bash
pnpm vitest run src/composables/useCoreCommands.test.ts
\`\`\`
┆Issue is synchronized with this [Notion
page](https://app.notion.com/p/PR-11748-test-add-unit-tests-for-useCoreCommands-canvas-help-commands-3516d73d365081c384ffcc72c15dfd47)
by [Unito](https://www.unito.io)
When in app mode, widgets can be drawn with size different from the size
of the parent node. Mouse events on legacy canvas widgets require that
the client code query the current state of the node and widget to
determine if any elements are being interacted with. This PR sets the
`widget.width` property when a legacy canvas widget draw operation
occurs so that custom nodes can properly resolve subsequent mouse
events.
At current, no core nodes exist that utilize legacy widgets. As a result
the setup code to test this bug fix is slightly involved.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-11574-Fix-legacy-widget-width-in-app-mode-34b6d73d365081caaa34c6204f8361f6)
by [Unito](https://www.unito.io)