mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
fix: prune stale proxyWidgets referencing nodes removed by nested subgraph packing
When nodes are packed into a nested subgraph, the outer SubgraphNode's proxyWidgets retained stale entries referencing grandchild node IDs that no longer exist in its direct subgraph. These entries rendered as 'Disconnected' placeholder widgets on the canvas. Two fixes: - Filter entries during hydration when source node doesn't exist in subgraph - Skip missing-node entries in _pruneStaleAliasFallbackEntries as defense Amp-Thread-ID: https://ampcode.com/threads/T-019d1335-0090-74bc-8615-0f07354f5ce4 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
@@ -509,6 +509,8 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
const prunedEntries: PromotedWidgetSource[] = []
|
||||
|
||||
for (const entry of fallbackStoredEntries) {
|
||||
if (!this.subgraph.getNodeById(entry.sourceNodeId)) continue
|
||||
|
||||
const concreteKey = this._resolveConcretePromotionEntryKey(entry)
|
||||
if (concreteKey && linkedConcreteKeys.has(concreteKey)) continue
|
||||
|
||||
@@ -1049,6 +1051,7 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
}
|
||||
return null
|
||||
}
|
||||
if (!this.subgraph.getNodeById(nodeId)) return null
|
||||
const entry: PromotedWidgetSource = {
|
||||
sourceNodeId: nodeId,
|
||||
sourceWidgetName: widgetName,
|
||||
@@ -1060,8 +1063,8 @@ export class SubgraphNode extends LGraphNode implements BaseLGraph {
|
||||
|
||||
store.setPromotions(this.rootGraph.id, this.id, entries)
|
||||
|
||||
// Write back resolved entries so legacy -1 format doesn't persist
|
||||
if (raw.some(([id]) => id === '-1')) {
|
||||
// Write back resolved entries so legacy or stale entries don't persist
|
||||
if (entries.length !== raw.length) {
|
||||
this.properties.proxyWidgets = this._serializeEntries(entries)
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
TWidgetType
|
||||
} from '@/lib/litegraph/src/litegraph'
|
||||
import { BaseWidget, LGraphNode } from '@/lib/litegraph/src/litegraph'
|
||||
import { isPromotedWidgetView } from '@/core/graph/subgraph/promotedWidgetTypes'
|
||||
|
||||
import {
|
||||
createEventCapture,
|
||||
@@ -263,6 +264,62 @@ describe('SubgraphWidgetPromotion', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('Nested Subgraph Widget Promotion', () => {
|
||||
it('should prune proxyWidgets referencing nodes not in subgraph on configure', () => {
|
||||
// Reproduces the bug where packing nodes into a nested subgraph leaves
|
||||
// stale proxyWidgets on the outer subgraph node referencing grandchild
|
||||
// node IDs that no longer exist directly in the outer subgraph.
|
||||
// Uses 3 inputs with only 1 having a linked widget entry, matching the
|
||||
// real workflow structure where model/vae inputs don't resolve widgets.
|
||||
const subgraph = createTestSubgraph({
|
||||
inputs: [
|
||||
{ name: 'clip', type: 'CLIP' },
|
||||
{ name: 'model', type: 'MODEL' },
|
||||
{ name: 'vae', type: 'VAE' }
|
||||
]
|
||||
})
|
||||
|
||||
const { node: samplerNode } = createNodeWithWidget(
|
||||
'Sampler',
|
||||
'number',
|
||||
42,
|
||||
'number'
|
||||
)
|
||||
subgraph.add(samplerNode)
|
||||
subgraph.inputNode.slots[1].connect(samplerNode.inputs[0], samplerNode)
|
||||
|
||||
// Add nodes without widget-connected inputs for the other slots
|
||||
const modelNode = new LGraphNode('ModelNode')
|
||||
modelNode.addInput('model', 'MODEL')
|
||||
subgraph.add(modelNode)
|
||||
|
||||
const vaeNode = new LGraphNode('VAENode')
|
||||
vaeNode.addInput('vae', 'VAE')
|
||||
subgraph.add(vaeNode)
|
||||
|
||||
const outerNode = createTestSubgraphNode(subgraph)
|
||||
|
||||
// Inject stale proxyWidgets referencing nodes that don't exist in
|
||||
// this subgraph (they were packed into a nested subgraph)
|
||||
outerNode.properties.proxyWidgets = [
|
||||
['999', 'text'],
|
||||
['998', 'text'],
|
||||
[String(samplerNode.id), 'widget']
|
||||
]
|
||||
|
||||
outerNode.configure(outerNode.serialize())
|
||||
|
||||
// Check widgets getter — stale entries should not produce views
|
||||
const widgetSourceIds = outerNode.widgets
|
||||
.filter(isPromotedWidgetView)
|
||||
.filter((w) => !w.name.startsWith('$$'))
|
||||
.map((w) => w.sourceNodeId)
|
||||
|
||||
expect(widgetSourceIds).not.toContain('999')
|
||||
expect(widgetSourceIds).not.toContain('998')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Tooltip Promotion', () => {
|
||||
it('should preserve widget tooltip when promoting', () => {
|
||||
const subgraph = createTestSubgraph({
|
||||
|
||||
Reference in New Issue
Block a user