fix(tui): centralize usable-width guards for narrow layouts

This commit is contained in:
Felipe Coury
2026-04-06 13:14:40 -03:00
committed by Felipe Coury
parent 8a94c65989
commit be9006c161
5 changed files with 101 additions and 3 deletions
+1 -1
View File
@@ -4189,7 +4189,7 @@ impl ChatWidget {
self.last_rendered_width
.get()
.or_else(|| terminal::size().ok().map(|(w, _)| usize::from(w)))
.map(|w| w.saturating_sub(reserved_cols))
.and_then(|w| crate::width::usable_content_width(w, reserved_cols))
}
pub(crate) fn on_terminal_resize(&mut self, width: u16) {
@@ -111,6 +111,21 @@ async fn turn_started_uses_runtime_context_window_before_first_token_count() {
"expected /status to avoid raw config context window, got: {context_line}"
);
}
#[tokio::test]
async fn current_stream_width_returns_none_when_reserved_columns_exhaust_width() {
let (chat, _rx, _op_rx) = make_chatwidget_manual(None).await;
chat.last_rendered_width.set(Some(2));
assert_eq!(chat.current_stream_width(2), None);
chat.last_rendered_width.set(Some(4));
assert_eq!(chat.current_stream_width(4), None);
chat.last_rendered_width.set(Some(5));
assert_eq!(chat.current_stream_width(4), Some(1));
}
#[tokio::test]
async fn helpers_are_available_and_do_not_panic() {
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
+50 -2
View File
@@ -407,7 +407,7 @@ impl ReasoningSummaryCell {
let mut lines: Vec<Line<'static>> = Vec::new();
append_markdown(
&self.content,
Some((width as usize).saturating_sub(2)),
crate::width::usable_content_width_u16(width, 2),
Some(self.cwd.as_path()),
&mut lines,
);
@@ -509,10 +509,13 @@ impl AgentMarkdownCell {
impl HistoryCell for AgentMarkdownCell {
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
let Some(wrap_width) = crate::width::usable_content_width_u16(width, 2) else {
return prefix_lines(vec![Line::default()], "• ".dim(), " ".into());
};
let mut lines: Vec<Line<'static>> = Vec::new();
// Re-render markdown from source at the current width. Reserve 2 columns for the "• " /
// " " prefix prepended below.
let wrap_width = (width as usize).saturating_sub(2);
crate::markdown::append_markdown_agent_with_cwd(
&self.markdown_source,
Some(wrap_width),
@@ -4807,6 +4810,51 @@ mod tests {
}
}
#[test]
fn agent_markdown_cell_narrow_width_shows_prefix_only() {
let source = "| Name | Role |\n|------|------|\n| Alice | Engineer |\n";
let cell = AgentMarkdownCell::new(source.to_string());
let lines = render_lines(&cell.display_lines(2));
assert_eq!(lines, vec!["• ".to_string()]);
}
#[test]
fn wrapped_and_prefixed_cells_handle_tiny_widths() {
let user_cell = UserHistoryCell {
message: "tiny width coverage for wrapped user history".to_string(),
text_elements: Vec::new(),
local_image_paths: Vec::new(),
};
let agent_message_cell = AgentMessageCell::new(vec!["tiny width agent line".into()], true);
let reasoning_cell = ReasoningSummaryCell::new(
"Plan".to_string(),
"Reasoning summary content for tiny widths.".to_string(),
false,
);
let agent_markdown_cell =
AgentMarkdownCell::new("| A | B |\n|---|---|\n| x | y |\n".to_string());
for width in 1..=4 {
assert!(
!user_cell.display_lines(width).is_empty(),
"user cell should render at width {width}",
);
assert!(
!agent_message_cell.display_lines(width).is_empty(),
"agent message cell should render at width {width}",
);
assert!(
!reasoning_cell.display_lines(width).is_empty(),
"reasoning cell should render at width {width}",
);
assert!(
!agent_markdown_cell.display_lines(width).is_empty(),
"agent markdown cell should render at width {width}",
);
}
}
#[test]
fn agent_markdown_cell_survives_insert_history_rewrap() {
let source = "\
+1
View File
@@ -208,6 +208,7 @@ mod voice {
pub(crate) fn clear(&self) {}
}
}
mod width;
mod wrapping;
+34
View File
@@ -0,0 +1,34 @@
/// Returns usable content width after reserving fixed columns.
///
/// Guarantees a strict positive width (`Some(n)` where `n > 0`) or `None` when
/// the reserved columns consume the full width.
pub(crate) fn usable_content_width(total_width: usize, reserved_cols: usize) -> Option<usize> {
total_width
.checked_sub(reserved_cols)
.filter(|remaining| *remaining > 0)
}
/// `u16` convenience wrapper around [`usable_content_width`].
pub(crate) fn usable_content_width_u16(total_width: u16, reserved_cols: u16) -> Option<usize> {
usable_content_width(usize::from(total_width), usize::from(reserved_cols))
}
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn usable_content_width_returns_none_when_reserved_exhausts_width() {
assert_eq!(usable_content_width(0, 0), None);
assert_eq!(usable_content_width(2, 2), None);
assert_eq!(usable_content_width(3, 4), None);
assert_eq!(usable_content_width(5, 4), Some(1));
}
#[test]
fn usable_content_width_u16_matches_usize_variant() {
assert_eq!(usable_content_width_u16(2, 2), None);
assert_eq!(usable_content_width_u16(5, 4), Some(1));
}
}