## Summary
Fixes a regression introduced in #9680 where groups and nodes could
render in different coordinate spaces when loading legacy
`workflowRendererVersion: "Vue"` workflows with Vue nodes mode enabled.
- Add post-normalization sync to copy normalized LiteGraph node bounds
into `layoutStore` during `loadGraphData`
- Keep sync scoped to Vue nodes mode and only when normalization
actually ran
- Add unit tests for the new layout-store sync helper
- Add Playwright regression coverage for legacy Vue workflow load path
using `groups/nested-groups-1-inner-node`, asserting node/group
centering-gap distances remain within baseline tolerances
## Testing
- pnpm test:unit
src/renderer/core/layout/sync/syncLayoutStoreFromGraph.test.ts
- pnpm test:unit
src/renderer/extensions/vueNodes/layout/ensureCorrectLayoutScale.test.ts
- pnpm exec eslint
src/renderer/core/layout/sync/syncLayoutStoreFromGraph.ts
src/renderer/core/layout/sync/syncLayoutStoreFromGraph.test.ts
- pnpm exec oxlint src/scripts/app.ts
src/renderer/core/layout/sync/syncLayoutStoreFromGraph.ts
src/renderer/core/layout/sync/syncLayoutStoreFromGraph.test.ts
- pnpm typecheck
- pnpm typecheck:browser
- pnpm exec eslint browser_tests/tests/vueNodes/groups/groups.spec.ts
- pnpm exec oxlint browser_tests/tests/vueNodes/groups/groups.spec.ts
- pnpm exec playwright test
browser_tests/tests/vueNodes/groups/groups.spec.ts --grep "legacy Vue
workflows"
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10256-fix-resync-vue-node-layout-store-after-legacy-normalization-3276d73d365081568eebc6aa0827d943)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
- On graph change, set the `graph.id` as location hash
- On hash change, navigate to the target `graph.id` either in the
current, or any other loaded workflow.
`canvasStore.currentGraph` does not trigger when `app.loadGraphData` is
called. A trigger could be forced here, but I'm concerned about side
effects. Instead `updateHash` is manually called.
Code search shows that there are no current custom nodes using
`onhashchange`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-6811-Allow-graph-navigation-by-browser-forward-backward-2b26d73d365081bb8414fdf7c3686124)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
- Fix `findProvidersWithFallback` to try all parent paths progressively
instead of jumping from exact match to top-level segment only.
Previously `"a/b/c"` would try `"a/b/c"` then `"a"`, now correctly tries
`"a/b/c"` → `"a/b"` → `"a"`
- Use empty key `''` for `DownloadAndLoadCogVideoControlNet` and
`DownloadAndLoadCogVideoModel` registrations so `shouldUseAssetBrowser`
returns false — these nodes use HuggingFace repo names, not file-based
assets, so the asset browser finds nothing and shows an empty dropdown
## Test plan
- [x] All 46 existing `modelToNodeStore` tests pass
- [ ] Verify CogVideo ControlNet and HF model dropdowns show options
(after backend deploy)
- [x] Verify "Use" button on CogVideo/ControlNet/* models creates the
correct node
- [ ] Verify GGUF asset browser still works
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
- Copy Url button in MissingModelRow now copies a browsable page URL
instead of a direct download URL
- HuggingFace: `/resolve/` → `/blob/` (file page with model info and
download button)
- Civitai: strips `/api/download` or `/api/v1` prefix (model page)
## Changes
- Add `toBrowsableUrl()` to `missingModelDownload.ts` — converts
download URLs to browsable page URLs for HuggingFace and Civitai
- Update `MissingModelRow.vue` to use `toBrowsableUrl()` when copying
- Add 5 unit tests covering HuggingFace, Civitai, and non-matching URL
cases
## Test plan
- [x] Unit tests pass (14/14) — `toBrowsableUrl` covered by 5 dedicated
tests
- [x] Lint, format, typecheck pass
- [x] Manual: load workflow with missing HuggingFace models, click Copy
Url, verify copied URL opens the file page
- [x] Manual: load workflow with missing Civitai models, click Copy Url,
verify copied URL opens the model page
### Why no E2E test
The Copy Url button is only visible when `!isCloud &&
model.representative.url && !isAssetSupported`. Writing an E2E test for
the clipboard content would require:
1. A test fixture with a real HuggingFace/Civitai URL (fragile — depends
on external availability)
2. Granting `clipboard-read` permission in Playwright context
3. Ensuring `isAssetSupported` evaluates to `false` against the local
test server's node definitions
The URL transformation logic is a pure function, fully covered by unit
tests. E2E clipboard content verification has no existing patterns in
the codebase and would be environment-dependent and flaky.
---------
Co-authored-by: Jin Yi <jin12cc@gmail.com>
## Summary
Address code review feedback from #10134 by renaming the component and
improving implementation quality.
## Changes
- Rename `ResultGallery` → `MediaLightbox` across all references
- Replace `useEventListener(window, 'keydown')` with `@keydown` on
dialog element
- Remove change detector tests (`renders close button`, `prevents
default on arrow keys`)
- Remove redundant `toBeVisible()` before Playwright click (implicit
wait)
- Update keyboard tests to dispatch on dialog element instead of
`window`
- Sort button icon sizes (`icon-sm`, `icon`, `icon-lg`)
- Wire zoom event to lightbox in `MediaAssetCard` story via
`context.args`
- Add standalone `MediaLightbox` Storybook story under
`Platform/Assets/`
Fixes#10134
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10235-refactor-Rename-ResultGallery-to-MediaLightbox-and-address-code-review-3276d73d365081299b42f682373a12f1)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: Amp <amp@ampcode.com>
## Summary
- Add `quickRegister()` entries for LTX Video prompt enhancer models:
- `LLM/Llama-3.2-3B-Instruct` → `LTXVPromptEnhancerLoader` (`llm_name`)
- `LLM/Florence-2-large-PromptGen-v2.0` → `LTXVPromptEnhancerLoader`
(`image_captioner_name`)
These mappings ensure the "Use" button in the model browser correctly
creates
the appropriate loader node when users click on these LLM models.
## Test plan
- [x] Verify "Use" button works for LLM/Llama-3.2-3B-Instruct in model
browser
- [x] Verify "Use" button works for LLM/Florence-2-large-PromptGen-v2.0
in model browser
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10234-feat-add-model-to-node-mappings-for-LTX-Video-prompt-enhancer-3276d73d36508154847cdbae8c6e9bf1)
by [Unito](https://www.unito.io)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
Fix 3D asset disappearing when switching between 3D and image outputs in
app mode — missing `onUnmounted` cleanup leaked WebGL contexts.
## Changes
- **What**: Add `onUnmounted` hook to `Preview3d.vue` that calls
`viewer.cleanup()`, releasing the WebGL context when Vue destroys the
component via its v-if chain. Add unit tests covering init, cleanup on
unmount, and remount behavior.
## Review Focus
When switching outputs in app mode, Vue's v-if chain destroys and
recreates `Preview3d`. Without `onUnmounted` cleanup, the old `Load3d`
instance (WebGL context, RAF loop, ResizeObserver) leaks. After ~8-16
toggles, the browser's WebGL context limit is exhausted and new 3D
viewers silently fail to render.
<!-- Pipeline-Ticket: e36489d2-a9fb-47ca-9e27-88eb3170836b -->
---------
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
- WebcamCapture's `serializeValue` was ignoring the `/upload/image`
response and returning the original timestamp-based filename
(`webcam/${timestamp}.png [temp]`)
- This breaks content-addressed storage backends (e.g. Comfy Cloud)
where the server returns a different (hash-based) filename, causing
`ImageDownloadError` at inference time
- Now reads `data.name` and `data.subfolder` from the upload response,
matching the pattern already used by `useNodeImageUpload` and
`useMaskEditorSaver`
- No behavioral change for local ComfyUI — the server already returns
the same filename, it was just being ignored
## Test plan
- [x] WebcamCapture node captures and uploads an image successfully on
local ComfyUI
- [x] WebcamCapture node works correctly on Comfy Cloud (no
`ImageDownloadError`)
- [ ] Duplicate filename edge case: upload two captures with same
timestamp — server-renamed file is used correctly
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10220-fix-use-server-response-filename-in-WebcamCapture-serialization-3266d73d3650818aa9bdff8d27586180)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
`queueStore.update()` previously used a request-ID guard that discarded
responses whenever a newer call had been initiated. During concurrent
job execution, rapid WebSocket events (`status`, `execution_success`,
etc.) created a sustained stream of `update()` calls where:
1. **Every response got discarded** — each returning response saw a
higher `updateRequestId`, so data was never applied to the store until
the event burst subsided
2. **All HTTP requests still fired** — the guard didn't suppress
outgoing calls, just discarded stale responses after the fact
This caused the UI to freeze showing stale job states (e.g. completed
jobs still showing as "Running") during bursts, then snap all at once
when the storm ended.
## Changes
Replace the request-ID guard with a single-flight coalescing pattern:
- **At most one fetch in flight** at a time — no request spam
- **Every response is applied** — no UI starvation
- **Dirty flag** triggers one re-fetch if calls arrived during flight —
eventual consistency
## Testing
Added 5 unit tests covering:
- Concurrent calls coalesce into a single re-fetch
- No response starvation (every response applied)
- No duplicate requests when no concurrent calls
- Loading state transitions through coalesced re-fetches
- Normal sequential behavior preserved
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10203-fix-replace-stale-request-guard-with-single-flight-coalescing-in-queueStore-update-3266d73d365081088656e4e55ca4dbd3)
by [Unito](https://www.unito.io)
## Summary
The `computed` in `usePromotedPreviews` only tracked `nodeOutputs` as a
reactive dependency. GLSL live previews (and other preview-only sources)
write to `nodePreviewImages` instead of `nodeOutputs`, so promoted
preview widgets on SubgraphNodes never re-evaluated when live previews
updated.
## Changes
**Production** (`usePromotedPreviews.ts` — 3-line fix):
- Add `nodePreviewImages[locatorId]` as a second reactive dependency
alongside `nodeOutputs[locatorId]`
- Guard now passes when *either* source has data, not just `nodeOutputs`
**Tests** (`usePromotedPreviews.test.ts`):
- Add `nodePreviewImages` to mock store type and factory
- Add `seedPreviewImages()` helper
- Add `getNodeImageUrls.mockReset()` in `beforeEach` for proper test
isolation
- Two new test cases:
- `returns preview when only nodePreviewImages exist (e.g. GLSL live
preview)`
- `recomputes when preview images are populated after first evaluation`
- Clean up existing tests to use hoisted `getNodeImageUrls` mock
directly instead of `vi.mocked(useNodeOutputStore().getNodeImageUrls)`
## What this supersedes
This is a minimal re-implementation of #9461. That PR also modified
`promotionStore.ts` with a `_version`/`_touch()` monotonic counter to
manually force reactivity — that approach is dropped here as it is an
anti-pattern (manually managing reactivity counters instead of using
Vue's built-in reactivity system). The promotionStore changes were not
needed for this fix.
## Related
- Supersedes #9461
- Prerequisite work: #9198 (add GLSLShader to canvas image preview node
types)
- Upstream feature: #9201 (useGLSLPreview composable)
- Adjacent: #9435 (centralize node image rendering state in
NodeImageStore)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10165-fix-track-nodePreviewImages-in-usePromotedPreviews-for-GLSL-live-preview-propagation-3266d73d365081cd87d0d12c4c041907)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Harden subgraph test coverage: remove low-value change-detector tests,
consolidate fixtures, add behavioral coverage, and fix test
infrastructure issues. Includes minor production code corrections
discovered during test hardening.
## Changes
- **What**: Comprehensive subgraph test suite overhaul across 6 phases
- Removed change-detector tests and redundant assertions
- Consolidated fixture helpers into `subgraphHelpers.ts` /
`subgraphFixtures.ts`
- Added Pinia initialization and fixture reset to all test files
- Fixed barrel import violations (circular dependency prevention)
- Added behavioral coverage for slot connections, events, edge cases
- Added E2E helper and smoke test for subgraph promotion
- Exported `SubgraphSlotBase` from litegraph barrel for test access
- **Production code changes** (minor correctness fixes found during
testing):
- `resolveSubgraphInputLink.ts`: iterate forward (first-connected-wins)
to match `_resolveLinkedPromotionBySubgraphInput`
- `promotionSchema.ts`: return `[]` instead of throwing on invalid
`proxyWidgets`; console.warn always (not DEV-only)
- `LGraph.ts`: disconnect-after-veto ordering fix
- `litegraph.ts`: barrel export swap for `SubgraphSlotBase`
- **Stats**: 349 tests passing, 0 skipped across 26 test files
## Review Focus
- Tests that merely asserted default property values were deleted
(change detectors)
- Fixture state is now reset via `resetSubgraphFixtureState()` in
`beforeEach`
- All imports use `@/lib/litegraph/src/litegraph` barrel to avoid
circular deps
- Production changes are small and directly motivated by test findings
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: bymyself <cbyrne@comfy.org>
## Summary
Migrate two TypeScript lint rules from ESLint to oxlint and upgrade
`eslint-plugin-oxlint` to 1.55.0.
## Changes
- **Rule migration**: Move `typescript/no-import-type-side-effects` and
`typescript/no-empty-object-type` (with `allowInterfaces: 'always'`)
from `eslint.config.ts` to `.oxlintrc.json`.
- **Plugin upgrade**: Bump `eslint-plugin-oxlint` from 1.25.0 to 1.55.0,
which auto-disables additional ESLint rules now covered by oxlint. This
surfaced pre-existing `no-unused-expressions` violations (the `void`
prefixes and ternary-to-if/else changes), which are fixed in a separate
commit.
- **Dependencies**: None — `eslint-plugin-oxlint` auto-disables the
corresponding ESLint rules when they appear in `.oxlintrc.json`.
## Review Focus
- Verify the two migrated rules produce identical diagnostics in oxlint
as they did in ESLint
## Migration Status
See `temp/plans/eslint-to-oxlint-migration.md` for what can/can't
migrate and why.
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
- Fix nested SubgraphNode input slots doubling on each page reload
- Root cause: during configure, `_configureSubgraph` recreates
`SubgraphInput` objects with new references, and the `input-added` event
handler used `===` identity check which failed for these new objects,
causing `addInput()` to duplicate inputs
- Add `id`-based fallback matching in the `input-added` handler and
rebind `_subgraphSlot` with re-registered listeners
## Changes
**`SubgraphNode.ts:614-622`**: Add UUID `id` fallback to the `===`
reference check in the `input-added` event handler. When a stale
reference is matched by id, call `_addSubgraphInputListeners()` to
update `_subgraphSlot` and re-register listeners on the new
`SubgraphInput` object.
**`SubgraphNode.test.ts`**: 2 regression tests for nested subgraph
reconfigure scenarios.
## Test plan
- [x] Existing SubgraphNode tests pass (6 passed, 34 skipped)
- [x] New tests verify inputs don't duplicate after single and repeated
reconfigure cycles
- [x] Manual: create a subgraph containing another subgraph node, save,
reload — input slots should remain unchanged
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10187-fix-prevent-nested-SubgraphNode-input-slots-from-doubling-on-reload-3266d73d3650817286abea52365a626e)
by [Unito](https://www.unito.io)
## Summary
- Update the E2E test expectation for missing nodes overlay behavior
when switching between opened workflows
- `showMissingNodes` was intentionally changed to `true` in
`workflowService.ts` so users always see missing node pack info when
switching tabs, allowing them to install missing packs without a page
reload
- The test previously asserted the overlay should NOT reappear; now it
asserts the overlay SHOULD reappear
## Test plan
- [x] CI E2E tests pass with updated expectation
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10190-test-update-E2E-to-expect-missing-nodes-overlay-on-tab-switch-3266d73d36508106a48efff7309bd4e5)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Add historical trend visualization (ASCII sparklines + directional
arrows) to the performance PR report, showing how each metric has moved
over recent commits on main.
## Changes
- **What**: New `sparkline()`, `trendDirection()`, `trendArrow()`
functions in `perf-stats.ts`. New collapsible "Trend" section in the
perf report showing per-metric sparklines, direction indicators, and
latest values. CI workflow updated to download historical data from the
`perf-data` orphan branch and switched to `setup-frontend` action with
`pnpm exec tsx`.
## Review Focus
- The trend section only renders when ≥3 historical data points exist
(gracefully absent otherwise)
- `trendDirection()` uses a split-half mean comparison with ±10%
threshold — review whether this sensitivity is appropriate
- The `git archive` step in `pr-perf-report.yaml` is idempotent and
fails silently if no perf-history data exists yet on the perf-data
branch
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9939-feat-add-trend-visualization-with-sparklines-to-perf-report-3246d73d36508125a6fcc39612f850fe)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Enable virtual nodes (e.g. Set/Get) to resolve their output source
directly when the source lives in a different subgraph.
## Changes
- **What**: Added optional resolveVirtualOutput method on LGraphNode and
a new resolution path in ExecutableNodeDTO.resolveOutput that checks it
before falling through to the existing getInputLink path. Includes unit
tests for the three code paths (happy path, missing DTO, fallthrough).
## Review Focus
- Fully backwards compatible — no existing node implements
resolveVirtualOutput, so the new path is always skipped for current
virtual nodes (Reroute, PrimitiveNode, etc.).
<!-- If this PR fixes an issue, uncomment and update the line below -->
<!-- Fixes #ISSUE_NUMBER -->
## Screenshots
Simple example in actual use, combined with new changes in KJNodes
allows using Get nodes inside subgraphs:
<img width="2242" height="1434" alt="image"
src="https://github.com/user-attachments/assets/cc940a95-e0bb-4adf-91b6-9adc43a74aa2"
/>
<img width="1436" height="440" alt="image"
src="https://github.com/user-attachments/assets/62044af5-0d6e-4c4e-b34c-d33e85f2b969"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10111-feat-resolveVirtualOutput-for-cross-subgraph-virtual-nodes-eg-Set-Get-3256d73d3650816a9f20e28029561c58)
by [Unito](https://www.unito.io)
## Summary
Replace `app.rootGraph.getNodeById()` with `resolveNode()` so node
lookups search subgraphs, fixing broken image copy/paste and display for
nodes inside subgraphs.
## Changes
- **What**: Updated 7 call sites across 5 files to use `resolveNode()`
from `litegraphUtil.ts` instead of `app.rootGraph.getNodeById()`. The
`resolveNode` function already existed and searches both the root graph
and all subgraphs. Added a unit test verifying subgraph node resolution
in `nodeOutputStore`.
## Review Focus
The fix is mechanical — each call site simply swaps
`app.rootGraph?.getNodeById(id)` for `resolveNode(id)`. The
`resolveNode` utility iterates `graph.subgraphs` if the node is not
found in the root graph.
Fixes#9993
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10009-fix-resolve-nodes-in-subgraphs-for-image-copy-paste-and-display-3256d73d3650814f9467c53999f5d755)
by [Unito](https://www.unito.io)
## Summary
Remove the 225px minimum width constraint from reroute nodes so they
render at their intended ~75×26px size.
## Changes
- **What**: Reroute nodes now bypass the `min-w-[225px]` CSS constraint
and bottom padding applied to regular nodes. `resizable: false` is set
on the RerouteNode constructor to hide the resize handle. An
`isRerouteNode` computed in `LGraphNode.vue` gates these behaviors by
checking `nodeData.type === "Reroute"`.
## Review Focus
- Detection mechanism uses `type === "Reroute"` (explicit) rather than
`titleMode === NO_TITLE` (semantic but too broad). See PR #8574 as prior
art for reroute-specific conditionals.
- PR #7993 (open) modifies `LGraphNode.ts` pos/size setters but does not
touch `LGraphNode.vue` template or resize callback — no conflict
expected.
Fixes#4704
## Screenshots (if applicable)
Reroute nodes now render at ~75px wide instead of being forced to 225px
minimum.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8734-fix-vueNodes-decrease-default-size-of-reroute-nodes-3016d73d3650816dbeead93517a52f25)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
## Summary
- `waitForAuthInitialization` in `api.ts` was silently passing through
without actually waiting for Firebase auth
- `authStore.isInitialized` is unwrapped by Pinia (plain boolean, not a
ref), so `until()` received a static value
- `until()` without `.toBe()` returns a builder object, not a promise —
`Promise.race` treated it as immediately resolved
- Fixed with `storeToRefs` to preserve the ref and `.toBe(true)` to
return an actual promise
## Test plan
- [ ] Verify cloud mode API calls wait for Firebase initialization
before firing
- [ ] Verify non-cloud mode is unaffected
- [ ] Verify no 401s on initial page load in cloud mode
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10115-fix-broken-Firebase-auth-gate-in-API-layer-3256d73d365081e39b0df4d644f38c84)
by [Unito](https://www.unito.io)
Upgrades `pnpm/action-setup` from v4.2.0 to v4.4.0 across all 16
workflow files and the shared `setup-frontend` action.
## Why
GitHub Actions will force Node.js 24 as the default starting June 2,
2026. The v4.2.0 pin ran on Node.js 20 and emitted deprecation warnings
on every CI run. v4.4.0 was released specifically to address this,
updating the action runtime to Node.js 24.
- Fixes the warning: *"pnpm/action-setup@41ff72... Actions will be
forced to run with Node.js 24 by default starting June 2nd, 2026"*
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10137-ci-upgrade-pnpm-action-setup-to-v4-4-0-Node-js-24-3266d73d36508176b157fcd1d33f2274)
by [Unito](https://www.unito.io)
## WIP — waiting on Typeform ID from Alex Tov (Monday)
Pre-wires `trackFeatureUsed('node-search')` in
`NodeSearchBoxPopover.vue`. Increments a localStorage counter each time
the user opens node search. Currently a no-op because the
`FEATURE_SURVEYS` registry is empty.
**Monday**: Alex provides Typeform ID → add registry entry → survey goes
live on nightly builds.
### What this PR does
- Imports `useSurveyFeatureTracking` composable
- Calls `trackFeatureUsed()` in `showNewSearchBox()`
### What's still needed (will update this PR)
- [x] Add `node-search` entry to `FEATURE_SURVEYS` in
`surveyRegistry.ts` with Typeform ID
- [x] Set up Typeform → Slack webhook to
`#frontend-nightly-user-feedback`
- [ ] Test end-to-end on nightly build
### How the survey system works
After 3 uses of node search on a nightly build, a Typeform survey
popover slides in (once per user, 4-day global cooldown between
surveys). Eligibility: nightly + localhost only, respects opt-out.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9934-WIP-Track-node-search-usage-for-nightly-survey-3246d73d365081308847dd4c0085f21c)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
- On cloud deployments, the bootstrap sequence was firing authenticated
API calls (`/api/users`, `/api/settings`, `/api/userdata`, `/api/i18n`)
before verifying the user was authenticated via Firebase, causing
repeated 401 responses
- Moves the Firebase auth gate to the top of `startStoreBootstrap()` and
waits for both `isInitialized` **and** `isAuthenticated` before making
any API calls
- The router guard already redirects unauthenticated users to login;
once they authenticate, `isAuthenticated` becomes `true` and the
bootstrap proceeds normally
## Test plan
- [ ] Verify on cloud deployment: unauthenticated users see no 401 API
errors in the network tab
- [ ] Verify on cloud: after login, settings/i18n/workflows load
correctly
- [ ] Verify non-cloud deployments are unaffected (no `isCloud` guard
hit)
- [ ] Unit tests pass (`pnpm test:unit -- --run
src/stores/bootstrapStore.test.ts`)
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9909-fix-gate-cloud-API-calls-behind-Firebase-authentication-3236d73d3650819287e4e4c68623463b)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
Replaces two separate PR comment workflows (bundle size + performance)
with a single unified report that posts one combined comment per PR.
## Changes
- **What**: New `pr-report.yaml` aggregator workflow triggers on both
`CI: Size Data` and `CI: Performance Report` completions. Finds sibling
workflow runs by PR head SHA. Renders combined report via
`unified-report.js` (shells out to existing `size-report.js` and
`perf-report.ts`). Sections show "pending" or "failed" placeholders when
data is unavailable.
- **Breaking**: Removes `pr-size-report.yaml` and `pr-perf-report.yaml`.
Legacy `<!-- COMFYUI_FRONTEND_SIZE -->` and `<!-- COMFYUI_FRONTEND_PERF
-->` comments are auto-cleaned on first run.
- **Dependencies**: None
## Review Focus
- Concurrency key uses `head_sha` so the later-completing workflow
cancels the earlier report run, ensuring the final comment always has
both sections.
- Stale-run guard: verifies workflow_run SHA matches the live PR head
before posting.
- The `workflow_dispatch` re-trigger path from `pr-size-report.yaml` is
not carried forward — the unified workflow handles re-trigger naturally
via its dual-trigger design.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9911-feat-unified-PR-report-combining-bundle-size-and-runtime-perf-3236d73d365081baac1cce6f0d9244ac)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Revives #6845. One-time modal introducing Comfy Cloud to macOS desktop
users on first launch, with copy aligned to the latest comfy.org/cloud
page.
## Changes
- **What**: One-time cloud notification modal for macOS + Electron
users, shown 2s after first launch. Includes updated messaging (400 free
monthly credits, no-setup GPU access), telemetry tracking, and UTM
parameters (`utm_id`, `utm_source_platform`).
- **Dependencies**: None
## Review Focus
- Copy alignment with comfy.org/cloud
- Platform guard: macOS + Electron only
- UTM parameters for funnel attribution
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10116-feat-add-cloud-notification-modal-for-macOS-desktop-users-3256d73d36508105995edb71253ae824)
by [Unito](https://www.unito.io)
---------
Co-authored-by: bymyself <cbyrne@comfy.org>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Keep the restored job details popover visible when the job history
sidebar is docked on the left edge of the workspace.
## Changes
- **What**: Replace the fixed `right`-based hover popover positioning
with viewport-aware `left` positioning so the popover opens on the side
with available space, reuse that logic in both `JobAssetsList` and
`QueueJobItem`, and add coverage for left-edge/right-edge placement plus
the job history sidebar integration.
## Review Focus
Please verify the hover popover opens on-screen for left-docked job
history, and that queue overlay / legacy queue row behavior still
matches the intended hover handoff.
## Screenshots (if applicable)
N/A
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9679-fix-keep-job-details-popover-on-screen-in-sidebar-31e6d73d3650816d9e7ffb0749430218)
by [Unito](https://www.unito.io)
---------
Co-authored-by: bymyself <cbyrne@comfy.org>
Documents 3 hard-learned gotchas from debugging subgraph context menu
E2E tests across ~8 threads:
- Canvas `z-999` overlay intercepting `click()` on DOM widgets → use
`dispatchEvent`
- Context menus requiring node selection before right-click
- Subgraph node sizing minimum for reliable `navigateIntoSubgraph()`
Added to both `browser_tests/AGENTS.md` (auto-loaded for agents working
in that dir) and the Playwright test-writing skill's Common Issues
table.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9951-docs-add-E2E-testing-gotchas-for-canvas-overlay-context-menus-and-subgraph-navigation-3246d73d3650819a928bf91d66e22c2c)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Prune orphaned inputs in `_internalConfigureAfterSlots()` to fix
duplicate SubgraphNode inputs that accumulate on serialize-load cycles.
## Changes
- **What**: After `_rebindInputSubgraphSlots()`, filter out inputs with
no matching `_subgraphSlot`. This prevents `LGraphNode.configure()`
`cloneObject` expansion from persisting stale duplicates.
- Added 3 regression tests covering: corrupted serialized data,
reconfigure round-trips, and serialization output.
## Review Focus
The fix is a single `filter()` call. The existing `console.warn` guard
at line ~976 (for inputs without `_subgraphSlot`) becomes dead code
after this fix but is retained as defense-in-depth.
Fixes#9977
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10020-fix-prune-orphaned-SubgraphNode-inputs-after-configure-3256d73d3650812e8cecf4a3c86f2c33)
by [Unito](https://www.unito.io)
## Summary
Extract the shared delayed queue-details hover state into a composable
so the queue overlay and grouped queue rows use the same timing and
handoff behavior.
## Changes
- **What**: Add `useJobDetailsHover`, migrate `JobAssetsList` and
`JobGroupsList` to it, convert the touched helper functions to
declarations, and add a grouped-row regression test for the A -> B ->
leave stale-popover case.
## Review Focus
Verify that details hover timing stays consistent between the queue
overlay and grouped queue rows, especially around the A -> B hover
handoff and brief-hover exit path.
Stacked on #9549.
## Screenshots (if applicable)
N/A
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9924-fix-share-queue-details-hover-state-3236d73d36508108aca5dcfb99bf33c6)
by [Unito](https://www.unito.io)
## Summary
Fix missing capture button on WebcamCapture node in Vue renderer mode.
## Changes
- **What**: Remove `canvasOnly: true` from the capture button widget
options. This flag prevented `WidgetButton.vue` from rendering the
button. The WEBCAM DOM widget (video feed) already renders correctly via
`WidgetDOM.vue`.
## Review Focus
Single-line change: `{ canvasOnly: true }` → `{}`. The button's label,
callback, disabled state, and serializeValue behavior are all handled by
WidgetButton.vue already.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9936-fix-show-webcam-capture-button-in-Vue-renderer-3246d73d36508173a6e5ca59d9fb7af1)
by [Unito](https://www.unito.io)
## Summary
- Move `contain-layout contain-style` from the outer node container to
the inner wrapper to prevent CSS containment from interfering with
inter-node compositing
- Add `isolation: isolate` to the outer container for explicit stacking
context isolation
- Fixes#9988
## Root Cause
Originally, PR
[#5169](https://github.com/Comfy-Org/ComfyUI_frontend/pull/5169)
(`57db10f`, 2025-08-23) applied all three containment properties to the
outer node container:
```
contain-layout contain-style contain-paint
```
`contain-paint` ensured each node's painting was clipped to its own
boundaries, guaranteeing render isolation between sibling nodes.
Later, PR
[#6289](https://github.com/Comfy-Org/ComfyUI_frontend/pull/6289)
(`6e4471ad`, 2025-10-28) removed `contain-paint` to fix a node sizing
issue — likely because `contain-paint` clips content to the element's
padding box, which would clip selection overlays and footer tabs that
extend beyond the node boundary via negative insets.
Removing `contain-paint` while keeping `contain-layout contain-style` on
the outer container broke paint isolation between nodes. Without paint
containment, the browser's compositor no longer treated each node as an
isolated painting unit, allowing text from lower z-index nodes to
visually bleed through higher z-index nodes.
## Fix
1. **Move containment inward**: `contain-layout contain-style` is moved
from the outer container to the inner wrapper (`node-inner-wrapper`).
This preserves layout/style containment performance benefits while
keeping them scoped to the node body — away from the stacking context
boundary between sibling nodes.
2. **Explicit isolation**: `isolation: isolate` is added to the outer
container. While `transform` + `z-index` already create a stacking
context, `isolation: isolate` provides an unambiguous signal to the
browser's compositor to treat each node as an isolated compositing unit
— replacing the paint isolation role that `contain-paint` previously
served, without clipping overlays.
## Red-Green Verification
| Commit | Status | Purpose |
|--------|--------|---------|
|
[`feb8555`](https://github.com/Comfy-Org/ComfyUI_frontend/commit/feb855501)
`test:` | 🔴 Red (verified locally) | Test asserts `isolate`
on outer container and `contain-layout contain-style` on inner wrapper
only — fails against current main |
|
[`c47f89e`](https://github.com/Comfy-Org/ComfyUI_frontend/commit/c47f89ed2)
`fix:` | 🟢 Green (CI: Tests Unit passing) | Moves
containment to inner wrapper, adds `isolation: isolate` — test passes |
**Local red verification:**
```
AssertionError: expected [ 'group/node', 'lg-node', …(9) ] to include 'isolate'
❯ src/renderer/extensions/vueNodes/components/LGraphNode.test.ts:264:26
```
## Test Plan
- [x] CI red on test-only commit (verified locally — test fails without
fix)
- [x] CI green on fix commit (CI: Tests Unit passes)
- [ ] Place a String Literal node with long text on the canvas
- [ ] Place a Load Image node (with an image loaded) overlapping the
String Literal
- [ ] Verify text from the String Literal does NOT bleed through the
Load Image node
- [ ] Verify node selection outlines, error borders, and footer tabs
render correctly
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
- Read `sentry_dsn` from remote config at runtime for cloud builds,
following the same pattern as `firebase.ts` and `comfyApi.ts`
- Upload sourcemaps to both staging (`SENTRY_PROJECT`) and prod
(`SENTRY_PROJECT_PROD`) Sentry projects at build time
- Remove `SENTRY_DSN_PROD` / `USE_PROD_CONFIG`-based DSN selection — DSN
is now delivered per-environment via dynamic config
## Companion PR
- https://github.com/Comfy-Org/cloud/pull/2883 (cloud repo: dynamic
config + workflow)
## Setup required
- Add `SENTRY_PROJECT_PROD` variable (`cloud-frontend-prod`) to GitHub
STAGING environment in cloud repo
## Test plan
- [ ] Verify staging build reads staging DSN from remote config
- [ ] Verify prod build reads prod DSN from remote config
- [ ] Confirm sourcemaps are uploaded to both Sentry projects
- [ ] Validate fallback to `__SENTRY_DSN__` when remote config is
unavailable
## Summary
Fix high-resolution image preview expanding to full screen width, hiding
the close button.
## Changes
- **What**: Override scoped `ComfyImage` styles (`width/height: 100%`,
`object-fit: cover`) in the gallery context with `auto` sizing and
`max-width: 100vw; max-height: 100vh` constraints. The scoped
`[data-v-xxx]` attribute selector gave `comfy-image-main` higher
specificity than the existing `img.galleria-image` rule, so the new
selector `img.comfy-image-main.galleria-image` is used to win
specificity.
## Review Focus
- CSS specificity approach: using `.comfy-image-main.galleria-image` to
override Vue scoped styles without `!important`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10129-fix-high-res-image-preview-overflowing-screen-and-hiding-close-button-3266d73d36508191a03bcc75f9912421)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
Co-authored-by: GitHub Action <action@github.com>