Codex Handbook
tui/src/history_cell/exec.rs 242 lines
//! Background terminal interaction and process-summary history cells.use super::*;#[derive(Debug)]pub(crate) struct UnifiedExecInteractionCell {    command_display: Option<String>,    stdin: String,}impl UnifiedExecInteractionCell {    pub(crate) fn new(command_display: Option<String>, stdin: String) -> Self {        Self {            command_display,            stdin,        }    }}impl HistoryCell for UnifiedExecInteractionCell {    fn display_lines(&self, width: u16) -> Vec<Line<'static>> {        if width == 0 {            return Vec::new();        }        let wrap_width = width as usize;        let waited_only = self.stdin.is_empty();        let mut header_spans = if waited_only {            vec!["• Waited for background terminal".bold()]        } else {            vec!["↳ ".dim(), "Interacted with background terminal".bold()]        };        if let Some(command) = &self.command_display            && !command.is_empty()        {            header_spans.push(" · ".dim());            header_spans.push(command.clone().dim());        }        let header = Line::from(header_spans);        let mut out: Vec<Line<'static>> = Vec::new();        let header_wrapped = adaptive_wrap_line(&header, RtOptions::new(wrap_width));        push_owned_lines(&header_wrapped, &mut out);        if waited_only {            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);        out    }    fn raw_lines(&self) -> Vec<Line<'static>> {        let mut out = Vec::new();        if self.stdin.is_empty() {            if let Some(command) = self                .command_display                .as_ref()                .filter(|command| !command.is_empty())            {                out.push(Line::from(format!(                    "Waited for background terminal: {command}"                )));            } else {                out.push(Line::from("Waited for background terminal"));            }            return out;        }        if let Some(command) = self            .command_display            .as_ref()            .filter(|command| !command.is_empty())        {            out.push(Line::from(format!(                "Interacted with background terminal: {command}"            )));        } else {            out.push(Line::from("Interacted with background terminal"));        }        out.extend(raw_lines_from_source(&self.stdin));        out    }}pub(crate) fn new_unified_exec_interaction(    command_display: Option<String>,    stdin: String,) -> UnifiedExecInteractionCell {    UnifiedExecInteractionCell::new(command_display, stdin)}#[derive(Debug)]struct UnifiedExecProcessesCell {    processes: Vec<UnifiedExecProcessDetails>,}impl UnifiedExecProcessesCell {    fn new(processes: Vec<UnifiedExecProcessDetails>) -> Self {        Self { processes }    }}#[derive(Debug, Clone)]pub(crate) struct UnifiedExecProcessDetails {    pub(crate) command_display: String,    pub(crate) recent_chunks: Vec<String>,}impl HistoryCell for UnifiedExecProcessesCell {    fn display_lines(&self, width: u16) -> Vec<Line<'static>> {        if width == 0 {            return Vec::new();        }        let wrap_width = width as usize;        let max_processes = 16usize;        let mut out: Vec<Line<'static>> = Vec::new();        out.push(vec!["Background terminals".bold()].into());        out.push("".into());        if self.processes.is_empty() {            out.push("  • No background terminals running.".italic().into());            return out;        }        let prefix = "  • ";        let prefix_width = UnicodeWidthStr::width(prefix);        let truncation_suffix = " [...]";        let truncation_suffix_width = UnicodeWidthStr::width(truncation_suffix);        let mut shown = 0usize;        for process in &self.processes {            if shown >= max_processes {                break;            }            let command = &process.command_display;            let (snippet, snippet_truncated) = {                let (first_line, has_more_lines) = match command.split_once('\n') {                    Some((first, _)) => (first, true),                    None => (command.as_str(), false),                };                let max_graphemes = 80;                let mut graphemes = first_line.grapheme_indices(true);                if let Some((byte_index, _)) = graphemes.nth(max_graphemes) {                    (first_line[..byte_index].to_string(), true)                } else {                    (first_line.to_string(), has_more_lines)                }            };            if wrap_width <= prefix_width {                out.push(Line::from(prefix.dim()));                shown += 1;                continue;            }            let budget = wrap_width.saturating_sub(prefix_width);            let mut needs_suffix = snippet_truncated;            if !needs_suffix {                let (_, remainder, _) = take_prefix_by_width(&snippet, budget);                if !remainder.is_empty() {                    needs_suffix = true;                }            }            if needs_suffix && budget > truncation_suffix_width {                let available = budget.saturating_sub(truncation_suffix_width);                let (truncated, _, _) = take_prefix_by_width(&snippet, available);                out.push(vec![prefix.dim(), truncated.cyan(), truncation_suffix.dim()].into());            } else {                let (truncated, _, _) = take_prefix_by_width(&snippet, budget);                out.push(vec![prefix.dim(), truncated.cyan()].into());            }            let chunk_prefix_first = "    ↳ ";            let chunk_prefix_next = "      ";            for (idx, chunk) in process.recent_chunks.iter().enumerate() {                let chunk_prefix = if idx == 0 {                    chunk_prefix_first                } else {                    chunk_prefix_next                };                let chunk_prefix_width = UnicodeWidthStr::width(chunk_prefix);                if wrap_width <= chunk_prefix_width {                    out.push(Line::from(chunk_prefix.dim()));                    continue;                }                let budget = wrap_width.saturating_sub(chunk_prefix_width);                let (truncated, remainder, _) = take_prefix_by_width(chunk, budget);                if !remainder.is_empty() && budget > truncation_suffix_width {                    let available = budget.saturating_sub(truncation_suffix_width);                    let (shorter, _, _) = take_prefix_by_width(chunk, available);                    out.push(                        vec![chunk_prefix.dim(), shorter.dim(), truncation_suffix.dim()].into(),                    );                } else {                    out.push(vec![chunk_prefix.dim(), truncated.dim()].into());                }            }            shown += 1;        }        let remaining = self.processes.len().saturating_sub(shown);        if remaining > 0 {            let more_text = format!("... and {remaining} more running");            if wrap_width <= prefix_width {                out.push(Line::from(prefix.dim()));            } else {                let budget = wrap_width.saturating_sub(prefix_width);                let (truncated, _, _) = take_prefix_by_width(&more_text, budget);                out.push(vec![prefix.dim(), truncated.dim()].into());            }        }        out    }    fn raw_lines(&self) -> Vec<Line<'static>> {        plain_lines(self.display_lines(u16::MAX))    }    fn desired_height(&self, width: u16) -> u16 {        self.display_lines(width).len() as u16    }}pub(crate) fn new_unified_exec_processes_output(    processes: Vec<UnifiedExecProcessDetails>,) -> CompositeHistoryCell {    let command = PlainHistoryCell::new(vec!["/ps".magenta().into()]);    let summary = UnifiedExecProcessesCell::new(processes);    CompositeHistoryCell::new(vec![Box::new(command), Box::new(summary)])}