Compare commits

...

2 Commits

Author SHA1 Message Date
kevin zhao
0edf70a181 clean up when re-rendering 2025-10-28 17:43:55 -07:00
kevin zhao
126156a56f fixing terminal rendering 2025-10-28 16:59:38 -07:00
5 changed files with 109 additions and 5 deletions

View File

@@ -68,6 +68,7 @@ pub(crate) struct App {
pub(crate) overlay: Option<Overlay>,
pub(crate) deferred_history_lines: Vec<Line<'static>>,
has_emitted_history_lines: bool,
rendered_history_width: Option<u16>,
pub(crate) enhanced_keys_supported: bool,
@@ -164,6 +165,7 @@ impl App {
overlay: None,
deferred_history_lines: Vec::new(),
has_emitted_history_lines: false,
rendered_history_width: None,
commit_anim_running: Arc::new(AtomicBool::new(false)),
backtrack: BacktrackState::default(),
feedback: feedback.clone(),
@@ -231,6 +233,12 @@ impl App {
{
return Ok(true);
}
if self.overlay.is_none() {
let screen_width = tui.terminal.size()?.width;
if self.rendered_history_width != Some(screen_width) {
self.reflow_transcript(tui, screen_width)?;
}
}
tui.draw(
self.chat_widget.desired_height(tui.terminal.size()?.width),
|frame| {
@@ -534,6 +542,43 @@ impl App {
}
};
}
pub(crate) fn transcript_lines_for_width(&self, width: u16) -> (Vec<Line<'static>>, bool) {
let mut lines: Vec<Line<'static>> = Vec::new();
let mut has_visible_output = false;
for cell in &self.transcript_cells {
let display = cell.display_lines(width);
if display.is_empty() {
continue;
}
if !cell.is_stream_continuation() {
if has_visible_output {
lines.push(Line::from(""));
} else {
has_visible_output = true;
}
} else if !has_visible_output {
has_visible_output = true;
}
lines.extend(display);
}
(lines, has_visible_output)
}
pub(crate) fn update_transcript_layout_state(&mut self, width: u16, has_visible_output: bool) {
self.has_emitted_history_lines = has_visible_output;
self.rendered_history_width = Some(width);
}
fn reflow_transcript(&mut self, tui: &mut tui::Tui, width: u16) -> Result<()> {
let (lines, has_visible_output) = self.transcript_lines_for_width(width);
tui.clear_history()?;
if !lines.is_empty() {
tui.insert_history_lines(lines);
}
self.update_transcript_layout_state(width, has_visible_output);
Ok(())
}
}
#[cfg(test)]
@@ -580,6 +625,7 @@ mod tests {
overlay: None,
deferred_history_lines: Vec::new(),
has_emitted_history_lines: false,
rendered_history_width: None,
enhanced_keys_supported: false,
commit_anim_running: Arc::new(AtomicBool::new(false)),
backtrack: BacktrackState::default(),
@@ -668,4 +714,23 @@ mod tests {
assert_eq!(nth, 1);
assert_eq!(prefill, "follow-up (edited)");
}
#[test]
fn transcript_lines_rewrap_when_width_changes() {
let mut app = make_test_app();
let paragraph = "This is a very long streamed line that should expand when the \
terminal width grows so that it no longer appears in a single column.";
let cell: Arc<dyn HistoryCell> =
Arc::new(AgentMessageCell::new(vec![Line::from(paragraph)], true));
app.transcript_cells.push(cell);
let (narrow, _) = app.transcript_lines_for_width(12);
let (wide, _) = app.transcript_lines_for_width(80);
assert!(
narrow.len() > wide.len(),
"expected wider renders to require fewer lines (narrow: {}, wide: {})",
narrow.len(),
wide.len()
);
}
}

View File

@@ -143,9 +143,11 @@ impl App {
pub(crate) fn render_transcript_once(&mut self, tui: &mut tui::Tui) {
if !self.transcript_cells.is_empty() {
let width = tui.terminal.last_known_screen_size.width;
for cell in &self.transcript_cells {
tui.insert_history_lines(cell.display_lines(width));
let (lines, has_visible_output) = self.transcript_lines_for_width(width);
if !lines.is_empty() {
tui.insert_history_lines(lines);
}
self.update_transcript_layout_state(width, has_visible_output);
}
}

View File

@@ -757,9 +757,7 @@ impl ChatWidget {
self.add_to_history(history_cell::FinalMessageSeparator::new(elapsed_seconds));
self.needs_final_message_separator = false;
}
self.stream_controller = Some(StreamController::new(
self.last_rendered_width.get().map(|w| w.saturating_sub(2)),
));
self.stream_controller = Some(StreamController::new(None));
}
if let Some(controller) = self.stream_controller.as_mut()
&& controller.push(&delta)

View File

@@ -220,4 +220,22 @@ mod tests {
"expected exact rendered lines for loose/tight section"
);
}
#[tokio::test]
async fn streaming_cells_reflow_when_width_changes() {
let mut ctrl = StreamController::new(None);
ctrl.push(
"This is a long streamed line that should collapse when the terminal grows wider.",
);
let cell = ctrl
.finalize()
.expect("finalize should return a history cell for streamed content");
let narrow = cell.display_lines(10);
let wide = cell.display_lines(80);
assert!(
narrow.len() > wide.len(),
"expected rewrapping to reduce the number of lines when width increases"
);
}
}

View File

@@ -485,6 +485,27 @@ impl Tui {
self.frame_requester().schedule_frame();
}
pub fn clear_history(&mut self) -> Result<()> {
{
let backend = self.terminal.backend_mut();
std::io::Write::write_all(backend, b"\x1b[3J")?;
std::io::Write::write_all(backend, b"\x1b[2J")?;
std::io::Write::write_all(backend, b"\x1b[H")?;
std::io::Write::flush(backend)?;
}
let size = self.terminal.size()?;
let original_viewport = self.terminal.viewport_area;
let full = ratatui::layout::Rect::new(0, 0, size.width, size.height);
self.terminal.set_viewport_area(full);
self.terminal.clear()?;
self.terminal.set_viewport_area(original_viewport);
self.terminal
.set_cursor_position(original_viewport.as_position())?;
self.pending_history_lines.clear();
Ok(())
}
pub fn draw(
&mut self,
height: u16,