## Summary
Fix regression from #10247 where template workflows (e.g. LTX2.3) loaded
with a broken viewport.
## Problem
`restoreViewport()` called `fitView()` on every cache miss via rAF. This
raced with `loadGraphData`'s own viewport restore (`extra.ds` for saved
workflows, or its own `fitView()` for templates at line 1266 of app.ts).
The second `fitView()` overwrote the correct viewport, causing templates
with subgraphs to display incorrectly.
## Fix
On cache miss, check if any nodes are already visible in the current
viewport before calling `fitView()`. If `loadGraphData` already
positioned things correctly, we don't override it. Only intervene when
the viewport is genuinely empty (first visit to a subgraph with no prior
cached state AND no loadGraphData restore).
## Review Focus
Single-file change in `subgraphNavigationStore.ts`. The visibility check
mirrors the same pattern used in `app.ts:1272-1281` where loadGraphData
itself checks for visible nodes.
## E2E Regression Test
The existing Playwright tests in
`browser_tests/tests/subgraphViewport.spec.ts` (added in #10247) already
cover viewport restoration after subgraph navigation. The specific
regression (template load viewport race) is not practically testable in
E2E because:
1. Template loading requires the backend's template API which returns
different templates per environment
2. The race condition depends on exact timing between `loadGraphData`'s
viewport restore and the rAF-deferred `restoreViewport` — Playwright
cannot reliably reproduce frame-level timing races
3. The fix is a guard condition (skip fitView if nodes visible) that
makes the behavior idempotent regardless of timing
## Alternative to #10790
This can replace the full revert in #10790 — it preserves the viewport
persistence feature while fixing the template regression.
Fixes regression from #10247
# Summary
* Model Info sidebar panel displays `asset.name` (registry name) instead
of the actual filename from `user_metadata.filename`
* Other UI components (asset cards, widgets, missing model scan)
correctly use `getAssetFilename()` which prefers
`user_metadata.filename` over `asset.name`
* One-line template fix: `{{ asset.name }}` → `{{
getAssetFilename(asset) }}`
* Fixes#10598
# Bug
`ModelInfoPanel.vue:35` used raw `asset.name` for the "File Name" field.
When `user_metadata.filename` differs from `asset.name` (e.g. registry
name vs actual path like `checkpoints/v1-5-pruned.safetensors`), users
see inconsistent filenames across the UI.
# AS-IS / TO-BE
<img width="800" height="600" alt="before-after-10598"
src="https://github.com/user-attachments/assets/15beb6c8-4bad-4ed2-9c85-6f8c7c0b6d3e"
/>
| | File Name field shows |
| :--- | :--- |
| **AS-IS** (bug) | `sdxl-lightning-4step` — raw `asset.name` (registry
display name) |
| **TO-BE** (fix) | `checkpoints/sdxl_lightning_4step.safetensors` —
`getAssetFilename(asset)` (actual file path) |
# Red-Green Verification
| Commit | CI Status | Purpose |
| :--- | :--- | :--- |
| `test: add failing test for ModelInfoPanel showing wrong filename` | 🔴
Red | Proves the test catches the bug |
| `fix: use getAssetFilename in ModelInfoPanel filename field` | 🟢 Green
| Proves the fix resolves the bug |
# Test Plan
- [x] CI red on test-only commit
- [x] CI green on fix commit
- [x] Unit test: `prefers user_metadata.filename over asset.name for
filename field`
- [ ] Manual: open Asset Browser → click a model → verify File Name in
Model Info panel matches the actual file path (requires
`--enable-assets`)
## Summary
Fix subgraph internal node positions being permanently corrupted when
entering a subgraph after a draft workflow reload. The corruption
accumulated across page refreshes, causing nodes to progressively drift
apart or compress together.
## Changes
- **What**: In the `ResizeObserver` callback
(`useVueNodeResizeTracking.ts`), node positions are now read from the
Layout Store (source of truth, initialized from LiteGraph) instead of
reverse-converting DOM screen coordinates via `getBoundingClientRect()`
+ `clientPosToCanvasPos()`. The fallback to DOM-based conversion is
retained only for nodes not yet present in the Layout Store.
## Root Cause
`ResizeObserver` was using `getBoundingClientRect()` to get DOM element
positions, then converting them to canvas coordinates via
`clientPosToCanvasPos()`. This conversion depends on the current
`canvas.ds.scale` and `canvas.ds.offset`.
During graph transitions (e.g., entering a subgraph from a draft-loaded
workflow), the canvas viewport was stale — it still had the **parent
graph's zoom level** because `fitView()` hadn't run yet (it's scheduled
via `requestAnimationFrame`). The `ResizeObserver` callback fired before
`fitView`, converting DOM positions using the wrong scale/offset, and
writing the corrupted positions to the Layout Store. The `useLayoutSync`
writeback then permanently overwrote the LiteGraph node positions.
The corruption accumulated across sessions:
1. Load workflow → enter subgraph → `ResizeObserver` writes corrupted
positions
2. Draft auto-saves the corrupted positions to localStorage
3. Page refresh → draft loads with corrupted positions → enter subgraph
→ positions corrupted further
4. Each cycle amplifies the drift based on the parent graph's zoom level
This is the same class of bug that PR #9121 fixed for **slot** positions
— the DOM→canvas coordinate conversion is inherently fragile during
viewport transitions. This PR applies the same principle to **node**
positions.
## Why This Only Affects `main` (No Backport Needed)
This bug requires two features that only exist on `main`, not on
`core/1.41` or `core/1.42`:
1. **PR #10247** changed `subgraphNavigationStore`'s watcher to `flush:
'sync'` and added `requestAnimationFrame(fitView)` on viewport cache
miss. This creates the timing window where `ResizeObserver` fires before
`fitView` corrects the canvas scale.
2. **PR #6811** added hash-based subgraph auto-entry on page load, which
triggers graph transitions during the draft reload flow.
On 1.41/1.42, `restoreViewport` does nothing on cache miss (no `fitView`
scheduling), and the watcher uses default async flush — so the
`ResizeObserver` never runs with a stale viewport.
## Review Focus
- The core change is small: use `nodeLayout.position` (already in the
Layout Store from `initializeFromLiteGraph`) instead of computing
position from `getBoundingClientRect()`. This eliminates the dependency
on canvas scale/offset being up-to-date during `ResizeObserver`
callbacks.
- The fallback path (`getBoundingClientRect` → `clientPosToCanvasPos`)
is retained for nodes not yet in the Layout Store (e.g., first render of
a newly created node). At that point the canvas transform is stable, so
the conversion is safe.
- Unit tests updated to reflect that position is no longer overwritten
from DOM when Layout Store already has the position.
- E2E test added: load subgraph workflow → enter subgraph → reload
(draft) → verify positions preserved.
## E2E Test Fixes
- `subgraphDraftPositions.spec.ts`: replaced `comfyPage.setup({
clearStorage: false })` with `page.reload()` + explicit draft
persistence polling. The `setup()` method performs a full navigation via
`goto()` which bypassed the draft auto-load flow.
- `SubgraphHelper.packAllInteriorNodes`: replaced `canvas.click()` with
`dispatchEvent('pointerdown'/'pointerup')`. The position fix places
subgraph nodes at their correct locations, which now overlap with DOM
widget textareas that intercept pointer events.
## Test Plan
- [x] Unit tests pass (`useVueNodeResizeTracking.test.ts`)
- [x] E2E test: `subgraphDraftPositions.spec.ts` — draft reload
preserves subgraph node positions
- [x] Manual: load workflow with subgraph, zoom in/out on root graph,
enter subgraph, verify no position drift
- [x] Manual: repeat with page refresh (draft reload) — positions should
be stable across reloads
- [x] Manual: drag nodes inside subgraph — positions should update
correctly
- [x] Manual: create new node inside subgraph — position should be set
correctly (fallback path)
## Screenshots
Before
<img width="1331" height="879" alt="스크린샷 2026-04-03 오전 3 56 48"
src="https://github.com/user-attachments/assets/377d1b2e-6d47-4884-8181-920e22fa6541"
/>
After
<img width="1282" height="715" alt="스크린샷 2026-04-03 오전 3 58 24"
src="https://github.com/user-attachments/assets/34528f6c-0225-4538-9383-227c849bccad"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10828-fix-prevent-subgraph-node-position-corruption-during-graph-transitions-3366d73d365081418502dbb78da54013)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Add dedicated auth token priority tests and reconcile workspace token
resolution between `authStore` and `workspaceAuthStore`.
## Changes
- **What**: Created `src/stores/__tests__/authTokenPriority.test.ts`
with 6 scenarios covering the full priority chain: workspace token →
Firebase token → API key → null. Added `getWorkspaceToken()` method to
`workspaceAuthStore`. Replaced raw `sessionStorage` reads in `authStore`
with proper store delegation.
- **Files**: 3 files changed (+ 1 new test file)
## Review Focus
- Token priority chain in `authTokenPriority.test.ts` — validates
workspace > Firebase > API key ordering
- The `getAuthToken()` and `getAuthHeader()` methods now delegate to
`workspaceAuthStore` instead of reading sessionStorage directly
## Stack
PR 3/5: #10483 → #10484 → **→ This PR** → #10486 → #10487
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Merge email dataLayer push event into existing signup/login push. Hash
with SHA 256 and deduplicate with a util.
AI summary below
---
## Summary
- attach hashed auth email to the `login` / `sign_up` GTM event payload
instead of pushing a separate standalone `user_data` message
- add a shared telemetry email hashing utility and reuse it for both GTM
(`SHA-256`) and Impact (`SHA-1`)
- extend tests to cover merged auth payloads, both hash algorithms, and
fallback behavior when Web Crypto is unavailable
## Why
The current GTM auth flow was splitting `user_data` and the auth event
into separate dataLayer pushes, which makes GTM preview harder to
interpret and relies on carried-over dataLayer state instead of the
event payload that actually fired.
There was also an implementation gap: the previous GTM change was
intended to hash email client-side, but the provider was still sending
normalized plaintext email into `dataLayer`.
## Impact
Auth events now carry hashed email on the same event payload that GTM
tags consume, so the preview timeline is cleaner and downstream matching
can rely on the auth event directly.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10778-codex-merge-hashed-auth-user-data-into-GTM-auth-events-3346d73d36508197b57deada345dbeaa)
by [Unito](https://www.unito.io)
## Summary
Virtualize the shared job queue history list so opening the jobs panel
does not eagerly mount the full history on cloud.
## Changes
- **What**: Virtualize the shared queue history list used by the overlay
and sidebar, flatten date headers plus job rows into a single virtual
stream, and preserve hover/menu behavior with updated queue list tests.
- **Why `@tanstack/vue-virtual` instead of Reka virtualizers**: the
installed `reka-ui@2.5.0` does not expose a generic list virtualizer. It
only exposes `ListboxVirtualizer`, `ComboboxVirtualizer`, and
`TreeVirtualizer`, and those components inject `ListboxRoot`/`TreeRoot`
context and carry listbox or tree selection/keyboard semantics. The job
history UI is a flat grouped action list, not a selectable listbox or
navigable tree, so this uses the same TanStack virtualizer layer
directly without forcing the wrong semantics onto the component.
## Review Focus
Please verify the virtual row sizing and inter-group spacing behavior
across date headers and the last row in each group.
> [!TIP]
> Diff reads much cleaner through vscode's unified view with show
leading/trailing whitespace differences enabled
Linear: COM-304
https://tanstack.com/virtual/latest/docs/api/virtualizer
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10592-fix-virtualize-cloud-job-queue-history-list-3306d73d3650819d956bf4b2d8b59a40)
by [Unito](https://www.unito.io)
## Summary
Add missing delete and bookmark actions for user blueprints in the V2
node library sidebar, fixing parity with the V1 sidebar.
## Changes
- **What**:
- Add delete button (inline + context menu) for user blueprints in
`TreeExplorerV2Node` and `TreeExplorerV2`
- Extract `isUserBlueprint()` helper in `subgraphStore` for DRY usage
across V1/V2 sidebars

## Review Focus
- `isUserBlueprint` consolidates logic previously duplicated between
`NodeTreeLeaf` and the new V2 components
- Context menu guard `contextMenuNode?.data` prevents showing empty
menus
- Folder `@contextmenu` handler clears stale `contextMenuNode` to
prevent wrong actions
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10827-fix-add-delete-bookmark-actions-for-blueprints-in-V2-node-library-sidebar-3366d73d36508111afd2c2c7d8ff0220)
by [Unito](https://www.unito.io)
## Summary
- Replace raw CSS selectors (`.lg-node-header`, `.p-contextmenu`,
`.node-title-editor input`, `.image-preview img`) with centralized
`TestIds` constants and existing fixtures in the context menu E2E spec
- Add `data-testid="title-editor-input"` to TitleEditor overlay for
stable selector targeting
- Use `NodeLibrarySidebarTab` fixture for node library sidebar
interaction
## Changes
- `browser_tests/fixtures/selectors.ts`: add `pinIndicator`,
`innerWrapper`, `titleEditorInput`, `mainImage` to `TestIds.node`
- `browser_tests/fixtures/utils/vueNodeFixtures.ts`: add `pinIndicator`
getter
- `src/components/graph/TitleEditor.vue`: add `data-testid` via
`input-attrs`
- `browser_tests/.../contextMenu.spec.ts`: replace all raw selectors
with TestIds/fixtures
## Test plan
- [x] All 23 context menu E2E tests pass locally
- [x] Typecheck passes
- [x] Lint passes
Fixes#10750Fixes#10749
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10760-test-replace-raw-CSS-selectors-with-TestIds-in-context-menu-spec-3336d73d3650818790c0e32e0b6f1e98)
by [Unito](https://www.unito.io)
## Summary
Fix four bugs in the subgraph promoted widget panel where linked
promotions were not distinguished from independent ones, causing
incorrect UI state in both the SubgraphEditor (Settings) panel and the
Parameters tab WidgetActions menu.
## Changes
- **What**: Add `isLinkedPromotion` helper to correctly identify widgets
driven by subgraph input connections. Fix `disambiguatingSourceNodeId`
lookup mismatch that broke `isWidgetShownOnParents` and
`handleHideInput` for non-nested promoted widgets. Replace fragile CSS
icon selectors with `data-testid` attributes.
## Bugs fixed
Companion fix PR for #10502 (red-green test PR). All 4 E2E tests from
#10502 now pass:
| Bug | Root cause | Fix |
|-----|-----------|-----|
| Linked promoted widgets have hide toggle enabled | `SubgraphEditor`
only checked `node.id === -1` (physical) — linked promotions from
subgraph input connections were not detected | Added `isLinkedPromotion`
helper that checks `input._widget` bindings; `SubgraphNodeWidget`
`:is-physical` prop now covers both physical and linked cases |
| Linked promoted widgets show eye icon instead of link icon | Same root
cause as above — `isPhysical` prop was only true for `node.id === -1` |
Extended the `:is-physical` condition to include `isLinkedPromotion`
check |
| Widget labels show raw names instead of renamed values |
`SubgraphEditor` passed `widget.name` instead of `widget.label \|\|
widget.name` | Changed `:widget-name` binding to prefer `widget.label` |
| WidgetActions menu shows Hide/Show for linked promotions |
`v-if="hasParents"` didn't exclude linked promotions | Added
`canToggleVisibility` computed that combines `hasParents` with
`!isLinked` check via `isLinkedPromotion` |
### Additional bugs discovered and fixed
| Bug | Root cause | Fix |
|-----|-----------|-----|
| "Show input" always displayed instead of "Hide input" for promoted
widgets | `SectionWidgets.isWidgetShownOnParents` used
`getSourceNodeId(widget)` which falls back to `widget.sourceNodeId` when
`disambiguatingSourceNodeId` is undefined — this mismatches the
promotion store key (`undefined`) | Changed to
`widget.disambiguatingSourceNodeId` directly |
| "Hide input" click does nothing | `WidgetActions.handleHideInput` used
`getSourceNodeId(widget)` for the same reason — `demote()` couldn't find
the entry to remove | Same fix — use `widget.disambiguatingSourceNodeId`
directly |
## Tests added
### E2E (Playwright) —
`browser_tests/tests/subgraphPromotedWidgetPanel.spec.ts`
| Test | What it verifies |
|------|-----------------|
| linked promoted widgets have hide toggle disabled | All toggle buttons
in SubgraphEditor shown section are disabled for linked widgets (covers
1-level and 2-level nested promotions via `subgraph-nested-promotion`
fixture) |
| linked promoted widgets show link icon instead of eye icon | Link
icons appear for linked widgets, no eye icons present |
| widget labels display renamed values instead of raw names |
`widget.label` is displayed when set, not `widget.name` |
| linked promoted widget menu should not show Hide/Show input |
Three-dot menu on Parameters tab omits Hide/Show options for linked
promotions, Rename is still available |
### Unit (Vitest) — `src/core/graph/subgraph/promotionUtils.test.ts`
7 tests covering `isLinkedPromotion`: basic matching, negative cases,
nested subgraph with `disambiguatingSourceNodeId`, multiple inputs, and
mixed linked/independent state.
### Unit (Vitest) —
`src/components/rightSidePanel/parameters/WidgetActions.test.ts`
- Added `isSubgraphNode: () => false` to mock nodes to prevent crash
from new `isLinked` computed
## Review Focus
- `isLinkedPromotion` reads `input._widget` (WeakRef-backed,
non-reactive) directly in the template. This is intentional — `_widget`
bindings are set during subgraph initialization before the user opens
the panel, so stale reads don't occur in practice. A computed-based
approach was attempted but reverted because `_widget` changes cannot
trigger Vue reactivity.
- `getSourceNodeId` removal in `SectionWidgets` and `WidgetActions` is
intentional — the old fallback (`?? widget.sourceNodeId`) caused key
mismatches with the promotion store for non-nested widgets.
## Screenshots
Before
<img width="723" height="935" alt="image"
src="https://github.com/user-attachments/assets/09862578-a0d1-45b4-929c-f22f7494ebe2"
/>
After
<img width="999" height="952" alt="image"
src="https://github.com/user-attachments/assets/ed8fe604-6b44-46b9-a315-6da31d6b405a"
/>
## Summary
Fix connection links rendering at wrong positions when nodes are
collapsed in Vue nodes mode.
## Changes
- **What**: Fall back to `clientPosToCanvasPos` for collapsed node slot
positioning since DOM-relative scale derivation is invalid when layout
store preserves expanded size. Clear stale `cachedOffset` on collapse
and defer sync when canvas is not yet initialized.
- 3 unit tests for collapsed node slot sync fallback
(clientPosToCanvasPos, cachedOffset clearing, canvas-not-initialized
deferral)
- 3 E2E tests for collapsed node link positions (within bounds, after
position change, after expand recovery)
## Review Focus
- `clientPosToCanvasPos` fallback is safe for collapsed nodes because
collapse is user-initiated (no loading-time transform desync risk that
#9121 originally fixed)
- `cachedOffset` clearing prevents stale expanded-state offsets during
collapsed node drag
- Regression from #9121 (DOM-relative scale) combined with #9680
(collapsed node ResizeObserver skip)
## Screenshots
Before
<img width="1030" height="434" alt="image"
src="https://github.com/user-attachments/assets/2f8b8a1f-ed22-4588-ab62-72b89880e53f"
/>
After
<img width="1029" height="476" alt="image"
src="https://github.com/user-attachments/assets/52dbbf7c-61ed-465b-ae19-a9781513e7e8"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10641-fix-collapsed-node-connection-link-positions-3316d73d365081f4aee3fecb92c83b91)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Alexander Brown <DrJKL0424@gmail.com>
## Summary
- Convert `fromAny` → `fromPartial` in 7 test files where object
literals or interfaces are passed
- `fromPartial` type-checks the provided fields, unlike `fromAny` which
bypasses all checking (same as `as unknown as`)
- Class-based types (`LGraphNode`, `LGraph`) remain `fromAny` due to
shoehorn's `PartialDeep` incompatibility with class constructors
## Changes
- **Pure conversions** (all `fromAny` → `fromPartial`):
`domWidgetZIndex`, `matchPromotedInput`, `promotionUtils`,
`subgraphNavigationStore`
- **Mixed** (some converted, some kept): `promotedWidgetView`,
`widgetUtil`
- **Cleanup**: `nodeOutputStore` type param normalization
Follows up on #10761.
## Test plan
- [x] `pnpm typecheck` passes
- [x] `pnpm vitest run` on all 7 changed files — 169 tests pass
- [x] `pnpm lint` passes
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10788-test-migrate-fromAny-to-fromPartial-for-type-checked-test-mocks-3356d73d365081f7bf61d48a47af530c)
by [Unito](https://www.unito.io)
*PR Created by the Glary-Bot Agent*
---
## Summary
- Replace all `as unknown as Type` assertions in 59 unit test files with
type-safe `@total-typescript/shoehorn` functions
- Use `fromPartial<Type>()` for partial mock objects where deep-partial
type-checks (21 files)
- Use `fromAny<Type>()` for fundamentally incompatible types: null,
undefined, primitives, variables, class expressions, and mocks with
test-specific extra properties that `PartialDeepObject` rejects
(remaining files)
- All explicit type parameters preserved so TypeScript return types are
correct
- Browser test `.spec.ts` files excluded (shoehorn unavailable in
`page.evaluate` browser context)
## Verification
- `pnpm typecheck` ✅
- `pnpm lint` ✅
- `pnpm format` ✅
- Pre-commit hooks passed (format + oxlint + eslint + typecheck)
- Migrated test files verified passing (ran representative subset)
- No test behavior changes — only type assertion syntax changed
- No UI changes — screenshots not applicable
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10761-test-migrate-as-unknown-as-to-total-typescript-shoehorn-3336d73d365081f6b8adc44db5dcc380)
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
- Closing an inactive workflow tab and clicking "Save" overwrites that
workflow with the **active** tab's content, causing permanent data loss
- `saveWorkflow()` and `saveWorkflowAs()` call `checkState()` which
serializes `app.rootGraph` (the active canvas) into the inactive
workflow's `changeTracker.activeState`
- Guard `checkState()` to only run when the workflow being saved is the
active one — in both `saveWorkflow` and `saveWorkflowAs`
## Linked Issues
- Fixes https://github.com/Comfy-Org/ComfyUI/issues/13230
## Root Cause
PR #9137 (commit `9fb93a5b0`, v1.41.7) added
`workflow.changeTracker?.checkState()` inside `saveWorkflow()` and
`saveWorkflowAs()`. `checkState()` always serializes `app.rootGraph` —
the graph on the canvas. When called on an inactive tab's change
tracker, it captures the active tab's data instead.
## Test plan
- [x] E2E: "Closing an inactive tab with save preserves its own content"
— persisted workflow B with added node, close while A is active, re-open
and verify
- [x] E2E: "Closing an inactive unsaved tab with save preserves its own
content" — temporary workflow B with added node, close while A is
active, save-as with filename, re-open and verify
- [x] Manual: open A and B, edit B, switch to A, close B tab, click
Save, re-open B — content should be B's not A's
## Summary
replacement for https://github.com/Comfy-Org/ComfyUI_frontend/pull/9201
the first commit squashed
https://github.com/Comfy-Org/ComfyUI_frontend/pull/9201 and fixed
conflict.
the second commit change needed by:
- Enable GLSL live preview on SubgraphNodes by detecting the inner
GLSLShader and rendering its preview directly on the parent SubgraphNode
- Previously, SubgraphNodes containing a GLSLShader showed no live
preview at all To achieve this:
- Read shader source, uniform values, and renderer config from the inner
GLSLShader's widgets
- Trace IMAGE inputs through the subgraph boundary so the inner shader
can use images connected to the SubgraphNode's outer inputs
- Set preview output using the inner node's locator ID so the promoted
preview system picks it up on the SubgraphNode
- Extract setNodePreviewsByLocatorId from nodeOutputStore to support
setting previews by locator ID directly
- Fix graphId to use rootGraph.id for widget store lookups (was using
graph.id, which broke lookups for nodes inside subgraphs)
- Read uniform values from connected upstream nodes, not just local
widgets
- Fix blob URL lifecycle: use the store's
createSharedObjectUrl/releaseSharedObjectUrl reference-counting system
instead of manual revoke, preventing leaks on composable re-creation
## Screenshot
https://github.com/user-attachments/assets/9623fa32-de39-4a3a-b8b3-28688851390b
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10349-Feat-glsl-live-preview-3296d73d3650814b83aef52ab1962a77)
by [Unito](https://www.unito.io)
## Summary
Extract auth-routing logic (`getAuthHeaderOrThrow`,
`getFirebaseAuthHeaderOrThrow`) from `workspaceApi.ts` into
`authStore.ts`, eliminating a layering violation where the workspace API
re-implemented auth header resolution.
## Changes
- **What**: Moved `getAuthHeaderOrThrow` and
`getFirebaseAuthHeaderOrThrow` from `workspaceApi.ts` to `authStore.ts`.
`workspaceApi.ts` now calls through `useAuthStore()` instead of
re-implementing token resolution. Added tests for the new methods in
`authStore.test.ts`. Updated `authStoreMock.ts` with the new methods.
- **Files**: 4 files changed
## Review Focus
- The `getAuthHeaderOrThrow` / `getFirebaseAuthHeaderOrThrow` methods
throw `AuthStoreError` (auth domain error) — callers in workspace can
catch and re-wrap if needed
- `workspaceApi.ts` is simplified by ~19 lines
## Stack
PR 2/5: #10483 → **→ This PR** → #10485 → #10486 → #10487
## Summary
Expose `renderMarkdownToHtml()` on the `ExtensionManager` interface so
custom node extensions can render markdown to sanitized HTML without
bundling their own copies of `marked`/`DOMPurify`.
## Motivation
Multiple custom node packs (KJNodes, comfy_mtb, rgthree-comfy) bundle
their own markdown rendering libraries to implement help popups on
nodes. This causes:
- **Cloud breakage**: KJNodes uses a `kjweb_async` pattern (custom
aiohttp static route) to lazily load `marked.min.js` and
`purify.min.js`. This 404s on Cloud because the custom route is not
registered.
- **Redundant bundling**: Both `marked` (^15.0.11) and `dompurify`
(^3.2.5) are already direct dependencies of the frontend, used
internally by `markdownRendererUtil.ts`, `NodePreview.vue`,
`WhatsNewPopup.vue`, etc.
- **XSS risk**: Custom nodes using raw `marked` without `DOMPurify`
could introduce XSS vulnerabilities.
By exposing the existing `renderMarkdownToHtml()` through the official
`ExtensionManager` API, custom nodes can:
```js
const html = app.extensionManager.renderMarkdownToHtml(nodeData.description)
```
...instead of bundling and loading their own copies.
## Changes
- **`src/types/extensionTypes.ts`**: Add `renderMarkdownToHtml(markdown:
string, baseUrl?: string): string` to the `ExtensionManager` interface
with JSDoc.
- **`src/stores/workspaceStore.ts`**: Import and re-export
`renderMarkdownToHtml` from `@/utils/markdownRendererUtil`.
## Impact
- **Zero bundle size increase** — the function and its dependencies are
already bundled in the `vendor-markdown` chunk.
- **No breaking changes** — purely additive to the `ExtensionManager`
interface.
- **Follows existing pattern** — same approach as `toast`, `dialog`,
`command`, `setting` on `ExtensionManager`.
Related: #TBD (long-term plan for custom node extension library
dependencies)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10700-feat-expose-renderMarkdownToHtml-on-ExtensionManager-3326d73d36508149bc1dc6bb45e7c077)
by [Unito](https://www.unito.io)
## Summary
Add a deprecation warning when custom nodes access `widget.inputEl` on
STRING multiline widgets, directing them to use `widget.element`
instead.
## Changes
- **What**: Add a reusable `defineDeprecatedProperty` helper in
`feedback.ts` that creates an ODP getter/setter proxy from a deprecated
property to its replacement, logging via the existing `warnDeprecated`
utility (deduplicates: warns once per unique message per session). Use
it to deprecate `widget.inputEl` → `widget.element`.
## Review Focus
- `defineDeprecatedProperty` is generic and can be reused for future
property deprecations across the codebase.
- `warnDeprecated` already handles deduplication via a `Set`, so heavy
access patterns (e.g. custom nodes reading `widget.inputEl` in tight
loops) won't spam.
- `enumerable: false` keeps the deprecated alias out of `Object.keys()`
/ `for...in` / `JSON.stringify`.
FixesComfy-Org/ComfyUI#12893
<!-- Pipeline-Ticket: 6b291ba2-694c-42d6-ac0c-fcbdcba9373a -->
---------
Co-authored-by: Dante <bunggl@naver.com>
## What
Replace `v-if` with `v-show` on SelectionRectangle and NodeTooltip
components.
## Why
Firefox profiler shows 687 Vue `insert` markers from mount/unmount
cycling during canvas interaction. These components toggle frequently
during drag and mouse move events.
## How
- **SelectionRectangle**: `v-if` → `v-show` (single element, safe to
keep in DOM)
- **NodeTooltip**: `v-if` → `v-show` + no-op guard on `hideTooltip()` to
skip redundant reactivity triggers
## Perf Impact
Expected reduction: ~687 Vue insert/remove operations per profiling
session
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9401-fix-use-v-show-for-frequently-toggled-canvas-overlay-components-31a6d73d365081aba2d7fce079bde7e9)
by [Unito](https://www.unito.io)
## Summary
With a previously saved workflow, selecting "Save as" in app mode would
not correctly change the file extension to the chosen mode, and would
require an additional save after to persist the actual mode change.
Recreation:
- Build app
- Save as worklow X, app mode
- Select Save as from builder footer [Save | v] chevron button
- Select node graph
- Save
- Check workflow on disk - it's still called X.app.json and doesn't have
linearMode: false <-- bug
## Changes
- **What**:
- pass isApp to save workflow
- ensure active graph & initialMode are correctly set when calling
saveAs BEFORE the actual saveWorkflow call
- add linearMode to workflowShema to prevent casts
- tests
## Review Focus
e2e tests coming in a follow up PR along with some refactoring of the
browser tests (left this PR focused to the actual fix)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10679-fix-App-mode-Save-as-not-using-correct-extension-or-persisting-mode-on-change-3316d73d365081ef985cf57c91c34299)
by [Unito](https://www.unito.io)
## Summary
- Rename `text-xxxs`/`text-xxs` to `text-3xs`/`text-2xs` in design
system CSS — fixes `tailwind-merge` incorrectly classifying custom
font-size utilities as color classes, which clobbered text color
- Add `Badge` component with updated severity colors matching Figma
design (white text on colored backgrounds)
- Add Badge stories under `Components/Badges/Badge`
- Add unit tests including twMerge regression coverage
Split from #10438 per review feedback — this PR contains the
foundational Badge component; migration of consumers follows in a
separate PR.
## Test plan
- [x] Unit tests pass (`Badge.test.ts` — 12 tests)
- [x] Typecheck passes
- [x] Lint passes
- [ ] Verify Badge stories render correctly in Storybook
- [ ] Verify existing components using `text-2xs`/`text-3xs` render
unchanged
Fixes#10438 (partial)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10580-refactor-add-Badge-component-and-fix-twMerge-font-size-detection-32f6d73d3650810dae7cd0d4af67fd1c)
by [Unito](https://www.unito.io)
## Summary
Adds SHA-256 hashed user email to GTM dataLayer `sign_up` and `login`
events to improve Meta/LinkedIn Conversions API (CAPI) match rate via
Stape server-side tracking.
## Privacy
- Email is SHA-256 hashed client-side before being pushed to dataLayer —
the raw email never enters the analytics pipeline.
- Email is normalized (trimmed + lowercased) before hashing per
Google/Meta requirements.
- If email is absent (e.g., GitHub OAuth without public email), no
`user_data` entry is pushed.
## Testing
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10591-feat-add-SHA-256-hashed-email-to-GTM-dataLayer-for-sign_up-login-events-3306d73d36508148a321d62810698013)
by [Unito](https://www.unito.io)
## Summary
Add property-based tests (using `fast-check`) for asset-related pure
utility functions, complementing existing example-based unit tests with
algebraic invariant checks across thousands of randomized inputs.
Fixes#10617
## Changes
- **What**: 4 new `*.property.test.ts` files covering
`assetFilterUtils`, `assetSortUtils`, `useAssetSelection`, and
`useOutputStacks` — 32 property-based tests total
## Why property-based testing (fast-check)?
### Gap in existing tests
The existing example-based unit tests (53 tests across 3 files) verify
behavior for **hand-picked inputs** — specific category names, known
sort orderings, fixed asset lists. This leaves two blind spots:
1. **Edge-case discovery**: Example tests only cover cases the author
anticipates. Property tests generate hundreds of randomized inputs per
run, probing boundaries the author didn't consider (e.g., empty strings,
single-char names, deeply nested tag paths, assets with `undefined`
metadata fields).
2. **Algebraic invariants**: Certain guarantees should hold for **all**
inputs, not just the handful tested. For example:
- "Filtering always produces a subset" — impossible to violate with 5
examples, easy to violate in production with unexpected metadata shapes
- "Sorting is idempotent" — an unstable sort bug would only surface with
specific duplicate patterns
- "Reconciled selection IDs are always within visible assets" — a
set-intersection bug might only appear with specific overlap patterns
between selection and visible sets
3. **No test coverage for `useOutputStacks`**: The composable had zero
tests before this PR.
### What these tests verify (invariant catalog)
| Module | # Properties | Key invariants |
|--------|-------------|----------------|
| `assetFilterUtils` | 10 | Filter result ⊆ input; `"all"` is identity;
ownership partitions into disjoint my/public; empty constraint is
identity |
| `assetSortUtils` | 8 | Never mutates input; output is permutation of
input; idempotent (sort∘sort = sort); adjacent pairs satisfy comparator;
`"default"` preserves order |
| `useAssetSelection` | 7 | After reconcile: selected ⊆ visible;
reconcile never adds new IDs; superset preserves all; empty visible
clears; `getOutputCount` ≥ 1; `getTotalOutputCount` ≥ len(assets) |
| `useOutputStacks` | 7 | Collapsed count = input count; items reference
input assets; unique keys; selectableAssets length = assetItems length;
no collapsed child flags; reactive ref updates |
### Quantitative impact
Each property runs 100 iterations by default → **3,200 randomized inputs
per test run** vs 53 hand-picked examples in existing tests.
**Coverage delta** (v8, measured against target modules):
| Module | Metric | Before (53 tests) | After (+32 property) | Delta |
|--------|--------|-------------------|---------------------|-------|
| `useAssetSelection.ts` | Branch | 76.92% | 94.87% | **+17.95pp** |
| `useAssetSelection.ts` | Stmts | 82.50% | 90.00% | **+7.50pp** |
| `useAssetSelection.ts` | Lines | 81.69% | 88.73% | **+7.04pp** |
| `useOutputStacks.ts` | Stmts | 0% | 37.50% | **+37.50pp** (new) |
| `useOutputStacks.ts` | Funcs | 0% | 75.00% | **+75.00pp** (new) |
| `assetFilterUtils.ts` | All | 97.5%+ | 97.5%+ | maintained |
| `assetSortUtils.ts` | All | 100% | 100% | maintained |
### Prior art
Follows the established pattern from
`src/platform/workflow/persistence/base/draftCacheV2.property.test.ts`.
## Review Focus
- Are the chosen invariants correct and meaningful (not just
change-detector tests)?
- Are the `fc.Arbitrary` generators representative of real-world asset
data shapes?
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10619-test-assets-add-property-based-tests-for-asset-utility-functions-3306d73d3650816985ebcd611bbe0837)
by [Unito](https://www.unito.io)
<img width="1305" height="730" alt="스크린샷 2026-03-28 오전 10 17
30"
src="https://github.com/user-attachments/assets/316fcb72-e749-40da-b29f-05af91f30610"
/>
## Summary
- Replace hardcoded `COMFY_HUB_TAG_OPTIONS` with dynamic fetch from `GET
/hub/labels?type=tag`
- Falls back to the existing static tag list when the API call fails
- Adds `zHubLabelListResponse` Zod schema and `fetchTagLabels` service
method
## Test plan
- [ ] Open publish wizard → verify tag suggestions load from API
- [ ] Disconnect network / use env without hub API → verify hardcoded
fallback tags appear
- [ ] Select and deselect tags → verify behavior unchanged
- [ ] Unit tests pass (`pnpm vitest run` on affected files)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10497-feat-fetch-publish-tag-suggestions-from-hub-labels-API-32e6d73d3650815fb113cf591030d4e8)
by [Unito](https://www.unito.io)
## Summary
Fix timezone-dependent test failure in SubscriptionPanel and add a local
CI script.
## Changes
- **What**: The `renders refill date with literal slashes` test
hardcoded `12/31/24` but the component renders using local timezone
`Date` methods. In UTC-negative timezones, `2024-12-31T00:00:00Z`
renders as Dec 30. Now computes the expected string the same way the
component does.
- **What**: Added `pnpm test:ci:local` script
(`scripts/test-ci-local.sh`) that builds the frontend, starts a ComfyUI
backend with `--multi-user --front-end-root dist`, runs vitest +
Playwright, then cleans up. One command for full local CI.
## Review Focus
This is a test-only change — no production code modified. The
SubscriptionPanel component itself is unchanged; only the test assertion
is made timezone-agnostic.
## E2E Regression Test
Not applicable — this PR fixes a unit test assertion, not a production
bug. No user-facing behavior changed.
## Summary
Phase 3 of the VTL migration: migrate 8 hard-case component tests from
@vue/test-utils to @testing-library/vue (68 tests).
Stacked on #10490.
## Changes
- **What**: Migrate SignInForm, CurrentUserButton, NodeSearchBoxPopover,
BaseThumbnail, JobAssetsList, SelectionToolbox, QueueOverlayExpanded,
PackVersionSelectorPopover from VTU to VTL
- **`wrapper.vm` elimination**: 13 instances across 4 files (5 in
SignInForm, 3 in CurrentUserButton, 3 in PackVersionSelectorPopover, 2
in BaseThumbnail) replaced with user interactions or removed
- **`vm.$emit()` on stubs**: Interactive stubs with `setup(_, { emit })`
expose buttons or closure-based emit functions (QueueOverlayExpanded,
NodeSearchBoxPopover, JobAssetsList)
- **Removed**: 6 change-detector/redundant tests, 3 `@ts-expect-error`
annotations, `PackVersionSelectorVM` interface, `getVM` helper
- **BaseThumbnail**: Removed `useEventListener` mock — real event
handler attaches, `fireEvent.error(img)` triggers error state
## Review Focus
- Interactive stub patterns: `JobAssetsListStub` and `NodeSearchBoxStub`
use closure-based emit functions to trigger parent event handlers
without `vm.$emit`
- SignInForm form submission test fills PrimeVue Form fields via
`userEvent.type` and submits via button click (replaces `vm.onSubmit()`
direct call)
- CurrentUserButton Popover stub tracks open/close state reactively
- JobAssetsList: file-level `eslint-disable` for
`no-container`/`no-node-access`/`prefer-user-event` since stubs lack
ARIA roles and hover tests need `fireEvent`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10493-test-migrate-8-hard-case-component-tests-from-VTU-to-VTL-Phase-3-32e6d73d365081f88097df634606d7e3)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Fix subgraph viewport (zoom + position) drifting when navigating in/out
of subgraphs and switching workflow tabs.
## Problem
Three root causes:
1. **First visit**: `restoreViewport()` silently returned on cache miss,
leaving canvas at stale position
2. **Cross-workflow leakage**: Cache keyed by bare `graphId` — two
workflows with the same subgraph or unsaved workflows shared cache
entries
3. **Stale save on tab switch**: `loadGraphData` and
`changeTracker.restore()` overwrite `canvas.ds` before the async watcher
could save the old viewport
## Solution
1. **Workflow-scoped cache keys**: `${path}#${instanceId}:${graphId}` —
WeakMap assigns unique IDs per workflow object, handling unsaved
workflows with identical paths
2. **`flush: 'sync'` on activeSubgraph watcher**: Fires immediately
during `setGraph()`, BEFORE `loadGraphData`/`changeTracker` can corrupt
`canvas.ds`
3. **Cache miss → rAF fitToBounds**: On first visit, computes bounds
from `graph._nodes` and calls `ds.fitToBounds()` after the browser has
rendered
4. **Workflow switch watcher** (`flush: 'sync'`): Pre-saves viewport
under old workflow identity, suppresses `onNavigated` saves during load
cycle
Key architectural insight: `setGraph()` never touches `canvas.ds`, but
`loadGraphData` and `changeTracker.restore()` both write to it. By using
`flush: 'sync'`, the save happens during `setGraph` (before the
overwrites).
## Review Focus
- `subgraphNavigationStore.ts` — the three fixes and their interaction
- `flush: 'sync'` watchers — critical for correct save timing
- `suppressNavigatedSave` flag — prevents stale saves during async
workflow load
## Breaking Changes
None. Viewport cache is session-only (in-memory LRU). Existing workflows
unaffected.
## Demo Video of Fix
https://github.com/user-attachments/assets/71dd4107-a030-4e68-aa11-47fe00101b25
## Test plan
- [x] Unit: save/restore with workflow-scoped keys
- [x] Unit: cache miss doesn't mutate canvas synchronously
- [x] Unit: navigation integration (enter/exit preserves viewport)
- [x] E2E: first subgraph visit has visible nodes
- [x] Manual: enter subgraph → zoom/pan → exit → re-enter → viewport
restored
- [x] Manual: tab with subgraph → different tab → back → viewport
restored
- [x] Manual: two unsaved workflows → switch between → viewports
isolated
- Fixes#10246
- Related: #8173
<!-- QA_REPORT_SECTION -->
---
## 🔍 Automated QA Report
| | |
|---|---|
| **Status** | ✅ Complete |
| **Report** |
[sno-qa-10247.comfy-qa.pages.dev](https://sno-qa-10247.comfy-qa.pages.dev/)
|
| **CI Run** | [View
workflow](https://github.com/Comfy-Org/ComfyUI_frontend/actions/runs/23373279990)
|
Before/after video recordings with **Behavior Changes** and **Timeline
Comparison** tables.