From f23fc5d966be9559adfe2f97c9b45fbed3c1543d Mon Sep 17 00:00:00 2001 From: Vito Sansevero Date: Mon, 2 Feb 2026 15:10:34 -0800 Subject: [PATCH] fix(prompt_manager): sync and fix widget values --- web/prompt_manager.js | 242 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 237 insertions(+), 5 deletions(-) diff --git a/web/prompt_manager.js b/web/prompt_manager.js index 83f2053..4021d21 100644 --- a/web/prompt_manager.js +++ b/web/prompt_manager.js @@ -1,6 +1,7 @@ // PromptManager/web/prompt_manager.js import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js"; // ============================================================================ // Top Bar Button Integration (similar to rgthree-comfy approach) @@ -158,6 +159,47 @@ function addPMTopBarButtonLegacy() { // Extension Registration // ============================================================================ +/** + * CRITICAL FIX: Sync all PromptManager widget values from DOM + * This must be called before any serialization happens + */ +function syncPromptManagerWidgets() { + const graph = app.graph; + if (!graph) return; + + const nodes = graph._nodes || graph.nodes || []; + + for (const node of nodes) { + if (node.type === "PromptManager" || node.type === "PromptManagerText") { + if (node.widgets && node.widgets.length > 0) { + if (!node.widgets_values) { + node.widgets_values = []; + } + + node.widgets.forEach((widget, idx) => { + if (widget) { + // CRITICAL: For multiline widgets, inputEl.value is the true source + const actualValue = widget.inputEl ? widget.inputEl.value : widget.value; + if (actualValue !== undefined) { + const oldValue = node.widgets_values[idx]; + node.widgets_values[idx] = actualValue; + + // Also sync widget.value + if (widget.inputEl && widget.value !== actualValue) { + widget.value = actualValue; + } + + if (widget.name === "text" && oldValue !== actualValue) { + console.log(`[PromptManager] graphToPrompt sync: "${String(oldValue).substring(0, 30)}..." -> "${String(actualValue).substring(0, 30)}..."`); + } + } + } + }); + } + } + } +} + app.registerExtension({ name: "PromptManager.UI", @@ -166,6 +208,56 @@ app.registerExtension({ setTimeout(() => { addPMTopBarButton(); }, 100); + + // CRITICAL: Wrap app.graphToPrompt to sync widget values BEFORE serialization + // This runs before any other extension's wrapper can access stale data + const originalGraphToPrompt = app.graphToPrompt.bind(app); + app.graphToPrompt = async function(...args) { + console.log("[PromptManager] graphToPrompt intercepted - syncing widgets NOW"); + syncPromptManagerWidgets(); + return originalGraphToPrompt(...args); + }; + console.log("[PromptManager] Wrapped app.graphToPrompt for value sync"); + + // CRITICAL FIX: Intercept api.queuePrompt to fix prompt data at the LAST moment + // This catches any cached/stale data that other extensions might have passed through + const originalQueuePrompt = api.queuePrompt.bind(api); + api.queuePrompt = async function(number, { output, workflow }) { + console.log("[PromptManager] api.queuePrompt intercepted - fixing prompt data"); + + // Find PromptManager nodes in the output and fix their text values + const graph = app.graph; + if (graph && output) { + const nodes = graph._nodes || graph.nodes || []; + for (const node of nodes) { + if ((node.type === "PromptManager" || node.type === "PromptManagerText") && output[node.id]) { + // Find the text widget and get its current DOM value + const textWidget = node.widgets?.find(w => w.name === "text"); + if (textWidget && textWidget.inputEl) { + const currentValue = textWidget.inputEl.value; + const outputNode = output[node.id]; + + // The output structure has "inputs" with widget values + if (outputNode.inputs && outputNode.inputs.text !== currentValue) { + console.log(`[PromptManager] FIXING node ${node.id} text: "${String(outputNode.inputs.text).substring(0, 30)}..." -> "${currentValue.substring(0, 30)}..."`); + outputNode.inputs.text = currentValue; + } + } + } + } + } + + return originalQueuePrompt(number, { output, workflow }); + }; + console.log("[PromptManager] Wrapped api.queuePrompt for final data fix"); + }, + + /** + * CRITICAL: Also hook beforeQueuePrompt as a backup sync point + */ + async beforeQueuePrompt(promptInfo) { + console.log("[PromptManager] beforeQueuePrompt - additional sync"); + syncPromptManagerWidgets(); }, async beforeRegisterNodeDef(nodeType, nodeData, app) { @@ -181,6 +273,62 @@ app.registerExtension({ onNodeCreated.apply(this, arguments); } + // CRITICAL FIX: Ensure text widget always returns current value during serialization + // This fixes the issue where graphToPrompt reads stale cached values + const self = this; + setTimeout(() => { + const textWidget = self.widgets?.find((w) => w.name === "text"); + if (textWidget) { + console.log("[PromptManager] Patching text widget for proper serialization"); + + // Store reference to the widget for the closure + const widgetRef = textWidget; + const originalSerializeValue = textWidget.serializeValue; + + // Override serializeValue - use arrow function to capture widgetRef + textWidget.serializeValue = (nodeId, widgetIndex) => { + // Always return the current DOM value using our stored reference + if (widgetRef.inputEl) { + const currentValue = widgetRef.inputEl.value; + // Sync widget.value as well + widgetRef.value = currentValue; + console.log(`[PromptManager] serializeValue returning: "${currentValue.substring(0, 50)}..."`); + return currentValue; + } + // Fallback to widget.value + console.log(`[PromptManager] serializeValue fallback to widget.value: "${String(widgetRef.value).substring(0, 50)}..."`); + return widgetRef.value; + }; + + // Add aggressive sync listeners for all value change events + if (textWidget.inputEl) { + const syncValue = function() { + const currentVal = textWidget.inputEl.value; + textWidget.value = currentVal; + // Also update widgets_values array + const widgetIndex = self.widgets?.indexOf(textWidget); + if (self.widgets_values && widgetIndex >= 0) { + self.widgets_values[widgetIndex] = currentVal; + } + }; + + // Sync on every keystroke + textWidget.inputEl.addEventListener('input', syncValue); + // Sync on change (for paste, autocomplete, etc.) + textWidget.inputEl.addEventListener('change', syncValue); + // Sync when focus leaves the field + textWidget.inputEl.addEventListener('blur', syncValue); + // Sync on keyup as backup + textWidget.inputEl.addEventListener('keyup', syncValue); + + // Initial sync + syncValue(); + + console.log("[PromptManager] Added comprehensive input listeners to text widget"); + } + } + }, 50); // Shorter delay to beat other extensions + // Initialize properties for search state (non-serialized runtime state) this.properties = this.properties || {}; this.properties.resultTimeout = 3; // Default 3 seconds @@ -261,12 +409,40 @@ app.registerExtension({ } }; - // Hook into serialization to preserve resize flag and prevent runtime data from being saved + // Hook into serialization to preserve resize flag, sync widget values, and prevent runtime data from being saved const originalSerialize = this.serialize; this.serialize = function () { + // CRITICAL: Sync widget values BEFORE serialization + // This ensures graphToPrompt reads current values, not cached ones + // For multiline widgets, the value is stored in inputEl.value + if (this.widgets && this.widgets.length > 0) { + if (!this.widgets_values) { + this.widgets_values = []; + } + this.widgets.forEach((widget, idx) => { + if (widget) { + // Get the actual value - inputEl.value for DOM widgets, otherwise widget.value + const actualValue = widget.inputEl ? widget.inputEl.value : widget.value; + if (actualValue !== undefined) { + this.widgets_values[idx] = actualValue; + // Sync widget.value with inputEl.value + if (widget.inputEl && widget.value !== actualValue) { + widget.value = actualValue; + } + } + } + }); + console.log(`[PromptManager] serialize: Synced ${this.widgets.length} widget values`); + } + const data = originalSerialize ? originalSerialize.call(this) : {}; data._userHasResized = this._userHasResized; - + + // Ensure widgets_values is included in serialization + if (this.widgets_values) { + data.widgets_values = [...this.widgets_values]; + } + // Ensure search results are never saved to workflow if (data.properties && data.properties.searchResults) { delete data.properties.searchResults; @@ -274,7 +450,7 @@ app.registerExtension({ if (data.properties && data.properties.selectedPromptIndex) { delete data.properties.selectedPromptIndex; } - + return data; }; @@ -677,32 +853,88 @@ app.registerExtension({ // Find the text widget and set its value const textWidget = this.widgets?.find((w) => w.name === "text"); if (textWidget) { + const textWidgetIndex = this.widgets.indexOf(textWidget); + + console.log(`[PromptManager] DEBUG: Setting prompt on node ${this.id}`); + console.log(`[PromptManager] DEBUG: textWidget found at index ${textWidgetIndex}`); + console.log(`[PromptManager] DEBUG: Old value: ${textWidget.value?.substring(0, 50)}...`); + console.log(`[PromptManager] DEBUG: New value: ${prompt.text.substring(0, 50)}...`); + + // Set the widget value textWidget.value = prompt.text; + // CRITICAL: For multiline widgets, the value is stored in the DOM element (inputEl) + // graphToPrompt calls serializeValue() which reads from inputEl.value + if (textWidget.inputEl) { + textWidget.inputEl.value = prompt.text; + console.log(`[PromptManager] DEBUG: Updated inputEl.value directly`); + } + + // CRITICAL: Also update widgets_values array if it exists + // ComfyUI's graphToPrompt reads from this array for execution + if (this.widgets_values && textWidgetIndex >= 0) { + this.widgets_values[textWidgetIndex] = prompt.text; + console.log(`[PromptManager] DEBUG: Updated widgets_values[${textWidgetIndex}]`); + } + + // Trigger widget callback to notify ComfyUI of value change + if (textWidget.callback) { + textWidget.callback(prompt.text, app.canvas, this, null, null); + console.log(`[PromptManager] DEBUG: Called textWidget.callback`); + } + // Also set metadata if available const categoryWidget = this.widgets?.find( (w) => w.name === "category", ); if (categoryWidget && prompt.category) { + const categoryIdx = this.widgets.indexOf(categoryWidget); categoryWidget.value = prompt.category; + if (categoryWidget.inputEl) { + categoryWidget.inputEl.value = prompt.category; + } + if (this.widgets_values && categoryIdx >= 0) { + this.widgets_values[categoryIdx] = prompt.category; + } + if (categoryWidget.callback) { + categoryWidget.callback(prompt.category, app.canvas, this, null, null); + } } const tagsWidget = this.widgets?.find((w) => w.name === "tags"); if (tagsWidget && prompt.tags) { - tagsWidget.value = Array.isArray(prompt.tags) + const tagsIdx = this.widgets.indexOf(tagsWidget); + const tagsValue = Array.isArray(prompt.tags) ? prompt.tags.join(", ") : prompt.tags; + tagsWidget.value = tagsValue; + if (tagsWidget.inputEl) { + tagsWidget.inputEl.value = tagsValue; + } + if (this.widgets_values && tagsIdx >= 0) { + this.widgets_values[tagsIdx] = tagsValue; + } + if (tagsWidget.callback) { + tagsWidget.callback(tagsValue, app.canvas, this, null, null); + } } // Visual feedback this.highlightSelectedResult(index); - // Trigger widget change events + // Trigger widget change events and mark graph as needing re-execution this.setDirtyCanvas(true, true); + // Force graph to recognize value change for execution cache invalidation + if (app.graph) { + app.graph.setDirtyCanvas(true, true); + } + console.log( `[PromptManager] Loaded prompt: ${prompt.text.substring(0, 50)}...`, ); + console.log(`[PromptManager] DEBUG: Final widget value: ${textWidget.value?.substring(0, 50)}...`); + console.log(`[PromptManager] DEBUG: widgets_values: ${JSON.stringify(this.widgets_values?.slice(0, 3))}...`); } };