diff --git a/codex-rs/tui/src/markdown_render.rs b/codex-rs/tui/src/markdown_render.rs index 87861989c0c..aa764c6d26c 100644 --- a/codex-rs/tui/src/markdown_render.rs +++ b/codex-rs/tui/src/markdown_render.rs @@ -22,9 +22,9 @@ //! alignment count. //! 3. **Compute column widths** -- allocate widths with content-aware //! priority and iterative shrinking. -//! 4. **Render row-separated layout** -- theme-accented bold headers, a -//! heavier segmented header rule, and low-contrast segmented body -//! separators, or fallback to pipe format when the minimum cannot fit. +//! 4. **Choose presentation** -- render theme-accented row-separated columns +//! while values remain scannable, otherwise transpose body rows +//! into key/value records separated by muted rules. //! 5. **Append spillover** -- extracted spillover rows rendered as plain text //! after the table. //! @@ -34,8 +34,10 @@ //! or hashes), or Compact (short values such as counts and status labels). //! Token-heavy columns give up excess width before narrative columns so an //! oversized path does not collapse readable prose; compact values are -//! preserved last. When even 3-char-wide columns cannot fit, the table falls -//! back to pipe-delimited format. +//! preserved last. When compact values split, token-heavy values collapse into +//! unusably short chunks, expansive cells form tall narrow strips across enough +//! body rows, or even 3-char-wide columns cannot fit, body rows render as +//! key/value records. use crate::render::highlight::foreground_style_for_scopes; use crate::render::highlight::highlight_code_to_lines; @@ -69,6 +71,8 @@ use std::sync::LazyLock; use unicode_width::UnicodeWidthStr; use url::Url; +mod table_key_value; + const TABLE_COLUMN_GAP: usize = 2; const TABLE_CELL_PADDING: usize = 1; const TABLE_HEADER_SEPARATOR_CHAR: char = '━'; @@ -215,8 +219,8 @@ impl TableState { /// Rendered table output split by wrapping behavior. /// -/// `table_lines` are either prewrapped aligned rows or pipe -/// fallback rows that should still pass through normal wrapping. +/// `table_lines` are prewrapped aligned rows or key/value records, except +/// header-only tables may retain pipe fallback rows for normal wrapping. /// `spillover_lines` are prose rows extracted from parser artifacts and should /// be routed through normal wrapping. struct RenderedTableLines { @@ -267,11 +271,11 @@ pub fn render_markdown_text(input: &str) -> Text<'static> { /// Render markdown constrained to a known terminal width. /// -/// The renderer preserves table structure when possible and falls back to -/// pipe-table output when an aligned table cannot fit the available width. Passing -/// `None` keeps intrinsic line widths and disables width-driven wrapping in the -/// markdown writer. Local file links render relative to the current process -/// working directory. +/// The renderer preserves columnar table structure while values remain +/// scannable and falls back to key/value records when body rows cannot fit +/// readably. Passing `None` keeps intrinsic line widths and disables +/// width-driven wrapping in the markdown writer. Local file links render +/// relative to the current process working directory. pub(crate) fn render_markdown_text_with_width(input: &str, width: Option) -> Text<'static> { let cwd = std::env::current_dir().ok(); render_markdown_text_with_width_and_cwd(input, width, cwd.as_deref()) @@ -990,16 +994,17 @@ where } } - /// Convert a completed `TableState` into styled, row-separated `Line`s. + /// Convert a completed `TableState` into styled table `Line`s. /// /// Pipeline: filter spillover rows -> normalize column counts -> compute - /// column widths -> render aligned rows (or fall back to pipe format if the - /// minimum column widths exceed available terminal width). Spillover rows - /// are appended as plain text after the table. + /// column widths -> render aligned rows or key/value records when values + /// systemically lose token readability or expansive cells become tall + /// narrow strips. Spillover rows are appended as plain text after the + /// table. /// - /// Falls back to `render_table_pipe_fallback` (raw `| A | B |` format) - /// when `compute_column_widths` returns `None` (terminal too narrow for - /// even 3-char-wide columns). + /// Falls back to key/value records when body rows cannot fit in the aligned + /// grid; header-only tables retain raw pipe output because they contain no + /// records to transpose. fn render_table_lines(&self, mut table_state: TableState) -> RenderedTableLines { let column_count = table_state.alignments.len(); if column_count == 0 { @@ -1035,6 +1040,7 @@ where Self::normalize_row(row, column_count); } + let metrics = Self::collect_table_column_metrics(&header, &rows, column_count); let available_width = self.available_table_width(column_count); let widths = self.compute_column_widths(&header, &rows, &table_state.alignments, available_width); @@ -1042,8 +1048,27 @@ where .into_iter() .flat_map(|spillover| spillover.lines) .collect(); + let header_style = + foreground_style_for_scopes(&["entity.name.type", "support.type", "variable"]) + .unwrap_or(self.styles.strong) + .bold(); + let separator_style = table_separator_style(); let Some(column_widths) = widths else { + if !rows.is_empty() { + return RenderedTableLines { + table_lines: table_key_value::render_records( + &header, + &rows, + &metrics, + self.available_record_width(), + header_style, + separator_style, + ), + table_lines_prewrapped: true, + spillover_lines, + }; + } return RenderedTableLines { table_lines: self.render_table_pipe_fallback( &header, @@ -1055,11 +1080,21 @@ where }; }; - let header_style = - foreground_style_for_scopes(&["entity.name.type", "support.type", "variable"]) - .unwrap_or(self.styles.strong) - .bold(); - let separator_style = table_separator_style(); + if table_key_value::should_render_records(&rows, &column_widths, &metrics) { + return RenderedTableLines { + table_lines: table_key_value::render_records( + &header, + &rows, + &metrics, + self.available_record_width(), + header_style, + separator_style, + ), + table_lines_prewrapped: true, + spillover_lines, + }; + } + let mut out = Vec::with_capacity(2 + rows.len() * 2); out.extend(self.render_table_row( &header, @@ -1111,6 +1146,15 @@ where }) } + /// Return the full content budget for record fallback rendering. + fn available_record_width(&self) -> Option { + self.wrap_width.map(|wrap_width| { + let prefix_width = + Self::spans_display_width(&self.prefix_spans(self.pending_marker_line)); + wrap_width.saturating_sub(prefix_width) + }) + } + /// Allocate column widths for aligned, row-separated table rendering. /// /// Each column starts at its natural (max cell content) width, then columns @@ -1376,11 +1420,11 @@ where out } - /// Render the table as raw pipe-delimited lines (`| A | B |`). + /// Render a header-only table as raw pipe-delimited lines (`| A | B |`). /// - /// Used when `compute_column_widths` returns `None` (terminal too narrow - /// for even 3-char-wide columns). Pipe characters inside cell content are - /// escaped as `\|` so downstream parsers keep cell boundaries intact. + /// Used when `compute_column_widths` returns `None` and there are no body + /// records to transpose. Pipe characters inside cell content are escaped + /// as `\|` so downstream parsers keep cell boundaries intact. fn render_table_pipe_fallback( &self, header: &[TableCell], diff --git a/codex-rs/tui/src/markdown_render/table_key_value.rs b/codex-rs/tui/src/markdown_render/table_key_value.rs new file mode 100644 index 00000000000..9e506a0d459 --- /dev/null +++ b/codex-rs/tui/src/markdown_render/table_key_value.rs @@ -0,0 +1,254 @@ +//! Vertical key/value rendering for markdown tables that no longer scan well as grids. + +use super::TABLE_BODY_SEPARATOR_CHAR; +use super::TableCell; +use super::TableColumnKind; +use super::TableColumnMetrics; +use crate::render::line_utils::push_owned_lines; +use crate::wrapping::RtOptions; +use crate::wrapping::word_wrap_line; +use ratatui::style::Style; +use ratatui::text::Line; +use ratatui::text::Span; +use unicode_width::UnicodeWidthStr; + +const FIELD_LEADING_PADDING: usize = 1; +const FIELD_GAP: usize = 2; +const MIN_VALUE_WIDTH: usize = 3; +const MIN_ALIGNED_COMPACT_VALUE_WIDTH: usize = 12; +const MIN_ALIGNED_EXPANSIVE_VALUE_WIDTH: usize = 24; +const MIN_SCANNABLE_NARRATIVE_WIDTH: usize = 12; +const MIN_SCANNABLE_TOKEN_HEAVY_WIDTH: usize = 12; +const CRAMPED_EXPANSIVE_CELL_LINES: usize = 4; +const CATASTROPHIC_NARRATIVE_CELL_LINES: usize = 7; +const STACKED_VALUE_INDENT: usize = 2; + +/// Switch modes after enough records contain values the grid can no longer +/// present in useful chunks or expansive content collapses into tall strips. +pub(super) fn should_render_records( + rows: &[Vec], + column_widths: &[usize], + metrics: &[TableColumnMetrics], +) -> bool { + if rows.is_empty() { + return false; + } + + let affected_rows = rows + .iter() + .filter(|row| { + let contains_fragmented_value = + row.iter() + .zip(column_widths) + .zip(metrics) + .any(|((cell, width), metrics)| { + let has_fragmented_token = cell + .plain_text() + .split_whitespace() + .any(|token| token.width() > *width); + match metrics.kind { + TableColumnKind::Compact => has_fragmented_token, + TableColumnKind::TokenHeavy => { + *width < MIN_SCANNABLE_TOKEN_HEAVY_WIDTH && has_fragmented_token + } + TableColumnKind::Narrative => false, + } + }); + + contains_fragmented_value || expansive_cells_are_starved(row, column_widths, metrics) + }) + .count(); + let threshold = if rows.len() == 1 { + 1 + } else { + 2.max(rows.len().div_ceil(3)) + }; + + affected_rows >= threshold +} + +fn expansive_cells_are_starved( + row: &[TableCell], + column_widths: &[usize], + metrics: &[TableColumnMetrics], +) -> bool { + let expansive_cells: Vec<(TableColumnKind, usize, usize)> = row + .iter() + .zip(column_widths) + .zip(metrics) + .filter(|&((_cell, _width), metrics)| metrics.kind != TableColumnKind::Compact) + .map(|((cell, width), metrics)| (metrics.kind, *width, wrap_cell(cell, *width).len())) + .collect(); + + expansive_cells + .iter() + .filter(|(_, _, height)| *height >= CRAMPED_EXPANSIVE_CELL_LINES) + .count() + >= 2 + || expansive_cells.iter().any(|(kind, width, height)| { + *kind == TableColumnKind::Narrative + && *width < MIN_SCANNABLE_NARRATIVE_WIDTH + && *height >= CATASTROPHIC_NARRATIVE_CELL_LINES + }) +} + +pub(super) fn render_records( + headers: &[TableCell], + rows: &[Vec], + metrics: &[TableColumnMetrics], + available_width: Option, + label_style: Style, + separator_style: Style, +) -> Vec> { + let label_width = headers + .iter() + .map(|header| header.plain_text().width()) + .max() + .unwrap_or(0); + let minimum_value_width = if metrics + .iter() + .any(|metrics| metrics.kind != TableColumnKind::Compact) + { + MIN_ALIGNED_EXPANSIVE_VALUE_WIDTH + } else { + MIN_ALIGNED_COMPACT_VALUE_WIDTH + }; + let aligned_fields = available_width.is_none_or(|width| { + FIELD_LEADING_PADDING + label_width + FIELD_GAP + minimum_value_width <= width + }); + let mut out = Vec::new(); + + for (row_index, row) in rows.iter().enumerate() { + for (header, value) in headers.iter().zip(row) { + if aligned_fields { + render_aligned_field( + &mut out, + header, + value, + label_width, + available_width, + label_style, + ); + } else { + render_stacked_field(&mut out, header, value, available_width, label_style); + } + } + if row_index + 1 < rows.len() { + let width = available_width.unwrap_or_else(|| widest_line_width(&out)); + out.push(Line::from(Span::styled( + TABLE_BODY_SEPARATOR_CHAR.to_string().repeat(width), + separator_style, + ))); + } + } + + out +} + +fn render_aligned_field( + out: &mut Vec>, + header: &TableCell, + value: &TableCell, + label_width: usize, + available_width: Option, + label_style: Style, +) { + let value_indent = FIELD_LEADING_PADDING + label_width + FIELD_GAP; + let value_width = available_width + .map(|width| width.saturating_sub(value_indent).max(MIN_VALUE_WIDTH)) + .unwrap_or_else(|| cell_width(value).max(MIN_VALUE_WIDTH)); + let wrapped_value = wrap_cell(value, value_width); + for (line_index, value_line) in wrapped_value.into_iter().enumerate() { + let mut spans = Vec::new(); + if line_index == 0 { + let label = header.plain_text(); + spans.push(Span::raw(" ".repeat(FIELD_LEADING_PADDING))); + spans.push(Span::styled(label.clone(), label_style)); + spans.push(Span::raw( + " ".repeat(label_width.saturating_sub(label.width()) + FIELD_GAP), + )); + } else { + spans.push(Span::raw(" ".repeat(value_indent))); + } + spans.extend(value_line.spans); + out.push(Line::from(spans)); + } +} + +fn render_stacked_field( + out: &mut Vec>, + header: &TableCell, + value: &TableCell, + available_width: Option, + label_style: Style, +) { + let label_width = available_width + .map(|width| width.saturating_sub(FIELD_LEADING_PADDING).max(1)) + .unwrap_or_else(|| header.plain_text().width().max(1)); + let label = Line::from(Span::styled(header.plain_text(), label_style)); + let mut wrapped_labels = Vec::new(); + push_owned_lines( + &word_wrap_line(&label, RtOptions::new(label_width)), + &mut wrapped_labels, + ); + for label_line in wrapped_labels { + let mut spans = vec![Span::raw(" ".repeat(FIELD_LEADING_PADDING))]; + spans.extend(label_line.spans); + out.push(Line::from(spans)); + } + + let value_width = available_width + .map(|width| width.saturating_sub(STACKED_VALUE_INDENT).max(1)) + .unwrap_or_else(|| cell_width(value).max(1)); + for value_line in wrap_cell(value, value_width) { + let mut spans = vec![Span::raw(" ".repeat(STACKED_VALUE_INDENT))]; + spans.extend(value_line.spans); + out.push(Line::from(spans)); + } +} + +fn wrap_cell(cell: &TableCell, width: usize) -> Vec> { + if cell.lines.is_empty() { + return vec![Line::default()]; + } + + let mut wrapped = Vec::new(); + for source_line in &cell.lines { + let rendered = word_wrap_line(source_line, RtOptions::new(width.max(1))); + if rendered.is_empty() { + wrapped.push(Line::default()); + } else { + push_owned_lines(&rendered, &mut wrapped); + } + } + if wrapped.is_empty() { + wrapped.push(Line::default()); + } + wrapped +} + +fn cell_width(cell: &TableCell) -> usize { + cell.lines + .iter() + .map(|line| { + line.spans + .iter() + .map(|span| span.content.width()) + .sum::() + }) + .max() + .unwrap_or(0) +} + +fn widest_line_width(lines: &[Line<'_>]) -> usize { + lines + .iter() + .map(|line| { + line.spans + .iter() + .map(|span| span.content.width()) + .sum::() + }) + .max() + .unwrap_or(0) +} diff --git a/codex-rs/tui/src/markdown_render_tests.rs b/codex-rs/tui/src/markdown_render_tests.rs index 68e19616992..206bae1b702 100644 --- a/codex-rs/tui/src/markdown_render_tests.rs +++ b/codex-rs/tui/src/markdown_render_tests.rs @@ -1582,6 +1582,60 @@ fn table_wraps_file_paths_before_collapsing_narrative_columns_snapshot() { assert_snapshot!(plain_lines(&text).join("\n")); } +#[test] +fn table_renders_stacked_key_value_records_when_path_column_becomes_too_narrow_snapshot() { + let md = r#"| Session | Why useful | Detected table blocks | +| --- | --- | --- | +| [2026-05-25 current gallery](/Users/felipe.coury/.codex/sessions/2026/05/25/rollout-2026-05-25T18-13-09-019e60fc-0518-7c21-9596-980fe97225ba.jsonl) | The large gallery from this thread: emojis, links, emphasis, code, alignment, paragraphs, and a 30+ row table | 7 | +| [2026-05-14 renderer testing](/Users/felipe.coury/.codex/sessions/2026/05/14/rollout-2026-05-14T12-57-18-019e2734-e500-7011-8278-975c94d06000.jsonl) | Explicit "markdown tables for testing" session with several successive assistant samples | 16 | +| [2026-05-14 five-table test](/Users/felipe.coury/.codex/sessions/2026/05/14/rollout-2026-05-14T12-27-57-019e271a-064c-78c3-a5cd-a6f20a0c1ad5.jsonl) | Explicit request for five tables containing emojis, code, italics, and varied cell content | 10 | +"#; + let text = render_markdown_text_with_width(md, Some(/*width*/ 42)); + + assert_snapshot!(plain_lines(&text).join("\n")); +} + +#[test] +fn table_renders_records_when_multiple_prose_columns_are_starved_snapshot() { + let md = r#"| Issue | Activity | Complexity | Why start | +| --- | ---: | ---: | --- | +| [#24485: newline shortcut fails in PyCharm terminal on Windows](https://github.com/openai/codex/issues/24485) | `+1` 0, substantive comments 0 | Low | New, deterministic regression range; localized composer/keymap path. | +| [#23926: Vim composer `e` stalls at word end](https://github.com/openai/codex/issues/23926) | `+1` 0, comments 0 | Low | Standing best quick win; deterministic motion bug. | +| [#23651: Zellij scrollback misses Codex transcript over SSH](https://github.com/openai/codex/issues/23651) | `+1` 3, human comments 2 | Medium | Clear regression and strong scrollback evidence. | +| [#23740: raw ANSI/control sequences in Windows Terminal](https://github.com/openai/codex/issues/23740) | `+1` 7, human comments 7 | Medium | Highest activity; established Windows rendering regression family. | +| [#24527: typing lag increases with session length](https://github.com/openai/codex/issues/24527) | `+1` 0, substantive comments 0 | Medium | New TUI-visible performance report; needs profiling before implementation. | +"#; + let text = render_markdown_text_with_width(md, Some(/*width*/ 76)); + + assert_snapshot!(plain_lines(&text).join("\n")); +} + +#[test] +fn table_keeps_grid_when_only_one_compact_record_fragments_snapshot() { + let md = r#"| Key | Date | State | +| --- | --- | --- | +| short | 2025-01-01 | Ready | +| verylongidentifier | 2025-02-02 | Ready | +| final | 2025-03-03 | Done | +"#; + let text = render_markdown_text_with_width(md, Some(/*width*/ 40)); + + assert_snapshot!(plain_lines(&text).join("\n")); +} + +#[test] +fn table_renders_key_value_records_when_compact_fragmentation_is_systemic_snapshot() { + let md = r#"| Key | Notes | +| --- | --- | +| firstlongid | A readable explanatory sentence for this row. | +| secondlongid | Another readable explanatory sentence for this row. | +| short | A final readable explanatory sentence for this row. | +"#; + let text = render_markdown_text_with_width(md, Some(/*width*/ 17)); + + assert_snapshot!(plain_lines(&text).join("\n")); +} + #[test] fn table_inside_blockquote_has_quote_prefix() { let md = "> | A | B |\n> |---|---|\n> | 1 | 2 |\n"; @@ -1610,19 +1664,46 @@ fn escaped_pipes_render_in_table_cells() { } #[test] -fn table_falls_back_to_pipe_rendering_if_it_cannot_fit() { +fn table_falls_back_to_key_value_records_if_grid_cannot_fit() { let md = "| c1 | c2 | c3 | c4 | c5 | c6 | c7 | c8 | c9 | c10 |\n|---|---|---|---|---|---|---|---|---|---|\n| 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 |\n"; - let text = crate::markdown_render::render_markdown_text_with_width(md, Some(20)); + let text = crate::markdown_render::render_markdown_text_with_width(md, Some(/*width*/ 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.first().is_some_and(|line| line.contains("c1"))); + assert!(lines.iter().any(|line| line.contains("c10") && line.contains("10"))); assert!( !lines .iter() - .any(|line| line.contains('━') || line.contains('─')) + .any(|line| line.starts_with('|') || line.contains('━') || line.contains('─')) ); } + +#[test] +fn table_key_value_fallback_preserves_rich_values_and_themed_labels() { + let md = "| Key | Content | Extra | More |\n|---|---|---|---|\n| item | [link](https://example.com) | **bold** | `code` |\n"; + let text = crate::markdown_render::render_markdown_text_with_width(md, Some(/*width*/ 16)); + let lines = plain_lines(&text); + + assert!(lines.iter().any(|line| line.contains("Key"))); + assert!(lines.iter().any(|line| line.contains("item"))); + assert!(lines.iter().any(|line| line.contains("link"))); + assert!(lines.iter().any(|line| line.contains("bold"))); + assert!(lines.iter().any(|line| line.contains("code"))); + assert!( + text.lines[0] + .spans + .iter() + .any(|span| span.content.contains("Key") + && span.style.add_modifier.contains(Modifier::BOLD) + && span.style.fg.is_some()) + ); + assert!(text.lines.iter().any(|line| { + line.spans + .iter() + .any(|span| span.style.add_modifier.contains(Modifier::UNDERLINED)) + })); +} diff --git a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_keeps_grid_when_only_one_compact_record_fragments_snapshot.snap b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_keeps_grid_when_only_one_compact_record_fragments_snapshot.snap new file mode 100644 index 00000000000..74f3fad4d6a --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_keeps_grid_when_only_one_compact_record_fragments_snapshot.snap @@ -0,0 +1,12 @@ +--- +source: tui/src/markdown_render_tests.rs +expression: "plain_lines(&text).join(\"\\n\")" +--- + Key Date State +━━━━━━━━━━━━━━━━━ ━━━━━━━━━━━━ ━━━━━━━ + short 2025-01-01 Ready +───────────────── ──────────── ─────── + verylongidentif 2025-02-02 Ready + ier +───────────────── ──────────── ─────── + final 2025-03-03 Done diff --git a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_renders_key_value_records_when_compact_fragmentation_is_systemic_snapshot.snap b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_renders_key_value_records_when_compact_fragmentation_is_systemic_snapshot.snap new file mode 100644 index 00000000000..2700b8194a1 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_renders_key_value_records_when_compact_fragmentation_is_systemic_snapshot.snap @@ -0,0 +1,29 @@ +--- +source: tui/src/markdown_render_tests.rs +expression: "plain_lines(&text).join(\"\\n\")" +--- + Key + firstlongid + Notes + A readable + explanatory + sentence for + this row. +───────────────── + Key + secondlongid + Notes + Another + readable + explanatory + sentence for + this row. +───────────────── + Key + short + Notes + A final + readable + explanatory + sentence for + this row. diff --git a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_renders_records_when_multiple_prose_columns_are_starved_snapshot.snap b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_renders_records_when_multiple_prose_columns_are_starved_snapshot.snap new file mode 100644 index 00000000000..84938048d01 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_renders_records_when_multiple_prose_columns_are_starved_snapshot.snap @@ -0,0 +1,36 @@ +--- +source: tui/src/markdown_render_tests.rs +expression: "plain_lines(&text).join(\"\\n\")" +--- + Issue #24485: newline shortcut fails in PyCharm terminal on Windows + (https://github.com/openai/codex/issues/24485) + Activity +1 0, substantive comments 0 + Complexity Low + Why start New, deterministic regression range; localized composer/keymap + path. +──────────────────────────────────────────────────────────────────────────── + Issue #23926: Vim composer e stalls at word end (https://github.com/ + openai/codex/issues/23926) + Activity +1 0, comments 0 + Complexity Low + Why start Standing best quick win; deterministic motion bug. +──────────────────────────────────────────────────────────────────────────── + Issue #23651: Zellij scrollback misses Codex transcript over SSH + (https://github.com/openai/codex/issues/23651) + Activity +1 3, human comments 2 + Complexity Medium + Why start Clear regression and strong scrollback evidence. +──────────────────────────────────────────────────────────────────────────── + Issue #23740: raw ANSI/control sequences in Windows Terminal + (https://github.com/openai/codex/issues/23740) + Activity +1 7, human comments 7 + Complexity Medium + Why start Highest activity; established Windows rendering regression + family. +──────────────────────────────────────────────────────────────────────────── + Issue #24527: typing lag increases with session length (https:// + github.com/openai/codex/issues/24527) + Activity +1 0, substantive comments 0 + Complexity Medium + Why start New TUI-visible performance report; needs profiling before + implementation. diff --git a/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_renders_stacked_key_value_records_when_path_column_becomes_too_narrow_snapshot.snap b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_renders_stacked_key_value_records_when_path_column_becomes_too_narrow_snapshot.snap new file mode 100644 index 00000000000..bcdecdc149c --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__markdown_render__markdown_render_tests__table_renders_stacked_key_value_records_when_path_column_becomes_too_narrow_snapshot.snap @@ -0,0 +1,40 @@ +--- +source: tui/src/markdown_render_tests.rs +expression: "plain_lines(&text).join(\"\\n\")" +--- + Session + /Users/felipe.coury/.codex/ + sessions/2026/05/25/rollout-2026-05- + 25T18-13-09-019e60fc-0518-7c21-9596- + 980fe97225ba.jsonl + Why useful + The large gallery from this thread: + emojis, links, emphasis, code, + alignment, paragraphs, and a 30+ row + table + Detected table blocks + 7 +────────────────────────────────────────── + Session + /Users/felipe.coury/.codex/ + sessions/2026/05/14/rollout-2026-05- + 14T12-57-18-019e2734-e500-7011-8278- + 975c94d06000.jsonl + Why useful + Explicit "markdown tables for testing" + session with several successive + assistant samples + Detected table blocks + 16 +────────────────────────────────────────── + Session + /Users/felipe.coury/.codex/ + sessions/2026/05/14/rollout-2026-05- + 14T12-27-57-019e271a-064c-78c3-a5cd- + a6f20a0c1ad5.jsonl + Why useful + Explicit request for five tables + containing emojis, code, italics, and + varied cell content + Detected table blocks + 10