Codex Handbook
tui/src/exec_cell/render.rs 1095 lines
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}"        );    }}