## Summary Backend validation errors (`node_errors`) are reported against the **flattened** prompt, so an error whose real fix lives on a subgraph host node gets attached to an interior node the user may never open — and, after ADR 0009, often *cannot* meaningfully fix there. This PR re-surfaces a validation error onto the subgraph host node **when — and only when — the error's subject (the specific input/widget named by `extra_info.input_name`) is exposed through the subgraph boundary**. ## Why this is needed Two concrete situations motivated this, both observed in real workflows: 1. **Broken link at the host.** Root node A should feed subgraph host B, whose boundary input is linked to interior node C. If the A→B link is missing, the backend flattens the prompt, sees C with no resolved input, and raises `required_input_missing` **on C** (`"B:C"`). The actual fix — connect B's input — is one level up, on a node the error never points at. 2. **Host-owned widget values.** Per ADR 0009 (subgraph promoted widgets use linked inputs), a promoted widget's value is owned by the host `SubgraphNode`; the interior widget only supplies schema/defaults. When the backend raises `value_not_in_list` (or min/max violations) for that value, attributing it to the interior node is factually wrong — the value that failed validation *is the host's value*. This continues the direction of #13059, which moved **missing-model** detection identity to `{hostExecutionId, hostWidgetName}` with the interior path kept as diagnostics. That was possible in the FE pre-scan; this PR applies the same ownership principle to **backend-received** errors via a receive-side mapping, since the backend cannot know about subgraph boundaries in a flattened prompt. ## The rule (design) > Lift an error from interior node N to host H **iff** N's input slot named by the error is linked to the containing subgraph's boundary (`SubgraphInput`). Apply the same test again at H (boundary-by-boundary, matching ADR 0009's chaining principle) and stop at the first level where the subject is no longer boundary-linked — that node is where the user can actually fix it. The predicate is **structural (boundary exposure), not data-flow**. ### In scope — examples - `required_input_missing` on interior `"12:5"` whose input is fed by the boundary → surfaces on host `12`'s input slot (red slot ring on the host, errors-tab card titled/located at the host, message names the host's `SubgraphInput.name`). - `value_not_in_list` / `value_smaller_than_min` / `value_bigger_than_max` on a promoted interior widget → surfaces on the host's promoted widget. Nested hosts chain: `"1:2:3"` lifts to `"1:2"`, and further to `"1"` only if `1:2`'s own slot is boundary-linked too. - Clearing follows the surface: connecting the highlighted host input or fixing the host widget clears the underlying interior (raw) error — range-guarded per target, so a still-out-of-range host value does **not** clear. ### Out of scope — examples - **No value-flow ancestry.** All in the root graph: A's widget links to B, B's to C, and C rejects the value that originated at A → the error **stays on C**. Following same-graph links to a "root cause" node is explicitly not this feature. - Errors without an `input_name` subject, node-level types (`exception_during_validation`, `dependency_cycle`, image-not-loaded), and unknown validation types — never lifted. Unknown types stay node-scoped to match how the error catalog renders them (the shared `isNodeLevelValidationError` in `executionErrorUtil` encodes this, and the catalog derives its node-level rules from the same set). - Runtime execution errors (exceptions during a run) — validation responses only. - Interior errors whose input is fed by another interior node — fixable in place, stay in place. - Fan-out display dedupe: when one boundary input feeds multiple interior nodes and the host slot is unconnected, each interior error lifts to the same host slot as a separate panel line. A single fix (connecting the host input) clears all of them — the clearing translation already fans out — so the duplication is cosmetic. Display-level dedupe is a follow-up; deduping inside the lift would break the one-source-per-error clearing contract. - Reactive re-lifting on graph topology edits while errors are displayed (invariant documented on the computed; follow-up), and deriving the error catalog's full validation rule table from the shared classification (follow-up; the node-level type set and the image-not-loaded predicate are already single-sourced in `executionErrorUtil` and consumed by both the lift and the catalog). ## Changes - **What**: New pure module `core/graph/subgraph/liftNodeErrorsToBoundary.ts` — per-error, fail-open record transform (unresolvable ids/slots/links leave the error where the backend put it; raw payload is never mutated). `executionErrorStore` derives `surfacedNodeErrors` from it and display consumers switch over (errors tab grouping, canvas node/slot flags, Vue node badges, app-mode/linear hints); raw `lastNodeErrors` remains the source of truth for mutation. Host-side clearing translates through the lift's diagnostics fields (`source_execution_id` / `source_input_name`) with a per-target range guard. - **Breaking**: None. No persistence/serialization changes; interior identity survives as diagnostics metadata only (ADR 0009 language). ## Review Focus - The lift predicate lives entirely on link topology (`LLink.originIsIoNode` → `SubgraphInput`) — no `proxyWidgets`/promotion-store style source authority is reintroduced. - `clearSlotErrorsWithRangeCheck` now resolves clear targets first and range-checks each target's raw errors; the lifted-path twin of the existing range-retention test pins this. - `useProcessedWidgets` deliberately stays on the raw record: host promoted widgets already map to interior errors via `widget.sourceExecutionId`, so an interior widget keeps its local red hint when the user opens the subgraph (hint layer vs surface layer). - Unit coverage is carried by the pure module (real litegraph subgraph fixtures, incl. nested recursion, promoted widgets via `promoteValueWidgetViaSubgraphInput`, ordering, fail-open/no-mutation); one e2e pins the user-visible contract (host ring + host slot dot + interior clean). ## Screenshots ### Before https://github.com/user-attachments/assets/81e5c4db-515d-4f1f-8f8a-e07ac490510f ### After https://github.com/user-attachments/assets/2949da06-a049-41c1-a480-98ee28333bf2
Stores
This directory contains Pinia stores for the ComfyUI frontend application. Stores provide centralized state management for the application.
Table of Contents
Overview
Stores in ComfyUI use Pinia, Vue's official state management library. Each store is responsible for managing a specific domain of the application state, such as user data, workflow information, graph state, and UI configuration.
Stores provide a way to maintain global application state that can be accessed from any component, regardless of where those components are in the component hierarchy. This solves the problem of "prop drilling" (passing data down through multiple levels of components) and allows components that aren't directly related to share and modify the same state.
For example, without global state:
App
│
┌──────────┴──────────┐
│ │
HeaderBar Canvas
│ │
│ │
UserMenu NodeProperties
In this structure, if the UserMenu component needs to update something that affects NodeProperties, the data would need to be passed up to App and then down again, through all intermediate components.
With Pinia stores, components can directly access and update the shared state:
┌─────────────────┐
│ │
│ Pinia Stores │
│ │
└───────┬─────────┘
│
│ Accessed by
▼
┌──────────────────────────┐
│ │
│ Components │
│ │
└──────────────────────────┘
Store Architecture
The store architecture in ComfyUI follows these principles:
- Domain-driven: Each store focuses on a specific domain of the application
- Single source of truth: Stores serve as the definitive source for specific data
- Composition: Stores can interact with other stores when needed
- Actions for logic: Business logic is encapsulated in store actions
- Getters for derived state: Computed values are exposed via getters
The following diagram illustrates the store architecture and data flow:
┌─────────────────────────────────────────────────────────┐
│ Vue Components │
│ │
│ ┌───────────────┐ ┌───────────────┐ │
│ │ Component A │ │ Component B │ │
│ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │
└───────────┼────────────────────────────┼────────────────┘
│ │
│ ┌───────────────┐ │
└────►│ Composables │◄─────┘
└───────┬───────┘
│
┌─────────────────────────┼─────────────────────────────┐
│ Pinia Stores │ │
│ │ │
│ ┌───────────────────▼───────────────────────┐ │
│ │ Actions │ │
│ └───────────────────┬───────────────────────┘ │
│ │ │
│ ┌───────────────────▼───────────────────────┐ │
│ │ State │ │
│ └───────────────────┬───────────────────────┘ │
│ │ │
│ ┌───────────────────▼───────────────────────┐ │
│ │ Getters │ │
│ └───────────────────┬───────────────────────┘ │
│ │ │
└─────────────────────────┼─────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ External Services │
│ (API, localStorage, WebSocket, etc.) │
└─────────────────────────────────────────────────────────┘
Core Stores
The following table lists ALL 46 store instances in the system as of 2026-01-29:
Main Stores
| File | Store | Description | Category |
|---|---|---|---|
| aboutPanelStore.ts | useAboutPanelStore | Manages the About panel state and badges | UI |
| apiKeyAuthStore.ts | useApiKeyAuthStore | Handles API key authentication | Auth |
| comfyManagerStore.ts | useComfyManagerStore | Manages ComfyUI application state | Core |
| comfyRegistryStore.ts | useComfyRegistryStore | Handles extensions registry | Registry |
| commandStore.ts | useCommandStore | Manages commands and command execution | Core |
| dialogStore.ts | useDialogStore | Controls dialog/modal display and state | UI |
| domWidgetStore.ts | useDomWidgetStore | Manages DOM widget state | Widgets |
| electronDownloadStore.ts | useElectronDownloadStore | Handles Electron-specific download operations | Platform |
| executionStore.ts | useExecutionStore | Tracks workflow execution state | Execution |
| extensionStore.ts | useExtensionStore | Manages extension registration and state | Extensions |
| firebaseAuthStore.ts | useFirebaseAuthStore | Handles Firebase authentication | Auth |
| graphStore.ts | useTitleEditorStore | Manages title editing for nodes and groups | UI |
| graphStore.ts | useCanvasStore | Manages the graph canvas state and interactions | Core |
| helpCenterStore.ts | useHelpCenterStore | Manages help center visibility and state | UI |
| nodeOutputStore.ts | useNodeOutputStore | Manages node outputs and execution results | Media |
| maintenanceTaskStore.ts | useMaintenanceTaskStore | Handles system maintenance tasks | System |
| menuItemStore.ts | useMenuItemStore | Handles menu items and their state | UI |
| modelStore.ts | useModelStore | Manages AI models information | Models |
| modelToNodeStore.ts | useModelToNodeStore | Maps models to compatible nodes | Models |
| nodeBookmarkStore.ts | useNodeBookmarkStore | Manages node bookmarks and favorites | Nodes |
| nodeDefStore.ts | useNodeDefStore | Manages node definitions and schemas | Nodes |
| nodeDefStore.ts | useNodeFrequencyStore | Tracks node usage frequency | Nodes |
| queueStore.ts | useQueueStore | Manages execution queue and task history | Execution |
| queueStore.ts | useQueuePendingTaskCountStore | Tracks pending task counts | Execution |
| queueStore.ts | useQueueSettingsStore | Manages queue execution settings | Execution |
| releaseStore.ts | useReleaseStore | Manages application release information | System |
| serverConfigStore.ts | useServerConfigStore | Handles server configuration | Config |
| settingStore.ts | useSettingStore | Manages application settings | Config |
| subgraphNavigationStore.ts | useSubgraphNavigationStore | Handles subgraph navigation state | Navigation |
| systemStatsStore.ts | useSystemStatsStore | Tracks system performance statistics | System |
| toastStore.ts | useToastStore | Manages toast notifications | UI |
| userFileStore.ts | useUserFileStore | Manages user file operations | Files |
| userStore.ts | useUserStore | Manages user data and preferences | User |
| versionCompatibilityStore.ts | useVersionCompatibilityStore | Manages frontend/backend version compatibility warnings | Core |
| widgetStore.ts | useWidgetStore | Manages widget configurations | Widgets |
| workflowStore.ts | useWorkflowStore | Handles workflow data and operations | Workflows |
| workflowStore.ts | useWorkflowBookmarkStore | Manages workflow bookmarks and favorites | Workflows |
| workflowTemplatesStore.ts | useWorkflowTemplatesStore | Manages workflow templates | Workflows |
| workspaceStore.ts | useWorkspaceStore | Manages overall workspace state | Workspace |
Workspace Stores
Located in stores/workspace/:
| File | Store | Description | Category |
|---|---|---|---|
| bottomPanelStore.ts | useBottomPanelStore | Controls bottom panel visibility and state | UI |
| colorPaletteStore.ts | useColorPaletteStore | Manages color palette configurations | UI |
| nodeHelpStore.ts | useNodeHelpStore | Handles node help and documentation display | UI |
| searchBoxStore.ts | useSearchBoxStore | Manages search box functionality | UI |
| sidebarTabStore.ts | useSidebarTabStore | Controls sidebar tab states and navigation | UI |
Store Development Guidelines
When developing or modifying stores, follow these best practices:
- Define clear purpose: Each store should have a specific responsibility
- Use actions for async operations: Encapsulate asynchronous logic in actions
- Keep stores focused: Each store should manage related state
- Document public API: Add comments for state properties, actions, and getters
- Use getters for derived state: Compute derived values using getters
- Test store functionality: Write unit tests for stores
Store Template
Here's a template for creating a new Pinia store, following the setup style used in ComfyUI:
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useExampleStore = defineStore('example', () => {
// State
const items = ref([])
const isLoading = ref(false)
const error = ref(null)
// Getters
const itemCount = computed(() => items.value.length)
const hasError = computed(() => error.value !== null)
// Actions
function addItem(item) {
items.value.push(item)
}
async function fetchItems() {
isLoading.value = true
error.value = null
try {
const response = await fetch('/api/items')
const data = await response.json()
items.value = data
} catch (err) {
error.value = err.message
} finally {
isLoading.value = false
}
}
// Expose state, getters, and actions
return {
// State
items,
isLoading,
error,
// Getters
itemCount,
hasError,
// Actions
addItem,
fetchItems
}
})
Common Patterns
Stores in ComfyUI frequently use these patterns:
API Integration
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { api } from '@/scripts/api'
export const useDataStore = defineStore('data', () => {
const data = ref([])
const loading = ref(false)
const error = ref(null)
async function fetchData() {
loading.value = true
try {
const result = await api.getExtensions()
data.value = result
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}
return {
data,
loading,
error,
fetchData
}
})
Store Composition
import { defineStore, storeToRefs } from 'pinia'
import { computed, ref, watch } from 'vue'
import { useOtherStore } from './otherStore'
export const useComposedStore = defineStore('composed', () => {
const otherStore = useOtherStore()
const { someData } = storeToRefs(otherStore)
// Local state
const localState = ref(0)
// Computed value based on other store
const derivedValue = computed(() => {
return computeFromOtherData(someData.value, localState.value)
})
// Action that uses another store
async function complexAction() {
await otherStore.someAction()
localState.value += 1
}
return {
localState,
derivedValue,
complexAction
}
})
Persistent State
import { defineStore } from 'pinia'
import { ref, watch } from 'vue'
export const usePreferencesStore = defineStore('preferences', () => {
// Load from localStorage if available
const theme = ref(localStorage.getItem('theme') || 'light')
const fontSize = ref(parseInt(localStorage.getItem('fontSize') || '14'))
// Save to localStorage when changed
watch(theme, (newTheme) => {
localStorage.setItem('theme', newTheme)
})
watch(fontSize, (newSize) => {
localStorage.setItem('fontSize', newSize.toString())
})
function setTheme(newTheme) {
theme.value = newTheme
}
return {
theme,
fontSize,
setTheme
}
})
Testing Stores
Stores should be tested to ensure they behave as expected. Here's an example of how to test a store:
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { api } from '@/scripts/api'
import { useExampleStore } from '@/stores/exampleStore'
// Mock API dependencies
vi.mock('@/scripts/api', () => ({
api: {
getData: vi.fn()
}
}))
describe('useExampleStore', () => {
let store: ReturnType<typeof useExampleStore>
beforeEach(() => {
// Create a fresh pinia instance and make it active
setActivePinia(createPinia())
store = useExampleStore()
// Clear all mocks
vi.clearAllMocks()
})
it('should initialize with default state', () => {
expect(store.items).toEqual([])
expect(store.isLoading).toBe(false)
expect(store.error).toBeNull()
})
it('should add an item', () => {
store.addItem('test')
expect(store.items).toEqual(['test'])
expect(store.itemCount).toBe(1)
})
it('should fetch items', async () => {
// Setup mock response
vi.mocked(api.getData).mockResolvedValue(['item1', 'item2'])
// Call the action
await store.fetchItems()
// Verify state changes
expect(store.isLoading).toBe(false)
expect(store.items).toEqual(['item1', 'item2'])
expect(store.error).toBeNull()
})
})
For more information on Pinia, refer to the Pinia documentation.