Skip to content

Commit a95e6e5

Browse files
TUI: clip inline terminal block painting (#14594)
## Description Adds a targeted follow-up for expanded shell commands rendered inside Warp Agent CLI blocks. Top-level terminal blocks already receive a viewport-clipped row range, but inline `TerminalBlockRows::Content` previously walked every retained command/output row and column on each paint while the nested paint surface discarded offscreen writes. This change exposes the element-local visible row window from `TuiPaintSurface`, intersects terminal block painting with that window, and translates the source-row and cursor origins before rendering. Inline command paint work is now proportional to the visible viewport rather than total retained output; the same intersection applies defensively to already-preclipped top-level terminal blocks. Regression coverage verifies nested clip coordinates and exact inline terminal row-window rendering. Benchmark at 120×50 with a fixed 50-row viewport versus stack PR 3: - Expanded inline terminal content, 100 retained rows: 61.6 → 30.9 µs/frame (50% faster, 2.0×). - Expanded inline terminal content, 1,000 retained rows: 123.8 → 36.1 µs/frame (71% faster, 3.4×). - Baseline paint cost doubles from 100 to 1,000 retained rows; the clipped implementation remains close to the fixed viewport cost. Agent conversation: https://staging.warp.dev/conversation/963ead54-9843-4cc1-8d8b-dbf695ee7046 ## Linked Issue N/A — Warp Agent CLI transcript rendering performance. ## Testing - `cargo nextest run -p warpui_core --features tui -E 'test(visible_rows_are_relative_to_the_element_origin) | test(widget_renders_only_visible_rows) | test(nested_surface_clip_contains_cells_styles_and_widgets)'` — 3 passed. - `cargo nextest run -p warp_tui -E 'test(terminal_block::tests)'` — 10 passed. - `cargo bench -p warp_tui --features test-util --bench transcript_bench -- 'tui_terminal_block/clipped_content'` - `./script/format` - [x] I have manually tested my changes locally with `./script/run-tui` ## Agent Mode - [x] Warp Agent Mode - This PR was created via Warp's AI Agent Mode CHANGELOG-NONE Co-Authored-By: Oz <oz-agent@warp.dev> --------- Co-authored-by: Oz <oz-agent@warp.dev>
1 parent 0fb1bae commit a95e6e5

6 files changed

Lines changed: 199 additions & 19 deletions

File tree

crates/warp_tui/benches/transcript_bench.rs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,20 @@ use std::hint::black_box;
22
use std::time::Duration;
33

44
use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
5-
use warp_tui::benchmark_support::{TranscriptBenchmark, TranscriptDataset};
5+
use warp_tui::benchmark_support::{
6+
ClippedTerminalBlockBenchmark, TranscriptBenchmark, TranscriptDataset,
7+
};
8+
9+
fn benchmark_clipped_terminal_block(criterion: &mut Criterion) {
10+
let mut group = criterion.benchmark_group("tui_terminal_block/clipped_content");
11+
for rows in [100, 1_000] {
12+
let mut benchmark = ClippedTerminalBlockBenchmark::new(rows, 120, 50);
13+
group.bench_with_input(BenchmarkId::new("end_frame", rows), &rows, |b, _| {
14+
b.iter(|| black_box(benchmark.present()))
15+
});
16+
}
17+
group.finish();
18+
}
619

720
fn benchmark_many_small_blocks(criterion: &mut Criterion) {
821
let mut group = criterion.benchmark_group("tui_transcript/many_small_blocks");
@@ -93,6 +106,7 @@ criterion_group! {
93106
.warm_up_time(Duration::from_millis(500))
94107
.measurement_time(Duration::from_secs(1));
95108
targets =
109+
benchmark_clipped_terminal_block,
96110
benchmark_many_small_blocks,
97111
benchmark_long_agent_response,
98112
benchmark_offscreen_streaming_tail

crates/warp_tui/src/benchmark_support.rs

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,21 +9,22 @@ use parking_lot::FairMutex;
99
use warp::tui_export::{
1010
AIAgentExchangeId, AIAgentInput, AIAgentOutput, AIAgentOutputMessage, AIAgentOutputMessageType,
1111
AIAgentText, AIAgentTextSection, AIBlockModel, AIBlockOutputStatus, AIConversationId,
12-
AIRequestType, Appearance, LLMId, MessageId, OutputStatusUpdateCallback, RichContentItem,
13-
RichContentType, ServerOutputId, Shared, TerminalModel,
12+
AIRequestType, Appearance, BlockId, LLMId, MessageId, OutputStatusUpdateCallback,
13+
RichContentItem, RichContentType, ServerOutputId, Shared, TerminalModel,
1414
};
1515
use warpui::platform::WindowStyle;
1616
use warpui::{
1717
AddWindowOptions, App, AppContext, Entity, EntityId, EntityIdSet, TuiView, TypedActionView,
1818
ViewContext, ViewHandle, WindowInvalidation,
1919
};
2020
use warpui_core::elements::tui::{
21-
TuiElement, TuiRect, TuiViewportPosition, TuiViewportVerticalAlignment, TuiViewportedList,
22-
TuiViewportedListState,
21+
TuiClipped, TuiElement, TuiRect, TuiViewportPosition, TuiViewportVerticalAlignment,
22+
TuiViewportedList, TuiViewportedListState,
2323
};
2424
use warpui_core::presenter::tui::TuiPresenter;
2525

2626
use crate::agent_block::TuiAIBlock;
27+
use crate::terminal_block::{TerminalBlockElement, block_content_rows};
2728
use crate::test_fixtures::add_test_action_model_and_events;
2829
use crate::tui_block_list_viewport_source::{
2930
AgentBlockRegistry, CLISubagentBlockRegistry, HandoffBlockRegistry, TuiBlockListViewportSource,
@@ -44,6 +45,61 @@ pub enum TranscriptDataset {
4445
},
4546
}
4647

48+
/// One inline terminal block painted through a fixed-height clipped viewport.
49+
pub struct ClippedTerminalBlockBenchmark {
50+
app: App,
51+
model: Arc<FairMutex<TerminalModel>>,
52+
block_id: BlockId,
53+
viewport_origin_y: usize,
54+
presenter: TuiPresenter,
55+
area: TuiRect,
56+
}
57+
58+
impl ClippedTerminalBlockBenchmark {
59+
/// Builds and primes a long terminal block with `rows` output rows.
60+
pub fn new(rows: usize, width: u16, height: u16) -> Self {
61+
App::test((), move |app| async move {
62+
let mut terminal_model = TerminalModel::mock(None, None);
63+
let output = "benchmark terminal output\r\n".repeat(rows);
64+
terminal_model.simulate_block("printf benchmark", output.as_str());
65+
let block = terminal_model
66+
.block_list()
67+
.blocks()
68+
.iter()
69+
.rev()
70+
.find(|block| block.finished())
71+
.expect("simulated block should exist");
72+
let block_id = block.id().clone();
73+
let content_height = block_content_rows(block).len();
74+
let mut benchmark = Self {
75+
app,
76+
model: Arc::new(FairMutex::new(terminal_model)),
77+
block_id,
78+
viewport_origin_y: content_height.saturating_sub(usize::from(height)),
79+
presenter: TuiPresenter::new(),
80+
area: TuiRect::new(0, 0, width, height),
81+
};
82+
benchmark.present();
83+
benchmark
84+
})
85+
}
86+
87+
/// Lays out and paints one clipped frame and returns a cheap checksum.
88+
pub fn present(&mut self) -> u64 {
89+
let element = TuiClipped::new(
90+
TerminalBlockElement::content(self.model.clone(), self.block_id.clone()).finish(),
91+
)
92+
.with_viewport_origin_y(self.viewport_origin_y)
93+
.finish();
94+
let frame = self
95+
.app
96+
.read(|ctx| self.presenter.present_element(element, self.area, ctx));
97+
frame.buffer.content.iter().fold(0u64, |checksum, cell| {
98+
checksum.wrapping_add(cell.symbol().len() as u64)
99+
})
100+
}
101+
}
102+
47103
/// One production-shaped retained transcript benchmark.
48104
pub struct TranscriptBenchmark {
49105
app: App,

crates/warp_tui/src/terminal_block.rs

Lines changed: 41 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,11 @@ impl TuiElement for TerminalBlockElement {
186186
let Some(size) = self.size else {
187187
return;
188188
};
189+
190+
let Some(visible_element_rows) = surface.visible_rows(origin, size) else {
191+
return;
192+
};
193+
189194
let model = self.model.lock();
190195
let colors = model.colors();
191196
let block_list = model.block_list();
@@ -197,22 +202,43 @@ impl TuiElement for TerminalBlockElement {
197202
TerminalBlockRows::Visible { rows, width } => (rows.clone(), (*width).min(size.width)),
198203
TerminalBlockRows::Content => (block_content_rows(block), size.width),
199204
};
200-
let cursor = terminal_block_cursor(block, cursor_owner == Some(block.id()), &rows, size)
201-
.and_then(|(column, row)| {
202-
let column = if self.command_style.is_some() && block.is_command_grid_active() {
203-
column.saturating_add(SHELL_COMMAND_PREFIX_WIDTH)
204-
} else {
205-
column
206-
};
207-
(column < size.width).then_some((column, row))
208-
});
205+
206+
let visible_rows = rows
207+
.start
208+
.saturating_add(usize::from(visible_element_rows.start))
209+
..rows
210+
.start
211+
.saturating_add(usize::from(visible_element_rows.end))
212+
.min(rows.end);
213+
let visible_size = TuiSize::new(
214+
size.width,
215+
visible_element_rows
216+
.end
217+
.saturating_sub(visible_element_rows.start),
218+
);
219+
let visible_origin = origin.offset(0, i32::from(visible_element_rows.start));
220+
let cursor = terminal_block_cursor(
221+
block,
222+
cursor_owner == Some(block.id()),
223+
&visible_rows,
224+
visible_size,
225+
)
226+
.and_then(|(column, row)| {
227+
let column = if self.command_style.is_some() && block.is_command_grid_active() {
228+
column.saturating_add(SHELL_COMMAND_PREFIX_WIDTH)
229+
} else {
230+
column
231+
};
232+
(column < visible_size.width).then_some((column, row))
233+
});
234+
209235
render_block_rows(
210236
block,
211-
rows,
237+
visible_rows,
212238
width,
213239
TerminalBlockPaintBounds {
214-
origin,
215-
size,
240+
origin: visible_origin,
241+
size: visible_size,
216242
background: None,
217243
content_offset: 0,
218244
prefix_style: None,
@@ -223,7 +249,9 @@ impl TuiElement for TerminalBlockElement {
223249
);
224250
drop(model);
225251
if let Some((col, row)) = cursor {
226-
ctx.set_terminal_cursor(ctx.scene_point(origin.offset(i32::from(col), i32::from(row))));
252+
ctx.set_terminal_cursor(
253+
ctx.scene_point(visible_origin.offset(i32::from(col), i32::from(row))),
254+
);
227255
}
228256
}
229257

crates/warp_tui/src/terminal_block_tests.rs

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ use warp::tui_export::{
88
};
99
use warpui::App;
1010
use warpui_core::r#async::Timer;
11-
use warpui_core::elements::tui::{Color, Modifier, TuiBufferExt, TuiElement, TuiRect, TuiSize};
11+
use warpui_core::elements::tui::{
12+
Color, Modifier, TuiBufferExt, TuiClipped, TuiElement, TuiRect, TuiSize,
13+
};
1214
use warpui_core::presenter::tui::TuiPresenter;
1315

1416
use super::{
@@ -166,6 +168,59 @@ fn top_level_shell_command_row_uses_tinted_background() {
166168
});
167169
}
168170

171+
#[test]
172+
fn inline_shell_command_content_renders_a_clipped_row_window() {
173+
App::test((), |app| async move {
174+
app.add_singleton_model(|_| Appearance::mock());
175+
let mut model = TerminalModel::mock(None, None);
176+
model.simulate_block(
177+
"printf rows",
178+
"zero\r\none\r\ntwo\r\nthree\r\nfour\r\nfive\r\n",
179+
);
180+
let block_id = model
181+
.block_list()
182+
.blocks()
183+
.iter()
184+
.rev()
185+
.find(|block| block.finished())
186+
.expect("simulated block should exist")
187+
.id()
188+
.clone();
189+
let height = block_content_rows(
190+
model
191+
.block_list()
192+
.block_with_id(&block_id)
193+
.expect("simulated block should exist"),
194+
)
195+
.len() as u16;
196+
let model = Arc::new(FairMutex::new(model));
197+
198+
app.read(|ctx| {
199+
let mut presenter = TuiPresenter::new();
200+
let full = presenter.present_element(
201+
TerminalBlockElement::content(model.clone(), block_id.clone()).finish(),
202+
TuiRect::new(0, 0, 12, height),
203+
ctx,
204+
);
205+
let full_lines = full.buffer.to_lines();
206+
let viewport_origin = 3usize;
207+
let viewport_height = 3u16;
208+
let clipped = presenter.present_element(
209+
TuiClipped::new(TerminalBlockElement::content(model, block_id).finish())
210+
.with_viewport_origin_y(viewport_origin)
211+
.finish(),
212+
TuiRect::new(0, 0, 12, viewport_height),
213+
ctx,
214+
);
215+
216+
assert_eq!(
217+
clipped.buffer.to_lines(),
218+
full_lines[viewport_origin..viewport_origin + usize::from(viewport_height)],
219+
);
220+
});
221+
});
222+
}
223+
169224
#[test]
170225
fn inline_shell_command_content_keeps_terminal_background() {
171226
App::test((), |app| async move {

crates/warpui_core/src/elements/tui/buffer.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//! element tests: it renders each row to a `String`, skipping the trailing
1111
//! columns of wide graphemes so every glyph appears exactly once (mirroring how
1212
//! ratatui's own `Buffer` debug output collapses multi-width cells).
13+
use std::ops::Range;
1314

1415
use ratatui::buffer::CellWidth;
1516
pub use ratatui::buffer::{Buffer as TuiBuffer, Cell};
@@ -155,6 +156,17 @@ impl<'a> TuiPaintSurface<'a> {
155156
true
156157
}
157158

159+
/// Returns the element-local rows intersecting the active clip.
160+
pub fn visible_rows(&self, origin: TuiScreenPosition, size: TuiSize) -> Option<Range<u16>> {
161+
let visible = self.visible_widget_buffer_area(origin, size)?;
162+
Some(
163+
visible.clipped_rows_above
164+
..visible
165+
.clipped_rows_above
166+
.saturating_add(visible.area.height),
167+
)
168+
}
169+
158170
fn visible_widget_buffer_area(
159171
&self,
160172
origin: TuiScreenPosition,

crates/warpui_core/src/elements/tui/buffer_tests.rs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,3 +187,18 @@ fn nested_surface_clip_contains_cells_styles_and_widgets() {
187187
assert_eq!(b[(0, 2)].fg, Color::Red);
188188
assert_eq!(b[(0, 3)].fg, Color::Reset);
189189
}
190+
191+
#[test]
192+
fn visible_rows_are_relative_to_the_element_origin() {
193+
let mut b = buffer(3, 4);
194+
let mut surface = TuiPaintSurface::new(&mut b);
195+
196+
assert_eq!(
197+
surface.with_clip(
198+
TuiScreenPosition::new(0, 1),
199+
TuiSize::new(3, 2),
200+
|surface| surface.visible_rows(TuiScreenPosition::new(0, -2), TuiSize::new(3, 6),),
201+
),
202+
Some(Some(3..5)),
203+
);
204+
}

0 commit comments

Comments
 (0)