From b90d1befe632524f719f55397e935423de7eb810 Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 26 Jun 2026 09:58:32 +0200 Subject: [PATCH] fix: text gate sticky edit by intent, not upstream-text comparison Run-from-here now preserves the edited text via an explicit _tgKeepEdit flag set when the button is pressed, instead of comparing incoming vs last text. A non-deterministic upstream (random/seeded prompt) regenerates text on every re-queue, which made the old comparison clobber the edit. Normal toolbar Queue still shows fresh upstream text. Co-Authored-By: Claude Opus 4.8 --- ...026-06-26-textgate-run-from-here-design.md | 31 +++++++++++-------- web/text_gate.js | 29 ++++++++++------- 2 files changed, 36 insertions(+), 24 deletions(-) diff --git a/docs/plans/2026-06-26-textgate-run-from-here-design.md b/docs/plans/2026-06-26-textgate-run-from-here-design.md index 0757e11..6f52625 100644 --- a/docs/plans/2026-06-26-textgate-run-from-here-design.md +++ b/docs/plans/2026-06-26-textgate-run-from-here-design.md @@ -23,22 +23,27 @@ The node currently has no explicit state. Add three: **Run from here** click → `app.queuePrompt(0, 1)` with `app.queuePrompt(0)` fallback — copied verbatim from the Image Gate's `queueFromHere`. -## Sticky edited text +## Sticky edited text (by intent, not text comparison) -The Image Gate keeps its mask sticky; the Text Gate keeps its text. The live -textarea IS the sticky store, gated by the last-seen input: +The Image Gate keeps its mask sticky; the Text Gate keeps its text. Stickiness is +keyed off **which action triggered the run**, not a text comparison — because the +upstream feeding `text` is often non-deterministic (random/seeded prompts), so a +text comparison would wrongly clobber the edit on every Run-from-here. -- Track `node._tgInput` = the last incoming text the server pushed. -- On each re-pause with `incoming`: - - if `incoming === node._tgInput` (upstream unchanged — the Run-from-here - case) → **keep** the current textarea value, so the gate re-runs *your* - edited version (including any edits made after Pass). - - else (a genuine upstream recompute) → overwrite the textarea with `incoming`. - - always set `node._tgInput = incoming`. +- The "Run from here" button sets `node._tgKeepEdit = true` before re-queuing. +- On the next re-pause (`datasete-textgate-show`): + - if `node._tgKeepEdit` → **keep** the current textarea value and clear the + flag, so the gate re-emits *your* edited text downstream. + - else (a normal toolbar Queue) → overwrite the textarea with the incoming + upstream text. -Net: "Run from here" re-runs your version, but a real upstream change still -surfaces instead of hiding behind a stale edit. `_tgInput` is per-session -(not serialized) — a page reload starts fresh, which is fine. +Net: Run-from-here always preserves your edit; a deliberate full Queue shows the +fresh upstream text. `_tgKeepEdit` is per-session (not serialized). + +**Out of scope:** re-queuing still recomputes non-cacheable upstream nodes — that +is inherent to ComfyUI and identical for the Image Gate. With intent-based +stickiness the regenerated text is simply ignored, so it can't change the result; +to skip the compute, Bypass (Ctrl+B) the upstream node manually. ## Verification diff --git a/web/text_gate.js b/web/text_gate.js index 2bf5599..37b864d 100644 --- a/web/text_gate.js +++ b/web/text_gate.js @@ -8,10 +8,13 @@ import { api } from "../../scripts/api.js"; // edited text back. Outputs are static (text, signal) — no dynamic slots. // // After Pass, a "▶ Run from here" button re-queues the prompt (Image Gate -// parity): the gate re-arms every run and IS_CHANGED is NaN, so cached upstream -// means it re-pauses near-instantly. The edited text is sticky — kept across -// re-runs while the upstream input is unchanged, so Run-from-here re-runs YOUR -// version; a genuine upstream change still surfaces the new input. +// parity): the gate re-arms every run and IS_CHANGED is NaN, so it re-pauses +// each run. The edited text is sticky by INTENT: a Run-from-here re-queue keeps +// YOUR edited text (even if a non-deterministic upstream regenerates it), while +// a normal toolbar Queue shows whatever the upstream produced. Keying off which +// button ran — not a text comparison — means a random/seeded upstream can't +// clobber the edit on re-run. (Re-queuing still recomputes non-cacheable +// upstream, as in any ComfyUI run; that regenerated text is simply ignored.) // // Sizing follows the Image Pool node: the editor is always present and FILLS the // node, with only a min-height floor (no max) so the node stays freely resizable @@ -129,6 +132,7 @@ function setupTextGateNode(node) { runHere.textContent = "▶ Run from here"; runHere.style.display = "none"; runHere.onclick = async () => { + node._tgKeepEdit = true; // tell the next re-pause to preserve this edit node._tg.status.textContent = "re-running…"; await queueFromHere(node); }; @@ -173,13 +177,16 @@ app.registerExtension({ const d = e.detail || {}; const node = app.graph?.getNodeById?.(parseInt(d.id, 10)); if (!node || node.type !== NODE || !node._tg) return; - const incoming = d.text || ""; - // Sticky edit: keep the current editor text when the upstream input is - // unchanged (the Run-from-here case, upstream cached), so the gate re-runs - // YOUR version. Only overwrite on a genuine upstream change. - const unchanged = node._tgInput !== undefined && incoming === node._tgInput; - if (!unchanged) node._tg.area.value = incoming; - node._tgInput = incoming; + // Sticky edit by intent: a Run-from-here re-queue (the _tgKeepEdit flag) + // keeps YOUR edited text so the gate re-emits it downstream; a normal + // Queue shows whatever the upstream produced. Keying off the button — + // not a text comparison — means a non-deterministic upstream can't + // clobber the edit on re-run. + if (node._tgKeepEdit) { + node._tgKeepEdit = false; + } else { + node._tg.area.value = d.text || ""; + } setState(node, "paused"); try { node._tg.area.focus(); } catch (err) { /* ignore */ } });