[feat] Node replacement UI (#8604)

## 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>
This commit is contained in:
Jin Yi
2026-02-17 16:33:41 +09:00
committed by GitHub
parent e70484d596
commit efe78b799f
14 changed files with 1707 additions and 58 deletions

View File

@@ -113,7 +113,7 @@ import { pasteImageNode, pasteImageNodes } from '@/composables/usePaste'
export const ANIM_PREVIEW_WIDGET = '$$comfy_animation_preview'
function sanitizeNodeName(string: string) {
export function sanitizeNodeName(string: string) {
let entityMap = {
'&': '',
'<': '',
@@ -1162,16 +1162,6 @@ export class ComfyApp {
return
}
for (let n of nodes) {
// When node replacement is disabled, fall back to hardcoded patches
if (!nodeReplacementStore.isEnabled) {
if (n.type == 'T2IAdapterLoader') n.type = 'ControlNetLoader'
if (n.type == 'ConditioningAverage ') n.type = 'ConditioningAverage'
if (n.type == 'SDV_img2vid_Conditioning')
n.type = 'SVD_img2vid_Conditioning'
if (n.type == 'Load3DAnimation') n.type = 'Load3D'
if (n.type == 'Preview3DAnimation') n.type = 'Preview3D'
}
// Find missing node types
if (!(n.type in LiteGraph.registered_node_types)) {
const replacement = nodeReplacementStore.getReplacementFor(n.type)