## Summary
Replace the Proxy-based proxy widget system with a store-driven
architecture where `promotionStore` and `widgetValueStore` are the
single sources of truth for subgraph widget promotion and widget values,
and `SubgraphNode.widgets` is a synthetic getter composing lightweight
`PromotedWidgetView` objects from store state.
## Motivation
The subgraph widget promotion system previously scattered state across
multiple unsynchronized layers:
- **Persistence**: `node.properties.proxyWidgets` (tuples on the
LiteGraph node)
- **Runtime**: Proxy-based `proxyWidget.ts` with `Overlay` objects,
`DisconnectedWidget` singleton, and `isProxyWidget` type guards
- **UI**: Each Vue component independently calling `parseProxyWidgets()`
via `customRef` hacks
- **Mutation flags**: Imperative `widget.promoted = true/false` set on
`subgraph-opened` events
This led to 4+ independent parsings of the same data, complex cache
invalidation, and no reactive contract between the promotion state and
the rendering layer. Widget values were similarly owned by LiteGraph
with no Vue-reactive backing.
The core principle driving these changes: **Vue owns truth**. Pinia
stores are the canonical source; LiteGraph objects delegate to stores
via getters/setters; Vue components react to store state directly.
## Changes
### New stores (single sources of truth)
- **`promotionStore`** — Reactive `Map<NodeId, PromotionEntry[]>`
tracking which interior widgets are promoted on which SubgraphNode
instances. Graph-scoped by root graph ID to prevent cross-workflow state
collision. Replaces `properties.proxyWidgets` parsing, `customRef`
hacks, `widget.promoted` mutation, and the `subgraph-opened` event
listener.
- **`widgetValueStore`** — Graph-scoped `Map<WidgetKey, WidgetState>`
that is the canonical owner of widget values. `BaseWidget.value`
delegates to this store via getter/setter when a node ID is assigned.
Eliminates the need for Proxy-based value forwarding.
### Synthetic widgets getter (SubgraphNode)
`SubgraphNode.widgets` is now a getter that reads
`promotionStore.getPromotions(rootGraphId, nodeId)` and returns cached
`PromotedWidgetView` objects. No stubs, no Proxies, no fake widgets
persisted in the array. The setter is a no-op — mutations go through
`promotionStore`.
### PromotedWidgetView
A class behind a `createPromotedWidgetView` factory, implementing the
`PromotedWidgetView` interface. Delegates value/type/options/drawing to
the resolved interior widget and stores. Owns positional state (`y`,
`computedHeight`) for canvas layout. Cached by
`PromotedWidgetViewManager` for object-identity stability across frames.
### DOM widget promotion
Promoted DOM widgets (textarea, image upload, etc.) render on the
SubgraphNode surface via `positionOverride` in `domWidgetStore`.
`DomWidgets.vue` checks for overrides and uses the SubgraphNode's
coordinates instead of the interior node's.
### Promoted previews
New `usePromotedPreviews` composable resolves image/audio/video preview
widgets from promoted entries, enabling SubgraphNodes to display
previews of interior preview nodes.
### Deleted
- `proxyWidget.ts` (257 lines) — Proxy handler, `Overlay`,
`newProxyWidget`, `isProxyWidget`
- `DisconnectedWidget.ts` (39 lines) — Singleton Proxy target
- `useValueTransform.ts` (32 lines) — Replaced by store delegation
### Key architectural changes
- `BaseWidget.value` getter/setter delegates to `widgetValueStore` when
node ID is set
- `LGraph.add()` reordered: `node.graph` assigned before widget
`setNodeId` (enables store registration)
- `LGraph.clear()` cleans up graph-scoped stores to prevent stale
entries across workflow switches
- `promotionStore` and `widgetValueStore` state nested under root graph
UUID for multi-workflow isolation
- `SubgraphNode.serialize()` writes promotions back to
`properties.proxyWidgets` for persistence compatibility
- Legacy `-1` promotion entries resolved and migrated on first load with
dev warning
## Test coverage
- **3,700+ lines of new/updated tests** across 36 test files
- **Unit**: `promotionStore.test.ts`, `widgetValueStore.test.ts`,
`promotedWidgetView.test.ts` (921 lines),
`subgraphNodePromotion.test.ts`, `proxyWidgetUtils.test.ts`,
`DomWidgets.test.ts`, `PromotedWidgetViewManager.test.ts`,
`usePromotedPreviews.test.ts`, `resolvePromotedWidget.test.ts`,
`subgraphPseudoWidgetCache.test.ts`
- **E2E**: `subgraphPromotion.spec.ts` (622 lines) — promote/demote,
manual/auto promotion, paste preservation, seed control augmentation,
image preview promotion; `imagePreview.spec.ts` extended with
multi-promoted-preview coverage
- **Fixtures**: 2 new subgraph workflow fixtures for preview promotion
scenarios
## Review focus
- Graph-scoped store keying (`rootGraphId`) — verify isolation across
workflows/tabs and cleanup on `LGraph.clear()`
- `PromotedWidgetView` positional stability — `_arrangeWidgets` writes
to `y`/`computedHeight` on cached objects; getter returns fresh array
but stable object references
- DOM widget position override lifecycle — overrides set on promote,
cleared on demote/removal/subgraph navigation
- Legacy `-1` entry migration — resolved and written back on first load;
unresolvable entries dropped with dev warning
- Serialization round-trip — `promotionStore` state →
`properties.proxyWidgets` on serialize, hydrated back on configure
## Diff breakdown (excluding lockfile)
- 153 files changed, ~7,500 insertions, ~1,900 deletions (excluding
pnpm-lock.yaml churn)
- ~3,700 lines are tests
- ~300 lines deleted (proxyWidget.ts, DisconnectedWidget.ts,
useValueTransform.ts)
<!-- Fixes #ISSUE_NUMBER -->
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8856-feat-synthetic-widgets-getter-for-SubgraphNode-proxy-widget-v2-3076d73d365081c7b517f5ec7cb514f3)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Fix subgraph unpacking creating spurious links to widget inputs (e.g.
seed) when the subgraph contains ComfySwitchNode with duplicate internal
links.
## Changes
- **What**: Two fixes in `_unpackSubgraphImpl`:
1. Strip links from serialized node data **before** `configure()` so
`onConnectionsChange` doesn't resolve subgraph-internal link IDs against
the parent graph's link map (which may contain unrelated links with
colliding numeric IDs).
2. Deduplicate links by `(origin, origin_slot, target, target_slot)`
before reconnecting, preventing repeated disconnect/reconnect cycles on
widget inputs that cause slot index drift.
## Review Focus
- The link-stripping before `configure()` mirrors what
`LGraphNode.clone()` already does — nodes should be configured without
stale link references when links will be recreated separately.
- Deduplication is defensive against malformed subgraph data; the
duplicate links in the reproduction workflow likely originated from a
prior serialization bug.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9046-fix-subgraph-unpacking-creates-extra-link-to-seed-widget-30e6d73d36508125a5fefa1309485516)
by [Unito](https://www.unito.io)
## Summary
- Fixes SaveWebM node showing "Error loading image" in Vue nodes mode
- Extracts `isAnimatedOutput`/`isVideoOutput` utility functions from
inline logic in `unsafeUpdatePreviews` so both the litegraph canvas
renderer and Vue nodes renderer can detect video output directly from
execution data
- Uses output-based detection in `imagePreviewStore.isImageOutputs` to
avoid applying image preview format conversion to video files
## Background
In Vue nodes mode, `nodeMedia` relied on `node.previewMediaType` to
determine if output is video. This property is only set via
`onDrawBackground` → `unsafeUpdatePreviews` in the litegraph canvas
path, which doesn't run in Vue nodes mode. This caused webm output to
render via `<img>` instead of `<video>`.
## Before
https://github.com/user-attachments/assets/36f8a033-0021-4351-8f82-d19e3faa80c2
## After
https://github.com/user-attachments/assets/6558d261-d70e-4968-9637-6c24532e23ac
## Test plan
- [x] `pnpm typecheck` passes
- [x] `pnpm lint` passes
- [x] `pnpm test:unit` passes (4500 tests)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8943-fix-detect-video-output-from-data-in-Vue-nodes-mode-30a6d73d365081e98e91d6d1dcc88785)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Add node replacement UI to the missing nodes dialog. Users can select
and replace deprecated/missing nodes with compatible alternatives
directly from the dialog.
## Changes
- Classify missing nodes into **Replaceable** (quick fix) and **Install
Required** sections
- Add select-all checkbox + per-node checkboxes for batch replacement
- `useNodeReplacement` composable handles in-place node replacement on
the graph:
- Simple replacement (configure+copy) for nodes without mapping
- Input/output connection remapping for nodes with mapping
- Widget value transfer via `old_widget_ids`
- Dot-notation input handling for Autogrow/DynamicCombo
- Undo/redo support via `changeTracker` (try/finally)
- Title and properties preservation
- Footer UX: "Skip for Now" button when all nodes are replaceable (cloud
+ OSS)
- Auto-close dialog when all replaceable nodes are replaced and no
non-replaceable remain
- Settings navigation link from "Don't show again" checkbox
- 505-line unit test suite for `useNodeReplacement`
## Review Focus
- `useNodeReplacement.ts` — core graph manipulation logic
- `MissingNodesContent.vue` — checkbox selection state management
- `MissingNodesFooter.vue` — conditional button rendering (cloud vs OSS
vs all-replaceable)
[screen-capture.webm](https://github.com/user-attachments/assets/7dae891c-926c-4f26-987f-9637c4a2ca16)
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8604-feat-Node-replacement-UI-2fd6d73d36508148a371dabb8f4115af)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Add `ensureGlobalIdUniqueness` to reassign duplicate node IDs across
subgraphs when loading workflows, gated behind an experimental setting.
## Changes
- **What**: Shared `LGraphState` between root graph and subgraphs so ID
counters are global. Added `ensureGlobalIdUniqueness()` method that
detects and remaps colliding node IDs in subgraphs, preserving root
graph IDs as canonical and patching link references. Gated behind
`Comfy.Graph.DeduplicateSubgraphNodeIds` (experimental, default
`false`).
- **Dependencies**: None
## Review Focus
- Shared state override on `Subgraph` (getter delegates to root, setter
is no-op) — verify no existing code sets `subgraph.state` directly.
- `Math.max` state merging in `configure()` prevents ID counter
regression when loading subgraph definitions.
- Feature flag wiring: static property on `LGraph`, synced from settings
via `useLitegraphSettings`.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8762-feat-deduplicate-subgraph-node-IDs-on-workflow-load-experimental-3036d73d36508184b6cee5876dc4d935)
by [Unito](https://www.unito.io)
---------
Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
## Summary
Fixes PNG iTXt chunk parsing to comply with the PNG specification.
## Problem
The current iTXt parser incorrectly reads the text content immediately
after the keyword null terminator, but iTXt chunks have additional
fields:
- Compression flag (1 byte)
- Compression method (1 byte)
- Language tag (null-terminated)
- Translated keyword (null-terminated)
- Text content
This caused PNGs that correctly follow the spec to fail loading with
JSON parse errors due to leading null bytes.
## Solution
Skip the compression flag, method, language tag, and translated keyword
fields before reading the text content.
Fixes#8150
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-8530-fix-properly-parse-PNG-iTXt-chunks-per-specification-2fa6d73d36508189bef4cc5fa3899096)
by [Unito](https://www.unito.io)
---------
Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
## Summary
Fixes issue when dragging a group that had inner groups when in vue
mode.
When dragging the outer group in Vue mode:
1. getAllNestedItems(selected) returns ALL items: outer group + inner
groups + nodes
2. moveChildNodesInGroupVueMode loops through all items
3. For outer group G1: calls G1.move(delta, true) then
moveGroupChildren(G1, ...)
4. moveGroupChildren calls G2.move(delta) (no skipChildren) - this moves
G2 AND G2's children!
5. Then the loop reaches G2: calls G2.move(delta, true) - moves G2 again
6. Plus moveGroupChildren(G2, ...) processes G2's children again
This PR fixes it by adding `skipChildren=true` to the `move` call.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7447-fix-inner-groups-being-moved-double-when-moving-outer-group-in-vue-mode-2c86d73d365081ce97abec682f2a8518)
by [Unito](https://www.unito.io)
## Summary
This change extends
https://github.com/Comfy-Org/ComfyUI_frontend/pull/7154 by making sure
the `prompt` metadata tag is parsed before the legacy A1111 fallback
when files are dropped onto the canvas.
ComfyUI embeds two structured payloads into every first-class export
format we support (PNG, WEBP, WEBM, MP4/MOV/M4V, GLB, SVG, MP3,
OGG/FLAC, etc.): `workflow`, which is the full editor JSON with layout
state, and `prompt`, which is the API graph sent to `/prompt`.
During import we try format-specific decoders first and only as a last
resort look for an A1111 file by scanning text chunks for a `parameters`
entry. That compatibility path was always meant to be a best-effort
option, but when we refactored the loader it accidentally enforced the
order `workflow → parameters → prompt`. As soon as a dropped asset
contained a `parameters` chunk—something Image Saver’s “A1111
compatibility” mode always adds—the A1111 converter activated and
blocked the subsequent `prompt` loading logic.
PR #7154 already lifted `workflow` ahead of the fallback, yet any file
lacking the `workflow` chunk but holding both `prompt` and `parameters`
still regressed. Reordering to `workflow → prompt → parameters`
preserves the compatibility shim for genuine A1111 exports while
guaranteeing native Comfy metadata always wins, eliminating the entire
class of failures triggered merely by the presence of the word
`parameters` in an unrelated metadata chunk.
Fixes https://github.com/Comfy-Org/ComfyUI_frontend/issues/7096, fixes
https://github.com/Comfy-Org/ComfyUI_frontend/issues/6988
## Related
(fixed by https://github.com/Comfy-Org/ComfyUI_frontend/pull/7154)
- https://github.com/Comfy-Org/ComfyUI_frontend/issues/6633
- https://github.com/Comfy-Org/ComfyUI_frontend/issues/6561
---------
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
## Cause
When graphs are actually exported, several layers of cleanup are
applied. Among these is link compression. Any widgets with inputs that
aren't used do not have inputs stored in the workflow. This was
implemented for backwards compatibility with the old "convert to input"
system for widgets. As part of this process, the target_slots on links
are rewritten such that they point to the index of the widget as if
unconnected widgets did not exist.
This "incorrect" state for links is only corrected AFTER a workflow has
loaded because the 'fix' method needs nodes to be initialized in order
to calculate the correct target_slot
This becomes a problem when subgraphs are introduced. SubgraphInputs
need to resolve a link to its target slot in order to construct a clone
of the linked widget DURING the loading process. Since this target slot
is not accurate, this can result in the cloned widget having the wrong
type.
For a minimal reproduction:
- Create a subgraph with an Empty Latent Image with batch_size linked to
the Subgraph Input
- Export the workflow
- On load, the batch_size has step and min attributes which incorrectly
correspond to width
## Fix
There's multiple possible ways to address this and input on direction is
appreciated
- Fix links before loading graph
- Likely to break with any dynamic state
- Fix links, then load graph again
- Ugly, bad performance, dynamic state may require multiple passes to
correctly ripple
- In the Subgraph code, ignore target_slot and instead `.find()` input
with matching linkId (proposed)
- Promising, but means accepting that state is just wrong sometimes.
Another forever footgun.
- Entirely remove the input compression
- Some people may complain, and old workflows still need to be supported
- Only remove target_slot redirection inside subgraphs
- Creates ugly logical difference between what happens inside and
outside subgraphs.
- Still leaves old workflows broken
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-7388-Remove-target_slot-compression-from-subgraph-exports-2c66d73d3650815d8c96c5047958ab67)
by [Unito](https://www.unito.io)
## Summary
Fixes https://github.com/Comfy-Org/ComfyUI_frontend/issues/5692 by
making widget link connection status trigger on change so Vue widgets
with connected links could properly switch to the `disabled` state when
they are implicitly converted to inputs.
## Changes
- **What**: Added `node:slot-links:changed` event tracking and reactive
slot data synchronization for Vue widgets
```mermaid
graph TD
A[Widget Link Change] --> B[NodeInputSlot.link setter]
B --> C{Is Widget Input?}
C -->|Yes| D[Trigger slot-links:changed]
C -->|No| E[End]
D --> F[Graph Event Handler]
F --> G[syncNodeSlotData]
G --> H[Update Vue Reactive Data]
H --> I[Widget Re-render]
style A fill:#f9f9f9,stroke:#333,color:#000
style I fill:#f9f9f9,stroke:#333,color:#000
```
## Review Focus
Widget reactivity performance with frequent link changes and event
handler memory management in graph operations.
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5834-fix-Vue-node-widgets-should-be-in-disabled-state-if-their-slots-are-connected-with-a-link-27c6d73d365081f6a6c3c1ddc3905c5e)
by [Unito](https://www.unito.io)
## Summary
Added `canvasOnly` flag to runtime-generated widgets to prevent Vue
renderer from displaying them while keeping canvas functionality intact.
## Changes
- **What**: Added `canvasOnly` widget option to hide upload, webcam, and
refresh widgets from Vue renderer
In the Canvas (LiteGraph) system, there was a small set of widgets with
strictly defined components. There, if we wanted some unique or
relatively complex behavior (like an upload butotn), we needed to create
a separate widget that would be coupled to the original widget at
runtime (and would not be serialized).
In the Vue renderer system, we can simply add flags to the inputSpec or
widget options and conditionally render complex UI additions -- i.e.,
there is no need for the hard-to-maintain runtime widget associations.
Expressing such things entirely in the view layer simplifies business
logic related to graph state, as we no longer need to account for
preserving the connections between runtime widgets and their special
siblings -- we also do not need to worry about the implications for
state serialization.
## Related
- https://github.com/Comfy-Org/ComfyUI_frontend/pull/5798
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-5831-designate-canvasOnly-on-runtime-generated-virtual-widgets-so-they-are-hidden-in-Vue-ren-27c6d73d365081fb8641feec010190df)
by [Unito](https://www.unito.io)
---------
Co-authored-by: github-actions <github-actions@github.com>
* temp: move tailwind calls out of the layer
* temp: ts tailwind config
* upgrade: Tailwind v4
This got a little out of hand.
Had to add a relative reference to the stylesheet in any component that uses @apply instead of the utility classes directly.
* upgrade: bg-opacity is now a modifier
* fix: Classic menu buttons assume a border
* Update test expectations [skip ci]
* fix: New preflight removal pattern
* fix: Skeletons don't have skin
* Update test expectations [skip ci]
* fix: Missing @reference
* [auto-fix] Apply ESLint and Prettier fixes
---------
Co-authored-by: github-actions <github-actions@github.com>
Co-authored-by: GitHub Action <action@github.com>
* move subgraph test assets into subfolder
* [refactor] Organize browser test assets into logical folders
Reorganized test assets for better maintainability:
- groupnodes/: GroupNode feature tests
- groups/: Visual grouping tests
- missing/: Missing nodes/models tests
- links/: Link-related tests
- inputs/: Input widget tests
- widgets/: Widget-specific tests
- nodes/: Node-related tests
- workflowInMedia/: Workflow media files
Updated all loadWorkflow references to use new folder structure.
Fixed programmatic filename references to prevent test failures.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
* [fix] Update mobile test to use new asset path
* [fix] Update remaining loadWorkflow calls to use new folder structure
* [fix] Fix remaining programmatic filename references
* [fix] Run prettier formatting
* [fix] Fix setupWorkflowsDirectory references to use correct folder paths
* [refactor] Rename subgraph folder to subgraphs for consistency
* [fix] Fix breadcrumb name in subgraph DOM widget test
* Update test expectations [skip ci]
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: github-actions <github-actions@github.com>