diff --git a/src/components/rightSidePanel/RightSidePanel.vue b/src/components/rightSidePanel/RightSidePanel.vue index 505e58b74..f991f94ea 100644 --- a/src/components/rightSidePanel/RightSidePanel.vue +++ b/src/components/rightSidePanel/RightSidePanel.vue @@ -106,7 +106,7 @@ const hasContainerInternalError = computed(() => { if (allErrorExecutionIds.value.length === 0) return false return selectedNodes.value.some((node) => { if (!(node instanceof SubgraphNode || isGroupNode(node))) return false - return executionErrorStore.hasInternalErrorForNode(node.id) + return executionErrorStore.isContainerWithInternalError(node) }) }) diff --git a/src/components/rightSidePanel/errors/useErrorGroups.ts b/src/components/rightSidePanel/errors/useErrorGroups.ts index a4947d783..ace8299d4 100644 --- a/src/components/rightSidePanel/errors/useErrorGroups.ts +++ b/src/components/rightSidePanel/errors/useErrorGroups.ts @@ -13,6 +13,7 @@ import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import { getNodeByExecutionId, + getExecutionIdByNode, getRootParentNode } from '@/utils/graphTraversalUtil' import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil' @@ -20,6 +21,7 @@ import { isLGraphNode } from '@/utils/litegraphUtil' import { isGroupNode } from '@/utils/executableGroupNodeDto' import { st } from '@/i18n' import type { ErrorCardData, ErrorGroup, ErrorItem } from './types' +import type { NodeExecutionId } from '@/types/nodeIdentification' import { isNodeExecutionId } from '@/types/nodeIdentification' const PROMPT_CARD_ID = '__prompt__' @@ -200,26 +202,30 @@ export function useErrorGroups( const selectedNodeInfo = computed(() => { const items = canvasStore.selectedItems const nodeIds = new Set() - const containerIds = new Set() + const containerExecutionIds = new Set() for (const item of items) { if (!isLGraphNode(item)) continue nodeIds.add(String(item.id)) - if (item instanceof SubgraphNode || isGroupNode(item)) { - containerIds.add(String(item.id)) + if ( + (item instanceof SubgraphNode || isGroupNode(item)) && + app.rootGraph + ) { + const execId = getExecutionIdByNode(app.rootGraph, item) + if (execId) containerExecutionIds.add(execId) } } return { nodeIds: nodeIds.size > 0 ? nodeIds : null, - containerIds + containerExecutionIds } }) const isSingleNodeSelected = computed( () => selectedNodeInfo.value.nodeIds?.size === 1 && - selectedNodeInfo.value.containerIds.size === 0 + selectedNodeInfo.value.containerExecutionIds.size === 0 ) const errorNodeCache = computed(() => { @@ -238,8 +244,9 @@ export function useErrorGroups( const graphNode = errorNodeCache.value.get(executionNodeId) if (graphNode && nodeIds.has(String(graphNode.id))) return true - for (const containerId of selectedNodeInfo.value.containerIds) { - if (executionNodeId.startsWith(`${containerId}:`)) return true + for (const containerExecId of selectedNodeInfo.value + .containerExecutionIds) { + if (executionNodeId.startsWith(`${containerExecId}:`)) return true } return false diff --git a/src/components/rightSidePanel/parameters/SectionWidgets.vue b/src/components/rightSidePanel/parameters/SectionWidgets.vue index d6427091c..9bd469b0f 100644 --- a/src/components/rightSidePanel/parameters/SectionWidgets.vue +++ b/src/components/rightSidePanel/parameters/SectionWidgets.vue @@ -121,7 +121,7 @@ const hasContainerInternalError = computed(() => { targetNode.value instanceof SubgraphNode || isGroupNode(targetNode.value) if (!isContainer) return false - return executionErrorStore.hasInternalErrorForNode(targetNode.value.id) + return executionErrorStore.isContainerWithInternalError(targetNode.value) }) const nodeHasError = computed(() => { diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index 1571640bc..aff058570 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -342,7 +342,9 @@ const hasAnyError = computed((): boolean => { hasExecutionError.value || nodeData.hasErrors || error || - (executionErrorStore.lastNodeErrors?.[nodeData.id]?.errors.length ?? 0) > 0 + executionErrorStore.getNodeErrors(nodeLocatorId.value) || + (lgraphNode.value && + executionErrorStore.isContainerWithInternalError(lgraphNode.value)) ) }) diff --git a/src/stores/executionErrorStore.ts b/src/stores/executionErrorStore.ts index e7388390c..e93335bbf 100644 --- a/src/stores/executionErrorStore.ts +++ b/src/stores/executionErrorStore.ts @@ -10,11 +10,13 @@ import type { PromptError } from '@/schemas/apiSchema' import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema' -import type { NodeLocatorId } from '@/types/nodeIdentification' +import type { LGraphNode } from '@/lib/litegraph/src/litegraph' +import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification' import { executionIdToNodeLocatorId, forEachNode, - getNodeByExecutionId + getNodeByExecutionId, + getExecutionIdByNode } from '@/utils/graphTraversalUtil' /** @@ -186,9 +188,32 @@ export const useExecutionErrorStore = defineStore('executionError', () => { return nodeError.errors.some((e) => e.extra_info?.input_name === slotName) } - function hasInternalErrorForNode(nodeId: string | number): boolean { - const prefix = `${nodeId}:` - return allErrorExecutionIds.value.some((id) => id.startsWith(prefix)) + /** + * Set of all execution ID prefixes derived from active error nodes, + * including the error nodes themselves. + * + * Example: error at "65:70:63" → Set { "65", "65:70", "65:70:63" } + */ + const errorAncestorExecutionIds = computed>(() => { + const ids = new Set() + + for (const executionId of allErrorExecutionIds.value) { + const parts = executionId.split(':') + // Add every prefix including the full ID (error leaf node itself) + for (let i = 1; i <= parts.length; i++) { + ids.add(parts.slice(0, i).join(':')) + } + } + + return ids + }) + + /** True if the node has errors inside it at any nesting depth. */ + function isContainerWithInternalError(node: LGraphNode): boolean { + if (!app.rootGraph) return false + const execId = getExecutionIdByNode(app.rootGraph, node) + if (!execId) return false + return errorAncestorExecutionIds.value.has(execId) } /** @@ -275,6 +300,7 @@ export const useExecutionErrorStore = defineStore('executionError', () => { // Lookup helpers getNodeErrors, slotHasError, - hasInternalErrorForNode + errorAncestorExecutionIds, + isContainerWithInternalError } }) diff --git a/src/utils/graphTraversalUtil.ts b/src/utils/graphTraversalUtil.ts index 64a33b660..adc411257 100644 --- a/src/utils/graphTraversalUtil.ts +++ b/src/utils/graphTraversalUtil.ts @@ -331,6 +331,35 @@ export function getNodeByExecutionId( return targetGraph.getNodeById(localNodeId) || null } +/** + * Returns the execution ID for a node relative to the root graph. + * + * Root-level nodes return their ID directly (e.g. "42"). + * Nodes inside subgraphs return a colon-separated chain (e.g. "65:70:63"). + * + * @param rootGraph - The root graph to resolve from + * @param node - The node whose execution ID to compute + * @returns The execution ID string, or null if the node has no graph + */ +export function getExecutionIdByNode( + rootGraph: LGraph, + node: LGraphNode +): NodeExecutionId | null { + if (!node.graph) return null + + if (node.graph === rootGraph || node.graph.isRootGraph) { + return String(node.id) + } + + const parentPath = findPartialExecutionPathToGraph( + node.graph as LGraph, + rootGraph + ) + if (parentPath === undefined) return null + + return `${parentPath}:${node.id}` +} + /** * Get a node by its locator ID from anywhere in the graph hierarchy. * Locator IDs use UUID format like "uuid:nodeId" for subgraph nodes.