diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index e1e7196fd2..ff9c5bb02c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -4192,7 +4192,13 @@ impl ChatWidget { self.last_rendered_width .get() .or_else(|| terminal::size().ok().map(|(w, _)| usize::from(w))) - .and_then(|w| crate::width::usable_content_width(w, reserved_cols)) + .and_then(|w| { + if w == 0 { + None + } else { + Some(crate::width::usable_content_width(w, reserved_cols).unwrap_or(1)) + } + }) } pub(crate) fn on_terminal_resize(&mut self, width: u16) { diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index f87ff63a96..c484a43e00 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -113,14 +113,14 @@ async fn turn_started_uses_runtime_context_window_before_first_token_count() { } #[tokio::test] -async fn current_stream_width_returns_none_when_reserved_columns_exhaust_width() { +async fn current_stream_width_clamps_to_minimum_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); + assert_eq!(chat.current_stream_width(2), Some(1)); chat.last_rendered_width.set(Some(4)); - assert_eq!(chat.current_stream_width(4), None); + assert_eq!(chat.current_stream_width(4), Some(1)); chat.last_rendered_width.set(Some(5)); assert_eq!(chat.current_stream_width(4), Some(1)); diff --git a/codex-rs/tui/src/markdown_render.rs b/codex-rs/tui/src/markdown_render.rs index bdff1c17fa..ac4c6eb08f 100644 --- a/codex-rs/tui/src/markdown_render.rs +++ b/codex-rs/tui/src/markdown_render.rs @@ -195,11 +195,13 @@ impl TableState { /// Rendered table output split by wrapping behavior. /// -/// `table_lines` are grid/fallback rows that are already width-laid-out and -/// should bypass paragraph wrapping. `spillover_lines` are prose rows extracted -/// from parser artifacts and should be routed through normal wrapping. +/// `table_lines` are either prewrapped grid rows (box rendering) or pipe +/// fallback rows that should still pass through normal wrapping. +/// `spillover_lines` are prose rows extracted from parser artifacts and should +/// be routed through normal wrapping. struct RenderedTableLines { table_lines: Vec>, + table_lines_prewrapped: bool, spillover_lines: Vec>, } @@ -810,11 +812,17 @@ where let RenderedTableLines { table_lines, + table_lines_prewrapped, spillover_lines, } = self.render_table_lines(table_state); let mut pending_marker_line = self.pending_marker_line; for line in table_lines { - self.push_prewrapped_line(line, pending_marker_line); + if table_lines_prewrapped { + self.push_prewrapped_line(line, pending_marker_line); + } else { + self.push_line(line); + self.flush_current_line(); + } pending_marker_line = false; } self.pending_marker_line = false; @@ -945,6 +953,7 @@ where if column_count == 0 { return RenderedTableLines { table_lines: Vec::new(), + table_lines_prewrapped: true, spillover_lines: Vec::new(), }; } @@ -989,6 +998,7 @@ where &rows, &table_state.alignments, ), + table_lines_prewrapped: false, spillover_lines, }; }; @@ -1014,6 +1024,7 @@ where out.push(self.render_border_line('└', '┴', '┘', &column_widths, border_style)); RenderedTableLines { table_lines: out, + table_lines_prewrapped: true, spillover_lines, } } diff --git a/codex-rs/tui/src/markdown_render_tests.rs b/codex-rs/tui/src/markdown_render_tests.rs index 125f88bd80..803e935161 100644 --- a/codex-rs/tui/src/markdown_render_tests.rs +++ b/codex-rs/tui/src/markdown_render_tests.rs @@ -1461,6 +1461,23 @@ fn table_falls_back_to_pipe_rendering_if_it_cannot_fit() { assert!(!lines.iter().any(|line| line.contains('┌'))); } +#[test] +fn table_pipe_fallback_rows_wrap_in_narrow_width() { + let md = "| c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c9 | c10 |\n|---|---|---|---|---|---|---|---|---|---|\n| 111111 | 222222 | 333333 | 444444 | 555555 | 666666 | 777777 | 888888 | 999999 | 101010 |\n"; + let text = crate::markdown_render::render_markdown_text_with_width(md, Some(20)); + let lines: Vec = text + .lines + .iter() + .map(|line| line.spans.iter().map(|span| span.content.clone()).collect()) + .collect(); + + assert!(lines.first().is_some_and(|line| line.starts_with('|'))); + assert!( + lines.len() > 3, + "expected wrapped pipe-fallback rows at narrow width, got {lines:?}" + ); +} + #[test] fn table_pipe_fallback_escapes_literal_pipes_in_cell_content() { let md = "| c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c9 | c10 |\n|---|---|---|---|---|---|---|---|---|---|\n| keep | keep | keep | keep | keep | keep | keep | keep | a \\| b | keep |\n"; @@ -1472,7 +1489,10 @@ fn table_pipe_fallback_escapes_literal_pipes_in_cell_content() { .collect(); assert!(lines.first().is_some_and(|line| line.starts_with('|'))); - assert!(lines.iter().any(|line| line.contains("a \\| b"))); + assert!( + lines.iter().any(|line| line.contains("\\|")), + "expected escaped pipe marker to be preserved in wrapped fallback rows: {lines:?}" + ); } #[test] diff --git a/codex-rs/tui/src/streaming/controller.rs b/codex-rs/tui/src/streaming/controller.rs index 75058e1952..5859daf1b2 100644 --- a/codex-rs/tui/src/streaming/controller.rs +++ b/codex-rs/tui/src/streaming/controller.rs @@ -306,9 +306,10 @@ impl StreamCore { }; let mut stable_prefix_render = Vec::new(); - append_markdown_agent( + append_markdown_agent_with_cwd( &self.raw_source[..last_nonblank_start], self.width, + Some(self.cwd.as_path()), &mut stable_prefix_render, ); self.rendered_lines