@@ -79,6 +79,17 @@ void accumulate_grep_hits(const std::string& output, GrepHits& out) {
7979// final card renders everything.
8080constexpr std::size_t kStreamTailLines = 64 ;
8181
82+ // Live-tail window budget for line-oriented bodies (FileRead,
83+ // CodeBlock, GitDiff, Json, web_fetch). With show_all=false and
84+ // tail_only=true (maya's defaults for these kinds) the widget renders
85+ // only the last max(head,tail) lines — so feeding the full body makes
86+ // split_lines/elide walk O(file) every frame for a fixed-size preview.
87+ // A tail slice generous enough to cover every renderer's tail budget
88+ // keeps the visible output byte-identical while bounding per-frame cost
89+ // to O(window). The frozen snapshot (built under FrozenBuildScope) still
90+ // gets the full body — painted once, then blitted.
91+ constexpr std::size_t kLiveTailLines = 64 ;
92+
8293[[nodiscard]] std::string tail_window (std::string_view s,
8394 std::size_t keep_lines) {
8495 if (s.empty ()) return {};
@@ -95,6 +106,16 @@ constexpr std::size_t kStreamTailLines = 64;
95106 return std::string{s.substr (start)};
96107}
97108
109+ // Body for a terminal line-oriented tool sitting in the LIVE tail.
110+ // Frozen builds keep the full body (full content, painted once); the
111+ // live path elides to a bounded tail window so per-frame split_lines
112+ // stays O(window) instead of O(file). Cheap no-op for short bodies
113+ // (tail_window returns the whole string when it has <= keep_lines).
114+ [[nodiscard]] std::string live_tail_body (std::string_view body) {
115+ if (building_frozen ()) return std::string{body};
116+ return tail_window (body, kLiveTailLines );
117+ }
118+
98119} // namespace
99120
100121// ── Frozen-build scope ──────────────────────────────────────────────
@@ -171,9 +192,21 @@ maya::ToolBodyPreview::Config tool_body_preview_config(
171192 auto b = body.find (kClose , a);
172193 if (b == std::string::npos) b = body.size ();
173194 out.kind = Kind::GitDiff;
174- out.text = body.substr (a, b - a);
195+ // Frozen build keeps the full diff (show_all, painted
196+ // once). In the live tail elide to a tail window so the
197+ // per-frame split_lines stays O(window) for a large edit
198+ // — same discipline as Write's terminal-but-not-frozen
199+ // branch.
200+ if (building_frozen ()) {
201+ out.text = body.substr (a, b - a);
202+ out.show_all = true ;
203+ } else {
204+ out.text = tail_window (
205+ std::string_view{body}.substr (a, b - a),
206+ kStreamTailLines );
207+ out.show_all = false ;
208+ }
175209 out.text_color = text_tertiary;
176- out.show_all = true ;
177210 return out;
178211 }
179212 // Fence missing — fall through to args-based EditDiff below.
@@ -192,7 +225,11 @@ maya::ToolBodyPreview::Config tool_body_preview_config(
192225 it != tc.args .end () && it->is_array () && !it->empty ())
193226 {
194227 out.kind = Kind::EditDiff;
195- out.show_all = !streaming_now;
228+ // show_all only in the frozen snapshot. While streaming
229+ // OR sitting in the live tail (re-rendered every frame)
230+ // keep the elided per-side/per-hunk preview so the cost
231+ // is bounded; the frozen card carries the full diff.
232+ out.show_all = !streaming_now && building_frozen ();
196233 out.is_streaming = streaming_now;
197234 out.hunks .reserve (it->size ());
198235 for (const auto & e : *it) {
@@ -209,7 +246,7 @@ maya::ToolBodyPreview::Config tool_body_preview_config(
209246 if (nt.empty ()) nt = safe_arg (tc.args , " new_string" );
210247 if (!ot.empty () || !nt.empty ()) {
211248 out.kind = Kind::EditDiff;
212- out.show_all = !streaming_now;
249+ out.show_all = !streaming_now && building_frozen () ;
213250 out.is_streaming = streaming_now;
214251 out.hunks .push_back ({std::move (ot), std::move (nt)});
215252 }
@@ -312,7 +349,7 @@ maya::ToolBodyPreview::Config tool_body_preview_config(
312349 const auto & body = tc.output ();
313350 if (!body.empty () && body != " no changes" ) {
314351 out.kind = Kind::GitDiff;
315- out.text = body;
352+ out.text = live_tail_body ( body) ;
316353 out.text_color = text_tertiary;
317354 }
318355 return out;
@@ -329,7 +366,7 @@ maya::ToolBodyPreview::Config tool_body_preview_config(
329366 const auto & body = tc.output ();
330367 if (!body.empty ()) {
331368 out.kind = Kind::FileRead;
332- out.text = body;
369+ out.text = live_tail_body ( body) ;
333370 out.text_color = text_tertiary; // bright cyan — file content rendered as code
334371 // Anchor the gutter to the real source line numbers the tool
335372 // returned, not 1. The read tool accepts both `offset` and the
@@ -370,7 +407,7 @@ maya::ToolBodyPreview::Config tool_body_preview_config(
370407 const auto & body = tc.output ();
371408 if (!body.empty ()) {
372409 out.kind = Kind::Json;
373- out.text = body;
410+ out.text = live_tail_body ( body) ;
374411 out.text_color = text_tertiary;
375412 }
376413 return out;
@@ -397,7 +434,7 @@ maya::ToolBodyPreview::Config tool_body_preview_config(
397434 {
398435 if (!tc.output ().empty ()) {
399436 out.kind = Kind::CodeBlock;
400- out.text = tc.output ();
437+ out.text = live_tail_body ( tc.output () );
401438 out.text_color = text_tertiary;
402439 }
403440 return out;
@@ -407,7 +444,7 @@ maya::ToolBodyPreview::Config tool_body_preview_config(
407444 // matches the card's failure cue; body content stays dim.
408445 if (tc.is_failed () && !tc.output ().empty ()) {
409446 out.kind = Kind::Failure;
410- out.text = tc.output ();
447+ out.text = live_tail_body ( tc.output () );
411448 out.chrome_color = status_error;
412449 return out;
413450 }
0 commit comments