diff --git a/src/components/dialog/content/MissingNodesContent.vue b/src/components/dialog/content/MissingNodesContent.vue index b9898148a0..9aea195f2a 100644 --- a/src/components/dialog/content/MissingNodesContent.vue +++ b/src/components/dialog/content/MissingNodesContent.vue @@ -234,6 +234,7 @@ import { isCloud } from '@/platform/distribution/types' import type { NodeReplacement } from '@/platform/nodeReplacement/types' import { useNodeReplacement } from '@/platform/nodeReplacement/useNodeReplacement' import { useDialogStore } from '@/stores/dialogStore' +import { useExecutionErrorStore } from '@/stores/executionErrorStore' import type { MissingNodeType } from '@/types/comfy' import { cn } from '@/utils/tailwindUtil' import { useMissingNodes } from '@/workbench/extensions/manager/composables/nodePack/useMissingNodes' @@ -245,6 +246,7 @@ const { missingNodeTypes } = defineProps<{ const { missingCoreNodes } = useMissingNodes() const { replaceNodesInPlace } = useNodeReplacement() const dialogStore = useDialogStore() +const executionErrorStore = useExecutionErrorStore() interface ProcessedNode { label: string @@ -339,6 +341,14 @@ function handleReplaceSelected() { replacedTypes.value = nextReplaced selectedTypes.value = nextSelected + // replaceNodesInPlace() handles canvas rendering via onNodeAdded(), + // but the modal only updates its own local UI state above. + // Without this call the Errors Tab would still list the replaced nodes + // as missing because executionErrorStore is not aware of the replacement. + if (result.length > 0) { + executionErrorStore.removeMissingNodesByType(result) + } + // Auto-close when all replaceable nodes replaced and no non-replaceable remain const allReplaced = replaceableNodes.value.every((n) => nextReplaced.has(n.label) diff --git a/src/platform/nodeReplacement/useNodeReplacement.ts b/src/platform/nodeReplacement/useNodeReplacement.ts index 270fdea6e5..3706ebc102 100644 --- a/src/platform/nodeReplacement/useNodeReplacement.ts +++ b/src/platform/nodeReplacement/useNodeReplacement.ts @@ -226,11 +226,22 @@ export function useNodeReplacement() { useWorkflowStore().activeWorkflow?.changeTracker ?? null changeTracker?.beforeChange() + // Target types come from node_replacements fetched at workflow load time + // and the missing nodes detected at that point — not from the current + // registered_node_types. This ensures replacement still works even if + // the user has since installed the missing node pack. + const targetTypes = new Set( + selectedTypes.map((t) => (typeof t === 'string' ? t : t.type)) + ) + try { - const placeholders = collectAllNodes( - graph, - (n) => !!n.has_errors && !!n.last_serialization - ) + const placeholders = collectAllNodes(graph, (n) => { + if (!n.last_serialization) return false + // Prefer the original serialized type; fall back to the live type + // for nodes whose serialization predates the type field. + const originalType = n.last_serialization.type ?? n.type + return !!originalType && targetTypes.has(originalType) + }) for (const node of placeholders) { const match = findMatchingType(node, selectedTypes)