import { defineStore } from 'pinia' import { computed, ref, watch } from 'vue' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import { app } from '@/scripts/app' import type { ExecutionErrorWsMessage, NodeError, PromptError } from '@/schemas/apiSchema' import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification' import { executionIdToNodeLocatorId, forEachNode, getNodeByExecutionId, getExecutionIdByNode } from '@/utils/graphTraversalUtil' /** * Store dedicated to execution error state management. * * Extracted from executionStore to separate error-related concerns * (state, computed properties, graph flag propagation, overlay UI) * from execution flow management (progress, queuing, events). */ export const useExecutionErrorStore = defineStore('executionError', () => { const workflowStore = useWorkflowStore() const canvasStore = useCanvasStore() const lastNodeErrors = ref | null>(null) const lastExecutionError = ref(null) const lastPromptError = ref(null) const isErrorOverlayOpen = ref(false) function showErrorOverlay() { isErrorOverlayOpen.value = true } function dismissErrorOverlay() { isErrorOverlayOpen.value = false } /** Clear all error state. Called at execution start. */ function clearAllErrors() { lastExecutionError.value = null lastPromptError.value = null lastNodeErrors.value = null isErrorOverlayOpen.value = false } /** Clear only prompt-level errors. Called during resetExecutionState. */ function clearPromptError() { lastPromptError.value = null } const lastExecutionErrorNodeLocatorId = computed(() => { const err = lastExecutionError.value if (!err) return null return executionIdToNodeLocatorId(app.rootGraph, String(err.node_id)) }) const lastExecutionErrorNodeId = computed(() => { const locator = lastExecutionErrorNodeLocatorId.value if (!locator) return null const localId = workflowStore.nodeLocatorIdToNodeId(locator) return localId != null ? String(localId) : null }) /** Whether a runtime execution error is present */ const hasExecutionError = computed(() => !!lastExecutionError.value) /** Whether a prompt-level error is present (e.g. invalid_prompt, prompt_no_outputs) */ const hasPromptError = computed(() => !!lastPromptError.value) /** Whether any node validation errors are present */ const hasNodeError = computed( () => !!lastNodeErrors.value && Object.keys(lastNodeErrors.value).length > 0 ) /** Whether any error (node validation, runtime execution, or prompt-level) is present */ const hasAnyError = computed( () => hasExecutionError.value || hasPromptError.value || hasNodeError.value ) const allErrorExecutionIds = computed(() => { const ids: string[] = [] if (lastNodeErrors.value) { ids.push(...Object.keys(lastNodeErrors.value)) } if (lastExecutionError.value) { const nodeId = lastExecutionError.value.node_id if (nodeId !== null && nodeId !== undefined) { ids.push(String(nodeId)) } } return ids }) /** Count of prompt-level errors (0 or 1) */ const promptErrorCount = computed(() => (lastPromptError.value ? 1 : 0)) /** Count of all individual node validation errors */ const nodeErrorCount = computed(() => { if (!lastNodeErrors.value) return 0 let count = 0 for (const nodeError of Object.values(lastNodeErrors.value)) { count += nodeError.errors.length } return count }) /** Count of runtime execution errors (0 or 1) */ const executionErrorCount = computed(() => (lastExecutionError.value ? 1 : 0)) /** Total count of all individual errors */ const totalErrorCount = computed( () => promptErrorCount.value + nodeErrorCount.value + executionErrorCount.value ) /** Pre-computed Set of graph node IDs (as strings) that have errors in the current graph scope. */ const activeGraphErrorNodeIds = computed>(() => { const ids = new Set() if (!app.rootGraph) return ids // Fall back to rootGraph when currentGraph hasn't been initialized yet const activeGraph = canvasStore.currentGraph ?? app.rootGraph if (lastNodeErrors.value) { for (const executionId of Object.keys(lastNodeErrors.value)) { const graphNode = getNodeByExecutionId(app.rootGraph, executionId) if (graphNode?.graph === activeGraph) { ids.add(String(graphNode.id)) } } } if (lastExecutionError.value) { const execNodeId = String(lastExecutionError.value.node_id) const graphNode = getNodeByExecutionId(app.rootGraph, execNodeId) if (graphNode?.graph === activeGraph) { ids.add(String(graphNode.id)) } } return ids }) /** Map of node errors indexed by locator ID. */ const nodeErrorsByLocatorId = computed>( () => { if (!lastNodeErrors.value) return {} const map: Record = {} for (const [executionId, nodeError] of Object.entries( lastNodeErrors.value )) { const locatorId = executionIdToNodeLocatorId(app.rootGraph, executionId) if (locatorId) { map[locatorId] = nodeError } } return map } ) /** Get node errors by locator ID. */ const getNodeErrors = ( nodeLocatorId: NodeLocatorId ): NodeError | undefined => { return nodeErrorsByLocatorId.value[nodeLocatorId] } /** Check if a specific slot has validation errors. */ const slotHasError = ( nodeLocatorId: NodeLocatorId, slotName: string ): boolean => { const nodeError = getNodeErrors(nodeLocatorId) if (!nodeError) return false return nodeError.errors.some((e) => e.extra_info?.input_name === slotName) } /** * 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) } /** * Update node and slot error flags when validation errors change. * Propagates errors up subgraph chains. */ watch(lastNodeErrors, () => { if (!app.rootGraph) return // Clear all error flags forEachNode(app.rootGraph, (node) => { node.has_errors = false if (node.inputs) { for (const slot of node.inputs) { slot.hasErrors = false } } }) if (!lastNodeErrors.value) return // Set error flags on nodes and slots for (const [executionId, nodeError] of Object.entries( lastNodeErrors.value )) { const node = getNodeByExecutionId(app.rootGraph, executionId) if (!node) continue node.has_errors = true // Mark input slots with errors if (node.inputs) { for (const error of nodeError.errors) { const slotName = error.extra_info?.input_name if (!slotName) continue const slot = node.inputs.find((s) => s.name === slotName) if (slot) { slot.hasErrors = true } } } // Propagate errors to parent subgraph nodes const parts = executionId.split(':') for (let i = parts.length - 1; i > 0; i--) { const parentExecutionId = parts.slice(0, i).join(':') const parentNode = getNodeByExecutionId( app.rootGraph, parentExecutionId ) if (parentNode) { parentNode.has_errors = true } } } }) return { // Raw state lastNodeErrors, lastExecutionError, lastPromptError, // Clearing clearAllErrors, clearPromptError, // Overlay UI isErrorOverlayOpen, showErrorOverlay, dismissErrorOverlay, // Derived state hasExecutionError, hasPromptError, hasNodeError, hasAnyError, allErrorExecutionIds, totalErrorCount, lastExecutionErrorNodeId, activeGraphErrorNodeIds, // Lookup helpers getNodeErrors, slotHasError, errorAncestorExecutionIds, isContainerWithInternalError } })