[1/3] Parse exec commands and format them more nicely in the UI (#2095)
# Note for reviewers The bulk of this PR is in in the new file, `parse_command.rs`. This file is designed to be written TDD and implemented with Codex. Do not worry about reviewing the code, just review the unit tests (if you want). If any cases are missing, we'll add more tests and have Codex fix them. I think the best approach will be to land and iterate. I have some follow-ups I want to do after this lands. The next PR after this will let us merge (and dedupe) multiple sequential cells of the same such as multiple read commands. The deduping will also be important because the model often reads the same file multiple times in a row in chunks === This PR formats common commands like reading, formatting, testing, etc more nicely: It tries to extract things like file names, tests and falls back to the cmd if it doesn't. It also only shows stdout/err if the command failed. <img width="770" height="238" alt="CleanShot 2025-08-09 at 16 05 15" src="https://github.com/user-attachments/assets/0ead179a-8910-486b-aa3d-7d26264d751e" /> <img width="348" height="158" alt="CleanShot 2025-08-09 at 16 05 32" src="https://github.com/user-attachments/assets/4302681b-5e87-4ff3-85b4-0252c6c485a9" /> <img width="834" height="324" alt="CleanShot 2025-08-09 at 16 05 56 2" src="https://github.com/user-attachments/assets/09fb3517-7bd6-40f6-a126-4172106b700f" /> Part 2: https://github.com/openai/codex/pull/2097 Part 3: https://github.com/openai/codex/pull/2110
This commit is contained in:
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
In the codex-rs folder where the rust code lives:
|
In the codex-rs folder where the rust code lives:
|
||||||
|
|
||||||
|
- Crate names are prefixed with `codex-`. For examole, the `core` folder's crate is named `codex-core`
|
||||||
|
- When using format! and you can inline variables into {}, always do that.
|
||||||
- Never add or modify any code related to `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` or `CODEX_SANDBOX_ENV_VAR`.
|
- Never add or modify any code related to `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` or `CODEX_SANDBOX_ENV_VAR`.
|
||||||
- You operate in a sandbox where `CODEX_SANDBOX_NETWORK_DISABLED=1` will be set whenever you use the `shell` tool. Any existing code that uses `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` was authored with this fact in mind. It is often used to early exit out of tests that the author knew you would not be able to run given your sandbox limitations.
|
- You operate in a sandbox where `CODEX_SANDBOX_NETWORK_DISABLED=1` will be set whenever you use the `shell` tool. Any existing code that uses `CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR` was authored with this fact in mind. It is often used to early exit out of tests that the author knew you would not be able to run given your sandbox limitations.
|
||||||
- Similarly, when you spawn a process using Seatbelt (`/usr/bin/sandbox-exec`), `CODEX_SANDBOX=seatbelt` will be set on the child process. Integration tests that want to run Seatbelt themselves cannot be run under Seatbelt, so checks for `CODEX_SANDBOX=seatbelt` are also often used to early exit out of tests, as appropriate.
|
- Similarly, when you spawn a process using Seatbelt (`/usr/bin/sandbox-exec`), `CODEX_SANDBOX=seatbelt` will be set on the child process. Integration tests that want to run Seatbelt themselves cannot be run under Seatbelt, so checks for `CODEX_SANDBOX=seatbelt` are also often used to early exit out of tests, as appropriate.
|
||||||
|
|||||||
@@ -65,6 +65,7 @@ use crate::models::ResponseItem;
|
|||||||
use crate::models::ShellToolCallParams;
|
use crate::models::ShellToolCallParams;
|
||||||
use crate::openai_tools::ToolsConfig;
|
use crate::openai_tools::ToolsConfig;
|
||||||
use crate::openai_tools::get_openai_tools;
|
use crate::openai_tools::get_openai_tools;
|
||||||
|
use crate::parse_command::parse_command;
|
||||||
use crate::plan_tool::handle_update_plan;
|
use crate::plan_tool::handle_update_plan;
|
||||||
use crate::project_doc::get_user_instructions;
|
use crate::project_doc::get_user_instructions;
|
||||||
use crate::protocol::AgentMessageDeltaEvent;
|
use crate::protocol::AgentMessageDeltaEvent;
|
||||||
@@ -402,6 +403,7 @@ impl Session {
|
|||||||
call_id,
|
call_id,
|
||||||
command: command_for_display.clone(),
|
command: command_for_display.clone(),
|
||||||
cwd,
|
cwd,
|
||||||
|
parsed_cmd: parse_command(&command_for_display),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
let event = Event {
|
let event = Event {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ mod mcp_connection_manager;
|
|||||||
mod mcp_tool_call;
|
mod mcp_tool_call;
|
||||||
mod message_history;
|
mod message_history;
|
||||||
mod model_provider_info;
|
mod model_provider_info;
|
||||||
|
pub mod parse_command;
|
||||||
pub use model_provider_info::BUILT_IN_OSS_MODEL_PROVIDER_ID;
|
pub use model_provider_info::BUILT_IN_OSS_MODEL_PROVIDER_ID;
|
||||||
pub use model_provider_info::ModelProviderInfo;
|
pub use model_provider_info::ModelProviderInfo;
|
||||||
pub use model_provider_info::WireApi;
|
pub use model_provider_info::WireApi;
|
||||||
|
|||||||
2045
codex-rs/core/src/parse_command.rs
Normal file
2045
codex-rs/core/src/parse_command.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -21,6 +21,7 @@ use crate::config_types::ReasoningEffort as ReasoningEffortConfig;
|
|||||||
use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||||
use crate::message_history::HistoryEntry;
|
use crate::message_history::HistoryEntry;
|
||||||
use crate::model_provider_info::ModelProviderInfo;
|
use crate::model_provider_info::ModelProviderInfo;
|
||||||
|
use crate::parse_command::ParsedCommand;
|
||||||
use crate::plan_tool::UpdatePlanArgs;
|
use crate::plan_tool::UpdatePlanArgs;
|
||||||
|
|
||||||
/// Submission Queue Entry - requests from user
|
/// Submission Queue Entry - requests from user
|
||||||
@@ -579,6 +580,7 @@ pub struct ExecCommandBeginEvent {
|
|||||||
pub command: Vec<String>,
|
pub command: Vec<String>,
|
||||||
/// The command's working directory if not the default cwd for the agent.
|
/// The command's working directory if not the default cwd for the agent.
|
||||||
pub cwd: PathBuf,
|
pub cwd: PathBuf,
|
||||||
|
pub parsed_cmd: Vec<ParsedCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||||
|
|||||||
@@ -255,6 +255,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
|||||||
call_id,
|
call_id,
|
||||||
command,
|
command,
|
||||||
cwd,
|
cwd,
|
||||||
|
parsed_cmd: _,
|
||||||
}) => {
|
}) => {
|
||||||
self.call_id_to_command.insert(
|
self.call_id_to_command.insert(
|
||||||
call_id.clone(),
|
call_id.clone(),
|
||||||
|
|||||||
@@ -936,6 +936,7 @@ mod tests {
|
|||||||
call_id: "c1".into(),
|
call_id: "c1".into(),
|
||||||
command: vec!["bash".into(), "-lc".into(), "echo hi".into()],
|
command: vec!["bash".into(), "-lc".into(), "echo hi".into()],
|
||||||
cwd: std::path::PathBuf::from("/work"),
|
cwd: std::path::PathBuf::from("/work"),
|
||||||
|
parsed_cmd: vec![],
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -947,7 +948,8 @@ mod tests {
|
|||||||
"type": "exec_command_begin",
|
"type": "exec_command_begin",
|
||||||
"call_id": "c1",
|
"call_id": "c1",
|
||||||
"command": ["bash", "-lc", "echo hi"],
|
"command": ["bash", "-lc", "echo hi"],
|
||||||
"cwd": "/work"
|
"cwd": "/work",
|
||||||
|
"parsed_cmd": []
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ use std::sync::Arc;
|
|||||||
use codex_core::codex_wrapper::CodexConversation;
|
use codex_core::codex_wrapper::CodexConversation;
|
||||||
use codex_core::codex_wrapper::init_codex;
|
use codex_core::codex_wrapper::init_codex;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
|
use codex_core::parse_command::ParsedCommand;
|
||||||
use codex_core::protocol::AgentMessageDeltaEvent;
|
use codex_core::protocol::AgentMessageDeltaEvent;
|
||||||
use codex_core::protocol::AgentMessageEvent;
|
use codex_core::protocol::AgentMessageEvent;
|
||||||
use codex_core::protocol::AgentReasoningDeltaEvent;
|
use codex_core::protocol::AgentReasoningDeltaEvent;
|
||||||
@@ -57,6 +58,7 @@ struct RunningCommand {
|
|||||||
command: Vec<String>,
|
command: Vec<String>,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
cwd: PathBuf,
|
cwd: PathBuf,
|
||||||
|
parsed_cmd: Vec<ParsedCommand>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct ChatWidget<'a> {
|
pub(crate) struct ChatWidget<'a> {
|
||||||
@@ -452,6 +454,7 @@ impl ChatWidget<'_> {
|
|||||||
call_id,
|
call_id,
|
||||||
command,
|
command,
|
||||||
cwd,
|
cwd,
|
||||||
|
parsed_cmd,
|
||||||
}) => {
|
}) => {
|
||||||
self.finalize_active_stream();
|
self.finalize_active_stream();
|
||||||
// Ensure the status indicator is visible while the command runs.
|
// Ensure the status indicator is visible while the command runs.
|
||||||
@@ -462,9 +465,11 @@ impl ChatWidget<'_> {
|
|||||||
RunningCommand {
|
RunningCommand {
|
||||||
command: command.clone(),
|
command: command.clone(),
|
||||||
cwd: cwd.clone(),
|
cwd: cwd.clone(),
|
||||||
|
parsed_cmd: parsed_cmd.clone(),
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
self.active_history_cell = Some(HistoryCell::new_active_exec_command(command));
|
self.active_history_cell =
|
||||||
|
Some(HistoryCell::new_active_exec_command(command, parsed_cmd));
|
||||||
}
|
}
|
||||||
EventMsg::ExecCommandOutputDelta(_) => {
|
EventMsg::ExecCommandOutputDelta(_) => {
|
||||||
// TODO
|
// TODO
|
||||||
@@ -494,14 +499,19 @@ impl ChatWidget<'_> {
|
|||||||
// Compute summary before moving stdout into the history cell.
|
// Compute summary before moving stdout into the history cell.
|
||||||
let cmd = self.running_commands.remove(&call_id);
|
let cmd = self.running_commands.remove(&call_id);
|
||||||
self.active_history_cell = None;
|
self.active_history_cell = None;
|
||||||
|
let (command, parsed_cmd) = match cmd {
|
||||||
|
Some(cmd) => (cmd.command, cmd.parsed_cmd),
|
||||||
|
None => (vec![call_id], vec![]),
|
||||||
|
};
|
||||||
self.add_to_history(HistoryCell::new_completed_exec_command(
|
self.add_to_history(HistoryCell::new_completed_exec_command(
|
||||||
cmd.map(|cmd| cmd.command).unwrap_or_else(|| vec![call_id]),
|
command,
|
||||||
|
parsed_cmd,
|
||||||
CommandOutput {
|
CommandOutput {
|
||||||
exit_code,
|
exit_code,
|
||||||
stdout,
|
stdout,
|
||||||
stderr,
|
stderr,
|
||||||
},
|
},
|
||||||
));
|
))
|
||||||
}
|
}
|
||||||
EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
|
EventMsg::McpToolCallBegin(McpToolCallBeginEvent {
|
||||||
call_id: _,
|
call_id: _,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::colors::LIGHT_BLUE;
|
||||||
use crate::exec_command::relativize_to_home;
|
use crate::exec_command::relativize_to_home;
|
||||||
use crate::exec_command::strip_bash_lc_and_escape;
|
use crate::exec_command::strip_bash_lc_and_escape;
|
||||||
use crate::slash_command::SlashCommand;
|
use crate::slash_command::SlashCommand;
|
||||||
@@ -8,6 +9,7 @@ use codex_ansi_escape::ansi_escape_line;
|
|||||||
use codex_common::create_config_summary_entries;
|
use codex_common::create_config_summary_entries;
|
||||||
use codex_common::elapsed::format_duration;
|
use codex_common::elapsed::format_duration;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
|
use codex_core::parse_command::ParsedCommand;
|
||||||
use codex_core::plan_tool::PlanItemArg;
|
use codex_core::plan_tool::PlanItemArg;
|
||||||
use codex_core::plan_tool::StepStatus;
|
use codex_core::plan_tool::StepStatus;
|
||||||
use codex_core::plan_tool::UpdatePlanArgs;
|
use codex_core::plan_tool::UpdatePlanArgs;
|
||||||
@@ -160,6 +162,7 @@ impl HistoryCell {
|
|||||||
/// Return a cloned, plain representation of the cell's lines suitable for
|
/// Return a cloned, plain representation of the cell's lines suitable for
|
||||||
/// one‑shot insertion into the terminal scrollback. Image cells are
|
/// one‑shot insertion into the terminal scrollback. Image cells are
|
||||||
/// represented with a simple placeholder for now.
|
/// represented with a simple placeholder for now.
|
||||||
|
/// These lines are also rendered directly by ratatui wrapped in a Paragraph.
|
||||||
pub(crate) fn plain_lines(&self) -> Vec<Line<'static>> {
|
pub(crate) fn plain_lines(&self) -> Vec<Line<'static>> {
|
||||||
match self {
|
match self {
|
||||||
HistoryCell::WelcomeMessage { view }
|
HistoryCell::WelcomeMessage { view }
|
||||||
@@ -194,48 +197,6 @@ impl HistoryCell {
|
|||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn output_lines(src: &str) -> Vec<Line<'static>> {
|
|
||||||
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 i == 0 { " ⎿ " } 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(Line::from(format!("… +{omitted} lines")));
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
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 new_session_info(
|
pub(crate) fn new_session_info(
|
||||||
config: &Config,
|
config: &Config,
|
||||||
event: SessionConfiguredEvent,
|
event: SessionConfiguredEvent,
|
||||||
@@ -303,59 +264,99 @@ impl HistoryCell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new_active_exec_command(command: Vec<String>) -> Self {
|
pub(crate) fn new_active_exec_command(
|
||||||
let command_escaped = strip_bash_lc_and_escape(&command);
|
command: Vec<String>,
|
||||||
|
parsed: Vec<ParsedCommand>,
|
||||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
) -> Self {
|
||||||
let mut iter = command_escaped.lines();
|
let lines = HistoryCell::exec_command_lines(&command, &parsed, None);
|
||||||
if let Some(first) = iter.next() {
|
|
||||||
lines.push(Line::from(vec![
|
|
||||||
"▌ ".cyan(),
|
|
||||||
"Running command ".magenta(),
|
|
||||||
first.to_string().into(),
|
|
||||||
]));
|
|
||||||
} else {
|
|
||||||
lines.push(Line::from(vec!["▌ ".cyan(), "Running command".magenta()]));
|
|
||||||
}
|
|
||||||
for cont in iter {
|
|
||||||
lines.push(Line::from(cont.to_string()));
|
|
||||||
}
|
|
||||||
lines.push(Line::from(""));
|
|
||||||
|
|
||||||
HistoryCell::ActiveExecCommand {
|
HistoryCell::ActiveExecCommand {
|
||||||
view: TextBlock::new(lines),
|
view: TextBlock::new(lines),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new_completed_exec_command(command: Vec<String>, output: CommandOutput) -> Self {
|
pub(crate) fn new_completed_exec_command(
|
||||||
let CommandOutput {
|
command: Vec<String>,
|
||||||
exit_code,
|
parsed: Vec<ParsedCommand>,
|
||||||
stdout,
|
output: CommandOutput,
|
||||||
stderr,
|
) -> Self {
|
||||||
} = output;
|
let lines = HistoryCell::exec_command_lines(&command, &parsed, Some(&output));
|
||||||
|
HistoryCell::CompletedExecCommand {
|
||||||
|
view: TextBlock::new(lines),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn exec_command_lines(
|
||||||
|
command: &[String],
|
||||||
|
parsed: &[ParsedCommand],
|
||||||
|
output: Option<&CommandOutput>,
|
||||||
|
) -> Vec<Line<'static>> {
|
||||||
|
if parsed.is_empty() {
|
||||||
|
HistoryCell::new_exec_command_generic(command, output)
|
||||||
|
} else {
|
||||||
|
HistoryCell::new_parsed_command(parsed, output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_parsed_command(
|
||||||
|
parsed_commands: &[ParsedCommand],
|
||||||
|
output: Option<&CommandOutput>,
|
||||||
|
) -> Vec<Line<'static>> {
|
||||||
|
let mut lines: Vec<Line> = vec![Line::from("⚙︎ Working")];
|
||||||
|
|
||||||
|
for (i, parsed) in parsed_commands.iter().enumerate() {
|
||||||
|
let str = match parsed {
|
||||||
|
ParsedCommand::Read { name, .. } => format!("📖 {name}"),
|
||||||
|
ParsedCommand::ListFiles { cmd, path } => match path {
|
||||||
|
Some(p) => format!("📂 {p}"),
|
||||||
|
None => format!("📂 {}", shlex_join_safe(cmd)),
|
||||||
|
},
|
||||||
|
ParsedCommand::Search { query, path, cmd } => match (query, path) {
|
||||||
|
(Some(q), Some(p)) => format!("🔎 {q} in {p}"),
|
||||||
|
(Some(q), None) => format!("🔎 {q}"),
|
||||||
|
(None, Some(p)) => format!("🔎 {p}"),
|
||||||
|
(None, None) => format!("🔎 {}", shlex_join_safe(cmd)),
|
||||||
|
},
|
||||||
|
ParsedCommand::Format { .. } => "✨ Formatting".to_string(),
|
||||||
|
ParsedCommand::Test { cmd } => format!("🧪 {}", shlex_join_safe(cmd)),
|
||||||
|
ParsedCommand::Lint { cmd, .. } => format!("🧹 {}", shlex_join_safe(cmd)),
|
||||||
|
ParsedCommand::Unknown { cmd } => format!("⌨️ {}", shlex_join_safe(cmd)),
|
||||||
|
};
|
||||||
|
|
||||||
|
let prefix = if i == 0 { " L " } else { " " };
|
||||||
|
lines.push(Line::from(vec![
|
||||||
|
Span::styled(prefix, Style::default().add_modifier(Modifier::DIM)),
|
||||||
|
Span::styled(str, Style::default().fg(LIGHT_BLUE)),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.extend(output_lines(output, true, false));
|
||||||
|
lines.push(Line::from(""));
|
||||||
|
|
||||||
|
lines
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_exec_command_generic(
|
||||||
|
command: &[String],
|
||||||
|
output: Option<&CommandOutput>,
|
||||||
|
) -> Vec<Line<'static>> {
|
||||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||||
let command_escaped = strip_bash_lc_and_escape(&command);
|
let command_escaped = strip_bash_lc_and_escape(command);
|
||||||
let mut cmd_lines = command_escaped.lines();
|
let mut cmd_lines = command_escaped.lines();
|
||||||
if let Some(first) = cmd_lines.next() {
|
if let Some(first) = cmd_lines.next() {
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
"⚡ Ran command ".magenta(),
|
"⚡ Running ".to_string().magenta(),
|
||||||
first.to_string().into(),
|
first.to_string().into(),
|
||||||
]));
|
]));
|
||||||
} else {
|
} else {
|
||||||
lines.push(Line::from("⚡ Ran command".magenta()));
|
lines.push(Line::from("⚡ Running".to_string().magenta()));
|
||||||
}
|
}
|
||||||
for cont in cmd_lines {
|
for cont in cmd_lines {
|
||||||
lines.push(Line::from(cont.to_string()));
|
lines.push(Line::from(cont.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let src = if exit_code == 0 { stdout } else { stderr };
|
lines.extend(output_lines(output, false, true));
|
||||||
lines.extend(Self::output_lines(&src));
|
|
||||||
lines.push(Line::from(""));
|
|
||||||
|
|
||||||
HistoryCell::CompletedExecCommand {
|
lines
|
||||||
view: TextBlock::new(lines),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new_active_mcp_tool_call(invocation: McpInvocation) -> Self {
|
pub(crate) fn new_active_mcp_tool_call(invocation: McpInvocation) -> Self {
|
||||||
@@ -835,7 +836,15 @@ impl HistoryCell {
|
|||||||
lines.push(Line::from("✘ Failed to apply patch".magenta().bold()));
|
lines.push(Line::from("✘ Failed to apply patch".magenta().bold()));
|
||||||
|
|
||||||
if !stderr.trim().is_empty() {
|
if !stderr.trim().is_empty() {
|
||||||
lines.extend(Self::output_lines(&stderr));
|
lines.extend(output_lines(
|
||||||
|
Some(&CommandOutput {
|
||||||
|
exit_code: 1,
|
||||||
|
stdout: String::new(),
|
||||||
|
stderr,
|
||||||
|
}),
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
lines.push(Line::from(""));
|
lines.push(Line::from(""));
|
||||||
@@ -854,6 +863,67 @@ impl WidgetRef for &HistoryCell {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn output_lines(
|
||||||
|
output: Option<&CommandOutput>,
|
||||||
|
only_err: bool,
|
||||||
|
include_angle_pipe: bool,
|
||||||
|
) -> Vec<Line<'static>> {
|
||||||
|
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 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(Line::from(format!("… +{omitted} lines")));
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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 create_diff_summary(title: &str, changes: HashMap<PathBuf, FileChange>) -> Vec<RtLine<'static>> {
|
fn create_diff_summary(title: &str, changes: HashMap<PathBuf, FileChange>) -> Vec<RtLine<'static>> {
|
||||||
let mut files: Vec<FileSummary> = Vec::new();
|
let mut files: Vec<FileSummary> = Vec::new();
|
||||||
|
|
||||||
@@ -1008,3 +1078,10 @@ fn format_mcp_invocation<'a>(invocation: McpInvocation) -> Line<'a> {
|
|||||||
];
|
];
|
||||||
Line::from(invocation_spans)
|
Line::from(invocation_spans)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn shlex_join_safe(command: &[String]) -> String {
|
||||||
|
match shlex::try_join(command.iter().map(|s| s.as_str())) {
|
||||||
|
Ok(cmd) => cmd,
|
||||||
|
Err(_) => command.join(" "),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ impl UserApprovalWidget<'_> {
|
|||||||
match decision {
|
match decision {
|
||||||
ReviewDecision::Approved => {
|
ReviewDecision::Approved => {
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
"✓ ".fg(Color::Green),
|
"✔ ".fg(Color::Green),
|
||||||
"You ".into(),
|
"You ".into(),
|
||||||
"approved".bold(),
|
"approved".bold(),
|
||||||
" codex to run ".into(),
|
" codex to run ".into(),
|
||||||
@@ -258,7 +258,7 @@ impl UserApprovalWidget<'_> {
|
|||||||
}
|
}
|
||||||
ReviewDecision::ApprovedForSession => {
|
ReviewDecision::ApprovedForSession => {
|
||||||
lines.push(Line::from(vec![
|
lines.push(Line::from(vec![
|
||||||
"✓ ".fg(Color::Green),
|
"✔ ".fg(Color::Green),
|
||||||
"You ".into(),
|
"You ".into(),
|
||||||
"approved".bold(),
|
"approved".bold(),
|
||||||
" codex to run ".into(),
|
" codex to run ".into(),
|
||||||
|
|||||||
Reference in New Issue
Block a user