feat: node-specific error tab with selection-aware grouping and error overlay (#8956)

## Summary
Enhances the error panel with node-specific views: single-node selection
shows errors grouped by message in compact mode, container nodes
(subgraph/group) expose child errors via a badge and "See Error" button,
and a floating ErrorOverlay appears after execution failure with a
deduplicated summary and quick navigation to the errors tab.

## Changes
- **Consolidate error tab**: Remove `TabError.vue`; merge all error
display into `TabErrors.vue` and drop the separate `error` tab type from
`rightSidePanelStore`
- **Selection-aware grouping**: Single-node selection regroups errors by
message (not `class_type`) and renders `ErrorNodeCard` in compact mode
- **Container node support**: Detect child-node errors in subgraph/group
nodes via execution ID prefix matching; show error badge and "See Error"
button in `SectionWidgets`
- **ErrorOverlay**: New floating card shown after execution failure with
deduplicated error messages, "Dismiss" and "See Errors" actions;
`isErrorOverlayOpen` / `showErrorOverlay` / `dismissErrorOverlay` added
to `executionStore`
- **Refactor**: Centralize error ID collection in `executionStore`
(`allErrorExecutionIds`, `hasInternalErrorForNode`); split `errorGroups`
into `allErrorGroups` (unfiltered) and `tabErrorGroups`
(selection-filtered); move `ErrorOverlay` business logic into
`useErrorGroups`

## Review Focus
- `useErrorGroups.ts`: split into `allErrorGroups` / `tabErrorGroups`
and the new `filterBySelection` parameter flow
- `executionStore.ts`: `hasInternalErrorForNode` helper and
`allErrorExecutionIds` computed
- `ErrorOverlay.vue`: integration with `executionStore` overlay state
and `useErrorGroups`

## Screenshots
<img width="853" height="461" alt="image"
src="https://github.com/user-attachments/assets/a49ab620-4209-4ae7-b547-fba13da0c633"
/>
<img width="854" height="203" alt="image"
src="https://github.com/user-attachments/assets/c119da54-cd78-4e7a-8b7a-456cfd348f1d"
/>
<img width="497" height="361" alt="image"
src="https://github.com/user-attachments/assets/74b16161-cf45-454b-ae60-24922fe36931"
/>

---------

Co-authored-by: GitHub Action <action@github.com>
Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
jaeone94
2026-02-21 05:14:52 +09:00
committed by GitHub
parent c2452c5d20
commit 46c40c755e
25 changed files with 997 additions and 240 deletions

View File

@@ -1,6 +1,5 @@
import { defineStore } from 'pinia'
import { computed, ref, watch } from 'vue'
import { isEmpty } from 'es-toolkit/compat'
import { useNodeProgressText } from '@/composables/node/useNodeProgressText'
import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
@@ -36,6 +35,7 @@ import { useJobPreviewStore } from '@/stores/jobPreviewStore'
import type { NodeLocatorId } from '@/types/nodeIdentification'
import { createNodeLocatorId } from '@/types/nodeIdentification'
import { forEachNode, getNodeByExecutionId } from '@/utils/graphTraversalUtil'
import { classifyCloudValidationError } from '@/utils/executionErrorUtil'
interface QueuedJob {
/**
@@ -291,6 +291,8 @@ export const useExecutionStore = defineStore('execution', () => {
function handleExecutionStart(e: CustomEvent<ExecutionStartWsMessage>) {
lastExecutionError.value = null
lastPromptError.value = null
lastNodeErrors.value = null
isErrorOverlayOpen.value = false
activeJobId.value = e.detail.prompt_id
queuedJobs.value[activeJobId.value] ??= { nodes: {} }
clearInitializationByJobId(activeJobId.value)
@@ -391,7 +393,6 @@ export const useExecutionStore = defineStore('execution', () => {
}
function handleExecutionError(e: CustomEvent<ExecutionErrorWsMessage>) {
lastExecutionError.value = e.detail
if (isCloud) {
useTelemetry()?.trackExecutionError({
jobId: e.detail.prompt_id,
@@ -399,11 +400,55 @@ export const useExecutionStore = defineStore('execution', () => {
nodeType: e.detail.node_type,
error: e.detail.exception_message
})
// Cloud wraps validation errors (400) in exception_message as embedded JSON.
if (handleCloudValidationError(e.detail)) return
}
// Service-level errors (e.g. "Job has stagnated") have no associated node.
// Route them as job errors
if (handleServiceLevelError(e.detail)) return
// OSS path / Cloud fallback (real runtime errors)
lastExecutionError.value = e.detail
clearInitializationByJobId(e.detail.prompt_id)
resetExecutionState(e.detail.prompt_id)
}
function handleServiceLevelError(detail: ExecutionErrorWsMessage): boolean {
const nodeId = detail.node_id
if (nodeId !== null && nodeId !== undefined && String(nodeId) !== '')
return false
clearInitializationByJobId(detail.prompt_id)
resetExecutionState(detail.prompt_id)
lastPromptError.value = {
type: detail.exception_type ?? 'error',
message: detail.exception_type
? `${detail.exception_type}: ${detail.exception_message}`
: (detail.exception_message ?? ''),
details: detail.traceback?.join('\n') ?? ''
}
return true
}
function handleCloudValidationError(
detail: ExecutionErrorWsMessage
): boolean {
const result = classifyCloudValidationError(detail.exception_message)
if (!result) return false
clearInitializationByJobId(detail.prompt_id)
resetExecutionState(detail.prompt_id)
if (result.kind === 'nodeErrors') {
lastNodeErrors.value = result.nodeErrors
} else {
lastPromptError.value = result.promptError
}
return true
}
/**
* Notification handler used for frontend/cloud initialization tracking.
* Marks a job as initializing when cloud notifies it is waiting for a machine.
@@ -653,7 +698,7 @@ export const useExecutionStore = defineStore('execution', () => {
/** Whether any node validation errors are present */
const hasNodeError = computed(
() => !!lastNodeErrors.value && !isEmpty(lastNodeErrors.value)
() => !!lastNodeErrors.value && Object.keys(lastNodeErrors.value).length > 0
)
/** Whether any error (node validation, runtime execution, or prompt-level) is present */
@@ -661,12 +706,49 @@ export const useExecutionStore = defineStore('execution', () => {
() => hasExecutionError.value || hasPromptError.value || hasNodeError.value
)
/** Pre-computed Set of graph node IDs (as strings) that have errors. */
const allErrorExecutionIds = computed<string[]>(() => {
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<Set<string>>(() => {
const ids = new Set<string>()
if (!app.rootGraph) return ids
const activeGraph = useCanvasStore().currentGraph ?? app.rootGraph
// 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)) {
@@ -688,6 +770,21 @@ export const useExecutionStore = defineStore('execution', () => {
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,
@@ -697,6 +794,8 @@ export const useExecutionStore = defineStore('execution', () => {
lastExecutionError,
lastPromptError,
hasAnyError,
allErrorExecutionIds,
totalErrorCount,
lastExecutionErrorNodeId,
executingNodeId,
executingNodeIds,
@@ -730,6 +829,10 @@ export const useExecutionStore = defineStore('execution', () => {
// Node error lookup helpers
getNodeErrors,
slotHasError,
activeGraphErrorNodeIds
hasInternalErrorForNode,
activeGraphErrorNodeIds,
isErrorOverlayOpen,
showErrorOverlay,
dismissErrorOverlay
}
})

View File

@@ -4,7 +4,6 @@ import { computed, ref, watch } from 'vue'
import { useSettingStore } from '@/platform/settings/settingStore'
export type RightSidePanelTab =
| 'error'
| 'parameters'
| 'nodes'
| 'settings'