fix: stabilize nested subgraph promoted widget resolution (#9282)

## Summary

Fix multiple issues with promoted widget resolution in nested subgraphs,
ensuring correct value propagation, slot matching, and rendering for
deeply nested promoted widgets.

## Changes

- **What**: Stabilize nested subgraph promoted widget resolution chain
- Use deep source keys for promoted widget values in Vue rendering mode
- Resolve effective widget options from the source widget instead of the
promoted view
  - Stabilize slot resolution for nested promoted widgets
  - Preserve combo value rendering for promoted subgraph widgets
- Prevent subgraph definition deletion while other nodes still reference
the same type
  - Clean up unused exported resolution types

## Review Focus

- `resolveConcretePromotedWidget.ts` — new recursive resolution logic
for deeply nested promoted widgets
- `useGraphNodeManager.ts` — option extraction now uses
`effectiveWidget` for promoted widgets
- `SubgraphNode.ts` — unpack no longer force-deletes definitions
referenced by other nodes

┆Issue is synchronized with this [Notion
page](https://www.notion.so/PR-9282-fix-stabilize-nested-subgraph-promoted-widget-resolution-3146d73d365081208a4fe931bb7569cf)
by [Unito](https://www.unito.io)

---------

Co-authored-by: Amp <amp@ampcode.com>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Alexander Brown
2026-02-28 13:45:04 -08:00
committed by GitHub
parent 0ab3fdc2c9
commit dd1a1f77d6
24 changed files with 2866 additions and 147 deletions

View File

@@ -0,0 +1,55 @@
import type { INodeInputSlot } from '@/lib/litegraph/src/interfaces'
import type { LGraphNode } from '@/lib/litegraph/src/litegraph'
type SubgraphInputLinkContext = {
inputNode: LGraphNode
targetInput: INodeInputSlot
getTargetWidget: () => ReturnType<LGraphNode['getWidgetFromSlot']>
}
export function resolveSubgraphInputLink<TResult>(
node: LGraphNode,
inputName: string,
resolve: (context: SubgraphInputLinkContext) => TResult | undefined
): TResult | undefined {
if (!node.isSubgraphNode()) return undefined
const inputSlot = node.subgraph.inputNode.slots.find(
(slot) => slot.name === inputName
)
if (!inputSlot) return undefined
// Iterate from newest to oldest so the latest connection wins.
for (let index = inputSlot.linkIds.length - 1; index >= 0; index -= 1) {
const linkId = inputSlot.linkIds[index]
const link = node.subgraph.getLink(linkId)
if (!link) continue
const { inputNode } = link.resolve(node.subgraph)
if (!inputNode) continue
if (!Array.isArray(inputNode.inputs)) continue
const targetInput = inputNode.inputs.find((entry) => entry.link === linkId)
if (!targetInput) continue
let cachedTargetWidget:
| ReturnType<LGraphNode['getWidgetFromSlot']>
| undefined
let hasCachedTargetWidget = false
const resolved = resolve({
inputNode,
targetInput,
getTargetWidget: () => {
if (!hasCachedTargetWidget) {
cachedTargetWidget = inputNode.getWidgetFromSlot(targetInput)
hasCachedTargetWidget = true
}
return cachedTargetWidget
}
})
if (resolved !== undefined) return resolved
}
return undefined
}