Skip to content
Open
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
1 change: 1 addition & 0 deletions crates/ai/src/grok_subscription/oauth_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ fn bind_test_listener() -> (TcpListener, std::net::SocketAddr) {
/// so a reintroduced teardown race is caught here instead of showing up as an
/// intermittent CI failure.
#[test]

fn cancelling_loopback_wait_releases_listener() {
const CANCEL_CYCLES: usize = 100;

Expand Down
20 changes: 20 additions & 0 deletions crates/warp_tui/benches/transcript_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,25 @@ fn benchmark_clipped_terminal_block(criterion: &mut Criterion) {
group.finish();
}

fn benchmark_running_agent_command(criterion: &mut Criterion) {
let mut group = criterion.benchmark_group("tui_transcript/running_agent_command");
for rows in [100, 1_000] {
let mut benchmark =
TranscriptBenchmark::new(TranscriptDataset::RunningAgentCommand { rows }, 120, 50);
group.bench_with_input(
BenchmarkId::new("viewport_relayout_unchanged_frame", rows),
&rows,
|b, _| {
b.iter(|| {
benchmark.invalidate_viewport();
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");
for blocks in [100, 1_000, 10_000] {
Expand Down Expand Up @@ -107,6 +126,7 @@ criterion_group! {
.measurement_time(Duration::from_secs(1));
targets =
benchmark_clipped_terminal_block,
benchmark_running_agent_command,
benchmark_many_small_blocks,
benchmark_long_agent_response,
benchmark_offscreen_streaming_tail
Expand Down
28 changes: 22 additions & 6 deletions crates/warp_tui/src/agent_block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1126,16 +1126,13 @@ impl TuiAIBlock {
/// Whether the cached height is stale at `width`.
pub(super) fn needs_height_measurement(&self, width: u16, app: &AppContext) -> bool {
self.last_measured_width.get() != Some(width)
|| self.block_model.status(app).is_streaming()
|| self.action_views.values().any(|view| match view {
TuiToolCallView::AskQuestion(_)
| TuiToolCallView::FileEdits(_)
| TuiToolCallView::Generic(_)
| TuiToolCallView::Plan(_)
| TuiToolCallView::OrchestrationBlock(_) => false,
TuiToolCallView::ShellCommand(view) => {
view.as_ref(app).needs_continuous_height_measurement()
}
TuiToolCallView::ShellCommand(view) => view.as_ref(app).needs_height_measurement(),
})
}

Expand Down Expand Up @@ -1178,16 +1175,35 @@ impl TuiAIBlock {
ctx: &mut TuiLayoutContext,
app: &AppContext,
) -> usize {
let command_extents = self
.action_views
.values()
.filter_map(|view| match view {
TuiToolCallView::ShellCommand(view) => {
Some((view, view.as_ref(app).dynamic_content_extent()))
}
TuiToolCallView::AskQuestion(_)
| TuiToolCallView::FileEdits(_)
| TuiToolCallView::Generic(_)
| TuiToolCallView::Plan(_)
| TuiToolCallView::OrchestrationBlock(_) => None,
})
.collect::<Vec<_>>();
let mut element = self.render_element(app);
usize::from(
let height = usize::from(
element
.layout(
TuiConstraint::loose(TuiSize::new(width, u16::MAX)),
ctx,
app,
)
.height,
)
);
for (view, command_extent) in command_extents {
view.as_ref(app)
.record_content_extent_measurement(command_extent);
}
height
}

/// Logical (unwrapped) text for a selection over this block's text
Expand Down
117 changes: 112 additions & 5 deletions crates/warp_tui/src/benchmark_support.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ use std::sync::Arc;

use parking_lot::FairMutex;
use warp::tui_export::{
AIAgentExchangeId, AIAgentInput, AIAgentOutput, AIAgentOutputMessage, AIAgentOutputMessageType,
AIAgentText, AIAgentTextSection, AIBlockModel, AIBlockOutputStatus, AIConversationId,
AIRequestType, Appearance, BlockId, LLMId, MessageId, OutputStatusUpdateCallback,
RichContentItem, RichContentType, ServerOutputId, Shared, TerminalModel,
AIAgentAction, AIAgentActionId, AIAgentActionType, AIAgentExchangeId, AIAgentInput,
AIAgentOutput, AIAgentOutputMessage, AIAgentOutputMessageType, AIAgentText, AIAgentTextSection,
AIBlockModel, AIBlockOutputStatus, AIConversationId, AIRequestType, Appearance, BlockId, LLMId,
MessageId, OutputStatusUpdateCallback, RichContentItem, RichContentType, ServerOutputId,
Shared, TaskId, TerminalModel,
};
use warpui::platform::WindowStyle;
use warpui::{
Expand All @@ -30,6 +31,7 @@ use crate::tui_block_list_viewport_source::{
AgentBlockRegistry, CLISubagentBlockRegistry, HandoffBlockRegistry, TuiBlockListViewportSource,
};
use crate::tui_builder::TuiUiBuilder;
use crate::tui_shell_command_view::TuiShellCommandViewAction;

/// Shape of the retained transcript fixture.
#[derive(Clone, Copy, Debug)]
Expand All @@ -38,6 +40,8 @@ pub enum TranscriptDataset {
ManySmallBlocks { blocks: usize },
/// One rich agent response containing `rows` hard lines.
LongAgentResponse { rows: usize },
/// One expanded agent-requested command that remains active at `rows` output rows.
RunningAgentCommand { rows: usize },
/// A streaming rich response below `preceding_rows` of fixed history.
OffscreenStreamingTail {
preceding_rows: usize,
Expand Down Expand Up @@ -130,6 +134,24 @@ impl TranscriptBenchmark {
.lock()
.simulate_block("history", output.as_str());
}
let running_command_action =
if let TranscriptDataset::RunningAgentCommand { rows } = dataset {
let action = benchmark_command_action();
let output = "running command output\r\n".repeat(rows);
let mut model = terminal_model.lock();
model.simulate_long_running_block("printf benchmark", output.as_str());
model
.block_list_mut()
.active_block_mut()
.set_agent_interaction_mode_for_requested_command(
action.id.clone(),
None,
AIConversationId::new(),
);
Some(action)
} else {
None
};

let agent_blocks = AgentBlockRegistry::new(RefCell::new(HashMap::new()));
let cli_subagent_blocks = CLISubagentBlockRegistry::new(RefCell::new(HashMap::new()));
Expand Down Expand Up @@ -167,7 +189,8 @@ impl TranscriptBenchmark {
TranscriptDataset::OffscreenStreamingTail { .. } => {
streaming_text_status(text)
}
TranscriptDataset::ManySmallBlocks { .. } => unreachable!(),
TranscriptDataset::ManySmallBlocks { .. }
| TranscriptDataset::RunningAgentCommand { .. } => unreachable!(),
};
let terminal_model_for_block = terminal_model.clone();
let agent_block = app.update(|ctx| {
Expand All @@ -191,6 +214,48 @@ impl TranscriptBenchmark {
);
Some(view_id)
}
TranscriptDataset::RunningAgentCommand { .. } => {
let (action_model, model_events) = add_test_action_model_and_events(&mut app);
let action =
running_command_action.expect("running command dataset has an action");
let status = running_command_status(action);
let terminal_model_for_block = terminal_model.clone();
let agent_block = app.update(|ctx| {
ctx.add_typed_action_tui_view(window_id, move |ctx| {
TuiAIBlock::new(
(AIConversationId::new(), AIAgentExchangeId::new()),
Rc::new(BenchmarkAgentBlockModel { status }),
action_model,
&model_events,
terminal_model_for_block,
false,
ctx,
)
})
});
let view_id = agent_block.id();
agent_blocks
.borrow_mut()
.insert(view_id, agent_block.clone());
terminal_model.lock().block_list_mut().append_rich_content(
RichContentItem::new(Some(RichContentType::AIBlock), view_id, None, false),
false,
);
app.update(|ctx| {
let shell_view_id = agent_block
.as_ref(ctx)
.child_view_ids(ctx)
.into_iter()
.next()
.expect("running command agent block has a shell child");
ctx.dispatch_typed_action_for_view(
window_id,
shell_view_id,
&TuiShellCommandViewAction::ToggleExpanded,
);
});
Some(view_id)
}
};

let mut benchmark = Self {
Expand All @@ -217,6 +282,18 @@ impl TranscriptBenchmark {
})
}

/// Re-renders the transcript viewport without dirtying retained agent blocks.
pub fn invalidate_viewport(&mut self) {
let invalidation = WindowInvalidation {
updated: EntityIdSet::from_iter([self.root.id()]),
..Default::default()
};
self.app.read(|ctx| {
self.presenter
.invalidate(&invalidation, ctx, self.root.window_id(ctx));
});
}

/// Re-renders the root and long agent block before the next frame.
pub fn invalidate_all(&mut self) {
let mut updated = EntityIdSet::from_iter([self.root.id()]);
Expand Down Expand Up @@ -346,6 +423,36 @@ impl AIBlockModel for BenchmarkAgentBlockModel {
}
}

fn benchmark_command_action() -> AIAgentAction {
AIAgentAction {
id: AIAgentActionId::from("benchmark-command".to_owned()),
task_id: TaskId::new("benchmark-task".to_owned()),
action: AIAgentActionType::RequestCommandOutput {
command: "printf benchmark".to_owned(),
is_read_only: Some(true),
is_risky: Some(false),
wait_until_completion: true,
uses_pager: Some(false),
rationale: None,
citations: Vec::new(),
},
requires_result: true,
}
}

fn running_command_status(action: AIAgentAction) -> AIBlockOutputStatus {
AIBlockOutputStatus::Complete {
output: Shared::new(AIAgentOutput {
messages: vec![AIAgentOutputMessage {
id: MessageId::new("benchmark-command-message".to_owned()),
message: AIAgentOutputMessageType::Action(action),
citations: Vec::new(),
}],
..Default::default()
}),
}
}

fn completed_text_status(text: String) -> AIBlockOutputStatus {
AIBlockOutputStatus::Complete {
output: Shared::new(AIAgentOutput {
Expand Down
101 changes: 56 additions & 45 deletions crates/warp_tui/src/tui_block_list_viewport_source.rs
Original file line number Diff line number Diff line change
Expand Up @@ -103,64 +103,75 @@ impl TuiBlockListViewportSource {
///
/// A non-dirty band block is re-measured only when its cached height cannot
/// be trusted: its last measurement was at a different width (reflow), it
/// has never been measured (no recorded width), or it is still streaming
/// (its height can grow without a per-update invalidation — e.g. an
/// expanded, still-running shell command). At a stable width with no
/// dynamic height, nothing extra is measured and the cached
/// `last_laid_out_height` is reused. Off-band blocks keep their cached
/// height until they scroll into the band.
/// has never been measured (no recorded width), or an expanded running
/// shell command's terminal row extent changed. Agent output updates dirty
/// their block directly, so streaming status alone does not require layout
/// polling. At a stable width with no dynamic height change, the cached
/// height is reused. Off-band blocks keep their cached height until they
/// scroll into the band.
fn agent_heights_to_measure(
&self,
window: TuiViewportWindow,
available_width: u16,
app: &AppContext,
) -> HashSet<EntityId> {
let mut model = self.model.lock();
let mut view_ids = model.block_list_mut().take_dirty_rich_content_items();
let mut view_ids = self
.model
.lock()
.block_list_mut()
.take_dirty_rich_content_items();
let band_view_ids = {
let model = self.model.lock();
let block_list = model.block_list();
let band_top = window.scroll_top.saturating_sub(OVERHANG_ROWS);
let band_bottom = window
.scroll_top
.saturating_add(usize::from(window.viewport_height))
.saturating_add(OVERHANG_ROWS);
let mut cursor = block_list
.block_heights()
.cursor::<BlockHeight, BlockHeightSummary>();
cursor.seek_clamped(&BlockHeight::from(band_top as f64), SeekBias::Left);
let mut band_view_ids = Vec::new();
while let Some(item) = cursor.item() {
let item_top = cursor.start().height.as_f64().floor().max(0.0) as usize;
if item_top >= band_bottom {
break;
}
let item_bottom = item_top.saturating_add(item.height().as_f64().ceil() as usize);
if item_bottom > band_top
&& let BlockHeightItem::RichContent(rich_content) = item
&& !rich_content.should_hide
{
band_view_ids.push(rich_content.view_id);
}
cursor.next();
}
band_view_ids
};

let agent_blocks = self.agent_blocks.borrow();
let cli_subagent_blocks = self.cli_subagent_blocks.borrow();
let handoff_blocks = self.handoff_blocks.borrow();
let block_list = model.block_list();
let band_top = window.scroll_top.saturating_sub(OVERHANG_ROWS);
let band_bottom = window
.scroll_top
.saturating_add(usize::from(window.viewport_height))
.saturating_add(OVERHANG_ROWS);
let mut cursor = block_list
.block_heights()
.cursor::<BlockHeight, BlockHeightSummary>();
cursor.seek_clamped(&BlockHeight::from(band_top as f64), SeekBias::Left);
while let Some(item) = cursor.item() {
let item_top = cursor.start().height.as_f64().floor().max(0.0) as usize;
if item_top >= band_bottom {
break;
}
let item_bottom = item_top.saturating_add(item.height().as_f64().ceil() as usize);
if item_bottom > band_top
&& let BlockHeightItem::RichContent(rich_content) = item
&& !rich_content.should_hide
{
if let Some(view) = agent_blocks.get(&rich_content.view_id) {
if view
.as_ref(app)
.needs_height_measurement(available_width, app)
{
view_ids.insert(rich_content.view_id);
}
} else if let Some(view) = cli_subagent_blocks.get(&rich_content.view_id)
&& view
.as_ref(app)
.needs_height_measurement(available_width, app)
for view_id in band_view_ids {
if let Some(view) = agent_blocks.get(&view_id) {
if view
.as_ref(app)
.needs_height_measurement(available_width, app)
{
view_ids.insert(rich_content.view_id);
} else if let Some(view) = handoff_blocks.get(&rich_content.view_id)
&& view.as_ref(app).needs_height_measurement(available_width)
{
view_ids.insert(rich_content.view_id);
view_ids.insert(view_id);
}
} else if let Some(view) = cli_subagent_blocks.get(&view_id)
&& view
.as_ref(app)
.needs_height_measurement(available_width, app)
{
view_ids.insert(view_id);
} else if let Some(view) = handoff_blocks.get(&view_id)
&& view.as_ref(app).needs_height_measurement(available_width)
{
view_ids.insert(view_id);
}
cursor.next();
}
view_ids
}
Expand Down
Loading
Loading