mirror of
https://github.com/openai/codex.git
synced 2026-09-05 15:18:41 +00:00
Merge 5c7ee85738 into sapling-pr-archive-bolinfest
This commit is contained in:
77
MODULE.bazel.lock
generated
77
MODULE.bazel.lock
generated
File diff suppressed because one or more lines are too long
@@ -1,9 +1,11 @@
|
||||
[advisories]
|
||||
# Reviewed 2026-04-15. Keep this list in sync with ../deny.toml.
|
||||
# Reviewed 2026-05-27. Keep this list in sync with ../deny.toml.
|
||||
ignore = [
|
||||
"RUSTSEC-2024-0388", # derivative 2.2.0 via starlark; upstream crate is unmaintained
|
||||
"RUSTSEC-2025-0057", # fxhash 0.2.1 via starlark_map; upstream crate is unmaintained
|
||||
"RUSTSEC-2024-0388", # derivative 2.2.0 via starlark/starlark_syntax; upstream crate is unmaintained
|
||||
"RUSTSEC-2025-0057", # fxhash 0.2.1 via starlark_map/sled; upstream crate is unmaintained
|
||||
"RUSTSEC-2024-0436", # paste 1.0.15 via starlark/ratatui; upstream crate is unmaintained
|
||||
"RUSTSEC-2023-0089", # atomic-polyfill via postcard/heapless/pagable; upstream crate is unmaintained
|
||||
"RUSTSEC-2024-0384", # instant via parking_lot/sled/pagable; upstream crate is unmaintained
|
||||
"RUSTSEC-2024-0320", # yaml-rust via syntect; remove when syntect drops or updates it
|
||||
"RUSTSEC-2025-0141", # bincode via syntect; remove when syntect drops or updates it
|
||||
"RUSTSEC-2026-0118", # hickory-proto via rama-dns/rama-tcp; remove when rama updates to hickory 0.26.1 or hickory-net
|
||||
|
||||
870
codex-rs/Cargo.lock
generated
870
codex-rs/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -377,7 +377,7 @@ sqlx = { version = "0.9.0", default-features = false, features = [
|
||||
"time",
|
||||
"uuid",
|
||||
] }
|
||||
starlark = "0.13.0"
|
||||
starlark = "0.14.0"
|
||||
strum = "0.27.2"
|
||||
strum_macros = "0.28.0"
|
||||
supports-color = "3.0.2"
|
||||
|
||||
@@ -70,11 +70,13 @@ feature-depth = 1
|
||||
# A list of advisory IDs to ignore. Note that ignored advisories will still
|
||||
# output a note when they are encountered.
|
||||
ignore = [
|
||||
# Reviewed 2026-04-15. Keep this list in sync with .cargo/audit.toml.
|
||||
# Reviewed 2026-05-27. Keep this list in sync with .cargo/audit.toml.
|
||||
# Each exception must identify the dependency path and removal condition.
|
||||
{ id = "RUSTSEC-2024-0388", reason = "derivative is unmaintained; pulled in via starlark v0.13.0 used by execpolicy/cli/core; no fixed release yet" },
|
||||
{ id = "RUSTSEC-2025-0057", reason = "fxhash is unmaintained; pulled in via starlark_map/starlark v0.13.0 used by execpolicy/cli/core; no fixed release yet" },
|
||||
{ id = "RUSTSEC-2024-0388", reason = "derivative is unmaintained; pulled in via starlark/starlark_syntax v0.14.0 used by execpolicy/cli/core; no fixed starlark release yet" },
|
||||
{ id = "RUSTSEC-2025-0057", reason = "fxhash is unmaintained; pulled in via starlark_map/sled under starlark v0.14.0 used by execpolicy/cli/core; no fixed starlark release yet" },
|
||||
{ id = "RUSTSEC-2024-0436", reason = "paste is unmaintained; pulled in via ratatui/rmcp/starlark used by tui/execpolicy; no fixed release yet" },
|
||||
{ id = "RUSTSEC-2023-0089", reason = "atomic-polyfill is unmaintained; pulled in via postcard/heapless/pagable under starlark v0.14.0 used by execpolicy/cli/core; no fixed starlark release yet" },
|
||||
{ id = "RUSTSEC-2024-0384", reason = "instant is unmaintained; pulled in via parking_lot/sled/pagable under starlark v0.14.0 used by execpolicy/cli/core; no fixed starlark release yet" },
|
||||
# TODO(fcoury): remove this exception when syntect drops yaml-rust and bincode, or updates to versions that have fixed the vulnerabilities.
|
||||
{ id = "RUSTSEC-2024-0320", reason = "yaml-rust is unmaintained; pulled in via syntect v5.3.0 used by codex-tui for syntax highlighting; no fixed release yet" },
|
||||
{ id = "RUSTSEC-2025-0141", reason = "bincode is unmaintained; pulled in via syntect v5.3.0 used by codex-tui for syntax highlighting; no fixed release yet" },
|
||||
|
||||
@@ -95,7 +95,7 @@ impl ArgMatcherCardinality {
|
||||
}
|
||||
|
||||
impl<'v> AllocValue<'v> for ArgMatcher {
|
||||
fn alloc_value(self, heap: &'v Heap) -> Value<'v> {
|
||||
fn alloc_value(self, heap: Heap<'v>) -> Value<'v> {
|
||||
heap.alloc_simple(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,7 +66,7 @@ impl<'v> UnpackValue<'v> for Opt {
|
||||
}
|
||||
|
||||
impl<'v> AllocValue<'v> for Opt {
|
||||
fn alloc_value(self, heap: &'v Heap) -> Value<'v> {
|
||||
fn alloc_value(self, heap: Heap<'v>) -> Value<'v> {
|
||||
heap.alloc_simple(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ use starlark::environment::Module;
|
||||
use starlark::eval::Evaluator;
|
||||
use starlark::syntax::AstModule;
|
||||
use starlark::syntax::Dialect;
|
||||
use starlark::values::Heap;
|
||||
use starlark::values::list::UnpackList;
|
||||
use starlark::values::none::NoneType;
|
||||
use std::cell::RefCell;
|
||||
@@ -41,31 +40,28 @@ impl PolicyParser {
|
||||
let globals = GlobalsBuilder::extended_by(&[LibraryExtension::Typing])
|
||||
.with(policy_builtins)
|
||||
.build();
|
||||
let module = Module::new();
|
||||
|
||||
let heap = Heap::new();
|
||||
|
||||
module.set("ARG_OPAQUE_VALUE", heap.alloc(ArgMatcher::OpaqueNonFile));
|
||||
module.set("ARG_RFILE", heap.alloc(ArgMatcher::ReadableFile));
|
||||
module.set("ARG_WFILE", heap.alloc(ArgMatcher::WriteableFile));
|
||||
module.set("ARG_RFILES", heap.alloc(ArgMatcher::ReadableFiles));
|
||||
module.set(
|
||||
"ARG_RFILES_OR_CWD",
|
||||
heap.alloc(ArgMatcher::ReadableFilesOrCwd),
|
||||
);
|
||||
module.set("ARG_POS_INT", heap.alloc(ArgMatcher::PositiveInteger));
|
||||
module.set("ARG_SED_COMMAND", heap.alloc(ArgMatcher::SedCommand));
|
||||
module.set(
|
||||
"ARG_UNVERIFIED_VARARGS",
|
||||
heap.alloc(ArgMatcher::UnverifiedVarargs),
|
||||
);
|
||||
|
||||
let policy_builder = PolicyBuilder::new();
|
||||
{
|
||||
Module::with_temp_heap(|module| {
|
||||
let heap = module.heap();
|
||||
module.set("ARG_OPAQUE_VALUE", heap.alloc(ArgMatcher::OpaqueNonFile));
|
||||
module.set("ARG_RFILE", heap.alloc(ArgMatcher::ReadableFile));
|
||||
module.set("ARG_WFILE", heap.alloc(ArgMatcher::WriteableFile));
|
||||
module.set("ARG_RFILES", heap.alloc(ArgMatcher::ReadableFiles));
|
||||
module.set(
|
||||
"ARG_RFILES_OR_CWD",
|
||||
heap.alloc(ArgMatcher::ReadableFilesOrCwd),
|
||||
);
|
||||
module.set("ARG_POS_INT", heap.alloc(ArgMatcher::PositiveInteger));
|
||||
module.set("ARG_SED_COMMAND", heap.alloc(ArgMatcher::SedCommand));
|
||||
module.set(
|
||||
"ARG_UNVERIFIED_VARARGS",
|
||||
heap.alloc(ArgMatcher::UnverifiedVarargs),
|
||||
);
|
||||
|
||||
let mut eval = Evaluator::new(&module);
|
||||
eval.extra = Some(&policy_builder);
|
||||
eval.eval_module(ast, &globals)?;
|
||||
}
|
||||
eval.eval_module(ast, &globals).map(|_| ())
|
||||
})?;
|
||||
let policy = policy_builder.build();
|
||||
policy.map_err(|e| starlark::Error::new_kind(starlark::ErrorKind::Other(e.into())))
|
||||
}
|
||||
|
||||
@@ -65,12 +65,13 @@ impl PolicyParser {
|
||||
)
|
||||
.map_err(Error::Starlark)?;
|
||||
let globals = GlobalsBuilder::standard().with(policy_builtins).build();
|
||||
let module = Module::new();
|
||||
{
|
||||
Module::with_temp_heap(|module| {
|
||||
let mut eval = Evaluator::new(&module);
|
||||
eval.extra = Some(&self.builder);
|
||||
eval.eval_module(ast, &globals).map_err(Error::Starlark)?;
|
||||
}
|
||||
eval.eval_module(ast, &globals)
|
||||
.map(|_| ())
|
||||
.map_err(Error::Starlark)
|
||||
})?;
|
||||
self.builder
|
||||
.borrow()
|
||||
.validate_pending_examples_from(pending_validation_count)?;
|
||||
|
||||
@@ -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],
|
||||
|
||||
254
codex-rs/tui/src/markdown_render/table_key_value.rs
Normal file
254
codex-rs/tui/src/markdown_render/table_key_value.rs
Normal file
@@ -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))
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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