@@ -920,10 +920,30 @@ maya::Cmd<Msg> finalize_turn(Model& m, StopReason stop_reason) {
920920 // On long sessions that's the dominant per-frame cost — the
921921 // canvas resizes to fit total transcript height and every
922922 // render clears + paints all of it.
923+ //
924+ // Either way, absorb the freeze-seam height delta with
925+ // commit_scrollback_overflow. At this instant the live tail's
926+ // last run loses its reserved indicator/spacer slot
927+ // (reserve_slot = active() && is_last_run flips false now that
928+ // phase==Idle) AND its markdown just settled via finish() above,
929+ // so the run's row count differs from the previous (streaming)
930+ // frame by ±N rows. maya's inline diff compares new-vs-prev: a
931+ // row-count change in the live tail shifts every row below it and
932+ // the visible viewport gets re-emitted — the "whole scrollback
933+ // re-renders at turn end" flicker. commit_scrollback_overflow
934+ // re-bases prev_cells (drops max(0, prev_rows - term_h), zero
935+ // wire bytes) so the next diff measures the settled frame against
936+ // a clean baseline instead of scrolling on-screen rows. The
937+ // trim path already issues it; the non-trim path needs it too
938+ // because the seam delta exists whether or not we trimmed. Fold
939+ // it into `kp` here so the post-turn auto-compaction check below
940+ // still runs.
923941 if (auto trim = trim_frozen_if_oversized (m); !trim.is_none ()) {
924- auto rest = std::move (kp);
925- return Cmd<Msg>::batch (std::vector<Cmd<Msg>>{
926- std::move (trim), std::move (rest)});
942+ kp = Cmd<Msg>::batch (std::vector<Cmd<Msg>>{
943+ std::move (trim), std::move (kp)});
944+ } else {
945+ kp = Cmd<Msg>::batch (std::vector<Cmd<Msg>>{
946+ maya::Cmd<Msg>::commit_scrollback_overflow (), std::move (kp)});
927947 }
928948 }
929949
0 commit comments