aboutsummaryrefslogtreecommitdiffstats
path: root/pi-ext
diff options
context:
space:
mode:
authoruser <user@clank>2026-07-26 09:32:49 +0200
committeruser <user@clank>2026-08-12 19:34:35 +0200
commit84483b6d6020eee944ada9822c5d6229dc567e04 (patch)
tree956db77c19c50c7840be631970d7cac3bae65509 /pi-ext
parentadd obnoxious-working: loud spinners + sl0p-swag status messages (diff)
downloadsl0ppi-84483b6d6020eee944ada9822c5d6229dc567e04.tar.gz
sl0ppi-84483b6d6020eee944ada9822c5d6229dc567e04.tar.xz
sl0ppi-84483b6d6020eee944ada9822c5d6229dc567e04.zip
slop-tool-boxes: stop full-redraw flicker on tall, top-clipped active boxes
An in-flight tool box taller than the viewport (top scrolled off-screen) flickered violently the whole time it was active. Cause: the working spinner (and pi's default spinner) drive ui.requestRender() at ~10-16fps, and the active call slot recomputed a wall-clock pulse color every render, recoloring the TOP border. tui.js falls back to a full-screen redraw when the first changed line is above the viewport (firstChanged < prevViewportTop), so every spinner tick = one full clear+repaint. Fix: once the result slot is streaming (box can be tall), freeze the call slot pulse color, caching built lines per (width, inner). The header/command is static, so the top border is byte-identical between render ticks and never triggers an above-viewport change. The pre-output phase (box still fully visible) keeps the live pulse + ticking runtime; the result slot still pulses its in-viewport bottom border. NOTES.md documents this as flicker #4. Confirmed by repro: massive flicker with the fix reverted, calm with it in.
Diffstat (limited to 'pi-ext')
-rw-r--r--pi-ext/NOTES.md28
-rw-r--r--pi-ext/slop-tool-boxes.ts28
2 files changed, 53 insertions, 3 deletions
diff --git a/pi-ext/NOTES.md b/pi-ext/NOTES.md
index f3125cb..db62e7a 100644
--- a/pi-ext/NOTES.md
+++ b/pi-ext/NOTES.md
@@ -1,5 +1,33 @@
# Notes / TODO
+## ✅ FIXED: tool-box flicker #4 (off-screen top border + external render ticks)
+
+**Symptom:** an in-flight tool box taller than the viewport, scrolled so its TOP
+is clipped off-screen, flickers violently (full-screen redraw) the entire time
+it is active; stops the instant it settles.
+
+**Cause:** two things compound. (1) `obnoxious-working.ts` (and pi's own default
+spinner) animate via `ui.requestRender()` at ~10–16fps, so the whole UI
+re-renders even when the tool is silent. (2) The active tool-box render did not
+cache (`cw = -1`) and recomputed a wall-clock `pulsePaint()` every render, so the
+**top border** (owned by the *call* slot) changed color on every tick. tui.js
+only diff-renders what's on screen: `if (firstChanged < prevViewportTop)
+fullRender(true)`. A recolored top border sitting *above* the viewport is
+`firstChanged < viewportTop` → full clear+repaint, every spinner frame.
+
+(The *result* slot only recolors its **bottom** border, which stays in-viewport,
+so it was never the culprit. Streaming appends new lines at the bottom too, so it
+never mutates off-screen lines.)
+
+**Fix:** while active **and** the result slot has started streaming (box can be
+tall), FREEZE the call slot's pulse color, caching the built lines per
+`(width, inner)`. The command/header is static, so the top border becomes
+byte-identical between render ticks → `firstChanged` never lands above the
+viewport → no full redraw. The pre-output phase (box still short + fully visible)
+keeps the live pulse and the ticking runtime. Net: top border no longer pulses
+while output streams (usually off-screen anyway); the result slot's bottom border
+still pulses in-viewport.
+
## ✅ FIXED: tool-box flicker × 3 (streaming rails, startup storm, streaming timer)
**Final invariant: the pulse NEVER forces a repaint.** The pulse color is a pure
diff --git a/pi-ext/slop-tool-boxes.ts b/pi-ext/slop-tool-boxes.ts
index 0b72dcf..2a3bd94 100644
--- a/pi-ext/slop-tool-boxes.ts
+++ b/pi-ext/slop-tool-boxes.ts
@@ -396,6 +396,13 @@ type FrameState = {
summaryText?: string;
callW: number;
callLines?: string[];
+ // Active-render freeze cache (anti-flicker): while streaming, hold the pulse
+ // color steady per (width, content) so the top border—often scrolled off the
+ // top of a tall box—stays byte-identical between the working-spinner's render
+ // ticks. Recoloring an off-screen line forces a full-screen redraw.
+ callActiveW?: number;
+ callActiveInner?: string[];
+ callActiveLines?: string[];
resultRef?: any;
resultExpanded?: boolean;
resultW: number;
@@ -524,9 +531,23 @@ function registerFramedTool(pi: ExtensionAPI, def: ToolDef) {
return out;
};
if (activeNow) {
- cw = -1; // don't cache while animating; recompute solid once settled
- // in-flight: pulsing top/bottom borders + STATIC gray dotted rails
- return build(pulsePaint(), theme.fg("muted", DOTTED_RAIL));
+ cw = -1; // settled cache must rebuild once the row finishes
+ const rail = theme.fg("muted", DOTTED_RAIL);
+ // Pre-output (no result slot yet): the whole box is short + visible,
+ // so keep the live pulse (and ticking runtime in the bottom border).
+ if (!st.resultActive) return build(pulsePaint(), rail);
+ // Result is streaming below — this slot owns the TOP border, which may
+ // be scrolled off-screen. Freeze the pulse color while our content +
+ // width are unchanged so the top border is byte-identical between the
+ // spinner's render ticks; otherwise every tick recolors an off-screen
+ // line and tui.js escalates to a full-screen redraw (violent flicker
+ // on tall, top-clipped tool boxes). See NOTES.md.
+ if (width !== st.callActiveW || inner !== st.callActiveInner) {
+ st.callActiveW = width;
+ st.callActiveInner = inner;
+ st.callActiveLines = build(pulsePaint(), rail);
+ }
+ return st.callActiveLines!;
}
if (width === cw) return cl;
// settled: solid borders + rails in the status color
@@ -538,6 +559,7 @@ function registerFramedTool(pi: ExtensionAPI, def: ToolDef) {
invalidate() {
cw = -1;
st.callW = -1;
+ st.callActiveW = -1; // drop the frozen active lines on theme change
st.callArgsRef = undefined; // rebuild native on theme change
st.callInner?.invalidate?.();
},