From 75f05c8bda0825a6ccc8619b332d1440b422a4b9 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Tue, 17 Feb 2026 01:26:22 -0300 Subject: [PATCH] fix(tui): tighten fence indent and sparse spillover detection --- codex-rs/tui/src/markdown_render.rs | 20 +++++++++++++++++++- codex-rs/tui/src/markdown_render_tests.rs | 22 ++++++++++++++++++++++ codex-rs/tui/src/streaming/controller.rs | 19 ++++++++++++++++++- 3 files changed, 59 insertions(+), 2 deletions(-) diff --git a/codex-rs/tui/src/markdown_render.rs b/codex-rs/tui/src/markdown_render.rs index 1799eab0e9..bdff1c17fa 100644 --- a/codex-rs/tui/src/markdown_render.rs +++ b/codex-rs/tui/src/markdown_render.rs @@ -1426,7 +1426,25 @@ where } fn looks_like_html_content(text: &str) -> bool { - text.contains('<') && text.contains('>') + let bytes = text.as_bytes(); + for idx in 0..bytes.len() { + if bytes[idx] != b'<' { + continue; + } + + let mut tag_start = idx + 1; + if tag_start < bytes.len() && (bytes[tag_start] == b'/' || bytes[tag_start] == b'!') { + tag_start += 1; + } + + if tag_start < bytes.len() + && bytes[tag_start].is_ascii_alphabetic() + && bytes[tag_start + 1..].contains(&b'>') + { + return true; + } + } + false } fn looks_like_html_label_line(text: &str) -> bool { diff --git a/codex-rs/tui/src/markdown_render_tests.rs b/codex-rs/tui/src/markdown_render_tests.rs index 6475a693ab..125f88bd80 100644 --- a/codex-rs/tui/src/markdown_render_tests.rs +++ b/codex-rs/tui/src/markdown_render_tests.rs @@ -1542,6 +1542,28 @@ fn table_spillover_prose_wraps_in_narrow_width() { ); } +#[test] +fn table_keeps_sparse_comparison_row_inside_grid() { + let md = "| A | B | C |\n|---|---|---|\n| x < y > z | | |\n"; + let text = render_markdown_text(md); + let lines: Vec = text + .lines + .iter() + .map(|line| line.spans.iter().map(|span| span.content.clone()).collect()) + .collect(); + + assert!( + lines + .iter() + .any(|line| line.contains("│ x < y > z") && line.ends_with('│')), + "expected sparse comparison row to remain inside table grid: {lines:?}" + ); + assert!( + !lines.iter().any(|line| line.trim() == "x < y > z"), + "did not expect sparse comparison row to spill outside table: {lines:?}" + ); +} + #[test] fn table_keeps_sparse_rows_with_empty_trailing_cells() { let md = "| A | B | C |\n|---|---|---|\n| a | | |\n"; diff --git a/codex-rs/tui/src/streaming/controller.rs b/codex-rs/tui/src/streaming/controller.rs index d8396694ca..9685513720 100644 --- a/codex-rs/tui/src/streaming/controller.rs +++ b/codex-rs/tui/src/streaming/controller.rs @@ -616,7 +616,15 @@ fn parse_lines_with_fence_state(source: &str) -> Vec> { }, }); - let trimmed = raw_line.trim_start(); + let leading_spaces = raw_line + .as_bytes() + .iter() + .take_while(|byte| **byte == b' ') + .count(); + if leading_spaces > 3 { + continue; + } + let trimmed = &raw_line[leading_spaces..]; if let Some((marker, len)) = parse_fence_marker(trimmed) { if !in_fence { in_fence = true; @@ -1391,6 +1399,15 @@ mod tests { ); } + #[test] + fn table_holdback_state_treats_indented_fence_text_as_plain_content() { + let source = " ```sh\n| Key | Description |\n| --- | --- |\n"; + assert!( + matches!(table_holdback_state(source), TableHoldbackState::Confirmed), + "indented fence-like text should not open a fence and should not block table detection", + ); + } + // ----------------------------------------------------------------------- // source_bytes_for_rendered_count — prefix-stability tests // -----------------------------------------------------------------------