From 1c3984a17837abf96fa1ca383e5ba311c8406c63 Mon Sep 17 00:00:00 2001
From: jaeone94 <89377375+jaeone94@users.noreply.github.com>
Date: Fri, 27 Feb 2026 10:37:48 +0900
Subject: [PATCH] feat: add node replacement UI to Errors Tab (#9253)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary
Adds a node replacement UI to the Errors Tab so users can swap missing
nodes with compatible alternatives directly from the error panel,
without opening a separate dialog.
## Changes
- **What**: New `SwapNodesCard` and `SwapNodeGroupRow` components render
swap groups in the Errors Tab; each group shows the missing node type,
its instances (with locate buttons), and a Replace button. Added
`useMissingNodeScan` composable to scan the graph for missing nodes and
populate `executionErrorStore`. Added `removeMissingNodesByType()` to
`executionErrorStore` so replaced nodes are pruned from the error list
reactively.
## Bug Fixes Found During Implementation
### Bug 1: Replaced nodes render as empty shells until page refresh
`replaceWithMapping()` directly mutates `_nodes[idx]`, bypassing the Vue
rendering pipeline entirely. Because the replacement node reuses the
same ID, `vueNodeData` retains the stale entry from the old placeholder
(`hasErrors: true`, empty widgets/inputs). `graph.setDirtyCanvas()` only
repaints the LiteGraph canvas and has no effect on Vue.
**Fix**: After `replaceWithMapping()`, manually call
`nodeGraph.onNodeAdded?.(newNode)` to trigger `handleNodeAdded` in
`useGraphNodeManager`, which runs `extractVueNodeData(newNode)` and
updates `vueNodeData` correctly. Also added a guard in `handleNodeAdded`
to skip `layoutStore.createNode()` when a layout for the same ID already
exists, preventing a duplicate `spatialIndex.insert()`.
### Bug 2: Missing node error list overwritten by incomplete server
response
Two compounding issues: (A) the server's `missing_node_type` error only
reports the *first* missing node — the old handler parsed this and
called `surfaceMissingNodes([singleNode])`, overwriting the full list
collected at load time. (B) `queuePrompt()` calls `clearAllErrors()`
before the API request; if the subsequent rescan used the stale
`has_errors` flag and found nothing, the missing nodes were permanently
lost.
**Fix**: Created `useMissingNodeScan.ts` which scans
`LiteGraph.registered_node_types` directly (not `has_errors`). The
`missing_node_type` catch block in `app.ts` now calls
`rescanAndSurfaceMissingNodes(this.rootGraph)` instead of parsing the
server's partial response.
## Review Focus
- `handleReplaceNode` removes the group from the store only when
`replaceNodesInPlace` returns at least one replaced node — should we
always clear, or only on full success?
- `useMissingNodeScan` re-scans on every execution-error change; confirm
no performance concerns for large graphs with many subgraphs.
## Screenshots
https://github.com/user-attachments/assets/78310fc4-0424-4920-b369-cef60a123d50
https://github.com/user-attachments/assets/3d2fd5e1-5e85-4c20-86aa-8bf920e86987
┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9253-feat-add-node-replacement-UI-to-Errors-Tab-3136d73d365081718d4ddfd628cb4449)
by [Unito](https://www.unito.io)
---
.../errors/SwapNodeGroupRow.vue | 150 ++++++++++++++++++
.../rightSidePanel/errors/SwapNodesCard.vue | 38 +++++
.../rightSidePanel/errors/TabErrors.vue | 51 +++++-
src/components/rightSidePanel/errors/types.ts | 1 +
.../rightSidePanel/errors/useErrorGroups.ts | 56 ++++++-
src/composables/graph/useGraphNodeManager.ts | 6 +
src/composables/useMissingNodeScan.ts | 44 +++++
src/locales/en/main.json | 9 +-
.../nodeReplacement/useNodeReplacement.ts | 17 ++
src/scripts/app.ts | 41 +----
src/stores/executionErrorStore.ts | 12 ++
.../manager/types/missingNodeErrorTypes.ts | 9 --
.../utils/missingNodeErrorUtil.test.ts | 115 ++++++--------
.../manager/utils/missingNodeErrorUtil.ts | 44 -----
14 files changed, 424 insertions(+), 169 deletions(-)
create mode 100644 src/components/rightSidePanel/errors/SwapNodeGroupRow.vue
create mode 100644 src/components/rightSidePanel/errors/SwapNodesCard.vue
create mode 100644 src/composables/useMissingNodeScan.ts
delete mode 100644 src/workbench/extensions/manager/types/missingNodeErrorTypes.ts
diff --git a/src/components/rightSidePanel/errors/SwapNodeGroupRow.vue b/src/components/rightSidePanel/errors/SwapNodeGroupRow.vue
new file mode 100644
index 0000000000..82dcf3b6be
--- /dev/null
+++ b/src/components/rightSidePanel/errors/SwapNodeGroupRow.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+ {{ `${group.type} (${group.nodeTypes.length})` }}
+
+
+
+
+
+
+
+
+
+
+
+
+ #{{ nodeType.nodeId }}
+
+
+ {{ getLabel(nodeType) }}
+
+
+
+
+
+
+
+
+
+
+ {{
+ t('nodeReplacement.willBeReplacedBy', 'This node will be replaced by:')
+ }}
+ {{
+ group.newNodeId ?? t('nodeReplacement.unknownNode', 'Unknown')
+ }}
+
+
+
+
+
+
+
+ {{ t('nodeReplacement.replaceNode', 'Replace Node') }}
+
+
+
+
+
+
+
diff --git a/src/components/rightSidePanel/errors/SwapNodesCard.vue b/src/components/rightSidePanel/errors/SwapNodesCard.vue
new file mode 100644
index 0000000000..d27e24b7dc
--- /dev/null
+++ b/src/components/rightSidePanel/errors/SwapNodesCard.vue
@@ -0,0 +1,38 @@
+
+
+
+
+ {{
+ t(
+ 'nodeReplacement.swapNodesGuide',
+ 'The following nodes can be automatically replaced with compatible alternatives.'
+ )
+ }}
+
+
+
+
+
+
+
diff --git a/src/components/rightSidePanel/errors/TabErrors.vue b/src/components/rightSidePanel/errors/TabErrors.vue
index c743eafca2..7f40b9e63b 100644
--- a/src/components/rightSidePanel/errors/TabErrors.vue
+++ b/src/components/rightSidePanel/errors/TabErrors.vue
@@ -27,7 +27,11 @@
:key="group.title"
:collapse="collapseState[group.title] ?? false"
class="border-b border-interface-stroke"
- :size="group.type === 'missing_node' ? 'lg' : 'default'"
+ :size="
+ group.type === 'missing_node' || group.type === 'swap_nodes'
+ ? 'lg'
+ : 'default'
+ "
@update:collapse="collapseState[group.title] = $event"
>
@@ -40,7 +44,9 @@
{{
group.type === 'missing_node'
? `${group.title} (${missingPackGroups.length})`
- : group.title
+ : group.type === 'swap_nodes'
+ ? `${group.title} (${swapNodeGroups.length})`
+ : group.title
}}
+
+ {{ t('nodeReplacement.replaceAll', 'Replace All') }}
+
@@ -82,8 +103,16 @@
@open-manager-info="handleOpenManagerInfo"
/>
+
+
+
-
+
missingNodePacks.value)
+const { replaceNodesInPlace } = useNodeReplacement()
+const executionErrorStore = useExecutionErrorStore()
const searchQuery = ref('')
@@ -183,7 +217,8 @@ const {
isSingleNodeSelected,
errorNodeCache,
missingNodeCache,
- missingPackGroups
+ missingPackGroups,
+ swapNodeGroups
} = useErrorGroups(searchQuery, t)
/**
@@ -229,6 +264,14 @@ function handleOpenManagerInfo(packId: string) {
}
}
+function handleReplaceAll() {
+ const allNodeTypes = swapNodeGroups.value.flatMap((g) => g.nodeTypes)
+ const replaced = replaceNodesInPlace(allNodeTypes)
+ if (replaced.length > 0) {
+ executionErrorStore.removeMissingNodesByType(replaced)
+ }
+}
+
function handleEnterSubgraph(nodeId: string) {
enterSubgraph(nodeId, errorNodeCache.value)
}
diff --git a/src/components/rightSidePanel/errors/types.ts b/src/components/rightSidePanel/errors/types.ts
index 718d0e9966..223fe4650d 100644
--- a/src/components/rightSidePanel/errors/types.ts
+++ b/src/components/rightSidePanel/errors/types.ts
@@ -22,3 +22,4 @@ export type ErrorGroup =
priority: number
}
| { type: 'missing_node'; title: string; priority: number }
+ | { type: 'swap_nodes'; title: string; priority: number }
diff --git a/src/components/rightSidePanel/errors/useErrorGroups.ts b/src/components/rightSidePanel/errors/useErrorGroups.ts
index 610a56e720..08135703a7 100644
--- a/src/components/rightSidePanel/errors/useErrorGroups.ts
+++ b/src/components/rightSidePanel/errors/useErrorGroups.ts
@@ -42,6 +42,12 @@ export interface MissingPackGroup {
isResolving: boolean
}
+export interface SwapNodeGroup {
+ type: string
+ newNodeId: string | undefined
+ nodeTypes: MissingNodeType[]
+}
+
interface GroupEntry {
type: 'execution'
priority: number
@@ -444,6 +450,8 @@ export function useErrorGroups(
const resolvingKeys = new Set()
for (const nodeType of nodeTypes) {
+ if (typeof nodeType !== 'string' && nodeType.isReplaceable) continue
+
let packId: string | null
if (typeof nodeType === 'string') {
@@ -495,18 +503,53 @@ export function useErrorGroups(
}))
})
+ const swapNodeGroups = computed(() => {
+ const nodeTypes = executionErrorStore.missingNodesError?.nodeTypes ?? []
+ const map = new Map()
+
+ for (const nodeType of nodeTypes) {
+ if (typeof nodeType === 'string' || !nodeType.isReplaceable) continue
+
+ const typeName = nodeType.type
+ const existing = map.get(typeName)
+ if (existing) {
+ existing.nodeTypes.push(nodeType)
+ } else {
+ map.set(typeName, {
+ type: typeName,
+ newNodeId: nodeType.replacement?.new_node_id,
+ nodeTypes: [nodeType]
+ })
+ }
+ }
+
+ return Array.from(map.values()).sort((a, b) => a.type.localeCompare(b.type))
+ })
+
/** Builds an ErrorGroup from missingNodesError. Returns [] when none present. */
function buildMissingNodeGroups(): ErrorGroup[] {
const error = executionErrorStore.missingNodesError
if (!error) return []
- return [
- {
+ const groups: ErrorGroup[] = []
+
+ if (swapNodeGroups.value.length > 0) {
+ groups.push({
+ type: 'swap_nodes' as const,
+ title: st('nodeReplacement.swapNodesTitle', 'Swap Nodes'),
+ priority: 0
+ })
+ }
+
+ if (missingPackGroups.value.length > 0) {
+ groups.push({
type: 'missing_node' as const,
title: error.message,
- priority: 0
- }
- ]
+ priority: 1
+ })
+ }
+
+ return groups.sort((a, b) => a.priority - b.priority)
}
const allErrorGroups = computed(() => {
@@ -564,6 +607,7 @@ export function useErrorGroups(
errorNodeCache,
missingNodeCache,
groupedErrorMessages,
- missingPackGroups
+ missingPackGroups,
+ swapNodeGroups
}
}
diff --git a/src/composables/graph/useGraphNodeManager.ts b/src/composables/graph/useGraphNodeManager.ts
index be3d8bd8e0..71438e6acb 100644
--- a/src/composables/graph/useGraphNodeManager.ts
+++ b/src/composables/graph/useGraphNodeManager.ts
@@ -14,6 +14,7 @@ import type {
} from '@/lib/litegraph/src/interfaces'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
+import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import { LayoutSource } from '@/renderer/core/layout/types'
import type { NodeId } from '@/renderer/core/layout/types'
import type { InputSpec } from '@/schemas/nodeDef/nodeDefSchemaV2'
@@ -442,6 +443,11 @@ export function useGraphNodeManager(graph: LGraph): GraphNodeManager {
const nodePosition = { x: node.pos[0], y: node.pos[1] }
const nodeSize = { width: node.size[0], height: node.size[1] }
+ // Skip layout creation if it already exists
+ // (e.g. in-place node replacement where the old node's layout is reused for the new node with the same ID).
+ const existingLayout = layoutStore.getNodeLayoutRef(id).value
+ if (existingLayout) return
+
// Add node to layout store with final positions
setSource(LayoutSource.Canvas)
void createNode(id, {
diff --git a/src/composables/useMissingNodeScan.ts b/src/composables/useMissingNodeScan.ts
new file mode 100644
index 0000000000..4c7be563ee
--- /dev/null
+++ b/src/composables/useMissingNodeScan.ts
@@ -0,0 +1,44 @@
+import { LiteGraph } from '@/lib/litegraph/src/litegraph'
+import type { LGraph } from '@/lib/litegraph/src/litegraph'
+import { useNodeReplacementStore } from '@/platform/nodeReplacement/nodeReplacementStore'
+import { useExecutionErrorStore } from '@/stores/executionErrorStore'
+import type { MissingNodeType } from '@/types/comfy'
+import {
+ collectAllNodes,
+ getExecutionIdByNode
+} from '@/utils/graphTraversalUtil'
+import { getCnrIdFromNode } from '@/workbench/extensions/manager/utils/missingNodeErrorUtil'
+
+/** Scan the live graph for unregistered node types and build a full MissingNodeType list. */
+function scanMissingNodes(rootGraph: LGraph): MissingNodeType[] {
+ const nodeReplacementStore = useNodeReplacementStore()
+ const missingNodeTypes: MissingNodeType[] = []
+
+ const allNodes = collectAllNodes(rootGraph)
+
+ for (const node of allNodes) {
+ const originalType = node.last_serialization?.type ?? node.type ?? 'Unknown'
+
+ if (originalType in LiteGraph.registered_node_types) continue
+
+ const cnrId = getCnrIdFromNode(node)
+ const replacement = nodeReplacementStore.getReplacementFor(originalType)
+ const executionId = getExecutionIdByNode(rootGraph, node)
+
+ missingNodeTypes.push({
+ type: originalType,
+ nodeId: executionId ?? String(node.id),
+ cnrId,
+ isReplaceable: replacement !== null,
+ replacement: replacement ?? undefined
+ })
+ }
+
+ return missingNodeTypes
+}
+
+/** Re-scan the graph for missing nodes and update the error store. */
+export function rescanAndSurfaceMissingNodes(rootGraph: LGraph): void {
+ const types = scanMissingNodes(rootGraph)
+ useExecutionErrorStore().surfaceMissingNodes(types)
+}
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 213869a7e4..1af245a216 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -3078,7 +3078,14 @@
"openNodeManager": "Open Node Manager",
"skipForNow": "Skip for Now",
"installMissingNodes": "Install Missing Nodes",
- "replaceWarning": "This will permanently modify the workflow. Save a copy first if unsure."
+ "replaceWarning": "This will permanently modify the workflow. Save a copy first if unsure.",
+ "swapNodesGuide": "The following nodes can be automatically replaced with compatible alternatives.",
+ "willBeReplacedBy": "This node will be replaced by:",
+ "replaceNode": "Replace Node",
+ "replaceAll": "Replace All",
+ "unknownNode": "Unknown",
+ "replaceAllWarning": "Replaces all available nodes in this group.",
+ "swapNodesTitle": "Swap Nodes"
},
"rightSidePanel": {
"togglePanel": "Toggle properties panel",
diff --git a/src/platform/nodeReplacement/useNodeReplacement.ts b/src/platform/nodeReplacement/useNodeReplacement.ts
index 5b39cb90dc..270fdea6e5 100644
--- a/src/platform/nodeReplacement/useNodeReplacement.ts
+++ b/src/platform/nodeReplacement/useNodeReplacement.ts
@@ -261,6 +261,10 @@ export function useNodeReplacement() {
}
replaceWithMapping(node, newNode, effectiveReplacement, nodeGraph, idx)
+ // Refresh Vue node data — replaceWithMapping bypasses graph.add()
+ // so onNodeAdded must be called explicitly to update VueNodeData.
+ nodeGraph.onNodeAdded?.(newNode)
+
if (!replacedTypes.includes(match.type)) {
replacedTypes.push(match.type)
}
@@ -279,6 +283,19 @@ export function useNodeReplacement() {
life: 3000
})
}
+ } catch (error) {
+ console.error('Failed to replace nodes:', error)
+ if (replacedTypes.length > 0) {
+ graph.updateExecutionOrder()
+ graph.setDirtyCanvas(true, true)
+ }
+ toastStore.add({
+ severity: 'error',
+ summary: t('g.error', 'Error'),
+ detail: t('nodeReplacement.replaceFailed', 'Failed to replace nodes'),
+ life: 5000
+ })
+ return replacedTypes
} finally {
changeTracker?.afterChange()
}
diff --git a/src/scripts/app.ts b/src/scripts/app.ts
index 8661d6134e..14851afea4 100644
--- a/src/scripts/app.ts
+++ b/src/scripts/app.ts
@@ -79,12 +79,8 @@ import type { ComfyExtension, MissingNodeType } from '@/types/comfy'
import type { ExtensionManager } from '@/types/extensionTypes'
import type { NodeExecutionId } from '@/types/nodeIdentification'
import { graphToPrompt } from '@/utils/executionUtil'
-import type { MissingNodeTypeExtraInfo } from '@/workbench/extensions/manager/types/missingNodeErrorTypes'
-import {
- createMissingNodeTypeFromError,
- getCnrIdFromNode,
- getCnrIdFromProperties
-} from '@/workbench/extensions/manager/utils/missingNodeErrorUtil'
+import { getCnrIdFromProperties } from '@/workbench/extensions/manager/utils/missingNodeErrorUtil'
+import { rescanAndSurfaceMissingNodes } from '@/composables/useMissingNodeScan'
import { anyItemOverlapsRect } from '@/utils/mathUtil'
import {
collectAllNodes,
@@ -1190,7 +1186,7 @@ export class ComfyApp {
const embeddedModels: ModelFile[] = []
const nodeReplacementStore = useNodeReplacementStore()
-
+ await nodeReplacementStore.load()
const collectMissingNodesAndModels = (
nodes: ComfyWorkflowJSON['nodes'],
pathPrefix: string = '',
@@ -1527,35 +1523,8 @@ export class ComfyApp {
typeof error.response.error === 'object' &&
error.response.error?.type === 'missing_node_type'
) {
- const extraInfo = (error.response.error.extra_info ??
- {}) as MissingNodeTypeExtraInfo
-
- let graphNode = null
- if (extraInfo.node_id && this.rootGraph) {
- graphNode = getNodeByExecutionId(
- this.rootGraph,
- extraInfo.node_id
- )
- }
-
- const enrichedExtraInfo: MissingNodeTypeExtraInfo = {
- ...extraInfo,
- class_type: extraInfo.class_type ?? graphNode?.type,
- node_title: extraInfo.node_title ?? graphNode?.title
- }
-
- const missingNodeType =
- createMissingNodeTypeFromError(enrichedExtraInfo)
-
- if (
- graphNode &&
- typeof missingNodeType !== 'string' &&
- !missingNodeType.cnrId
- ) {
- missingNodeType.cnrId = getCnrIdFromNode(graphNode)
- }
-
- this.showMissingNodesError([missingNodeType])
+ // Re-scan the full graph instead of using the server's single-node response.
+ rescanAndSurfaceMissingNodes(this.rootGraph)
} else if (
!useSettingStore().get('Comfy.RightSidePanel.ShowErrorsTab') ||
!(error instanceof PromptExecutionError)
diff --git a/src/stores/executionErrorStore.ts b/src/stores/executionErrorStore.ts
index 620a1ffcf1..dbe6935d44 100644
--- a/src/stores/executionErrorStore.ts
+++ b/src/stores/executionErrorStore.ts
@@ -112,6 +112,17 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
}
}
+ /** Remove specific node types from the missing nodes list (e.g. after replacement). */
+ function removeMissingNodesByType(typesToRemove: string[]) {
+ if (!missingNodesError.value) return
+ const removeSet = new Set(typesToRemove)
+ const remaining = missingNodesError.value.nodeTypes.filter((node) => {
+ const nodeType = typeof node === 'string' ? node : node.type
+ return !removeSet.has(nodeType)
+ })
+ setMissingNodeTypes(remaining)
+ }
+
function setMissingNodeTypes(types: MissingNodeType[]) {
if (!types.length) {
missingNodesError.value = null
@@ -406,6 +417,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => {
// Missing node actions
setMissingNodeTypes,
surfaceMissingNodes,
+ removeMissingNodesByType,
// Lookup helpers
getNodeErrors,
diff --git a/src/workbench/extensions/manager/types/missingNodeErrorTypes.ts b/src/workbench/extensions/manager/types/missingNodeErrorTypes.ts
deleted file mode 100644
index 8af9b21d26..0000000000
--- a/src/workbench/extensions/manager/types/missingNodeErrorTypes.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-/**
- * Extra info returned by the backend for missing_node_type errors
- * from the /prompt endpoint validation.
- */
-export interface MissingNodeTypeExtraInfo {
- class_type?: string | null
- node_title?: string | null
- node_id?: string
-}
diff --git a/src/workbench/extensions/manager/utils/missingNodeErrorUtil.test.ts b/src/workbench/extensions/manager/utils/missingNodeErrorUtil.test.ts
index 85b7485218..242d46c233 100644
--- a/src/workbench/extensions/manager/utils/missingNodeErrorUtil.test.ts
+++ b/src/workbench/extensions/manager/utils/missingNodeErrorUtil.test.ts
@@ -1,93 +1,70 @@
import { describe, expect, it } from 'vitest'
+import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
+
import {
- buildMissingNodeHint,
- createMissingNodeTypeFromError
+ getCnrIdFromProperties,
+ getCnrIdFromNode
} from './missingNodeErrorUtil'
-describe('buildMissingNodeHint', () => {
- it('returns hint with title and node ID when both available', () => {
- expect(buildMissingNodeHint('My Node', 'MyNodeClass', '42')).toBe(
- '"My Node" (Node ID #42)'
+describe('getCnrIdFromProperties', () => {
+ it('returns cnr_id when present', () => {
+ expect(getCnrIdFromProperties({ cnr_id: 'my-pack' })).toBe('my-pack')
+ })
+
+ it('returns aux_id when cnr_id is absent', () => {
+ expect(getCnrIdFromProperties({ aux_id: 'my-aux-pack' })).toBe(
+ 'my-aux-pack'
)
})
- it('returns hint with title only when no node ID', () => {
- expect(buildMissingNodeHint('My Node', 'MyNodeClass', undefined)).toBe(
- '"My Node"'
- )
- })
-
- it('returns hint with node ID only when title matches class type', () => {
- expect(buildMissingNodeHint('MyNodeClass', 'MyNodeClass', '42')).toBe(
- 'Node ID #42'
- )
- })
-
- it('returns undefined when title matches class type and no node ID', () => {
+ it('prefers cnr_id over aux_id', () => {
expect(
- buildMissingNodeHint('MyNodeClass', 'MyNodeClass', undefined)
- ).toBeUndefined()
+ getCnrIdFromProperties({ cnr_id: 'primary', aux_id: 'secondary' })
+ ).toBe('primary')
})
- it('returns undefined when title is null and no node ID', () => {
- expect(buildMissingNodeHint(null, 'MyNodeClass', undefined)).toBeUndefined()
+ it('returns undefined when neither is present', () => {
+ expect(getCnrIdFromProperties({})).toBeUndefined()
})
- it('returns node ID hint when title is null but node ID exists', () => {
- expect(buildMissingNodeHint(null, 'MyNodeClass', '42')).toBe('Node ID #42')
+ it('returns undefined for null properties', () => {
+ expect(getCnrIdFromProperties(null)).toBeUndefined()
+ })
+
+ it('returns undefined for undefined properties', () => {
+ expect(getCnrIdFromProperties(undefined)).toBeUndefined()
+ })
+
+ it('returns undefined when cnr_id is not a string', () => {
+ expect(getCnrIdFromProperties({ cnr_id: 123 })).toBeUndefined()
})
})
-describe('createMissingNodeTypeFromError', () => {
- it('returns string type when no hint is generated', () => {
- const result = createMissingNodeTypeFromError({
- class_type: 'MyNodeClass',
- node_title: 'MyNodeClass'
- })
- expect(result).toBe('MyNodeClass')
+describe('getCnrIdFromNode', () => {
+ it('returns cnr_id from node properties', () => {
+ const node = {
+ properties: { cnr_id: 'node-pack' }
+ } as unknown as LGraphNode
+ expect(getCnrIdFromNode(node)).toBe('node-pack')
})
- it('returns object with hint when title differs from class type', () => {
- const result = createMissingNodeTypeFromError({
- class_type: 'MyNodeClass',
- node_title: 'My Custom Title',
- node_id: '42'
- })
- expect(result).toEqual({
- type: 'MyNodeClass',
- nodeId: '42',
- hint: '"My Custom Title" (Node ID #42)'
- })
+ it('returns aux_id when cnr_id is absent', () => {
+ const node = {
+ properties: { aux_id: 'node-aux-pack' }
+ } as unknown as LGraphNode
+ expect(getCnrIdFromNode(node)).toBe('node-aux-pack')
})
- it('handles null class_type by defaulting to Unknown', () => {
- const result = createMissingNodeTypeFromError({
- class_type: null,
- node_title: 'Some Title',
- node_id: '42'
- })
- expect(result).toEqual({
- type: 'Unknown',
- nodeId: '42',
- hint: '"Some Title" (Node ID #42)'
- })
+ it('prefers cnr_id over aux_id in node properties', () => {
+ const node = {
+ properties: { cnr_id: 'primary', aux_id: 'secondary' }
+ } as unknown as LGraphNode
+ expect(getCnrIdFromNode(node)).toBe('primary')
})
- it('handles empty extra_info', () => {
- const result = createMissingNodeTypeFromError({})
- expect(result).toBe('Unknown')
- })
-
- it('returns object with node ID hint when only node_id is available', () => {
- const result = createMissingNodeTypeFromError({
- class_type: 'MyNodeClass',
- node_id: '123'
- })
- expect(result).toEqual({
- type: 'MyNodeClass',
- nodeId: '123',
- hint: 'Node ID #123'
- })
+ it('returns undefined when node has no cnr_id or aux_id', () => {
+ const node = { properties: {} } as unknown as LGraphNode
+ expect(getCnrIdFromNode(node)).toBeUndefined()
})
})
diff --git a/src/workbench/extensions/manager/utils/missingNodeErrorUtil.ts b/src/workbench/extensions/manager/utils/missingNodeErrorUtil.ts
index 06761b3f4b..99cb381659 100644
--- a/src/workbench/extensions/manager/utils/missingNodeErrorUtil.ts
+++ b/src/workbench/extensions/manager/utils/missingNodeErrorUtil.ts
@@ -1,48 +1,4 @@
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
-import type { MissingNodeType } from '@/types/comfy'
-
-import type { MissingNodeTypeExtraInfo } from '../types/missingNodeErrorTypes'
-
-/**
- * Builds a hint string from missing node metadata.
- * Provides context about which node is missing (title, ID) when available.
- */
-export function buildMissingNodeHint(
- nodeTitle: string | null | undefined,
- classType: string,
- nodeId: string | undefined
-): string | undefined {
- const hasTitle = nodeTitle && nodeTitle !== classType
- if (hasTitle && nodeId) {
- return `"${nodeTitle}" (Node ID #${nodeId})`
- } else if (hasTitle) {
- return `"${nodeTitle}"`
- } else if (nodeId) {
- return `Node ID #${nodeId}`
- }
- return undefined
-}
-
-/**
- * Creates a MissingNodeType from backend error extra_info.
- * Used when the /prompt endpoint returns a missing_node_type error.
- */
-export function createMissingNodeTypeFromError(
- extraInfo: MissingNodeTypeExtraInfo
-): MissingNodeType {
- const classType = extraInfo.class_type ?? 'Unknown'
- const nodeTitle = extraInfo.node_title ?? classType
- const hint = buildMissingNodeHint(nodeTitle, classType, extraInfo.node_id)
-
- if (hint) {
- return {
- type: classType,
- ...(extraInfo.node_id ? { nodeId: extraInfo.node_id } : {}),
- ...(hint ? { hint } : {})
- }
- }
- return classType
-}
/**
* Extracts the custom node registry ID (cnr_id or aux_id) from a raw