From 45f112e2263cf891637cf53b0ff36fae8d918462 Mon Sep 17 00:00:00 2001 From: jaeone94 <89377375+jaeone94@users.noreply.github.com> Date: Sat, 28 Feb 2026 21:05:58 +0900 Subject: [PATCH] fix: node replacement fails after execution and modal sync (#9269) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes two bugs in the node replacement flow: placeholder detection failing after workflow execution or pack reinstallation, and missing UI sync in the Errors Tab when replacements are applied from the modal dialog. ## Changes - **Placeholder detection**: Node placeholder detection now matches against `targetTypes` (derived from the replaceable node list built at workflow load time) instead of relying on `has_errors` flag or `registered_node_types` lookup. This ensures replacement works reliably after execution (where `has_errors` gets cleared) and after pack reinstallation (where the type becomes registered). - **Modal → Errors Tab sync**: Added `executionErrorStore.removeMissingNodesByType()` call in `MissingNodesContent.vue` after replacement, so the Errors Tab reflects changes immediately without requiring a page reload. ## Review Focus - `collectAllNodes` predicate change in `useNodeReplacement.ts`: now uses `targetTypes.has(originalType)` to find nodes by their original serialized type. This is independent of runtime state like `has_errors` or `registered_node_types`. - `executionErrorStore.removeMissingNodesByType` call timing in `MissingNodesContent.vue` — runs synchronously after `replaceNodesInPlace` resolves, before auto-close logic. ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9269-fix-node-replacement-fails-after-execution-and-modal-sync-3146d73d365081218398c961639b450f) by [Unito](https://www.unito.io) --- .../dialog/content/MissingNodesContent.vue | 10 ++++++++++ .../nodeReplacement/useNodeReplacement.ts | 19 +++++++++++++++---- 2 files changed, 25 insertions(+), 4 deletions(-) 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)