diff --git a/codex-rs/tui/src/markdown_render.rs b/codex-rs/tui/src/markdown_render.rs index ee8af31a32..b73bb670a5 100644 --- a/codex-rs/tui/src/markdown_render.rs +++ b/codex-rs/tui/src/markdown_render.rs @@ -1308,10 +1308,9 @@ where // Keep common intro + html-block spillover together: // "HTML block:" followed by "
". first_text.trim_end().ends_with(':') - && (next_row + && next_row .and_then(|row| Self::first_non_empty_only_text(row)) .is_some_and(|text| Self::looks_like_html_content(&text)) - || Self::looks_like_label_line(&first_text)) } fn first_non_empty_only_text(row: &[TableCell]) -> Option { @@ -1329,10 +1328,6 @@ where text.contains('<') && text.contains('>') } - fn looks_like_label_line(text: &str) -> bool { - text.trim_end().ends_with(':') && text.split_whitespace().count() <= 3 - } - fn spans_display_width(spans: &[Span<'_>]) -> usize { spans.iter().map(|span| span.content.width()).sum() } diff --git a/codex-rs/tui/src/markdown_render_tests.rs b/codex-rs/tui/src/markdown_render_tests.rs index ebfedff446..ef7eb505a0 100644 --- a/codex-rs/tui/src/markdown_render_tests.rs +++ b/codex-rs/tui/src/markdown_render_tests.rs @@ -1505,6 +1505,28 @@ fn table_keeps_sparse_sentence_row_inside_grid() { ); } +#[test] +fn table_keeps_label_only_sparse_row_inside_grid() { + let md = "| A | B | C |\n|---|---|---|\n| Status: | | |\n| ok | | |\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("│ Status:") && line.ends_with('│')), + "expected label-only sparse row to remain inside table grid: {lines:?}" + ); + assert!( + !lines.iter().any(|line| line.trim() == "Status:"), + "did not expect label-only sparse row to spill outside table: {lines:?}" + ); +} + #[test] fn table_preserves_structured_leading_columns_when_last_column_is_long() { let md = "| Milestone | Planned Date | Outcome | Retrospective Summary |\n|---|---|---|---|\n| Canary rollout | 2026-01-10 | Completed | Canary diff --git a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__markdown_render_complex_snapshot.snap b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__markdown_render_complex_snapshot.snap index 34cbd1c63e..818c76e496 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__markdown_render_complex_snapshot.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__markdown_render_complex_snapshot.snap @@ -29,13 +29,13 @@ Image: alt text Table below (alignment test): -┌──────┬────────┬───────┐ -│ Left │ Center │ Right │ -├──────┼────────┼───────┤ -│ a │ b │ c │ -└──────┴────────┴───────┘ +┌─────────────┬────────┬───────┐ +│ Left │ Center │ Right │ +├─────────────┼────────┼───────┤ +│ a │ b │ c │ +│ HTML block: │ │ │ +└─────────────┴────────┴───────┘ Inline HTML: sup and sub. -HTML block:
inline block
Escapes: \_underscores\_, backslash \\, ticks ``code with `backtick` inside``. Emoji shortcodes: :sparkles: :tada: (if supported). diff --git a/codex-rs/tui/src/streaming/controller.rs b/codex-rs/tui/src/streaming/controller.rs index 592d44a23e..4d734feaec 100644 --- a/codex-rs/tui/src/streaming/controller.rs +++ b/codex-rs/tui/src/streaming/controller.rs @@ -585,6 +585,7 @@ fn is_markdown_fence(trimmed_line: &str, marker_len: usize) -> bool { fn parse_lines_with_fence_state(source: &str) -> Vec> { let mut in_fence = false; let mut fence_char = '\0'; + let mut fence_len = 0usize; let mut fence_context = FenceContext::Other; let mut lines = Vec::new(); @@ -603,13 +604,15 @@ fn parse_lines_with_fence_state(source: &str) -> Vec> { if !in_fence { in_fence = true; fence_char = marker; + fence_len = len; fence_context = if is_markdown_fence(trimmed, len) { FenceContext::Markdown } else { FenceContext::Other }; - } else if marker == fence_char && len >= 3 { + } else if marker == fence_char && len >= fence_len && trimmed[len..].trim().is_empty() { in_fence = false; + fence_len = 0; fence_context = FenceContext::Other; } } @@ -1347,6 +1350,15 @@ mod tests { )); } + #[test] + fn table_holdback_state_ignores_table_like_lines_inside_unclosed_long_fence() { + let source = "````sh\n```cmd\n| Key | Description |\n| --- | --- |\n````\n"; + assert!( + matches!(table_holdback_state(source), TableHoldbackState::None), + "table holdback should ignore pipe lines inside an open non-markdown fence", + ); + } + // ----------------------------------------------------------------------- // source_bytes_for_rendered_count — prefix-stability tests // -----------------------------------------------------------------------