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

@@ -186,8 +186,22 @@ onMounted(() => {
(container as any).__widgetInputEl.inputEl = textareaRef.value (container as any).__widgetInputEl.inputEl = textareaRef.value
} }
// Initialize hasText state // 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 hasText.value = textareaRef.value.value.length > 0
}
// Listen for external value change events from setValue // Listen for external value change events from setValue
textareaRef.value.addEventListener('lora-manager:autocomplete-value-changed', onExternalValueChange as EventListener) 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) => const originalWidgetsInputs = Object.values(inputDefs).filter((input: any) =>
widgetNames.has(input.name) widgetNames.has(input.name) || input.forceInput
) )
const widgetIndexHasForceInput = originalWidgetsInputs.flatMap((input: any) => const widgetIndexHasForceInput = originalWidgetsInputs.flatMap((input: any) =>
@@ -441,10 +441,12 @@ function shouldBypassAutocompleteWidgetMigration(
: [!!input.forceInput] : [!!input.forceInput]
) )
return ( const result = (
widgetIndexHasForceInput.some(Boolean) && widgetIndexHasForceInput.some(Boolean) &&
widgetIndexHasForceInput.length === widgetValues.length widgetIndexHasForceInput.length === widgetValues.length
) )
return result
} }
function remapWidgetValuesByName( function remapWidgetValuesByName(
@@ -459,6 +461,7 @@ function remapWidgetValuesByName(
} }
}) })
const currentWidgetNameSet = new Set(currentWidgetNames)
const remappedValues: unknown[] = [] const remappedValues: unknown[] = []
for (const name of currentWidgetNames) { for (const name of currentWidgetNames) {
if (valueByName.has(name)) { 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 return remappedValues
} }
@@ -498,6 +513,7 @@ function normalizeAutocompleteWidgetValues(node: any, info: any) {
} }
const currentWidgetNames = getSerializableWidgetNames(node) const currentWidgetNames = getSerializableWidgetNames(node)
if (currentWidgetNames.length === 0) { if (currentWidgetNames.length === 0) {
return return
} }
@@ -615,6 +631,8 @@ function createAutocompleteTextWidgetFactory(
inputEl.dispatchEvent(new CustomEvent('lora-manager:autocomplete-value-changed', { inputEl.dispatchEvent(new CustomEvent('lora-manager:autocomplete-value-changed', {
detail: { value: v ?? '' } detail: { value: v ?? '' }
})) }))
} else {
;(widget as any)._pendingValue = v ?? ''
} }
// Also call onSetValue if defined (for Vue component integration) // Also call onSetValue if defined (for Vue component integration)
if (typeof widget.onSetValue === 'function') { if (typeof widget.onSetValue === 'function') {
@@ -751,10 +769,16 @@ app.registerExtension({
nodeType.prototype.configure = function (info: any) { nodeType.prototype.configure = function (info: any) {
normalizeAutocompleteWidgetValues(this, info) 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] info.widgets_values = [...(info.widgets_values ?? []), null]
} }
return originalConfigure?.apply(this, arguments)
const result = originalConfigure?.apply(this, arguments)
return result
} }
} }

View File

@@ -2118,14 +2118,14 @@ to { transform: rotate(360deg);
padding: 20px 0; padding: 20px 0;
} }
.autocomplete-text-widget[data-v-76ce0f19] { .autocomplete-text-widget[data-v-5514bf46] {
background: transparent; background: transparent;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-sizing: border-box; box-sizing: border-box;
} }
.input-wrapper[data-v-76ce0f19] { .input-wrapper[data-v-5514bf46] {
position: relative; position: relative;
flex: 1; flex: 1;
display: flex; display: flex;
@@ -2133,7 +2133,7 @@ to { transform: rotate(360deg);
} }
/* Canvas mode styles (default) - matches built-in comfy-multiline-input */ /* Canvas mode styles (default) - matches built-in comfy-multiline-input */
.text-input[data-v-76ce0f19] { .text-input[data-v-5514bf46] {
flex: 1; flex: 1;
width: 100%; width: 100%;
background-color: var(--comfy-input-bg, #222); background-color: var(--comfy-input-bg, #222);
@@ -2150,7 +2150,7 @@ to { transform: rotate(360deg);
} }
/* Vue DOM mode styles - matches built-in p-textarea in Vue DOM mode */ /* Vue DOM mode styles - matches built-in p-textarea in Vue DOM mode */
.text-input.vue-dom-mode[data-v-76ce0f19] { .text-input.vue-dom-mode[data-v-5514bf46] {
background-color: var(--color-charcoal-400, #313235); background-color: var(--color-charcoal-400, #313235);
color: #fff; color: #fff;
padding: 8px 12px 30px 12px; /* Reserve bottom space for clear button */ padding: 8px 12px 30px 12px; /* Reserve bottom space for clear button */
@@ -2159,12 +2159,12 @@ to { transform: rotate(360deg);
font-size: 12px; font-size: 12px;
font-family: inherit; font-family: inherit;
} }
.text-input[data-v-76ce0f19]:focus { .text-input[data-v-5514bf46]:focus {
outline: none; outline: none;
} }
/* Clear button styles */ /* Clear button styles */
.clear-button[data-v-76ce0f19] { .clear-button[data-v-5514bf46] {
position: absolute; position: absolute;
right: 6px; right: 6px;
bottom: 6px; /* Changed from top to bottom */ bottom: 6px; /* Changed from top to bottom */
@@ -2187,31 +2187,31 @@ to { transform: rotate(360deg);
} }
/* Show clear button when hovering over input wrapper */ /* Show clear button when hovering over input wrapper */
.input-wrapper:hover .clear-button[data-v-76ce0f19] { .input-wrapper:hover .clear-button[data-v-5514bf46] {
opacity: 0.7; opacity: 0.7;
pointer-events: auto; pointer-events: auto;
} }
.clear-button[data-v-76ce0f19]:hover { .clear-button[data-v-5514bf46]:hover {
opacity: 1; opacity: 1;
background: rgba(255, 100, 100, 0.8); background: rgba(255, 100, 100, 0.8);
} }
.clear-button svg[data-v-76ce0f19] { .clear-button svg[data-v-5514bf46] {
width: 12px; width: 12px;
height: 12px; height: 12px;
} }
/* Vue DOM mode adjustments for clear button */ /* Vue DOM mode adjustments for clear button */
.text-input.vue-dom-mode ~ .clear-button[data-v-76ce0f19] { .text-input.vue-dom-mode ~ .clear-button[data-v-5514bf46] {
right: 8px; right: 8px;
bottom: 10px; /* Changed from top to bottom, adjusted for Vue DOM padding */ bottom: 10px; /* Changed from top to bottom, adjusted for Vue DOM padding */
width: 20px; width: 20px;
height: 20px; height: 20px;
background: rgba(107, 114, 128, 0.6); background: rgba(107, 114, 128, 0.6);
} }
.text-input.vue-dom-mode ~ .clear-button[data-v-76ce0f19]:hover { .text-input.vue-dom-mode ~ .clear-button[data-v-5514bf46]:hover {
background: oklch(62% 0.18 25); background: oklch(62% 0.18 25);
} }
.text-input.vue-dom-mode ~ .clear-button svg[data-v-76ce0f19] { .text-input.vue-dom-mode ~ .clear-button svg[data-v-5514bf46] {
width: 14px; width: 14px;
height: 14px; height: 14px;
}`)); }`));
@@ -14864,7 +14864,18 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
if (container && container.__widgetInputEl) { if (container && container.__widgetInputEl) {
container.__widgetInputEl.inputEl = textareaRef.value; container.__widgetInputEl.inputEl = textareaRef.value;
} }
const pendingValue = props.widget._pendingValue;
if (pendingValue !== void 0) {
textareaRef.value.value = pendingValue;
hasText.value = pendingValue.length > 0;
delete props.widget._pendingValue;
textareaRef.value.dispatchEvent(new CustomEvent("lora-manager:autocomplete-value-changed", {
detail: { value: pendingValue }
}));
}
if (pendingValue === void 0) {
hasText.value = textareaRef.value.value.length > 0; hasText.value = textareaRef.value.value.length > 0;
}
textareaRef.value.addEventListener("lora-manager:autocomplete-value-changed", onExternalValueChange); textareaRef.value.addEventListener("lora-manager:autocomplete-value-changed", onExternalValueChange);
} }
if (textareaRef.value && typeof props.widget.callback === "function") { if (textareaRef.value && typeof props.widget.callback === "function") {
@@ -14932,7 +14943,7 @@ const _sfc_main = /* @__PURE__ */ defineComponent({
}; };
} }
}); });
const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-76ce0f19"]]); const AutocompleteTextWidget = /* @__PURE__ */ _export_sfc(_sfc_main, [["__scopeId", "data-v-5514bf46"]]);
function createVueWidgetCleanup(vueApp, onCleanup) { function createVueWidgetCleanup(vueApp, onCleanup) {
let didUnmount = false; let didUnmount = false;
return () => { return () => {
@@ -15573,12 +15584,13 @@ function shouldBypassAutocompleteWidgetMigration(node, widgetValues) {
return false; return false;
} }
const originalWidgetsInputs = Object.values(inputDefs).filter( const originalWidgetsInputs = Object.values(inputDefs).filter(
(input) => widgetNames.has(input.name) (input) => widgetNames.has(input.name) || input.forceInput
); );
const widgetIndexHasForceInput = originalWidgetsInputs.flatMap( const widgetIndexHasForceInput = originalWidgetsInputs.flatMap(
(input) => input.control_after_generate ? [!!input.forceInput, false] : [!!input.forceInput] (input) => input.control_after_generate ? [!!input.forceInput, false] : [!!input.forceInput]
); );
return widgetIndexHasForceInput.some(Boolean) && widgetIndexHasForceInput.length === widgetValues.length; const result = widgetIndexHasForceInput.some(Boolean) && widgetIndexHasForceInput.length === widgetValues.length;
return result;
} }
function remapWidgetValuesByName(widgetValues, savedWidgetNames, currentWidgetNames) { function remapWidgetValuesByName(widgetValues, savedWidgetNames, currentWidgetNames) {
const valueByName = /* @__PURE__ */ new Map(); const valueByName = /* @__PURE__ */ new Map();
@@ -15587,12 +15599,18 @@ function remapWidgetValuesByName(widgetValues, savedWidgetNames, currentWidgetNa
valueByName.set(name, widgetValues[index]); valueByName.set(name, widgetValues[index]);
} }
}); });
const currentWidgetNameSet = new Set(currentWidgetNames);
const remappedValues = []; const remappedValues = [];
for (const name of currentWidgetNames) { for (const name of currentWidgetNames) {
if (valueByName.has(name)) { if (valueByName.has(name)) {
remappedValues.push(valueByName.get(name)); remappedValues.push(valueByName.get(name));
} }
} }
for (const name of savedWidgetNames) {
if (!currentWidgetNameSet.has(name) && valueByName.has(name)) {
remappedValues.push(valueByName.get(name));
}
}
return remappedValues; return remappedValues;
} }
function injectDefaultAutocompleteMetadataValues(widgetValues, currentWidgetNames) { function injectDefaultAutocompleteMetadataValues(widgetValues, currentWidgetNames) {
@@ -15707,6 +15725,8 @@ function createAutocompleteTextWidgetFactory(node, widgetName, modelType, inputO
inputEl.dispatchEvent(new CustomEvent("lora-manager:autocomplete-value-changed", { inputEl.dispatchEvent(new CustomEvent("lora-manager:autocomplete-value-changed", {
detail: { value: v2 ?? "" } detail: { value: v2 ?? "" }
})); }));
} else {
widget._pendingValue = v2 ?? "";
} }
if (typeof widget.onSetValue === "function") { if (typeof widget.onSetValue === "function") {
widget.onSetValue(v2 ?? ""); widget.onSetValue(v2 ?? "");
@@ -15823,10 +15843,12 @@ app$1.registerExtension({
}; };
nodeType.prototype.configure = function(info) { nodeType.prototype.configure = function(info) {
normalizeAutocompleteWidgetValues(this, info); normalizeAutocompleteWidgetValues(this, info);
if (shouldBypassAutocompleteWidgetMigration(this, (info == null ? void 0 : info.widgets_values) ?? [])) { const bypassResult = shouldBypassAutocompleteWidgetMigration(this, (info == null ? void 0 : info.widgets_values) ?? []);
if (bypassResult) {
info.widgets_values = [...info.widgets_values ?? [], null]; info.widgets_values = [...info.widgets_values ?? [], null];
} }
return originalConfigure == null ? void 0 : originalConfigure.apply(this, arguments); const result = originalConfigure == null ? void 0 : originalConfigure.apply(this, arguments);
return result;
}; };
} }
if (LORA_CHAIN_NODE_TYPES$1.includes(comfyClass)) { if (LORA_CHAIN_NODE_TYPES$1.includes(comfyClass)) {

File diff suppressed because one or more lines are too long