Show exec output on success with trimmed display (#4113)
- Refactor Exec Cell into its own module - update exec command rendering to inline the first command line - limit continuation lines - always show trimmed output
This commit is contained in:
@@ -70,11 +70,12 @@ use crate::bottom_pane::custom_prompt_view::CustomPromptView;
|
||||
use crate::bottom_pane::popup_consts::STANDARD_POPUP_HINT_LINE;
|
||||
use crate::clipboard_paste::paste_image_to_temp_png;
|
||||
use crate::diff_render::display_path_for;
|
||||
use crate::exec_cell::CommandOutput;
|
||||
use crate::exec_cell::ExecCell;
|
||||
use crate::exec_cell::new_active_exec_command;
|
||||
use crate::get_git_diff::get_git_diff;
|
||||
use crate::history_cell;
|
||||
use crate::history_cell::AgentMessageCell;
|
||||
use crate::history_cell::CommandOutput;
|
||||
use crate::history_cell::ExecCell;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::history_cell::McpToolCallCell;
|
||||
use crate::history_cell::PatchEventType;
|
||||
@@ -673,7 +674,7 @@ impl ChatWidget {
|
||||
.unwrap_or(true);
|
||||
if needs_new {
|
||||
self.flush_active_cell();
|
||||
self.active_cell = Some(Box::new(history_cell::new_active_exec_command(
|
||||
self.active_cell = Some(Box::new(new_active_exec_command(
|
||||
ev.call_id.clone(),
|
||||
command,
|
||||
parsed,
|
||||
@@ -777,7 +778,7 @@ impl ChatWidget {
|
||||
} else {
|
||||
self.flush_active_cell();
|
||||
|
||||
self.active_cell = Some(Box::new(history_cell::new_active_exec_command(
|
||||
self.active_cell = Some(Box::new(new_active_exec_command(
|
||||
ev.call_id.clone(),
|
||||
ev.command.clone(),
|
||||
ev.parsed_cmd,
|
||||
|
||||
@@ -17,11 +17,15 @@ expression: visible_after
|
||||
through crates for heavy dependencies in Cargo.toml, including cli, core,
|
||||
exec, linux-sandbox, tui, login, ollama, and mcp.
|
||||
|
||||
• Ran
|
||||
└ for d in ansi-escape apply-patch arg0 cli common core exec execpolicy
|
||||
file-search linux-sandbox login mcp-client mcp-server mcp-types ollama
|
||||
tui; do echo "--- $d/Cargo.toml"; sed -n '1,200p' $d/Cargo.toml; echo;
|
||||
done
|
||||
• Ran for d in ansi-escape apply-patch arg0 cli common core exec execpolicy
|
||||
│ file-search linux-sandbox login mcp-client mcp-server mcp-types ollama
|
||||
│ tui; do echo "--- $d/Cargo.toml"; sed -n '1,200p' $d/Cargo.toml; echo;
|
||||
│ … +1 lines
|
||||
└ --- ansi-escape/Cargo.toml
|
||||
[package]
|
||||
… +7 lines
|
||||
] }
|
||||
tracing = { version
|
||||
|
||||
• Explored
|
||||
└ Read Cargo.toml
|
||||
|
||||
12
codex-rs/tui/src/exec_cell/mod.rs
Normal file
12
codex-rs/tui/src/exec_cell/mod.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
mod model;
|
||||
mod render;
|
||||
|
||||
pub(crate) use model::CommandOutput;
|
||||
#[cfg(test)]
|
||||
pub(crate) use model::ExecCall;
|
||||
pub(crate) use model::ExecCell;
|
||||
pub(crate) use render::OutputLinesParams;
|
||||
pub(crate) use render::TOOL_CALL_MAX_LINES;
|
||||
pub(crate) use render::new_active_exec_command;
|
||||
pub(crate) use render::output_lines;
|
||||
pub(crate) use render::spinner;
|
||||
123
codex-rs/tui/src/exec_cell/model.rs
Normal file
123
codex-rs/tui/src/exec_cell/model.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
use std::time::Duration;
|
||||
use std::time::Instant;
|
||||
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct CommandOutput {
|
||||
pub(crate) exit_code: i32,
|
||||
pub(crate) stdout: String,
|
||||
pub(crate) stderr: String,
|
||||
pub(crate) formatted_output: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ExecCall {
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) command: Vec<String>,
|
||||
pub(crate) parsed: Vec<ParsedCommand>,
|
||||
pub(crate) output: Option<CommandOutput>,
|
||||
pub(crate) start_time: Option<Instant>,
|
||||
pub(crate) duration: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ExecCell {
|
||||
pub(crate) calls: Vec<ExecCall>,
|
||||
}
|
||||
|
||||
impl ExecCell {
|
||||
pub(crate) fn new(call: ExecCall) -> Self {
|
||||
Self { calls: vec![call] }
|
||||
}
|
||||
|
||||
pub(crate) fn with_added_call(
|
||||
&self,
|
||||
call_id: String,
|
||||
command: Vec<String>,
|
||||
parsed: Vec<ParsedCommand>,
|
||||
) -> Option<Self> {
|
||||
let call = ExecCall {
|
||||
call_id,
|
||||
command,
|
||||
parsed,
|
||||
output: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
};
|
||||
if self.is_exploring_cell() && Self::is_exploring_call(&call) {
|
||||
Some(Self {
|
||||
calls: [self.calls.clone(), vec![call]].concat(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn complete_call(
|
||||
&mut self,
|
||||
call_id: &str,
|
||||
output: CommandOutput,
|
||||
duration: Duration,
|
||||
) {
|
||||
if let Some(call) = self.calls.iter_mut().rev().find(|c| c.call_id == call_id) {
|
||||
call.output = Some(output);
|
||||
call.duration = Some(duration);
|
||||
call.start_time = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn should_flush(&self) -> bool {
|
||||
!self.is_exploring_cell() && self.calls.iter().all(|c| c.output.is_some())
|
||||
}
|
||||
|
||||
pub(crate) fn mark_failed(&mut self) {
|
||||
for call in self.calls.iter_mut() {
|
||||
if call.output.is_none() {
|
||||
let elapsed = call
|
||||
.start_time
|
||||
.map(|st| st.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_millis(0));
|
||||
call.start_time = None;
|
||||
call.duration = Some(elapsed);
|
||||
call.output = Some(CommandOutput {
|
||||
exit_code: 1,
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
formatted_output: String::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn is_exploring_cell(&self) -> bool {
|
||||
self.calls.iter().all(Self::is_exploring_call)
|
||||
}
|
||||
|
||||
pub(crate) fn is_active(&self) -> bool {
|
||||
self.calls.iter().any(|c| c.output.is_none())
|
||||
}
|
||||
|
||||
pub(crate) fn active_start_time(&self) -> Option<Instant> {
|
||||
self.calls
|
||||
.iter()
|
||||
.find(|c| c.output.is_none())
|
||||
.and_then(|c| c.start_time)
|
||||
}
|
||||
|
||||
pub(crate) fn iter_calls(&self) -> impl Iterator<Item = &ExecCall> {
|
||||
self.calls.iter()
|
||||
}
|
||||
|
||||
pub(super) fn is_exploring_call(call: &ExecCall) -> bool {
|
||||
!call.parsed.is_empty()
|
||||
&& call.parsed.iter().all(|p| {
|
||||
matches!(
|
||||
p,
|
||||
ParsedCommand::Read { .. }
|
||||
| ParsedCommand::ListFiles { .. }
|
||||
| ParsedCommand::Search { .. }
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
492
codex-rs/tui/src/exec_cell/render.rs
Normal file
492
codex-rs/tui/src/exec_cell/render.rs
Normal file
@@ -0,0 +1,492 @@
|
||||
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::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::word_wrap_line;
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_common::elapsed::format_duration;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use itertools::Itertools;
|
||||
use ratatui::prelude::*;
|
||||
use ratatui::style::Modifier;
|
||||
use ratatui::style::Stylize;
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
use ratatui::widgets::Wrap;
|
||||
use textwrap::WordSplitter;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
pub(crate) const TOOL_CALL_MAX_LINES: usize = 5;
|
||||
|
||||
pub(crate) struct OutputLinesParams {
|
||||
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>,
|
||||
) -> ExecCell {
|
||||
ExecCell::new(ExecCall {
|
||||
call_id,
|
||||
command,
|
||||
parsed,
|
||||
output: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn output_lines(
|
||||
output: Option<&CommandOutput>,
|
||||
params: OutputLinesParams,
|
||||
) -> Vec<Line<'static>> {
|
||||
let OutputLinesParams {
|
||||
only_err,
|
||||
include_angle_pipe,
|
||||
include_prefix,
|
||||
} = params;
|
||||
let CommandOutput {
|
||||
exit_code,
|
||||
stdout,
|
||||
stderr,
|
||||
..
|
||||
} = match output {
|
||||
Some(output) if only_err && output.exit_code == 0 => return vec![],
|
||||
Some(output) => output,
|
||||
None => return vec![],
|
||||
};
|
||||
|
||||
let src = if *exit_code == 0 { stdout } else { stderr };
|
||||
let lines: Vec<&str> = src.lines().collect();
|
||||
let total = lines.len();
|
||||
let limit = TOOL_CALL_MAX_LINES;
|
||||
|
||||
let mut out = Vec::new();
|
||||
|
||||
let head_end = total.min(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 * limit;
|
||||
if show_ellipsis {
|
||||
let omitted = total - 2 * limit;
|
||||
out.push(format!("… +{omitted} lines").into());
|
||||
}
|
||||
|
||||
let tail_start = if show_ellipsis {
|
||||
total - 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);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
pub(crate) fn spinner(start_time: Option<Instant>) -> Span<'static> {
|
||||
const FRAMES: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||
let idx = start_time
|
||||
.map(|st| ((st.elapsed().as_millis() / 100) as usize) % FRAMES.len())
|
||||
.unwrap_or(0);
|
||||
let ch = FRAMES[idx];
|
||||
ch.to_string().into()
|
||||
}
|
||||
|
||||
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) -> Vec<Line<'static>> {
|
||||
let mut lines: Vec<Line<'static>> = vec![];
|
||||
for call in self.iter_calls() {
|
||||
let cmd_display = strip_bash_lc_and_escape(&call.command);
|
||||
for (i, part) in cmd_display.lines().enumerate() {
|
||||
if i == 0 {
|
||||
lines.push(vec!["$ ".magenta(), part.to_string().into()].into());
|
||||
} else {
|
||||
lines.push(vec![" ".into(), part.to_string().into()].into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(output) = call.output.as_ref() {
|
||||
lines.extend(output.formatted_output.lines().map(ansi_escape_line));
|
||||
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.push("".into());
|
||||
}
|
||||
lines
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for &ExecCell {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
if area.height == 0 {
|
||||
return;
|
||||
}
|
||||
let content_area = Rect {
|
||||
x: area.x,
|
||||
y: area.y,
|
||||
width: area.width,
|
||||
height: area.height,
|
||||
};
|
||||
let lines = self.display_lines(area.width);
|
||||
let max_rows = area.height as usize;
|
||||
let rendered = if lines.len() > max_rows {
|
||||
lines[lines.len() - max_rows..].to_vec()
|
||||
} else {
|
||||
lines
|
||||
};
|
||||
|
||||
Paragraph::new(Text::from(rendered))
|
||||
.wrap(Wrap { trim: false })
|
||||
.render(content_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecCell {
|
||||
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() {
|
||||
spinner(self.active_start_time())
|
||||
} else {
|
||||
"•".bold()
|
||||
},
|
||||
" ".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 = word_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 => spinner(call.start_time),
|
||||
};
|
||||
let title = if self.is_active() { "Running" } else { "Ran" };
|
||||
|
||||
let mut header_line =
|
||||
Line::from(vec![bullet.clone(), " ".into(), title.bold(), " ".into()]);
|
||||
let header_prefix_width = header_line.width();
|
||||
|
||||
let cmd_display = 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(&word_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(
|
||||
&word_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 raw_output_lines = output_lines(
|
||||
Some(output),
|
||||
OutputLinesParams {
|
||||
only_err: false,
|
||||
include_angle_pipe: false,
|
||||
include_prefix: false,
|
||||
},
|
||||
);
|
||||
let trimmed_output =
|
||||
Self::truncate_lines_middle(&raw_output_lines, layout.output_max_lines);
|
||||
|
||||
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 trimmed_output {
|
||||
push_owned_lines(
|
||||
&word_wrap_line(&line, output_opts.clone()),
|
||||
&mut wrapped_output,
|
||||
);
|
||||
}
|
||||
|
||||
if !wrapped_output.is_empty() {
|
||||
lines.extend(prefix_lines(
|
||||
wrapped_output,
|
||||
Span::from(layout.output_block.initial_prefix).dim(),
|
||||
Span::from(layout.output_block.subsequent_prefix),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
fn truncate_lines_middle(lines: &[Line<'static>], max: usize) -> Vec<Line<'static>> {
|
||||
if max == 0 {
|
||||
return Vec::new();
|
||||
}
|
||||
if lines.len() <= max {
|
||||
return lines.to_vec();
|
||||
}
|
||||
if max == 1 {
|
||||
return vec![Self::ellipsis_line(lines.len())];
|
||||
}
|
||||
|
||||
let head = (max - 1) / 2;
|
||||
let tail = max - head - 1;
|
||||
let mut out: Vec<Line<'static>> = Vec::new();
|
||||
|
||||
if head > 0 {
|
||||
out.extend(lines[..head].iter().cloned());
|
||||
}
|
||||
|
||||
let omitted = lines.len().saturating_sub(head + tail);
|
||||
out.push(Self::ellipsis_line(omitted));
|
||||
|
||||
if tail > 0 {
|
||||
out.extend(lines[lines.len() - tail..].iter().cloned());
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn ellipsis_line(omitted: usize) -> Line<'static> {
|
||||
Line::from(vec![format!("… +{omitted} lines").dim()])
|
||||
}
|
||||
}
|
||||
|
||||
#[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(" │ ", " │ "),
|
||||
2,
|
||||
PrefixedBlock::new(" └ ", " "),
|
||||
5,
|
||||
);
|
||||
@@ -1,10 +1,14 @@
|
||||
use crate::diff_render::create_diff_summary;
|
||||
use crate::exec_cell::CommandOutput;
|
||||
use crate::exec_cell::OutputLinesParams;
|
||||
use crate::exec_cell::TOOL_CALL_MAX_LINES;
|
||||
use crate::exec_cell::output_lines;
|
||||
use crate::exec_cell::spinner;
|
||||
use crate::exec_command::relativize_to_home;
|
||||
use crate::exec_command::strip_bash_lc_and_escape;
|
||||
use crate::markdown::append_markdown;
|
||||
use crate::render::line_utils::line_to_static;
|
||||
use crate::render::line_utils::prefix_lines;
|
||||
use crate::render::line_utils::push_owned_lines;
|
||||
pub(crate) use crate::status::RateLimitSnapshotDisplay;
|
||||
pub(crate) use crate::status::new_status_output;
|
||||
pub(crate) use crate::status::rate_limit_snapshot_display;
|
||||
@@ -14,8 +18,6 @@ use crate::wrapping::RtOptions;
|
||||
use crate::wrapping::word_wrap_line;
|
||||
use crate::wrapping::word_wrap_lines;
|
||||
use base64::Engine;
|
||||
use codex_ansi_escape::ansi_escape_line;
|
||||
use codex_common::elapsed::format_duration;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config_types::ReasoningSummaryFormat;
|
||||
use codex_core::plan_tool::PlanItemArg;
|
||||
@@ -25,10 +27,8 @@ use codex_core::protocol::FileChange;
|
||||
use codex_core::protocol::McpInvocation;
|
||||
use codex_core::protocol::SessionConfiguredEvent;
|
||||
use codex_core::protocol_config_types::ReasoningEffort as ReasoningEffortConfig;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use image::DynamicImage;
|
||||
use image::ImageReader;
|
||||
use itertools::Itertools;
|
||||
use mcp_types::EmbeddedResourceResource;
|
||||
use mcp_types::ResourceLink;
|
||||
use ratatui::prelude::*;
|
||||
@@ -49,14 +49,6 @@ use std::time::Instant;
|
||||
use tracing::error;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct CommandOutput {
|
||||
pub(crate) exit_code: i32,
|
||||
pub(crate) stdout: String,
|
||||
pub(crate) stderr: String,
|
||||
pub(crate) formatted_output: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) enum PatchEventType {
|
||||
ApprovalRequest,
|
||||
@@ -266,357 +258,6 @@ impl HistoryCell for PatchHistoryCell {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ExecCall {
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) command: Vec<String>,
|
||||
pub(crate) parsed: Vec<ParsedCommand>,
|
||||
pub(crate) output: Option<CommandOutput>,
|
||||
start_time: Option<Instant>,
|
||||
duration: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct ExecCell {
|
||||
calls: Vec<ExecCall>,
|
||||
}
|
||||
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) -> Vec<Line<'static>> {
|
||||
let mut lines: Vec<Line<'static>> = vec![];
|
||||
for call in &self.calls {
|
||||
let cmd_display = strip_bash_lc_and_escape(&call.command);
|
||||
for (i, part) in cmd_display.lines().enumerate() {
|
||||
if i == 0 {
|
||||
lines.push(vec!["$ ".magenta(), part.to_string().into()].into());
|
||||
} else {
|
||||
lines.push(vec![" ".into(), part.to_string().into()].into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(output) = call.output.as_ref() {
|
||||
lines.extend(output.formatted_output.lines().map(ansi_escape_line));
|
||||
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.push("".into());
|
||||
}
|
||||
lines
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecCell {
|
||||
fn is_active(&self) -> bool {
|
||||
self.calls.iter().any(|c| c.output.is_none())
|
||||
}
|
||||
|
||||
fn exploring_display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
let mut out: Vec<Line<'static>> = Vec::new();
|
||||
let active_start_time = self
|
||||
.calls
|
||||
.iter()
|
||||
.find(|c| c.output.is_none())
|
||||
.and_then(|c| c.start_time);
|
||||
out.push(Line::from(vec![
|
||||
if self.is_active() {
|
||||
// Show an animated spinner while exploring
|
||||
spinner(active_start_time)
|
||||
} else {
|
||||
"•".bold()
|
||||
},
|
||||
" ".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(|c| matches!(c, ParsedCommand::Read { .. }))
|
||||
{
|
||||
while let Some(next) = calls.first() {
|
||||
if next
|
||||
.parsed
|
||||
.iter()
|
||||
.all(|c| matches!(c, ParsedCommand::Read { .. }))
|
||||
{
|
||||
call.parsed.extend(next.parsed.clone());
|
||||
calls.remove(0);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
let call_lines: Vec<(&str, Vec<Span<'static>>)> = if call
|
||||
.parsed
|
||||
.iter()
|
||||
.all(|c| matches!(c, ParsedCommand::Read { .. }))
|
||||
{
|
||||
let names = call
|
||||
.parsed
|
||||
.iter()
|
||||
.map(|c| match c {
|
||||
ParsedCommand::Read { name, .. } => name.clone(),
|
||||
_ => unreachable!(),
|
||||
})
|
||||
.unique();
|
||||
vec![(
|
||||
"Read",
|
||||
itertools::Itertools::intersperse(
|
||||
names.into_iter().map(Into::into),
|
||||
", ".dim(),
|
||||
)
|
||||
.collect(),
|
||||
)]
|
||||
} else {
|
||||
let mut lines = Vec::new();
|
||||
for p in call.parsed {
|
||||
match p {
|
||||
ParsedCommand::Read { name, .. } => {
|
||||
lines.push(("Read", vec![name.into()]));
|
||||
}
|
||||
ParsedCommand::ListFiles { cmd, path } => {
|
||||
lines.push(("List", vec![path.unwrap_or(cmd).into()]));
|
||||
}
|
||||
ParsedCommand::Search { cmd, query, path } => {
|
||||
lines.push((
|
||||
"Search",
|
||||
match (query, path) {
|
||||
(Some(q), Some(p)) => {
|
||||
vec![q.into(), " in ".dim(), p.into()]
|
||||
}
|
||||
(Some(q), None) => vec![q.into()],
|
||||
_ => vec![cmd.into()],
|
||||
},
|
||||
));
|
||||
}
|
||||
ParsedCommand::Unknown { cmd } => {
|
||||
lines.push(("Run", vec![cmd.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 = word_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>> {
|
||||
use textwrap::Options as TwOptions;
|
||||
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
let [call] = &self.calls.as_slice() else {
|
||||
panic!("Expected exactly one call in a command display cell");
|
||||
};
|
||||
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 => spinner(call.start_time),
|
||||
};
|
||||
let title = if self.is_active() { "Running" } else { "Ran" };
|
||||
let cmd_display = strip_bash_lc_and_escape(&call.command);
|
||||
|
||||
// If the command fits on the same line as the header at the current width,
|
||||
// show a single compact line: "• Ran <command>". Use the width of
|
||||
// "• Running " (including trailing space) as the reserved prefix width.
|
||||
// If the command contains newlines, always use the multi-line variant.
|
||||
let reserved = "• Running ".width();
|
||||
|
||||
let mut body_lines: Vec<Line<'static>> = Vec::new();
|
||||
|
||||
let highlighted_lines = crate::render::highlight::highlight_bash_to_lines(&cmd_display);
|
||||
|
||||
if highlighted_lines.len() == 1
|
||||
&& highlighted_lines[0].width() < (width as usize).saturating_sub(reserved)
|
||||
{
|
||||
let mut line = Line::from(vec![bullet, " ".into(), title.bold(), " ".into()]);
|
||||
line.extend(highlighted_lines[0].clone());
|
||||
lines.push(line);
|
||||
} else {
|
||||
lines.push(vec![bullet, " ".into(), title.bold()].into());
|
||||
|
||||
for hl_line in highlighted_lines.iter() {
|
||||
let opts = crate::wrapping::RtOptions::new((width as usize).saturating_sub(4))
|
||||
.initial_indent("".into())
|
||||
.subsequent_indent(" ".into())
|
||||
// Hyphenation likes to break words on hyphens, which is bad for bash scripts --because-of-flags.
|
||||
.word_splitter(textwrap::WordSplitter::NoHyphenation);
|
||||
let wrapped_borrowed = crate::wrapping::word_wrap_line(hl_line, opts);
|
||||
body_lines.extend(wrapped_borrowed.iter().map(|l| line_to_static(l)));
|
||||
}
|
||||
}
|
||||
if let Some(output) = call.output.as_ref()
|
||||
&& output.exit_code != 0
|
||||
{
|
||||
let out = output_lines(
|
||||
Some(output),
|
||||
OutputLinesParams {
|
||||
only_err: false,
|
||||
include_angle_pipe: false,
|
||||
include_prefix: false,
|
||||
},
|
||||
)
|
||||
.into_iter()
|
||||
.join("\n");
|
||||
if !out.trim().is_empty() {
|
||||
// Wrap the output.
|
||||
for line in out.lines() {
|
||||
let wrapped = textwrap::wrap(line, TwOptions::new(width as usize - 4));
|
||||
body_lines.extend(wrapped.into_iter().map(|l| Line::from(l.to_string().dim())));
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.extend(prefix_lines(body_lines, " └ ".dim(), " ".into()));
|
||||
lines
|
||||
}
|
||||
}
|
||||
|
||||
impl WidgetRef for &ExecCell {
|
||||
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
|
||||
if area.height == 0 {
|
||||
return;
|
||||
}
|
||||
let content_area = Rect {
|
||||
x: area.x,
|
||||
y: area.y,
|
||||
width: area.width,
|
||||
height: area.height,
|
||||
};
|
||||
let lines = self.display_lines(area.width);
|
||||
let max_rows = area.height as usize;
|
||||
let rendered = if lines.len() > max_rows {
|
||||
// Keep the last `max_rows` lines in original order
|
||||
lines[lines.len() - max_rows..].to_vec()
|
||||
} else {
|
||||
lines
|
||||
};
|
||||
|
||||
Paragraph::new(Text::from(rendered))
|
||||
.wrap(Wrap { trim: false })
|
||||
.render(content_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
impl ExecCell {
|
||||
pub(crate) fn mark_failed(&mut self) {
|
||||
for call in self.calls.iter_mut() {
|
||||
if call.output.is_none() {
|
||||
let elapsed = call
|
||||
.start_time
|
||||
.map(|st| st.elapsed())
|
||||
.unwrap_or_else(|| Duration::from_millis(0));
|
||||
call.start_time = None;
|
||||
call.duration = Some(elapsed);
|
||||
call.output = Some(CommandOutput {
|
||||
exit_code: 1,
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
formatted_output: String::new(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new(call: ExecCall) -> Self {
|
||||
ExecCell { calls: vec![call] }
|
||||
}
|
||||
|
||||
fn is_exploring_call(call: &ExecCall) -> bool {
|
||||
!call.parsed.is_empty()
|
||||
&& call.parsed.iter().all(|p| {
|
||||
matches!(
|
||||
p,
|
||||
ParsedCommand::Read { .. }
|
||||
| ParsedCommand::ListFiles { .. }
|
||||
| ParsedCommand::Search { .. }
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
fn is_exploring_cell(&self) -> bool {
|
||||
self.calls.iter().all(Self::is_exploring_call)
|
||||
}
|
||||
|
||||
pub(crate) fn with_added_call(
|
||||
&self,
|
||||
call_id: String,
|
||||
command: Vec<String>,
|
||||
parsed: Vec<ParsedCommand>,
|
||||
) -> Option<Self> {
|
||||
let call = ExecCall {
|
||||
call_id,
|
||||
command,
|
||||
parsed,
|
||||
output: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
};
|
||||
if self.is_exploring_cell() && Self::is_exploring_call(&call) {
|
||||
Some(Self {
|
||||
calls: [self.calls.clone(), vec![call]].concat(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn complete_call(
|
||||
&mut self,
|
||||
call_id: &str,
|
||||
output: CommandOutput,
|
||||
duration: Duration,
|
||||
) {
|
||||
if let Some(call) = self.calls.iter_mut().rev().find(|c| c.call_id == call_id) {
|
||||
call.output = Some(output);
|
||||
call.duration = Some(duration);
|
||||
call.start_time = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn should_flush(&self) -> bool {
|
||||
!self.is_exploring_cell() && self.calls.iter().all(|c| c.output.is_some())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct CompletedMcpToolCallWithImageOutput {
|
||||
_image: DynamicImage,
|
||||
@@ -627,7 +268,6 @@ impl HistoryCell for CompletedMcpToolCallWithImageOutput {
|
||||
}
|
||||
}
|
||||
|
||||
const TOOL_CALL_MAX_LINES: usize = 5;
|
||||
pub(crate) const SESSION_HEADER_MAX_INNER_WIDTH: usize = 56; // Just an eyeballed value
|
||||
|
||||
pub(crate) fn card_inner_width(width: u16, max_inner_width: usize) -> Option<usize> {
|
||||
@@ -783,21 +423,6 @@ pub(crate) fn new_user_approval_decision(lines: Vec<Line<'static>>) -> PlainHist
|
||||
PlainHistoryCell { lines }
|
||||
}
|
||||
|
||||
pub(crate) fn new_active_exec_command(
|
||||
call_id: String,
|
||||
command: Vec<String>,
|
||||
parsed: Vec<ParsedCommand>,
|
||||
) -> ExecCell {
|
||||
ExecCell::new(ExecCall {
|
||||
call_id,
|
||||
command,
|
||||
parsed,
|
||||
output: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct SessionHeaderHistoryCell {
|
||||
version: &'static str,
|
||||
@@ -1116,15 +741,6 @@ impl WidgetRef for &McpToolCallCell {
|
||||
}
|
||||
}
|
||||
|
||||
fn spinner(start_time: Option<Instant>) -> Span<'static> {
|
||||
const FRAMES: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
||||
let idx = start_time
|
||||
.map(|st| ((st.elapsed().as_millis() / 100) as usize) % FRAMES.len())
|
||||
.unwrap_or(0);
|
||||
let ch = FRAMES[idx];
|
||||
ch.to_string().into()
|
||||
}
|
||||
|
||||
pub(crate) fn new_active_mcp_tool_call(
|
||||
call_id: String,
|
||||
invocation: McpInvocation,
|
||||
@@ -1444,79 +1060,6 @@ pub(crate) fn new_reasoning_summary_block(
|
||||
Box::new(new_reasoning_block(full_reasoning_buffer, config))
|
||||
}
|
||||
|
||||
struct OutputLinesParams {
|
||||
only_err: bool,
|
||||
include_angle_pipe: bool,
|
||||
include_prefix: bool,
|
||||
}
|
||||
|
||||
fn output_lines(output: Option<&CommandOutput>, params: OutputLinesParams) -> Vec<Line<'static>> {
|
||||
let OutputLinesParams {
|
||||
only_err,
|
||||
include_angle_pipe,
|
||||
include_prefix,
|
||||
} = params;
|
||||
let CommandOutput {
|
||||
exit_code,
|
||||
stdout,
|
||||
stderr,
|
||||
..
|
||||
} = match output {
|
||||
Some(output) if only_err && output.exit_code == 0 => return vec![],
|
||||
Some(output) => output,
|
||||
None => return vec![],
|
||||
};
|
||||
|
||||
let src = if *exit_code == 0 { stdout } else { stderr };
|
||||
let lines: Vec<&str> = src.lines().collect();
|
||||
let total = lines.len();
|
||||
let limit = TOOL_CALL_MAX_LINES;
|
||||
|
||||
let mut out = Vec::new();
|
||||
|
||||
let head_end = total.min(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);
|
||||
}
|
||||
|
||||
// If we will ellipsize less than the limit, just show it.
|
||||
let show_ellipsis = total > 2 * limit;
|
||||
if show_ellipsis {
|
||||
let omitted = total - 2 * limit;
|
||||
out.push(format!("… +{omitted} lines").into());
|
||||
}
|
||||
|
||||
let tail_start = if show_ellipsis {
|
||||
total - 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);
|
||||
}
|
||||
|
||||
out
|
||||
}
|
||||
|
||||
fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> {
|
||||
let args_str = invocation
|
||||
.arguments
|
||||
@@ -1541,9 +1084,13 @@ fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::exec_cell::CommandOutput;
|
||||
use crate::exec_cell::ExecCall;
|
||||
use crate::exec_cell::ExecCell;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::config::ConfigOverrides;
|
||||
use codex_core::config::ConfigToml;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use dirs::home_dir;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
|
||||
@@ -41,6 +41,7 @@ mod cli;
|
||||
mod clipboard_paste;
|
||||
pub mod custom_terminal;
|
||||
mod diff_render;
|
||||
mod exec_cell;
|
||||
mod exec_command;
|
||||
mod file_search;
|
||||
mod frames;
|
||||
|
||||
@@ -556,7 +556,7 @@ mod tests {
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::history_cell::CommandOutput;
|
||||
use crate::exec_cell::CommandOutput;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::history_cell::PatchEventType;
|
||||
use crate::history_cell::new_patch_event;
|
||||
@@ -687,7 +687,7 @@ mod tests {
|
||||
]));
|
||||
cells.push(apply_end_cell);
|
||||
|
||||
let mut exec_cell = crate::history_cell::new_active_exec_command(
|
||||
let mut exec_cell = crate::exec_cell::new_active_exec_command(
|
||||
"exec-1".into(),
|
||||
vec!["bash".into(), "-lc".into(), "ls".into()],
|
||||
vec![ParsedCommand::Unknown { cmd: "ls".into() }],
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
---
|
||||
source: tui/src/history_cell.rs
|
||||
assertion_line: 1942
|
||||
expression: rendered
|
||||
---
|
||||
• Ran
|
||||
└ first_token_is_long_enou
|
||||
gh_to_wrap
|
||||
second_token_is_also_lon
|
||||
g_enough_to_wrap
|
||||
• Ran first_token_is_long_en
|
||||
│ ough_to_wrap
|
||||
│ second_token_is_also_lon
|
||||
│ … +1 lines
|
||||
|
||||
@@ -2,6 +2,5 @@
|
||||
source: tui/src/history_cell.rs
|
||||
expression: rendered
|
||||
---
|
||||
• Ran
|
||||
└ echo one
|
||||
echo two
|
||||
• Ran echo one
|
||||
│ echo two
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
---
|
||||
source: tui/src/history_cell.rs
|
||||
assertion_line: 1797
|
||||
expression: rendered
|
||||
---
|
||||
• Ran
|
||||
└ set -o pipefail
|
||||
cargo test
|
||||
--all-features
|
||||
--quiet
|
||||
• Ran set -o pipefail
|
||||
│ cargo test
|
||||
│ --all-features --quiet
|
||||
|
||||
@@ -2,13 +2,11 @@
|
||||
source: tui/src/history_cell.rs
|
||||
expression: rendered
|
||||
---
|
||||
• Ran
|
||||
└ echo
|
||||
this_is_a_very_long_
|
||||
single_token_that_wi
|
||||
ll_wrap_across_the_a
|
||||
vailable_width
|
||||
error: first line on
|
||||
• Ran echo
|
||||
│ this_is_a_very_long_si
|
||||
│ ngle_token_that_will_w
|
||||
│ … +2 lines
|
||||
└ error: first line on
|
||||
stderr
|
||||
error: second line on
|
||||
stderr
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
---
|
||||
source: tui/src/history_cell.rs
|
||||
assertion_line: 1869
|
||||
expression: rendered
|
||||
---
|
||||
• Ran
|
||||
└ a_very_long_token_wi
|
||||
thout_spaces_to_
|
||||
force_wrapping
|
||||
• Ran a_very_long_token_
|
||||
│ without_spaces_to_
|
||||
│ force_wrapping
|
||||
|
||||
@@ -5,11 +5,6 @@ expression: rendered
|
||||
• Ran seq 1 10 1>&2 && false
|
||||
└ 1
|
||||
2
|
||||
3
|
||||
4
|
||||
5
|
||||
6
|
||||
7
|
||||
8
|
||||
… +6 lines
|
||||
9
|
||||
10
|
||||
|
||||
Reference in New Issue
Block a user