Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Limit background terminal input previews (#41937)
## Why

Large input sent to a background terminal can require excessive wrapping work and fill the inline history view.

## What changed

- Limit inline input previews to 12 rendered rows and bound preview processing to 64 KiB.
- Show a transcript hint when input is omitted, while preserving the complete input in transcript and raw views.
- Keep URLs intact when they fit and truncate only the final overflowing line when needed.

## Testing

Add regression coverage for long wrapped input, narrow viewports, zero-width Unicode payloads, URLs, and inputs that fit within the preview budget.

GitOrigin-RevId: c20edf156d5c8364083b1690e41cabd29786a279
  • Loading branch information
bc-openai authored and copyberry committed Aug 31, 2026
commit 115ffaf8bf1eda460526605cf44d9a96d88f4371
82 changes: 65 additions & 17 deletions codex-rs/tui/src/history_cell/exec.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
//! Background terminal interaction and process-summary history cells.
//! Background terminal history cells, with bounded inline input and full transcript views.

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 {
Expand All @@ -16,10 +21,8 @@ impl UnifiedExecInteractionCell {
stdin,
}
}
}

impl HistoryCell for UnifiedExecInteractionCell {
fn display_lines(&self, width: u16) -> Vec<Line<'static>> {
fn render_lines(&self, width: u16, max_input_rows: usize) -> Vec<Line<'static>> {
if width == 0 {
return Vec::new();
}
Expand Down Expand Up @@ -47,21 +50,62 @@ impl HistoryCell for UnifiedExecInteractionCell {
return out;
}

let input_lines: Vec<Line<'static>> = self
.stdin
.lines()
.map(|line| Line::from(line.to_string()))
.collect();

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);
// 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 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(),
);
}
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();
Expand Down Expand Up @@ -96,6 +140,10 @@ 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,
Expand Down
139 changes: 139 additions & 0 deletions codex-rs/tui/src/history_cell/exec_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
//! 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());
}
}
Loading