use std::time::Instant;use super::model::CommandOutput;use super::model::ExecCall;use super::model::ExecCell;use crate::exec_command::strip_bash_lc_and_escape;use crate::history_cell::HistoryCell;use crate::history_cell::plain_lines;use crate::motion::MotionMode;use crate::motion::ReducedMotionIndicator;use crate::motion::activity_indicator;use crate::render::highlight::highlight_bash_to_lines;use crate::render::line_utils::prefix_lines;use crate::render::line_utils::push_owned_lines;use crate::wrapping::RtOptions;use crate::wrapping::adaptive_wrap_line;use crate::wrapping::adaptive_wrap_lines;use codex_ansi_escape::ansi_escape_line;use codex_app_server_protocol::CommandExecutionSource as ExecCommandSource;use codex_protocol::parse_command::ParsedCommand;use codex_shell_command::bash::extract_bash_command;use codex_utils_elapsed::format_duration;use itertools::Itertools;use ratatui::prelude::*;use ratatui::style::Modifier;use ratatui::style::Stylize;use ratatui::widgets::Paragraph;use ratatui::widgets::Wrap;use textwrap::WordSplitter;use unicode_width::UnicodeWidthStr;pub(crate) const TOOL_CALL_MAX_LINES: usize = 5;const USER_SHELL_TOOL_CALL_MAX_LINES: usize = 50;const MAX_INTERACTION_PREVIEW_CHARS: usize = 80;const TRANSCRIPT_HINT: &str = "ctrl + t to view transcript";pub(crate) struct OutputLinesParams { pub(crate) line_limit: usize, pub(crate) only_err: bool, pub(crate) include_angle_pipe: bool, pub(crate) include_prefix: bool,}pub(crate) fn new_active_exec_command( call_id: String, command: Vec<String>, parsed: Vec<ParsedCommand>, source: ExecCommandSource, interaction_input: Option<String>, animations_enabled: bool,) -> ExecCell { ExecCell::new( ExecCall { call_id, command, parsed, output: None, source, start_time: Some(Instant::now()), duration: None, interaction_input, }, animations_enabled, )}fn format_unified_exec_interaction(command: &[String], input: Option<&str>) -> String { let command_display = if let Some((_, script)) = extract_bash_command(command) { script.to_string() } else { command.join(" ") }; match input { Some(data) if !data.is_empty() => { let preview = summarize_interaction_input(data); format!("Interacted with `{command_display}`, sent `{preview}`") } _ => format!("Waited for `{command_display}`"), }}fn summarize_interaction_input(input: &str) -> String { let single_line = input.replace('\n', "\\n"); let sanitized = single_line.replace('`', "\\`"); if sanitized.chars().count() <= MAX_INTERACTION_PREVIEW_CHARS { return sanitized; } let mut preview = String::new(); for ch in sanitized.chars().take(MAX_INTERACTION_PREVIEW_CHARS) { preview.push(ch); } preview.push_str("..."); preview}#[derive(Clone)]pub(crate) struct OutputLines { pub(crate) lines: Vec<Line<'static>>, pub(crate) omitted: Option<usize>,}pub(crate) fn output_lines( output: Option<&CommandOutput>, params: OutputLinesParams,) -> OutputLines { let OutputLinesParams { line_limit, only_err, include_angle_pipe, include_prefix, } = params; let CommandOutput { aggregated_output, .. } = match output { Some(output) if only_err && output.exit_code == 0 => { return OutputLines { lines: Vec::new(), omitted: None, }; } Some(output) => output, None => { return OutputLines { lines: Vec::new(), omitted: None, }; } }; let src = aggregated_output; let lines: Vec<&str> = src.lines().collect(); let total = lines.len(); let mut out: Vec<Line<'static>> = Vec::new(); let head_end = total.min(line_limit); for (i, raw) in lines[..head_end].iter().enumerate() { let mut line = ansi_escape_line(raw); let prefix = if !include_prefix { "" } else if i == 0 && include_angle_pipe { " └ " } else { " " }; line.spans.insert(0, prefix.into()); line.spans.iter_mut().for_each(|span| { span.style = span.style.add_modifier(Modifier::DIM); }); out.push(line); } let show_ellipsis = total > 2 * line_limit; let omitted = if show_ellipsis { Some(total - 2 * line_limit) } else { None }; if show_ellipsis { let omitted = total - 2 * line_limit; out.push(ExecCell::output_ellipsis_line(omitted)); } let tail_start = if show_ellipsis { total - line_limit } else { head_end }; for raw in lines[tail_start..].iter() { let mut line = ansi_escape_line(raw); if include_prefix { line.spans.insert(0, " ".into()); } line.spans.iter_mut().for_each(|span| { span.style = span.style.add_modifier(Modifier::DIM); }); out.push(line); } OutputLines { lines: out, omitted, }}fn activity_marker(start_time: Option<Instant>, animations_enabled: bool) -> Span<'static> { activity_indicator( start_time, MotionMode::from_animations_enabled(animations_enabled), ReducedMotionIndicator::StaticBullet, ) .unwrap_or_else(|| "•".dim())}impl HistoryCell for ExecCell { fn display_lines(&self, width: u16) -> Vec<Line<'static>> { if self.is_exploring_cell() { self.exploring_display_lines(width) } else { self.command_display_lines(width) } } fn transcript_lines(&self, width: u16) -> Vec<Line<'static>> { let mut lines: Vec<Line<'static>> = vec![]; for (i, call) in self.iter_calls().enumerate() { if i > 0 { lines.push("".into()); } let script = strip_bash_lc_and_escape(&call.command); let highlighted_script = highlight_bash_to_lines(&script); let cmd_display = adaptive_wrap_lines( &highlighted_script, RtOptions::new(width as usize) .initial_indent("$ ".magenta().into()) .subsequent_indent(" ".into()), ); lines.extend(cmd_display); if let Some(output) = call.output.as_ref() { if !call.is_unified_exec_interaction() { let wrap_width = width.max(1) as usize; let wrap_opts = RtOptions::new(wrap_width); for unwrapped in output.formatted_output.lines().map(ansi_escape_line) { let wrapped = adaptive_wrap_line(&unwrapped, wrap_opts.clone()); push_owned_lines(&wrapped, &mut lines); } } let duration = call .duration .map(format_duration) .unwrap_or_else(|| "unknown".to_string()); let mut result: Line = if output.exit_code == 0 { Line::from("✓".green().bold()) } else { Line::from(vec![ "✗".red().bold(), format!(" ({})", output.exit_code).into(), ]) }; result.push_span(format!(" • {duration}").dim()); lines.push(result); } } lines } fn raw_lines(&self) -> Vec<Line<'static>> { plain_lines(self.transcript_lines(u16::MAX)) }}impl ExecCell { fn output_ellipsis_text(omitted: usize) -> String { format!("… +{omitted} lines ({TRANSCRIPT_HINT})") } fn output_ellipsis_line(omitted: usize) -> Line<'static> { Line::from(vec![Self::output_ellipsis_text(omitted).dim()]) } fn exploring_display_lines(&self, width: u16) -> Vec<Line<'static>> { let mut out: Vec<Line<'static>> = Vec::new(); out.push(Line::from(vec![ if self.is_active() { activity_marker(self.active_start_time(), self.animations_enabled()) } else { "•".dim() }, " ".into(), if self.is_active() { "Exploring".bold() } else { "Explored".bold() }, ])); let mut calls = self.calls.clone(); let mut out_indented = Vec::new(); while !calls.is_empty() { let mut call = calls.remove(0); if call .parsed .iter() .all(|parsed| matches!(parsed, ParsedCommand::Read { .. })) { while let Some(next) = calls.first() { if next .parsed .iter() .all(|parsed| matches!(parsed, ParsedCommand::Read { .. })) { call.parsed.extend(next.parsed.clone()); calls.remove(0); } else { break; } } } let reads_only = call .parsed .iter() .all(|parsed| matches!(parsed, ParsedCommand::Read { .. })); let call_lines: Vec<(&str, Vec<Span<'static>>)> = if reads_only { let names = call .parsed .iter() .map(|parsed| match parsed { ParsedCommand::Read { name, .. } => name.clone(), _ => unreachable!(), }) .unique(); vec![( "Read", Itertools::intersperse(names.into_iter().map(Into::into), ", ".dim()).collect(), )] } else { let mut lines = Vec::new(); for parsed in &call.parsed { match parsed { ParsedCommand::Read { name, .. } => { lines.push(("Read", vec![name.clone().into()])); } ParsedCommand::ListFiles { cmd, path } => { lines.push(("List", vec![path.clone().unwrap_or(cmd.clone()).into()])); } ParsedCommand::Search { cmd, query, path } => { let spans = match (query, path) { (Some(q), Some(p)) => { vec![q.clone().into(), " in ".dim(), p.clone().into()] } (Some(q), None) => vec![q.clone().into()], _ => vec![cmd.clone().into()], }; lines.push(("Search", spans)); } ParsedCommand::Unknown { cmd } => { lines.push(("Run", vec![cmd.clone().into()])); } } } lines }; for (title, line) in call_lines { let line = Line::from(line); let initial_indent = Line::from(vec![title.cyan(), " ".into()]); let subsequent_indent = " ".repeat(initial_indent.width()).into(); let wrapped = adaptive_wrap_line( &line, RtOptions::new(width as usize) .initial_indent(initial_indent) .subsequent_indent(subsequent_indent), ); push_owned_lines(&wrapped, &mut out_indented); } } out.extend(prefix_lines(out_indented, " └ ".dim(), " ".into())); out } fn command_display_lines(&self, width: u16) -> Vec<Line<'static>> { let [call] = &self.calls.as_slice() else { panic!("Expected exactly one call in a command display cell"); }; let layout = EXEC_DISPLAY_LAYOUT; let success = call.output.as_ref().map(|o| o.exit_code == 0); let bullet = match success { Some(true) => "•".green().bold(), Some(false) => "•".red().bold(), None => activity_marker(call.start_time, self.animations_enabled()), }; let is_interaction = call.is_unified_exec_interaction(); let title = if is_interaction { "" } else if self.is_active() { "Running" } else if call.is_user_shell_command() { "You ran" } else { "Ran" }; let mut header_line = if is_interaction { Line::from(vec![bullet.clone(), " ".into()]) } else { Line::from(vec![bullet.clone(), " ".into(), title.bold(), " ".into()]) }; let header_prefix_width = header_line.width(); let cmd_display = if call.is_unified_exec_interaction() { format_unified_exec_interaction(&call.command, call.interaction_input.as_deref()) } else { strip_bash_lc_and_escape(&call.command) }; let highlighted_lines = highlight_bash_to_lines(&cmd_display); let continuation_wrap_width = layout.command_continuation.wrap_width(width); let continuation_opts = RtOptions::new(continuation_wrap_width).word_splitter(WordSplitter::NoHyphenation); let mut continuation_lines: Vec<Line<'static>> = Vec::new(); if let Some((first, rest)) = highlighted_lines.split_first() { let available_first_width = (width as usize).saturating_sub(header_prefix_width).max(1); let first_opts = RtOptions::new(available_first_width).word_splitter(WordSplitter::NoHyphenation); let mut first_wrapped: Vec<Line<'static>> = Vec::new(); push_owned_lines(&adaptive_wrap_line(first, first_opts), &mut first_wrapped); let mut first_wrapped_iter = first_wrapped.into_iter(); if let Some(first_segment) = first_wrapped_iter.next() { header_line.extend(first_segment); } continuation_lines.extend(first_wrapped_iter); for line in rest { push_owned_lines( &adaptive_wrap_line(line, continuation_opts.clone()), &mut continuation_lines, ); } } let mut lines: Vec<Line<'static>> = vec![header_line]; let continuation_lines = Self::limit_lines_from_start( &continuation_lines, layout.command_continuation_max_lines, ); if !continuation_lines.is_empty() { lines.extend(prefix_lines( continuation_lines, Span::from(layout.command_continuation.initial_prefix).dim(), Span::from(layout.command_continuation.subsequent_prefix).dim(), )); } if let Some(output) = call.output.as_ref() { let line_limit = if call.is_user_shell_command() { USER_SHELL_TOOL_CALL_MAX_LINES } else { TOOL_CALL_MAX_LINES }; let raw_output = output_lines( Some(output), OutputLinesParams { line_limit, only_err: false, include_angle_pipe: false, include_prefix: false, }, ); let display_limit = if call.is_user_shell_command() { USER_SHELL_TOOL_CALL_MAX_LINES } else { layout.output_max_lines }; if raw_output.lines.is_empty() { if !call.is_unified_exec_interaction() { lines.extend(prefix_lines( vec![Line::from("(no output)".dim())], Span::from(layout.output_block.initial_prefix).dim(), Span::from(layout.output_block.subsequent_prefix), )); } } else { // Wrap first so that truncation is applied to on-screen lines // rather than logical lines. This ensures that a small number // of very long lines cannot flood the viewport. let mut wrapped_output: Vec<Line<'static>> = Vec::new(); let output_wrap_width = layout.output_block.wrap_width(width); let output_opts = RtOptions::new(output_wrap_width).word_splitter(WordSplitter::NoHyphenation); for line in &raw_output.lines { push_owned_lines( &adaptive_wrap_line(line, output_opts.clone()), &mut wrapped_output, ); } let prefixed_output = prefix_lines( wrapped_output, Span::from(layout.output_block.initial_prefix).dim(), Span::from(layout.output_block.subsequent_prefix), ); let trimmed_output = Self::truncate_lines_middle( &prefixed_output, display_limit, width, raw_output.omitted, Some(Line::from( Span::from(layout.output_block.subsequent_prefix).dim(), )), ); if !trimmed_output.is_empty() { lines.extend(trimmed_output); } } } lines } fn limit_lines_from_start(lines: &[Line<'static>], keep: usize) -> Vec<Line<'static>> { if lines.len() <= keep { return lines.to_vec(); } if keep == 0 { return vec![Self::ellipsis_line(lines.len())]; } let mut out: Vec<Line<'static>> = lines[..keep].to_vec(); out.push(Self::ellipsis_line(lines.len() - keep)); out } /// Truncates a list of lines to fit within `max_rows` viewport rows, /// keeping a head portion and a tail portion with an ellipsis line /// in between. /// /// `max_rows` is measured in viewport rows (the actual space a line /// occupies after `Paragraph::wrap`), not logical lines. Each line's /// row cost is computed via `Paragraph::line_count` at the given /// `width`. This ensures that a single logical line containing a /// long URL (which wraps to several viewport rows) is properly /// accounted for. /// /// The ellipsis message reports the number of omitted *lines* /// (logical, not rows) to keep the count stable across terminal /// widths. `omitted_hint` carries forward any previously reported /// omitted count (from upstream truncation); `ellipsis_prefix` /// prepends the output gutter prefix to the ellipsis line. fn truncate_lines_middle( lines: &[Line<'static>], max_rows: usize, width: u16, omitted_hint: Option<usize>, ellipsis_prefix: Option<Line<'static>>, ) -> Vec<Line<'static>> { let width = width.max(1); if max_rows == 0 { return Vec::new(); } let line_rows: Vec<usize> = lines .iter() .map(|line| { let is_whitespace_only = line .spans .iter() .all(|span| span.content.chars().all(char::is_whitespace)); if is_whitespace_only { line.width().div_ceil(usize::from(width)).max(1) } else { Paragraph::new(Text::from(vec![line.clone()])) .wrap(Wrap { trim: false }) .line_count(width) .max(1) } }) .collect(); let total_rows: usize = line_rows.iter().sum(); if total_rows <= max_rows { return lines.to_vec(); } // Reserve space for the transcript hint itself so the returned output // still respects the row budget on narrow terminals. let estimated_omitted = omitted_hint.unwrap_or(0) + lines .len() .saturating_sub(usize::from(omitted_hint.is_some())); let ellipsis_rows = Self::output_ellipsis_row_count(estimated_omitted, width, ellipsis_prefix.as_ref()); if ellipsis_rows >= max_rows { return vec![Self::output_ellipsis_line_with_prefix( estimated_omitted, ellipsis_prefix.as_ref(), )]; } let available_rows = max_rows - ellipsis_rows; let head_budget = available_rows / 2; let tail_budget = available_rows - head_budget; let mut head_lines: Vec<Line<'static>> = Vec::new(); let mut head_rows = 0usize; let mut head_end = 0usize; while head_end < lines.len() { let line_row_count = line_rows[head_end]; if head_rows + line_row_count > head_budget { break; } head_rows += line_row_count; head_lines.push(lines[head_end].clone()); head_end += 1; } let mut tail_lines_reversed: Vec<Line<'static>> = Vec::new(); let mut tail_rows = 0usize; let mut tail_start = lines.len(); while tail_start > head_end { let idx = tail_start - 1; let line_row_count = line_rows[idx]; if tail_rows + line_row_count > tail_budget { break; } tail_rows += line_row_count; tail_lines_reversed.push(lines[idx].clone()); tail_start -= 1; } let mut out = head_lines; let base = omitted_hint.unwrap_or(0); let additional = lines .len() .saturating_sub(out.len() + tail_lines_reversed.len()) .saturating_sub(usize::from(omitted_hint.is_some())); out.push(Self::output_ellipsis_line_with_prefix( base + additional, ellipsis_prefix.as_ref(), )); out.extend(tail_lines_reversed.into_iter().rev()); out } fn ellipsis_line(omitted: usize) -> Line<'static> { Line::from(vec![format!("… +{omitted} lines").dim()]) } fn output_ellipsis_row_count( omitted: usize, width: u16, prefix: Option<&Line<'static>>, ) -> usize { Paragraph::new(Text::from(vec![Self::output_ellipsis_line_with_prefix( omitted, prefix, )])) .wrap(Wrap { trim: false }) .line_count(width) .max(1) } /// Builds an output ellipsis line (`… +N lines (ctrl + t to view transcript)`) /// with an optional leading prefix so the ellipsis aligns with the output gutter. fn output_ellipsis_line_with_prefix( omitted: usize, prefix: Option<&Line<'static>>, ) -> Line<'static> { let mut line = prefix.cloned().unwrap_or_default(); line.push_span(Self::output_ellipsis_text(omitted).dim()); line }}#[derive(Clone, Copy)]struct PrefixedBlock { initial_prefix: &'static str, subsequent_prefix: &'static str,}impl PrefixedBlock { const fn new(initial_prefix: &'static str, subsequent_prefix: &'static str) -> Self { Self { initial_prefix, subsequent_prefix, } } fn wrap_width(self, total_width: u16) -> usize { let prefix_width = UnicodeWidthStr::width(self.initial_prefix) .max(UnicodeWidthStr::width(self.subsequent_prefix)); usize::from(total_width).saturating_sub(prefix_width).max(1) }}#[derive(Clone, Copy)]struct ExecDisplayLayout { command_continuation: PrefixedBlock, command_continuation_max_lines: usize, output_block: PrefixedBlock, output_max_lines: usize,}impl ExecDisplayLayout { const fn new( command_continuation: PrefixedBlock, command_continuation_max_lines: usize, output_block: PrefixedBlock, output_max_lines: usize, ) -> Self { Self { command_continuation, command_continuation_max_lines, output_block, output_max_lines, } }}const EXEC_DISPLAY_LAYOUT: ExecDisplayLayout = ExecDisplayLayout::new( PrefixedBlock::new(" │ ", " │ "), /*command_continuation_max_lines*/ 2, PrefixedBlock::new(" └ ", " "), /*output_max_lines*/ 5,);#[cfg(test)]mod tests { use super::*; use codex_app_server_protocol::CommandExecutionSource as ExecCommandSource; use pretty_assertions::assert_eq; fn render_line_text(line: &Line<'static>) -> String { line.spans .iter() .map(|span| span.content.as_ref()) .collect::<String>() } #[test] fn user_shell_output_is_limited_by_screen_lines() { let long_url_like = format!( "https://example.test/api/v1/projects/alpha-team/releases/2026-02-17/builds/1234567890/{}", "very-long-segment-".repeat(120), ); let aggregated_output = format!("{long_url_like}\n{long_url_like}\n"); // Baseline: how many screen lines would we get if we simply wrapped // all logical lines without any truncation? let output = CommandOutput { exit_code: 0, aggregated_output, formatted_output: String::new(), }; let width = 20; let layout = EXEC_DISPLAY_LAYOUT; let raw_output = output_lines( Some(&output), OutputLinesParams { // Large enough to include all logical lines without // triggering the ellipsis in `output_lines`. line_limit: 100, only_err: false, include_angle_pipe: false, include_prefix: false, }, ); let output_wrap_width = layout.output_block.wrap_width(width); let output_opts = RtOptions::new(output_wrap_width).word_splitter(WordSplitter::NoHyphenation); let mut full_wrapped_output: Vec<Line<'static>> = Vec::new(); for line in &raw_output.lines { push_owned_lines( &adaptive_wrap_line(line, output_opts.clone()), &mut full_wrapped_output, ); } let full_prefixed_output = prefix_lines( full_wrapped_output, Span::from(layout.output_block.initial_prefix).dim(), Span::from(layout.output_block.subsequent_prefix), ); let full_screen_lines = Paragraph::new(Text::from(full_prefixed_output)) .wrap(Wrap { trim: false }) .line_count(width); // Sanity check: this scenario should produce more screen lines than // the user shell per-call limit when no truncation is applied. If // this ever fails, the test no longer exercises the regression. assert!( full_screen_lines > USER_SHELL_TOOL_CALL_MAX_LINES, "expected unbounded wrapping to produce more than {USER_SHELL_TOOL_CALL_MAX_LINES} screen lines, got {full_screen_lines}", ); let call = ExecCall { call_id: "call-id".to_string(), command: vec!["bash".into(), "-lc".into(), "echo long".into()], parsed: Vec::new(), output: Some(output), source: ExecCommandSource::UserShell, start_time: None, duration: None, interaction_input: None, }; let cell = ExecCell::new(call, /*animations_enabled*/ false); // Use a narrow width so each logical line wraps into many on-screen lines. let lines = cell.command_display_lines(width); let rendered_rows = Paragraph::new(Text::from(lines.clone())) .wrap(Wrap { trim: false }) .line_count(width); let header_rows = Paragraph::new(Text::from(vec![lines[0].clone()])) .wrap(Wrap { trim: false }) .line_count(width); let output_screen_rows = rendered_rows.saturating_sub(header_rows); let contains_ellipsis = lines .iter() .any(|line| line.spans.iter().any(|span| span.content.contains("… +"))); // Regression guard: previously this scenario could render hundreds of // wrapped rows because truncation happened before final viewport // wrapping. The row-aware truncation now caps visible output rows. assert!( output_screen_rows <= USER_SHELL_TOOL_CALL_MAX_LINES, "expected at most {USER_SHELL_TOOL_CALL_MAX_LINES} output rows, got {output_screen_rows} (total rows: {rendered_rows})", ); assert!( contains_ellipsis, "expected truncated output to include an ellipsis line" ); let normalized = lines .iter() .map(render_line_text) .join(" ") .split_whitespace() .join(" "); assert!( normalized.contains(TRANSCRIPT_HINT), "expected truncated output to advertise transcript shortcut, got {normalized}" ); } #[test] fn truncate_lines_middle_keeps_omitted_count_in_line_units() { let lines = vec![ Line::from(" └ short"), Line::from(" this-is-a-very-long-token-that-wraps-many-rows"), Line::from(format!( " {}", ExecCell::output_ellipsis_text(/*omitted*/ 4) )), Line::from(" tail"), ]; let truncated = ExecCell::truncate_lines_middle( &lines, /*max_rows*/ 2, /*width*/ 80, Some(4), Some(Line::from(" ".dim())), ); let rendered: Vec<String> = truncated.iter().map(render_line_text).collect(); assert!( rendered .iter() .any(|line| line.contains("… +6 lines (ctrl + t to view transcript)")), "expected omitted hint to count hidden lines (not wrapped rows), got: {rendered:?}" ); } #[test] fn output_lines_ellipsis_includes_transcript_hint() { let output = CommandOutput { exit_code: 0, aggregated_output: (1..=7).map(|n| n.to_string()).join("\n"), formatted_output: String::new(), }; let rendered: Vec<String> = output_lines( Some(&output), OutputLinesParams { line_limit: 2, only_err: false, include_angle_pipe: false, include_prefix: false, }, ) .lines .iter() .map(render_line_text) .collect(); assert!( rendered .iter() .any(|line| line.contains("… +3 lines (ctrl + t to view transcript)")), "expected logical truncation to include transcript hint, got: {rendered:?}" ); } #[test] fn command_truncation_ellipsis_does_not_include_transcript_hint() { let truncated = ExecCell::limit_lines_from_start( &[ Line::from("first"), Line::from("second"), Line::from("third"), ], /*keep*/ 2, ); let rendered: Vec<String> = truncated.iter().map(render_line_text).collect(); assert_eq!( rendered, vec![ "first".to_string(), "second".to_string(), "… +1 lines".to_string(), ] ); } #[test] fn truncate_lines_middle_does_not_truncate_blank_prefixed_output_lines() { let mut lines = vec![Line::from(" └ start")]; lines.extend(std::iter::repeat_n(Line::from(" "), 26)); lines.push(Line::from(" end")); let truncated = ExecCell::truncate_lines_middle( &lines, /*max_rows*/ 28, /*width*/ 80, /*omitted_hint*/ None, /*ellipsis_prefix*/ None, ); assert_eq!(truncated, lines); } #[test] fn command_display_does_not_split_long_url_token() { let url = "http://example.com/long-url-with-dashes-wider-than-terminal-window/blah-blah-blah-text/more-gibberish-text"; let call = ExecCall { call_id: "call-id".to_string(), command: vec!["bash".into(), "-lc".into(), format!("echo {url}")], parsed: Vec::new(), output: None, source: ExecCommandSource::UserShell, start_time: None, duration: None, interaction_input: None, }; let cell = ExecCell::new(call, /*animations_enabled*/ false); let rendered: Vec<String> = cell .command_display_lines(/*width*/ 36) .iter() .map(|line| { line.spans .iter() .map(|span| span.content.as_ref()) .collect::<String>() }) .collect(); assert_eq!( rendered.iter().filter(|line| line.contains(url)).count(), 1, "expected full URL in one rendered line, got: {rendered:?}" ); } #[test] fn active_command_without_animations_is_stable() { let call = ExecCall { call_id: "call-id".to_string(), command: vec!["bash".into(), "-lc".into(), "echo done".into()], parsed: Vec::new(), output: None, source: ExecCommandSource::Agent, start_time: Some(Instant::now()), duration: None, interaction_input: None, }; let cell = ExecCell::new(call, /*animations_enabled*/ false); let first: Vec<String> = cell .command_display_lines(/*width*/ 80) .iter() .map(render_line_text) .collect(); let second: Vec<String> = cell .command_display_lines(/*width*/ 80) .iter() .map(render_line_text) .collect(); assert_eq!(first, second); assert_eq!(first, vec!["• Running echo done".to_string()]); } #[test] fn exploring_display_does_not_split_long_url_like_search_query() { let url_like = "example.test/api/v1/projects/alpha-team/releases/2026-02-17/builds/1234567890/artifacts/reports/performance/summary/detail/with/a/very/long/path"; let call = ExecCall { call_id: "call-id".to_string(), command: vec!["bash".into(), "-lc".into(), "rg foo".into()], parsed: vec![ParsedCommand::Search { cmd: format!("rg {url_like}"), query: Some(url_like.to_string()), path: None, }], output: None, source: ExecCommandSource::Agent, start_time: None, duration: None, interaction_input: None, }; let cell = ExecCell::new(call, /*animations_enabled*/ false); let rendered: Vec<String> = cell .display_lines(/*width*/ 36) .iter() .map(|line| { line.spans .iter() .map(|span| span.content.as_ref()) .collect::<String>() }) .collect(); assert_eq!( rendered .iter() .filter(|line| line.contains(url_like)) .count(), 1, "expected full URL-like query in one rendered line, got: {rendered:?}" ); } #[test] fn output_display_does_not_split_long_url_like_token_without_scheme() { let url = "example.test/api/v1/projects/alpha-team/releases/2026-02-17/builds/1234567890/artifacts/reports/performance/summary/detail/session_id=abc123def456ghi789jkl012mno345pqr678"; let call = ExecCall { call_id: "call-id".to_string(), command: vec!["bash".into(), "-lc".into(), "echo done".into()], parsed: Vec::new(), output: Some(CommandOutput { exit_code: 0, formatted_output: String::new(), aggregated_output: url.to_string(), }), source: ExecCommandSource::UserShell, start_time: None, duration: None, interaction_input: None, }; let cell = ExecCell::new(call, /*animations_enabled*/ false); let rendered: Vec<String> = cell .command_display_lines(/*width*/ 36) .iter() .map(|line| { line.spans .iter() .map(|span| span.content.as_ref()) .collect::<String>() }) .collect(); assert_eq!( rendered.iter().filter(|line| line.contains(url)).count(), 1, "expected full URL-like token in one rendered line, got: {rendered:?}" ); } #[test] fn desired_transcript_height_accounts_for_wrapped_url_like_rows() { let url = "https://example.test/api/v1/projects/alpha-team/releases/2026-02-17/builds/1234567890/artifacts/reports/performance/summary/detail/with/a/very/long/path/that/keeps/going/for/testing/purposes"; let call = ExecCall { call_id: "call-id".to_string(), command: vec!["bash".into(), "-lc".into(), "echo done".into()], parsed: Vec::new(), output: Some(CommandOutput { exit_code: 0, formatted_output: url.to_string(), aggregated_output: url.to_string(), }), source: ExecCommandSource::Agent, start_time: None, duration: None, interaction_input: None, }; let cell = ExecCell::new(call, /*animations_enabled*/ false); let width: u16 = 36; let logical_height = cell.transcript_lines(width).len() as u16; let wrapped_height = cell.desired_transcript_height(width); assert!( wrapped_height > logical_height, "expected transcript height to account for wrapped URL-like rows, logical_height={logical_height}, wrapped_height={wrapped_height}" ); }}