## Summary
Eliminate the double `goto()` in `ComfyPage.setup()` by using
`addInitScript` to seed localStorage before the first navigation.
## Changes
- **What**: Move route mocking and localStorage seeding before `goto()`,
replacing `page.evaluate` with `page.addInitScript` so values are set
before app JS executes on first load. Removes the second `goto()` call
entirely.
## Review Focus
- Verify all Playwright E2E shards pass — the `clearStorage: false` and
`mockReleases: false` paths should be unaffected.
- Note: `addInitScript` persists for the page lifetime (like
`FeatureFlagHelper.seedFlags` already does), so any subsequent
`page.reload()` or `goto()` in tests will also clear storage. This
should be fine since tests that use `clearStorage: false` skip this
block.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10313-perf-eliminate-double-page-navigation-in-Playwright-test-setup-3286d73d36508158a8edeefb27fcae20)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Fix workflow loading for nested subgraphs with duplicate node IDs by
configuring subgraph definitions in topological (leaf-first) order.
## Changes
- **What**: Three pre-existing bugs that surface when loading nested
subgraphs with colliding node IDs:
1. Subgraph definitions configured in serialization order — a parent
subgraph's `SubgraphNode.configure` would run before its referenced
child subgraph was populated, causing link/widget resolution failures.
2. `_resolveLegacyEntry` returned `undefined` when `input._widget`
wasn't set yet, instead of falling back to `resolveSubgraphInputTarget`.
3. `_removeDuplicateLinks` removed duplicate links without updating
`SubgraphOutput.linkIds`, leaving stale references that broke prompt
execution.
- **What (housekeeping)**: Moved `subgraphDeduplication.ts` from
`utils/` to `subgraph/` directory where it belongs.
## Review Focus
- Topological sort correctness: Kahn's algorithm with edges from
dependency→dependent ensures leaves configure first. Cycle fallback
returns original order.
- IO slot link repair: `_repairIOSlotLinkIds` runs after
`_configureSubgraph` creates IO slots, patching any `linkIds` that point
to links removed by `_removeDuplicateLinks` during `super.configure()`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10314-fix-configure-nested-subgraph-definitions-in-dependency-order-3286d73d36508171b149e238b8de84c2)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
## Summary
Fix the bottom-right graph canvas toolbar (zoom controls, fit-to-view,
minimap toggle) not being visible on mobile devices in normal graph
mode.
## Problem
PrimeVue applies `overflow: hidden` to all `.p-splitterpanel` elements
by default. The `GraphCanvasMenu` component is absolutely positioned
(`right-0 bottom-0`) inside the `graph-canvas-panel` SplitterPanel. On
mobile viewports, the panel's bounding box can be smaller than the full
canvas area, causing the toolbar to be clipped by the `overflow:
hidden`.
## Solution
Add `overflow-visible` to the `graph-canvas-panel` SplitterPanel class
to override PrimeVue's default `overflow: hidden`. This allows the
absolutely-positioned toolbar (and minimap) to remain visible regardless
of viewport size.
<img width="873" height="1056" alt="image"
src="https://github.com/user-attachments/assets/7239a5ce-8ce8-4e1d-a8ff-6d6d3c61f5da"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10168-fix-make-graph-canvas-toolbar-visible-on-mobile-3266d73d36508130b675e839cb748fd5)
by [Unito](https://www.unito.io)
## 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
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
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
- 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
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>
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
- Remove the legacy missing nodes modal dialog and migrate all
functionality to the existing Error Overlay / TabErrors system
- Migrate core node version warning from `MissingCoreNodesMessage.vue`
to `MissingNodeCard` in the errors tab
- Remove `Comfy.Workflow.ShowMissingNodesWarning` setting (errors tab
always surfaces missing nodes)
- Delete 6 legacy files: `useMissingNodesDialog.ts`,
`MissingNodesContent.vue`, `MissingNodesFooter.vue`,
`MissingNodesHeader.vue`, `MissingCoreNodesMessage.vue` and its test
- Rename `showMissingNodesDialog`/`showMissingModelsDialog` params to
`showMissingNodes`/`showMissingModels`
- Add `errorOverlay` and `missingNodeCard` to centralized `TestIds`
- Migrate all E2E tests from legacy dialog selectors to error overlay
testIds
- Add new E2E test: MissingNodeCard visible via "See Errors" button flow
- Add new E2E test: subgraph missing node type verified by expanding
pack row
- Add `surfaceMissingNodes` unit tests to `executionErrorStore`
- Guard `semver.compare` against invalid version strings
- Add `role="alert"`, `aria-hidden` for accessibility
- Use reactive props destructuring in `MissingNodeCard` and
`MissingPackGroupRow`
**Net change: -669 lines** (19 files, +323 / -992)
<img width="733" height="579" alt="image"
src="https://github.com/user-attachments/assets/c497809d-b176-43bf-9872-34bd74c6ea0d"
/>
## Test plan
- [x] Unit tests: MissingNodeCard core node warning (7 tests)
- [x] Unit tests: surfaceMissingNodes (4 tests)
- [x] Unit tests: workflowService showPendingWarnings (updated)
- [x] E2E: Error overlay visible on missing nodes workflow
- [x] E2E: Error overlay visible on subgraph missing nodes
- [x] E2E: MissingNodeCard visible via See Errors button
- [x] E2E: Subgraph node type visible after expanding pack row
- [x] E2E: Error overlay does not resurface on undo/redo
- [x] E2E: Error overlay does not reappear on workflow tab switch
- [x] Typecheck, lint, knip all passing
## Related issues
- Closes#9923 (partially — `errorOverlay` and `missingNodeCard` added
to TestIds)
- References #10027 (mock hoisting inconsistency)
- References #10033 (i18n-based test selectors)
- References #10085 (DDD layer violation + focusedErrorNodeId)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-10102-refactor-remove-legacy-missing-nodes-dialog-3256d73d365081c194d2e90bc6401846)
by [Unito](https://www.unito.io)
## Summary
- Adds a **Paste Image** context menu option to nodes that support image
pasting (e.g. Load Image), complementing the existing **Copy Image**
option
- Reads clipboard image data via `navigator.clipboard.read()` and
delegates to `node.pasteFiles()` — same path as `Ctrl+V` paste
- Only shown on nodes that implement `pasteFiles` (e.g. LoadImage)
- Available even when no image is loaded yet (e.g. fresh LoadImage node)
- **Node 2.0 context menu only** — legacy litegraph menu is not
supported
- Fixes#9989
<img width="852" height="685" alt="스크린샷 2026-03-16 오후 5 34 28"
src="https://github.com/user-attachments/assets/219e8162-312a-400b-90ec-961b95b5f472"
/>
## Test plan
- [x] Right-click a Load Image node (with or without image loaded) →
verify "Paste Image" appears
- [x] Copy an image to clipboard → click "Paste Image" → verify image is
loaded into the node
- [x] Verify "Paste Image" does not appear on output-only image nodes
(e.g. Save Image, Preview Image)
- [x] Unit tests pass: `pnpm vitest run
src/composables/graph/useImageMenuOptions.test.ts`
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
## Summary
Add 2 reusable test helpers for Playwright E2E tests, integrated into
the ComfyPage fixture. These provide standardized patterns for mocking
feature flags and queue state across all E2E tests.
## Changes
- **`FeatureFlagHelper.ts`** — manage localStorage `ff:` prefixed
feature flags (`seedFlags` for init-time, `setFlags` for runtime) and
mock `/api/features` route
- **`QueueHelper.ts`** — mock `/api/queue` and `/api/history` routes
with configurable running/pending counts and success/error job entries
- **`ComfyPage.ts`** — integrate both helpers as
`comfyPage.featureFlags` and `comfyPage.queue`
## Review Focus
- Helper API design: are `seedFlags`/`setFlags`/`mockServerFeatures` the
right abstractions for feature flag testing?
- Queue mock fidelity: does the mock history shape match real ComfyUI
API responses closely enough?
- These are test-only infrastructure — no production code changes.
## Stack
This is the base PR for the Playwright E2E coverage stack. Waves 1-4 all
branch from this and can merge independently once this lands:
- **→ This PR**: Test infrastructure helpers
- #9555: Toasts, error overlay, selection toolbox, linear mode,
selection rectangle
- #9556: Node search, bottom panel, focus mode, job history, side panel
- #9557: Errors tab, node headers, queue notifications, settings sidebar
- #9558: Minimap, widget copy, floating menus, node library essentials
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Adds Total Blocking Time (TBT) and frame duration metrics to the
performance testing infrastructure, plus three new test scenarios
covering zoom, pan, and many-nodes-idle.
## Changes
### New Metrics
- **`totalBlockingTimeMs`** — Computed from PerformanceObserver
`longtask` entries: `sum(duration - 50ms)` for tasks >50ms. Measures
main thread blocking.
- **`frameDurationMs`** — Average frame duration via rAF timing (16.67ms
= 60fps target). Measures rendering smoothness.
### New Test Scenarios
| Scenario | Description |
|---|---|
| `canvas-zoom-sweep` | 10 zoom-in + 10 zoom-out cycles on default
workflow |
| `canvas-pan-many-nodes` | 10 pan sweeps over 100-node workflow |
| `canvas-many-nodes-idle` | 2-second idle measurement with 100 nodes
rendered |
### Infrastructure
- `PerformanceHelper.ts`: Installs PerformanceObserver for longtask,
collects TBT, measures frame duration via rAF
- `perf-report.ts`: Reports TBT and frame duration in PR comment tables
- `browser_tests/assets/perf/many_nodes_100.json`: 100-node (10×10 grid)
test fixture
## Review Focus
- TBT collection clears entries at `startMeasuring()` and reads at
`stopMeasuring()` — ensure no race with observer buffering
- Frame duration sampling uses 10 frames — enough for signal without
slowing tests
Depends on: #9886, #9887
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9910-feat-add-TBT-frameDuration-metrics-and-new-perf-test-scenarios-3236d73d365081488ae3c594a8bf7cff)
by [Unito](https://www.unito.io)
## What
Adds a 245-node workflow asset and two `@perf` tests to establish a
baseline for large-graph performance regressions (Tier 6 in the
performance backlog).
## Why
Backlog item N5: we need CI regression detection for compositor layer
management, GPU texture count, and transform pane cost at 245+ nodes.
This is PR1 of 2 — establishes baseline metrics on main. Future
optimization PRs will show improvement deltas against this baseline.
## Tests Added
- **`large graph idle rendering`** — 120 frames idle with 245 nodes,
measures style recalcs, layouts, task duration, heap delta
- **`large graph pan interaction`** — middle-click pan across 245 nodes,
stresses compositor layer management and transform recalculation
## Workflow Asset
`browser_tests/assets/large-graph-workflow.json` — 245 nodes (49
pipelines of CheckpointLoader → 2× CLIPTextEncode → KSampler +
EmptyLatentImage), 294 links. Minimal structure focused on node count.
## Verification
- [x] `pnpm typecheck:browser` passes
- [x] `pnpm lint` passes (eslint on changed file)
- [x] All link references in JSON validated programmatically
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9940-test-add-large-graph-perf-test-with-245-node-workflow-backlog-N5-3246d73d365081f6b5d8ddb9a85e6ad0)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Expands the performance testing infrastructure to collect 4 additional
CDP metrics that are already returned by `Performance.getMetrics` but
were not being read. This is a zero-cost expansion — no additional CDP
calls, just reading more fields from the existing response.
## New Metrics
| Metric | CDP Source | What It Detects |
|---|---|---|
| `domNodes` | `Nodes` | DOM node count delta — widget DOM leaks during
node create/destroy |
| `jsHeapTotalBytes` | `JSHeapTotalSize` | Total heap delta — combined
with `heapDeltaBytes` shows GC pressure |
| `scriptDurationMs` | `ScriptDuration` | JS execution time vs total
task time — script vs rendering balance |
| `eventListeners` | `JSEventListeners` | Listener count delta — detects
listener accumulation across lifecycle |
## Changes
### `browser_tests/fixtures/helpers/PerformanceHelper.ts`
- Added 4 fields to `PerfSnapshot` interface
- Added 4 fields to `PerfMeasurement` interface
- Wired through `getSnapshot()` and `stopMeasuring()`
### `scripts/perf-report.ts`
- Added 4 fields to `PerfMeasurement` interface
- Expanded `MetricKey` type and `REPORTED_METRICS` array with 3 new
reported metrics (`domNodes`, `scriptDurationMs`, `eventListeners`)
- `jsHeapTotalBytes` is collected but not in `REPORTED_METRICS` — it's
used alongside `heapDeltaBytes` for GC pressure ratio analysis
## Why These 4
From a gap analysis of all ~30 CDP metrics, these were identified as
highest priority for ComfyUI:
- **`Nodes`** (P0): ComfyUI dynamically creates/destroys widget DOM. DOM
bloat from leaked widgets is a key performance risk, especially for Vue
Nodes 2.0.
- **`ScriptDuration`** (P1): Separates JS execution from layout/paint.
Reveals whether perf issues are script-heavy or rendering-heavy.
- **`JSEventListeners`** (P1): Widget lifecycle can leak listeners
across node add/remove cycles.
- **`JSHeapTotalSize`** (P1): With `JSHeapUsedSize`, the ratio shows GC
fragmentation pressure.
## Backward Compatibility
The `PerfMeasurement` interface is extended (not changed). Old baseline
`perf-metrics.json` files without these fields will have `undefined`
values, which the report script handles gracefully (shows `—` for
missing data).
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9887-feat-expand-CDP-perf-metrics-add-DOM-nodes-script-duration-event-listeners-3226d73d3650818abea1d4a441667c38)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Add E2E test coverage for the textarea widget right-click context menu
inside subgraphs.
The fix was shipped in #9840 — this PR adds the missing browser test.
## Test
- Loads a subgraph workflow with a CLIPTextEncode (textarea) node
- Navigates into the subgraph
- Right-clicks the textarea DOM element
- Asserts that the ComfyUI "Promote Widget" context menu option appears
## Related
- Fixes the test gap from #9840
- Notion ticket: d7a53160-e1e1-42bb-a5ac-c0c2702c629c
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9891-test-add-browser-test-for-textarea-right-click-context-menu-in-subgraph-3226d73d365081a4be51f89b5d505361)
by [Unito](https://www.unito.io)
## Problem
When navigating back from a subgraph to the root graph, the SubgraphNode
can retain a stale progress bar. This happens because the progress
watcher in `GraphCanvas.vue` watches `[nodeLocationProgressStates,
canvasStore.canvas]`, but neither value changes reference during
subgraph navigation:
- `nodeLocationProgressStates` is already `{}` (execution completed
while viewing the subgraph)
- `canvasStore.canvas` is a `shallowRef` set once at startup — only
`canvas.graph` changes (via `setGraph()`)
**Reproduction** (from PR #4382 comment thread by @guill):
1. Create a subgraph with a KSampler
2. Execute the workflow
3. While progress bar is halfway, enter the subgraph
4. Wait for execution to complete
5. Navigate back to root graph
6. Progress bar is stuck at 50%
## Root Cause
`canvasStore.canvas` is a `shallowRef` — subgraph navigation mutates
`canvas.graph` (a nested property) via `LGraphCanvas.setGraph()`, which
doesn't trigger a shallow watch. The watcher never re-fires to clear
stale `node.progress` values.
## Fix
Add `canvasStore.currentGraph` to the watcher's dependency array. This
is already a `shallowRef` in `canvasStore` that's updated on every
`litegraph:set-graph` event. Zero overhead, precise targeting.
## Context
- Original discussion:
https://github.com/Comfy-Org/ComfyUI_frontend/pull/4382/files/BASE..868e047272f6c5d710db7e607b8997d4c243490f#r2202024855
- PR #9248 correctly removed `deep: true` from this watcher but missed
the subgraph edge case
- `deep: true` was the wrong fix — `canvasStore.currentGraph` is the
precise solution
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9865-fix-clear-stale-progress-bar-on-SubgraphNode-after-navigation-3226d73d3650811c8f9de612d81ef98a)
by [Unito](https://www.unito.io)
## Summary
Fix node layout drift from repeated `ensureCorrectLayoutScale` scaling,
simplify it to a pure one-time normalizer, and fix links not following
Vue nodes during drag.
## Changes
- **What**:
- `ensureCorrectLayoutScale` simplified to a one-time normalizer:
unprojects legacy Vue-scaled coordinates back to canonical LiteGraph
coordinates, marks the graph as corrected, and does nothing else. No
longer touches the layout store, syncs reroutes, or changes canvas
scale.
- Removed no-op calls from `useVueNodeLifecycle.ts` (a renderer version
string was passed where an `LGraph` was expected).
- `layoutStore.finalizeOperation` now calls `notifyChange` synchronously
instead of via `setTimeout`. This ensures `useLayoutSync`'s `onChange`
callback pushes positions to LiteGraph `node.pos` and calls
`canvas.setDirty()` within the same RAF frame as a drag update, fixing
links not following Vue nodes during drag.
- **Tests**: Added tests for `ensureCorrectLayoutScale` (idempotency,
round-trip, unknown-renderer no-op) and `graphRenderTransform`
(project/unproject round-trips, anchor caching).
## Review Focus
- The `setTimeout(() => this.notifyChange(change), 0)` →
`this.notifyChange(change)` change in `layoutStore.ts` is the key fix
for the drag-link-sync bug. The listener (`useLayoutSync`) only writes
to LiteGraph, not back to the layout store, so synchronous notification
is safe.
- `ensureCorrectLayoutScale` no longer has any side effects beyond
normalizing coordinates and setting `workflowRendererVersion` metadata.
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: Christian Byrne <cbyrne@comfy.org>
Co-authored-by: jaeone94 <89377375+jaeone94@users.noreply.github.com>
Co-authored-by: AustinMroz <austin@comfy.org>
Co-authored-by: Hunter <huntcsg@users.noreply.github.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Adds VS Code-style multi-keybinding support to the Keybinding settings
panel. Commands can now have multiple keybindings displayed, expanded,
and individually managed.
- Fixes#1088
## Changes
### Store (`keybindingStore.ts`)
- `removeAllKeybindingsForCommand(commandId)` — unsets all bindings for
a command
- `updateSpecificKeybinding(old, new)` — replaces a single binding
without affecting others
- `resetKeybindingForCommand` — updated to restore **all** default
bindings, not just the first
- `isCommandKeybindingModified` — updated to compare full sorted sets of
bindings
### UI (`KeybindingPanel.vue`)
- **Data model**: `keybinding: KeybindingImpl | null` → `keybindings:
KeybindingImpl[]`
- **Multi-binding display**: shows up to 2 combos inline with `, `
separator, then `+ N more` badge
- **Expand/collapse**: click any row with 2+ bindings to expand
individual binding rows; chevron-right icon rotates on expand
- **Per-binding actions**: edit (pencil), reset, trash on each expanded
sub-row
- **Parent row actions**: `+`/trash for 2+ bindings, pencil/reset/trash
for 1, `+`/disabled for 0
- **Edit modes**: `edit` (replace specific binding via
`updateSpecificKeybinding`) and `add` (append via `addUserKeybinding`)
- **Right-click context menu**: Change keybinding, Add new, Reset to
default, Remove keybinding — with proper disabled states and lucide
icons
- **Remove all dialog**: confirmation via `showSmallLayoutDialog` with
`RemoveAllKeybindingsHeader`/`Content` components
- **Reset all dialog**: confirmation via `showConfirmDialog` before
resetting all keybindings to defaults
- **Double-click**: 0 bindings → add, 1 → edit, 2+ → no-op (single click
toggles expand)
- **Consistent alignment**: commands without chevron get `pl-5` padding
to align with those that have it
### Tests (`keybindingStore.test.ts`)
- 7 new tests covering `removeAllKeybindingsForCommand`,
`updateSpecificKeybinding`, multi-binding `isCommandKeybindingModified`,
and multi-binding `resetKeybindingForCommand`
### i18n (`main.json`)
- 11 new keys: removeAllKeybindingsTitle/Message, removeAll,
changeKeybinding, addNewKeybinding, resetToDefault, removeKeybinding,
nMoreKeybindings, resetAllKeybindingsTitle/Message, allKeybindingsReset
### New components
- `RemoveAllKeybindingsHeader.vue` — dialog header
- `RemoveAllKeybindingsContent.vue` — dialog body with Close/Remove all
buttons
## Test plan
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes (no new errors)
- [x] `pnpm vitest run src/platform/keybindings/` — 45 tests pass
- [x] CodeRabbit review — 0 findings
- [ ] Manual: open Settings → Keybindings, verify multi-binding commands
(e.g. Delete Selected Items, Zoom In) show multiple combos
- [ ] Manual: click row to expand, verify per-binding actions work
- [ ] Manual: right-click row, verify context menu actions
- [ ] Manual: click trash on 2+ binding command, verify "Remove all"
confirmation dialog
- [ ] Manual: click "Reset All" button, verify confirmation dialog
appears
- [ ] Manual: add/edit/remove individual bindings, verify persistence
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9738-feat-multi-keybinding-support-in-settings-panel-3206d73d365081e9b08bd3cfe21495f1)
by [Unito](https://www.unito.io)
## Summary
- Fixes a bug where widgets marked as `advanced` were always visible,
ignoring the "Always show advanced widgets on all nodes" setting
- Root cause: `extractWidgetDisplayOptions` in `useGraphNodeManager.ts`
read `widget.advanced` (always `undefined` on BaseWidget) instead of
`widget.options?.advanced` (where `litegraphService` actually sets the
flag)
- Consistent with how `hidden` is already read from
`widget.options.hidden` on the adjacent line
## Test plan
- [ ] Load a node with advanced inputs (e.g. `LTXVScheduler`)
- [ ] Verify `max_shift`, `base_shift`, `stretch`, `terminal` are hidden
when "Always show advanced widgets on all nodes" is disabled
- [ ] Verify they become visible when the setting is enabled or the
per-node toggle is clicked
- [ ] Verify the advanced toggle button appears on nodes with advanced
widgets
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9857-fix-advanced-widgets-always-visible-regardless-of-setting-3226d73d36508132b338d098fbf19433)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
## Summary
Add a setting to select all children (nodes, reroutes, nested groups)
when clicking a group on the canvas.
## Changes
- **What**: New `LiteGraph.Group.SelectChildrenOnClick` boolean setting
(default: `false`). When enabled, selecting a group cascades `select()`
to all its `_children`, and deselecting cascades `deselect()`. Recursion
handles nested groups naturally. No double-move risk — the drag handler
already uses `skipChildren=true`. The setting is wired via `onChange` to
`canvas.groupSelectChildren`, keeping litegraph free of platform
imports.
## Review Focus
- The select/deselect cascading in `LGraphCanvas.select()` /
`deselect()` — verify no infinite recursion risk with deeply nested
groups.
- The `groupSelectChildren` property is set via the setting's `onChange`
callback on `LGraphCanvas.active_canvas` — confirm this covers canvas
re-creation scenarios.
## Screenshots (if applicable)
N/A — behavioral change behind a setting toggle.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9149-feat-select-group-children-on-click-3116d73d365081a1a7b8c82dea95b242)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
## Summary
Adds a comprehensive `TROUBLESHOOTING.md` guide to help developers
resolve common development issues.
## Motivation
a developer reported issues where `pnpm dev` would get stuck on 'nx
serve'. This highlighted the need for centralized troubleshooting
documentation to help developers quickly resolve common issues without
having to wait for help.
## Changes
- Created `TROUBLESHOOTING.md` with FAQ-style documentation
- Added Mermaid flowchart for quick issue diagnosis
- Documented solutions for common problems:
- Development server issues (nx serve hanging)
- Build and TypeScript errors
- Dependency and package management problems
- Testing issues
- Git and branch conflicts
## Structure
The guide includes:
- Quick diagnostic flowchart (Mermaid)
- Frequently Asked Questions with:
- Clear symptoms
- Step-by-step solutions
- Explanations of why issues occur
- Links to community support resources
- Contribution guidelines
## Test Plan
- [x] File created and committed
- [x] Mermaid flowchart renders correctly
- [x] All commands are accurate and tested
- [x] Links to Discord and GitHub are valid
🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7738-docs-Add-TROUBLESHOOTING-md-guide-for-common-development-issues-2d26d73d365081eda291e619b3067bc6)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: snomiao <snomiao@gmail.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
When many nodes are rendered in the transform container, both zoom and
pan can cause FPS drops because the browser re-rasterizes all visible
content at the new transform. `will-change: transform` tells the browser
to keep the layer as a GPU texture and skip re-rasterization during
active interaction, restoring visual quality only after settling.
- Add pointer drag detection so `will-change: transform` covers pan in
addition to zoom. Without this, dragging with 256+ nodes causes jank as
the browser re-rasterizes the entire layer on every frame of the pan.
- Fix settleDelay from 16ms to 256ms. At 16ms the debounce fires between
consecutive wheel events (~50ms apart on a physical mouse), causing
`will-change` to toggle on/off rapidly. Each toggle forces the browser
to promote/demote the compositor layer, which is more expensive than not
having the optimization at all.
- Replace scoped CSS with Tailwind `will-change-transform`.
- Remove per-node `will-change: transform` on `.lg-node`. Promoting each
node to its own compositor layer (256 nodes = 256 GPU textures)
increases memory pressure and compositing overhead, making performance
worse than a single promoted container.
- Previously, the virtual DOM of Nodes was updated during zooming and
dragging, but now this update is avoided through some techniques.
- Using the 3D versions of scale and translate can provide a smoother
experience when dealing with a large number of nodes.
## Test plan
- [x] Unit tests updated and passing
- [x] Manual: verify during both zoom and pan
- [x] Manual: compare pan FPS with 256 nodes before/after
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9649-perf-detect-pointer-drag-in-useTransformSettling-for-pan-optimization-31e6d73d3650818bb2c3ccd01a465140)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
## Summary
- When a media node (LoadImage/LoadAudio/LoadVideo) is selected and the
clipboard contains stale node metadata from a prior Ctrl+C, pasting
skips the node-metadata deserialization so that the paste falls through
to litegraph's default handler instead of incorrectly pasting the old
copied node.
- FixesComfy-Org/ComfyUI#12896
## Root Cause
The paste handler in `usePaste.ts` checks clipboard `text/html` for
`data-metadata` (serialized node data) **before** falling through to
litegraph's default paste. When a user copies a node, then copies a web
image, the browser clipboard may retain the old `data-metadata` in
`text/html` while the image data is not available as a
`DataTransferItem` file. This causes the stale node to be pasted instead
of the image.
## Fix
Skip `pasteClipboardItems()` when a media node is selected, allowing the
paste to fall through to litegraph's default handler which can handle
the clipboard content appropriately.
## Test plan
- [x] Added unit test verifying node metadata paste is skipped when
media node is selected
- [x] Manual: Copy a node → copy a web image → select LoadImage node →
Ctrl+V → verify image is pasted, not the node
## AS IS
https://github.com/user-attachments/assets/210d77d3-5c49-4e38-91b7-b9d9ea0e7ca0
## TO BE
https://github.com/user-attachments/assets/b68e4582-0c57-48b8-9ed9-0b3243bb1554🤖 Generated with [Claude Code](https://claude.com/claude-code)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9773-fix-skip-node-metadata-paste-when-media-node-is-selected-3216d73d3650814d92dadcd0c0ec79c7)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Replace legacy `SearchBox` (PrimeVue) and `SearchBoxV2` with the unified
`SearchInput` (reka-ui) component across all consumers.
## Changes
- **What**: Remove `SearchBox.vue`, `SearchBoxV2.vue`, their tests and
stories. Migrate all 14 consumers to `SearchInput`. Move layout classes
to `ComboboxRoot` for proper flex sizing. Extract filter button/chips in
`NodeLibrarySidebarTab`. Standardize modal search width to `flex-1
max-w-lg`.
- **Dependencies**: None new — `SearchInput` already existed using
reka-ui
## Review Focus
- `NodeLibrarySidebarTab.vue`: filter button and `SearchFilterChip`
rendering moved outside the search component
- `SearchInput.vue`: `className` now applied to `ComboboxRoot` instead
of `ComboboxAnchor` for correct flex layout
- Modal dialogs (`WorkflowTemplateSelectorDialog`, `AssetBrowserModal`,
`SampleModelSelector`) unified to `flex-1 max-w-lg`
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9644-feat-unify-search-components-by-replacing-SearchBox-SearchBoxV2-with-SearchInput-31e6d73d365081ebac55cb265f33b631)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Standardize the repo's Node contract on 24 while centralizing workflow
resolution through `.nvmrc` so local setup, CI, and package metadata
stay aligned from one version file.
## Changes
- **What**: Add `package.json` `engines.node = 24.x`, switch every
`actions/setup-node` workflow in the repo to `node-version-file:
'.nvmrc'`, and update contributor and Playwright docs to point to
`.nvmrc` as the Node source of truth.
## Review Focus
The workflow behavior should be unchanged apart from sourcing the Node
version from `.nvmrc` instead of repeating literals like `20`, `22`,
`24.x`, or `lts/*`. GitHub's formatter also moved the new `engines`
block to the package metadata section near the end of `package.json`.
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: Alexander Brown <drjkl@comfy.org>
## Summary
Refactors the error and subgraph node footer UI by extracting a
dedicated `NodeFooter` component and replacing the CSS `outline`
approach with a layered border overlay for selection/executing state
indicators.
## Changes
- **What**: Extracted `NodeFooter.vue` from `LGraphNode.vue` to
encapsulate the footer tab logic (subgraph enter, error, advanced
inputs). Replaced CSS `outline` with an absolutely-positioned border
overlay div for selection and executing state. Added a separate root
border overlay div for the node body border. Removed unused
`isTransparent` function from `colorUtil.ts`.
- **Dependencies**: None
## Review Focus
- The layered overlay approach (`absolute -inset-[3px] border-3`) for
selection/executing outlines vs the previous `outline-3` approach —
ensures the outline renders outside the node bounds correctly including
the footer area
- `NodeFooter` handles 4 cases: subgraph+error (dual tabs), error only,
subgraph only, advanced inputs — verify edge cases render correctly
- Resize handle bottom offset adjustments for nodes with footers
(`hasFooter`)
## Screenshots
<img width="1142" height="603" alt="image"
src="https://github.com/user-attachments/assets/e0d401f0-8516-4f5f-ab77-48a79530f4bd"
/>
<img width="1175" height="577" alt="image"
src="https://github.com/user-attachments/assets/bcf08fff-728a-491c-add9-5b96d2f3bfce"
/>
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9360-style-Update-error-subgraph-node-footer-design-with-layered-overlay-approach-3186d73d365081b2ac31f166f4d1944a)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
Disable missing model warnings in browser tests by default.
Browser tests run without model files on disk, so workflows that embed
model metadata can render differently in CI than the test actually
intends to cover. The viewport screenshot golden had started depending
on the missing-model popup even though the test is only about restoring
an offscreen viewport.
Set `Comfy.Workflow.ShowMissingModelsWarning` to `false` in the shared
Playwright fixture, keep the missing-model dialog coverage by explicitly
enabling the setting in the dialog tests, and update the viewport
screenshot expectation to the no-popup rendering.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9513-test-disable-missing-model-warnings-in-browser-tests-31b6d73d365081d1908bfe11ec0c3bc2)
by [Unito](https://www.unito.io)
## Summary
Fixes flakey test to only assert that the upload request is made with
the correct URL
## Changes
- **What**
- Replace waitForResponse with waitForRequest for the no_workflow.webp
upload test to only assert the request is initiated with the correct URL
- Move request listener setup before the drag-drop action to avoid race
conditions
- Remove screenshot assertion for the upload case since the upload may
not complete before the screenshot is taken
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9488-fix-update-loadWorkflowInMedia-test-to-only-assert-upload-request-URL-31b6d73d365081f69a9aeb1095da7d60)
by [Unito](https://www.unito.io)