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