diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 9d3b7f1baa..39d4d09444 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -340,7 +340,8 @@
"conflicting": "Conflicting",
"inWorkflowSection": "IN WORKFLOW",
"allInWorkflow": "All in: {workflowName}",
- "missingNodes": "Missing Nodes"
+ "missingNodes": "Missing Nodes",
+ "unresolvedNodes": "Unresolved Nodes"
},
"infoPanelEmpty": "Click an item to see the info",
"applyChanges": "Apply Changes",
@@ -405,6 +406,10 @@
"noDescription": "No description available",
"installSelected": "Install Selected",
"installAllMissingNodes": "Install All",
+ "unresolvedNodes": {
+ "title": "Unresolved Missing Nodes",
+ "message": "The following nodes are not installed and could not be found in the registry."
+ },
"allMissingNodesInstalled": "All missing nodes have been successfully installed",
"packsSelected": "packs selected",
"mixedSelectionMessage": "Cannot perform bulk action on mixed selection",
diff --git a/src/workbench/extensions/manager/components/manager/ManagerDialog.vue b/src/workbench/extensions/manager/components/manager/ManagerDialog.vue
index 8bdf696a0b..f676818aab 100644
--- a/src/workbench/extensions/manager/components/manager/ManagerDialog.vue
+++ b/src/workbench/extensions/manager/components/manager/ManagerDialog.vue
@@ -128,6 +128,10 @@
+
(() => [
id: ManagerTab.Missing,
label: t('manager.nav.missingNodes'),
icon: 'icon-[lucide--triangle-alert]'
- }
+ },
+ ...(unresolvedNodeNames.value.length > 0
+ ? [
+ {
+ id: ManagerTab.Unresolved,
+ label: t('manager.nav.unresolvedNodes'),
+ icon: 'icon-[lucide--help-circle]',
+ badge: unresolvedNodeNames.value.length
+ }
+ ]
+ : [])
]
}
])
@@ -337,6 +353,12 @@ const selectedTab = computed(() =>
findNavItemById(navItems.value, selectedNavId.value)
)
+watch(navItems, (items) => {
+ if (selectedNavId.value && !findNavItemById(items, selectedNavId.value)) {
+ selectedNavId.value = ManagerTab.Missing
+ }
+})
+
const {
searchQuery,
pageNumber,
@@ -403,6 +425,9 @@ const isUpdateAvailableTab = computed(
const isMissingTab = computed(
() => selectedTab.value?.id === ManagerTab.Missing
)
+const isUnresolvedTab = computed(
+ () => selectedTab.value?.id === ManagerTab.Unresolved
+)
// Map of tab IDs to their empty state i18n key suffixes
const tabEmptyStateKeys: Partial> = {
diff --git a/src/workbench/extensions/manager/components/manager/UnresolvedNodesMessage.vue b/src/workbench/extensions/manager/components/manager/UnresolvedNodesMessage.vue
new file mode 100644
index 0000000000..e766eb5c88
--- /dev/null
+++ b/src/workbench/extensions/manager/components/manager/UnresolvedNodesMessage.vue
@@ -0,0 +1,26 @@
+
+
+
+
+ {{ $t('manager.unresolvedNodes.title') }}
+
+
+ {{ $t('manager.unresolvedNodes.message') }}
+
+
+
+
+
+
diff --git a/src/workbench/extensions/manager/composables/nodePack/useMissingNodes.test.ts b/src/workbench/extensions/manager/composables/nodePack/useMissingNodes.test.ts
index a5da7d5245..6a9d2b645a 100644
--- a/src/workbench/extensions/manager/composables/nodePack/useMissingNodes.test.ts
+++ b/src/workbench/extensions/manager/composables/nodePack/useMissingNodes.test.ts
@@ -94,6 +94,7 @@ describe('useMissingNodes', () => {
mockUseWorkflowPacks.mockReturnValue({
workflowPacks: ref([]),
+ unresolvedNodeNames: ref([]),
isLoading: ref(false),
error: ref(null),
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
@@ -119,6 +120,7 @@ describe('useMissingNodes', () => {
it('filters out installed packs correctly', () => {
mockUseWorkflowPacks.mockReturnValue({
workflowPacks: ref(mockWorkflowPacks),
+ unresolvedNodeNames: ref([]),
isLoading: ref(false),
error: ref(null),
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
@@ -140,6 +142,7 @@ describe('useMissingNodes', () => {
it('returns empty array when all packs are installed', () => {
mockUseWorkflowPacks.mockReturnValue({
workflowPacks: ref(mockWorkflowPacks),
+ unresolvedNodeNames: ref([]),
isLoading: ref(false),
error: ref(null),
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
@@ -158,6 +161,7 @@ describe('useMissingNodes', () => {
it('returns all packs when none are installed', () => {
mockUseWorkflowPacks.mockReturnValue({
workflowPacks: ref(mockWorkflowPacks),
+ unresolvedNodeNames: ref([]),
isLoading: ref(false),
error: ref(null),
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
@@ -191,6 +195,7 @@ describe('useMissingNodes', () => {
it('fetches even when packs already exist (watch always fires with immediate:true)', async () => {
mockUseWorkflowPacks.mockReturnValue({
workflowPacks: ref(mockWorkflowPacks),
+ unresolvedNodeNames: ref([]),
isLoading: ref(false),
error: ref(null),
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
@@ -206,6 +211,7 @@ describe('useMissingNodes', () => {
it('fetches even when already loading (watch fires regardless of loading state)', async () => {
mockUseWorkflowPacks.mockReturnValue({
workflowPacks: ref([]),
+ unresolvedNodeNames: ref([]),
isLoading: ref(true),
error: ref(null),
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
@@ -223,6 +229,7 @@ describe('useMissingNodes', () => {
it('exposes loading state from useWorkflowPacks', () => {
mockUseWorkflowPacks.mockReturnValue({
workflowPacks: ref([]),
+ unresolvedNodeNames: ref([]),
isLoading: ref(true),
error: ref(null),
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
@@ -239,6 +246,7 @@ describe('useMissingNodes', () => {
const testError = 'Failed to fetch workflow packs'
mockUseWorkflowPacks.mockReturnValue({
workflowPacks: ref([]),
+ unresolvedNodeNames: ref([]),
isLoading: ref(false),
error: ref(testError),
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
@@ -257,6 +265,7 @@ describe('useMissingNodes', () => {
const workflowPacksRef = ref([])
mockUseWorkflowPacks.mockReturnValue({
workflowPacks: workflowPacksRef,
+ unresolvedNodeNames: ref([]),
isLoading: ref(false),
error: ref(null),
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
@@ -283,6 +292,7 @@ describe('useMissingNodes', () => {
const workflowPacksRef = ref(mockWorkflowPacks)
mockUseWorkflowPacks.mockReturnValue({
workflowPacks: workflowPacksRef,
+ unresolvedNodeNames: ref([]),
isLoading: ref(false),
error: ref(null),
startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
@@ -306,6 +316,68 @@ describe('useMissingNodes', () => {
})
})
+ describe('unresolved nodes', () => {
+ it('reports hasMissingNodes when unresolvedNodeNames is non-empty', () => {
+ mockUseWorkflowPacks.mockReturnValue({
+ workflowPacks: ref([]),
+ unresolvedNodeNames: ref(['UnknownNode1', 'UnknownNode2']),
+ isLoading: ref(false),
+ error: ref(null),
+ startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
+ isReady: ref(true),
+ filterWorkflowPack: vi.fn()
+ })
+
+ const { hasMissingNodes, unresolvedNodeNames } = useMissingNodes()
+
+ expect(hasMissingNodes.value).toBe(true)
+ expect(unresolvedNodeNames.value).toEqual([
+ 'UnknownNode1',
+ 'UnknownNode2'
+ ])
+ })
+
+ it('does not report hasMissingNodes when unresolvedNodeNames is empty', () => {
+ mockUseWorkflowPacks.mockReturnValue({
+ workflowPacks: ref([]),
+ unresolvedNodeNames: ref([]),
+ isLoading: ref(false),
+ error: ref(null),
+ startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
+ isReady: ref(true),
+ filterWorkflowPack: vi.fn()
+ })
+
+ const { hasMissingNodes } = useMissingNodes()
+
+ expect(hasMissingNodes.value).toBe(false)
+ })
+
+ it('updates reactively when unresolvedNodeNames changes', async () => {
+ const unresolvedRef = ref([])
+ mockUseWorkflowPacks.mockReturnValue({
+ workflowPacks: ref([]),
+ unresolvedNodeNames: unresolvedRef,
+ isLoading: ref(false),
+ error: ref(null),
+ startFetchWorkflowPacks: mockStartFetchWorkflowPacks,
+ isReady: ref(true),
+ filterWorkflowPack: vi.fn()
+ })
+
+ const { hasMissingNodes, unresolvedNodeNames } = useMissingNodes()
+
+ expect(hasMissingNodes.value).toBe(false)
+ expect(unresolvedNodeNames.value).toEqual([])
+
+ unresolvedRef.value = ['NewMissingNode']
+ await nextTick()
+
+ expect(hasMissingNodes.value).toBe(true)
+ expect(unresolvedNodeNames.value).toEqual(['NewMissingNode'])
+ })
+ })
+
describe('missing core nodes detection', () => {
const createMockNode = (type: string, packId?: string, version?: string) =>
createMockLGraphNode({
diff --git a/src/workbench/extensions/manager/composables/nodePack/useMissingNodes.ts b/src/workbench/extensions/manager/composables/nodePack/useMissingNodes.ts
index 24daee3090..45760622dd 100644
--- a/src/workbench/extensions/manager/composables/nodePack/useMissingNodes.ts
+++ b/src/workbench/extensions/manager/composables/nodePack/useMissingNodes.ts
@@ -21,8 +21,13 @@ export const useMissingNodes = createSharedComposable(() => {
const nodeDefStore = useNodeDefStore()
const comfyManagerStore = useComfyManagerStore()
const workflowStore = useWorkflowStore()
- const { workflowPacks, isLoading, error, startFetchWorkflowPacks } =
- useWorkflowPacks()
+ const {
+ workflowPacks,
+ unresolvedNodeNames,
+ isLoading,
+ error,
+ startFetchWorkflowPacks
+ } = useWorkflowPacks()
const filterMissingPacks = (packs: components['schemas']['Node'][]) =>
packs.filter((pack) => !comfyManagerStore.isPackInstalled(pack.id))
@@ -67,7 +72,8 @@ export const useMissingNodes = createSharedComposable(() => {
const hasMissingNodes = computed(() => {
return (
missingNodePacks.value.length > 0 ||
- Object.keys(missingCoreNodes.value).length > 0
+ Object.keys(missingCoreNodes.value).length > 0 ||
+ unresolvedNodeNames.value.length > 0
)
})
@@ -83,6 +89,7 @@ export const useMissingNodes = createSharedComposable(() => {
return {
missingNodePacks,
missingCoreNodes,
+ unresolvedNodeNames,
hasMissingNodes,
isLoading,
error
diff --git a/src/workbench/extensions/manager/composables/nodePack/useWorkflowPacks.ts b/src/workbench/extensions/manager/composables/nodePack/useWorkflowPacks.ts
index 5fa3fcf385..0c45c2d493 100644
--- a/src/workbench/extensions/manager/composables/nodePack/useWorkflowPacks.ts
+++ b/src/workbench/extensions/manager/composables/nodePack/useWorkflowPacks.ts
@@ -31,6 +31,7 @@ const _useWorkflowPacks = () => {
const { inferPackFromNodeName } = useComfyRegistryStore()
const workflowPacks = ref([])
+ const unresolvedNodeNames = ref([])
const getWorkflowNodePackId = (node: LGraphNode): string | undefined => {
if (typeof node.properties?.cnr_id === 'string') {
@@ -111,12 +112,39 @@ const _useWorkflowPacks = () => {
/**
* Get the node packs for all nodes in the workflow (including subgraphs).
+ * Nodes that have no local definition and no registry match are tracked
+ * as unresolved so downstream consumers can surface them to the user.
*/
const getWorkflowPacks = async () => {
- if (!app.rootGraph) return []
- const packPromises = mapAllNodes(app.rootGraph, workflowNodeToPack)
- const packs = await Promise.all(packPromises)
- workflowPacks.value = packs.filter((pack) => pack !== undefined)
+ if (!app.rootGraph) {
+ workflowPacks.value = []
+ unresolvedNodeNames.value = []
+ return
+ }
+
+ const resolvedPacks: WorkflowPack[] = []
+ const unresolved: string[] = []
+
+ await Promise.all(
+ mapAllNodes(app.rootGraph, async (node) => {
+ const pack = await workflowNodeToPack(node)
+ if (pack) {
+ resolvedPacks.push(pack)
+ } else {
+ const nodeName = node.type
+ if (
+ nodeName &&
+ getWorkflowNodePackId(node) === undefined &&
+ !nodeDefStore.nodeDefsByName[nodeName]
+ ) {
+ unresolved.push(nodeName)
+ }
+ }
+ })
+ )
+
+ workflowPacks.value = resolvedPacks
+ unresolvedNodeNames.value = [...new Set(unresolved)]
}
const packsToUniqueIds = (packs: WorkflowPack[]) =>
@@ -147,6 +175,7 @@ const _useWorkflowPacks = () => {
isLoading,
isReady,
workflowPacks: nodePacks,
+ unresolvedNodeNames,
startFetchWorkflowPacks: async () => {
await getWorkflowPacks() // Parse the packs from the workflow nodes
await startFetch() // Fetch the packs infos from the registry
diff --git a/src/workbench/extensions/manager/composables/useManagerDisplayPacks.ts b/src/workbench/extensions/manager/composables/useManagerDisplayPacks.ts
index fe725ba1cc..bab85c33d3 100644
--- a/src/workbench/extensions/manager/composables/useManagerDisplayPacks.ts
+++ b/src/workbench/extensions/manager/composables/useManagerDisplayPacks.ts
@@ -176,6 +176,9 @@ export function useManagerDisplayPacks(
return sortPacks(filterNotInstalled(base))
}
+ case ManagerTab.Unresolved:
+ return []
+
default:
return searchResults.value
}
diff --git a/src/workbench/extensions/manager/types/comfyManagerTypes.ts b/src/workbench/extensions/manager/types/comfyManagerTypes.ts
index b009ec64d7..a9327b4572 100644
--- a/src/workbench/extensions/manager/types/comfyManagerTypes.ts
+++ b/src/workbench/extensions/manager/types/comfyManagerTypes.ts
@@ -19,7 +19,8 @@ export enum ManagerTab {
UpdateAvailable = 'updateAvailable',
Conflicting = 'conflicting',
Workflow = 'workflow',
- Missing = 'missing'
+ Missing = 'missing',
+ Unresolved = 'unresolved'
}
export type TaskLog = {