Compare commits

...

2 Commits

Author SHA1 Message Date
Marwan Ahmed
97922b96de fix: respect control_after_generate setting on API nodes
For API nodes, the backend schema defines control_after_generate as an
explicit COMBO input alongside the seed. This caused two widgets to exist:
a hidden functional ACW (created automatically for seed inputs) and a
visible CCW (from the node definition). The user's dropdown selection
only updated the CCW, while the ACW—which actually drives execution—
always defaulted to 'randomize', causing the seed to change on every run
regardless of the selected mode.

- applyWidgetControl now syncs CCW → ACW at queue time via shared helper
- getControlWidget uses CCW as the display source and writes through to
  both on update, so the seed panel and node widget stay in sync
- WidgetWithControl adds a reverse watch to reflect loaded workflow state
- Extract findExternalControlWidget to avoid predicate duplication
- Reuse isControlOption from simplifiedWidget.ts instead of local duplicate
2026-04-22 04:15:01 +02:00
Marwan Ahmed
b96a8e0d0b fix: respect control_after_generate setting on API nodes
For API nodes, the backend schema defines control_after_generate as an
explicit COMBO input alongside the seed. This caused two widgets to exist:
a hidden functional ACW (created automatically for seed inputs) and a
visible CCW (from the node definition). The user's dropdown selection
only updated the CCW, while the ACW—which actually drives execution—
always defaulted to 'randomize', causing the seed to change on every run
regardless of the selected mode.

- applyWidgetControl now syncs CCW → ACW at queue time
- getControlWidget uses CCW as the display source and writes through to
  both on update, so the seed panel and node widget stay in sync
- WidgetWithControl adds a reverse watch to reflect loaded workflow state
2026-04-22 01:04:25 +02:00
5 changed files with 63 additions and 14 deletions

View File

@@ -87,7 +87,7 @@ const simplifiedWidget = computed((): SimplifiedWidget => {
label: widgetState?.label ?? widget.label,
options: widgetState?.options ?? widget.options,
spec: nodeDefStore.getInputSpecForWidget(sourceNode, sourceWidget.name),
controlWidget: getControlWidget(sourceWidget)
controlWidget: getControlWidget(sourceWidget, sourceNode)
}
})

View File

@@ -26,6 +26,7 @@ import { isDOMWidget } from '@/scripts/domWidget'
import { useNodeDefStore } from '@/stores/nodeDefStore'
import type { WidgetValue, SafeControlWidget } from '@/types/simplifiedWidget'
import { normalizeControlOption } from '@/types/simplifiedWidget'
import { IS_CONTROL_WIDGET, findExternalControlWidget } from '@/scripts/widgets'
import type {
LGraph,
@@ -150,15 +151,28 @@ function isPromotedDOMWidget(widget: IBaseWidget): boolean {
}
export function getControlWidget(
widget: IBaseWidget
widget: IBaseWidget,
node?: LGraphNode
): SafeControlWidget | undefined {
const cagWidget = widget.linkedWidgets?.find(
(w) => w.name == 'control_after_generate'
)
// Prefer the marker symbol so group/primitive nodes that prefix the widget
// name (e.g. "KSampler control_after_generate") still resolve.
const cagWidget =
widget.linkedWidgets?.find((w) => w[IS_CONTROL_WIDGET]) ??
widget.linkedWidgets?.find((w) => w.name === 'control_after_generate')
if (!cagWidget) return
const externalControl = node
? findExternalControlWidget(node, cagWidget)
: undefined
const displayWidget = externalControl ?? cagWidget
return {
value: normalizeControlOption(cagWidget.value),
update: (value) => (cagWidget.value = normalizeControlOption(value))
value: normalizeControlOption(displayWidget.value),
update: (value) => {
const normalized = normalizeControlOption(value)
cagWidget.value = normalized
if (externalControl) externalControl.value = normalized
}
}
}
@@ -174,7 +188,7 @@ function getSharedWidgetEnhancements(
const nodeDefStore = useNodeDefStore()
return {
controlWidget: getControlWidget(widget),
controlWidget: getControlWidget(widget, node),
spec: nodeDefStore.getInputSpecForWidget(node, widget.name)
}
}

View File

@@ -24,6 +24,15 @@ const modelValue = defineModel<T>()
const controlModel = ref(props.widget.controlWidget.value)
watch(controlModel, props.widget.controlWidget.update)
// Sync litegraph → Vue when the external control widget value changes
// (e.g. when a workflow is loaded with a specific control mode set)
watch(
() => props.widget.controlWidget.value,
(newVal) => {
if (controlModel.value !== newVal) controlModel.value = newVal
}
)
</script>
<template>
<div class="relative grid grid-cols-subgrid">

View File

@@ -7,6 +7,7 @@ import type {
} from '@/lib/litegraph/src/types/widgets'
import { useSettingStore } from '@/platform/settings/settingStore'
import { dynamicWidgets } from '@/core/graph/widgets/dynamicWidgets'
import { isControlOption } from '@/types/simplifiedWidget'
import { useBooleanWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useBooleanWidget'
import { useBoundingBoxWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useBoundingBoxWidget'
import { useCurveWidget } from '@/renderer/extensions/vueNodes/widgets/composables/useCurveWidget'
@@ -80,6 +81,20 @@ export function updateControlWidgetLabel(widget: IBaseWidget) {
export const IS_CONTROL_WIDGET = Symbol()
const HAS_EXECUTED = Symbol()
/**
* Finds a user-visible control_after_generate widget that is separate from
* the hidden ACW. API nodes define control_after_generate as an explicit
* COMBO input alongside seed, producing two widgets with the same name.
*/
export function findExternalControlWidget(
node: LGraphNode,
cagWidget: IBaseWidget
): IBaseWidget | undefined {
return node.widgets?.find(
(w) => w.name === cagWidget.name && w !== cagWidget && !w[IS_CONTROL_WIDGET]
)
}
export function addValueControlWidget(
node: LGraphNode,
targetWidget: IBaseWidget,
@@ -88,10 +103,11 @@ export function addValueControlWidget(
widgetName?: string,
inputData?: InputSpec
): IComboWidget {
let name = inputData?.[1]?.control_after_generate
if (typeof name !== 'string') {
name = widgetName
}
const rawName = inputData?.[1]?.control_after_generate
const name =
typeof rawName === 'string' && !isControlOption(rawName)
? rawName
: widgetName
const widgets = addValueControlWidgets(
node,
targetWidget,
@@ -119,7 +135,13 @@ export function addValueControlWidgets(
let name = defaultName
if (options[optionName]) {
name = options[optionName]
} else if (typeof inputData?.[1]?.[defaultName] === 'string') {
} else if (
typeof inputData?.[1]?.[defaultName] === 'string' &&
!(
defaultName === 'control_after_generate' &&
isControlOption(inputData[1][defaultName])
)
) {
name = inputData?.[1]?.[defaultName]
} else if (inputData?.[1]?.control_prefix) {
name = inputData?.[1]?.control_prefix + ' ' + name
@@ -175,6 +197,10 @@ export function addValueControlWidgets(
}
const applyWidgetControl = () => {
const externalControl = findExternalControlWidget(node, valueControl)
if (externalControl !== undefined) {
valueControl.value = externalControl.value as string
}
var v = valueControl.value
if (isCombo && v !== 'fixed') {

View File

@@ -24,7 +24,7 @@ export const CONTROL_OPTIONS = [
] as const
export type ControlOptions = (typeof CONTROL_OPTIONS)[number]
function isControlOption(val: WidgetValue): val is ControlOptions {
export function isControlOption(val: WidgetValue): val is ControlOptions {
return CONTROL_OPTIONS.includes(val as ControlOptions)
}