From 6a479e1813fc44c1ea3c85b7b4023ee3d4c21b8f Mon Sep 17 00:00:00 2001 From: Benjamin Carlsson Date: Tue, 1 Sep 2026 17:17:50 +0000 Subject: [PATCH] Show full patches and terminal input in TUI history (#42107) ## What changed - Render complete patch contents directly in TUI history cells. - Render all input sent to background terminals inline. - Remove the preview row and byte limits and their transcript hints so inline and transcript views contain the same content. ## Testing Update snapshots to cover patches and terminal input longer than the former 12-row preview limit. GitOrigin-RevId: 9960818a9b51fc0c7f84d9def77b9b1bd8acad6c --- .../tui/src/chatwidget/tests/exec_flow.rs | 29 +- codex-rs/tui/src/diff_render.rs | 171 ++--------- codex-rs/tui/src/diff_render/preview.rs | 90 ------ codex-rs/tui/src/history_cell/exec.rs | 80 ++---- codex-rs/tui/src/history_cell/exec_tests.rs | 139 --------- codex-rs/tui/src/history_cell/patches.rs | 5 - codex-rs/tui/src/history_cell/tests.rs | 271 ++---------------- 7 files changed, 77 insertions(+), 708 deletions(-) delete mode 100644 codex-rs/tui/src/diff_render/preview.rs delete mode 100644 codex-rs/tui/src/history_cell/exec_tests.rs diff --git a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs index 8a10378e52..46f7fafd2e 100644 --- a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs +++ b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs @@ -1628,7 +1628,7 @@ async fn apply_patch_events_emit_history_cells() { changes.insert( PathBuf::from("foo.txt"), FileChange::Add { - content: "hello\n".to_string(), + content: (1..=16).map(|line| format!("line {line}\n")).collect(), }, ); let ev = ApplyPatchApprovalRequestEvent { @@ -1649,24 +1649,39 @@ async fn apply_patch_events_emit_history_cells() { changes2.insert( PathBuf::from("foo.txt"), FileChange::Add { - content: "hello\n".to_string(), + content: (1..=16).map(|line| format!("line {line}\n")).collect(), }, ); handle_patch_apply_begin(&mut chat, "c1", "turn-c1", changes2); let cells = drain_insert_history(&mut rx); assert!(!cells.is_empty(), "expected apply block cell to be sent"); let blob = lines_to_single_string(cells.last().unwrap()); - assert!( - blob.contains("Added foo.txt") || blob.contains("Edited foo.txt"), - "expected single-file header with filename (Added/Edited): {blob:?}" - ); + insta::assert_snapshot!(blob, @" + • Added foo.txt (+16 -0) + 1 +line 1 + 2 +line 2 + 3 +line 3 + 4 +line 4 + 5 +line 5 + 6 +line 6 + 7 +line 7 + 8 +line 8 + 9 +line 9 + 10 +line 10 + 11 +line 11 + 12 +line 12 + 13 +line 13 + 14 +line 14 + 15 +line 15 + 16 +line 16 + "); // 3) End apply success -> success cell let mut end_changes = HashMap::new(); end_changes.insert( PathBuf::from("foo.txt"), FileChange::Add { - content: "hello\n".to_string(), + content: (1..=16).map(|line| format!("line {line}\n")).collect(), }, ); handle_patch_apply_end( diff --git a/codex-rs/tui/src/diff_render.rs b/codex-rs/tui/src/diff_render.rs index ce38cb460f..f9e3b034a1 100644 --- a/codex-rs/tui/src/diff_render.rs +++ b/codex-rs/tui/src/diff_render.rs @@ -41,7 +41,6 @@ use ratatui::style::Stylize; use ratatui::text::Line as RtLine; use ratatui::text::Span as RtSpan; use ratatui::widgets::Paragraph; -use ratatui::widgets::Wrap; use std::collections::HashMap; use std::path::Path; use std::path::PathBuf; @@ -49,8 +48,6 @@ use std::path::PathBuf; use codex_utils_absolute_path::AbsolutePathBuf; use unicode_width::UnicodeWidthChar; -mod preview; - /// Replacement for a tab character in rendered diff content. const TAB_REPLACEMENT: &str = " "; /// Display width of a tab character in columns. @@ -312,25 +309,13 @@ impl DiffSummary { impl Renderable for FileChange { fn render(&self, area: Rect, buf: &mut Buffer) { let mut lines = vec![]; - render_change( - self, - &mut lines, - area.width as usize, - /*lang*/ None, - usize::MAX, - ); + render_change(self, &mut lines, area.width as usize, /*lang*/ None); Paragraph::new(lines).render(area, buf); } fn desired_height(&self, width: u16) -> u16 { let mut lines = vec![]; - render_change( - self, - &mut lines, - width as usize, - /*lang*/ None, - usize::MAX, - ); + render_change(self, &mut lines, width as usize, /*lang*/ None); lines.len() as u16 } } @@ -369,15 +354,7 @@ pub(crate) fn create_diff_summary( wrap_cols: usize, ) -> Vec> { let rows = collect_rows(changes); - render_changes_block(rows, wrap_cols, cwd, usize::MAX) -} - -pub(crate) fn create_diff_preview( - changes: &HashMap, - cwd: &Path, - wrap_cols: usize, -) -> Vec> { - render_changes_block(collect_rows(changes), wrap_cols, cwd, preview::PREVIEW_ROWS) + render_changes_block(rows, wrap_cols, cwd) } // Shared row for per-file presentation @@ -430,12 +407,7 @@ fn render_line_count_summary(added: usize, removed: usize) -> Vec>, - wrap_cols: usize, - cwd: &Path, - mut remaining_rows: usize, -) -> Vec> { +fn render_changes_block(rows: Vec>, wrap_cols: usize, cwd: &Path) -> Vec> { let mut out: Vec> = Vec::new(); let render_path = |row: &Row<'_>| -> Vec> { @@ -471,7 +443,6 @@ fn render_changes_block( } out.push(RtLine::from(header_spans)); - let mut omitted = false; for (idx, r) in rows.into_iter().enumerate() { // Insert a blank separator between file chunks (except before the first) if idx > 0 { @@ -488,11 +459,6 @@ fn render_changes_block( out.push(RtLine::from(header)); } - if remaining_rows == 0 && (omitted || r.added > 0 || r.removed > 0) { - omitted = true; - continue; - } - // For renames, use the destination extension for highlighting — the // diff content reflects the new file, not the old one. let lang_path = r.move_path.unwrap_or(r.path); @@ -500,47 +466,10 @@ fn render_changes_block( let mut lines = vec![]; let prefix = " "; let content_width = wrap_cols.saturating_sub(prefix.len()); - omitted |= render_change( - r.change, - &mut lines, - content_width, - lang.as_deref(), - remaining_rows, - ); - for line in prefix_lines(lines, prefix.into(), prefix.into()) { - if remaining_rows != usize::MAX { - let rows = Paragraph::new(line.clone()) - .wrap(Wrap { trim: false }) - .line_count(wrap_cols as u16); - if rows > remaining_rows { - out.extend(preview::rendered_prefix( - line, - wrap_cols as u16, - remaining_rows as u16, - )); - remaining_rows = 0; - omitted = true; - break; - } - remaining_rows -= rows; - } - out.push(line); - } - if omitted { - remaining_rows = 0; - } + render_change(r.change, &mut lines, content_width, lang.as_deref()); + out.extend(prefix_lines(lines, prefix.into(), prefix.into())); } - if omitted { - out.push( - format!( - " … Diff preview limited ({}).", - crate::ui_consts::TRANSCRIPT_HINT - ) - .dim() - .into(), - ); - } out } @@ -557,27 +486,14 @@ fn render_change( out: &mut Vec>, width: usize, lang: Option<&str>, - max_rows: usize, -) -> bool { - let mut omitted = false; +) { let style_context = current_diff_render_style_context(); match change { FileChange::Add { content } => { + // Pre-highlight the entire file content as a whole. + let syntax_lines = lang.and_then(|l| highlight_code_to_styled_spans(content, l)); let line_number_width = line_number_width(content.lines().count()); - let end = if max_rows == usize::MAX { - content.len() - } else { - preview::visible_byte_count( - content.split_inclusive('\n'), - width.saturating_sub(line_number_width + 2), - max_rows, - ) - }; - let visible_content = &content[..end]; - omitted = visible_content.len() < content.len(); - let syntax_lines = - lang.and_then(|l| highlight_code_to_styled_spans(visible_content, l)); - for (i, raw) in visible_content.lines().enumerate() { + for (i, raw) in content.lines().enumerate() { let syn = syntax_lines.as_ref().and_then(|sl| sl.get(i)); if let Some(spans) = syn { out.extend(push_wrapped_diff_line_inner_with_theme_and_color_level( @@ -607,21 +523,9 @@ fn render_change( } } FileChange::Delete { content } => { + let syntax_lines = lang.and_then(|l| highlight_code_to_styled_spans(content, l)); let line_number_width = line_number_width(content.lines().count()); - let end = if max_rows == usize::MAX { - content.len() - } else { - preview::visible_byte_count( - content.split_inclusive('\n'), - width.saturating_sub(line_number_width + 2), - max_rows, - ) - }; - let visible_content = &content[..end]; - omitted = visible_content.len() < content.len(); - let syntax_lines = - lang.and_then(|l| highlight_code_to_styled_spans(visible_content, l)); - for (i, raw) in visible_content.lines().enumerate() { + for (i, raw) in content.lines().enumerate() { let syn = syntax_lines.as_ref().and_then(|sl| sl.get(i)); if let Some(spans) = syn { out.extend(push_wrapped_diff_line_inner_with_theme_and_color_level( @@ -696,9 +600,6 @@ fn render_change( let line_number_width = line_number_width(max_line_number); let mut is_first_hunk = true; for h in patch.hunks() { - if out.len() >= max_rows { - return true; - } if !is_first_hunk { let spacer = format!("{:width$} ", "", width = line_number_width.max(1)); let spacer_span = RtSpan::styled( @@ -713,46 +614,11 @@ fn render_change( } is_first_hunk = false; - let visible_bytes = if max_rows == usize::MAX { - usize::MAX - } else { - preview::visible_byte_count( - h.lines().iter().map(|line| match line { - diffy::Line::Insert(text) - | diffy::Line::Delete(text) - | diffy::Line::Context(text) => *text, - }), - width.saturating_sub(line_number_width + 2), - max_rows.saturating_sub(out.len()), - ) - }; - let visible_lines: Vec<_> = h - .lines() - .iter() - .scan(visible_bytes, |remaining, line| { - if *remaining == 0 { - return None; - } - let text = match line { - diffy::Line::Insert(text) - | diffy::Line::Delete(text) - | diffy::Line::Context(text) => *text, - }; - let end = (*remaining).min(text.len()); - *remaining -= end; - omitted |= end < text.len(); - Some(match line { - diffy::Line::Insert(_) => diffy::Line::Insert(&text[..end]), - diffy::Line::Delete(_) => diffy::Line::Delete(&text[..end]), - diffy::Line::Context(_) => diffy::Line::Context(&text[..end]), - }) - }) - .collect(); - // Highlight each hunk as a single block so syntect parser // state is preserved across consecutive lines. let hunk_syntax_lines = diff_lang.and_then(|language| { - let hunk_text: String = visible_lines + let hunk_text: String = h + .lines() .iter() .map(|line| match line { diffy::Line::Insert(text) @@ -761,12 +627,12 @@ fn render_change( }) .collect(); let syntax_lines = highlight_code_to_styled_spans(&hunk_text, language)?; - (syntax_lines.len() == visible_lines.len()).then_some(syntax_lines) + (syntax_lines.len() == h.lines().len()).then_some(syntax_lines) }); let mut old_ln = h.old_range().start(); let mut new_ln = h.new_range().start(); - for (line_idx, l) in visible_lines.iter().enumerate() { + for (line_idx, l) in h.lines().iter().enumerate() { let syntax_spans = hunk_syntax_lines .as_ref() .and_then(|syntax_lines| syntax_lines.get(line_idx)); @@ -873,15 +739,10 @@ fn render_change( } } } - if omitted || visible_lines.len() < h.lines().len() { - omitted = true; - break; - } } } } } - omitted } /// Format a path for display relative to the current working directory when diff --git a/codex-rs/tui/src/diff_render/preview.rs b/codex-rs/tui/src/diff_render/preview.rs deleted file mode 100644 index a1cb5741fd..0000000000 --- a/codex-rs/tui/src/diff_render/preview.rs +++ /dev/null @@ -1,90 +0,0 @@ -//! Bounds inline diff work before allocating wrapped rows or syntax spans. - -use super::TAB_WIDTH; -use ratatui::buffer::Buffer; -use ratatui::buffer::CellWidth; -use ratatui::layout::Rect; -use ratatui::text::Line; -use ratatui::text::Span; -use ratatui::widgets::Paragraph; -use ratatui::widgets::Widget; -use ratatui::widgets::Wrap; -use unicode_width::UnicodeWidthChar; - -pub(super) const PREVIEW_ROWS: usize = 12; -const MAX_PREVIEW_BYTES: usize = 64 * 1024; - -/// Counts a UTF-8 source prefix within a pre-render character-width budget. -/// The caller also limits final viewport rows after styling and wrapping. -/// Include the start of an oversized line, stopping its scan at either budget. -/// The byte ceiling also bounds zero-width content that consumes no columns. -/// Lines must retain their terminators so the count can slice the source. -pub(super) fn visible_byte_count<'a>( - lines: impl Iterator, - content_width: usize, - max_rows: usize, -) -> usize { - let content_width = content_width.max(/*other*/ 1); - let mut remaining_rows = max_rows; - let mut count = 0; - for line in lines { - if remaining_rows == 0 { - break; - } - let mut rows = 1; - let mut col = 0; - let end = line.floor_char_boundary(MAX_PREVIEW_BYTES - count); - let visible = &line[..end]; - for (offset, ch) in visible.trim_end_matches(['\r', '\n']).char_indices() { - let width = ch.width().unwrap_or(if ch == '\t' { TAB_WIDTH } else { 0 }); - if col > 0 && col + width > content_width { - rows += 1; - col = 0; - } - if rows > remaining_rows { - return count + offset; - } - col += width; - } - remaining_rows -= rows; - count += visible.len(); - if end < line.len() { - return count; - } - } - count -} - -/// Keep a partial line using the viewport renderer's own grapheme and word -/// wrapping. Only the remaining preview rows are allocated; full views bypass it. -pub(super) fn rendered_prefix( - line: Line<'static>, - width: u16, - max_rows: u16, -) -> Vec> { - let style = line.style; - let area = Rect::new(/*x*/ 0, /*y*/ 0, width, max_rows); - let mut buffer = Buffer::empty(area); - Paragraph::new(line) - .wrap(Wrap { trim: false }) - .render(area, &mut buffer); - buffer - .content - .chunks(usize::from(width.max(/*other*/ 1))) - .map(|cells| { - let end = cells - .iter() - .rposition(|cell| cell.symbol() != " ") - .map_or(/*default*/ 0, |index| index + 1); - let mut spans = Vec::new(); - let mut column = 0; - while column < end { - let cell = &cells[column]; - spans.push(Span::styled(cell.symbol().to_string(), cell.style())); - // Buffer cells following a wide grapheme are continuation cells. - column += usize::from(cell.cell_width().max(/*other*/ 1)); - } - Line::from(spans).style(style) - }) - .collect() -} diff --git a/codex-rs/tui/src/history_cell/exec.rs b/codex-rs/tui/src/history_cell/exec.rs index 1a28ba2466..365558eb30 100644 --- a/codex-rs/tui/src/history_cell/exec.rs +++ b/codex-rs/tui/src/history_cell/exec.rs @@ -1,12 +1,7 @@ -//! Background terminal history cells, with bounded inline input and full transcript views. +//! Background terminal interaction and process-summary history cells. use super::*; -use crate::line_truncation::truncate_line_to_width; use crate::width::display_width; -use crate::wrapping::word_wrap_lines; - -const INPUT_PREVIEW_ROWS: usize = 12; -const INPUT_PREVIEW_BYTES: usize = 64 * 1024; #[derive(Debug)] pub(crate) struct UnifiedExecInteractionCell { @@ -21,8 +16,10 @@ impl UnifiedExecInteractionCell { stdin, } } +} - fn render_lines(&self, width: u16, max_input_rows: usize) -> Vec> { +impl HistoryCell for UnifiedExecInteractionCell { + fn display_lines(&self, width: u16) -> Vec> { if width == 0 { return Vec::new(); } @@ -50,62 +47,21 @@ impl UnifiedExecInteractionCell { return out; } - // Limit work before wrapping, including payloads with no newlines or visible width. - let input = if max_input_rows == usize::MAX { - self.stdin.as_str() - } else { - &self.stdin[..self.stdin.floor_char_boundary(INPUT_PREVIEW_BYTES)] - }; - let input_lines = input.lines().take(max_input_rows.saturating_add(1)); - let options = RtOptions::new(wrap_width) - .initial_indent(Line::from(" └ ".dim())) - .subsequent_indent(Line::from(" ".dim())); + let input_lines: Vec> = self + .stdin + .lines() + .map(|line| Line::from(line.to_string())) + .collect(); - let mut remaining_rows = max_input_rows; - let mut omitted = input.len() < self.stdin.len(); - for line in adaptive_wrap_lines(input_lines, options) { - if max_input_rows != usize::MAX { - let rows = Paragraph::new(line.clone()) - .wrap(Wrap { trim: false }) - .line_count(width); - if rows > remaining_rows { - // Keep fitting URLs intact; shorten only the overflowing final line. - out.extend( - word_wrap_lines([line], wrap_width) - .into_iter() - .take(remaining_rows) - // Control sequences differ in textwrap and ratatui width. - .map(|line| truncate_line_to_width(line, wrap_width)), - ); - omitted = true; - break; - } - remaining_rows -= rows; - } - out.push(line); - } - if omitted { - out.push( - format!( - " … Input preview limited ({}).", - crate::ui_consts::TRANSCRIPT_HINT - ) - .dim() - .into(), - ); - } + let input_wrapped = adaptive_wrap_lines( + input_lines, + RtOptions::new(wrap_width) + .initial_indent(Line::from(" └ ".dim())) + .subsequent_indent(Line::from(" ".dim())), + ); + out.extend(input_wrapped); out } -} - -impl HistoryCell for UnifiedExecInteractionCell { - fn display_lines(&self, width: u16) -> Vec> { - self.render_lines(width, INPUT_PREVIEW_ROWS) - } - - fn transcript_lines(&self, width: u16) -> Vec> { - self.render_lines(width, usize::MAX) - } fn raw_lines(&self) -> Vec> { let mut out = Vec::new(); @@ -140,10 +96,6 @@ impl HistoryCell for UnifiedExecInteractionCell { } } -#[cfg(test)] -#[path = "exec_tests.rs"] -mod tests; - pub(crate) fn new_unified_exec_interaction( command_display: Option, stdin: String, diff --git a/codex-rs/tui/src/history_cell/exec_tests.rs b/codex-rs/tui/src/history_cell/exec_tests.rs deleted file mode 100644 index 3593de8b77..0000000000 --- a/codex-rs/tui/src/history_cell/exec_tests.rs +++ /dev/null @@ -1,139 +0,0 @@ -//! Regression coverage for bounded background-terminal input previews. - -use super::*; -use pretty_assertions::assert_eq; - -#[test] -fn unified_exec_input_preview_limits_wrapped_payload() { - let input = "abcdefghijklmnopqrstuvwxyz0123456789".repeat(/*n*/ 2_000); - let cell = new_unified_exec_interaction(Some("cat".to_string()), input.clone()); - let preview = cell.display_lines(/*width*/ 40); - insta::assert_snapshot!( - preview.iter().map(ToString::to_string).collect::>().join("\n"), - @" - ↳ Interacted with background terminal · - cat - └ abcdefghijklmnopqrstuvwxyz0123456789 - abcdefghijklmnopqrstuvwxyz0123456789 - abcdefghijklmnopqrstuvwxyz0123456789 - abcdefghijklmnopqrstuvwxyz0123456789 - abcdefghijklmnopqrstuvwxyz0123456789 - abcdefghijklmnopqrstuvwxyz0123456789 - abcdefghijklmnopqrstuvwxyz0123456789 - abcdefghijklmnopqrstuvwxyz0123456789 - abcdefghijklmnopqrstuvwxyz0123456789 - abcdefghijklmnopqrstuvwxyz0123456789 - abcdefghijklmnopqrstuvwxyz0123456789 - abcdefghijklmnopqrstuvwxyz0123456789 - … Input preview limited (ctrl + t to view transcript). - ", - ); - assert_eq!( - cell.raw_lines(), - vec![ - Line::from("Interacted with background terminal: cat"), - Line::from(input.clone()), - ], - ); - let transcript = cell.transcript_lines(/*width*/ 40); - assert_eq!( - transcript[2..], - adaptive_wrap_lines( - input.lines(), - RtOptions::new(/*width*/ 40) - .initial_indent(Line::from(" └ ".dim())) - .subsequent_indent(Line::from(" ".dim())), - ), - ); -} - -#[test] -fn unified_exec_input_preview_preserves_fitting_lines() { - for input in [ - "line\n".repeat(/*n*/ 20), - format!( - "prefixprefixprefix https://example.test/a-b\n{}", - "line\n".repeat(/*n*/ 20) - ), - ] { - let cell = new_unified_exec_interaction(/*command_display*/ None, input); - let mut expected = cell.transcript_lines(/*width*/ 40); - expected.truncate(/*len*/ 13); - expected.push( - " … Input preview limited (ctrl + t to view transcript)." - .dim() - .into(), - ); - assert_eq!(cell.display_lines(/*width*/ 40), expected); - } -} - -#[test] -fn unified_exec_input_preview_respects_viewport_rows() { - for width in [1, 2, 4, 5, 8, 40, 80] { - for input in [ - "a".repeat(/*n*/ 4_000), - format!("https://example.com/{}", "a".repeat(/*n*/ 4_000)), - "界👩‍💻e\u{301}\t ".repeat(/*n*/ 1_000), - "\x1b[A".repeat(/*n*/ 1_000), - ] { - let cell = new_unified_exec_interaction(/*command_display*/ None, input); - let preview = cell.display_lines(width); - let header = - new_unified_exec_interaction(/*command_display*/ None, "x".to_string()) - .display_lines(width); - let content = &preview[header.len() - 1..preview.len() - 1]; - assert!( - Paragraph::new(content.to_vec()) - .wrap(Wrap { trim: false }) - .line_count(width) - <= INPUT_PREVIEW_ROWS, - "input preview exceeds row budget at width {width}", - ); - } - } -} - -#[test] -fn unified_exec_input_preview_bounds_zero_width_payload() { - let input = "\u{301}".repeat(/*n*/ 100_000); - let cell = new_unified_exec_interaction(/*command_display*/ None, input.clone()); - let preview = cell.display_lines(/*width*/ 80); - assert!( - preview - .last() - .unwrap() - .to_string() - .contains("Input preview limited") - ); - assert!( - preview - .iter() - .map(|line| line.to_string().len()) - .sum::() - < 66_000 - ); - assert_eq!( - cell.raw_lines(), - vec![ - Line::from("Interacted with background terminal"), - Line::from(input) - ], - ); -} - -#[test] -fn unified_exec_input_preview_keeps_short_and_exact_budget_input() { - for input in [ - "".to_string(), - "ls\npwd".to_string(), - "x\n".repeat(/*n*/ 12), - ] { - let cell = new_unified_exec_interaction(Some("cat".to_string()), input); - assert_eq!( - cell.display_lines(/*width*/ 80), - cell.transcript_lines(/*width*/ 80) - ); - assert_eq!(cell.display_lines(/*width*/ 0), Vec::>::new()); - } -} diff --git a/codex-rs/tui/src/history_cell/patches.rs b/codex-rs/tui/src/history_cell/patches.rs index 0026cb8b62..659f3fe390 100644 --- a/codex-rs/tui/src/history_cell/patches.rs +++ b/codex-rs/tui/src/history_cell/patches.rs @@ -1,7 +1,6 @@ //! Patch summaries and image-tool transcript helpers. use super::*; -use crate::diff_render::create_diff_preview; use codex_utils_path_uri::LegacyAppPathString; #[derive(Debug)] @@ -12,10 +11,6 @@ pub(crate) struct PatchHistoryCell { impl HistoryCell for PatchHistoryCell { fn display_lines(&self, width: u16) -> Vec> { - create_diff_preview(&self.changes, &self.cwd, width as usize) - } - - fn transcript_lines(&self, width: u16) -> Vec> { create_diff_summary(&self.changes, &self.cwd, width as usize) } diff --git a/codex-rs/tui/src/history_cell/tests.rs b/codex-rs/tui/src/history_cell/tests.rs index 13ca45cf06..ae781fc3a9 100644 --- a/codex-rs/tui/src/history_cell/tests.rs +++ b/codex-rs/tui/src/history_cell/tests.rs @@ -49,244 +49,6 @@ fn test_cwd() -> PathBuf { std::env::temp_dir() } -#[test] -fn patch_preview_shows_minified_prefix_but_preserves_full_views() { - let changes = HashMap::from([( - PathBuf::from("metrics.json"), - FileChange::Add { - content: format!("[{}0]", "123,".repeat(/*n*/ 10_000)), - }, - )]); - let cwd = test_cwd(); - let cell = new_patch_event(changes.clone(), &cwd); - - insta::assert_snapshot!(render_lines(&cell.display_lines(/*width*/ 80)).join("\n"), @" - • Added metrics.json (+1 -0) - 1 +[123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123, - 123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,1 - 23,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,12 - 3,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123 - ,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123, - 123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,1 - 23,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,12 - 3,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123 - ,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123, - 123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,1 - 23,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,12 - 3,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123,123 - … Diff preview limited (ctrl + t to view transcript). - "); - assert_eq!( - cell.transcript_lines(/*width*/ 80), - create_diff_summary(&changes, &cwd, /*wrap_cols*/ 80), - ); - assert_eq!( - cell.raw_lines(), - plain_lines(create_diff_summary(&changes, &cwd, RAW_DIFF_SUMMARY_WIDTH)), - ); -} - -#[test] -fn patch_preview_limits_add_delete_and_update_content() { - for content in [ - "value\n".repeat(/*n*/ 20), - format!("short\n{}\ntail\n", "界123\t".repeat(/*n*/ 100)), - ] { - for change in [ - FileChange::Add { - content: content.clone(), - }, - FileChange::Delete { - content: content.clone(), - }, - FileChange::Update { - unified_diff: diffy::create_patch("", &content).to_string(), - move_path: Some(PathBuf::from("renamed.txt")), - }, - FileChange::Update { - unified_diff: diffy::create_patch(&content, "").to_string(), - move_path: None, - }, - FileChange::Update { - unified_diff: diffy::DiffOptions::new() - .set_context_len(/*context_len*/ 20) - .create_patch(&content, &format!("{content}new\n")) - .to_string(), - move_path: None, - }, - ] { - let changes = HashMap::from([(PathBuf::from("values.txt"), change)]); - let cwd = test_cwd(); - let cell = new_patch_event(changes.clone(), &cwd); - let full = create_diff_summary(&changes, &cwd, /*wrap_cols*/ 80); - let mut expected = full[..13].to_vec(); - expected.push( - " … Diff preview limited (ctrl + t to view transcript)." - .dim() - .into(), - ); - assert_eq!(cell.display_lines(/*width*/ 80), expected); - assert_eq!(cell.transcript_lines(/*width*/ 80), full); - } - } -} - -#[test] -fn patch_preview_shares_content_budget_and_keeps_file_summaries() { - let changes = HashMap::from([ - ( - PathBuf::from("a.txt"), - FileChange::Add { - content: "a\n".repeat(/*n*/ 11) + &"©️ ".repeat(/*n*/ 36), - }, - ), - ( - PathBuf::from("b.txt"), - FileChange::Add { - content: "b\n".repeat(/*n*/ 10), - }, - ), - ( - PathBuf::from("c.txt"), - FileChange::Add { - content: "c\n".into(), - }, - ), - ]); - let cell = new_patch_event(changes, &test_cwd()); - insta::assert_snapshot!(render_lines(&cell.display_lines(/*width*/ 80)).join("\n"), @" - • Edited 3 files (+23 -0) - └ a.txt (+12 -0) - 1 +a - 2 +a - 3 +a - 4 +a - 5 +a - 6 +a - 7 +a - 8 +a - 9 +a - 10 +a - 11 +a - 12 +©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ ©️ - - └ b.txt (+10 -0) - - └ c.txt (+1 -0) - … Diff preview limited (ctrl + t to view transcript). - "); -} - -#[test] -fn patch_preview_budgets_wrapped_rows_at_narrow_widths() { - for content in [ - "0123456789".repeat(/*n*/ 1_000), - "ガ".repeat(/*n*/ 1_000), - "0123456789\n".repeat(/*n*/ 20), - format!("{}\n", "界\tvalue".repeat(/*n*/ 100)).repeat(/*n*/ 20), - format!("{}\n", "\u{a9}\u{fe0f}".repeat(/*n*/ 50)).repeat(/*n*/ 20), - ] { - let changes = HashMap::from([(PathBuf::from("values.txt"), FileChange::Add { content })]); - let cell = new_patch_event(changes, &test_cwd()); - for width in [8, 16, 40, 80] { - let lines = cell.display_lines(width); - let body = lines[1..lines.len() - 1].to_vec(); - let rows = Paragraph::new(body) - .wrap(Wrap { trim: false }) - .line_count(width); - assert_eq!(rows, 12); - } - } -} - -#[test] -fn patch_preview_preserves_small_diffs_and_exact_budget() { - for count in [1, 12] { - let mut changes = HashMap::from([( - PathBuf::from("small.rs"), - FileChange::Add { - content: "let value = 1;\n".repeat(count), - }, - )]); - changes.extend([ - ( - PathBuf::from("z-empty"), - FileChange::Add { - content: String::new(), - }, - ), - ( - PathBuf::from("z-renamed"), - FileChange::Update { - unified_diff: String::new(), - move_path: Some(PathBuf::from("renamed")), - }, - ), - ]); - let cwd = test_cwd(); - let cell = new_patch_event(changes.clone(), &cwd); - assert_eq!( - cell.display_lines(/*width*/ 80), - create_diff_summary(&changes, &cwd, /*wrap_cols*/ 80), - ); - } -} - -#[test] -fn patch_preview_caps_zero_width_source_before_later_content() { - let make_changes: [fn(String) -> FileChange; 3] = [ - |content| FileChange::Add { content }, - |content| FileChange::Delete { content }, - |content| FileChange::Update { - unified_diff: diffy::create_patch("", &content).to_string(), - move_path: None, - }, - ]; - let cwd = test_cwd(); - let content = format!("head\nxy{}\ntail\n", "\u{301}".repeat(/*n*/ 40_000)); - let mut summaries = Vec::new(); - for make_change in make_changes { - let changes = HashMap::from([ - (PathBuf::from("a.txt"), make_change(content.clone())), - (PathBuf::from("b.txt"), make_change("later\n".into())), - ]); - let cell = new_patch_event(changes.clone(), &cwd); - let preview = render_lines(&cell.display_lines(/*width*/ 80)).join("\n"); - // The next two-byte combining mark straddles the 64 KiB boundary. - assert_eq!(preview.matches('\u{301}').count(), 32_764); - summaries.push(preview.replace('\u{301}', "")); - assert_eq!( - cell.transcript_lines(/*width*/ 80), - create_diff_summary(&changes, &cwd, /*wrap_cols*/ 80) - ); - } - insta::assert_snapshot!(summaries.join("\n\n"), @" - • Edited 2 files (+4 -0) - └ a.txt (+3 -0) - 1 +head - 2 +xy - - └ b.txt (+1 -0) - … Diff preview limited (ctrl + t to view transcript). - - • Edited 2 files (+0 -4) - └ a.txt (+0 -3) - 1 -head - 2 -xy - - └ b.txt (+0 -1) - … Diff preview limited (ctrl + t to view transcript). - - • Edited 2 files (+4 -0) - └ a.txt (+3 -0) - 1 +head - 2 +xy - - └ b.txt (+1 -0) - … Diff preview limited (ctrl + t to view transcript). - "); -} - #[test] fn streaming_agent_tail_blank_line_uses_one_viewport_row() { let cell = StreamingAgentTailCell::new( @@ -803,16 +565,29 @@ fn session_configured_event(model: &str) -> ThreadSessionState { #[test] fn unified_exec_interaction_cell_renders_input() { - let cell = new_unified_exec_interaction(Some("echo hello".to_string()), "ls\npwd".to_string()); - let lines = render_transcript(&cell); - assert_eq!( - lines, - vec![ - "↳ Interacted with background terminal · echo hello", - " └ ls", - " pwd", - ], - ); + let input = (1..=16).map(|line| format!("line {line}\n")).collect(); + let cell = new_unified_exec_interaction(Some("cat".to_string()), input); + let lines = render_lines(&cell.display_lines(/*width*/ 80)); + assert_eq!(lines, render_transcript(&cell)); + insta::assert_snapshot!(lines.join("\n"), @" + ↳ Interacted with background terminal · cat + └ line 1 + line 2 + line 3 + line 4 + line 5 + line 6 + line 7 + line 8 + line 9 + line 10 + line 11 + line 12 + line 13 + line 14 + line 15 + line 16 + "); } #[test]