From aa63f8e901dcc4a111c93de69e70e52d96e5d890 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Soares?= <37777652+Dnreikronos@users.noreply.github.com> Date: Mon, 23 Mar 2026 11:53:28 -0300 Subject: [PATCH] Fix terminal block missing first line via f32 tolerance (#52111) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Context `TerminalBounds::num_lines()` uses `floor(height / line_height)` to compute the terminal grid row count. When the height is derived from `N * line_height` (as it is for inline/embedded terminals in the Agent Panel), IEEE 754 float32 arithmetic can produce `N - epsilon` instead of `N`, causing `floor()` to return `N - 1`. This makes the terminal grid one row too small, leaving the first line of output in invisible scrollback (since `display_offset = 0`). The same issue applies to `num_columns()`. The fix adds a small tolerance (`0.01`) before flooring, which absorbs float precision errors without affecting genuine fractional results. Closes #51609 ## How to Review Small PR — focus on the tolerance value (`0.01`) in `num_lines()` and `num_columns()` in `crates/terminal/src/terminal.rs`. The two new tests (`test_num_lines_float_precision`, `test_num_columns_float_precision`) verify the fix across 1,000+ float combinations that previously triggered the bug. ## Self-Review Checklist - [x] I've reviewed my own diff for quality, security, and reliability - [ ] Unsafe blocks (if any) have justifying comments - [ ] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [x] Tests cover the new/changed behavior - [x] Performance impact has been considered and is acceptable Release Notes: - Fixed the first line of terminal output sometimes missing in Agent Panel terminal blocks. --- crates/terminal/src/terminal.rs | 63 +++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 56cca7cb40195298ed0479fc43c8b13b6c577249..c85d849cb034f99367f5cd8e6f1a978e4efd7ae3 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -207,11 +207,16 @@ impl TerminalBounds { } pub fn num_lines(&self) -> usize { - (self.bounds.size.height / self.line_height).floor() as usize + // Tolerance to prevent f32 precision from losing a row: + // `N * line_height / line_height` can be N-epsilon, which floor() + // would round down, pushing the first line into invisible scrollback. + let raw = self.bounds.size.height / self.line_height; + raw.next_up().floor() as usize } pub fn num_columns(&self) -> usize { - (self.bounds.size.width / self.cell_width).floor() as usize + let raw = self.bounds.size.width / self.cell_width; + raw.next_up().floor() as usize } pub fn height(&self) -> Pixels { @@ -3364,5 +3369,59 @@ mod tests { scroll_by(-1); } } + + #[test] + fn test_num_lines_float_precision() { + let line_heights = [ + 20.1f32, 16.7, 18.3, 22.9, 14.1, 15.6, 17.8, 19.4, 21.3, 23.7, + ]; + for &line_height in &line_heights { + for n in 1..=100 { + let height = n as f32 * line_height; + let bounds = TerminalBounds::new( + px(line_height), + px(8.0), + Bounds { + origin: Point::default(), + size: Size { + width: px(800.0), + height: px(height), + }, + }, + ); + assert_eq!( + bounds.num_lines(), + n, + "num_lines() should be {n} for height={height}, line_height={line_height}" + ); + } + } + } + + #[test] + fn test_num_columns_float_precision() { + let cell_widths = [8.1f32, 7.3, 9.7, 6.9, 10.1]; + for &cell_width in &cell_widths { + for n in 1..=200 { + let width = n as f32 * cell_width; + let bounds = TerminalBounds::new( + px(20.0), + px(cell_width), + Bounds { + origin: Point::default(), + size: Size { + width: px(width), + height: px(400.0), + }, + }, + ); + assert_eq!( + bounds.num_columns(), + n, + "num_columns() should be {n} for width={width}, cell_width={cell_width}" + ); + } + } + } } }