+
+
#{{ card.nodeId }}
@@ -19,7 +22,7 @@
variant="secondary"
size="sm"
class="rounded-lg text-sm shrink-0"
- @click.stop="emit('enterSubgraph', card.nodeId ?? '')"
+ @click.stop="handleEnterSubgraph"
>
{{ t('rightSidePanel.enterSubgraph') }}
@@ -27,7 +30,8 @@
variant="textonly"
size="icon-sm"
class="size-7 text-muted-foreground hover:text-base-foreground shrink-0"
- @click.stop="emit('locateNode', card.nodeId ?? '')"
+ :aria-label="t('rightSidePanel.locateNode')"
+ @click.stop="handleLocateNode"
>
@@ -43,7 +47,7 @@
>
{{ error.message }}
@@ -69,7 +73,7 @@
@@ -97,16 +98,16 @@
diff --git a/src/components/rightSidePanel/errors/__stories__/MockCloudMissingModel.vue b/src/components/rightSidePanel/errors/__stories__/MockCloudMissingModel.vue
new file mode 100644
index 00000000000..efe4d7117b8
--- /dev/null
+++ b/src/components/rightSidePanel/errors/__stories__/MockCloudMissingModel.vue
@@ -0,0 +1,521 @@
+
+
+
+
+
+
+
+
+ Workflow Overview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Search for nodes or inputs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/rightSidePanel/errors/__stories__/MockCloudMissingModelBasic.vue b/src/components/rightSidePanel/errors/__stories__/MockCloudMissingModelBasic.vue
new file mode 100644
index 00000000000..58e9d8efdfc
--- /dev/null
+++ b/src/components/rightSidePanel/errors/__stories__/MockCloudMissingModelBasic.vue
@@ -0,0 +1,472 @@
+
+
+
+
+
+
+
+
+ Workflow Overview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Search for nodes or inputs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/rightSidePanel/errors/__stories__/MockCloudMissingNodePack.vue b/src/components/rightSidePanel/errors/__stories__/MockCloudMissingNodePack.vue
new file mode 100644
index 00000000000..214050bde36
--- /dev/null
+++ b/src/components/rightSidePanel/errors/__stories__/MockCloudMissingNodePack.vue
@@ -0,0 +1,206 @@
+
+
+
+
+
+
+
+
+ Workflow Overview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Search for nodes or inputs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Unsupported Node Packs
+
+
+
+
+
+
+
+
+ This workflow requires custom nodes not yet available on Comfy Cloud.
+
+
+
+
+
+
+
+
+
+
+ {{ pack.displayName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/rightSidePanel/errors/__stories__/MockManagerDialog.vue b/src/components/rightSidePanel/errors/__stories__/MockManagerDialog.vue
new file mode 100644
index 00000000000..7ef5a48cca6
--- /dev/null
+++ b/src/components/rightSidePanel/errors/__stories__/MockManagerDialog.vue
@@ -0,0 +1,288 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Node Pack
+
+
+
+
+
+ Search
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ i === 0 && selectedPack ? selectedPack.packId : 'node-pack-' + (i + 1) }}
+
+
by publisher
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/rightSidePanel/errors/__stories__/MockOSSMissingModel.vue b/src/components/rightSidePanel/errors/__stories__/MockOSSMissingModel.vue
new file mode 100644
index 00000000000..6fc33ab73a3
--- /dev/null
+++ b/src/components/rightSidePanel/errors/__stories__/MockOSSMissingModel.vue
@@ -0,0 +1,405 @@
+
+
+
+
+
+
+
+
+ Workflow Overview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Search for nodes or inputs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/rightSidePanel/errors/__stories__/MockOSSMissingNodePack.vue b/src/components/rightSidePanel/errors/__stories__/MockOSSMissingNodePack.vue
new file mode 100644
index 00000000000..0a5868b4881
--- /dev/null
+++ b/src/components/rightSidePanel/errors/__stories__/MockOSSMissingNodePack.vue
@@ -0,0 +1,312 @@
+
+
+
+
+
+
+
+
+ Workflow Overview
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Search for nodes or inputs
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Missing Node Packs
+
+ Install All
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ pack.displayName }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ getInstallState(pack.id) === 'installing' ? 'Installing...' : 'Install node pack' }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/rightSidePanel/errors/__stories__/StoryOSSMissingNodePackFlow.vue b/src/components/rightSidePanel/errors/__stories__/StoryOSSMissingNodePackFlow.vue
new file mode 100644
index 00000000000..5f8d4a3112d
--- /dev/null
+++ b/src/components/rightSidePanel/errors/__stories__/StoryOSSMissingNodePackFlow.vue
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+
ComfyUI Canvas
+
+
+
{{ statusLog }}
+
+ Click the buttons in the right-side error tab
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/rightSidePanel/errors/useErrorGroups.ts b/src/components/rightSidePanel/errors/useErrorGroups.ts
index 9c83829af65..4a239040175 100644
--- a/src/components/rightSidePanel/errors/useErrorGroups.ts
+++ b/src/components/rightSidePanel/errors/useErrorGroups.ts
@@ -1,15 +1,28 @@
-import { computed } from 'vue'
+import { computed, reactive, watch } from 'vue'
import type { Ref } from 'vue'
import Fuse from 'fuse.js'
import type { IFuseOptions } from 'fuse.js'
import { useExecutionStore } from '@/stores/executionStore'
+import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
+import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { app } from '@/scripts/app'
+import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
+import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
import { getNodeByExecutionId } from '@/utils/graphTraversalUtil'
import { resolveNodeDisplayName } from '@/utils/nodeTitleUtil'
+import { isLGraphNode } from '@/utils/litegraphUtil'
+import { isGroupNode } from '@/utils/executableGroupNodeDto'
import { st } from '@/i18n'
-import type { ErrorCardData, ErrorGroup } from './types'
-import { isNodeExecutionId } from '@/types/nodeIdentification'
+import type { ErrorCardData, ErrorGroup, ErrorItem } from './types'
+import {
+ isNodeExecutionId,
+ parseNodeExecutionId
+} from '@/types/nodeIdentification'
+
+const PROMPT_CARD_ID = '__prompt__'
+const SINGLE_GROUP_KEY = '__single__'
+const KNOWN_PROMPT_ERROR_TYPES = new Set(['prompt_no_outputs', 'no_prompt'])
interface GroupEntry {
priority: number
@@ -25,20 +38,30 @@ interface ErrorSearchItem {
searchableDetails: string
}
-const KNOWN_PROMPT_ERROR_TYPES = new Set(['prompt_no_outputs', 'no_prompt'])
-
-function resolveNodeInfo(nodeId: string): {
- title: string
- graphNodeId: string | undefined
-} {
+/**
+ * Resolve display info for a node by its execution ID.
+ * For group node internals, resolves the parent group node's title instead.
+ */
+function resolveNodeInfo(nodeId: string) {
const graphNode = getNodeByExecutionId(app.rootGraph, nodeId)
+
+ const parts = parseNodeExecutionId(nodeId)
+ const parentId = parts && parts.length > 1 ? String(parts[0]) : null
+ const parentNode = parentId
+ ? app.rootGraph.getNodeById(Number(parentId))
+ : null
+ const isParentGroupNode = parentNode ? isGroupNode(parentNode) : false
+
return {
- title: resolveNodeDisplayName(graphNode, {
- emptyLabel: '',
- untitledLabel: '',
- st
- }),
- graphNodeId: graphNode ? String(graphNode.id) : undefined
+ title: isParentGroupNode
+ ? parentNode?.title || ''
+ : resolveNodeDisplayName(graphNode, {
+ emptyLabel: '',
+ untitledLabel: '',
+ st
+ }),
+ graphNodeId: graphNode ? String(graphNode.id) : undefined,
+ isParentGroupNode
}
}
@@ -55,93 +78,56 @@ function getOrCreateGroup(
return entry.cards
}
-function processPromptError(
- groupsMap: Map
,
- executionStore: ReturnType,
- t: (key: string) => string
-) {
- if (!executionStore.lastPromptError) return
-
- const error = executionStore.lastPromptError
- const groupTitle = error.message
- const cards = getOrCreateGroup(groupsMap, groupTitle, 0)
- const isKnown = KNOWN_PROMPT_ERROR_TYPES.has(error.type)
-
- cards.set('__prompt__', {
- id: '__prompt__',
- title: groupTitle,
- errors: [
- {
- message: isKnown
- ? t(`rightSidePanel.promptErrors.${error.type}.desc`)
- : error.message
- }
- ]
- })
+function createErrorCard(
+ nodeId: string,
+ classType: string,
+ idPrefix: string
+): ErrorCardData {
+ const nodeInfo = resolveNodeInfo(nodeId)
+ return {
+ id: `${idPrefix}-${nodeId}`,
+ title: classType,
+ nodeId,
+ nodeTitle: nodeInfo.title,
+ graphNodeId: nodeInfo.graphNodeId,
+ isSubgraphNode: isNodeExecutionId(nodeId) && !nodeInfo.isParentGroupNode,
+ errors: []
+ }
}
-function processNodeErrors(
- groupsMap: Map,
- executionStore: ReturnType
-) {
- if (!executionStore.lastNodeErrors) return
+/**
+ * In single-node mode, regroup cards by error message instead of class_type.
+ * This lets the user see "what kinds of errors this node has" at a glance.
+ */
+function regroupByErrorMessage(
+ groupsMap: Map
+): Map {
+ const allCards = Array.from(groupsMap.values()).flatMap((g) =>
+ Array.from(g.cards.values())
+ )
- for (const [nodeId, nodeError] of Object.entries(
- executionStore.lastNodeErrors
- )) {
- const cards = getOrCreateGroup(groupsMap, nodeError.class_type, 1)
- if (!cards.has(nodeId)) {
- const nodeInfo = resolveNodeInfo(nodeId)
- cards.set(nodeId, {
- id: `node-${nodeId}`,
- title: nodeError.class_type,
- nodeId,
- nodeTitle: nodeInfo.title,
- graphNodeId: nodeInfo.graphNodeId,
- isSubgraphNode: isNodeExecutionId(nodeId),
- errors: []
- })
- }
- const card = cards.get(nodeId)
- if (!card) continue
- card.errors.push(
- ...nodeError.errors.map((e) => ({
- message: e.message,
- details: e.details ?? undefined
- }))
- )
+ const cardErrorPairs = allCards.flatMap((card) =>
+ card.errors.map((error) => ({ card, error }))
+ )
+
+ const messageMap = new Map()
+ for (const { card, error } of cardErrorPairs) {
+ addCardErrorToGroup(messageMap, card, error)
}
+
+ return messageMap
}
-function processExecutionError(
- groupsMap: Map,
- executionStore: ReturnType
+function addCardErrorToGroup(
+ messageMap: Map,
+ card: ErrorCardData,
+ error: ErrorItem
) {
- if (!executionStore.lastExecutionError) return
-
- const e = executionStore.lastExecutionError
- const nodeId = String(e.node_id)
- const cards = getOrCreateGroup(groupsMap, e.node_type, 1)
-
- if (!cards.has(nodeId)) {
- const nodeInfo = resolveNodeInfo(nodeId)
- cards.set(nodeId, {
- id: `exec-${nodeId}`,
- title: e.node_type,
- nodeId,
- nodeTitle: nodeInfo.title,
- graphNodeId: nodeInfo.graphNodeId,
- isSubgraphNode: isNodeExecutionId(nodeId),
- errors: []
- })
+ const group = getOrCreateGroup(messageMap, error.message, 1)
+ if (!group.has(card.id)) {
+ group.set(card.id, { ...card, errors: [] })
}
- const card = cards.get(nodeId)
- if (!card) return
- card.errors.push({
- message: `${e.exception_type}: ${e.exception_message}`,
- details: e.traceback.join('\n'),
- isRuntimeError: true
- })
+ group.get(card.id)?.errors.push(error)
}
function toSortedGroups(groupsMap: Map): ErrorGroup[] {
@@ -157,27 +143,14 @@ function toSortedGroups(groupsMap: Map): ErrorGroup[] {
})
}
-function buildErrorGroups(
- executionStore: ReturnType,
- t: (key: string) => string
-): ErrorGroup[] {
- const groupsMap = new Map()
-
- processPromptError(groupsMap, executionStore, t)
- processNodeErrors(groupsMap, executionStore)
- processExecutionError(groupsMap, executionStore)
-
- return toSortedGroups(groupsMap)
-}
-
-function searchErrorGroups(groups: ErrorGroup[], query: string): ErrorGroup[] {
+function searchErrorGroups(groups: ErrorGroup[], query: string) {
if (!query) return groups
const searchableList: ErrorSearchItem[] = []
for (let gi = 0; gi < groups.length; gi++) {
- const group = groups[gi]!
+ const group = groups[gi]
for (let ci = 0; ci < group.cards.length; ci++) {
- const card = group.cards[ci]!
+ const card = group.cards[ci]
searchableList.push({
groupIndex: gi,
cardIndex: ci,
@@ -219,18 +192,214 @@ export function useErrorGroups(
t: (key: string) => string
) {
const executionStore = useExecutionStore()
+ const canvasStore = useCanvasStore()
+ const rightSidePanelStore = useRightSidePanelStore()
+ const collapseState = reactive>({})
- const errorGroups = computed(() =>
- buildErrorGroups(executionStore, t)
+ const selectedNodeInfo = computed(() => {
+ const items = canvasStore.selectedItems
+ const nodeIds = new Set()
+ const containerIds = new Set()
+
+ for (const item of items) {
+ if (!isLGraphNode(item)) continue
+ nodeIds.add(String(item.id))
+ if (item instanceof SubgraphNode || isGroupNode(item)) {
+ containerIds.add(String(item.id))
+ }
+ }
+
+ return {
+ nodeIds: nodeIds.size > 0 ? nodeIds : null,
+ containerIds
+ }
+ })
+
+ const isSingleNodeSelected = computed(
+ () =>
+ selectedNodeInfo.value.nodeIds?.size === 1 &&
+ selectedNodeInfo.value.containerIds.size === 0
)
+ const errorNodeCache = computed(() => {
+ const map = new Map()
+ for (const execId of executionStore.allErrorExecutionIds) {
+ const node = getNodeByExecutionId(app.rootGraph, execId)
+ if (node) map.set(execId, node)
+ }
+ return map
+ })
+
+ function isErrorInSelection(executionNodeId: string): boolean {
+ const nodeIds = selectedNodeInfo.value.nodeIds
+ if (!nodeIds) return true
+
+ const graphNode = errorNodeCache.value.get(executionNodeId)
+ if (graphNode && nodeIds.has(String(graphNode.id))) return true
+
+ for (const containerId of selectedNodeInfo.value.containerIds) {
+ if (executionNodeId.startsWith(`${containerId}:`)) return true
+ }
+
+ return false
+ }
+
+ function addNodeErrorToGroup(
+ groupsMap: Map,
+ nodeId: string,
+ classType: string,
+ idPrefix: string,
+ errors: ErrorItem[],
+ filterBySelection = false
+ ) {
+ if (filterBySelection && !isErrorInSelection(nodeId)) return
+ const groupKey = isSingleNodeSelected.value ? SINGLE_GROUP_KEY : classType
+ const cards = getOrCreateGroup(groupsMap, groupKey, 1)
+ if (!cards.has(nodeId)) {
+ cards.set(nodeId, createErrorCard(nodeId, classType, idPrefix))
+ }
+ cards.get(nodeId)?.errors.push(...errors)
+ }
+
+ function processPromptError(groupsMap: Map) {
+ if (selectedNodeInfo.value.nodeIds || !executionStore.lastPromptError)
+ return
+
+ const error = executionStore.lastPromptError
+ const groupTitle = error.message
+ const cards = getOrCreateGroup(groupsMap, groupTitle, 0)
+ const isKnown = KNOWN_PROMPT_ERROR_TYPES.has(error.type)
+
+ // Prompt errors are not tied to a node, so they bypass addNodeErrorToGroup.
+ cards.set(PROMPT_CARD_ID, {
+ id: PROMPT_CARD_ID,
+ title: groupTitle,
+ errors: [
+ {
+ message: isKnown
+ ? t(`rightSidePanel.promptErrors.${error.type}.desc`)
+ : error.message
+ }
+ ]
+ })
+ }
+
+ function processNodeErrors(
+ groupsMap: Map,
+ filterBySelection = false
+ ) {
+ if (!executionStore.lastNodeErrors) return
+
+ for (const [nodeId, nodeError] of Object.entries(
+ executionStore.lastNodeErrors
+ )) {
+ addNodeErrorToGroup(
+ groupsMap,
+ nodeId,
+ nodeError.class_type,
+ 'node',
+ nodeError.errors.map((e) => ({
+ message: e.message,
+ details: e.details ?? undefined
+ })),
+ filterBySelection
+ )
+ }
+ }
+
+ function processExecutionError(
+ groupsMap: Map,
+ filterBySelection = false
+ ) {
+ if (!executionStore.lastExecutionError) return
+
+ const e = executionStore.lastExecutionError
+ addNodeErrorToGroup(
+ groupsMap,
+ String(e.node_id),
+ e.node_type,
+ 'exec',
+ [
+ {
+ message: `${e.exception_type}: ${e.exception_message}`,
+ details: e.traceback.join('\n'),
+ isRuntimeError: true
+ }
+ ],
+ filterBySelection
+ )
+ }
+
+ const allErrorGroups = computed(() => {
+ const groupsMap = new Map()
+
+ processPromptError(groupsMap)
+ processNodeErrors(groupsMap)
+ processExecutionError(groupsMap)
+
+ return toSortedGroups(groupsMap)
+ })
+
+ const tabErrorGroups = computed(() => {
+ const groupsMap = new Map()
+
+ processPromptError(groupsMap)
+ processNodeErrors(groupsMap, true)
+ processExecutionError(groupsMap, true)
+
+ return isSingleNodeSelected.value
+ ? toSortedGroups(regroupByErrorMessage(groupsMap))
+ : toSortedGroups(groupsMap)
+ })
+
const filteredGroups = computed(() => {
const query = searchQuery.value.trim()
- return searchErrorGroups(errorGroups.value, query)
+ return searchErrorGroups(tabErrorGroups.value, query)
+ })
+
+ const groupedErrorMessages = computed(() => {
+ const messages = new Set()
+ for (const group of allErrorGroups.value) {
+ for (const card of group.cards) {
+ for (const err of card.errors) {
+ messages.add(err.message)
+ }
+ }
+ }
+ return Array.from(messages)
+ })
+
+ /**
+ * When an external trigger (e.g. "See Error" button in SectionWidgets)
+ * sets focusedErrorNodeId, expand only the group containing the target
+ * node and collapse all others so the user sees the relevant errors
+ * immediately.
+ */
+ function expandFocusedErrorGroup(graphNodeId: string | null) {
+ if (!graphNodeId) return
+ const prefix = `${graphNodeId}:`
+ for (const group of allErrorGroups.value) {
+ const hasMatch = group.cards.some(
+ (card) =>
+ card.graphNodeId === graphNodeId ||
+ (card.nodeId?.startsWith(prefix) ?? false)
+ )
+ collapseState[group.title] = !hasMatch
+ }
+ rightSidePanelStore.focusedErrorNodeId = null
+ }
+
+ watch(() => rightSidePanelStore.focusedErrorNodeId, expandFocusedErrorGroup, {
+ immediate: true
})
return {
- errorGroups,
- filteredGroups
+ allErrorGroups,
+ tabErrorGroups,
+ filteredGroups,
+ collapseState,
+ isSingleNodeSelected,
+ errorNodeCache,
+ groupedErrorMessages
}
}
diff --git a/src/components/rightSidePanel/parameters/SectionWidgets.vue b/src/components/rightSidePanel/parameters/SectionWidgets.vue
index 5e2e77ecd1a..8f865c87c86 100644
--- a/src/components/rightSidePanel/parameters/SectionWidgets.vue
+++ b/src/components/rightSidePanel/parameters/SectionWidgets.vue
@@ -5,17 +5,15 @@ import { useI18n } from 'vue-i18n'
import Button from '@/components/ui/button/Button.vue'
import { isProxyWidget } from '@/core/graph/subgraph/proxyWidget'
import { parseProxyWidgets } from '@/core/schemas/proxyWidget'
-import type {
- LGraphGroup,
- LGraphNode,
- SubgraphNode
-} from '@/lib/litegraph/src/litegraph'
+import type { LGraphGroup, LGraphNode } from '@/lib/litegraph/src/litegraph'
+import { SubgraphNode } from '@/lib/litegraph/src/litegraph'
import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { useExecutionStore } from '@/stores/executionStore'
import { useRightSidePanelStore } from '@/stores/workspace/rightSidePanelStore'
import { useSettingStore } from '@/platform/settings/settingStore'
import { cn } from '@/utils/tailwindUtil'
+import { isGroupNode } from '@/utils/executableGroupNodeDto'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import { getWidgetDefaultValue } from '@/utils/widgetUtil'
import type { WidgetValue } from '@/utils/widgetUtil'
@@ -110,11 +108,26 @@ const targetNode = computed(() => {
return allSameNode ? widgets.value[0].node : null
})
-const nodeHasError = computed(() => {
- if (canvasStore.selectedItems.length > 0 || !targetNode.value) return false
+const hasDirectError = computed(() => {
+ if (!targetNode.value) return false
return executionStore.activeGraphErrorNodeIds.has(String(targetNode.value.id))
})
+const hasContainerInternalError = computed(() => {
+ if (!targetNode.value) return false
+ const isContainer =
+ targetNode.value instanceof SubgraphNode || isGroupNode(targetNode.value)
+ if (!isContainer) return false
+
+ return executionStore.hasInternalErrorForNode(targetNode.value.id)
+})
+
+const nodeHasError = computed(() => {
+ if (!targetNode.value) return false
+ if (canvasStore.selectedItems.length === 1) return false
+ return hasDirectError.value || hasContainerInternalError.value
+})
+
const parentGroup = computed(() => {
if (!targetNode.value || !getNodeParentGroup) return null
return getNodeParentGroup(targetNode.value)
diff --git a/src/composables/canvas/useFocusNode.ts b/src/composables/canvas/useFocusNode.ts
index 9dd329dadef..7f3ccecd975 100644
--- a/src/composables/canvas/useFocusNode.ts
+++ b/src/composables/canvas/useFocusNode.ts
@@ -2,9 +2,15 @@ import { nextTick } from 'vue'
import { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
import { app } from '@/scripts/app'
-import type { LGraph, Subgraph } from '@/lib/litegraph/src/litegraph'
+import type {
+ LGraph,
+ LGraphNode,
+ Subgraph
+} from '@/lib/litegraph/src/litegraph'
import { getNodeByExecutionId } from '@/utils/graphTraversalUtil'
+import { isGroupNode } from '@/utils/executableGroupNodeDto'
import { useLitegraphService } from '@/services/litegraphService'
+import { parseNodeExecutionId } from '@/types/nodeIdentification'
async function navigateToGraph(targetGraph: LGraph) {
const canvasStore = useCanvasStore()
@@ -29,20 +35,44 @@ async function navigateToGraph(targetGraph: LGraph) {
export function useFocusNode() {
const canvasStore = useCanvasStore()
- async function focusNode(nodeId: string) {
+ /* Locate and focus a node on the canvas by its execution ID. */
+ async function focusNode(
+ nodeId: string,
+ executionIdMap?: Map
+ ) {
if (!canvasStore.canvas) return
- const graphNode = getNodeByExecutionId(app.rootGraph, nodeId)
+ // For group node internals, locate the parent group node instead
+ const parts = parseNodeExecutionId(nodeId)
+ const parentId = parts && parts.length > 1 ? String(parts[0]) : null
+ const parentNode = parentId
+ ? app.rootGraph.getNodeById(Number(parentId))
+ : null
+
+ if (parentNode && isGroupNode(parentNode) && parentNode.graph) {
+ await navigateToGraph(parentNode.graph as LGraph)
+ canvasStore.canvas?.animateToBounds(parentNode.boundingRect)
+ return
+ }
+
+ const graphNode = executionIdMap
+ ? executionIdMap.get(nodeId)
+ : getNodeByExecutionId(app.rootGraph, nodeId)
if (!graphNode?.graph) return
await navigateToGraph(graphNode.graph as LGraph)
canvasStore.canvas?.animateToBounds(graphNode.boundingRect)
}
- async function enterSubgraph(nodeId: string) {
+ async function enterSubgraph(
+ nodeId: string,
+ executionIdMap?: Map
+ ) {
if (!canvasStore.canvas) return
- const graphNode = getNodeByExecutionId(app.rootGraph, nodeId)
+ const graphNode = executionIdMap
+ ? executionIdMap.get(nodeId)
+ : getNodeByExecutionId(app.rootGraph, nodeId)
if (!graphNode?.graph) return
await navigateToGraph(graphNode.graph as LGraph)
diff --git a/src/locales/en/main.json b/src/locales/en/main.json
index 2988bc412a4..a4c7b2b6e6e 100644
--- a/src/locales/en/main.json
+++ b/src/locales/en/main.json
@@ -1361,6 +1361,7 @@
"Execution": "Execution",
"PLY": "PLY",
"Workspace": "Workspace",
+ "Error System": "Error System",
"Other": "Other",
"Secrets": "Secrets",
"Error System": "Error System"
@@ -2986,6 +2987,7 @@
"hideAdvancedInputsButton": "Hide advanced inputs",
"errors": "Errors",
"noErrors": "No errors",
+ "executionErrorOccurred": "An error occurred during execution. Check the Errors tab for details.",
"enterSubgraph": "Enter subgraph",
"seeError": "See Error",
"promptErrors": {
@@ -2999,9 +3001,14 @@
"errorHelp": "For more help, {github} or {support}",
"errorHelpGithub": "submit a GitHub issue",
"errorHelpSupport": "contact our support",
+ "contactSupportFailed": "Unable to open contact support. Please try again later.",
"resetToDefault": "Reset to default",
"resetAllParameters": "Reset all parameters"
},
+ "errorOverlay": {
+ "errorCount": "{count} ERRORS | {count} ERROR | {count} ERRORS",
+ "seeErrors": "See Errors"
+ },
"help": {
"recentReleases": "Recent releases",
"helpCenterMenu": "Help Center Menu"
diff --git a/src/renderer/extensions/vueNodes/components/LGraphNode.vue b/src/renderer/extensions/vueNodes/components/LGraphNode.vue
index f106888a928..3fdb423c532 100644
--- a/src/renderer/extensions/vueNodes/components/LGraphNode.vue
+++ b/src/renderer/extensions/vueNodes/components/LGraphNode.vue
@@ -134,11 +134,8 @@
as-child
>