feat(workflow): exclude text-capable nodes with connected text from send targets

CLIP Text Encode and friends whose text widget is backed by a connected
input cannot have their text changed via the widget (execution reads the
linked input), so sending to them was a silent no-op.

- Registry: compute text_widget_connected capability from the widget's
  backing input link state; has_text_widget drops to false when wired;
  include the flag in the registration fingerprint so link changes
  re-register the affected nodes
- Registry: hook link connect/disconnect (graph events on new litegraph,
  onAfterChange fallback for classic) on root and subgraphs, plus
  subgraph-created for future subgraphs
- applyWidgetUpdate: skip inject_text when the target widget is connected
  and self-heal the registry instead of writing a value that is ignored
- Web UI: drop text_widget_connected nodes from prompt/embedding send
  candidates; show a Mark as -> Send Prompt Target hint toast when no
  candidates remain (new uiHelpers.workflow.noPromptTargets key, synced
  to all locales; zh-CN/zh-TW translated)
- Extract shared resolveTextWidget() used by both the candidate-set
  logic and the write path so the two cannot drift apart
- Tests: workflow registry connection-state registration, subgraph
  handling, fingerprint re-registration, inject_text write/skip paths,
  setup link-change hooks; uiHelpers candidate filtering and hint toast
This commit is contained in:
Will Miao
2026-08-12 16:50:07 +08:00
parent 5c2b2aedcc
commit 94e3f54571
15 changed files with 640 additions and 27 deletions
+23 -2
View File
@@ -1092,6 +1092,9 @@ export async function sendEmbeddingToWorkflow(embeddingCode, onComplete = null)
if (!isNodeEnabled(node)) {
return false;
}
if (node.capabilities?.text_widget_connected === true) {
return false;
}
return (
node.capabilities?.has_text_widget === true ||
node.marker_role === "send_prompt_target"
@@ -1100,7 +1103,15 @@ export async function sendEmbeddingToWorkflow(embeddingCode, onComplete = null)
const nodeKeys = Object.keys(textNodes);
if (nodeKeys.length === 0) {
showToast('uiHelpers.workflow.noMatchingNodes', {}, 'warning');
showToast(
translate(
'uiHelpers.workflow.noPromptTargets',
{},
'No compatible prompt targets in the workflow.\nRight-click a node in ComfyUI → Mark as → Send Prompt Target'
),
{},
'warning'
);
return false;
}
@@ -1152,6 +1163,11 @@ export async function sendPromptToWorkflow(promptText, options = {}) {
if (!isNodeEnabled(node)) {
return false;
}
// A node whose text widget is backed by a connected input cannot have its
// text changed via the widget — execution reads the linked input.
if (node.capabilities?.text_widget_connected === true) {
return false;
}
return (
node.capabilities?.has_text_widget === true ||
node.marker_role === "send_prompt_target"
@@ -1160,7 +1176,12 @@ export async function sendPromptToWorkflow(promptText, options = {}) {
const nodeKeys = Object.keys(textNodes);
if (nodeKeys.length === 0) {
showToast(options.missingNodesMessage || 'uiHelpers.workflow.noMatchingNodes', {}, 'warning');
const defaultHint = translate(
'uiHelpers.workflow.noPromptTargets',
{},
'No compatible prompt targets in the workflow.\nRight-click a node in ComfyUI → Mark as → Send Prompt Target'
);
showToast(options.missingNodesMessage || defaultHint, {}, 'warning');
return false;
}