mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(tui): render cramped markdown tables as key-value records [2 of 2] (#24636)
## Stack - **Base: #24489 [1 of 2]** - render markdown tables in app style. - **Current: #24636 [2 of 2]** - render cramped markdown tables as key/value records. Review this PR against `fcoury/app-style-markdown-tables`; it contains only the fallback behavior for cramped tables. ## Why The row-separated markdown table rendering in #24489 remains readable while columns have usable room. Once long links or multiple prose-heavy columns are compressed into narrow allocations, however, the grid can turn words and paths into tall vertical strips that are difficult to scan. In those cases the content matters more than preserving the grid shape. ## What Changed <table> <tr><td> <p align="center"><b> Normal </b></p> <img width="1722" height="619" alt="CleanShot 2026-05-27 at 14 32 57" src="https://github.com/user-attachments/assets/d04f5fbd-6064-4acd-91bd-072d19b983df" /> </td></tr> <tr><td> <p align="center"><b> Narrow </b></p> <img width="863" height="1013" alt="CleanShot 2026-05-27 at 14 33 12" src="https://github.com/user-attachments/assets/6a7d2968-0a68-48fd-ab5d-209b3dbaf03e" /> </td></tr> <tr><td> <p align="center"><b> Very narrow </b></p> <img width="435" height="746" alt="CleanShot 2026-05-27 at 14 33 47" src="https://github.com/user-attachments/assets/f6a59e30-b1d2-4063-9c05-43933abc77d6" /> </td></tr> </table> - Detect tables whose grid allocation causes systemic token fragmentation or starves multiple prose-heavy columns. - Render those tables as repeated key/value records instead of retaining an unreadable grid. - Use aligned label/value records when there is useful horizontal room, and switch to a stacked narrow-record layout where each label is followed by a full-width value when width is especially constrained. - Preserve the themed label color, rich inline formatting, links, and the existing grid presentation for tables that remain readable. - Add snapshot coverage for path-heavy narrow tables, prose-heavy issue tables, systemic compact fragmentation, and a control case that should continue to render as a grid. ## How to Test 1. Start Codex from this branch and render a normal multi-column markdown table at a comfortable terminal width. Confirm it still appears as the styled row-separated grid from #24489. 2. Render a table containing a long linked record identifier or file-like value, then narrow the terminal until the grid would split the value into vertical fragments. Confirm it switches to key/value records, with labels above values at very narrow widths. 3. Render a table with multiple prose-heavy columns, such as an issue summary table with `Issue`, `Activity`, `Complexity`, and `Why start`. Confirm a cramped width switches to records rather than wrapping several columns into hard-to-read strips. 4. Render a compact table where only one value wraps mildly. Confirm it stays in grid form rather than switching prematurely. ## Validation - Ran `just test -p codex-tui` while developing the fallback and reviewed/accepted the intended new markdown-render snapshots. The command still reports two unrelated existing guardian feature-flag test failures outside this diff. - Ran `just fix -p codex-tui` and `just fmt` after the Rust changes were complete. - `just argument-comment-lint` cannot reach source linting locally because Bazel fails while resolving LLVM sanitizer headers; touched positional literal callsites were inspected manually and annotated where needed.
This commit is contained in:
committed by
GitHub
Unverified
parent
7a26497836
commit
26c9502121
@@ -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;
|
||||
@@ -74,6 +76,8 @@ use unicode_width::UnicodeWidthChar;
|
||||
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 = '━';
|
||||
@@ -233,8 +237,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 {
|
||||
@@ -285,11 +289,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<usize>) -> Text<'static> {
|
||||
let cwd = std::env::current_dir().ok();
|
||||
render_markdown_text_with_width_and_cwd(input, width, cwd.as_deref())
|
||||
@@ -1036,16 +1040,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 {
|
||||
@@ -1081,6 +1086,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);
|
||||
@@ -1088,8 +1094,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,
|
||||
@@ -1101,11 +1126,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,
|
||||
@@ -1157,6 +1192,15 @@ where
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the full content budget for record fallback rendering.
|
||||
fn available_record_width(&self) -> Option<usize> {
|
||||
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
|
||||
@@ -1447,11 +1491,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],
|
||||
|
||||
@@ -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<TableCell>],
|
||||
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<TableCell>],
|
||||
metrics: &[TableColumnMetrics],
|
||||
available_width: Option<usize>,
|
||||
label_style: Style,
|
||||
separator_style: Style,
|
||||
) -> Vec<Line<'static>> {
|
||||
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<Line<'static>>,
|
||||
header: &TableCell,
|
||||
value: &TableCell,
|
||||
label_width: usize,
|
||||
available_width: Option<usize>,
|
||||
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<Line<'static>>,
|
||||
header: &TableCell,
|
||||
value: &TableCell,
|
||||
available_width: Option<usize>,
|
||||
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<Line<'static>> {
|
||||
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::<usize>()
|
||||
})
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
fn widest_line_width(lines: &[Line<'_>]) -> usize {
|
||||
lines
|
||||
.iter()
|
||||
.map(|line| {
|
||||
line.spans
|
||||
.iter()
|
||||
.map(|span| span.content.width())
|
||||
.sum::<usize>()
|
||||
})
|
||||
.max()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
@@ -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<String> = 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))
|
||||
}));
|
||||
}
|
||||
|
||||
+12
@@ -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
|
||||
+29
@@ -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.
|
||||
+36
@@ -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.
|
||||
+40
@@ -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
|
||||
Reference in New Issue
Block a user