feat: auto-resolve simple validation errors on widget change and slot connection (#9464)

## Summary

Automatically clears transient validation errors
(`value_bigger_than_max`, `value_smaller_than_min`, `value_not_in_list`,
`required_input_missing`) when the user modifies a widget value or
connects an input slot, so resolved errors don't linger in the error
panel. Also clears missing model state when the user changes a combo
widget value.

## Changes

- **`useNodeErrorAutoResolve` composable**: watches widget changes and
slot connections, clears matching errors via `executionErrorStore`
- **`executionErrorStore`**: adds `clearSimpleNodeErrors` and
`clearSimpleWidgetErrorIfValid` with granular per-slot error removal
- **`executionErrorUtil`**: adds `isValueStillOutOfRange` to prevent
premature clearing when a new value still violates the constraint
- **`graphTraversalUtil`**: adds `getExecutionIdFromNodeData` for
subgraph-aware execution ID resolution
- **`GraphCanvas.vue`**: fixes subgraph error key lookup by using
`getExecutionIdByNode` instead of raw `node.id`
- **`NodeWidgets.vue`**: wires up the new composable to the widget layer
- **`missingModelStore`**: adds `removeMissingModelByWidget` to clear
missing model state on widget value change
- **`useGraphNodeManager`**: registers composable per node
- **Tests**: 126 new unit tests covering error clearing, range
validation, and graph traversal edge cases

## Screenshots



https://github.com/user-attachments/assets/515ea811-ff84-482a-a866-a17e5c779c39



https://github.com/user-attachments/assets/a2b30f02-4929-4537-952c-a0febe20f02e


┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9464-feat-auto-resolve-simple-validation-errors-on-widget-change-and-slot-connection-31b6d73d3650816b8afdc34f4b40295a)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
jaeone94
2026-03-13 23:49:44 +09:00
committed by GitHub
parent d73f8e1beb
commit 31a33a0ba2
16 changed files with 1790 additions and 105 deletions

View File

@@ -143,6 +143,7 @@ import TopbarBadges from '@/components/topbar/TopbarBadges.vue'
import TopbarSubscribeButton from '@/components/topbar/TopbarSubscribeButton.vue'
import WorkflowTabs from '@/components/topbar/WorkflowTabs.vue'
import { useChainCallback } from '@/composables/functional/useChainCallback'
import { installErrorClearingHooks } from '@/composables/graph/useErrorClearingHooks'
import type { VueNodeData } from '@/composables/graph/useGraphNodeManager'
import { useVueNodeLifecycle } from '@/composables/graph/useVueNodeLifecycle'
import { useNodeBadge } from '@/composables/node/useNodeBadge'
@@ -245,6 +246,16 @@ const { shouldRenderVueNodes } = useVueFeatureFlags()
// Vue node system
const vueNodeLifecycle = useVueNodeLifecycle()
// Error-clearing hooks run regardless of rendering mode (Vue or legacy canvas).
let cleanupErrorHooks: (() => void) | null = null
watch(
() => canvasStore.currentGraph,
(graph) => {
cleanupErrorHooks?.()
cleanupErrorHooks = graph ? installErrorClearingHooks(graph) : null
}
)
const handleVueNodeLifecycleReset = async () => {
if (shouldRenderVueNodes.value) {
vueNodeLifecycle.disposeNodeManagerAndSyncs()
@@ -391,39 +402,12 @@ watch(
}
)
// Update node slot errors for LiteGraph nodes
// (Vue nodes read from store directly)
// Repaint canvas when node errors change.
// Slot error flags are reconciled by reconcileNodeErrorFlags in executionErrorStore.
watch(
() => executionErrorStore.lastNodeErrors,
(lastNodeErrors) => {
if (!comfyApp.graph) return
forEachNode(comfyApp.rootGraph, (node) => {
// Clear existing errors
for (const slot of node.inputs) {
delete slot.hasErrors
}
for (const slot of node.outputs) {
delete slot.hasErrors
}
const nodeErrors = lastNodeErrors?.[node.id]
if (!nodeErrors) return
const validErrors = nodeErrors.errors.filter(
(error) => error.extra_info?.input_name !== undefined
)
validErrors.forEach((error) => {
const inputName = error.extra_info!.input_name!
const inputIndex = node.findInputSlot(inputName)
if (inputIndex !== -1) {
node.inputs[inputIndex].hasErrors = true
}
})
})
comfyApp.canvas.setDirty(true, true)
() => {
comfyApp.canvas?.setDirty(true, true)
}
)
@@ -526,6 +510,11 @@ onMounted(async () => {
comfyAppReady.value = true
// Install error-clearing hooks on the initial graph
if (comfyApp.canvas?.graph) {
cleanupErrorHooks = installErrorClearingHooks(comfyApp.canvas.graph)
}
vueNodeLifecycle.setupEmptyGraphListener()
} finally {
workspaceStore.spinner = false
@@ -569,6 +558,8 @@ onMounted(async () => {
})
onUnmounted(() => {
cleanupErrorHooks?.()
cleanupErrorHooks = null
vueNodeLifecycle.cleanup()
})
function forwardPanEvent(e: PointerEvent) {