mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
perf(tui): batch clear buffer regions with helper
This commit is contained in:
@@ -4,6 +4,7 @@ use crate::app_event::AppEvent;
|
||||
use crate::app_event_sender::AppEventSender;
|
||||
use crate::user_approval_widget::ApprovalRequest;
|
||||
use bottom_pane_view::BottomPaneView;
|
||||
use crate::util::buffer::fill_rect;
|
||||
use codex_core::protocol::TokenUsage;
|
||||
use codex_file_search::FileMatch;
|
||||
use crossterm::event::KeyEvent;
|
||||
@@ -716,11 +717,7 @@ impl WidgetRef for &BottomPane<'_> {
|
||||
let base_style = ratatui::style::Style::default()
|
||||
.bg(crate::colors::background())
|
||||
.fg(crate::colors::text());
|
||||
for y in area.y..area.y.saturating_add(area.height) {
|
||||
for x in area.x..area.x.saturating_add(area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(base_style);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, area, Some(' '), base_style);
|
||||
|
||||
let mut y_offset = 0u16;
|
||||
if let Some(ring) = &self.live_ring {
|
||||
@@ -767,11 +764,7 @@ impl WidgetRef for &BottomPane<'_> {
|
||||
};
|
||||
// Ensure view background is painted under its content
|
||||
let view_bg = ratatui::style::Style::default().bg(crate::colors::background());
|
||||
for y in view_rect.y..view_rect.y.saturating_add(view_rect.height) {
|
||||
for x in view_rect.x..view_rect.x.saturating_add(view_rect.width) {
|
||||
buf[(x, y)].set_style(view_bg);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, view_rect, None, view_bg);
|
||||
view.render(view_rect, buf);
|
||||
}
|
||||
return;
|
||||
@@ -784,22 +777,18 @@ impl WidgetRef for &BottomPane<'_> {
|
||||
|
||||
// Add horizontal padding (2 chars on each side) for Message input
|
||||
let horizontal_padding = 1u16;
|
||||
let composer_rect = Rect {
|
||||
x: area.x + horizontal_padding,
|
||||
y: area.y + y_offset,
|
||||
width: area.width.saturating_sub(horizontal_padding * 2),
|
||||
// Reserve bottom padding
|
||||
height: (area.height - y_offset)
|
||||
- BottomPane::BOTTOM_PAD_LINES.min((area.height - y_offset).saturating_sub(1)),
|
||||
};
|
||||
// Paint the composer area background before rendering widgets
|
||||
let comp_bg = ratatui::style::Style::default().bg(crate::colors::background());
|
||||
for y in composer_rect.y..composer_rect.y.saturating_add(composer_rect.height) {
|
||||
for x in composer_rect.x..composer_rect.x.saturating_add(composer_rect.width) {
|
||||
buf[(x, y)].set_style(comp_bg);
|
||||
}
|
||||
}
|
||||
(&self.composer).render_ref(composer_rect, buf);
|
||||
let composer_rect = Rect {
|
||||
x: area.x + horizontal_padding,
|
||||
y: area.y + y_offset,
|
||||
width: area.width.saturating_sub(horizontal_padding * 2),
|
||||
// Reserve bottom padding
|
||||
height: (area.height - y_offset)
|
||||
- BottomPane::BOTTOM_PAD_LINES.min((area.height - y_offset).saturating_sub(1)),
|
||||
};
|
||||
// Paint the composer area background before rendering widgets
|
||||
let comp_bg = ratatui::style::Style::default().bg(crate::colors::background());
|
||||
fill_rect(buf, composer_rect, None, comp_bg);
|
||||
(&self.composer).render_ref(composer_rect, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::util::buffer::fill_rect;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
use crossterm::event::KeyEventKind;
|
||||
@@ -923,17 +924,18 @@ impl TextArea {
|
||||
) {
|
||||
let bg = crate::colors::background();
|
||||
let fg = crate::colors::text();
|
||||
let line_style = Style::default().bg(bg).fg(fg);
|
||||
for (row, idx) in range.enumerate() {
|
||||
let r = &lines[idx];
|
||||
let y = area.y + row as u16;
|
||||
// Fill the entire row area with theme background to avoid terminal default leaking through.
|
||||
let line_bg = Style::default().bg(bg).fg(fg);
|
||||
for x in area.x..area.x + area.width {
|
||||
buf[(x, y)].set_style(line_bg);
|
||||
}
|
||||
// Paint the row background in one pass to avoid per-cell index math.
|
||||
fill_rect(buf, Rect::new(area.x, y, area.width, 1), None, line_style);
|
||||
|
||||
// Draw the text on top using theme foreground + background to preserve consistent look.
|
||||
let line_range = r.start..r.end - 1;
|
||||
buf.set_string(area.x, y, &self.text[line_range.clone()], line_bg);
|
||||
if r.end > r.start {
|
||||
let line_range = r.start..r.end - 1;
|
||||
buf.set_string(area.x, y, &self.text[line_range], line_style);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+63
-102
@@ -134,6 +134,7 @@ use crate::sanitize::Options as SanitizeOptions;
|
||||
use crate::sanitize::sanitize_for_tui;
|
||||
use crate::streaming::StreamKind;
|
||||
use crate::streaming::controller::AppEventHistorySink;
|
||||
use crate::util::buffer::fill_rect;
|
||||
use crate::user_approval_widget::ApprovalRequest;
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_browser::BrowserManager;
|
||||
@@ -1242,10 +1243,7 @@ impl ChatWidget<'_> {
|
||||
crate::markdown::append_markdown(text, &mut lines, &self.config);
|
||||
let key = self.next_internal_key();
|
||||
let _ = self.history_insert_with_key_global(
|
||||
Box::new(PlainHistoryCell {
|
||||
lines,
|
||||
kind: HistoryCellType::Assistant,
|
||||
}),
|
||||
Box::new(PlainHistoryCell::new(lines, HistoryCellType::Assistant)),
|
||||
key,
|
||||
);
|
||||
}
|
||||
@@ -1614,6 +1612,7 @@ impl ChatWidget<'_> {
|
||||
plain.kind = history_cell::HistoryCellType::Patch {
|
||||
kind: history_cell::PatchKind::ApplySuccess,
|
||||
};
|
||||
plain.invalidate_layout_cache();
|
||||
self.request_redraw();
|
||||
return;
|
||||
}
|
||||
@@ -3012,10 +3011,10 @@ impl ChatWidget<'_> {
|
||||
combined.extend(body);
|
||||
self.history_replace_at(
|
||||
idx - 1,
|
||||
Box::new(crate::history_cell::PlainHistoryCell {
|
||||
lines: combined,
|
||||
kind: crate::history_cell::HistoryCellType::Plain,
|
||||
}),
|
||||
Box::new(crate::history_cell::PlainHistoryCell::new(
|
||||
combined,
|
||||
crate::history_cell::HistoryCellType::Plain,
|
||||
)),
|
||||
);
|
||||
self.history_remove_at(idx);
|
||||
}
|
||||
@@ -3193,10 +3192,10 @@ impl ChatWidget<'_> {
|
||||
"command: {}",
|
||||
original_text.trim()
|
||||
)));
|
||||
self.history_push(crate::history_cell::PlainHistoryCell {
|
||||
lines: ack,
|
||||
kind: crate::history_cell::HistoryCellType::Notice,
|
||||
});
|
||||
self.history_push(crate::history_cell::PlainHistoryCell::new(
|
||||
ack,
|
||||
crate::history_cell::HistoryCellType::Notice,
|
||||
));
|
||||
|
||||
message
|
||||
.ordered_items
|
||||
@@ -3248,10 +3247,10 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
)));
|
||||
lines.push(Line::from(format!("command: {}", original_text.trim())));
|
||||
self.history_push(crate::history_cell::PlainHistoryCell {
|
||||
self.history_push(crate::history_cell::PlainHistoryCell::new(
|
||||
lines,
|
||||
kind: crate::history_cell::HistoryCellType::Notice,
|
||||
});
|
||||
crate::history_cell::HistoryCellType::Notice,
|
||||
));
|
||||
|
||||
// Replace the message with the resolved prompt
|
||||
message
|
||||
@@ -4526,15 +4525,15 @@ impl ChatWidget<'_> {
|
||||
return;
|
||||
}
|
||||
if tool_name == "wait" && !success && content.trim() == "Cancelled by user." {
|
||||
let wait_cancelled_cell = PlainHistoryCell {
|
||||
lines: vec![Line::styled(
|
||||
let wait_cancelled_cell = PlainHistoryCell::new(
|
||||
vec![Line::styled(
|
||||
"Wait cancelled",
|
||||
Style::default()
|
||||
.fg(crate::colors::error())
|
||||
.add_modifier(Modifier::BOLD),
|
||||
)],
|
||||
kind: HistoryCellType::Error,
|
||||
};
|
||||
HistoryCellType::Error,
|
||||
);
|
||||
|
||||
if let Some(idx) = entry_idx {
|
||||
if idx < self.history_cells.len() {
|
||||
@@ -4818,10 +4817,10 @@ impl ChatWidget<'_> {
|
||||
for l in text.lines() {
|
||||
lines.push(ratatui::text::Line::from(l.to_string()))
|
||||
}
|
||||
self.history_push(crate::history_cell::PlainHistoryCell {
|
||||
self.history_push(crate::history_cell::PlainHistoryCell::new(
|
||||
lines,
|
||||
kind: crate::history_cell::HistoryCellType::Notice,
|
||||
});
|
||||
crate::history_cell::HistoryCellType::Notice,
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn add_diff_output(&mut self, diff_output: String) {
|
||||
@@ -5015,10 +5014,10 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
self.history_push(crate::history_cell::PlainHistoryCell {
|
||||
self.history_push(crate::history_cell::PlainHistoryCell::new(
|
||||
lines,
|
||||
kind: crate::history_cell::HistoryCellType::Notice,
|
||||
});
|
||||
crate::history_cell::HistoryCellType::Notice,
|
||||
));
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
@@ -7091,6 +7090,7 @@ impl ChatWidget<'_> {
|
||||
.downcast_mut::<history_cell::PlainHistoryCell>()
|
||||
{
|
||||
history_cell::retint_lines_in_place(&mut plain.lines, &old, &new);
|
||||
plain.invalidate_layout_cache();
|
||||
} else if let Some(tool) = cell
|
||||
.as_any_mut()
|
||||
.downcast_mut::<history_cell::ToolCallCell>()
|
||||
@@ -8184,10 +8184,10 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
|
||||
// Add status message
|
||||
self.history_push(history_cell::PlainHistoryCell {
|
||||
lines: vec![Line::from("✅ Chrome launched with user profile")],
|
||||
kind: history_cell::HistoryCellType::BackgroundEvent,
|
||||
});
|
||||
self.history_push(history_cell::PlainHistoryCell::new(
|
||||
vec![Line::from("✅ Chrome launched with user profile")],
|
||||
history_cell::HistoryCellType::BackgroundEvent,
|
||||
));
|
||||
// Show browsing state in input border after launch
|
||||
self.bottom_pane
|
||||
.update_status_text("using browser".to_string());
|
||||
@@ -8809,13 +8809,13 @@ impl ChatWidget<'_> {
|
||||
}
|
||||
|
||||
// Add status message
|
||||
self.history_push(history_cell::PlainHistoryCell {
|
||||
lines: vec![Line::from(format!(
|
||||
self.history_push(history_cell::PlainHistoryCell::new(
|
||||
vec![Line::from(format!(
|
||||
"✅ Chrome launched with temporary profile at {}",
|
||||
profile_dir.display()
|
||||
))],
|
||||
kind: history_cell::HistoryCellType::BackgroundEvent,
|
||||
});
|
||||
history_cell::HistoryCellType::BackgroundEvent,
|
||||
));
|
||||
}
|
||||
|
||||
pub(crate) fn handle_browser_command(&mut self, command_text: String) {
|
||||
@@ -8918,10 +8918,10 @@ impl ChatWidget<'_> {
|
||||
|
||||
// Add status message
|
||||
let status_msg = format!("🌐 Opening internal browser: {}", full_url);
|
||||
self.history_push(history_cell::PlainHistoryCell {
|
||||
lines: vec![Line::from(status_msg)],
|
||||
kind: history_cell::HistoryCellType::BackgroundEvent,
|
||||
});
|
||||
self.history_push(history_cell::PlainHistoryCell::new(
|
||||
vec![Line::from(status_msg)],
|
||||
history_cell::HistoryCellType::BackgroundEvent,
|
||||
));
|
||||
// Also reflect browsing activity in the input border
|
||||
self.bottom_pane
|
||||
.update_status_text("using browser".to_string());
|
||||
@@ -9316,10 +9316,10 @@ impl ChatWidget<'_> {
|
||||
.lines()
|
||||
.map(|line| Line::from(line.to_string()))
|
||||
.collect();
|
||||
self.history_push(history_cell::PlainHistoryCell {
|
||||
self.history_push(history_cell::PlainHistoryCell::new(
|
||||
lines,
|
||||
kind: history_cell::HistoryCellType::BackgroundEvent,
|
||||
});
|
||||
history_cell::HistoryCellType::BackgroundEvent,
|
||||
));
|
||||
}
|
||||
|
||||
/// Handle `/mcp` command: manage MCP servers (status/on/off/add).
|
||||
@@ -9816,10 +9816,10 @@ impl ChatWidget<'_> {
|
||||
.lines()
|
||||
.map(|line| Line::from(line.to_string()))
|
||||
.collect();
|
||||
self.history_push(history_cell::PlainHistoryCell {
|
||||
self.history_push(history_cell::PlainHistoryCell::new(
|
||||
lines,
|
||||
kind: history_cell::HistoryCellType::BackgroundEvent,
|
||||
});
|
||||
history_cell::HistoryCellType::BackgroundEvent,
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -12764,31 +12764,22 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
.saturating_add(history_area.width)
|
||||
.saturating_sub(right_pad_start);
|
||||
if left_pad_w > 0 {
|
||||
for y in history_area.y..history_area.y.saturating_add(history_area.height) {
|
||||
for x in history_area.x..history_area.x.saturating_add(left_pad_w) {
|
||||
buf[(x, y)].set_char(' ').set_style(clear_style);
|
||||
}
|
||||
}
|
||||
let left_rect = Rect::new(history_area.x, history_area.y, left_pad_w, history_area.height);
|
||||
fill_rect(buf, left_rect, Some(' '), clear_style);
|
||||
cleared_cells =
|
||||
cleared_cells.saturating_add((left_pad_w as u64) * (history_area.height as u64));
|
||||
}
|
||||
if right_pad_w > 0 {
|
||||
for y in history_area.y..history_area.y.saturating_add(history_area.height) {
|
||||
for x in right_pad_start..right_pad_start.saturating_add(right_pad_w) {
|
||||
buf[(x, y)].set_char(' ').set_style(clear_style);
|
||||
}
|
||||
}
|
||||
let right_rect = Rect::new(right_pad_start, history_area.y, right_pad_w, history_area.height);
|
||||
fill_rect(buf, right_rect, Some(' '), clear_style);
|
||||
cleared_cells =
|
||||
cleared_cells.saturating_add((right_pad_w as u64) * (history_area.height as u64));
|
||||
}
|
||||
// Top gap inside content area when content is bottom-aligned
|
||||
if start_y > content_area.y {
|
||||
let gap_h = start_y.saturating_sub(content_area.y);
|
||||
for y in content_area.y..content_area.y.saturating_add(gap_h) {
|
||||
for x in content_area.x..content_area.x.saturating_add(content_area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(clear_style);
|
||||
}
|
||||
}
|
||||
let gap_rect = Rect::new(content_area.x, content_area.y, content_area.width, gap_h);
|
||||
fill_rect(buf, gap_rect, Some(' '), clear_style);
|
||||
cleared_cells =
|
||||
cleared_cells.saturating_add((gap_h as u64) * (content_area.width as u64));
|
||||
}
|
||||
@@ -12964,13 +12955,7 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
None
|
||||
};
|
||||
let style = Style::default().bg(gutter_bg);
|
||||
for y in gutter_area.y..gutter_area.y.saturating_add(gutter_area.height) {
|
||||
// Only the first column (symbol column) needs tint; the second is spacing to content
|
||||
// but tint both for visual continuity with the assistant block.
|
||||
for x in gutter_area.x..gutter_area.x.saturating_add(gutter_area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(style);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, gutter_area, Some(' '), style);
|
||||
// Also tint the single left padding column so the assistant
|
||||
// gutter visually reaches the outer edge. The content area
|
||||
// is inset by a uniform padding; when present, paint that
|
||||
@@ -12978,9 +12963,8 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
// vertical span of this item.
|
||||
if content_area.x > history_area.x {
|
||||
let left_col_x = content_area.x.saturating_sub(1);
|
||||
for y in gutter_area.y..gutter_area.y.saturating_add(gutter_area.height) {
|
||||
buf[(left_col_x, y)].set_char(' ').set_style(style);
|
||||
}
|
||||
let left_rect = Rect::new(left_col_x, gutter_area.y, 1, gutter_area.height);
|
||||
fill_rect(buf, left_rect, Some(' '), style);
|
||||
}
|
||||
// Also tint one column immediately to the right of the content area
|
||||
// so the assistant block is visually bookended. This column lives in the
|
||||
@@ -12989,9 +12973,8 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
let right_col_x = content_area.x.saturating_add(content_area.width);
|
||||
let history_right = history_area.x.saturating_add(history_area.width);
|
||||
if right_col_x < history_right {
|
||||
for y in item_area.y..item_area.y.saturating_add(item_area.height) {
|
||||
buf[(right_col_x, y)].set_char(' ').set_style(style);
|
||||
}
|
||||
let right_rect = Rect::new(right_col_x, item_area.y, 1, item_area.height);
|
||||
fill_rect(buf, right_rect, Some(' '), style);
|
||||
}
|
||||
if let Some(t0) = _perf_gutter_start {
|
||||
let dt = t0.elapsed().as_nanos();
|
||||
@@ -13218,10 +13201,10 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
for y in screen_y..content_area.y + content_area.height {
|
||||
for x in content_area.x..content_area.x + content_area.width {
|
||||
buf[(x, y)].set_char(' ').set_style(clear_style);
|
||||
}
|
||||
let gap_height = (content_area.y + content_area.height).saturating_sub(screen_y);
|
||||
if gap_height > 0 {
|
||||
let gap_rect = Rect::new(content_area.x, screen_y, content_area.width, gap_height);
|
||||
fill_rect(buf, gap_rect, Some(' '), clear_style);
|
||||
}
|
||||
if let Some(t0) = _perf_hist_clear2 {
|
||||
let dt = t0.elapsed().as_nanos();
|
||||
@@ -13284,11 +13267,7 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
|
||||
if self.terminal.overlay().is_some() {
|
||||
let bg_style = Style::default().bg(crate::colors::background());
|
||||
for y in bottom_pane_area.y..bottom_pane_area.y + bottom_pane_area.height {
|
||||
for x in bottom_pane_area.x..bottom_pane_area.x + bottom_pane_area.width {
|
||||
buf[(x, y)].set_style(bg_style).set_char(' ');
|
||||
}
|
||||
}
|
||||
fill_rect(buf, bottom_pane_area, Some(' '), bg_style);
|
||||
} else {
|
||||
// Render the bottom pane directly without a border for now
|
||||
// The composer has its own layout with hints at the bottom
|
||||
@@ -13299,11 +13278,7 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
let scrim_style = Style::default()
|
||||
.bg(crate::colors::overlay_scrim())
|
||||
.fg(crate::colors::text_dim());
|
||||
for y in area.y..area.y + area.height {
|
||||
for x in area.x..area.x + area.width {
|
||||
buf[(x, y)].set_style(scrim_style);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, area, None, scrim_style);
|
||||
|
||||
let padding = 1u16;
|
||||
let footer_reserved = 1.min(bottom_pane_area.height);
|
||||
@@ -13488,11 +13463,7 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
width: footer_area.width,
|
||||
height: footer_area.height.saturating_sub(1),
|
||||
};
|
||||
for y in spacer_area.y..spacer_area.y + spacer_area.height {
|
||||
for x in spacer_area.x..spacer_area.x + spacer_area.width {
|
||||
buf[(x, y)].set_char(' ').set_style(inner_bg);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, spacer_area, Some(' '), inner_bg);
|
||||
}
|
||||
|
||||
let instructions_area = Rect {
|
||||
@@ -13529,13 +13500,7 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
for y in area.y..area.y + area.height {
|
||||
for x in area.x..area.x + area.width {
|
||||
// Overwrite with a dimmed style; we don't Clear so existing glyphs remain,
|
||||
// but foreground is muted to reduce visual competition.
|
||||
buf[(x, y)].set_style(scrim_bg);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, area, None, scrim_bg);
|
||||
if let Some(t0) = _perf_scrim_start {
|
||||
let dt = t0.elapsed().as_nanos();
|
||||
let mut p = self.perf_state.stats.borrow_mut();
|
||||
@@ -13560,11 +13525,7 @@ impl WidgetRef for &ChatWidget<'_> {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
for y in area.y..area.y + area.height {
|
||||
for x in area.x..area.x + area.width {
|
||||
buf[(x, y)].set_style(bg_style);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, area, None, bg_style);
|
||||
if let Some(t0) = _perf_overlay_area_bg_start {
|
||||
let dt = t0.elapsed().as_nanos();
|
||||
let mut p = self.perf_state.stats.borrow_mut();
|
||||
|
||||
@@ -4,6 +4,7 @@ use crate::sanitize::Mode as SanitizeMode;
|
||||
use crate::sanitize::Options as SanitizeOptions;
|
||||
use crate::sanitize::sanitize_for_tui;
|
||||
use crate::slash_command::SlashCommand;
|
||||
use crate::util::buffer::fill_rect;
|
||||
use crate::text_formatting::format_json_compact;
|
||||
use base64::Engine;
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
@@ -190,11 +191,7 @@ pub(crate) trait HistoryCell {
|
||||
_ => crate::colors::background(),
|
||||
};
|
||||
let bg_style = Style::default().bg(cell_bg).fg(crate::colors::text());
|
||||
for y in area.y..area.y.saturating_add(area.height) {
|
||||
for x in area.x..area.x.saturating_add(area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(bg_style);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, area, Some(' '), bg_style);
|
||||
|
||||
// Ensure the entire allocated area is painted with the theme background
|
||||
// by attaching a background-styled Block to the Paragraph as well.
|
||||
@@ -1079,6 +1076,51 @@ impl HistoryCell for ExploreAggregationCell {
|
||||
pub(crate) struct PlainHistoryCell {
|
||||
pub(crate) lines: Vec<Line<'static>>,
|
||||
pub(crate) kind: HistoryCellType,
|
||||
cached_wrap: std::cell::RefCell<Option<PlainWrapCache>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct PlainWrapCache {
|
||||
requested_width: u16,
|
||||
effective_width: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
impl PlainHistoryCell {
|
||||
pub(crate) fn new(lines: Vec<Line<'static>>, kind: HistoryCellType) -> Self {
|
||||
Self {
|
||||
lines,
|
||||
kind,
|
||||
cached_wrap: std::cell::RefCell::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn invalidate_layout_cache(&self) {
|
||||
self.cached_wrap.borrow_mut().take();
|
||||
}
|
||||
|
||||
fn cache_desired_height(&self, requested_width: u16, effective_width: u16, height: u16) {
|
||||
*self.cached_wrap.borrow_mut() = Some(PlainWrapCache {
|
||||
requested_width,
|
||||
effective_width,
|
||||
height,
|
||||
});
|
||||
}
|
||||
|
||||
fn try_cached_height(&self, requested_width: u16, effective_width: u16) -> Option<u16> {
|
||||
self.cached_wrap
|
||||
.borrow()
|
||||
.as_ref()
|
||||
.and_then(|cache| {
|
||||
if cache.requested_width == requested_width
|
||||
&& cache.effective_width == effective_width
|
||||
{
|
||||
Some(cache.height)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl HistoryCell for PlainHistoryCell {
|
||||
@@ -1114,23 +1156,31 @@ impl HistoryCell for PlainHistoryCell {
|
||||
}
|
||||
|
||||
fn desired_height(&self, width: u16) -> u16 {
|
||||
if matches!(self.kind, HistoryCellType::User) {
|
||||
let effective_width = if matches!(self.kind, HistoryCellType::User) {
|
||||
// Match input composer wrapping by reserving shared right padding.
|
||||
// Keep this in sync with the composer constants.
|
||||
let inner_w = width.saturating_sub(crate::layout_consts::USER_HISTORY_RIGHT_PAD.into());
|
||||
let text = Text::from(self.display_lines_trimmed());
|
||||
Paragraph::new(text)
|
||||
.wrap(Wrap { trim: false })
|
||||
.line_count(inner_w)
|
||||
.try_into()
|
||||
.unwrap_or(0)
|
||||
width.saturating_sub(crate::layout_consts::USER_HISTORY_RIGHT_PAD.into())
|
||||
} else {
|
||||
Paragraph::new(Text::from(self.display_lines_trimmed()))
|
||||
.wrap(Wrap { trim: false })
|
||||
.line_count(width)
|
||||
.try_into()
|
||||
.unwrap_or(0)
|
||||
width
|
||||
};
|
||||
|
||||
if let Some(height) = self.try_cached_height(width, effective_width) {
|
||||
return height;
|
||||
}
|
||||
|
||||
if effective_width == 0 {
|
||||
self.cache_desired_height(width, effective_width, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
let text = Text::from(self.display_lines_trimmed());
|
||||
let height: u16 = Paragraph::new(text)
|
||||
.wrap(Wrap { trim: false })
|
||||
.line_count(effective_width)
|
||||
.try_into()
|
||||
.unwrap_or(0);
|
||||
self.cache_desired_height(width, effective_width, height);
|
||||
height
|
||||
}
|
||||
|
||||
fn custom_render_with_skip(&self, area: Rect, buf: &mut Buffer, skip_rows: u16) {
|
||||
@@ -1144,11 +1194,7 @@ impl HistoryCell for PlainHistoryCell {
|
||||
let bg_style = Style::default().bg(cell_bg).fg(crate::colors::text());
|
||||
|
||||
// Clear area
|
||||
for y in area.y..area.y.saturating_add(area.height) {
|
||||
for x in area.x..area.x.saturating_add(area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(bg_style);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, area, Some(' '), bg_style);
|
||||
|
||||
let lines = self.display_lines_trimmed();
|
||||
let text = Text::from(lines);
|
||||
@@ -1434,11 +1480,7 @@ impl HistoryCell for AssistantMarkdownCell {
|
||||
let bg_style = Style::default().bg(cell_bg);
|
||||
|
||||
// Clear full area with assistant background
|
||||
for y in area.y..area.y.saturating_add(area.height) {
|
||||
for x in area.x..area.x.saturating_add(area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(bg_style);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, area, Some(' '), bg_style);
|
||||
|
||||
// Build or reuse cached segments for this width
|
||||
let plan = self.ensure_layout(area.width);
|
||||
@@ -1786,11 +1828,7 @@ impl HistoryCell for ExecCell {
|
||||
let bg_style = Style::default()
|
||||
.bg(crate::colors::background())
|
||||
.fg(crate::colors::text());
|
||||
for y in pre_area.y..pre_area.y.saturating_add(pre_area.height) {
|
||||
for x in pre_area.x..pre_area.x.saturating_add(pre_area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(bg_style);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, pre_area, Some(' '), bg_style);
|
||||
let pre_block =
|
||||
Block::default().style(Style::default().bg(crate::colors::background()));
|
||||
Paragraph::new(pre_text)
|
||||
@@ -1813,11 +1851,7 @@ impl HistoryCell for ExecCell {
|
||||
let bg_style = Style::default()
|
||||
.bg(crate::colors::background())
|
||||
.fg(crate::colors::text_dim());
|
||||
for y in out_area.y..out_area.y.saturating_add(out_area.height) {
|
||||
for x in out_area.x..out_area.x.saturating_add(out_area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(bg_style);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, out_area, Some(' '), bg_style);
|
||||
let block = Block::default()
|
||||
.borders(Borders::LEFT)
|
||||
.border_style(
|
||||
@@ -1858,9 +1892,7 @@ impl HistoryCell for ExecCell {
|
||||
height: status_height,
|
||||
};
|
||||
let bg_style = Style::default().bg(crate::colors::background());
|
||||
for x in status_area.x..status_area.x.saturating_add(status_area.width) {
|
||||
buf[(x, status_area.y)].set_char(' ').set_style(bg_style);
|
||||
}
|
||||
fill_rect(buf, status_area, Some(' '), bg_style);
|
||||
Paragraph::new(Text::from(vec![line]))
|
||||
.wrap(Wrap { trim: false })
|
||||
.style(bg_style)
|
||||
@@ -2041,11 +2073,7 @@ impl HistoryCell for DiffCell {
|
||||
// Hard clear the entire area: write spaces + background so any
|
||||
// previously longer content does not bleed into shorter frames.
|
||||
let bg = Style::default().bg(crate::colors::background());
|
||||
for y in area.y..area.y.saturating_add(area.height) {
|
||||
for x in area.x..area.x.saturating_add(area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(bg);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, area, Some(' '), bg);
|
||||
|
||||
// Center the sign in the two-column gutter by leaving one leading
|
||||
// space and drawing the sign in the second column.
|
||||
@@ -2698,11 +2726,7 @@ impl HistoryCell for MergedExecCell {
|
||||
.bg(crate::colors::background())
|
||||
.fg(crate::colors::text());
|
||||
// Hard clear area first
|
||||
for y in area.y..area.y.saturating_add(area.height) {
|
||||
for x in area.x..area.x.saturating_add(area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(bg);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, area, Some(' '), bg);
|
||||
|
||||
// Build one header line based on exec kind
|
||||
let header_line = match self.kind {
|
||||
@@ -4448,11 +4472,7 @@ impl HistoryCell for CollapsibleReasoningCell {
|
||||
let bg_style = Style::default()
|
||||
.bg(crate::colors::background())
|
||||
.fg(crate::colors::text());
|
||||
for y in area.y..area.y.saturating_add(area.height) {
|
||||
for x in area.x..area.x.saturating_add(area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(bg_style);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, area, Some(' '), bg_style);
|
||||
let lines = self.display_lines_trimmed();
|
||||
Paragraph::new(Text::from(lines))
|
||||
.block(Block::default().style(Style::default().bg(crate::colors::background())))
|
||||
@@ -4481,11 +4501,7 @@ impl HistoryCell for CollapsibleReasoningCell {
|
||||
// Clear area
|
||||
let bg = crate::colors::background();
|
||||
let bg_style = Style::default().bg(bg).fg(dim);
|
||||
for y in area.y..area.y.saturating_add(area.height) {
|
||||
for x in area.x..area.x.saturating_add(area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(bg_style);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, area, Some(' '), bg_style);
|
||||
|
||||
// Left border like exec output: 1px border + 1px left padding
|
||||
let block = Block::default()
|
||||
@@ -4556,11 +4572,7 @@ impl HistoryCell for StreamingContentCell {
|
||||
let bg_style = Style::default().bg(cell_bg);
|
||||
|
||||
// Hard clear area with assistant background
|
||||
for y in area.y..area.y.saturating_add(area.height) {
|
||||
for x in area.x..area.x.saturating_add(area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(bg_style);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, area, Some(' '), bg_style);
|
||||
|
||||
// Build or reuse cached segments for this width
|
||||
let plan = self.ensure_stream_layout(area.width);
|
||||
@@ -5673,10 +5685,7 @@ pub(crate) fn new_background_event(message: String) -> PlainHistoryCell {
|
||||
let msg_norm = normalize_overwrite_sequences(&message);
|
||||
lines.extend(msg_norm.lines().map(|line| ansi_escape_line(line).dim()));
|
||||
// No empty line at end - trimming and spacing handled by renderer
|
||||
PlainHistoryCell {
|
||||
lines,
|
||||
kind: HistoryCellType::BackgroundEvent,
|
||||
}
|
||||
PlainHistoryCell::new(lines, HistoryCellType::BackgroundEvent)
|
||||
}
|
||||
|
||||
pub(crate) fn new_session_info(
|
||||
@@ -5696,15 +5705,9 @@ pub(crate) fn new_session_info(
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
lines.push(Line::from("notice".dim()));
|
||||
lines.extend(popular_commands_lines());
|
||||
PlainHistoryCell {
|
||||
lines,
|
||||
kind: HistoryCellType::Notice,
|
||||
}
|
||||
PlainHistoryCell::new(lines, HistoryCellType::Notice)
|
||||
} else if config.model == model {
|
||||
PlainHistoryCell {
|
||||
lines: Vec::new(),
|
||||
kind: HistoryCellType::Notice,
|
||||
}
|
||||
PlainHistoryCell::new(Vec::new(), HistoryCellType::Notice)
|
||||
} else {
|
||||
let lines = vec![
|
||||
Line::from("model changed:")
|
||||
@@ -5714,10 +5717,7 @@ pub(crate) fn new_session_info(
|
||||
Line::from(format!("used: {model}")),
|
||||
// No empty line at end - trimming and spacing handled by renderer
|
||||
];
|
||||
PlainHistoryCell {
|
||||
lines,
|
||||
kind: HistoryCellType::Notice,
|
||||
}
|
||||
PlainHistoryCell::new(lines, HistoryCellType::Notice)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5795,10 +5795,7 @@ pub(crate) fn new_popular_commands_notice(_connecting_mcp: bool) -> PlainHistory
|
||||
lines.extend(popular_commands_lines());
|
||||
// Connecting status is now rendered as a separate BackgroundEvent cell
|
||||
// with its own gutter icon and spacing. Keep this notice focused.
|
||||
PlainHistoryCell {
|
||||
lines,
|
||||
kind: HistoryCellType::Notice,
|
||||
}
|
||||
PlainHistoryCell::new(lines, HistoryCellType::Notice)
|
||||
}
|
||||
|
||||
/// Background status cell shown during startup while external MCP servers
|
||||
@@ -5814,10 +5811,7 @@ pub(crate) fn new_connecting_mcp_status() -> PlainHistoryCell {
|
||||
"Connecting MCP servers…",
|
||||
Style::default().fg(crate::colors::text_dim()),
|
||||
)));
|
||||
PlainHistoryCell {
|
||||
lines,
|
||||
kind: HistoryCellType::BackgroundEvent,
|
||||
}
|
||||
PlainHistoryCell::new(lines, HistoryCellType::BackgroundEvent)
|
||||
}
|
||||
|
||||
pub(crate) fn new_user_prompt(message: String) -> PlainHistoryCell {
|
||||
@@ -5842,10 +5836,7 @@ pub(crate) fn new_user_prompt(message: String) -> PlainHistoryCell {
|
||||
let content = trim_empty_lines(content);
|
||||
lines.extend(content);
|
||||
// No empty line at end - trimming and spacing handled by renderer
|
||||
PlainHistoryCell {
|
||||
lines,
|
||||
kind: HistoryCellType::User,
|
||||
}
|
||||
PlainHistoryCell::new(lines, HistoryCellType::User)
|
||||
}
|
||||
|
||||
/// Render a queued user message that will be sent in the next turn.
|
||||
@@ -5874,10 +5865,7 @@ pub(crate) fn new_queued_user_prompt(message: String) -> PlainHistoryCell {
|
||||
let content: Vec<Line<'static>> = sanitized.lines().map(|l| ansi_escape_line(l)).collect();
|
||||
let content = trim_empty_lines(content);
|
||||
lines.extend(content);
|
||||
PlainHistoryCell {
|
||||
lines,
|
||||
kind: HistoryCellType::User,
|
||||
}
|
||||
PlainHistoryCell::new(lines, HistoryCellType::User)
|
||||
}
|
||||
|
||||
/// Expand horizontal tabs to spaces using a fixed tab stop.
|
||||
@@ -5888,10 +5876,7 @@ pub(crate) fn new_queued_user_prompt(message: String) -> PlainHistoryCell {
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) fn new_text_line(line: Line<'static>) -> PlainHistoryCell {
|
||||
PlainHistoryCell {
|
||||
lines: vec![line],
|
||||
kind: HistoryCellType::Notice,
|
||||
}
|
||||
PlainHistoryCell::new(vec![line], HistoryCellType::Notice)
|
||||
}
|
||||
|
||||
pub(crate) fn new_streaming_content(lines: Vec<Line<'static>>) -> StreamingContentCell {
|
||||
@@ -8276,11 +8261,7 @@ impl HistoryCell for WebFetchToolCell {
|
||||
let bg_style = Style::default()
|
||||
.bg(crate::colors::background())
|
||||
.fg(crate::colors::text());
|
||||
for y in pre_area.y..pre_area.y.saturating_add(pre_area.height) {
|
||||
for x in pre_area.x..pre_area.x.saturating_add(pre_area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(bg_style);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, pre_area, Some(' '), bg_style);
|
||||
let pre_block =
|
||||
Block::default().style(Style::default().bg(crate::colors::background()));
|
||||
Paragraph::new(pre_text)
|
||||
@@ -8302,11 +8283,7 @@ impl HistoryCell for WebFetchToolCell {
|
||||
let bg_style = Style::default()
|
||||
.bg(crate::colors::background())
|
||||
.fg(crate::colors::text_dim());
|
||||
for y in body_area.y..body_area.y.saturating_add(body_area.height) {
|
||||
for x in body_area.x..body_area.x.saturating_add(body_area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(bg_style);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, body_area, Some(' '), bg_style);
|
||||
let block = Block::default()
|
||||
.borders(Borders::LEFT)
|
||||
.border_style(
|
||||
@@ -8948,10 +8925,7 @@ pub(crate) fn new_error_event(message: String) -> PlainHistoryCell {
|
||||
.map(|line| ansi_escape_line(line).style(Style::default().fg(crate::colors::error()))),
|
||||
);
|
||||
// No empty line at end - trimming and spacing handled by renderer
|
||||
PlainHistoryCell {
|
||||
lines,
|
||||
kind: HistoryCellType::Error,
|
||||
}
|
||||
PlainHistoryCell::new(lines, HistoryCellType::Error)
|
||||
}
|
||||
|
||||
pub(crate) fn new_diff_output(diff_output: String) -> DiffCell {
|
||||
@@ -8980,10 +8954,7 @@ pub(crate) fn new_reasoning_output(reasoning_effort: &ReasoningEffort) -> PlainH
|
||||
.bold(),
|
||||
Line::from(format!("Value: {}", reasoning_effort)),
|
||||
];
|
||||
PlainHistoryCell {
|
||||
lines,
|
||||
kind: HistoryCellType::Notice,
|
||||
}
|
||||
PlainHistoryCell::new(lines, HistoryCellType::Notice)
|
||||
}
|
||||
|
||||
pub(crate) fn new_model_output(model: &str, effort: ReasoningEffort) -> PlainHistoryCell {
|
||||
@@ -8995,10 +8966,7 @@ pub(crate) fn new_model_output(model: &str, effort: ReasoningEffort) -> PlainHis
|
||||
Line::from(format!("Model: {}", model)),
|
||||
Line::from(format!("Reasoning Effort: {}", effort)),
|
||||
];
|
||||
PlainHistoryCell {
|
||||
lines,
|
||||
kind: HistoryCellType::Notice,
|
||||
}
|
||||
PlainHistoryCell::new(lines, HistoryCellType::Notice)
|
||||
}
|
||||
|
||||
// Continue with more factory functions...
|
||||
@@ -9196,10 +9164,7 @@ pub(crate) fn new_status_output(
|
||||
}
|
||||
}
|
||||
|
||||
PlainHistoryCell {
|
||||
lines,
|
||||
kind: HistoryCellType::Notice,
|
||||
}
|
||||
PlainHistoryCell::new(lines, HistoryCellType::Notice)
|
||||
}
|
||||
|
||||
pub(crate) fn new_prompts_output() -> PlainHistoryCell {
|
||||
@@ -9214,10 +9179,7 @@ pub(crate) fn new_prompts_output() -> PlainHistoryCell {
|
||||
Line::from(" 6. Improve documentation in @filename"),
|
||||
Line::from(""),
|
||||
];
|
||||
PlainHistoryCell {
|
||||
lines,
|
||||
kind: HistoryCellType::Notice,
|
||||
}
|
||||
PlainHistoryCell::new(lines, HistoryCellType::Notice)
|
||||
}
|
||||
|
||||
fn plan_progress_icon(total: usize, completed: usize) -> &'static str {
|
||||
@@ -9378,12 +9340,12 @@ pub(crate) fn new_patch_apply_failure(stderr: String) -> PlainHistoryCell {
|
||||
}
|
||||
|
||||
lines.push(Line::from(""));
|
||||
PlainHistoryCell {
|
||||
PlainHistoryCell::new(
|
||||
lines,
|
||||
kind: HistoryCellType::Patch {
|
||||
HistoryCellType::Patch {
|
||||
kind: PatchKind::ApplyFailure,
|
||||
},
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// ==================== PatchSummaryCell ====================
|
||||
|
||||
@@ -66,6 +66,7 @@ mod text_formatting;
|
||||
mod text_processing;
|
||||
mod theme;
|
||||
mod util {
|
||||
pub mod buffer;
|
||||
pub mod list_window;
|
||||
}
|
||||
mod spinner;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
use std::io::Result;
|
||||
|
||||
use crate::insert_history;
|
||||
use crate::util::buffer::fill_rect;
|
||||
use crate::tui;
|
||||
use crossterm::event::KeyCode;
|
||||
use crossterm::event::KeyEvent;
|
||||
@@ -154,11 +155,7 @@ impl TranscriptApp {
|
||||
let clear_style = Style::default()
|
||||
.bg(crate::colors::background())
|
||||
.fg(crate::colors::text());
|
||||
for y in content_area.y..content_area.y.saturating_add(content_area.height) {
|
||||
for x in content_area.x..content_area.x.saturating_add(content_area.width) {
|
||||
buf[(x, y)].set_char(' ').set_style(clear_style);
|
||||
}
|
||||
}
|
||||
fill_rect(buf, content_area, Some(' '), clear_style);
|
||||
|
||||
// Clamp scroll offset to valid range
|
||||
self.scroll_offset = self
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
use ratatui::buffer::Buffer;
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Style;
|
||||
|
||||
/// Fill the given rectangular area with the provided style and optional character.
|
||||
///
|
||||
/// The rectangle is clipped to the buffer's bounds before being applied. When `fill_char`
|
||||
/// is `Some(_)`, each cell's symbol is replaced; otherwise only the style is updated.
|
||||
pub fn fill_rect(buf: &mut Buffer, area: Rect, fill_char: Option<char>, style: Style) {
|
||||
let rect = buf.area.intersection(area);
|
||||
if rect.width == 0 || rect.height == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let buf_width = buf.area.width as usize;
|
||||
let offset_x = rect.x.saturating_sub(buf.area.x) as usize;
|
||||
let offset_y = rect.y.saturating_sub(buf.area.y) as usize;
|
||||
let row_span = rect.width as usize;
|
||||
|
||||
for row in 0..rect.height as usize {
|
||||
let start = (offset_y + row) * buf_width + offset_x;
|
||||
let end = start + row_span;
|
||||
let row = &mut buf.content[start..end];
|
||||
for cell in row {
|
||||
cell.set_style(style);
|
||||
if let Some(ch) = fill_char {
|
||||
cell.set_char(ch);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user