diff --git a/poc-agent/Cargo.toml b/poc-agent/Cargo.toml index 8b4a97b..9b44b0c 100644 --- a/poc-agent/Cargo.toml +++ b/poc-agent/Cargo.toml @@ -17,7 +17,7 @@ reqwest = { version = "0.12", features = ["json"] } serde = { version = "1", features = ["derive"] } serde_json = "1" tokio = { version = "1", features = ["full"] } -ratatui = "0.30" +ratatui = { version = "0.30", features = ["unstable-rendered-line-info"] } crossterm = { version = "0.29", features = ["event-stream"] } walkdir = "2" glob = "0.3" diff --git a/poc-agent/src/main.rs b/poc-agent/src/main.rs index 16cfd95..2cfb487 100644 --- a/poc-agent/src/main.rs +++ b/poc-agent/src/main.rs @@ -1033,7 +1033,10 @@ async fn run(cli: cli::CliArgs) -> Result<()> { Some(Ok(Event::Mouse(mouse))) => { app.handle_mouse(mouse); } - Some(Ok(Event::Resize(_, _))) => {} + Some(Ok(Event::Resize(w, h))) => { + app.handle_resize(w, h); + terminal.clear()?; + } Some(Err(_)) => break, None => break, _ => continue, diff --git a/poc-agent/src/tui.rs b/poc-agent/src/tui.rs index a6b3d6f..a31fd46 100644 --- a/poc-agent/src/tui.rs +++ b/poc-agent/src/tui.rs @@ -107,10 +107,10 @@ struct PaneState { scroll: u16, /// Whether the user has scrolled away from the bottom. pinned: bool, - /// Last known inner dimensions (set during draw). Used by - /// scroll_down to compute max scroll without hardcoding. + /// Last known total visual lines (set during draw by Paragraph::line_count). + last_total_lines: u16, + /// Last known inner height (set during draw). last_height: u16, - last_width: u16, } impl PaneState { @@ -123,8 +123,8 @@ impl PaneState { use_markdown, scroll: 0, pinned: false, + last_total_lines: 0, last_height: 20, - last_width: 80, } } @@ -184,44 +184,15 @@ impl PaneState { self.evict(); } - /// Total visual (wrapped) lines given a pane width. - fn wrapped_line_count(&self, width: u16) -> u16 { - let w = width as usize; - let mut count: usize = 0; - for line in &self.lines { - count += wrapped_height_line(line, w); - } - if self.use_markdown && !self.md_buffer.is_empty() { - for line in parse_markdown(&self.md_buffer) { - count += wrapped_height_line(&line, w); - } - } else if !self.current_line.is_empty() { - count += wrapped_height(&self.current_line, w); - } - count.min(u16::MAX as usize) as u16 - } - - /// Auto-scroll to bottom unless user has pinned. Uses visual - /// (wrapped) line count so long lines don't cause clipping. - fn auto_scroll(&mut self, height: u16, width: u16) { - self.last_height = height; - self.last_width = width; - if !self.pinned { - let total = self.wrapped_line_count(width); - self.scroll = total.saturating_sub(height); - } - } - /// Scroll up by n visual lines, pinning if we move away from bottom. fn scroll_up(&mut self, n: u16) { self.scroll = self.scroll.saturating_sub(n); self.pinned = true; } - /// Scroll down by n visual lines, un-pinning if we reach bottom. + /// Scroll down by n visual lines. Un-pin if we reach bottom. fn scroll_down(&mut self, n: u16) { - let total = self.wrapped_line_count(self.last_width); - let max = total.saturating_sub(self.last_height); + let max = self.last_total_lines.saturating_sub(self.last_height); self.scroll = (self.scroll + n).min(max); if self.scroll >= max { self.pinned = false; @@ -245,28 +216,6 @@ impl PaneState { } } -/// How many visual lines a string occupies at a given width. -fn wrapped_height(line: &str, width: usize) -> usize { - if width == 0 || line.is_empty() { - return 1; - } - // Use unicode display width to match ratatui's Wrap behavior - let w = ratatui::text::Line::raw(line).width(); - ((w + width - 1) / width).max(1) -} - -/// How many visual lines a ratatui Line occupies at a given width. -fn wrapped_height_line(line: &Line<'_>, width: usize) -> usize { - if width == 0 { - return 1; - } - let w = line.width(); - if w == 0 { - return 1; - } - ((w + width - 1) / width).max(1) -} - /// Compute soft line break positions for word-wrapped text. /// Returns the character index where each soft line starts. /// Matches ratatui Wrap { trim: false } — breaks at word boundaries. @@ -758,6 +707,12 @@ impl App { } } + /// Handle terminal resize. Scroll is recalculated in draw_pane + /// via Paragraph::line_count; terminal.clear() in main.rs forces + /// a full redraw. + pub fn handle_resize(&mut self, _width: u16, _height: u16) { + } + /// Handle mouse events: scroll wheel and click-to-select-pane. pub fn handle_mouse(&mut self, mouse: MouseEvent) { match mouse.kind { @@ -1154,9 +1109,7 @@ fn draw_pane( pane: &mut PaneState, is_active: bool, ) { - let inner_height = area.height.saturating_sub(2); // borders - let inner_width = area.width.saturating_sub(2); - pane.auto_scroll(inner_height, inner_width); + let inner_height = area.height.saturating_sub(2); let border_style = if is_active { Style::default().fg(Color::Cyan) @@ -1171,10 +1124,19 @@ fn draw_pane( let lines = pane.all_lines(); let paragraph = Paragraph::new(lines) - .block(block) - .wrap(Wrap { trim: false }) - .scroll((pane.scroll, 0)); + .block(block.clone()) + .wrap(Wrap { trim: false }); + // Let ratatui tell us the total visual lines — no homegrown wrapping math. + let total = paragraph.line_count(area.width.saturating_sub(2)) as u16; + pane.last_total_lines = total; + pane.last_height = inner_height; + + if !pane.pinned { + pane.scroll = total.saturating_sub(inner_height); + } + + let paragraph = paragraph.scroll((pane.scroll, 0)); frame.render_widget(paragraph, area); }