From 8273c97b2f3ce7f5bbbb95e2d8714398c5b7658b Mon Sep 17 00:00:00 2001 From: DrJKL Date: Wed, 13 May 2026 13:07:33 -0700 Subject: [PATCH] refactor(appMode): resolve selectedInputs once at the computed boundary MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce `useResolvedSelectedInputs` that maps each persisted `[entityId, displayName, config?]` entry to a discriminated union of `{ status: 'resolved', node, widget, ... } | { status: 'unknown', ... }`. Handlers (rename, remove, resize, bounding) now receive widget instances directly instead of re-walking `rootGraph.nodes` per interaction. - Delete `src/world/widgetLookup.ts`; the lone remaining caller (`upgradeAndValidateInput` in `appModeStore.ts`) inlines the lookup as a module-private function. - `inlineRenameInput` deleted; the template calls `renameWidget(widget, node, $event)` directly with the in-scope widget. - `getWidgetBounding` accepts a `ResolvedSelection`; returns `undefined` for unresolved entries. - `useAppModeWidgetResizing.onPointerDown` and `updateInputConfig` accept a widget instance, matching the existing `removeSelectedInput` shape. - Persisted `selectedInputs` shape unchanged. - Unresolved entries continue to render as a removable "unknown widget" pill in the sidebar so users can clean up dangling selections. Slot-rename → entityId-drift bug investigated and confirmed not present: the `renaming-input` handler in `SubgraphNode.ts` mutates `widget.label` and `input.label`, never `widget.name`, so `entityId` is stable across renames. Documented as an invariant in `useResolvedSelectedInputs`. Amp-Thread-ID: https://ampcode.com/threads/T-019e2260-c2ba-70b4-9962-1638be4bf645 Co-authored-by: Amp --- src/components/builder/AppBuilder.vue | 94 ++++++++----------- src/components/builder/AppModeWidgetList.vue | 44 ++------- .../builder/useAppModeWidgetResizing.test.ts | 48 +++++----- .../builder/useAppModeWidgetResizing.ts | 8 +- .../builder/useResolvedSelectedInputs.ts | 90 ++++++++++++++++++ src/stores/appModeStore.test.ts | 20 +++- src/stores/appModeStore.ts | 35 +++++-- src/world/widgetLookup.ts | 28 ------ 8 files changed, 211 insertions(+), 156 deletions(-) create mode 100644 src/components/builder/useResolvedSelectedInputs.ts delete mode 100644 src/world/widgetLookup.ts diff --git a/src/components/builder/AppBuilder.vue b/src/components/builder/AppBuilder.vue index 126f8e5ae3..6ee74f1671 100644 --- a/src/components/builder/AppBuilder.vue +++ b/src/components/builder/AppBuilder.vue @@ -8,6 +8,9 @@ import AppModeWidgetList from '@/components/builder/AppModeWidgetList.vue' import DraggableList from '@/components/common/DraggableList.vue' import IoItem from '@/components/builder/IoItem.vue' import PropertiesAccordionItem from '@/components/rightSidePanel/layout/PropertiesAccordionItem.vue' +import { useResolvedSelectedInputs } from '@/components/builder/useResolvedSelectedInputs' +import type { ResolvedSelection } from '@/components/builder/useResolvedSelectedInputs' +import type { WidgetEntityId } from '@/world/entityIds' 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,8 +30,6 @@ import { DOMWidgetImpl } from '@/scripts/domWidget' import { renameWidget } from '@/utils/widgetUtil' import { useAppMode } from '@/composables/useAppMode' import { nodeTypeValidForApp, useAppModeStore } from '@/stores/appModeStore' -import type { WidgetEntityId } from '@/world/entityIds' -import { findWidgetByEntityId } from '@/world/widgetLookup' import { cn } from '@comfyorg/tailwind-utils' type BoundStyle = { top: string; left: string; width: string; height: string } @@ -47,27 +48,8 @@ const hoveringSelectable = ref(false) workflowStore.activeWorkflow?.changeTracker?.reset() -const inputsWithState = computed(() => - appModeStore.selectedInputs.map(([entityId]) => { - const found = - typeof entityId === 'string' - ? findWidgetByEntityId(app.rootGraph, entityId as WidgetEntityId) - : undefined - if (!found) { - return { - entityId: entityId as WidgetEntityId, - subLabel: t('linearMode.builder.unknownWidget') - } - } - const [node, widget] = found - return { - entityId: entityId as WidgetEntityId, - label: widget.label, - subLabel: node.title, - canRename: true - } - }) -) +const resolvedInputs = useResolvedSelectedInputs() + const outputsWithState = computed<[NodeId, string][]>(() => appModeStore.selectedOutputs.map((nodeId) => [ nodeId, @@ -75,13 +57,6 @@ const outputsWithState = computed<[NodeId, string][]>(() => ]) ) -function inlineRenameInput(entityId: WidgetEntityId, newLabel: string) { - const found = findWidgetByEntityId(app.rootGraph, entityId) - if (!found) return - const [node, widget] = found - renameWidget(widget, node, newLabel) -} - function getHovered( e: MouseEvent ): undefined | [LGraphNode, undefined] | [LGraphNode, IBaseWidget] { @@ -116,11 +91,10 @@ function getNodeBounding(nodeId: NodeId) { } } -function getWidgetBounding(entityId: WidgetEntityId) { +function getWidgetBounding(entry: ResolvedSelection): BoundStyle | undefined { if (settingStore.get('Comfy.VueNodes.Enabled')) return undefined - const found = findWidgetByEntityId(app.rootGraph, entityId) - if (!found) return - const [node, widget] = found + if (entry.status !== 'resolved') return undefined + const { node, widget } = entry const margin = widget instanceof DOMWidgetImpl ? widget.margin : undefined const marginX = margin ?? BaseWidget.margin @@ -136,6 +110,11 @@ function getWidgetBounding(entityId: WidgetEntityId) { } } +function removeSelectedEntityId(entityId: WidgetEntityId): void { + const index = appModeStore.selectedInputs.findIndex(([id]) => id === entityId) + if (index !== -1) appModeStore.selectedInputs.splice(index, 1) +} + function handleDown(e: MouseEvent) { const [node] = getHovered(e) ?? [] if (!node || e.button > 0) canvasInteractions.forwardEventToCanvas(e) @@ -191,15 +170,13 @@ const renderedOutputs = computed(() => { }) const renderedInputs = computed<[string, MaybeRef | undefined][]>( () => - appModeStore.selectedInputs.flatMap(([entityId]) => { - if (typeof entityId !== 'string') return [] - return [ - [entityId, getWidgetBounding(entityId as WidgetEntityId)] as [ + resolvedInputs.value.map( + (entry) => + [entry.entityId, getWidgetBounding(entry)] as [ string, MaybeRef | undefined ] - ] - }) + ) )