From 2f7f3c4e56e7ca5b95b041ed2a387950c9718901 Mon Sep 17 00:00:00 2001 From: jaeone94 <89377375+jaeone94@users.noreply.github.com> Date: Thu, 12 Mar 2026 16:21:54 +0900 Subject: [PATCH] [feat] Surface missing models in Errors tab (Cloud) (#9743) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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) --- .../rightSidePanel/RightSidePanel.vue | 21 +- .../rightSidePanel/errors/TabErrors.vue | 32 +- src/components/rightSidePanel/errors/types.ts | 1 + .../errors/useErrorGroups.test.ts | 118 ++ .../rightSidePanel/errors/useErrorGroups.ts | 77 +- src/components/ui/select/SelectContent.vue | 1 + src/locales/en/main.json | 28 + .../components/MissingModelCard.test.ts | 193 ++++ .../components/MissingModelCard.vue | 78 ++ .../components/MissingModelLibrarySelect.vue | 113 ++ .../components/MissingModelRow.vue | 224 ++++ .../components/MissingModelStatusCard.vue | 108 ++ .../components/MissingModelUrlInput.vue | 156 +++ .../useMissingModelInteractions.test.ts | 516 +++++++++ .../useMissingModelInteractions.ts | 393 +++++++ .../missingModel/missingModelScan.test.ts | 1019 +++++++++++++++++ src/platform/missingModel/missingModelScan.ts | 386 +++++++ .../missingModel/missingModelStore.test.ts | 189 +++ .../missingModel/missingModelStore.ts | 201 ++++ src/platform/missingModel/types.ts | 53 + .../workflow/core/services/workflowService.ts | 13 +- .../validation/schemas/workflowSchema.test.ts | 51 +- .../validation/schemas/workflowSchema.ts | 53 + .../vueNodes/components/LGraphNode.vue | 6 +- .../vueNodes/components/NodeWidgets.vue | 9 +- src/scripts/app.ts | 202 ++-- src/stores/assetsStore.ts | 13 +- src/stores/executionErrorStore.test.ts | 7 + src/stores/executionErrorStore.ts | 63 +- src/utils/graphTraversalUtil.ts | 24 + 30 files changed, 4219 insertions(+), 129 deletions(-) create mode 100644 src/platform/missingModel/components/MissingModelCard.test.ts create mode 100644 src/platform/missingModel/components/MissingModelCard.vue create mode 100644 src/platform/missingModel/components/MissingModelLibrarySelect.vue create mode 100644 src/platform/missingModel/components/MissingModelRow.vue create mode 100644 src/platform/missingModel/components/MissingModelStatusCard.vue create mode 100644 src/platform/missingModel/components/MissingModelUrlInput.vue create mode 100644 src/platform/missingModel/composables/useMissingModelInteractions.test.ts create mode 100644 src/platform/missingModel/composables/useMissingModelInteractions.ts create mode 100644 src/platform/missingModel/missingModelScan.test.ts create mode 100644 src/platform/missingModel/missingModelScan.ts create mode 100644 src/platform/missingModel/missingModelStore.test.ts create mode 100644 src/platform/missingModel/missingModelStore.ts create mode 100644 src/platform/missingModel/types.ts diff --git a/src/components/rightSidePanel/RightSidePanel.vue b/src/components/rightSidePanel/RightSidePanel.vue index 6f0697932c..38a9c663e9 100644 --- a/src/components/rightSidePanel/RightSidePanel.vue +++ b/src/components/rightSidePanel/RightSidePanel.vue @@ -13,6 +13,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 { useMissingModelStore } from '@/platform/missingModel/missingModelStore' import { useExecutionErrorStore } from '@/stores/executionErrorStore' import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore' import type { RightSidePanelTab } from '@/stores/workspace/rightSidePanelStore' @@ -36,6 +37,7 @@ import TabErrors from './errors/TabErrors.vue' const canvasStore = useCanvasStore() const executionErrorStore = useExecutionErrorStore() +const missingModelStore = useMissingModelStore() const rightSidePanelStore = useRightSidePanelStore() const settingStore = useSettingStore() const { t } = useI18n() @@ -43,6 +45,8 @@ const { t } = useI18n() const { hasAnyError, allErrorExecutionIds, activeMissingNodeGraphIds } = storeToRefs(executionErrorStore) +const { activeMissingModelGraphIds } = storeToRefs(missingModelStore) + const { findParentGroup } = useGraphHierarchy() const { selectedItems: directlySelectedItems } = storeToRefs(canvasStore) @@ -118,12 +122,21 @@ const hasMissingNodeSelected = computed( ) ) +const hasMissingModelSelected = computed( + () => + hasSelection.value && + selectedNodes.value.some((node) => + activeMissingModelGraphIds.value.has(String(node.id)) + ) +) + const hasRelevantErrors = computed(() => { if (!hasSelection.value) return hasAnyError.value return ( hasDirectNodeError.value || hasContainerInternalError.value || - hasMissingNodeSelected.value + hasMissingNodeSelected.value || + hasMissingModelSelected.value ) }) @@ -314,7 +327,11 @@ function handleTitleCancel() { :value="tab.value" > {{ tab.label() }} - +