@@ -84,8 +84,19 @@ int estimate_wrap_cols() {
8484// terminal redraws them instantly) and on disk (recall via picker).
8585std::size_t frozen_row_budget () {
8686 const int h = term_dims ().rows ;
87- // ~2 viewports, floored so a tiny window still keeps useful context.
88- return static_cast <std::size_t >(std::max (48 , h * 2 ));
87+ // ~1 extra viewport beyond the on-screen region, floored so a tiny
88+ // window still keeps useful context. CALIBRATION: this budget is
89+ // applied to estimate_msg_rows, which now counts REAL source lines
90+ // with no double-count. An earlier estimate over-counted tool bodies
91+ // ~2x (it summed both the parsed `args` AND the raw `args_streaming`
92+ // of the same payload), so the same budget value retained only ~half
93+ // this many REAL rows on the canvas. With the estimate corrected, a
94+ // 2x budget would retain ~2x the REAL rows and per-frame render
95+ // (O(real canvas rows): render_tree + clear + verify) slows as the
96+ // session grows. 1.5x keeps a couple screens of recent context while
97+ // holding the canvas — and the latency — flat. Older rows live in
98+ // native terminal scrollback and on disk.
99+ return static_cast <std::size_t >(std::max (48 , (h * 3 ) / 2 ));
89100}
90101
91102// Thin dim ─ rule between turns. Pushed before each fresh-speaker
@@ -816,13 +827,14 @@ maya::Cmd<Msg> trim_frozen_if_oversized(Model& m) {
816827 // full bodies are NEVER collapsed (the `show_all` UX is intact);
817828 // they simply graduate from the in-app re-render window into
818829 // native terminal scrollback.
819- // Live-canvas row cap = ~2 viewports (same budget rehydrate seeds
820- // to, so steady-state and resume agree). Above it, the oldest
830+ // Live-canvas row cap = frozen_row_budget() (~1.5 viewports of the
831+ // accurate row estimate; same budget rehydrate seeds to, so steady-
832+ // state and resume agree). Above it, the oldest
821833 // entries are dropped; maya's row diff sees a shorter live tree and
822834 // the already-overflowed rows commit to native scrollback. Bounding
823835 // the canvas to ~2 screens keeps EVERY full repaint cheap — the
824836 // per-frame tick, a resize (Divergent wipe+repaint), and Ctrl-L all
825- // walk only ~2 screens instead of thousands of rows.
837+ // walk only ~1.5 screens instead of thousands of rows.
826838 const std::size_t kFrozenMaxRows = frozen_row_budget ();
827839 constexpr std::size_t kFrozenMaxEntries = 120 ;
828840 // Retention floor. Expressed in ROWS, not a fixed entry count: the
0 commit comments