Skip to content

Commit 478d267

Browse files
committed
fix(render): lock fine-animation hash bucket to tick cadence on non-sync terminals to stop spinner/status-bar tearing
1 parent a7718b7 commit 478d267

1 file changed

Lines changed: 29 additions & 3 deletions

File tree

include/agentty/runtime/app/program.hpp

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <cstdint>
99

1010
#include <maya/maya.hpp>
11+
#include <maya/terminal/ansi.hpp> // env_supports_synchronized_output()
1112

1213
#include "agentty/runtime/app/deps.hpp"
1314
#include "agentty/runtime/app/subscribe.hpp"
@@ -178,8 +179,26 @@ struct AgenttyApp {
178179
//
179180
// Three regimes:
180181
// (a) fine animation live (spinner / streaming caret / welcome
181-
// bob / queued-chip pulse): 33 ms (~30 fps) — these advance
182-
// every frame, so a fine bucket is correct and necessary.
182+
// bob / queued-chip pulse): step at the SAME cadence the Tick
183+
// subscription wakes the loop. On DEC-2026 terminals that's
184+
// 33 ms (~30 fps) and the synchronized-output wrapper makes
185+
// each frame swap atomically — smooth, no tearing. On
186+
// terminals WITHOUT mode 2026 (Apple Terminal, plain xterm,
187+
// tmux without sync passthrough) every multi-row repaint
188+
// paints progressively, so the chrome at the bottom of the
189+
// frame (spinner / sparkline / status bar) visibly tears on
190+
// each render. There the Tick already drops to 100 ms
191+
// (subscribe.cpp), but this bucket must MATCH it: a 33 ms
192+
// bucket against a 100 ms tick advances ~3x per wake, so the
193+
// skip-render gate fires a torn repaint on every tick anyway
194+
// and the 100 ms throttle buys nothing. Locking the bucket
195+
// to the tick period means exactly one render per tick —
196+
// ~10 fps of torn frames instead of ~10 redundant ones, and
197+
// phase-locked so renders don't double up. The spinner is
198+
// already capped at 10 fps by the tick on those terminals,
199+
// so perceived smoothness is unchanged; we only stop the
200+
// extra tearing repaints. On sync terminals the period is
201+
// still 33 ms — UX identical to before.
183202
// (b) idle with the composer caret blinking: lock to the blink
184203
// HALF-period (265 ms = 530 ms / 2) so every hash step is
185204
// exactly one caret toggle. This keeps the RAF loop self-
@@ -203,8 +222,15 @@ struct AgenttyApp {
203222
// freeze-until-keypress bug. Bias toward animating.
204223
constexpr std::int64_t kBlinkHalfMs = 265;
205224
const bool caret_blinking = !m.s.active();
225+
// Fine-animation bucket period, matched to the Tick subscription's
226+
// cadence (subscribe.cpp) so the hash advances exactly once per
227+
// loop wake. 33 ms on DEC-2026 terminals (atomic frames, smooth),
228+
// 100 ms otherwise (one torn repaint per tick instead of ~3).
229+
// Detected once — the capability is immutable for the session.
230+
static const std::int64_t kFineAnimMs =
231+
maya::ansi::env_supports_synchronized_output() ? 33 : 100;
206232
if (fine_anim_live) {
207-
mix(static_cast<std::uint64_t>(now_ms / 33));
233+
mix(static_cast<std::uint64_t>(now_ms / kFineAnimMs));
208234
} else if (caret_blinking) {
209235
// Phase-locked: feed the blink PARITY, not a time bucket, so
210236
// the hash advances on exactly the same boundary maya uses

0 commit comments

Comments
 (0)