mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
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
This commit is contained in:
committed by
copyberry
parent
f40e08478c
commit
6a479e1813
@@ -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(
|
||||
|
||||
+16
-155
@@ -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<RtLine<'static>> {
|
||||
let rows = collect_rows(changes);
|
||||
render_changes_block(rows, wrap_cols, cwd, usize::MAX)
|
||||
}
|
||||
|
||||
pub(crate) fn create_diff_preview(
|
||||
changes: &HashMap<PathBuf, FileChange>,
|
||||
cwd: &Path,
|
||||
wrap_cols: usize,
|
||||
) -> Vec<RtLine<'static>> {
|
||||
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<RtSpan<'static
|
||||
spans
|
||||
}
|
||||
|
||||
fn render_changes_block(
|
||||
rows: Vec<Row<'_>>,
|
||||
wrap_cols: usize,
|
||||
cwd: &Path,
|
||||
mut remaining_rows: usize,
|
||||
) -> Vec<RtLine<'static>> {
|
||||
fn render_changes_block(rows: Vec<Row<'_>>, wrap_cols: usize, cwd: &Path) -> Vec<RtLine<'static>> {
|
||||
let mut out: Vec<RtLine<'static>> = Vec::new();
|
||||
|
||||
let render_path = |row: &Row<'_>| -> Vec<RtSpan<'static>> {
|
||||
@@ -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<RtLine<'static>>,
|
||||
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
|
||||
|
||||
@@ -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<Item = &'a str>,
|
||||
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<Line<'static>> {
|
||||
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()
|
||||
}
|
||||
@@ -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<Line<'static>> {
|
||||
impl HistoryCell for UnifiedExecInteractionCell {
|
||||
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
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<Line<'static>> = 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<Line<'static>> {
|
||||
self.render_lines(width, INPUT_PREVIEW_ROWS)
|
||||
}
|
||||
|
||||
fn transcript_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
self.render_lines(width, usize::MAX)
|
||||
}
|
||||
|
||||
fn raw_lines(&self) -> Vec<Line<'static>> {
|
||||
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<String>,
|
||||
stdin: String,
|
||||
|
||||
@@ -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::<Vec<_>>().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::<usize>()
|
||||
< 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::<Line<'static>>::new());
|
||||
}
|
||||
}
|
||||
@@ -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<Line<'static>> {
|
||||
create_diff_preview(&self.changes, &self.cwd, width as usize)
|
||||
}
|
||||
|
||||
fn transcript_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
create_diff_summary(&self.changes, &self.cwd, width as usize)
|
||||
}
|
||||
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user