fix(autocomplete): prevent migrateWidgetsValues from dropping text widget values (#915)

shouldBypassAutocompleteWidgetMigration only matched inputs by widget name,
but ComfyUI's migrateWidgetsValues also matches forceInput inputs (like "seed").
This discrepancy meant the bypass never triggered for TextLM/PromptLM nodes,
causing migrateWidgetsValues to filter out real widget values by incorrectly
mapping forceInput flags onto saved autocomplete values.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Will Miao
2026-04-29 16:44:08 +08:00
parent 055e94d77b
commit f3268a6179
4 changed files with 88 additions and 28 deletions

View File

@@ -176,7 +176,7 @@ onMounted(() => {
;(textareaRef.value as any)._autocompleteHostWidget = props.widget
;(textareaRef.value as any)._autocompleteMetadataWidget = props.widget.metadataWidget
;(textareaRef.value as any)._autocompleteTextWidgetName = props.widget.name ?? 'text'
// Also store on the container element for cloned widgets (subgraph promotion)
// When widgets are promoted to subgraph nodes, the cloned widget shares the same
// DOM element but has its own inputEl property. We store the reference on the
@@ -185,10 +185,24 @@ onMounted(() => {
if (container && (container as any).__widgetInputEl) {
(container as any).__widgetInputEl.inputEl = textareaRef.value
}
// Initialize hasText state
hasText.value = textareaRef.value.value.length > 0
// Apply pending value from setValue if exists (workflow loading before Vue mount)
const pendingValue = (props.widget as any)._pendingValue
if (pendingValue !== undefined) {
textareaRef.value.value = pendingValue
hasText.value = pendingValue.length > 0
delete (props.widget as any)._pendingValue
// Dispatch event to notify autocomplete of value change
textareaRef.value.dispatchEvent(new CustomEvent('lora-manager:autocomplete-value-changed', {
detail: { value: pendingValue }
}))
}
// Initialize hasText state (already done if pendingValue was applied, but safe to re-check)
if (pendingValue === undefined) {
hasText.value = textareaRef.value.value.length > 0
}
// Listen for external value change events from setValue
textareaRef.value.addEventListener('lora-manager:autocomplete-value-changed', onExternalValueChange as EventListener)
}

View File

@@ -432,7 +432,7 @@ function shouldBypassAutocompleteWidgetMigration(
}
const originalWidgetsInputs = Object.values(inputDefs).filter((input: any) =>
widgetNames.has(input.name)
widgetNames.has(input.name) || input.forceInput
)
const widgetIndexHasForceInput = originalWidgetsInputs.flatMap((input: any) =>
@@ -441,10 +441,12 @@ function shouldBypassAutocompleteWidgetMigration(
: [!!input.forceInput]
)
return (
const result = (
widgetIndexHasForceInput.some(Boolean) &&
widgetIndexHasForceInput.length === widgetValues.length
)
return result
}
function remapWidgetValuesByName(
@@ -459,6 +461,7 @@ function remapWidgetValuesByName(
}
})
const currentWidgetNameSet = new Set(currentWidgetNames)
const remappedValues: unknown[] = []
for (const name of currentWidgetNames) {
if (valueByName.has(name)) {
@@ -466,6 +469,18 @@ function remapWidgetValuesByName(
}
}
// Append values for saved widget names that are NOT in the current widget
// list (e.g. forceInput widgets like "seed" that haven't been converted
// back to DOM widgets yet at configure time). Without these, the
// resulting array may accidentally match the length of ComfyUI's
// widgetIndexHasForceInput array, causing migrateWidgetsValues to
// incorrectly filter out the wrong values and drop real widget content.
for (const name of savedWidgetNames) {
if (!currentWidgetNameSet.has(name) && valueByName.has(name)) {
remappedValues.push(valueByName.get(name))
}
}
return remappedValues
}
@@ -498,6 +513,7 @@ function normalizeAutocompleteWidgetValues(node: any, info: any) {
}
const currentWidgetNames = getSerializableWidgetNames(node)
if (currentWidgetNames.length === 0) {
return
}
@@ -615,6 +631,8 @@ function createAutocompleteTextWidgetFactory(
inputEl.dispatchEvent(new CustomEvent('lora-manager:autocomplete-value-changed', {
detail: { value: v ?? '' }
}))
} else {
;(widget as any)._pendingValue = v ?? ''
}
// Also call onSetValue if defined (for Vue component integration)
if (typeof widget.onSetValue === 'function') {
@@ -751,10 +769,16 @@ app.registerExtension({
nodeType.prototype.configure = function (info: any) {
normalizeAutocompleteWidgetValues(this, info)
if (shouldBypassAutocompleteWidgetMigration(this, info?.widgets_values ?? [])) {
const bypassResult = shouldBypassAutocompleteWidgetMigration(this, info?.widgets_values ?? [])
if (bypassResult) {
info.widgets_values = [...(info.widgets_values ?? []), null]
}
return originalConfigure?.apply(this, arguments)
const result = originalConfigure?.apply(this, arguments)
return result
}
}