From 8aa4e36fd5457e096644b4a26b3b5ff86c916548 Mon Sep 17 00:00:00 2001 From: jaeone94 <89377375+jaeone94@users.noreply.github.com> Date: Sun, 22 Feb 2026 09:51:22 +0900 Subject: [PATCH] [refactor] Extract executionErrorStore from executionStore (#9060) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Extracts error-related state and logic from `executionStore` into a dedicated `executionErrorStore` for better separation of concerns. ## Changes - **New store**: `executionErrorStore` with all error state (`lastNodeErrors`, `lastExecutionError`, `lastPromptError`), computed properties (`hasAnyError`, `totalErrorCount`, `activeGraphErrorNodeIds`), and UI state (`isErrorOverlayOpen`, `showErrorOverlay`, `dismissErrorOverlay`) - **Moved util**: `executionIdToNodeLocatorId` extracted to `graphTraversalUtil`, reusing `traverseSubgraphPath` and accepting `rootGraph` as parameter - **Updated consumers**: 12 files updated to import from `executionErrorStore` - **Backward compat**: Deprecated getters retained in `ComfyApp` for extension compatibility ## Review Focus - Deprecated getters in `app.ts` — can be removed in a future breaking-change PR once extension authors migrate ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9060-refactor-Extract-executionErrorStore-from-executionStore-30e6d73d36508101973de835ab6b199f) by [Unito](https://www.unito.io) --- src/components/TopMenuSection.vue | 4 +- src/components/error/ErrorOverlay.vue | 10 +- src/components/graph/GraphCanvas.vue | 6 +- .../rightSidePanel/RightSidePanel.vue | 10 +- .../rightSidePanel/errors/TabErrors.test.ts | 10 +- .../rightSidePanel/errors/useErrorGroups.ts | 19 +- .../parameters/SectionWidgets.vue | 10 +- .../extensions/linearMode/LinearControls.vue | 4 +- .../vueNodes/components/LGraphNode.vue | 8 +- .../vueNodes/components/NodeWidgets.vue | 6 +- src/scripts/app.ts | 37 +-- src/stores/executionErrorStore.ts | 280 ++++++++++++++++ src/stores/executionStore.test.ts | 18 +- src/stores/executionStore.ts | 309 +----------------- src/stores/imagePreviewStore.test.ts | 6 +- src/stores/imagePreviewStore.ts | 9 +- src/stores/subgraphStore.ts | 4 +- src/stores/workspaceStore.ts | 7 + src/types/extensionTypes.ts | 6 + src/utils/graphTraversalUtil.ts | 35 +- 20 files changed, 425 insertions(+), 373 deletions(-) create mode 100644 src/stores/executionErrorStore.ts diff --git a/src/components/TopMenuSection.vue b/src/components/TopMenuSection.vue index 30b39c75c..b402b93b1 100644 --- a/src/components/TopMenuSection.vue +++ b/src/components/TopMenuSection.vue @@ -169,6 +169,7 @@ import { useSettingStore } from '@/platform/settings/settingStore' import { app } from '@/scripts/app' import { useCommandStore } from '@/stores/commandStore' import { useExecutionStore } from '@/stores/executionStore' +import { useExecutionErrorStore } from '@/stores/executionErrorStore' import { useQueueStore, useQueueUIStore } from '@/stores/queueStore' import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore' import { useSidebarTabStore } from '@/stores/workspace/sidebarTabStore' @@ -189,6 +190,7 @@ const { toastErrorHandler } = useErrorHandling() const commandStore = useCommandStore() const queueStore = useQueueStore() const executionStore = useExecutionStore() +const executionErrorStore = useExecutionErrorStore() const queueUIStore = useQueueUIStore() const sidebarTabStore = useSidebarTabStore() const { activeJobsCount } = storeToRefs(queueStore) @@ -262,7 +264,7 @@ const shouldShowRedDot = computed((): boolean => { return shouldShowConflictRedDot.value }) -const { hasAnyError } = storeToRefs(executionStore) +const { hasAnyError } = storeToRefs(executionErrorStore) // Right side panel toggle const { isOpen: isRightSidePanelOpen } = storeToRefs(rightSidePanelStore) diff --git a/src/components/error/ErrorOverlay.vue b/src/components/error/ErrorOverlay.vue index 0bc886abb..22eb101f6 100644 --- a/src/components/error/ErrorOverlay.vue +++ b/src/components/error/ErrorOverlay.vue @@ -64,17 +64,17 @@ import { useI18n } from 'vue-i18n' import { storeToRefs } from 'pinia' import Button from '@/components/ui/button/Button.vue' -import { useExecutionStore } from '@/stores/executionStore' +import { useExecutionErrorStore } from '@/stores/executionErrorStore' import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useErrorGroups } from '@/components/rightSidePanel/errors/useErrorGroups' const { t } = useI18n() -const executionStore = useExecutionStore() +const executionErrorStore = useExecutionErrorStore() const rightSidePanelStore = useRightSidePanelStore() const canvasStore = useCanvasStore() -const { totalErrorCount, isErrorOverlayOpen } = storeToRefs(executionStore) +const { totalErrorCount, isErrorOverlayOpen } = storeToRefs(executionErrorStore) const { groupedErrorMessages } = useErrorGroups(ref(''), t) const errorCountLabel = computed(() => @@ -90,7 +90,7 @@ const isVisible = computed( ) function dismiss() { - executionStore.dismissErrorOverlay() + executionErrorStore.dismissErrorOverlay() } function seeErrors() { @@ -100,6 +100,6 @@ function seeErrors() { } rightSidePanelStore.openPanel('errors') - executionStore.dismissErrorOverlay() + executionErrorStore.dismissErrorOverlay() } diff --git a/src/components/graph/GraphCanvas.vue b/src/components/graph/GraphCanvas.vue index 0f9c6570b..0ea47dd37 100644 --- a/src/components/graph/GraphCanvas.vue +++ b/src/components/graph/GraphCanvas.vue @@ -70,7 +70,7 @@ :key="nodeData.id" :node-data="nodeData" :error=" - executionStore.lastExecutionError?.node_id === nodeData.id + executionErrorStore.lastExecutionError?.node_id === nodeData.id ? 'Execution error' : null " @@ -170,6 +170,7 @@ import { storeToRefs } from 'pinia' import { useBootstrapStore } from '@/stores/bootstrapStore' import { useCommandStore } from '@/stores/commandStore' import { useExecutionStore } from '@/stores/executionStore' +import { useExecutionErrorStore } from '@/stores/executionErrorStore' import { useNodeDefStore } from '@/stores/nodeDefStore' import { useColorPaletteStore } from '@/stores/workspace/colorPaletteStore' import { useSearchBoxStore } from '@/stores/workspace/searchBoxStore' @@ -196,6 +197,7 @@ const workspaceStore = useWorkspaceStore() const canvasStore = useCanvasStore() const workflowStore = useWorkflowStore() const executionStore = useExecutionStore() +const executionErrorStore = useExecutionErrorStore() const toastStore = useToastStore() const colorPaletteStore = useColorPaletteStore() const colorPaletteService = useColorPaletteService() @@ -376,7 +378,7 @@ watch( // Update node slot errors for LiteGraph nodes // (Vue nodes read from store directly) watch( - () => executionStore.lastNodeErrors, + () => executionErrorStore.lastNodeErrors, (lastNodeErrors) => { if (!comfyApp.graph) return diff --git a/src/components/rightSidePanel/RightSidePanel.vue b/src/components/rightSidePanel/RightSidePanel.vue index 1ef8441aa..505e58b74 100644 --- a/src/components/rightSidePanel/RightSidePanel.vue +++ b/src/components/rightSidePanel/RightSidePanel.vue @@ -14,7 +14,7 @@ import { SubgraphNode } from '@/lib/litegraph/src/litegraph' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import { useSettingStore } from '@/platform/settings/settingStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' -import { useExecutionStore } from '@/stores/executionStore' +import { useExecutionErrorStore } from '@/stores/executionErrorStore' import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore' import type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore' import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil' @@ -36,12 +36,12 @@ import SubgraphEditor from './subgraph/SubgraphEditor.vue' import TabErrors from './errors/TabErrors.vue' const canvasStore = useCanvasStore() -const executionStore = useExecutionStore() +const executionErrorStore = useExecutionErrorStore() const rightSidePanelStore = useRightSidePanelStore() const settingStore = useSettingStore() const { t } = useI18n() -const { hasAnyError, allErrorExecutionIds } = storeToRefs(executionStore) +const { hasAnyError, allErrorExecutionIds } = storeToRefs(executionErrorStore) const { findParentGroup } = useGraphHierarchy() @@ -98,7 +98,7 @@ type RightSidePanelTabList = Array<{ const hasDirectNodeError = computed(() => selectedNodes.value.some((node) => - executionStore.activeGraphErrorNodeIds.has(String(node.id)) + executionErrorStore.activeGraphErrorNodeIds.has(String(node.id)) ) ) @@ -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 executionStore.hasInternalErrorForNode(node.id) + return executionErrorStore.hasInternalErrorForNode(node.id) }) }) diff --git a/src/components/rightSidePanel/errors/TabErrors.test.ts b/src/components/rightSidePanel/errors/TabErrors.test.ts index e5fc2bbe1..f4e608762 100644 --- a/src/components/rightSidePanel/errors/TabErrors.test.ts +++ b/src/components/rightSidePanel/errors/TabErrors.test.ts @@ -94,7 +94,7 @@ describe('TabErrors.vue', () => { it('renders prompt-level errors (Group title = error message)', async () => { const wrapper = mountComponent({ - execution: { + executionError: { lastPromptError: { type: 'prompt_no_outputs', message: 'Server Error: No outputs', @@ -118,7 +118,7 @@ describe('TabErrors.vue', () => { } as ReturnType) const wrapper = mountComponent({ - execution: { + executionError: { lastNodeErrors: { '6': { class_type: 'CLIPTextEncode', @@ -143,7 +143,7 @@ describe('TabErrors.vue', () => { } as ReturnType) const wrapper = mountComponent({ - execution: { + executionError: { lastExecutionError: { prompt_id: 'abc', node_id: '10', @@ -167,7 +167,7 @@ describe('TabErrors.vue', () => { vi.mocked(getNodeByExecutionId).mockReturnValue(null) const wrapper = mountComponent({ - execution: { + executionError: { lastNodeErrors: { '1': { class_type: 'CLIPTextEncode', @@ -198,7 +198,7 @@ describe('TabErrors.vue', () => { vi.mocked(useCopyToClipboard).mockReturnValue({ copyToClipboard: mockCopy }) const wrapper = mountComponent({ - execution: { + executionError: { lastNodeErrors: { '1': { class_type: 'TestNode', diff --git a/src/components/rightSidePanel/errors/useErrorGroups.ts b/src/components/rightSidePanel/errors/useErrorGroups.ts index bfb468621..a4947d783 100644 --- a/src/components/rightSidePanel/errors/useErrorGroups.ts +++ b/src/components/rightSidePanel/errors/useErrorGroups.ts @@ -3,13 +3,14 @@ import type { Ref } from 'vue' import Fuse from 'fuse.js' import type { IFuseOptions } from 'fuse.js' -import { useExecutionStore } from '@/stores/executionStore' +import { useExecutionErrorStore } from '@/stores/executionErrorStore' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { app } from '@/scripts/app' import { isCloud } from '@/platform/distribution/types' import { SubgraphNode } from '@/lib/litegraph/src/litegraph' import type { LGraphNode } from '@/lib/litegraph/src/litegraph' + import { getNodeByExecutionId, getRootParentNode @@ -192,7 +193,7 @@ export function useErrorGroups( searchQuery: Ref, t: (key: string) => string ) { - const executionStore = useExecutionStore() + const executionErrorStore = useExecutionErrorStore() const canvasStore = useCanvasStore() const collapseState = reactive>({}) @@ -223,7 +224,7 @@ export function useErrorGroups( const errorNodeCache = computed(() => { const map = new Map() - for (const execId of executionStore.allErrorExecutionIds) { + for (const execId of executionErrorStore.allErrorExecutionIds) { const node = getNodeByExecutionId(app.rootGraph, execId) if (node) map.set(execId, node) } @@ -262,10 +263,10 @@ export function useErrorGroups( } function processPromptError(groupsMap: Map) { - if (selectedNodeInfo.value.nodeIds || !executionStore.lastPromptError) + if (selectedNodeInfo.value.nodeIds || !executionErrorStore.lastPromptError) return - const error = executionStore.lastPromptError + const error = executionErrorStore.lastPromptError const groupTitle = error.message const cards = getOrCreateGroup(groupsMap, groupTitle, 0) const isKnown = KNOWN_PROMPT_ERROR_TYPES.has(error.type) @@ -293,10 +294,10 @@ export function useErrorGroups( groupsMap: Map, filterBySelection = false ) { - if (!executionStore.lastNodeErrors) return + if (!executionErrorStore.lastNodeErrors) return for (const [nodeId, nodeError] of Object.entries( - executionStore.lastNodeErrors + executionErrorStore.lastNodeErrors )) { addNodeErrorToGroup( groupsMap, @@ -316,9 +317,9 @@ export function useErrorGroups( groupsMap: Map, filterBySelection = false ) { - if (!executionStore.lastExecutionError) return + if (!executionErrorStore.lastExecutionError) return - const e = executionStore.lastExecutionError + const e = executionErrorStore.lastExecutionError addNodeErrorToGroup( groupsMap, String(e.node_id), diff --git a/src/components/rightSidePanel/parameters/SectionWidgets.vue b/src/components/rightSidePanel/parameters/SectionWidgets.vue index 566756a4d..d6427091c 100644 --- a/src/components/rightSidePanel/parameters/SectionWidgets.vue +++ b/src/components/rightSidePanel/parameters/SectionWidgets.vue @@ -9,7 +9,7 @@ import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph' import { SubgraphNode } from '@/lib/litegraph/src/litegraph' import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' -import { useExecutionStore } from '@/stores/executionStore' +import { useExecutionErrorStore } from '@/stores/executionErrorStore' import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore' import { useSettingStore } from '@/platform/settings/settingStore' import { cn } from '@/utils/tailwindUtil' @@ -62,7 +62,7 @@ watchEffect(() => (widgets.value = widgetsProp)) provide(HideLayoutFieldKey, true) const canvasStore = useCanvasStore() -const executionStore = useExecutionStore() +const executionErrorStore = useExecutionErrorStore() const rightSidePanelStore = useRightSidePanelStore() const nodeDefStore = useNodeDefStore() const { t } = useI18n() @@ -110,7 +110,9 @@ const targetNode = computed(() => { const hasDirectError = computed(() => { if (!targetNode.value) return false - return executionStore.activeGraphErrorNodeIds.has(String(targetNode.value.id)) + return executionErrorStore.activeGraphErrorNodeIds.has( + String(targetNode.value.id) + ) }) const hasContainerInternalError = computed(() => { @@ -119,7 +121,7 @@ const hasContainerInternalError = computed(() => { targetNode.value instanceof SubgraphNode || isGroupNode(targetNode.value) if (!isContainer) return false - return executionStore.hasInternalErrorForNode(targetNode.value.id) + return executionErrorStore.hasInternalErrorForNode(targetNode.value.id) }) const nodeHasError = computed(() => { diff --git a/src/renderer/extensions/linearMode/LinearControls.vue b/src/renderer/extensions/linearMode/LinearControls.vue index 40c96268d..df87f282e 100644 --- a/src/renderer/extensions/linearMode/LinearControls.vue +++ b/src/renderer/extensions/linearMode/LinearControls.vue @@ -22,6 +22,7 @@ import { api } from '@/scripts/api' import { app } from '@/scripts/app' import { useCommandStore } from '@/stores/commandStore' import { useExecutionStore } from '@/stores/executionStore' +import { useExecutionErrorStore } from '@/stores/executionErrorStore' import { useQueueSettingsStore } from '@/stores/queueStore' import type { SimplifiedWidget } from '@/types/simplifiedWidget' import { cn } from '@/utils/tailwindUtil' @@ -29,6 +30,7 @@ import { cn } from '@/utils/tailwindUtil' const { t } = useI18n() const commandStore = useCommandStore() const executionStore = useExecutionStore() +const executionErrorStore = useExecutionErrorStore() const { batchCount } = storeToRefs(useQueueSettingsStore()) const settingStore = useSettingStore() const { isActiveSubscription } = useBillingContext() @@ -79,7 +81,7 @@ function nodeToNodeData(node: LGraphNode) { return { ...nodeData, //note lastNodeErrors uses exeuctionid, node.id is execution for root - hasErrors: !!executionStore.lastNodeErrors?.[node.id], + hasErrors: !!executionErrorStore.lastNodeErrors?.[node.id], dropIndicator, onDragDrop: node.onDragDrop, diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue index a490729fa..1f373529a 100644 --- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue +++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue @@ -246,7 +246,7 @@ import { useNodePreviewState } from '@/renderer/extensions/vueNodes/preview/useN import { nonWidgetedInputs } from '@/renderer/extensions/vueNodes/utils/nodeDataUtils' import { applyLightThemeColor } from '@/renderer/extensions/vueNodes/utils/nodeStyleUtils' import { app } from '@/scripts/app' -import { useExecutionStore } from '@/stores/executionStore' +import { useExecutionErrorStore } from '@/stores/executionErrorStore' import { useNodeOutputStore } from '@/stores/imagePreviewStore' import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore' import { isTransparent } from '@/utils/colorUtil' @@ -293,9 +293,9 @@ const isSelected = computed(() => { const nodeLocatorId = computed(() => getLocatorIdFromNodeData(nodeData)) const { executing, progress } = useNodeExecutionState(nodeLocatorId) -const executionStore = useExecutionStore() +const executionErrorStore = useExecutionErrorStore() const hasExecutionError = computed( - () => executionStore.lastExecutionErrorNodeId === nodeData.id + () => executionErrorStore.lastExecutionErrorNodeId === nodeData.id ) const hasAnyError = computed((): boolean => { @@ -303,7 +303,7 @@ const hasAnyError = computed((): boolean => { hasExecutionError.value || nodeData.hasErrors || error || - (executionStore.lastNodeErrors?.[nodeData.id]?.errors.length ?? 0) > 0 + (executionErrorStore.lastNodeErrors?.[nodeData.id]?.errors.length ?? 0) > 0 ) }) diff --git a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue index 6c60c6666..85895a01e 100644 --- a/src/renderer/extensions/vueNodes/components/NodeWidgets.vue +++ b/src/renderer/extensions/vueNodes/components/NodeWidgets.vue @@ -101,7 +101,7 @@ import { stripGraphPrefix, useWidgetValueStore } from '@/stores/widgetValueStore' -import { useExecutionStore } from '@/stores/executionStore' +import { useExecutionErrorStore } from '@/stores/executionErrorStore' import type { SimplifiedWidget, WidgetValue } from '@/types/simplifiedWidget' import { cn } from '@/utils/tailwindUtil' @@ -116,7 +116,7 @@ const { nodeData } = defineProps() const { shouldHandleNodePointerEvents, forwardEventToCanvas } = useCanvasInteractions() const { bringNodeToFront } = useNodeZIndex() -const executionStore = useExecutionStore() +const executionErrorStore = useExecutionErrorStore() function handleWidgetPointerEvent(event: PointerEvent) { if (shouldHandleNodePointerEvents.value) return @@ -170,7 +170,7 @@ interface ProcessedWidget { const processedWidgets = computed((): ProcessedWidget[] => { if (!nodeData?.widgets) return [] - const nodeErrors = executionStore.lastNodeErrors?.[nodeData.id ?? ''] + const nodeErrors = executionErrorStore.lastNodeErrors?.[nodeData.id ?? ''] const nodeId = nodeData.id const { widgets } = nodeData diff --git a/src/scripts/app.ts b/src/scripts/app.ts index 9aea89988..c820c5f83 100644 --- a/src/scripts/app.ts +++ b/src/scripts/app.ts @@ -60,6 +60,7 @@ import { useApiKeyAuthStore } from '@/stores/apiKeyAuthStore' import { useCommandStore } from '@/stores/commandStore' import { useDomWidgetStore } from '@/stores/domWidgetStore' import { useExecutionStore } from '@/stores/executionStore' +import { useExecutionErrorStore } from '@/stores/executionErrorStore' import { useExtensionStore } from '@/stores/extensionStore' import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore' import { useNodeOutputStore } from '@/stores/imagePreviewStore' @@ -218,18 +219,18 @@ export class ComfyApp { /** * The node errors from the previous execution. - * @deprecated Use useExecutionStore().lastNodeErrors instead + * @deprecated Use app.extensionManager.lastNodeErrors instead */ get lastNodeErrors(): Record | null { - return useExecutionStore().lastNodeErrors + return useExecutionErrorStore().lastNodeErrors } /** * The error from the previous execution. - * @deprecated Use useExecutionStore().lastExecutionError instead + * @deprecated Use app.extensionManager.lastExecutionError instead */ get lastExecutionError(): ExecutionErrorWsMessage | null { - return useExecutionStore().lastExecutionError + return useExecutionErrorStore().lastExecutionError } /** @@ -713,7 +714,7 @@ export class ComfyApp { }) } } else if (useSettingStore().get('Comfy.RightSidePanel.ShowErrorsTab')) { - useExecutionStore().showErrorOverlay() + useExecutionErrorStore().showErrorOverlay() } else { useDialogService().showExecutionErrorDialog(detail) } @@ -1402,9 +1403,8 @@ export class ComfyApp { this.processingQueue = true const executionStore = useExecutionStore() - executionStore.lastNodeErrors = null - executionStore.lastExecutionError = null - executionStore.lastPromptError = null + const executionErrorStore = useExecutionErrorStore() + executionErrorStore.clearAllErrors() // Get auth token for backend nodes - uses workspace token if enabled, otherwise Firebase token const comfyOrgAuthToken = await useFirebaseAuthStore().getAuthToken() @@ -1440,8 +1440,8 @@ export class ComfyApp { }) delete api.authToken delete api.apiKey - executionStore.lastNodeErrors = res.node_errors ?? null - if (executionStore.lastNodeErrors?.length) { + executionErrorStore.lastNodeErrors = res.node_errors ?? null + if (executionErrorStore.lastNodeErrors?.length) { this.canvas.draw(true, true) } else { try { @@ -1477,7 +1477,8 @@ export class ComfyApp { console.error(error) if (error instanceof PromptExecutionError) { - executionStore.lastNodeErrors = error.response.node_errors ?? null + executionErrorStore.lastNodeErrors = + error.response.node_errors ?? null // Store prompt-level error separately only when no node-specific errors exist, // because node errors already carry the full context. Prompt-level errors @@ -1489,13 +1490,13 @@ export class ComfyApp { if (!hasNodeErrors) { const respError = error.response.error if (respError && typeof respError === 'object') { - executionStore.lastPromptError = { + executionErrorStore.lastPromptError = { type: respError.type, message: respError.message, details: respError.details ?? '' } } else if (typeof respError === 'string') { - executionStore.lastPromptError = { + executionErrorStore.lastPromptError = { type: 'error', message: respError, details: '' @@ -1504,7 +1505,7 @@ export class ComfyApp { } if (useSettingStore().get('Comfy.RightSidePanel.ShowErrorsTab')) { - executionStore.showErrorOverlay() + executionErrorStore.showErrorOverlay() } this.canvas.draw(true, true) } @@ -1533,7 +1534,7 @@ export class ComfyApp { } finally { this.processingQueue = false } - return !executionStore.lastNodeErrors + return !executionErrorStore.lastNodeErrors } showErrorOnFileLoad(file: File) { @@ -1880,10 +1881,8 @@ export class ComfyApp { clean() { const nodeOutputStore = useNodeOutputStore() nodeOutputStore.resetAllOutputsAndPreviews() - const executionStore = useExecutionStore() - executionStore.lastNodeErrors = null - executionStore.lastExecutionError = null - executionStore.lastPromptError = null + const executionErrorStore = useExecutionErrorStore() + executionErrorStore.clearAllErrors() useDomWidgetStore().clear() diff --git a/src/stores/executionErrorStore.ts b/src/stores/executionErrorStore.ts new file mode 100644 index 000000000..e7388390c --- /dev/null +++ b/src/stores/executionErrorStore.ts @@ -0,0 +1,280 @@ +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 { NodeLocatorId } from '@/types/nodeIdentification' +import { + executionIdToNodeLocatorId, + forEachNode, + getNodeByExecutionId +} 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) + } + + function hasInternalErrorForNode(nodeId: string | number): boolean { + const prefix = `${nodeId}:` + return allErrorExecutionIds.value.some((id) => id.startsWith(prefix)) + } + + /** + * 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, + hasInternalErrorForNode + } +}) diff --git a/src/stores/executionStore.test.ts b/src/stores/executionStore.test.ts index 37588103c..d13c23680 100644 --- a/src/stores/executionStore.test.ts +++ b/src/stores/executionStore.test.ts @@ -2,6 +2,8 @@ import { setActivePinia } from 'pinia' import { beforeEach, describe, expect, it, vi } from 'vitest' import { app } from '@/scripts/app' import { useExecutionStore } from '@/stores/executionStore' +import { useExecutionErrorStore } from '@/stores/executionErrorStore' +import { executionIdToNodeLocatorId } from '@/utils/graphTraversalUtil' // Create mock functions that will be shared const mockNodeExecutionIdToNodeLocatorId = vi.fn() @@ -80,20 +82,20 @@ describe('useExecutionStore - NodeLocatorId conversions', () => { // Mock app.rootGraph.getNodeById to return the mock node vi.mocked(app.rootGraph.getNodeById).mockReturnValue(mockNode) - const result = store.executionIdToNodeLocatorId('123:456') + const result = executionIdToNodeLocatorId(app.rootGraph, '123:456') expect(result).toBe('a1b2c3d4-e5f6-7890-abcd-ef1234567890:456') }) it('should convert simple node ID to NodeLocatorId', () => { - const result = store.executionIdToNodeLocatorId('123') + const result = executionIdToNodeLocatorId(app.rootGraph, '123') // For simple node IDs, it should return the ID as-is expect(result).toBe('123') }) it('should handle numeric node IDs', () => { - const result = store.executionIdToNodeLocatorId(123) + const result = executionIdToNodeLocatorId(app.rootGraph, 123) // For numeric IDs, it should convert to string and return as-is expect(result).toBe('123') @@ -103,7 +105,9 @@ describe('useExecutionStore - NodeLocatorId conversions', () => { // Mock app.rootGraph.getNodeById to return null (node not found) vi.mocked(app.rootGraph.getNodeById).mockReturnValue(null) - expect(store.executionIdToNodeLocatorId('999:456')).toBe(undefined) + expect(executionIdToNodeLocatorId(app.rootGraph, '999:456')).toBe( + undefined + ) }) }) @@ -174,13 +178,13 @@ describe('useExecutionStore - reconcileInitializingJobs', () => { }) }) -describe('useExecutionStore - Node Error Lookups', () => { - let store: ReturnType +describe('useExecutionErrorStore - Node Error Lookups', () => { + let store: ReturnType beforeEach(() => { vi.clearAllMocks() setActivePinia(createTestingPinia({ stubActions: false })) - store = useExecutionStore() + store = useExecutionErrorStore() }) describe('getNodeErrors', () => { diff --git a/src/stores/executionStore.ts b/src/stores/executionStore.ts index 4751f4773..7901bfce9 100644 --- a/src/stores/executionStore.ts +++ b/src/stores/executionStore.ts @@ -1,8 +1,7 @@ import { defineStore } from 'pinia' -import { computed, ref, watch } from 'vue' +import { computed, ref } from 'vue' import { useNodeProgressText } from '@/composables/node/useNodeProgressText' -import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph' import { isCloud } from '@/platform/distribution/types' import { useTelemetry } from '@/platform/telemetry' import type { ComfyWorkflow } from '@/platform/workflow/management/stores/workflowStore' @@ -20,22 +19,20 @@ import type { ExecutionInterruptedWsMessage, ExecutionStartWsMessage, ExecutionSuccessWsMessage, - NodeError, NodeProgressState, NotificationWsMessage, ProgressStateWsMessage, ProgressTextWsMessage, - ProgressWsMessage, - PromptError + ProgressWsMessage } from '@/schemas/apiSchema' import { api } from '@/scripts/api' import { app } from '@/scripts/app' import { useNodeOutputStore } from '@/stores/imagePreviewStore' import { useJobPreviewStore } from '@/stores/jobPreviewStore' +import { useExecutionErrorStore } from '@/stores/executionErrorStore' import type { NodeLocatorId } from '@/types/nodeIdentification' -import { createNodeLocatorId } from '@/types/nodeIdentification' -import { forEachNode, getNodeByExecutionId } from '@/utils/graphTraversalUtil' import { classifyCloudValidationError } from '@/utils/executionErrorUtil' +import { executionIdToNodeLocatorId } from '@/utils/graphTraversalUtil' interface QueuedJob { /** @@ -49,73 +46,14 @@ interface QueuedJob { workflow?: ComfyWorkflow } -const subgraphNodeIdToSubgraph = (id: string, graph: LGraph | Subgraph) => { - const node = graph.getNodeById(id) - if (node?.isSubgraphNode()) return node.subgraph -} - -/** - * Recursively get the subgraph objects for the given subgraph instance IDs - * @param currentGraph The current graph - * @param subgraphNodeIds The instance IDs - * @param subgraphs The subgraphs - * @returns The subgraphs that correspond to each of the instance IDs. - */ -function getSubgraphsFromInstanceIds( - currentGraph: LGraph | Subgraph, - subgraphNodeIds: string[], - subgraphs: Subgraph[] = [] -): Subgraph[] | undefined { - // Last segment is the node portion; nothing to do. - if (subgraphNodeIds.length === 1) return subgraphs - - const currentPart = subgraphNodeIds.shift() - if (currentPart === undefined) return subgraphs - - const subgraph = subgraphNodeIdToSubgraph(currentPart, currentGraph) - if (!subgraph) { - console.warn(`Subgraph not found: ${currentPart}`) - return undefined - } - - subgraphs.push(subgraph) - return getSubgraphsFromInstanceIds(subgraph, subgraphNodeIds, subgraphs) -} - -/** - * Convert execution context node IDs to NodeLocatorIds - * @param nodeId The node ID from execution context (could be execution ID) - * @returns The NodeLocatorId - */ -function executionIdToNodeLocatorId( - nodeId: string | number -): NodeLocatorId | undefined { - const nodeIdStr = String(nodeId) - - if (!nodeIdStr.includes(':')) { - // It's a top-level node ID - return nodeIdStr - } - - // It's an execution node ID - const parts = nodeIdStr.split(':') - const localNodeId = parts[parts.length - 1] - const subgraphs = getSubgraphsFromInstanceIds(app.rootGraph, parts) - if (!subgraphs) return undefined - const nodeLocatorId = createNodeLocatorId(subgraphs.at(-1)!.id, localNodeId) - return nodeLocatorId -} - export const useExecutionStore = defineStore('execution', () => { const workflowStore = useWorkflowStore() const canvasStore = useCanvasStore() + const executionErrorStore = useExecutionErrorStore() const clientId = ref(null) const activeJobId = ref(null) const queuedJobs = ref>({}) - const lastNodeErrors = ref | null>(null) - const lastExecutionError = ref(null) - const lastPromptError = ref(null) // This is the progress of all nodes in the currently executing workflow const nodeProgressStates = ref>({}) const nodeProgressStatesByJob = ref< @@ -168,7 +106,7 @@ export const useExecutionStore = defineStore('execution', () => { const parts = String(state.display_node_id).split(':') for (let i = 0; i < parts.length; i++) { const executionId = parts.slice(0, i + 1).join(':') - const locatorId = executionIdToNodeLocatorId(executionId) + const locatorId = executionIdToNodeLocatorId(app.rootGraph, executionId) if (!locatorId) continue result[locatorId] = mergeExecutionProgressStates( @@ -245,19 +183,6 @@ export const useExecutionStore = defineStore('execution', () => { return total > 0 ? done / total : 0 }) - const lastExecutionErrorNodeLocatorId = computed(() => { - const err = lastExecutionError.value - if (!err) return null - return executionIdToNodeLocatorId(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 - }) - function bindExecutionEvents() { api.addEventListener('notification', handleNotification) api.addEventListener('execution_start', handleExecutionStart) @@ -289,10 +214,7 @@ export const useExecutionStore = defineStore('execution', () => { } function handleExecutionStart(e: CustomEvent) { - lastExecutionError.value = null - lastPromptError.value = null - lastNodeErrors.value = null - isErrorOverlayOpen.value = false + executionErrorStore.clearAllErrors() activeJobId.value = e.detail.prompt_id queuedJobs.value[activeJobId.value] ??= { nodes: {} } clearInitializationByJobId(activeJobId.value) @@ -410,7 +332,7 @@ export const useExecutionStore = defineStore('execution', () => { if (handleServiceLevelError(e.detail)) return // OSS path / Cloud fallback (real runtime errors) - lastExecutionError.value = e.detail + executionErrorStore.lastExecutionError = e.detail clearInitializationByJobId(e.detail.prompt_id) resetExecutionState(e.detail.prompt_id) } @@ -422,7 +344,7 @@ export const useExecutionStore = defineStore('execution', () => { clearInitializationByJobId(detail.prompt_id) resetExecutionState(detail.prompt_id) - lastPromptError.value = { + executionErrorStore.lastPromptError = { type: detail.exception_type ?? 'error', message: detail.exception_type ? `${detail.exception_type}: ${detail.exception_message}` @@ -442,9 +364,9 @@ export const useExecutionStore = defineStore('execution', () => { resetExecutionState(detail.prompt_id) if (result.kind === 'nodeErrors') { - lastNodeErrors.value = result.nodeErrors + executionErrorStore.lastNodeErrors = result.nodeErrors } else { - lastPromptError.value = result.promptError + executionErrorStore.lastPromptError = result.promptError } return true } @@ -515,7 +437,7 @@ export const useExecutionStore = defineStore('execution', () => { } activeJobId.value = null _executingNodeProgress.value = null - lastPromptError.value = null + executionErrorStore.clearPromptError() } function getNodeIdIfExecuting(nodeId: string | number) { @@ -596,207 +518,11 @@ export const useExecutionStore = defineStore('execution', () => { () => runningJobIds.value.length ) - /** 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(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) - } - - /** - * 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 - } - } - } - }) - - /** 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 - }) - - function hasInternalErrorForNode(nodeId: string | number): boolean { - const prefix = `${nodeId}:` - return allErrorExecutionIds.value.some((id) => id.startsWith(prefix)) - } - - const isErrorOverlayOpen = ref(false) - - function showErrorOverlay() { - isErrorOverlayOpen.value = true - } - - function dismissErrorOverlay() { - isErrorOverlayOpen.value = false - } - return { isIdle, clientId, activeJobId, queuedJobs, - lastNodeErrors, - lastExecutionError, - lastPromptError, - hasAnyError, - allErrorExecutionIds, - totalErrorCount, - lastExecutionErrorNodeId, executingNodeId, executingNodeIds, activeJob, @@ -823,16 +549,7 @@ export const useExecutionStore = defineStore('execution', () => { // Raw executing progress data for backward compatibility in ComfyApp. _executingNodeProgress, // NodeLocatorId conversion helpers - executionIdToNodeLocatorId, nodeLocatorIdToExecutionId, - jobIdToWorkflowId, - // Node error lookup helpers - getNodeErrors, - slotHasError, - hasInternalErrorForNode, - activeGraphErrorNodeIds, - isErrorOverlayOpen, - showErrorOverlay, - dismissErrorOverlay + jobIdToWorkflowId } }) diff --git a/src/stores/imagePreviewStore.test.ts b/src/stores/imagePreviewStore.test.ts index 9a8a10e9f..ff358c48b 100644 --- a/src/stores/imagePreviewStore.test.ts +++ b/src/stores/imagePreviewStore.test.ts @@ -38,10 +38,8 @@ const createMockOutputs = ( images?: ExecutedWsMessage['output']['images'] ): ExecutedWsMessage['output'] => ({ images }) -vi.mock('@/stores/executionStore', () => ({ - useExecutionStore: vi.fn(() => ({ - executionIdToNodeLocatorId: vi.fn((id: string) => id) - })) +vi.mock('@/utils/graphTraversalUtil', () => ({ + executionIdToNodeLocatorId: vi.fn((_rootGraph: unknown, id: string) => id) })) vi.mock('@/platform/workflow/management/stores/workflowStore', () => ({ diff --git a/src/stores/imagePreviewStore.ts b/src/stores/imagePreviewStore.ts index 08740e826..eea5063ca 100644 --- a/src/stores/imagePreviewStore.ts +++ b/src/stores/imagePreviewStore.ts @@ -12,7 +12,6 @@ import type { } from '@/schemas/apiSchema' import { api } from '@/scripts/api' import { app } from '@/scripts/app' -import { useExecutionStore } from '@/stores/executionStore' import type { NodeLocatorId } from '@/types/nodeIdentification' import { parseFilePath } from '@/utils/formatUtil' import { isAnimatedOutput, isVideoNode } from '@/utils/litegraphUtil' @@ -20,6 +19,7 @@ import { releaseSharedObjectUrl, retainSharedObjectUrl } from '@/utils/objectUrlUtil' +import { executionIdToNodeLocatorId } from '@/utils/graphTraversalUtil' const PREVIEW_REVOKE_DELAY_MS = 400 @@ -43,7 +43,6 @@ interface SetOutputOptions { export const useNodeOutputStore = defineStore('nodeOutput', () => { const { nodeIdToNodeLocatorId, nodeToNodeLocatorId } = useWorkflowStore() - const { executionIdToNodeLocatorId } = useExecutionStore() const scheduledRevoke: Record void }> = {} const latestPreview = ref([]) @@ -202,7 +201,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { outputs: ExecutedWsMessage['output'] | ResultItem, options: SetOutputOptions = {} ) { - const nodeLocatorId = executionIdToNodeLocatorId(executionId) + const nodeLocatorId = executionIdToNodeLocatorId(app.rootGraph, executionId) if (!nodeLocatorId) return setOutputsByLocatorId(nodeLocatorId, outputs, options) @@ -219,7 +218,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { executionId: string, previewImages: string[] ) { - const nodeLocatorId = executionIdToNodeLocatorId(executionId) + const nodeLocatorId = executionIdToNodeLocatorId(app.rootGraph, executionId) if (!nodeLocatorId) return const existingPreviews = app.nodePreviewImages[nodeLocatorId] if (scheduledRevoke[nodeLocatorId]) { @@ -275,7 +274,7 @@ export const useNodeOutputStore = defineStore('nodeOutput', () => { * @param executionId - The execution ID */ function revokePreviewsByExecutionId(executionId: string) { - const nodeLocatorId = executionIdToNodeLocatorId(executionId) + const nodeLocatorId = executionIdToNodeLocatorId(app.rootGraph, executionId) if (!nodeLocatorId) return scheduleRevoke(nodeLocatorId, () => revokePreviewsByLocatorId(nodeLocatorId) diff --git a/src/stores/subgraphStore.ts b/src/stores/subgraphStore.ts index 78fac3d13..57b69f817 100644 --- a/src/stores/subgraphStore.ts +++ b/src/stores/subgraphStore.ts @@ -25,7 +25,7 @@ import { TemplateIncludeOnDistributionEnum } from '@/platform/workflow/templates import { api } from '@/scripts/api' import type { GlobalSubgraphData } from '@/scripts/api' import { useDialogService } from '@/services/dialogService' -import { useExecutionStore } from '@/stores/executionStore' +import { useExecutionErrorStore } from '@/stores/executionErrorStore' import { ComfyNodeDefImpl } from '@/stores/nodeDefStore' import type { UserFile } from '@/stores/userFileStore' @@ -79,7 +79,7 @@ export const useSubgraphStore = defineStore('subgraph', () => { dependent_outputs: [] } } - useExecutionStore().lastNodeErrors = errors + useExecutionErrorStore().lastNodeErrors = errors useCanvasStore().getCanvas().draw(true, true) throw new Error( 'The root graph of a subgraph blueprint must consist of only a single subgraph node' diff --git a/src/stores/workspaceStore.ts b/src/stores/workspaceStore.ts index f9c3d13cd..01c411075 100644 --- a/src/stores/workspaceStore.ts +++ b/src/stores/workspaceStore.ts @@ -12,6 +12,7 @@ import type { SidebarTabExtension, ToastManager } from '@/types/extensionTypes' import { useApiKeyAuthStore } from './apiKeyAuthStore' import { useCommandStore } from './commandStore' +import { useExecutionErrorStore } from './executionErrorStore' import { useFirebaseAuthStore } from './firebaseAuthStore' import { useQueueSettingsStore } from './queueStore' import { useBottomPanelStore } from './workspace/bottomPanelStore' @@ -86,6 +87,8 @@ function workspaceStoreSetup() { return sidebarTab.value.sidebarTabs } + const executionErrorStore = useExecutionErrorStore() + return { spinner, shiftDown, @@ -104,6 +107,10 @@ function workspaceStoreSetup() { bottomPanel, user: partialUserStore, + // Execution error state (read-only, exposed for custom extensions) + lastNodeErrors: computed(() => executionErrorStore.lastNodeErrors), + lastExecutionError: computed(() => executionErrorStore.lastExecutionError), + registerSidebarTab, unregisterSidebarTab, getSidebarTabs diff --git a/src/types/extensionTypes.ts b/src/types/extensionTypes.ts index 7061a9b15..7c9c71b2c 100644 --- a/src/types/extensionTypes.ts +++ b/src/types/extensionTypes.ts @@ -1,5 +1,7 @@ import type { Component } from 'vue' +import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema' +import type { ExecutionErrorWsMessage, NodeError } from '@/schemas/apiSchema' import type { useDialogService } from '@/services/dialogService' import type { ComfyCommand } from '@/stores/commandStore' @@ -111,6 +113,10 @@ export interface ExtensionManager { get: (id: string) => T | undefined set: (id: string, value: T) => void } + + // Execution error state (read-only) + lastNodeErrors: Record | null + lastExecutionError: ExecutionErrorWsMessage | null } export interface CommandManager { diff --git a/src/utils/graphTraversalUtil.ts b/src/utils/graphTraversalUtil.ts index 92d7f4db0..64a33b660 100644 --- a/src/utils/graphTraversalUtil.ts +++ b/src/utils/graphTraversalUtil.ts @@ -4,7 +4,10 @@ import type { Subgraph } from '@/lib/litegraph/src/litegraph' import type { NodeExecutionId, NodeLocatorId } from '@/types/nodeIdentification' -import { parseNodeLocatorId } from '@/types/nodeIdentification' +import { + createNodeLocatorId, + parseNodeLocatorId +} from '@/types/nodeIdentification' import { isSubgraphIoNode } from './typeGuardUtil' @@ -359,6 +362,36 @@ export function getNodeByLocatorId( return targetSubgraph.getNodeById(localNodeId) || null } +/** + * Convert execution context node IDs to NodeLocatorIds. + * Uses traverseSubgraphPath to resolve the subgraph chain. + * + * @param rootGraph - The root graph to resolve against + * @param nodeId - The node ID from execution context (could be execution ID like "123:456:789") + * @returns The NodeLocatorId, or undefined if resolution fails + */ +export function executionIdToNodeLocatorId( + rootGraph: LGraph, + nodeId: string | number +): NodeLocatorId | undefined { + const nodeIdStr = String(nodeId) + + if (!nodeIdStr.includes(':')) { + // It's a top-level node ID + return nodeIdStr + } + + // It's an execution node ID — resolve subgraph path + const parts = nodeIdStr.split(':') + const localNodeId = parts.at(-1)! + const subgraphPath = parts.slice(0, -1) + + const targetGraph = traverseSubgraphPath(rootGraph, subgraphPath) + if (!targetGraph) return undefined + + return createNodeLocatorId(targetGraph.id, localNodeId) +} + /** * Finds the root graph from any graph in the hierarchy. *