mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-03-13 17:10:06 +00:00
[feat] Surface missing models in Errors tab (Cloud) (#9743)
## Summary When a workflow is loaded with missing models, users currently have no way to identify or resolve them from within the UI. This PR adds a full missing-model detection and resolution pipeline that surfaces missing models in the Errors tab, allowing users to install or import them without leaving the editor. ## Changes ### Missing Model Detection - Scan all COMBO widgets across root graph and subgraphs for model-like filenames during workflow load - Enrich candidates with embedded workflow metadata (url, hash, directory) when available - Verify asset-supported candidates against the asset store asynchronously to confirm installation status - Propagate missing model state to `executionErrorStore` alongside existing node/prompt errors ### Errors Tab UI — Model Resolution - Group missing models by directory (e.g. `checkpoints`, `loras`, `vae`) with collapsible category cards - Each model row displays: - Model name with copy-to-clipboard button - Expandable list of referencing nodes with locate-on-canvas button - **Library selector**: Pick an alternative from the user's existing models to substitute the missing model with one click - **URL import**: Paste a Civitai or HuggingFace URL to import a model directly; debounced metadata fetch shows filename and file size before confirming; type-mismatch warnings (e.g. importing a LoRA into checkpoints directory) are surfaced with an "Import Anyway" option - **Upgrade prompt**: In cloud environment, free-tier subscribers are shown an upgrade modal when attempting URL import - Separate "Import Not Supported" section for custom-node models that cannot be auto-resolved - Status card with live download progress, completion, failure, and category-mismatch states ### Canvas Integration - Highlight nodes and widgets that reference missing models with error indicators - Propagate missing-model badges through subgraph containers so issues are visible at every graph level ### Code Cleanup - Simplify `surfacePendingWarnings` in workflowService, remove stale widget-detected model merging logic - Add `flattenWorkflowNodes` utility to workflowSchema for traversing nested subgraph structures - Extract `MissingModelUrlInput`, `MissingModelLibrarySelect`, `MissingModelStatusCard` as focused single-responsibility components ## Testing - Unit tests for scan pipeline (`missingModelScan.test.ts`): enrichment, skip-installed, subgraph flattening - Unit tests for store (`missingModelStore.test.ts`): state management, removal helpers - Unit tests for interactions (`useMissingModelInteractions.test.ts`): combo select, URL input, import flow, library confirm - Component tests for `MissingModelCard` and error grouping (`useErrorGroups.test.ts`) - Updated `workflowService.test.ts` and `workflowSchema.test.ts` for new logic ## Review Focus - Missing model scan + enrichment pipeline in `missingModelScan.ts` - Interaction composable `useMissingModelInteractions.ts` — URL metadata fetch, library install, upload fallback - Store integration and canvas-level error propagation ## Screenshots https://github.com/user-attachments/assets/339a6d5b-93a3-43cd-98dd-0fb00681b66f ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9743-feat-Surface-missing-models-in-Errors-tab-Cloud-3206d73d365081678326d3a16c2165d8) by [Unito](https://www.unito.io)
This commit is contained in:
@@ -547,15 +547,16 @@ export const useWorkflowService = () => {
|
||||
if (settingStore.get('Comfy.Workflow.ShowMissingNodesWarning')) {
|
||||
missingNodesDialog.show({ missingNodeTypes })
|
||||
}
|
||||
|
||||
executionErrorStore.surfaceMissingNodes(missingNodeTypes)
|
||||
}
|
||||
|
||||
if (
|
||||
missingModels &&
|
||||
settingStore.get('Comfy.Workflow.ShowMissingModelsWarning')
|
||||
) {
|
||||
missingModelsDialog.show(missingModels)
|
||||
// Missing models are NOT surfaced to the Errors tab here.
|
||||
// On Cloud, the dedicated pipeline in app.ts handles detection and
|
||||
// surfacing via surfaceMissingModels(). OSS uses only this dialog.
|
||||
if (missingModels) {
|
||||
if (settingStore.get('Comfy.Workflow.ShowMissingModelsWarning')) {
|
||||
missingModelsDialog.show(missingModels)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,9 +3,13 @@ import { describe, expect, it } from 'vitest'
|
||||
|
||||
import {
|
||||
buildSubgraphExecutionPaths,
|
||||
flattenWorkflowNodes,
|
||||
validateComfyWorkflow
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type { ComfyNode } from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import type {
|
||||
ComfyNode,
|
||||
ComfyWorkflowJSON
|
||||
} from '@/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { defaultGraph } from '@/scripts/defaultGraph'
|
||||
|
||||
const WORKFLOW_DIR = 'src/platform/workflow/validation/schemas/__fixtures__'
|
||||
@@ -274,3 +278,48 @@ describe('buildSubgraphExecutionPaths', () => {
|
||||
).not.toThrow()
|
||||
})
|
||||
})
|
||||
|
||||
describe('flattenWorkflowNodes', () => {
|
||||
it('returns root nodes when no subgraphs exist', () => {
|
||||
const result = flattenWorkflowNodes({
|
||||
nodes: [node(1, 'KSampler'), node(2, 'CLIPLoader')]
|
||||
} as ComfyWorkflowJSON)
|
||||
|
||||
expect(result).toHaveLength(2)
|
||||
expect(result.map((n) => n.id)).toEqual([1, 2])
|
||||
})
|
||||
|
||||
it('returns empty array when nodes is undefined', () => {
|
||||
const result = flattenWorkflowNodes({} as ComfyWorkflowJSON)
|
||||
expect(result).toEqual([])
|
||||
})
|
||||
|
||||
it('includes subgraph nodes with prefixed IDs', () => {
|
||||
const result = flattenWorkflowNodes({
|
||||
nodes: [node(5, 'def-A')],
|
||||
definitions: {
|
||||
subgraphs: [
|
||||
subgraphDef('def-A', [node(10, 'Inner'), node(20, 'Inner2')])
|
||||
]
|
||||
}
|
||||
} as unknown as ComfyWorkflowJSON)
|
||||
|
||||
expect(result).toHaveLength(3) // 1 root + 2 subgraph
|
||||
expect(result.map((n) => n.id)).toEqual([5, '5:10', '5:20'])
|
||||
})
|
||||
|
||||
it('prefixes nested subgraph nodes with full execution path', () => {
|
||||
const result = flattenWorkflowNodes({
|
||||
nodes: [node(5, 'def-A')],
|
||||
definitions: {
|
||||
subgraphs: [
|
||||
subgraphDef('def-A', [node(10, 'def-B')]),
|
||||
subgraphDef('def-B', [node(3, 'Leaf')])
|
||||
]
|
||||
}
|
||||
} as unknown as ComfyWorkflowJSON)
|
||||
|
||||
// root:5, def-A inner: 5:10, def-B inner: 5:10:3
|
||||
expect(result.map((n) => n.id)).toEqual([5, '5:10', '5:10:3'])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -592,3 +592,56 @@ export function buildSubgraphExecutionPaths(
|
||||
build(rootNodes, '')
|
||||
return pathMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Recursively collect all subgraph definitions from root and nested levels.
|
||||
*/
|
||||
function collectAllSubgraphDefs(rootDefs: unknown[]): SubgraphDefinition[] {
|
||||
const result: SubgraphDefinition[] = []
|
||||
const seen = new Set<string>()
|
||||
|
||||
function collect(defs: unknown[]) {
|
||||
for (const def of defs) {
|
||||
if (!isSubgraphDefinition(def)) continue
|
||||
if (seen.has(def.id)) continue
|
||||
seen.add(def.id)
|
||||
result.push(def)
|
||||
if (def.definitions?.subgraphs?.length) {
|
||||
collect(def.definitions.subgraphs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collect(rootDefs)
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Flatten all workflow nodes (root + subgraphs) into a single array.
|
||||
* Each node's `id` is prefixed with its execution path (e.g. node "3" inside container "11" → "11:3").
|
||||
*/
|
||||
export function flattenWorkflowNodes(
|
||||
graphData: ComfyWorkflowJSON
|
||||
): Readonly<ComfyNode>[] {
|
||||
const rootNodes = graphData.nodes ?? []
|
||||
const allDefs = collectAllSubgraphDefs(graphData.definitions?.subgraphs ?? [])
|
||||
const pathMap = buildSubgraphExecutionPaths(rootNodes, allDefs)
|
||||
|
||||
const allNodes: ComfyNode[] = [...rootNodes]
|
||||
|
||||
const subgraphDefMap = new Map(allDefs.map((s) => [s.id, s]))
|
||||
for (const [defId, paths] of pathMap.entries()) {
|
||||
const def = subgraphDefMap.get(defId)
|
||||
if (!def?.nodes) continue
|
||||
for (const prefix of paths) {
|
||||
for (const node of def.nodes) {
|
||||
allNodes.push({
|
||||
...node,
|
||||
id: `${prefix}:${node.id}`
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allNodes
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user