From 7cc7a0ce07f08c18959e6001b557094357fe1353 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 11 Nov 2025 15:21:36 -0800 Subject: [PATCH 01/21] Initial implementation doesn't correctly apply on initial load --- src/services/litegraphService.ts | 55 +++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/src/services/litegraphService.ts b/src/services/litegraphService.ts index 35562533b2..29b3d8be60 100644 --- a/src/services/litegraphService.ts +++ b/src/services/litegraphService.ts @@ -2,6 +2,7 @@ import _ from 'es-toolkit/compat' import { downloadFile } from '@/base/common/downloadUtil' import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems' +import { useChainCallback } from '@/composables/functional/useChainCallback' import { useNodeAnimatedImage } from '@/composables/node/useNodeAnimatedImage' import { useNodeCanvasImagePreview } from '@/composables/node/useNodeCanvasImagePreview' import { useNodeImage, useNodeVideo } from '@/composables/node/useNodeImage' @@ -33,7 +34,10 @@ import { useToastStore } from '@/platform/updates/common/toastStore' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' -import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration' +import { + transformInputSpecV1ToV2, + transformInputSpecV2ToV1 +} from '@/schemas/nodeDef/migration' import type { ComfyNodeDef as ComfyNodeDefV2, InputSpec, @@ -511,6 +515,55 @@ export const useLitegraphService = () => { this.#initialMinSize.height, minHeight ) + + if (!inputSpec.dynamicWidgets) return + let dynamicWidgetsCount = 0 + widget.callback = useChainCallback(widget.callback, (value) => { + //TODO: use current values instead of original + if (!this.widgets) throw new Error('Not Reachable') + const newSpecs = (inputSpec.dynamicWidgets as Record)[ + value + ] + //NOTE: Unlike VHS implementation, swapping between 2 values which + //both provide a dynamic widget of the same name will result in + //any links to the dynamic widget being broken. + //This is probably undesirable, but would add significant complexity + //to this code. + const startIndex = this.widgets.findIndex((w) => w === widget) + if (startIndex === -1) + throw new Error('attempted change on non-existint format widget') + + //Allow for recursive removal of dynamic widgets + for (let i = 1; i <= dynamicWidgetsCount; i++) + this.widgets[i + startIndex].callback?.(undefined) + const removedWidgets = this.widgets.splice( + startIndex + 1, + dynamicWidgetsCount + ) + for (const removedWidget of removedWidgets) { + const slotToRemove = this.inputs.findIndex( + (inp) => inp.widget?.name === removedWidget.name + ) + this.removeInput(slotToRemove) + } + + const initialLength = this.widgets.length + //TODO: Try/finally here? Partial completion must be cleaned + for (const spec of newSpecs ?? []) { + const name = spec[0] + this.#addInputWidget( + transformInputSpecV1ToV2(spec.slice(1), { name }) + ) + this.widgets.at(-1)?.callback?.(this.widgets.at(-1)!.value) + } + const addedWidgets = this.widgets.splice(initialLength) + this.widgets.splice(startIndex + 1, 0, ...addedWidgets) + ///TODO: Add better handling for failed addition + dynamicWidgetsCount = Math.min( + addedWidgets.length, + newSpecs?.length ?? 0 + ) + }) } /** From 728376f2a776a65c16e5dca90cc143a6c010eb87 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 11 Nov 2025 17:28:32 -0800 Subject: [PATCH 02/21] Fix serialization --- src/lib/litegraph/src/LGraphNode.ts | 13 ++++++------- src/services/litegraphService.ts | 11 +++++++++++ tsconfig.json | 1 + 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index 94c0310b1e..87727f4d6a 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -845,15 +845,14 @@ export class LGraphNode } if (info.widgets_values) { - const widgetsWithValue = this.widgets.filter( - (w) => w.serialize !== false - ) - for (let i = 0; i < info.widgets_values.length; ++i) { - const widget = widgetsWithValue[i] + const widgetsWithValue = this.widgets + .values() + .filter((w) => w.serialize !== false) + widgetsWithValue.forEach((widget, i) => { if (widget) { - widget.value = info.widgets_values[i] + widget.value = info.widgets_values![i] } - } + }) } } diff --git a/src/services/litegraphService.ts b/src/services/litegraphService.ts index 29b3d8be60..1585e98ee6 100644 --- a/src/services/litegraphService.ts +++ b/src/services/litegraphService.ts @@ -564,6 +564,17 @@ export const useLitegraphService = () => { newSpecs?.length ?? 0 ) }) + //A little hacky, but onConfigure won't work. + //It fires too late and is overly disruptive + Object.defineProperty(widget, 'value', { + get() { + return this._value + }, + set(value) { + this._value = value + this.callback!(value) + } + }) } /** diff --git a/tsconfig.json b/tsconfig.json index 5470e2c2df..6bb37354c3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,7 @@ "lib": [ "ES2023", "ES2023.Array", + "ESNext.Iterator", "DOM", "DOM.Iterable" ], From 5c9689fe3314260fe55b6a2ba36b3b1cf39a2821 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Fri, 14 Nov 2025 09:43:22 -0800 Subject: [PATCH 03/21] Update to new schema and isolate code --- src/extensions/core/dynamicCombo.ts | 79 +++++++++++++++++++++++++++++ src/extensions/core/index.ts | 1 + src/services/litegraphService.ts | 66 +----------------------- 3 files changed, 81 insertions(+), 65 deletions(-) create mode 100644 src/extensions/core/dynamicCombo.ts diff --git a/src/extensions/core/dynamicCombo.ts b/src/extensions/core/dynamicCombo.ts new file mode 100644 index 0000000000..04563d7bb0 --- /dev/null +++ b/src/extensions/core/dynamicCombo.ts @@ -0,0 +1,79 @@ +import { useChainCallback } from '@/composables/functional/useChainCallback' +import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +//import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' + +import type { + ComboInputSpec, + ComfyInputsSpec, + InputSpec +} from '@/schemas/nodeDefSchema' +import { app } from '@/scripts/app' +import type { ComfyApp } from '@/scripts/app' + +function COMFY_DYNAMICCOMBO_V3( + node: LGraphNode, + inputName: string, + inputData: InputSpec, + appArg: ComfyApp, + widgetName?: string +) { + debugger + //FIXME: properly add to schema + const options = inputData[1]?.options as Record + + const subSpec: ComboInputSpec = [Object.keys(options), {}] + const { widget, minWidth, minHeight } = app.widgets['COMBO']( + node, + inputName, + subSpec, + appArg, + widgetName + ) + let currentDynamicNames: string[] = [] + widget.callback = useChainCallback(widget.callback, (value) => { + if (!node.widgets) throw new Error('Not Reachable') + const newSpec = options[value] + //TODO: Calculate intersection for widgets that persist across options + for (const name of currentDynamicNames) { + const inputIndex = node.inputs.findIndex((input) => input.name === name) + if (inputIndex !== -1) node.removeInput(inputIndex) + const widgetIndex = node.widgets.findIndex( + (widget) => widget.name === name + ) + if (widgetIndex === -1) return + node.widgets[widgetIndex].callback?.(undefined) + node.widgets.splice(widgetIndex, 1) + } + currentDynamicNames = [] + if (!newSpec) return + + const insertionPoint = node.widgets.findIndex((w) => w === widget) + 1 + const startingLength = node.widgets.length + if (insertionPoint === 0) + throw new Error("Dynamic widget doesn't exist on node") + //process new inputs + //FIXME: inputs MUST be well ordered + + const addedWidgets = node.widgets.splice(startingLength) + node.widgets.splice(insertionPoint, 0, ...addedWidgets) + }) + //A little hacky, but onConfigure won't work. + //It fires too late and is overly disruptive + Object.defineProperty(widget, 'value', { + get() { + return this._value + }, + set(value) { + this._value = value + this.callback!(value) + } + }) + return { widget, minWidth, minHeight } +} + +app.registerExtension({ + name: 'Comfy.DynamicCombo', + getCustomWidgets() { + return { COMFY_DYNAMICCOMBO_V3 } + } +}) diff --git a/src/extensions/core/index.ts b/src/extensions/core/index.ts index 4171dce89d..f6f74b5d79 100644 --- a/src/extensions/core/index.ts +++ b/src/extensions/core/index.ts @@ -3,6 +3,7 @@ import { isCloud } from '@/platform/distribution/types' import './clipspace' import './contextMenuFilter' import './dynamicPrompts' +import './dynamicCombo' import './editAttention' import './electronAdapter' import './groupNode' diff --git a/src/services/litegraphService.ts b/src/services/litegraphService.ts index 1585e98ee6..35562533b2 100644 --- a/src/services/litegraphService.ts +++ b/src/services/litegraphService.ts @@ -2,7 +2,6 @@ import _ from 'es-toolkit/compat' import { downloadFile } from '@/base/common/downloadUtil' import { useSelectedLiteGraphItems } from '@/composables/canvas/useSelectedLiteGraphItems' -import { useChainCallback } from '@/composables/functional/useChainCallback' import { useNodeAnimatedImage } from '@/composables/node/useNodeAnimatedImage' import { useNodeCanvasImagePreview } from '@/composables/node/useNodeCanvasImagePreview' import { useNodeImage, useNodeVideo } from '@/composables/node/useNodeImage' @@ -34,10 +33,7 @@ import { useToastStore } from '@/platform/updates/common/toastStore' import { useWorkflowStore } from '@/platform/workflow/management/stores/workflowStore' import type { NodeId } from '@/platform/workflow/validation/schemas/workflowSchema' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' -import { - transformInputSpecV1ToV2, - transformInputSpecV2ToV1 -} from '@/schemas/nodeDef/migration' +import { transformInputSpecV2ToV1 } from '@/schemas/nodeDef/migration' import type { ComfyNodeDef as ComfyNodeDefV2, InputSpec, @@ -515,66 +511,6 @@ export const useLitegraphService = () => { this.#initialMinSize.height, minHeight ) - - if (!inputSpec.dynamicWidgets) return - let dynamicWidgetsCount = 0 - widget.callback = useChainCallback(widget.callback, (value) => { - //TODO: use current values instead of original - if (!this.widgets) throw new Error('Not Reachable') - const newSpecs = (inputSpec.dynamicWidgets as Record)[ - value - ] - //NOTE: Unlike VHS implementation, swapping between 2 values which - //both provide a dynamic widget of the same name will result in - //any links to the dynamic widget being broken. - //This is probably undesirable, but would add significant complexity - //to this code. - const startIndex = this.widgets.findIndex((w) => w === widget) - if (startIndex === -1) - throw new Error('attempted change on non-existint format widget') - - //Allow for recursive removal of dynamic widgets - for (let i = 1; i <= dynamicWidgetsCount; i++) - this.widgets[i + startIndex].callback?.(undefined) - const removedWidgets = this.widgets.splice( - startIndex + 1, - dynamicWidgetsCount - ) - for (const removedWidget of removedWidgets) { - const slotToRemove = this.inputs.findIndex( - (inp) => inp.widget?.name === removedWidget.name - ) - this.removeInput(slotToRemove) - } - - const initialLength = this.widgets.length - //TODO: Try/finally here? Partial completion must be cleaned - for (const spec of newSpecs ?? []) { - const name = spec[0] - this.#addInputWidget( - transformInputSpecV1ToV2(spec.slice(1), { name }) - ) - this.widgets.at(-1)?.callback?.(this.widgets.at(-1)!.value) - } - const addedWidgets = this.widgets.splice(initialLength) - this.widgets.splice(startIndex + 1, 0, ...addedWidgets) - ///TODO: Add better handling for failed addition - dynamicWidgetsCount = Math.min( - addedWidgets.length, - newSpecs?.length ?? 0 - ) - }) - //A little hacky, but onConfigure won't work. - //It fires too late and is overly disruptive - Object.defineProperty(widget, 'value', { - get() { - return this._value - }, - set(value) { - this._value = value - this.callback!(value) - } - }) } /** From 135e9aa8e69c419fbeb7604b15f1d344d4033c13 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Mon, 17 Nov 2025 10:34:08 -0800 Subject: [PATCH 04/21] Actually implement widget adding Still includes some tempoary implementations and seems to have an issue with flawed logic for removing inputs --- src/extensions/core/dynamicCombo.ts | 41 ++++++++++++++++++++++++----- src/services/litegraphService.ts | 5 ++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/extensions/core/dynamicCombo.ts b/src/extensions/core/dynamicCombo.ts index 04563d7bb0..8302f5fbf5 100644 --- a/src/extensions/core/dynamicCombo.ts +++ b/src/extensions/core/dynamicCombo.ts @@ -1,5 +1,6 @@ import { useChainCallback } from '@/composables/functional/useChainCallback' import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' +import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration' //import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' import type { @@ -17,9 +18,12 @@ function COMFY_DYNAMICCOMBO_V3( appArg: ComfyApp, widgetName?: string ) { - debugger //FIXME: properly add to schema - const options = inputData[1]?.options as Record + const options = Object.fromEntries( + (inputData[1]?.options as { inputs: ComfyInputsSpec; key: string }[]).map( + ({ key, inputs }) => [key, inputs] + ) + ) const subSpec: ComboInputSpec = [Object.keys(options), {}] const { widget, minWidth, minHeight } = app.widgets['COMBO']( @@ -40,7 +44,7 @@ function COMFY_DYNAMICCOMBO_V3( const widgetIndex = node.widgets.findIndex( (widget) => widget.name === name ) - if (widgetIndex === -1) return + if (widgetIndex === -1) continue node.widgets[widgetIndex].callback?.(undefined) node.widgets.splice(widgetIndex, 1) } @@ -51,20 +55,45 @@ function COMFY_DYNAMICCOMBO_V3( const startingLength = node.widgets.length if (insertionPoint === 0) throw new Error("Dynamic widget doesn't exist on node") - //process new inputs //FIXME: inputs MUST be well ordered + //FIXME check for duplicates + + if (newSpec.required) + for (const name in newSpec.required) { + //@ts-expect-error temporary duck violence + node._addInput( + transformInputSpecV1ToV2(newSpec.required[name], { + name, + isOptional: false + }) + ) + currentDynamicNames.push(name) + } + if (newSpec.optional) + for (const name in newSpec.optional) { + //@ts-expect-error temporary duck violence + node._addInput( + transformInputSpecV1ToV2(newSpec.optional[name], { + name, + isOptional: false + }) + ) + currentDynamicNames.push(name) + } const addedWidgets = node.widgets.splice(startingLength) node.widgets.splice(insertionPoint, 0, ...addedWidgets) + node.computeSize(node.size) }) //A little hacky, but onConfigure won't work. //It fires too late and is overly disruptive + let widgetValue = widget.value Object.defineProperty(widget, 'value', { get() { - return this._value + return widgetValue }, set(value) { - this._value = value + widgetValue = value this.callback!(value) } }) diff --git a/src/services/litegraphService.ts b/src/services/litegraphService.ts index 35562533b2..e804769182 100644 --- a/src/services/litegraphService.ts +++ b/src/services/litegraphService.ts @@ -528,6 +528,11 @@ export const useLitegraphService = () => { this.#addInputWidget(inputSpec) } + _addInput(inputSpec: InputSpec) { + this.#addInputSocket(inputSpec) + this.#addInputWidget(inputSpec) + } + /** * @internal Add outputs to the node. */ From e1c03ec95d2fbb3be6b741e005ac0c78df581771 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Mon, 17 Nov 2025 10:47:46 -0800 Subject: [PATCH 05/21] Ensure widgets created without configure When a node is freshly added to a workflow, there's not a configure event and the widgets absed on the initial state were not applied --- src/extensions/core/dynamicCombo.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extensions/core/dynamicCombo.ts b/src/extensions/core/dynamicCombo.ts index 8302f5fbf5..77b1671912 100644 --- a/src/extensions/core/dynamicCombo.ts +++ b/src/extensions/core/dynamicCombo.ts @@ -97,6 +97,7 @@ function COMFY_DYNAMICCOMBO_V3( this.callback!(value) } }) + widget.value = widgetValue return { widget, minWidth, minHeight } } From 9ab78522b101c5076b29593e76d981a4f760ca76 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Mon, 17 Nov 2025 11:00:09 -0800 Subject: [PATCH 06/21] Fix doubling of callback triggers Callback is usually, but not always triggered when the value changes. This had two downsides: - The dynamic Combo code would usually apply twice on value change - If there was any code that also added callbacks to the widget, it would be applied multiple times. Both of these are fixed by not setting the widget change code as a callback. --- src/extensions/core/dynamicCombo.ts | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/extensions/core/dynamicCombo.ts b/src/extensions/core/dynamicCombo.ts index 77b1671912..0485b90c70 100644 --- a/src/extensions/core/dynamicCombo.ts +++ b/src/extensions/core/dynamicCombo.ts @@ -1,7 +1,5 @@ -import { useChainCallback } from '@/composables/functional/useChainCallback' import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration' -//import type { IBaseWidget } from '@/lib/litegraph/src/types/widgets' import type { ComboInputSpec, @@ -34,9 +32,9 @@ function COMFY_DYNAMICCOMBO_V3( widgetName ) let currentDynamicNames: string[] = [] - widget.callback = useChainCallback(widget.callback, (value) => { + const updateWidgets = (value?: string) => { if (!node.widgets) throw new Error('Not Reachable') - const newSpec = options[value] + const newSpec = value ? options[value] : undefined //TODO: Calculate intersection for widgets that persist across options for (const name of currentDynamicNames) { const inputIndex = node.inputs.findIndex((input) => input.name === name) @@ -84,7 +82,7 @@ function COMFY_DYNAMICCOMBO_V3( const addedWidgets = node.widgets.splice(startingLength) node.widgets.splice(insertionPoint, 0, ...addedWidgets) node.computeSize(node.size) - }) + } //A little hacky, but onConfigure won't work. //It fires too late and is overly disruptive let widgetValue = widget.value @@ -94,7 +92,7 @@ function COMFY_DYNAMICCOMBO_V3( }, set(value) { widgetValue = value - this.callback!(value) + updateWidgets(value) } }) widget.value = widgetValue From d07e4abd6b5ff8af35144359f628fcffc294cd85 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Mon, 17 Nov 2025 15:54:30 -0800 Subject: [PATCH 07/21] less duplication, more typechecking --- src/extensions/core/dynamicCombo.ts | 48 +++++++++++++---------------- src/schemas/nodeDefSchema.ts | 14 ++++++++- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/src/extensions/core/dynamicCombo.ts b/src/extensions/core/dynamicCombo.ts index 0485b90c70..4b271512c7 100644 --- a/src/extensions/core/dynamicCombo.ts +++ b/src/extensions/core/dynamicCombo.ts @@ -1,27 +1,30 @@ import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration' -import type { - ComboInputSpec, - ComfyInputsSpec, - InputSpec -} from '@/schemas/nodeDefSchema' +import type { ComboInputSpec, InputSpec } from '@/schemas/nodeDefSchema' +import { zDynamicComboInputSpec } from '@/schemas/nodeDefSchema' import { app } from '@/scripts/app' import type { ComfyApp } from '@/scripts/app' function COMFY_DYNAMICCOMBO_V3( node: LGraphNode, inputName: string, - inputData: InputSpec, + untypedInputData: InputSpec, appArg: ComfyApp, widgetName?: string ) { - //FIXME: properly add to schema + const parseResult = zDynamicComboInputSpec.safeParse(untypedInputData) + if (!parseResult.success) throw new Error('invalid DynamicCombo spec') + const inputData = parseResult.data const options = Object.fromEntries( - (inputData[1]?.options as { inputs: ComfyInputsSpec; key: string }[]).map( - ({ key, inputs }) => [key, inputs] - ) + inputData[1].options.map(({ key, inputs }) => [key, inputs]) ) + for (const option of Object.values(options)) + for (const inputType of [option.required, option.optional]) + for (const key in inputType ?? {}) { + inputType![key][1] ??= {} + inputType![key][1].label = key + } const subSpec: ComboInputSpec = [Object.keys(options), {}] const { widget, minWidth, minHeight } = app.widgets['COMBO']( @@ -55,25 +58,18 @@ function COMFY_DYNAMICCOMBO_V3( throw new Error("Dynamic widget doesn't exist on node") //FIXME: inputs MUST be well ordered //FIXME check for duplicates - - if (newSpec.required) - for (const name in newSpec.required) { - //@ts-expect-error temporary duck violence - node._addInput( - transformInputSpecV1ToV2(newSpec.required[name], { - name, - isOptional: false - }) - ) - currentDynamicNames.push(name) - } - if (newSpec.optional) - for (const name in newSpec.optional) { + const inputTypes: [Record | undefined, boolean][] = [ + [newSpec.required, false], + [newSpec.optional, true] + ] + for (const [inputType, isOptional] of inputTypes) + for (const key in inputType ?? {}) { + const name = `${widget.name}.${key}` //@ts-expect-error temporary duck violence node._addInput( - transformInputSpecV1ToV2(newSpec.optional[name], { + transformInputSpecV1ToV2(inputType![key], { name, - isOptional: false + isOptional }) ) currentDynamicNames.push(name) diff --git a/src/schemas/nodeDefSchema.ts b/src/schemas/nodeDefSchema.ts index 73401b14af..a090bee7b3 100644 --- a/src/schemas/nodeDefSchema.ts +++ b/src/schemas/nodeDefSchema.ts @@ -186,7 +186,7 @@ const zInputSpec = z.union([ zCustomInputSpec ]) -const zComfyInputsSpec = z.object({ +export const zComfyInputsSpec = z.object({ required: z.record(zInputSpec).optional(), optional: z.record(zInputSpec).optional(), // Frontend repo is not using it, but some custom nodes are using the @@ -230,6 +230,18 @@ export const zComfyNodeDef = z.object({ input_order: z.record(z.array(z.string())).optional() }) +export const zDynamicComboInputSpec = z.tuple([ + z.literal('COMFY_DYNAMICCOMBO_V3'), + zComboInputOptions.extend({ + options: z.array( + z.object({ + inputs: zComfyInputsSpec, + key: z.string() + }) + ) + }) +]) + // `/object_info` export type ComfyInputsSpec = z.infer export type ComfyOutputTypesSpec = z.infer From b9cd1bd1cd7ae96afc3fb985c36d0b46d3ca83c4 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Mon, 17 Nov 2025 16:39:49 -0800 Subject: [PATCH 08/21] Fix key renaming --- src/extensions/core/dynamicCombo.ts | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/extensions/core/dynamicCombo.ts b/src/extensions/core/dynamicCombo.ts index 4b271512c7..db2c1b9020 100644 --- a/src/extensions/core/dynamicCombo.ts +++ b/src/extensions/core/dynamicCombo.ts @@ -19,13 +19,6 @@ function COMFY_DYNAMICCOMBO_V3( const options = Object.fromEntries( inputData[1].options.map(({ key, inputs }) => [key, inputs]) ) - for (const option of Object.values(options)) - for (const inputType of [option.required, option.optional]) - for (const key in inputType ?? {}) { - inputType![key][1] ??= {} - inputType![key][1].label = key - } - const subSpec: ComboInputSpec = [Object.keys(options), {}] const { widget, minWidth, minHeight } = app.widgets['COMBO']( node, @@ -54,6 +47,7 @@ function COMFY_DYNAMICCOMBO_V3( const insertionPoint = node.widgets.findIndex((w) => w === widget) + 1 const startingLength = node.widgets.length + const startingInputLength = node.inputs.length if (insertionPoint === 0) throw new Error("Dynamic widget doesn't exist on node") //FIXME: inputs MUST be well ordered @@ -63,19 +57,24 @@ function COMFY_DYNAMICCOMBO_V3( [newSpec.optional, true] ] for (const [inputType, isOptional] of inputTypes) - for (const key in inputType ?? {}) { - const name = `${widget.name}.${key}` + for (const name in inputType ?? {}) { //@ts-expect-error temporary duck violence node._addInput( - transformInputSpecV1ToV2(inputType![key], { + transformInputSpecV1ToV2(inputType![name], { name, isOptional }) ) - currentDynamicNames.push(name) + currentDynamicNames.push(`${widget.name}.${name}`) } const addedWidgets = node.widgets.splice(startingLength) + for (const addedWidget of addedWidgets) { + addedWidget.name = `${widget.name}.${addedWidget.name}` + } + for (const input of node.inputs.slice(startingInputLength)) { + input.name = `${widget.name}.${input.name}` + } node.widgets.splice(insertionPoint, 0, ...addedWidgets) node.computeSize(node.size) } From 58eab623ebd6f24b55b4102920a23aab15a7981d Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Mon, 17 Nov 2025 19:02:01 -0800 Subject: [PATCH 09/21] Implement input splicing --- src/extensions/core/dynamicCombo.ts | 19 +++++++++++++++++-- src/lib/litegraph/src/LGraphNode.ts | 16 ++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/extensions/core/dynamicCombo.ts b/src/extensions/core/dynamicCombo.ts index db2c1b9020..47a3e4e233 100644 --- a/src/extensions/core/dynamicCombo.ts +++ b/src/extensions/core/dynamicCombo.ts @@ -47,6 +47,8 @@ function COMFY_DYNAMICCOMBO_V3( const insertionPoint = node.widgets.findIndex((w) => w === widget) + 1 const startingLength = node.widgets.length + const inputInsertionPoint = + node.inputs.findIndex((i) => i.name === widget.name) + 1 const startingInputLength = node.inputs.length if (insertionPoint === 0) throw new Error("Dynamic widget doesn't exist on node") @@ -72,11 +74,24 @@ function COMFY_DYNAMICCOMBO_V3( for (const addedWidget of addedWidgets) { addedWidget.name = `${widget.name}.${addedWidget.name}` } + node.widgets.splice(insertionPoint, 0, ...addedWidgets) + node.size[1] = node.computeSize([...node.size])[1] for (const input of node.inputs.slice(startingInputLength)) { input.name = `${widget.name}.${input.name}` + if (input.widget) + input.widget.name = `${widget.name}.${input.widget.name}` } - node.widgets.splice(insertionPoint, 0, ...addedWidgets) - node.computeSize(node.size) + if (inputInsertionPoint === 0) { + if ( + addedWidgets.length === 0 && + node.inputs.length !== startingInputLength + ) + //input is inputOnly, but lacks an insertion point + throw new Error('Failed to find input socket for ' + widget.name) + return + } + const addedInputs = node.spliceInputs(startingInputLength) + node.spliceInputs(inputInsertionPoint, 0, ...addedInputs) } //A little hacky, but onConfigure won't work. //It fires too late and is overly disruptive diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index 87727f4d6a..a1aeb5549a 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -1648,6 +1648,22 @@ export class LGraphNode this.onInputRemoved?.(slot, slot_info[0]) this.setDirtyCanvas(true, true) } + spliceInputs( + startIndex: number, + deleteCount = -1, + ...toAdd: INodeInputSlot[] + ): INodeInputSlot[] { + if (deleteCount === -1) deleteCount = this.inputs.length - startIndex + + const lengthDelta = toAdd.length - deleteCount + const ret = this.inputs.splice(startIndex, deleteCount, ...toAdd) + for (const input of this.inputs.slice(startIndex + toAdd.length)) { + const link = input.link && this.graph?.links?.get(input.link) + if (!link) continue + link.target_slot += lengthDelta + } + return ret + } /** * computes the minimum size of a node according to its inputs and output slots From 61018bb1027ecb5f72c9f661475077b1d86fa0f9 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Mon, 17 Nov 2025 19:07:00 -0800 Subject: [PATCH 10/21] Fix unused export --- src/schemas/nodeDefSchema.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/schemas/nodeDefSchema.ts b/src/schemas/nodeDefSchema.ts index a090bee7b3..dd311fbc91 100644 --- a/src/schemas/nodeDefSchema.ts +++ b/src/schemas/nodeDefSchema.ts @@ -186,7 +186,7 @@ const zInputSpec = z.union([ zCustomInputSpec ]) -export const zComfyInputsSpec = z.object({ +const zComfyInputsSpec = z.object({ required: z.record(zInputSpec).optional(), optional: z.record(zInputSpec).optional(), // Frontend repo is not using it, but some custom nodes are using the From 1e47ddcfae3bc89ced0fdb5ababb9251b76eeb4a Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Mon, 17 Nov 2025 19:31:39 -0800 Subject: [PATCH 11/21] Fix input duplication on load --- src/extensions/core/dynamicCombo.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/extensions/core/dynamicCombo.ts b/src/extensions/core/dynamicCombo.ts index 47a3e4e233..6caebdf8a7 100644 --- a/src/extensions/core/dynamicCombo.ts +++ b/src/extensions/core/dynamicCombo.ts @@ -75,7 +75,6 @@ function COMFY_DYNAMICCOMBO_V3( addedWidget.name = `${widget.name}.${addedWidget.name}` } node.widgets.splice(insertionPoint, 0, ...addedWidgets) - node.size[1] = node.computeSize([...node.size])[1] for (const input of node.inputs.slice(startingInputLength)) { input.name = `${widget.name}.${input.name}` if (input.widget) @@ -90,8 +89,17 @@ function COMFY_DYNAMICCOMBO_V3( throw new Error('Failed to find input socket for ' + widget.name) return } - const addedInputs = node.spliceInputs(startingInputLength) + const addedInputs = node + .spliceInputs(startingInputLength) + .filter( + (addedInput) => + !node.inputs.some( + (existingInput) => addedInput.name === existingInput.name + ) + ) + //assume existing inputs are in correct order node.spliceInputs(inputInsertionPoint, 0, ...addedInputs) + node.size[1] = node.computeSize([...node.size])[1] } //A little hacky, but onConfigure won't work. //It fires too late and is overly disruptive From d58b8feaa7719c7b9ecd5e330c8cdab4b1e26de1 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Mon, 17 Nov 2025 20:44:15 -0800 Subject: [PATCH 12/21] Fix nesting, remove namespacing --- src/extensions/core/dynamicCombo.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/extensions/core/dynamicCombo.ts b/src/extensions/core/dynamicCombo.ts index 6caebdf8a7..64eb647231 100644 --- a/src/extensions/core/dynamicCombo.ts +++ b/src/extensions/core/dynamicCombo.ts @@ -39,7 +39,7 @@ function COMFY_DYNAMICCOMBO_V3( (widget) => widget.name === name ) if (widgetIndex === -1) continue - node.widgets[widgetIndex].callback?.(undefined) + node.widgets[widgetIndex].value = undefined node.widgets.splice(widgetIndex, 1) } currentDynamicNames = [] @@ -67,19 +67,11 @@ function COMFY_DYNAMICCOMBO_V3( isOptional }) ) - currentDynamicNames.push(`${widget.name}.${name}`) + currentDynamicNames.push(name) } const addedWidgets = node.widgets.splice(startingLength) - for (const addedWidget of addedWidgets) { - addedWidget.name = `${widget.name}.${addedWidget.name}` - } node.widgets.splice(insertionPoint, 0, ...addedWidgets) - for (const input of node.inputs.slice(startingInputLength)) { - input.name = `${widget.name}.${input.name}` - if (input.widget) - input.widget.name = `${widget.name}.${input.widget.name}` - } if (inputInsertionPoint === 0) { if ( addedWidgets.length === 0 && From b5f3df08818655c152c038bc3e91976abd719d26 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 18 Nov 2025 09:12:21 -0800 Subject: [PATCH 13/21] Move out of extensions --- .../graph/widgets/dynamicWidgets.ts} | 7 +------ src/extensions/core/index.ts | 1 - src/scripts/widgets.ts | 4 +++- 3 files changed, 4 insertions(+), 8 deletions(-) rename src/{extensions/core/dynamicCombo.ts => core/graph/widgets/dynamicWidgets.ts} (96%) diff --git a/src/extensions/core/dynamicCombo.ts b/src/core/graph/widgets/dynamicWidgets.ts similarity index 96% rename from src/extensions/core/dynamicCombo.ts rename to src/core/graph/widgets/dynamicWidgets.ts index 64eb647231..d93f3abf91 100644 --- a/src/extensions/core/dynamicCombo.ts +++ b/src/core/graph/widgets/dynamicWidgets.ts @@ -109,9 +109,4 @@ function COMFY_DYNAMICCOMBO_V3( return { widget, minWidth, minHeight } } -app.registerExtension({ - name: 'Comfy.DynamicCombo', - getCustomWidgets() { - return { COMFY_DYNAMICCOMBO_V3 } - } -}) +export const dynamicWidgets = { COMFY_DYNAMICCOMBO_V3 } diff --git a/src/extensions/core/index.ts b/src/extensions/core/index.ts index f6f74b5d79..4171dce89d 100644 --- a/src/extensions/core/index.ts +++ b/src/extensions/core/index.ts @@ -3,7 +3,6 @@ import { isCloud } from '@/platform/distribution/types' import './clipspace' import './contextMenuFilter' import './dynamicPrompts' -import './dynamicCombo' import './editAttention' import './electronAdapter' import './groupNode' diff --git a/src/scripts/widgets.ts b/src/scripts/widgets.ts index dd75081af1..15149fd76d 100644 --- a/src/scripts/widgets.ts +++ b/src/scripts/widgets.ts @@ -6,6 +6,7 @@ import type { IStringWidget } from '@/lib/litegraph/src/types/widgets' import { useSettingStore } from '@/platform/settings/settingStore' +import { dynamicWidgets } from '@/core/graph/widgets/dynamicWidgets' import { useBooleanWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useBooleanWidget' import { useChartWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useChartWidget' import { useColorWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useColorWidget' @@ -304,5 +305,6 @@ export const ComfyWidgets: Record = { CHART: transformWidgetConstructorV2ToV1(useChartWidget()), GALLERIA: transformWidgetConstructorV2ToV1(useGalleriaWidget()), SELECTBUTTON: transformWidgetConstructorV2ToV1(useSelectButtonWidget()), - TEXTAREA: transformWidgetConstructorV2ToV1(useTextareaWidget()) + TEXTAREA: transformWidgetConstructorV2ToV1(useTextareaWidget()), + ...dynamicWidgets } From f185a9853289a40c3f8e6683440f1cf832330032 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 18 Nov 2025 09:49:10 -0800 Subject: [PATCH 14/21] Remove single ts-expect-error litegraphService had a very large amount of duplicated code that was private methods. Private methods are awful and prevent reuse. These methods have all been moved to anonymous functions with no duplication. The primary purpose of this is to expose a single addNodeInput function for use in dynamic widget code. The battle to end duck violence is long and hard fought. --- src/core/graph/widgets/dynamicWidgets.ts | 7 +- src/services/litegraphService.ts | 507 +++++++---------------- 2 files changed, 162 insertions(+), 352 deletions(-) diff --git a/src/core/graph/widgets/dynamicWidgets.ts b/src/core/graph/widgets/dynamicWidgets.ts index d93f3abf91..30c20ee7f6 100644 --- a/src/core/graph/widgets/dynamicWidgets.ts +++ b/src/core/graph/widgets/dynamicWidgets.ts @@ -1,8 +1,8 @@ import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode' import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration' - import type { ComboInputSpec, InputSpec } from '@/schemas/nodeDefSchema' import { zDynamicComboInputSpec } from '@/schemas/nodeDefSchema' +import { useLitegraphService } from '@/services/litegraphService' import { app } from '@/scripts/app' import type { ComfyApp } from '@/scripts/app' @@ -13,6 +13,7 @@ function COMFY_DYNAMICCOMBO_V3( appArg: ComfyApp, widgetName?: string ) { + const { addNodeInput } = useLitegraphService() const parseResult = zDynamicComboInputSpec.safeParse(untypedInputData) if (!parseResult.success) throw new Error('invalid DynamicCombo spec') const inputData = parseResult.data @@ -60,8 +61,8 @@ function COMFY_DYNAMICCOMBO_V3( ] for (const [inputType, isOptional] of inputTypes) for (const name in inputType ?? {}) { - //@ts-expect-error temporary duck violence - node._addInput( + addNodeInput( + node, transformInputSpecV1ToV2(inputType![name], { name, isOptional diff --git a/src/services/litegraphService.ts b/src/services/litegraphService.ts index e804769182..d8045f16d0 100644 --- a/src/services/litegraphService.ts +++ b/src/services/litegraphService.ts @@ -72,7 +72,155 @@ export const useLitegraphService = () => { const canvasStore = useCanvasStore() const { toggleSelectedNodesMode } = useSelectedLiteGraphItems() - // TODO: Dedupe `registerNodeDef`; this should remain synchronous. + /** + * @internal The key for the node definition in the i18n file. + */ + function nodeKey(node: LGraphNode): string { + return `nodeDefs.${normalizeI18nKey(node.constructor.nodeData!.name)}` + } + /** + * @internal Add input sockets to the node. (No widget) + */ + function addInputSocket(node: LGraphNode, inputSpec: InputSpec) { + const inputName = inputSpec.name + const nameKey = `${nodeKey(node)}.inputs.${normalizeI18nKey(inputName)}.name` + const widgetConstructor = widgetStore.widgets.get( + inputSpec.widgetType ?? inputSpec.type + ) + if (widgetConstructor && !inputSpec.forceInput) return + + node.addInput(inputName, inputSpec.type, { + shape: inputSpec.isOptional ? RenderShape.HollowCircle : undefined, + localized_name: st(nameKey, inputName) + }) + } + /** + * @internal Setup stroke styles for the node under various conditions. + */ + function setupStrokeStyles(node: LGraphNode) { + node.strokeStyles['running'] = function (this: LGraphNode) { + const nodeId = String(this.id) + const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(nodeId) + const state = + useExecutionStore().nodeLocationProgressStates[nodeLocatorId]?.state + if (state === 'running') { + return { color: '#0f0' } + } + } + node.strokeStyles['nodeError'] = function (this: LGraphNode) { + if (app.lastNodeErrors?.[this.id]?.errors) { + return { color: 'red' } + } + } + node.strokeStyles['dragOver'] = function (this: LGraphNode) { + if (app.dragOverNode?.id == this.id) { + return { color: 'dodgerblue' } + } + } + node.strokeStyles['executionError'] = function (this: LGraphNode) { + if (app.lastExecutionError?.node_id == this.id) { + return { color: '#f0f', lineWidth: 2 } + } + } + } + + /** + * Utility function. Implemented for use with dynamic widgets + */ + function addNodeInput(node: LGraphNode, inputSpec: InputSpec) { + addInputSocket(node, inputSpec) + addInputWidget(node, inputSpec) + } + + /** + * @internal Add a widget to the node. For both primitive types and custom widgets + * (unless `socketless`), an input socket is also added. + */ + function addInputWidget(node: LGraphNode, inputSpec: InputSpec) { + const widgetInputSpec = { ...inputSpec } + if (inputSpec.widgetType) { + widgetInputSpec.type = inputSpec.widgetType + } + const inputName = inputSpec.name + const nameKey = `${nodeKey(node)}.inputs.${normalizeI18nKey(inputName)}.name` + const widgetConstructor = widgetStore.widgets.get(widgetInputSpec.type) + if (!widgetConstructor || inputSpec.forceInput) return + + const { widget } = + widgetConstructor( + node, + inputName, + transformInputSpecV2ToV1(widgetInputSpec), + app + ) ?? {} + + if (widget) { + widget.label = st(nameKey, widget.label ?? inputName) + widget.options ??= {} + Object.assign(widget.options, { + advanced: inputSpec.advanced, + hidden: inputSpec.hidden + }) + } + + if (!widget?.options?.socketless) { + const inputSpecV1 = transformInputSpecV2ToV1(widgetInputSpec) + node.addInput(inputName, inputSpec.type, { + shape: inputSpec.isOptional ? RenderShape.HollowCircle : undefined, + localized_name: st(nameKey, inputName), + widget: { name: inputName, [GET_CONFIG]: () => inputSpecV1 } + }) + } + } + + /** + * @internal Add inputs to the node. + */ + function addInputs(node: LGraphNode, inputs: Record) { + // Use input_order if available to ensure consistent widget ordering + //@ts-expect-error was ComfyNode.nodeData as ComfyNodeDefImpl + const nodeDefImpl = node.constructor.nodeData as ComfyNodeDefImpl + const orderedInputSpecs = getOrderedInputSpecs(nodeDefImpl, inputs) + + // Create sockets and widgets in the determined order + for (const inputSpec of orderedInputSpecs) addInputSocket(node, inputSpec) + for (const inputSpec of orderedInputSpecs) addInputWidget(node, inputSpec) + } + + /** + * @internal Add outputs to the node. + */ + function addOutputs(node: LGraphNode, outputs: OutputSpec[]) { + for (const output of outputs) { + const { name, type, is_list } = output + const shapeOptions = is_list ? { shape: LiteGraph.GRID_SHAPE } : {} + const nameKey = `${nodeKey(node)}.outputs.${output.index}.name` + const typeKey = `dataTypes.${normalizeI18nKey(type)}` + const outputOptions = { + ...shapeOptions, + // If the output name is different from the output type, use the output name. + // e.g. + // - type ("INT"); name ("Positive") => translate name + // - type ("FLOAT"); name ("FLOAT") => translate type + localized_name: type !== name ? st(nameKey, name) : st(typeKey, name) + } + node.addOutput(name, type, outputOptions) + } + } + + /** + * @internal Set the initial size of the node. + */ + function setInitialSize(node: LGraphNode) { + const s = node.computeSize() + // Expand the width a little to fit widget values on screen. + const pad = + node.widgets?.length && + !useSettingStore().get('LiteGraph.Node.DefaultPadding') + s[0] = s[0] + (pad ? 60 : 0) + node.setSize(s) + } + function registerSubgraphNodeDef( nodeDefV1: ComfyNodeDefV1, subgraph: Subgraph, @@ -84,17 +232,6 @@ export const useLitegraphService = () => { static override category: string static nodeData: ComfyNodeDefV1 & ComfyNodeDefV2 - /** - * @internal The initial minimum size of the node. - */ - #initialMinSize = { width: 1, height: 1 } - /** - * @internal The key for the node definition in the i18n file. - */ - get #nodeKey(): string { - return `nodeDefs.${normalizeI18nKey(ComfyNode.nodeData.name)}` - } - constructor() { super(app.graph, subgraph, instanceData) @@ -129,165 +266,14 @@ export const useLitegraphService = () => { } }) - this.#setupStrokeStyles() - this.#addInputs(ComfyNode.nodeData.inputs) - this.#addOutputs(ComfyNode.nodeData.outputs) - this.#setInitialSize() + setupStrokeStyles(this) + addInputs(this, ComfyNode.nodeData.inputs) + addOutputs(this, ComfyNode.nodeData.outputs) + setInitialSize(this) this.serialize_widgets = true void extensionService.invokeExtensionsAsync('nodeCreated', this) } - /** - * @internal Setup stroke styles for the node under various conditions. - */ - #setupStrokeStyles() { - this.strokeStyles['running'] = function (this: LGraphNode) { - const nodeId = String(this.id) - const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(nodeId) - const state = - useExecutionStore().nodeLocationProgressStates[nodeLocatorId]?.state - if (state === 'running') { - return { color: '#0f0' } - } - } - this.strokeStyles['nodeError'] = function (this: LGraphNode) { - if (app.lastNodeErrors?.[this.id]?.errors) { - return { color: 'red' } - } - } - this.strokeStyles['dragOver'] = function (this: LGraphNode) { - if (app.dragOverNode?.id == this.id) { - return { color: 'dodgerblue' } - } - } - this.strokeStyles['executionError'] = function (this: LGraphNode) { - if (app.lastExecutionError?.node_id == this.id) { - return { color: '#f0f', lineWidth: 2 } - } - } - } - - /** - * @internal Add input sockets to the node. (No widget) - */ - #addInputSocket(inputSpec: InputSpec) { - const inputName = inputSpec.name - const nameKey = `${this.#nodeKey}.inputs.${normalizeI18nKey(inputName)}.name` - const widgetConstructor = widgetStore.widgets.get( - inputSpec.widgetType ?? inputSpec.type - ) - if (widgetConstructor && !inputSpec.forceInput) return - - this.addInput(inputName, inputSpec.type, { - shape: inputSpec.isOptional ? RenderShape.HollowCircle : undefined, - localized_name: st(nameKey, inputName) - }) - } - - /** - * @internal Add a widget to the node. For both primitive types and custom widgets - * (unless `socketless`), an input socket is also added. - */ - #addInputWidget(inputSpec: InputSpec) { - const widgetInputSpec = { ...inputSpec } - if (inputSpec.widgetType) { - widgetInputSpec.type = inputSpec.widgetType - } - const inputName = inputSpec.name - const nameKey = `${this.#nodeKey}.inputs.${normalizeI18nKey(inputName)}.name` - const widgetConstructor = widgetStore.widgets.get(widgetInputSpec.type) - if (!widgetConstructor || inputSpec.forceInput) return - - const { - widget, - minWidth = 1, - minHeight = 1 - } = widgetConstructor( - this, - inputName, - transformInputSpecV2ToV1(widgetInputSpec), - app - ) ?? {} - - if (widget) { - widget.label = st(nameKey, widget.label ?? inputName) - widget.options ??= {} - Object.assign(widget.options, { - advanced: inputSpec.advanced, - hidden: inputSpec.hidden - }) - } - - if (!widget?.options?.socketless) { - const inputSpecV1 = transformInputSpecV2ToV1(widgetInputSpec) - this.addInput(inputName, inputSpec.type, { - shape: inputSpec.isOptional ? RenderShape.HollowCircle : undefined, - localized_name: st(nameKey, inputName), - widget: { name: inputName, [GET_CONFIG]: () => inputSpecV1 } - }) - } - - this.#initialMinSize.width = Math.max( - this.#initialMinSize.width, - minWidth - ) - this.#initialMinSize.height = Math.max( - this.#initialMinSize.height, - minHeight - ) - } - - /** - * @internal Add inputs to the node. - */ - #addInputs(inputs: Record) { - // Use input_order if available to ensure consistent widget ordering - const nodeDefImpl = ComfyNode.nodeData as ComfyNodeDefImpl - const orderedInputSpecs = getOrderedInputSpecs(nodeDefImpl, inputs) - - // Create sockets and widgets in the determined order - for (const inputSpec of orderedInputSpecs) - this.#addInputSocket(inputSpec) - for (const inputSpec of orderedInputSpecs) - this.#addInputWidget(inputSpec) - } - - /** - * @internal Add outputs to the node. - */ - #addOutputs(outputs: OutputSpec[]) { - for (const output of outputs) { - const { name, type, is_list } = output - const shapeOptions = is_list ? { shape: LiteGraph.GRID_SHAPE } : {} - const nameKey = `${this.#nodeKey}.outputs.${output.index}.name` - const typeKey = `dataTypes.${normalizeI18nKey(type)}` - const outputOptions = { - ...shapeOptions, - // If the output name is different from the output type, use the output name. - // e.g. - // - type ("INT"); name ("Positive") => translate name - // - type ("FLOAT"); name ("FLOAT") => translate type - localized_name: - type !== name ? st(nameKey, name) : st(typeKey, name) - } - this.addOutput(name, type, outputOptions) - } - } - - /** - * @internal Set the initial size of the node. - */ - #setInitialSize() { - const s = this.computeSize() - // Expand the width a little to fit widget values on screen. - const pad = - this.widgets?.length && - !useSettingStore().get('LiteGraph.Node.DefaultPadding') - s[0] = Math.max(this.#initialMinSize.width, s[0] + (pad ? 60 : 0)) - s[1] = Math.max(this.#initialMinSize.height, s[1]) - this.setSize(s) - } - /** * Configure the node from a serialised node. Keep 'name', 'type', 'shape', * and 'localized_name' information from the original node definition. @@ -374,23 +360,12 @@ export const useLitegraphService = () => { static override category: string static nodeData: ComfyNodeDefV1 & ComfyNodeDefV2 - /** - * @internal The initial minimum size of the node. - */ - #initialMinSize = { width: 1, height: 1 } - /** - * @internal The key for the node definition in the i18n file. - */ - get #nodeKey(): string { - return `nodeDefs.${normalizeI18nKey(ComfyNode.nodeData.name)}` - } - constructor(title: string) { super(title) - this.#setupStrokeStyles() - this.#addInputs(ComfyNode.nodeData.inputs) - this.#addOutputs(ComfyNode.nodeData.outputs) - this.#setInitialSize() + setupStrokeStyles(this) + addInputs(this, ComfyNode.nodeData.inputs) + addOutputs(this, ComfyNode.nodeData.outputs) + setInitialSize(this) this.serialize_widgets = true // Mark API Nodes yellow by default to distinguish with other nodes. @@ -402,173 +377,6 @@ export const useLitegraphService = () => { void extensionService.invokeExtensionsAsync('nodeCreated', this) } - /** - * @internal Setup stroke styles for the node under various conditions. - */ - #setupStrokeStyles() { - this.strokeStyles['running'] = function (this: LGraphNode) { - const nodeId = String(this.id) - const nodeLocatorId = useWorkflowStore().nodeIdToNodeLocatorId(nodeId) - const state = - useExecutionStore().nodeLocationProgressStates[nodeLocatorId]?.state - if (state === 'running') { - return { color: '#0f0' } - } - } - this.strokeStyles['nodeError'] = function (this: LGraphNode) { - if (app.lastNodeErrors?.[this.id]?.errors) { - return { color: 'red' } - } - } - this.strokeStyles['dragOver'] = function (this: LGraphNode) { - if (app.dragOverNode?.id == this.id) { - return { color: 'dodgerblue' } - } - } - this.strokeStyles['executionError'] = function (this: LGraphNode) { - if (app.lastExecutionError?.node_id == this.id) { - return { color: '#f0f', lineWidth: 2 } - } - } - } - - /** - * @internal Add input sockets to the node. (No widget) - */ - #addInputSocket(inputSpec: InputSpec) { - const inputName = inputSpec.name - const nameKey = `${this.#nodeKey}.inputs.${normalizeI18nKey(inputName)}.name` - const widgetConstructor = widgetStore.widgets.get( - inputSpec.widgetType ?? inputSpec.type - ) - if (widgetConstructor && !inputSpec.forceInput) return - - this.addInput(inputName, inputSpec.type, { - shape: inputSpec.isOptional ? RenderShape.HollowCircle : undefined, - localized_name: st(nameKey, inputName) - }) - } - - /** - * @internal Add a widget to the node. For both primitive types and custom widgets - * (unless `socketless`), an input socket is also added. - */ - #addInputWidget(inputSpec: InputSpec) { - const widgetInputSpec = { ...inputSpec } - if (inputSpec.widgetType) { - widgetInputSpec.type = inputSpec.widgetType - } - const inputName = inputSpec.name - const nameKey = `${this.#nodeKey}.inputs.${normalizeI18nKey(inputName)}.name` - const widgetConstructor = widgetStore.widgets.get(widgetInputSpec.type) - if (!widgetConstructor || inputSpec.forceInput) return - - const { - widget, - minWidth = 1, - minHeight = 1 - } = widgetConstructor( - this, - inputName, - transformInputSpecV2ToV1(widgetInputSpec), - app - ) ?? {} - - if (widget) { - // Check if this is an Asset Browser button widget - const isAssetBrowserButton = - widget.type === 'button' && widget.value === 'Select model' - - if (isAssetBrowserButton) { - // Preserve Asset Browser button label (don't translate) - widget.label = String(widget.value) - } else { - // Apply normal translation for other widgets - widget.label = st(nameKey, widget.label ?? inputName) - } - - widget.options ??= {} - Object.assign(widget.options, { - advanced: inputSpec.advanced, - hidden: inputSpec.hidden - }) - } - - if (!widget?.options?.socketless) { - const inputSpecV1 = transformInputSpecV2ToV1(widgetInputSpec) - this.addInput(inputName, inputSpec.type, { - shape: inputSpec.isOptional ? RenderShape.HollowCircle : undefined, - localized_name: st(nameKey, inputName), - widget: { name: inputName, [GET_CONFIG]: () => inputSpecV1 } - }) - } - - this.#initialMinSize.width = Math.max( - this.#initialMinSize.width, - minWidth - ) - this.#initialMinSize.height = Math.max( - this.#initialMinSize.height, - minHeight - ) - } - - /** - * @internal Add inputs to the node. - */ - #addInputs(inputs: Record) { - // Use input_order if available to ensure consistent widget ordering - const nodeDefImpl = ComfyNode.nodeData as ComfyNodeDefImpl - const orderedInputSpecs = getOrderedInputSpecs(nodeDefImpl, inputs) - - // Create sockets and widgets in the determined order - for (const inputSpec of orderedInputSpecs) - this.#addInputSocket(inputSpec) - for (const inputSpec of orderedInputSpecs) - this.#addInputWidget(inputSpec) - } - - _addInput(inputSpec: InputSpec) { - this.#addInputSocket(inputSpec) - this.#addInputWidget(inputSpec) - } - - /** - * @internal Add outputs to the node. - */ - #addOutputs(outputs: OutputSpec[]) { - for (const output of outputs) { - const { name, type, is_list } = output - const shapeOptions = is_list ? { shape: LiteGraph.GRID_SHAPE } : {} - const nameKey = `${this.#nodeKey}.outputs.${output.index}.name` - const typeKey = `dataTypes.${normalizeI18nKey(type)}` - const outputOptions = { - ...shapeOptions, - // If the output name is different from the output type, use the output name. - // e.g. - // - type ("INT"); name ("Positive") => translate name - // - type ("FLOAT"); name ("FLOAT") => translate type - localized_name: - type !== name ? st(nameKey, name) : st(typeKey, name) - } - this.addOutput(name, type, outputOptions) - } - } - - /** - * @internal Set the initial size of the node. - */ - #setInitialSize() { - const s = this.computeSize() - // Expand the width a little to fit widget values on screen. - const pad = - this.widgets?.length && - !useSettingStore().get('LiteGraph.Node.DefaultPadding') - s[0] = Math.max(this.#initialMinSize.width, s[0] + (pad ? 60 : 0)) - s[1] = Math.max(this.#initialMinSize.height, s[1]) - this.setSize(s) - } - /** * Configure the node from a serialised node. Keep 'name', 'type', 'shape', * and 'localized_name' information from the original node definition. @@ -1036,6 +844,7 @@ export const useLitegraphService = () => { registerNodeDef, registerSubgraphNodeDef, addNodeOnGraph, + addNodeInput, getCanvasCenter, goToNode, resetView, From 5a28c64c56607cf527fe77de53e252ce943ec01a Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 18 Nov 2025 10:17:28 -0800 Subject: [PATCH 15/21] Reimplement initialMinSize The functionality is dumb and wrong, but required to keep expected sizing behaviour for nodes like Clip Text Encode --- src/services/litegraphService.ts | 48 +++++++++++++++++++++++++------- 1 file changed, 38 insertions(+), 10 deletions(-) diff --git a/src/services/litegraphService.ts b/src/services/litegraphService.ts index d8045f16d0..c11650a3a7 100644 --- a/src/services/litegraphService.ts +++ b/src/services/litegraphService.ts @@ -59,6 +59,10 @@ import { getOrderedInputSpecs } from '@/workbench/utils/nodeDefOrderingUtil' import { useExtensionService } from './extensionService' +interface HasInitialMinSize { + _initialMinSize: { width: number; height: number } +} + export const CONFIG = Symbol() export const GET_CONFIG = Symbol() @@ -146,13 +150,16 @@ export const useLitegraphService = () => { const widgetConstructor = widgetStore.widgets.get(widgetInputSpec.type) if (!widgetConstructor || inputSpec.forceInput) return - const { widget } = - widgetConstructor( - node, - inputName, - transformInputSpecV2ToV1(widgetInputSpec), - app - ) ?? {} + const { + widget, + minWidth = 1, + minHeight = 1 + } = widgetConstructor( + node, + inputName, + transformInputSpecV2ToV1(widgetInputSpec), + app + ) ?? {} if (widget) { widget.label = st(nameKey, widget.label ?? inputName) @@ -171,6 +178,15 @@ export const useLitegraphService = () => { widget: { name: inputName, [GET_CONFIG]: () => inputSpecV1 } }) } + const castedNode = node as LGraphNode & HasInitialMinSize + castedNode._initialMinSize.width = Math.max( + castedNode._initialMinSize.width, + minWidth + ) + castedNode._initialMinSize.height = Math.max( + castedNode._initialMinSize.height, + minHeight + ) } /** @@ -217,7 +233,9 @@ export const useLitegraphService = () => { const pad = node.widgets?.length && !useSettingStore().get('LiteGraph.Node.DefaultPadding') - s[0] = s[0] + (pad ? 60 : 0) + const castedNode = node as LGraphNode & HasInitialMinSize + s[0] = Math.max(castedNode._initialMinSize.width, s[0] + (pad ? 60 : 0)) + s[1] = Math.max(castedNode._initialMinSize.height, s[1]) node.setSize(s) } @@ -226,12 +244,17 @@ export const useLitegraphService = () => { subgraph: Subgraph, instanceData: ExportedSubgraphInstance ) { - const node = class ComfyNode extends SubgraphNode { + const node = class ComfyNode + extends SubgraphNode + implements HasInitialMinSize + { static comfyClass: string static override title: string static override category: string static nodeData: ComfyNodeDefV1 & ComfyNodeDefV2 + _initialMinSize = { width: 1, height: 1 } + constructor() { super(app.graph, subgraph, instanceData) @@ -354,12 +377,17 @@ export const useLitegraphService = () => { } async function registerNodeDef(nodeId: string, nodeDefV1: ComfyNodeDefV1) { - const node = class ComfyNode extends LGraphNode { + const node = class ComfyNode + extends LGraphNode + implements HasInitialMinSize + { static comfyClass: string static override title: string static override category: string static nodeData: ComfyNodeDefV1 & ComfyNodeDefV2 + _initialMinSize = { width: 1, height: 1 } + constructor(title: string) { super(title) setupStrokeStyles(this) From 699cec632e361ee28f92a091aace6962c51a1b45 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 18 Nov 2025 14:56:06 -0800 Subject: [PATCH 16/21] Add tests --- tests-ui/tests/widgets/dynamicCombo.test.ts | 90 +++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 tests-ui/tests/widgets/dynamicCombo.test.ts diff --git a/tests-ui/tests/widgets/dynamicCombo.test.ts b/tests-ui/tests/widgets/dynamicCombo.test.ts new file mode 100644 index 0000000000..751841e098 --- /dev/null +++ b/tests-ui/tests/widgets/dynamicCombo.test.ts @@ -0,0 +1,90 @@ +import { createPinia, setActivePinia } from 'pinia' +import { describe, expect, test } from 'vitest' +import { LGraphNode } from '@/lib/litegraph/src/litegraph' +import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration' +import type { InputSpec } from '@/schemas/nodeDefSchema' +import { useLitegraphService } from '@/services/litegraphService' + +setActivePinia(createPinia()) +type DynamicInputs = ('INT' | 'STRING' | 'IMAGE' | DynamicInputs)[][] + +const { addNodeInput } = useLitegraphService() + +function addDynamicCombo(node: LGraphNode, inputs: DynamicInputs) { + const namePrefix = `${node.widgets?.length ?? 0}` + function getSpec( + inputs: DynamicInputs, + depth: number = 0 + ): { key: string; inputs: object }[] { + return inputs.map((group, groupIndex) => { + const inputs = group.map((input, inputIndex) => [ + `${namePrefix}.${depth}.${inputIndex}`, + Array.isArray(input) + ? ['COMFY_DYNAMICCOMBO_V3', { options: getSpec(input, depth + 1) }] + : [input, {}] + ]) + return { + key: `${groupIndex}`, + inputs: { required: Object.fromEntries(inputs) } + } + }) + } + const inputSpec: Required = [ + 'COMFY_DYNAMICCOMBO_V3', + { options: getSpec(inputs) } + ] + addNodeInput( + node, + transformInputSpecV1ToV2(inputSpec, { name: namePrefix, isOptional: false }) + ) +} +function testNode() { + const node: LGraphNode & { + _initialMinSize?: { width: number; height: number } + } = new LGraphNode('test') + node.widgets = [] + node._initialMinSize = { width: 1, height: 1 } + node.constructor.nodeData = { + name: 'testnode' + } as typeof node.constructor.nodeData + return node as LGraphNode & Required> +} + +describe('Dynamic Combos', () => { + test('Can add widget on selection', () => { + const node = testNode() + addDynamicCombo(node, [['INT'], ['INT', 'STRING']]) + expect(node.widgets.length).toBe(2) + node.widgets[0].value = '1' + expect(node.widgets.length).toBe(3) + }) + test('Can add nested widgets', () => { + const node = testNode() + addDynamicCombo(node, [['INT'], [[[], ['STRING']]]]) + expect(node.widgets.length).toBe(2) + node.widgets[0].value = '1' + expect(node.widgets.length).toBe(2) + node.widgets[1].value = '1' + expect(node.widgets.length).toBe(3) + }) + test('Can add input', () => { + const node = testNode() + addDynamicCombo(node, [['INT'], ['IMAGE']]) + expect(node.widgets.length).toBe(2) + node.widgets[0].value = '1' + expect(node.widgets.length).toBe(1) + expect(node.inputs.length).toBe(2) + expect(node.inputs[1].type).toBe('IMAGE') + }) + test('Dynamically added inputs are well ordered', () => { + const node = testNode() + addDynamicCombo(node, [['INT'], ['IMAGE']]) + addDynamicCombo(node, [['INT'], ['IMAGE']]) + node.widgets[2].value = '1' + node.widgets[0].value = '1' + expect(node.widgets.length).toBe(2) + expect(node.inputs.length).toBe(4) + expect(node.inputs[1].name).toBe('0.0.0') + expect(node.inputs[3].name).toBe('2.0.0') + }) +}) From 0d8338ff2a2fe1e4125ae297c98928bef8ce1bd5 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 18 Nov 2025 15:29:23 -0800 Subject: [PATCH 17/21] Potential test case fix --- src/lib/litegraph/src/LGraphNode.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index a1aeb5549a..574c8509f4 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -849,7 +849,7 @@ export class LGraphNode .values() .filter((w) => w.serialize !== false) widgetsWithValue.forEach((widget, i) => { - if (widget) { + if (widget && i < info.widgets_values!.length) { widget.value = info.widgets_values![i] } }) From 61c5bfa670c82a5dce1b6a74a38220e335864f7e Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 18 Nov 2025 16:07:09 -0800 Subject: [PATCH 18/21] Workaround for edge case with out of order inputs --- src/core/graph/widgets/dynamicWidgets.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/core/graph/widgets/dynamicWidgets.ts b/src/core/graph/widgets/dynamicWidgets.ts index 30c20ee7f6..7113abe6cb 100644 --- a/src/core/graph/widgets/dynamicWidgets.ts +++ b/src/core/graph/widgets/dynamicWidgets.ts @@ -84,12 +84,14 @@ function COMFY_DYNAMICCOMBO_V3( } const addedInputs = node .spliceInputs(startingInputLength) - .filter( - (addedInput) => - !node.inputs.some( - (existingInput) => addedInput.name === existingInput.name - ) - ) + .map((addedInput) => { + const existingInput = node.inputs.findIndex( + (existingInput) => addedInput.name === existingInput.name + ) + return existingInput === -1 + ? addedInput + : node.spliceInputs(existingInput, 1)[0] + }) //assume existing inputs are in correct order node.spliceInputs(inputInsertionPoint, 0, ...addedInputs) node.size[1] = node.computeSize([...node.size])[1] From 756e39149bca7d950a0f3df86d1ad8f9b8299e85 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 18 Nov 2025 16:09:46 -0800 Subject: [PATCH 19/21] Update comments --- src/core/graph/widgets/dynamicWidgets.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/core/graph/widgets/dynamicWidgets.ts b/src/core/graph/widgets/dynamicWidgets.ts index 7113abe6cb..02b33d16a1 100644 --- a/src/core/graph/widgets/dynamicWidgets.ts +++ b/src/core/graph/widgets/dynamicWidgets.ts @@ -33,6 +33,7 @@ function COMFY_DYNAMICCOMBO_V3( if (!node.widgets) throw new Error('Not Reachable') const newSpec = value ? options[value] : undefined //TODO: Calculate intersection for widgets that persist across options + //This would potentially allow links to be retained for (const name of currentDynamicNames) { const inputIndex = node.inputs.findIndex((input) => input.name === name) if (inputIndex !== -1) node.removeInput(inputIndex) @@ -53,8 +54,6 @@ function COMFY_DYNAMICCOMBO_V3( const startingInputLength = node.inputs.length if (insertionPoint === 0) throw new Error("Dynamic widget doesn't exist on node") - //FIXME: inputs MUST be well ordered - //FIXME check for duplicates const inputTypes: [Record | undefined, boolean][] = [ [newSpec.required, false], [newSpec.optional, true] From 6d0712a2403b7840bfe4d91ab4aa30c5b22e0d9a Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Tue, 18 Nov 2025 19:14:23 -0800 Subject: [PATCH 20/21] nits --- src/core/graph/widgets/dynamicWidgets.ts | 4 ++-- src/lib/litegraph/src/LGraphNode.ts | 24 ++++++++++----------- src/services/litegraphService.ts | 2 +- tests-ui/tests/widgets/dynamicCombo.test.ts | 10 ++++----- 4 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/core/graph/widgets/dynamicWidgets.ts b/src/core/graph/widgets/dynamicWidgets.ts index 02b33d16a1..a113881ebf 100644 --- a/src/core/graph/widgets/dynamicWidgets.ts +++ b/src/core/graph/widgets/dynamicWidgets.ts @@ -6,7 +6,7 @@ import { useLitegraphService } from '@/services/litegraphService' import { app } from '@/scripts/app' import type { ComfyApp } from '@/scripts/app' -function COMFY_DYNAMICCOMBO_V3( +function dynamicComboWidget( node: LGraphNode, inputName: string, untypedInputData: InputSpec, @@ -111,4 +111,4 @@ function COMFY_DYNAMICCOMBO_V3( return { widget, minWidth, minHeight } } -export const dynamicWidgets = { COMFY_DYNAMICCOMBO_V3 } +export const dynamicWidgets = { COMFY_DYNAMICCOMBO_V3: dynamicComboWidget } diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index 574c8509f4..a449e93255 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -847,12 +847,13 @@ export class LGraphNode if (info.widgets_values) { const widgetsWithValue = this.widgets .values() - .filter((w) => w.serialize !== false) - widgetsWithValue.forEach((widget, i) => { - if (widget && i < info.widgets_values!.length) { - widget.value = info.widgets_values![i] - } - }) + .filter( + (w, idx) => + w.serialize !== false && idx < info.widgets_values!.length + ) + widgetsWithValue.forEach( + (widget, i) => (widget.value = info.widgets_values![i]) + ) } } @@ -1653,15 +1654,12 @@ export class LGraphNode deleteCount = -1, ...toAdd: INodeInputSlot[] ): INodeInputSlot[] { - if (deleteCount === -1) deleteCount = this.inputs.length - startIndex - - const lengthDelta = toAdd.length - deleteCount + if (deleteCount < 0) return this.inputs.splice(startIndex) const ret = this.inputs.splice(startIndex, deleteCount, ...toAdd) - for (const input of this.inputs.slice(startIndex + toAdd.length)) { + this.inputs.slice(startIndex).forEach((input, index) => { const link = input.link && this.graph?.links?.get(input.link) - if (!link) continue - link.target_slot += lengthDelta - } + if (link) link.target_slot = startIndex + index + }) return ret } diff --git a/src/services/litegraphService.ts b/src/services/litegraphService.ts index c11650a3a7..ff9bcadfe2 100644 --- a/src/services/litegraphService.ts +++ b/src/services/litegraphService.ts @@ -59,7 +59,7 @@ import { getOrderedInputSpecs } from '@/workbench/utils/nodeDefOrderingUtil' import { useExtensionService } from './extensionService' -interface HasInitialMinSize { +export interface HasInitialMinSize { _initialMinSize: { width: number; height: number } } diff --git a/tests-ui/tests/widgets/dynamicCombo.test.ts b/tests-ui/tests/widgets/dynamicCombo.test.ts index 751841e098..3e9c539e15 100644 --- a/tests-ui/tests/widgets/dynamicCombo.test.ts +++ b/tests-ui/tests/widgets/dynamicCombo.test.ts @@ -1,11 +1,13 @@ -import { createPinia, setActivePinia } from 'pinia' +import { setActivePinia } from 'pinia' +import { createTestingPinia } from '@pinia/testing' import { describe, expect, test } from 'vitest' import { LGraphNode } from '@/lib/litegraph/src/litegraph' import { transformInputSpecV1ToV2 } from '@/schemas/nodeDef/migration' import type { InputSpec } from '@/schemas/nodeDefSchema' import { useLitegraphService } from '@/services/litegraphService' +import type { HasInitialMinSize } from '@/services/litegraphService' -setActivePinia(createPinia()) +setActivePinia(createTestingPinia()) type DynamicInputs = ('INT' | 'STRING' | 'IMAGE' | DynamicInputs)[][] const { addNodeInput } = useLitegraphService() @@ -39,9 +41,7 @@ function addDynamicCombo(node: LGraphNode, inputs: DynamicInputs) { ) } function testNode() { - const node: LGraphNode & { - _initialMinSize?: { width: number; height: number } - } = new LGraphNode('test') + const node: LGraphNode & Partial = new LGraphNode('test') node.widgets = [] node._initialMinSize = { width: 1, height: 1 } node.constructor.nodeData = { From 6c0712e3f678b146f7052a864f3b1f519c32cfb0 Mon Sep 17 00:00:00 2001 From: Austin Mroz Date: Wed, 19 Nov 2025 19:52:37 -0800 Subject: [PATCH 21/21] Fix test --- src/lib/litegraph/src/LGraphNode.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index a449e93255..ace5b0bbab 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -847,10 +847,8 @@ export class LGraphNode if (info.widgets_values) { const widgetsWithValue = this.widgets .values() - .filter( - (w, idx) => - w.serialize !== false && idx < info.widgets_values!.length - ) + .filter((w) => w.serialize !== false) + .filter((_w, idx) => idx < info.widgets_values!.length) widgetsWithValue.forEach( (widget, i) => (widget.value = info.widgets_values![i]) )