From f2ac5e37f38fc773a770c50abde24f058de6cb9f Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 26 Jun 2026 09:38:17 +0200 Subject: [PATCH 1/2] docs: Text Gate run-from-here + sticky edit design Co-Authored-By: Claude Opus 4.8 --- ...026-06-26-textgate-run-from-here-design.md | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 docs/plans/2026-06-26-textgate-run-from-here-design.md 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 new file mode 100644 index 0000000..0757e11 --- /dev/null +++ b/docs/plans/2026-06-26-textgate-run-from-here-design.md @@ -0,0 +1,55 @@ +# Text Gate — "Run from here" + sticky edit (design) + +**Goal:** Bring the Text Gate to parity with the Image Gate's "Run from here" +affordance, plus a text-specific touch: keep the user's edited text across +re-runs ("start from there"). + +**Scope:** Frontend only — `web/text_gate.js`. No changes to `gates/textgate.py`, +`gates/gate_bus.py`, or `gates/gate_server.py`. The gate already re-arms and +re-pauses on every run (`GateBus.arm` → `wait_payload`) and `IS_CHANGED` returns +`NaN`, so re-queuing the prompt is enough to "resume": cached upstream means the +gate re-pauses near-instantly. + +## State machine + +The node currently has no explicit state. Add three: + +- **idle** — before the first run. Pass shown, Run-from-here hidden. +- **paused** — socket `datasete-textgate-show` arrived. Textarea editable & + populated, **▶ Pass** shown, **Run from here** hidden, status `edit, then Pass`. +- **passed** — after Pass click. Textarea keeps the edited text, **Pass** hidden, + **▶ Run from here** shown, status `passed — Run from here to re-run`. + +**Run from here** click → `app.queuePrompt(0, 1)` with `app.queuePrompt(0)` +fallback — copied verbatim from the Image Gate's `queueFromHere`. + +## Sticky edited text + +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: + +- 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`. + +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. + +## Verification + +- `node --check web/text_gate.js` (no JS test harness in the repo — consistent + with the other `web/*.js`). +- Manual: pause → edit → Pass → button appears → Run-from-here re-pauses showing + your edited text → downstream re-runs; change something upstream → new input + shows. + +## Dropped (YAGNI) + +- A separate "↺ reset to input" button — the upstream-change detection covers the + stale-edit footgun. +- Any backend auto-pass / bypass mode — not requested. From 99a5ccac82322a3446fda3bcf00c10b44073e65c Mon Sep 17 00:00:00 2001 From: Ethanfel Date: Fri, 26 Jun 2026 09:39:51 +0200 Subject: [PATCH 2/2] feat: text gate Run-from-here button + sticky edited text Co-Authored-By: Claude Opus 4.8 --- web/text_gate.js | 67 +++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 60 insertions(+), 7 deletions(-) diff --git a/web/text_gate.js b/web/text_gate.js index e2c27c9..2bf5599 100644 --- a/web/text_gate.js +++ b/web/text_gate.js @@ -7,6 +7,12 @@ import { api } from "../../scripts/api.js"; // the server pushes via the "datasete-textgate-show" socket event and POSTs the // 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. +// // 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 // and the textarea grows with it. @@ -28,6 +34,30 @@ async function postPass(node, text) { await api.fetchApi(`${R}/pass`, { method: "POST", body: fd }); } +// ---- run-from-here + state -------------------------------------------------- +// States: "idle" (pre-run), "paused" (waiting for Pass), "passed" (Run-from-here +// shown). Re-queuing the whole prompt is enough to "resume" — cached upstream +// re-pauses the gate, matching the Image Gate's queueFromHere. + +async function queueFromHere(node) { + try { + await app.queuePrompt(0, 1); + } catch (e) { + try { await app.queuePrompt(0); } catch (e2) { console.error("[tgate] queue failed", e2); } + } +} + +function setState(node, s) { + node._tgState = s; + const tg = node._tg; + if (!tg) return; + tg.pass.style.display = s === "passed" ? "none" : ""; + tg.runHere.style.display = s === "passed" ? "" : "none"; + if (s === "paused") tg.status.textContent = "edit, then Pass"; + else if (s === "passed") tg.status.textContent = "passed — Run from here to re-run"; + node.setDirtyCanvas?.(true, true); +} + // ---- sizing (Image Pool pattern) -------------------------------------------- // Only a min-height FLOOR — no max — so the DOM widget fills the node and grows @@ -59,6 +89,8 @@ function injectStyles() { border:1px solid #555; color:#fff; } .tgate-pass { background:rgba(40,130,70,0.95); } .tgate-pass:hover { background:rgba(55,160,90,0.98); } + .tgate-run { background:rgba(40,90,140,0.95); } + .tgate-run:hover { background:rgba(60,120,180,0.98); } .tgate-status { font-size:11px; opacity:0.6; margin-left:auto; } `; const style = document.createElement("style"); @@ -81,21 +113,37 @@ function setupTextGateNode(node) { const btns = document.createElement("div"); btns.className = "tgate-btns"; + const pass = document.createElement("button"); pass.className = "tgate-pass"; pass.textContent = "▶ Pass"; - const status = document.createElement("span"); - status.className = "tgate-status"; pass.onclick = async () => { await postPass(node, area.value); - status.textContent = "passed"; + setState(node, "passed"); }; + + // Re-queue the prompt; cached upstream re-pauses the gate so you can run your + // edited text downstream again without recomputing the graph above it. + const runHere = document.createElement("button"); + runHere.className = "tgate-run"; + runHere.textContent = "▶ Run from here"; + runHere.style.display = "none"; + runHere.onclick = async () => { + node._tg.status.textContent = "re-running…"; + await queueFromHere(node); + }; + + const status = document.createElement("span"); + status.className = "tgate-status"; + btns.appendChild(pass); + btns.appendChild(runHere); btns.appendChild(status); wrap.appendChild(area); wrap.appendChild(btns); - node._tg = { wrap, area, status }; + node._tg = { wrap, area, status, pass, runHere }; + node._tgState = "idle"; // FILLS the node: floor-only min height, no max (Image Pool pattern). node._tgWidget = node.addDOMWidget("textgate_editor", "div", wrap, { @@ -125,10 +173,15 @@ app.registerExtension({ const d = e.detail || {}; const node = app.graph?.getNodeById?.(parseInt(d.id, 10)); if (!node || node.type !== NODE || !node._tg) return; - node._tg.area.value = d.text || ""; - node._tg.status.textContent = "edit, then Pass"; + 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; + setState(node, "paused"); try { node._tg.area.focus(); } catch (err) { /* ignore */ } - node.setDirtyCanvas?.(true, true); }); },