Fix terminal block missing first line via f32 tolerance (#52111)

João Soares created

## 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.

Change summary

crates/terminal/src/terminal.rs | 63 +++++++++++++++++++++++++++++++++-
1 file changed, 61 insertions(+), 2 deletions(-)

Detailed changes

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}"
+                    );
+                }
+            }
+        }
     }
 }