Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion crates/warp_tui/benches/transcript_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,20 @@ use std::hint::black_box;
use std::time::Duration;

use criterion::{BenchmarkId, Criterion, criterion_group, criterion_main};
use warp_tui::benchmark_support::{TranscriptBenchmark, TranscriptDataset};
use warp_tui::benchmark_support::{
ClippedTerminalBlockBenchmark, TranscriptBenchmark, TranscriptDataset,
};

fn benchmark_clipped_terminal_block(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("tui_terminal_block/clipped_content");
for rows in [100, 1_000] {
let mut benchmark = ClippedTerminalBlockBenchmark::new(rows, 120, 50);
group.bench_with_input(BenchmarkId::new("end_frame", rows), &rows, |b, _| {
b.iter(|| black_box(benchmark.present()))
});
}
group.finish();
}

fn benchmark_many_small_blocks(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("tui_transcript/many_small_blocks");
Expand Down Expand Up @@ -93,6 +106,7 @@ criterion_group! {
.warm_up_time(Duration::from_millis(500))
.measurement_time(Duration::from_secs(1));
targets =
benchmark_clipped_terminal_block,
benchmark_many_small_blocks,
benchmark_long_agent_response,
benchmark_offscreen_streaming_tail
Expand Down
64 changes: 60 additions & 4 deletions crates/warp_tui/src/benchmark_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,21 +9,22 @@ use parking_lot::FairMutex;
use warp::tui_export::{
AIAgentExchangeId, AIAgentInput, AIAgentOutput, AIAgentOutputMessage, AIAgentOutputMessageType,
AIAgentText, AIAgentTextSection, AIBlockModel, AIBlockOutputStatus, AIConversationId,
AIRequestType, Appearance, LLMId, MessageId, OutputStatusUpdateCallback, RichContentItem,
RichContentType, ServerOutputId, Shared, TerminalModel,
AIRequestType, Appearance, BlockId, LLMId, MessageId, OutputStatusUpdateCallback,
RichContentItem, RichContentType, ServerOutputId, Shared, TerminalModel,
};
use warpui::platform::WindowStyle;
use warpui::{
AddWindowOptions, App, AppContext, Entity, EntityId, EntityIdSet, TuiView, TypedActionView,
ViewContext, ViewHandle, WindowInvalidation,
};
use warpui_core::elements::tui::{
TuiElement, TuiRect, TuiViewportPosition, TuiViewportVerticalAlignment, TuiViewportedList,
TuiViewportedListState,
TuiClipped, TuiElement, TuiRect, TuiViewportPosition, TuiViewportVerticalAlignment,
TuiViewportedList, TuiViewportedListState,
};
use warpui_core::presenter::tui::TuiPresenter;

use crate::agent_block::TuiAIBlock;
use crate::terminal_block::{TerminalBlockElement, block_content_rows};
use crate::test_fixtures::add_test_action_model_and_events;
use crate::tui_block_list_viewport_source::{
AgentBlockRegistry, CLISubagentBlockRegistry, HandoffBlockRegistry, TuiBlockListViewportSource,
Expand All @@ -44,6 +45,61 @@ pub enum TranscriptDataset {
},
}

/// One inline terminal block painted through a fixed-height clipped viewport.
pub struct ClippedTerminalBlockBenchmark {
app: App,
model: Arc<FairMutex<TerminalModel>>,
block_id: BlockId,
viewport_origin_y: usize,
presenter: TuiPresenter,
area: TuiRect,
}

impl ClippedTerminalBlockBenchmark {
/// Builds and primes a long terminal block with `rows` output rows.
pub fn new(rows: usize, width: u16, height: u16) -> Self {
App::test((), move |app| async move {
let mut terminal_model = TerminalModel::mock(None, None);
let output = "benchmark terminal output\r\n".repeat(rows);
terminal_model.simulate_block("printf benchmark", output.as_str());
let block = terminal_model
.block_list()
.blocks()
.iter()
.rev()
.find(|block| block.finished())
.expect("simulated block should exist");
let block_id = block.id().clone();
let content_height = block_content_rows(block).len();
let mut benchmark = Self {
app,
model: Arc::new(FairMutex::new(terminal_model)),
block_id,
viewport_origin_y: content_height.saturating_sub(usize::from(height)),
presenter: TuiPresenter::new(),
area: TuiRect::new(0, 0, width, height),
};
benchmark.present();
benchmark
})
}

/// Lays out and paints one clipped frame and returns a cheap checksum.
pub fn present(&mut self) -> u64 {
let element = TuiClipped::new(
TerminalBlockElement::content(self.model.clone(), self.block_id.clone()).finish(),
)
.with_viewport_origin_y(self.viewport_origin_y)
.finish();
let frame = self
.app
.read(|ctx| self.presenter.present_element(element, self.area, ctx));
frame.buffer.content.iter().fold(0u64, |checksum, cell| {
checksum.wrapping_add(cell.symbol().len() as u64)
})
}
}

/// One production-shaped retained transcript benchmark.
pub struct TranscriptBenchmark {
app: App,
Expand Down
54 changes: 41 additions & 13 deletions crates/warp_tui/src/terminal_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,11 @@ impl TuiElement for TerminalBlockElement {
let Some(size) = self.size else {
return;
};

let Some(visible_element_rows) = surface.visible_rows(origin, size) else {
return;
};

let model = self.model.lock();
let colors = model.colors();
let block_list = model.block_list();
Expand All @@ -197,22 +202,43 @@ impl TuiElement for TerminalBlockElement {
TerminalBlockRows::Visible { rows, width } => (rows.clone(), (*width).min(size.width)),
TerminalBlockRows::Content => (block_content_rows(block), size.width),
};
let cursor = terminal_block_cursor(block, cursor_owner == Some(block.id()), &rows, size)
.and_then(|(column, row)| {
let column = if self.command_style.is_some() && block.is_command_grid_active() {
column.saturating_add(SHELL_COMMAND_PREFIX_WIDTH)
} else {
column
};
(column < size.width).then_some((column, row))
});

let visible_rows = rows
.start
.saturating_add(usize::from(visible_element_rows.start))
..rows
.start
.saturating_add(usize::from(visible_element_rows.end))
.min(rows.end);
let visible_size = TuiSize::new(
size.width,
visible_element_rows
.end
.saturating_sub(visible_element_rows.start),
);
let visible_origin = origin.offset(0, i32::from(visible_element_rows.start));
let cursor = terminal_block_cursor(
block,
cursor_owner == Some(block.id()),
&visible_rows,
visible_size,
)
.and_then(|(column, row)| {
let column = if self.command_style.is_some() && block.is_command_grid_active() {
column.saturating_add(SHELL_COMMAND_PREFIX_WIDTH)
} else {
column
};
(column < visible_size.width).then_some((column, row))
});

render_block_rows(
block,
rows,
visible_rows,
width,
TerminalBlockPaintBounds {
origin,
size,
origin: visible_origin,
size: visible_size,
background: None,
content_offset: 0,
prefix_style: None,
Expand All @@ -223,7 +249,9 @@ impl TuiElement for TerminalBlockElement {
);
drop(model);
if let Some((col, row)) = cursor {
ctx.set_terminal_cursor(ctx.scene_point(origin.offset(i32::from(col), i32::from(row))));
ctx.set_terminal_cursor(
ctx.scene_point(visible_origin.offset(i32::from(col), i32::from(row))),
);
}
}

Expand Down
57 changes: 56 additions & 1 deletion crates/warp_tui/src/terminal_block_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use warp::tui_export::{
};
use warpui::App;
use warpui_core::r#async::Timer;
use warpui_core::elements::tui::{Color, Modifier, TuiBufferExt, TuiElement, TuiRect, TuiSize};
use warpui_core::elements::tui::{
Color, Modifier, TuiBufferExt, TuiClipped, TuiElement, TuiRect, TuiSize,
};
use warpui_core::presenter::tui::TuiPresenter;

use super::{
Expand Down Expand Up @@ -166,6 +168,59 @@ fn top_level_shell_command_row_uses_tinted_background() {
});
}

#[test]
fn inline_shell_command_content_renders_a_clipped_row_window() {
App::test((), |app| async move {
app.add_singleton_model(|_| Appearance::mock());
let mut model = TerminalModel::mock(None, None);
model.simulate_block(
"printf rows",
"zero\r\none\r\ntwo\r\nthree\r\nfour\r\nfive\r\n",
);
let block_id = model
.block_list()
.blocks()
.iter()
.rev()
.find(|block| block.finished())
.expect("simulated block should exist")
.id()
.clone();
let height = block_content_rows(
model
.block_list()
.block_with_id(&block_id)
.expect("simulated block should exist"),
)
.len() as u16;
let model = Arc::new(FairMutex::new(model));

app.read(|ctx| {
let mut presenter = TuiPresenter::new();
let full = presenter.present_element(
TerminalBlockElement::content(model.clone(), block_id.clone()).finish(),
TuiRect::new(0, 0, 12, height),
ctx,
);
let full_lines = full.buffer.to_lines();
let viewport_origin = 3usize;
let viewport_height = 3u16;
let clipped = presenter.present_element(
TuiClipped::new(TerminalBlockElement::content(model, block_id).finish())
.with_viewport_origin_y(viewport_origin)
.finish(),
TuiRect::new(0, 0, 12, viewport_height),
ctx,
);

assert_eq!(
clipped.buffer.to_lines(),
full_lines[viewport_origin..viewport_origin + usize::from(viewport_height)],
);
});
});
}

#[test]
fn inline_shell_command_content_keeps_terminal_background() {
App::test((), |app| async move {
Expand Down
12 changes: 12 additions & 0 deletions crates/warpui_core/src/elements/tui/buffer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
//! element tests: it renders each row to a `String`, skipping the trailing
//! columns of wide graphemes so every glyph appears exactly once (mirroring how
//! ratatui's own `Buffer` debug output collapses multi-width cells).
use std::ops::Range;

use ratatui::buffer::CellWidth;
pub use ratatui::buffer::{Buffer as TuiBuffer, Cell};
Expand Down Expand Up @@ -155,6 +156,17 @@ impl<'a> TuiPaintSurface<'a> {
true
}

/// Returns the element-local rows intersecting the active clip.
pub fn visible_rows(&self, origin: TuiScreenPosition, size: TuiSize) -> Option<Range<u16>> {
let visible = self.visible_widget_buffer_area(origin, size)?;
Some(
visible.clipped_rows_above
..visible
.clipped_rows_above
.saturating_add(visible.area.height),
)
}

fn visible_widget_buffer_area(
&self,
origin: TuiScreenPosition,
Expand Down
15 changes: 15 additions & 0 deletions crates/warpui_core/src/elements/tui/buffer_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,3 +187,18 @@ fn nested_surface_clip_contains_cells_styles_and_widgets() {
assert_eq!(b[(0, 2)].fg, Color::Red);
assert_eq!(b[(0, 3)].fg, Color::Reset);
}

#[test]
fn visible_rows_are_relative_to_the_element_origin() {
let mut b = buffer(3, 4);
let mut surface = TuiPaintSurface::new(&mut b);

assert_eq!(
surface.with_clip(
TuiScreenPosition::new(0, 1),
TuiSize::new(3, 2),
|surface| surface.visible_rows(TuiScreenPosition::new(0, -2), TuiSize::new(3, 6),),
),
Some(Some(3..5)),
);
}
Loading