From 7f0472fde47521b93d85419713416142305921cd Mon Sep 17 00:00:00 2001 From: AustinMroz Date: Mon, 9 Mar 2026 11:36:33 -0700 Subject: [PATCH] Always use interior nodeId for app mode (#9669) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit App mode stores the state of selected widgets as a tuple of `[NodeId, WidgetName]`. With recent subgraph changes, for a given node, `widget.name` will no longer uniquely resolve to a single widget. - From both Vue and Litegraph, selecting an input for display in App mode will now resolve the NodeId of the node which owns the widget instead of the selected node. - When displaying selections in litegraph, if the NodeId does not exist in the current graph, instead of resolving the actual node the rootGraph is searched for any subgraphNode which contains a view matching the `[NodeId, WidgetName]` pair. - When displaying widgets in App mode, the widget is always set as being a view of the real widget (This means that they will not display a purple promotion border. Known Issue: - These same subgraph changes made it so that a widget can be linked without being disabled. This PR makes it so widgets which have been linked instead display normally under the assumption that they are incorrectly marked as disabled. As disabled widgets can not be selected as inputs, this should handle normal usage fine, but a better solution is being investigated ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-9669-Always-use-interior-nodeId-for-app-mode-31e6d73d365081f8a918d0e43cb659ee) by [Unito](https://www.unito.io) --- src/components/builder/AppBuilder.vue | 46 ++++++++++++++----- .../extensions/linearMode/LinearControls.vue | 6 ++- 2 files changed, 39 insertions(+), 13 deletions(-) diff --git a/src/components/builder/AppBuilder.vue b/src/components/builder/AppBuilder.vue index fdc5266fcf..ef3adf946d 100644 --- a/src/components/builder/AppBuilder.vue +++ b/src/components/builder/AppBuilder.vue @@ -8,6 +8,7 @@ import DraggableList from '@/components/common/DraggableList.vue' import IoItem from '@/components/builder/IoItem.vue' import PropertiesAccordionItem from '@/components/rightSidePanel/layout/PropertiesAccordionItem.vue' import WidgetItem from '@/components/rightSidePanel/parameters/WidgetItem.vue' +import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes' import { LiteGraph } from '@/lib/litegraph/src/litegraph' import type { LGraphNode, NodeId } from '@/lib/litegraph/src/LGraphNode' import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' @@ -27,7 +28,6 @@ import { DOMWidgetImpl } from '@/scripts/domWidget' import { promptRenameWidget } from '@/utils/widgetUtil' import { useAppMode } from '@/composables/useAppMode' import { nodeTypeValidForApp, useAppModeStore } from '@/stores/appModeStore' -import { resolveNode } from '@/utils/litegraphUtil' import { cn } from '@/utils/tailwindUtil' import { HideLayoutFieldKey } from '@/types/widgetTypes' @@ -52,18 +52,15 @@ workflowStore.activeWorkflow?.changeTracker?.reset() const arrangeInputs = computed(() => appModeStore.selectedInputs .map(([nodeId, widgetName]) => { - const node = resolveNode(nodeId) - if (!node) return null - const widget = node.widgets?.find((w) => w.name === widgetName) - return { nodeId, widgetName, node, widget } + const [node, widget] = resolveNodeWidget(nodeId, widgetName) + return node ? { nodeId, widgetName, node, widget } : null }) .filter((item): item is NonNullable => item !== null) ) const inputsWithState = computed(() => appModeStore.selectedInputs.map(([nodeId, widgetName]) => { - const node = resolveNode(nodeId) - const widget = node?.widgets?.find((w) => w.name === widgetName) + const [node, widget] = resolveNodeWidget(nodeId, widgetName) if (!node || !widget) { return { nodeId, @@ -105,10 +102,34 @@ function getHovered( if (widget || node.constructor.nodeData?.output_node) return [node, widget] } +function resolveNodeWidget( + nodeId: NodeId, + widgetName?: string +): [LGraphNode, IBaseWidget] | [LGraphNode] | [] { + const node = app.graph.getNodeById(nodeId) + if (!widgetName) return node ? [node] : [] + if (node) { + const widget = node.widgets?.find((w) => w.name === widgetName) + return widget ? [node, widget] : [] + } + + for (const node of app.graph.nodes) { + if (!node.isSubgraphNode()) continue + const widget = node.widgets?.find( + (w) => + isPromotedWidgetView(w) && + w.sourceWidgetName === widgetName && + w.sourceNodeId === nodeId + ) + if (widget) return [node, widget] + } + + return [] +} function getBounding(nodeId: NodeId, widgetName?: string) { if (settingStore.get('Comfy.VueNodes.Enabled')) return undefined - const node = app.rootGraph.getNodeById(nodeId) + const [node, widget] = resolveNodeWidget(nodeId, widgetName) if (!node) return const titleOffset = @@ -121,7 +142,6 @@ function getBounding(nodeId: NodeId, widgetName?: string) { left: `${node.pos[0]}px`, top: `${node.pos[1] - titleOffset}px` } - const widget = node.widgets?.find((w) => w.name === widgetName) if (!widget) return const margin = widget instanceof DOMWidgetImpl ? widget.margin : undefined @@ -162,10 +182,14 @@ function handleClick(e: MouseEvent) { } if (!isSelectInputsMode.value || widget.options.canvasOnly) return + const storeId = isPromotedWidgetView(widget) ? widget.sourceNodeId : node.id + const storeName = isPromotedWidgetView(widget) + ? widget.sourceWidgetName + : widget.name const index = appModeStore.selectedInputs.findIndex( - ([nodeId, widgetName]) => node.id == nodeId && widget.name === widgetName + ([nodeId, widgetName]) => storeId == nodeId && storeName === widgetName ) - if (index === -1) appModeStore.selectedInputs.push([node.id, widget.name]) + if (index === -1) appModeStore.selectedInputs.push([storeId, storeName]) else appModeStore.selectedInputs.splice(index, 1) } diff --git a/src/renderer/extensions/linearMode/LinearControls.vue b/src/renderer/extensions/linearMode/LinearControls.vue index 9dafd9a8f8..74bb0b0054 100644 --- a/src/renderer/extensions/linearMode/LinearControls.vue +++ b/src/renderer/extensions/linearMode/LinearControls.vue @@ -107,8 +107,10 @@ function getDropIndicator(node: LGraphNode) { function nodeToNodeData(node: LGraphNode) { const dropIndicator = getDropIndicator(node) const nodeData = extractVueNodeData(node) - remove(nodeData.widgets ?? [], (w) => w.slotMetadata?.linked ?? false) - for (const widget of nodeData.widgets ?? []) widget.slotMetadata = undefined + for (const widget of nodeData.widgets ?? []) { + widget.slotMetadata = undefined + widget.nodeId = String(node.id) + } return { ...nodeData,