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
// -----------------------------------------------------------------------