mirror of
https://github.com/openai/codex.git
synced 2026-02-02 06:57:03 +00:00
Compare commits
1 Commits
dev/david.
...
gpeal/pars
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3be407fcbe |
53
codex-rs/cli/examples/fibonacci.rs
Normal file
53
codex-rs/cli/examples/fibonacci.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
//! Print the Fibonacci sequence.
|
||||
//!
|
||||
//! Usage:
|
||||
//! cargo run -p codex-cli --example fibonacci -- [COUNT]
|
||||
//!
|
||||
//! If COUNT is omitted, the first 10 numbers are printed.
|
||||
|
||||
use std::env;
|
||||
use std::process;
|
||||
|
||||
fn fibonacci(count: usize) -> Vec<u128> {
|
||||
let mut seq = Vec::with_capacity(count);
|
||||
if count == 0 {
|
||||
return seq;
|
||||
}
|
||||
// Start with 0, 1
|
||||
let mut a: u128 = 0;
|
||||
let mut b: u128 = 1;
|
||||
for _ in 0..count {
|
||||
seq.push(a);
|
||||
let next = a.saturating_add(b);
|
||||
a = b;
|
||||
b = next;
|
||||
}
|
||||
seq
|
||||
}
|
||||
|
||||
fn parse_count_arg() -> Result<usize, String> {
|
||||
let mut args = env::args().skip(1);
|
||||
match args.next() {
|
||||
None => Ok(10), // default
|
||||
Some(s) => s
|
||||
.parse::<usize>()
|
||||
.map_err(|_| format!("Invalid COUNT: '{}' (expected a non-negative integer)", s)),
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let count = match parse_count_arg() {
|
||||
Ok(n) => n,
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"{}\nUsage: cargo run -p codex-cli --example fibonacci -- [COUNT]",
|
||||
e
|
||||
);
|
||||
process::exit(2);
|
||||
}
|
||||
};
|
||||
|
||||
for n in fibonacci(count) {
|
||||
println!("{}", n);
|
||||
}
|
||||
}
|
||||
@@ -65,6 +65,7 @@ use crate::models::ResponseItem;
|
||||
use crate::models::ShellToolCallParams;
|
||||
use crate::openai_tools::ToolsConfig;
|
||||
use crate::openai_tools::get_openai_tools;
|
||||
use crate::parse_command::parse_command;
|
||||
use crate::plan_tool::handle_update_plan;
|
||||
use crate::project_doc::get_user_instructions;
|
||||
use crate::protocol::AgentMessageDeltaEvent;
|
||||
@@ -373,7 +374,7 @@ impl Session {
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_exec_command_begin(
|
||||
pub async fn on_exec_command_begin(
|
||||
&self,
|
||||
turn_diff_tracker: &mut TurnDiffTracker,
|
||||
exec_command_context: ExecCommandContext,
|
||||
@@ -402,6 +403,7 @@ impl Session {
|
||||
call_id,
|
||||
command: command_for_display.clone(),
|
||||
cwd,
|
||||
parsed_cmd: parse_command(&command_for_display),
|
||||
}),
|
||||
};
|
||||
let event = Event {
|
||||
|
||||
@@ -28,6 +28,7 @@ mod mcp_connection_manager;
|
||||
mod mcp_tool_call;
|
||||
mod message_history;
|
||||
mod model_provider_info;
|
||||
pub mod parse_command;
|
||||
pub use model_provider_info::BUILT_IN_OSS_MODEL_PROVIDER_ID;
|
||||
pub use model_provider_info::ModelProviderInfo;
|
||||
pub use model_provider_info::WireApi;
|
||||
|
||||
648
codex-rs/core/src/parse_command.rs
Normal file
648
codex-rs/core/src/parse_command.rs
Normal file
@@ -0,0 +1,648 @@
|
||||
use crate::bash::try_parse_bash;
|
||||
use crate::bash::try_parse_word_only_commands_sequence;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use shlex::split as shlex_split;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
|
||||
pub enum ParsedCommand {
|
||||
Read {
|
||||
cmd: Vec<String>,
|
||||
name: String,
|
||||
},
|
||||
Python {
|
||||
cmd: Vec<String>,
|
||||
},
|
||||
GitStatus {
|
||||
cmd: Vec<String>,
|
||||
},
|
||||
GitLog {
|
||||
cmd: Vec<String>,
|
||||
},
|
||||
GitDiff {
|
||||
cmd: Vec<String>,
|
||||
},
|
||||
Ls {
|
||||
cmd: Vec<String>,
|
||||
path: Option<String>,
|
||||
},
|
||||
Rg {
|
||||
cmd: Vec<String>,
|
||||
query: Option<String>,
|
||||
path: Option<String>,
|
||||
files_only: bool,
|
||||
},
|
||||
Shell {
|
||||
cmd: Vec<String>,
|
||||
display: String,
|
||||
},
|
||||
Pnpm {
|
||||
cmd: Vec<String>,
|
||||
pnpm_cmd: String,
|
||||
},
|
||||
Unknown {
|
||||
cmd: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
pub fn parse_command(command: &[String]) -> Vec<ParsedCommand> {
|
||||
let main_cmd = extract_main_cmd_tokens(command);
|
||||
|
||||
// 1) Try the "bash -lc <script>" path: leverage the existing parser so we
|
||||
// can get each sub-command (words-only) precisely.
|
||||
if let [bash, flag, script] = command {
|
||||
if bash == "bash" && flag == "-lc" {
|
||||
if let Some(tree) = try_parse_bash(script) {
|
||||
if let Some(all_commands) = try_parse_word_only_commands_sequence(&tree, script) {
|
||||
if !all_commands.is_empty() {
|
||||
// Tokenize the entire script once; used to preserve full context for certain summaries.
|
||||
let script_tokens = shlex_split(script).unwrap_or_else(|| {
|
||||
vec!["bash".to_string(), flag.clone(), script.clone()]
|
||||
});
|
||||
let commands: Vec<ParsedCommand> = all_commands
|
||||
.into_iter()
|
||||
.map(|tokens| {
|
||||
match summarize_main_tokens(&tokens) {
|
||||
// For ls within a bash -lc script, preserve the full script tokens for display.
|
||||
ParsedCommand::Ls { path, .. } => ParsedCommand::Ls {
|
||||
cmd: script_tokens.clone(),
|
||||
path,
|
||||
},
|
||||
other => other,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we couldn't parse with the bash parser, conservatively treat the
|
||||
// whole thing as one opaque shell command and mark unsafe.
|
||||
let display = script.clone();
|
||||
let commands = vec![ParsedCommand::Shell {
|
||||
cmd: main_cmd.clone(),
|
||||
display,
|
||||
}];
|
||||
return commands;
|
||||
}
|
||||
}
|
||||
|
||||
// 2) Not a "bash -lc" form. If there are connectors, split locally.
|
||||
let has_connectors = main_cmd
|
||||
.iter()
|
||||
.any(|t| t == "&&" || t == "||" || t == "|" || t == ";");
|
||||
|
||||
let split_subcommands = |tokens: &[String]| -> Vec<Vec<String>> {
|
||||
let mut out: Vec<Vec<String>> = Vec::new();
|
||||
let mut cur: Vec<String> = Vec::new();
|
||||
for t in tokens {
|
||||
if t == "&&" || t == "||" || t == "|" || t == ";" {
|
||||
if !cur.is_empty() {
|
||||
out.push(std::mem::take(&mut cur));
|
||||
}
|
||||
} else {
|
||||
cur.push(t.clone());
|
||||
}
|
||||
}
|
||||
if !cur.is_empty() {
|
||||
out.push(cur);
|
||||
}
|
||||
out
|
||||
};
|
||||
|
||||
let commands_tokens: Vec<Vec<String>> = if has_connectors {
|
||||
split_subcommands(&main_cmd)
|
||||
} else {
|
||||
vec![main_cmd.clone()]
|
||||
};
|
||||
|
||||
// 3) Summarize each sub-command.
|
||||
let commands: Vec<ParsedCommand> = commands_tokens
|
||||
.into_iter()
|
||||
.map(|tokens| summarize_main_tokens(&tokens))
|
||||
.collect();
|
||||
|
||||
commands
|
||||
}
|
||||
|
||||
/// Returns true if `arg` matches /^(\d+,)?\d+p$/
|
||||
fn is_valid_sed_n_arg(arg: Option<&str>) -> bool {
|
||||
let s = match arg {
|
||||
Some(s) => s,
|
||||
None => return false,
|
||||
};
|
||||
let core = match s.strip_suffix('p') {
|
||||
Some(rest) => rest,
|
||||
None => return false,
|
||||
};
|
||||
let parts: Vec<&str> = core.split(',').collect();
|
||||
match parts.as_slice() {
|
||||
[num] => !num.is_empty() && num.chars().all(|c| c.is_ascii_digit()),
|
||||
[a, b] => {
|
||||
!a.is_empty()
|
||||
&& !b.is_empty()
|
||||
&& a.chars().all(|c| c.is_ascii_digit())
|
||||
&& b.chars().all(|c| c.is_ascii_digit())
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_main_cmd_tokens(cmd: &[String]) -> Vec<String> {
|
||||
match cmd {
|
||||
[first, pipe, rest @ ..] if (first == "yes" || first == "y") && pipe == "|" => {
|
||||
let s = rest.join(" ");
|
||||
shlex_split(&s).unwrap_or_else(|| rest.to_vec())
|
||||
}
|
||||
[first, pipe, rest @ ..] if (first == "no" || first == "n") && pipe == "|" => {
|
||||
let s = rest.join(" ");
|
||||
shlex_split(&s).unwrap_or_else(|| rest.to_vec())
|
||||
}
|
||||
[bash, flag, script] if bash == "bash" && (flag == "-c" || flag == "-lc") => {
|
||||
shlex_split(script)
|
||||
.unwrap_or_else(|| vec!["bash".to_string(), flag.clone(), script.clone()])
|
||||
}
|
||||
_ => cmd.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
fn summarize_main_tokens(main_cmd: &[String]) -> ParsedCommand {
|
||||
let cut_at_connector = |tokens: &[String]| -> Vec<String> {
|
||||
let idx = tokens
|
||||
.iter()
|
||||
.position(|t| t == "|" || t == "&&" || t == "||")
|
||||
.unwrap_or(tokens.len());
|
||||
tokens[..idx].to_vec()
|
||||
};
|
||||
|
||||
let truncate_file_path_for_display = |path: &str| -> String {
|
||||
let mut parts = path.split('/').rev().filter(|p| {
|
||||
!p.is_empty() && *p != "build" && *p != "dist" && *p != "node_modules" && *p != "src"
|
||||
});
|
||||
parts
|
||||
.next()
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_else(|| path.to_string())
|
||||
};
|
||||
|
||||
match main_cmd.split_first() {
|
||||
Some((head, tail)) if head == "ls" => {
|
||||
let path = tail
|
||||
.iter()
|
||||
.find(|p| !p.starts_with('-'))
|
||||
.map(|p| truncate_file_path_for_display(p));
|
||||
ParsedCommand::Ls {
|
||||
cmd: main_cmd.to_vec(),
|
||||
path,
|
||||
}
|
||||
}
|
||||
Some((head, tail)) if head == "rg" => {
|
||||
let args_no_connector = cut_at_connector(tail);
|
||||
let files_only = args_no_connector.iter().any(|a| a == "--files");
|
||||
let non_flags: Vec<&String> = args_no_connector
|
||||
.iter()
|
||||
.filter(|p| !p.starts_with('-'))
|
||||
.collect();
|
||||
let (query, path) = if files_only {
|
||||
let p = non_flags.first().map(|s| truncate_file_path_for_display(s));
|
||||
(None, p)
|
||||
} else {
|
||||
let q = non_flags.first().map(|s| truncate_file_path_for_display(s));
|
||||
let p = non_flags.get(1).map(|s| truncate_file_path_for_display(s));
|
||||
(q, p)
|
||||
};
|
||||
ParsedCommand::Rg {
|
||||
cmd: main_cmd.to_vec(),
|
||||
query,
|
||||
path,
|
||||
files_only,
|
||||
}
|
||||
}
|
||||
Some((head, tail)) if head == "grep" => {
|
||||
let args_no_connector = cut_at_connector(tail);
|
||||
let non_flags: Vec<&String> = args_no_connector
|
||||
.iter()
|
||||
.filter(|p| !p.starts_with('-'))
|
||||
.collect();
|
||||
let query = non_flags.first().map(|s| truncate_file_path_for_display(s));
|
||||
let path = non_flags.get(1).map(|s| truncate_file_path_for_display(s));
|
||||
ParsedCommand::Rg {
|
||||
cmd: main_cmd.to_vec(),
|
||||
query,
|
||||
path,
|
||||
files_only: false,
|
||||
}
|
||||
}
|
||||
Some((head, tail)) if head == "cat" && tail.len() == 1 => {
|
||||
let name = truncate_file_path_for_display(&tail[0]);
|
||||
ParsedCommand::Read {
|
||||
cmd: main_cmd.to_vec(),
|
||||
name,
|
||||
}
|
||||
}
|
||||
Some((head, tail))
|
||||
if head == "head"
|
||||
&& tail.len() >= 3
|
||||
&& tail[0] == "-n"
|
||||
&& tail[1].chars().all(|c| c.is_ascii_digit()) =>
|
||||
{
|
||||
let name = truncate_file_path_for_display(&tail[2]);
|
||||
ParsedCommand::Read {
|
||||
cmd: main_cmd.to_vec(),
|
||||
name,
|
||||
}
|
||||
}
|
||||
Some((head, tail))
|
||||
if head == "tail" && tail.len() >= 3 && tail[0] == "-n" && {
|
||||
let n = &tail[1];
|
||||
let s = n.strip_prefix('+').unwrap_or(n);
|
||||
!s.is_empty() && s.chars().all(|c| c.is_ascii_digit())
|
||||
} =>
|
||||
{
|
||||
let name = truncate_file_path_for_display(&tail[2]);
|
||||
ParsedCommand::Read {
|
||||
cmd: main_cmd.to_vec(),
|
||||
name,
|
||||
}
|
||||
}
|
||||
Some((head, tail))
|
||||
if head == "sed"
|
||||
&& tail.len() >= 3
|
||||
&& tail[0] == "-n"
|
||||
&& is_valid_sed_n_arg(tail.get(1).map(|s| s.as_str())) =>
|
||||
{
|
||||
if let Some(path) = tail.get(2) {
|
||||
let name = truncate_file_path_for_display(path);
|
||||
ParsedCommand::Read {
|
||||
cmd: main_cmd.to_vec(),
|
||||
name,
|
||||
}
|
||||
} else {
|
||||
ParsedCommand::Unknown {
|
||||
cmd: main_cmd.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
Some((head, _tail)) if head == "python" => ParsedCommand::Python {
|
||||
cmd: main_cmd.to_vec(),
|
||||
},
|
||||
Some((first, rest)) if first == "git" => match rest.first().map(|s| s.as_str()) {
|
||||
Some("status") => ParsedCommand::GitStatus {
|
||||
cmd: main_cmd.to_vec(),
|
||||
},
|
||||
Some("log") => ParsedCommand::GitLog {
|
||||
cmd: main_cmd.to_vec(),
|
||||
},
|
||||
Some("diff") => ParsedCommand::GitDiff {
|
||||
cmd: main_cmd.to_vec(),
|
||||
},
|
||||
_ => ParsedCommand::Unknown {
|
||||
cmd: main_cmd.to_vec(),
|
||||
},
|
||||
},
|
||||
Some((tool, rest)) if (tool == "pnpm" || tool == "npm") => {
|
||||
let mut r = rest;
|
||||
let mut has_r = false;
|
||||
if let Some(flag) = r.first() {
|
||||
if flag == "-r" {
|
||||
has_r = true;
|
||||
r = &r[1..];
|
||||
}
|
||||
}
|
||||
if r.first().map(|s| s.as_str()) == Some("run") {
|
||||
let args = r[1..].to_vec();
|
||||
// For display, only include the script name before any "--" forwarded args.
|
||||
let script_name = args.first().cloned().unwrap_or_default();
|
||||
let pnpm_cmd = script_name;
|
||||
let mut full = vec![tool.clone()];
|
||||
if has_r {
|
||||
full.push("-r".to_string());
|
||||
}
|
||||
full.push("run".to_string());
|
||||
full.extend(args.clone());
|
||||
ParsedCommand::Pnpm {
|
||||
cmd: full,
|
||||
pnpm_cmd,
|
||||
}
|
||||
} else {
|
||||
ParsedCommand::Unknown {
|
||||
cmd: main_cmd.to_vec(),
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => ParsedCommand::Unknown {
|
||||
cmd: main_cmd.to_vec(),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used)]
|
||||
use super::*;
|
||||
|
||||
fn vec_str(args: &[&str]) -> Vec<String> {
|
||||
args.iter().map(|s| s.to_string()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn git_status_summary() {
|
||||
let out = parse_command(&vec_str(&["git", "status"]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![ParsedCommand::GitStatus {
|
||||
cmd: vec_str(&["git", "status"]),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn handles_complex_bash_command() {
|
||||
let inner =
|
||||
"rg --version && node -v && pnpm -v && rg --files | wc -l && rg --files | head -n 40";
|
||||
let out = parse_command(&vec_str(&["bash", "-lc", inner]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![
|
||||
ParsedCommand::Unknown {
|
||||
cmd: vec_str(&["head", "-n", "40"])
|
||||
},
|
||||
ParsedCommand::Rg {
|
||||
cmd: vec_str(&["rg", "--files"]),
|
||||
query: None,
|
||||
path: None,
|
||||
files_only: true,
|
||||
},
|
||||
ParsedCommand::Unknown {
|
||||
cmd: vec_str(&["wc", "-l"])
|
||||
},
|
||||
ParsedCommand::Rg {
|
||||
cmd: vec_str(&["rg", "--files"]),
|
||||
query: None,
|
||||
path: None,
|
||||
files_only: true,
|
||||
},
|
||||
ParsedCommand::Unknown {
|
||||
cmd: vec_str(&["pnpm", "-v"])
|
||||
},
|
||||
ParsedCommand::Unknown {
|
||||
cmd: vec_str(&["node", "-v"])
|
||||
},
|
||||
ParsedCommand::Rg {
|
||||
cmd: vec_str(&["rg", "--version"]),
|
||||
query: None,
|
||||
path: None,
|
||||
files_only: false,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_searching_for_navigate_to_route() {
|
||||
let inner = "rg -n \"navigate-to-route\" -S";
|
||||
let out = parse_command(&vec_str(&["bash", "-lc", inner]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![ParsedCommand::Rg {
|
||||
cmd: shlex_split(inner).unwrap(),
|
||||
query: Some("navigate-to-route".to_string()),
|
||||
path: None,
|
||||
files_only: false,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_rg_files_with_path_and_pipe() {
|
||||
let inner = "rg --files webview/src | sed -n";
|
||||
let out = parse_command(&vec_str(&["bash", "-lc", inner]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![
|
||||
ParsedCommand::Unknown {
|
||||
cmd: vec_str(&["sed", "-n"])
|
||||
},
|
||||
ParsedCommand::Rg {
|
||||
cmd: vec_str(&["rg", "--files", "webview/src"]),
|
||||
query: None,
|
||||
path: Some("webview".to_string()),
|
||||
files_only: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_rg_files_then_head() {
|
||||
let inner = "rg --files | head -n 50";
|
||||
let out = parse_command(&vec_str(&["bash", "-lc", inner]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![
|
||||
ParsedCommand::Unknown {
|
||||
cmd: vec_str(&["head", "-n", "50"])
|
||||
},
|
||||
ParsedCommand::Rg {
|
||||
cmd: vec_str(&["rg", "--files"]),
|
||||
query: None,
|
||||
path: None,
|
||||
files_only: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_cat() {
|
||||
let inner = "cat webview/README.md";
|
||||
let out = parse_command(&vec_str(&["bash", "-lc", inner]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![ParsedCommand::Read {
|
||||
cmd: shlex_split(inner).unwrap(),
|
||||
name: "README.md".to_string(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_ls_with_pipe() {
|
||||
let inner = "ls -la | sed -n '1,120p'";
|
||||
let out = parse_command(&vec_str(&["bash", "-lc", inner]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![
|
||||
ParsedCommand::Unknown {
|
||||
cmd: vec_str(&["sed", "-n", "1,120p"])
|
||||
},
|
||||
ParsedCommand::Ls {
|
||||
cmd: shlex_split(inner).unwrap(),
|
||||
path: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_head_n() {
|
||||
let inner = "head -n 50 Cargo.toml";
|
||||
let out = parse_command(&vec_str(&["bash", "-lc", inner]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![ParsedCommand::Read {
|
||||
cmd: shlex_split(inner).unwrap(),
|
||||
name: "Cargo.toml".to_string(),
|
||||
},]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_tail_n_plus() {
|
||||
let inner = "tail -n +522 README.md";
|
||||
let out = parse_command(&vec_str(&["bash", "-lc", inner]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![ParsedCommand::Read {
|
||||
cmd: shlex_split(inner).unwrap(),
|
||||
name: "README.md".to_string(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_tail_n_last_lines() {
|
||||
let inner = "tail -n 30 README.md";
|
||||
let out = parse_command(&vec_str(&["bash", "-lc", inner]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![ParsedCommand::Read {
|
||||
cmd: shlex_split(inner).unwrap(),
|
||||
name: "README.md".to_string(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_npm_run_build() {
|
||||
let out = parse_command(&vec_str(&["npm", "run", "build"]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![ParsedCommand::Pnpm {
|
||||
cmd: vec_str(&["npm", "run", "build"]),
|
||||
pnpm_cmd: "build".to_string(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_npm_run_with_forwarded_args() {
|
||||
let out = parse_command(&vec_str(&[
|
||||
"npm",
|
||||
"run",
|
||||
"lint",
|
||||
"--",
|
||||
"--max-warnings",
|
||||
"0",
|
||||
"--format",
|
||||
"json",
|
||||
]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![ParsedCommand::Pnpm {
|
||||
cmd: vec_str(&[
|
||||
"npm",
|
||||
"run",
|
||||
"lint",
|
||||
"--",
|
||||
"--max-warnings",
|
||||
"0",
|
||||
"--format",
|
||||
"json",
|
||||
]),
|
||||
pnpm_cmd: "lint".to_string(),
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_grep_recursive_current_dir() {
|
||||
let out = parse_command(&vec_str(&[
|
||||
"grep",
|
||||
"-R",
|
||||
"CODEX_SANDBOX_ENV_VAR",
|
||||
"-n",
|
||||
".",
|
||||
]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![ParsedCommand::Rg {
|
||||
cmd: vec_str(&["grep", "-R", "CODEX_SANDBOX_ENV_VAR", "-n", "."]),
|
||||
query: Some("CODEX_SANDBOX_ENV_VAR".to_string()),
|
||||
path: Some(".".to_string()),
|
||||
files_only: false,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_grep_recursive_specific_file() {
|
||||
let out = parse_command(&vec_str(&[
|
||||
"grep",
|
||||
"-R",
|
||||
"CODEX_SANDBOX_ENV_VAR",
|
||||
"-n",
|
||||
"core/src/spawn.rs",
|
||||
]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![ParsedCommand::Rg {
|
||||
cmd: vec_str(&[
|
||||
"grep",
|
||||
"-R",
|
||||
"CODEX_SANDBOX_ENV_VAR",
|
||||
"-n",
|
||||
"core/src/spawn.rs",
|
||||
]),
|
||||
query: Some("CODEX_SANDBOX_ENV_VAR".to_string()),
|
||||
path: Some("spawn.rs".to_string()),
|
||||
files_only: false,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_grep_weird_backtick_in_query() {
|
||||
let out = parse_command(&vec_str(&["grep", "-R", "COD`EX_SANDBOX", "-n"]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![ParsedCommand::Rg {
|
||||
cmd: vec_str(&["grep", "-R", "COD`EX_SANDBOX", "-n"]),
|
||||
query: Some("COD`EX_SANDBOX".to_string()),
|
||||
path: None,
|
||||
files_only: false,
|
||||
}]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn supports_cd_and_rg_files() {
|
||||
let out = parse_command(&vec_str(&["cd", "codex-rs", "&&", "rg", "--files"]));
|
||||
assert_eq!(
|
||||
out,
|
||||
vec![
|
||||
ParsedCommand::Unknown {
|
||||
cmd: vec_str(&["cd", "codex-rs"]),
|
||||
},
|
||||
ParsedCommand::Rg {
|
||||
cmd: vec_str(&["rg", "--files"]),
|
||||
query: None,
|
||||
path: None,
|
||||
files_only: true,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ use crate::config_types::ReasoningEffort as ReasoningEffortConfig;
|
||||
use crate::config_types::ReasoningSummary as ReasoningSummaryConfig;
|
||||
use crate::message_history::HistoryEntry;
|
||||
use crate::model_provider_info::ModelProviderInfo;
|
||||
use crate::parse_command::ParsedCommand;
|
||||
use crate::plan_tool::UpdatePlanArgs;
|
||||
|
||||
/// Submission Queue Entry - requests from user
|
||||
@@ -579,6 +580,7 @@ pub struct ExecCommandBeginEvent {
|
||||
pub command: Vec<String>,
|
||||
/// The command's working directory if not the default cwd for the agent.
|
||||
pub cwd: PathBuf,
|
||||
pub parsed_cmd: Vec<ParsedCommand>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize)]
|
||||
|
||||
@@ -255,6 +255,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
call_id,
|
||||
command,
|
||||
cwd,
|
||||
parsed_cmd: _,
|
||||
}) => {
|
||||
self.call_id_to_command.insert(
|
||||
call_id.clone(),
|
||||
|
||||
@@ -936,6 +936,7 @@ mod tests {
|
||||
call_id: "c1".into(),
|
||||
command: vec!["bash".into(), "-lc".into(), "echo hi".into()],
|
||||
cwd: std::path::PathBuf::from("/work"),
|
||||
parsed_cmd: vec![],
|
||||
}),
|
||||
};
|
||||
|
||||
@@ -947,7 +948,8 @@ mod tests {
|
||||
"type": "exec_command_begin",
|
||||
"call_id": "c1",
|
||||
"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::init_codex;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::parse_command::ParsedCommand;
|
||||
use codex_core::protocol::AgentMessageDeltaEvent;
|
||||
use codex_core::protocol::AgentMessageEvent;
|
||||
use codex_core::protocol::AgentReasoningDeltaEvent;
|
||||
@@ -57,6 +58,7 @@ struct RunningCommand {
|
||||
command: Vec<String>,
|
||||
#[allow(dead_code)]
|
||||
cwd: PathBuf,
|
||||
parsed_cmd: Vec<ParsedCommand>,
|
||||
}
|
||||
|
||||
pub(crate) struct ChatWidget<'a> {
|
||||
@@ -442,6 +444,7 @@ impl ChatWidget<'_> {
|
||||
call_id,
|
||||
command,
|
||||
cwd,
|
||||
parsed_cmd,
|
||||
}) => {
|
||||
self.finalize_active_stream();
|
||||
// Ensure the status indicator is visible while the command runs.
|
||||
@@ -452,6 +455,7 @@ impl ChatWidget<'_> {
|
||||
RunningCommand {
|
||||
command: command.clone(),
|
||||
cwd: cwd.clone(),
|
||||
parsed_cmd: parsed_cmd.clone(),
|
||||
},
|
||||
);
|
||||
self.active_history_cell = Some(HistoryCell::new_active_exec_command(command));
|
||||
@@ -482,10 +486,15 @@ impl ChatWidget<'_> {
|
||||
stderr,
|
||||
}) => {
|
||||
// Compute summary before moving stdout into the history cell.
|
||||
let cmd = self.running_commands.remove(&call_id);
|
||||
let removed = self.running_commands.remove(&call_id);
|
||||
let (command, parsed_cmd) = match removed {
|
||||
Some(rc) => (rc.command, rc.parsed_cmd),
|
||||
None => (vec![call_id.clone()], vec![]),
|
||||
};
|
||||
self.active_history_cell = None;
|
||||
self.add_to_history(HistoryCell::new_completed_exec_command(
|
||||
cmd.map(|cmd| cmd.command).unwrap_or_else(|| vec![call_id]),
|
||||
command,
|
||||
parsed_cmd,
|
||||
CommandOutput {
|
||||
exit_code,
|
||||
stdout,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::colors::LIGHT_BLUE;
|
||||
use crate::exec_command::relativize_to_home;
|
||||
use crate::exec_command::strip_bash_lc_and_escape;
|
||||
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::elapsed::format_duration;
|
||||
use codex_core::config::Config;
|
||||
use codex_core::parse_command::ParsedCommand;
|
||||
use codex_core::plan_tool::PlanItemArg;
|
||||
use codex_core::plan_tool::StepStatus;
|
||||
use codex_core::plan_tool::UpdatePlanArgs;
|
||||
@@ -32,6 +34,7 @@ use ratatui::widgets::Paragraph;
|
||||
use ratatui::widgets::WidgetRef;
|
||||
use ratatui::widgets::Wrap;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::HashSet;
|
||||
use std::io::Cursor;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
@@ -278,13 +281,121 @@ impl HistoryCell {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_completed_exec_command(command: Vec<String>, output: CommandOutput) -> Self {
|
||||
pub(crate) fn new_completed_exec_command(
|
||||
command: Vec<String>,
|
||||
parsed: Vec<ParsedCommand>,
|
||||
output: CommandOutput,
|
||||
) -> Self {
|
||||
let is_read_command = parsed
|
||||
.iter()
|
||||
.all(|c| matches!(c, ParsedCommand::Read { .. }));
|
||||
|
||||
let is_list_command = parsed.iter().all(|c| matches!(c, ParsedCommand::Ls { .. }));
|
||||
let is_search_command = parsed.iter().all(|c| matches!(c, ParsedCommand::Rg { .. }));
|
||||
|
||||
if is_read_command {
|
||||
return HistoryCell::new_read_command(parsed);
|
||||
} else if is_list_command {
|
||||
return HistoryCell::new_list_command(parsed);
|
||||
} else if is_search_command {
|
||||
return HistoryCell::new_search_command(parsed);
|
||||
}
|
||||
HistoryCell::new_completed_exec_command_generic(command, output)
|
||||
}
|
||||
|
||||
fn new_read_command(read_commands: Vec<ParsedCommand>) -> Self {
|
||||
let file_names: HashSet<&String> = read_commands
|
||||
.iter()
|
||||
.flat_map(|c| match c {
|
||||
ParsedCommand::Read { name, .. } => Some(name),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let count = file_names.len();
|
||||
let mut lines: Vec<Line> = vec![match count {
|
||||
0 => Line::from("📖 Reading files"),
|
||||
1 => Line::from("📖 Reading 1 file"),
|
||||
_ => Line::from(format!("📖 Reading {count} files")),
|
||||
}];
|
||||
|
||||
for name in file_names {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" L ", Style::default().fg(Color::Gray)),
|
||||
Span::styled(name.clone(), Style::default().fg(LIGHT_BLUE)),
|
||||
]));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
|
||||
HistoryCell::CompletedExecCommand {
|
||||
view: TextBlock::new(lines),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_list_command(list_commands: Vec<ParsedCommand>) -> Self {
|
||||
let paths: HashSet<&String> = list_commands
|
||||
.iter()
|
||||
.flat_map(|c| match c {
|
||||
ParsedCommand::Ls { path, .. } => path.as_ref(),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let count = paths.len();
|
||||
let mut lines: Vec<Line> = vec![match count {
|
||||
0 => Line::from("📖 Exploring files"),
|
||||
1 => Line::from("📖 Exploring 1 folder"),
|
||||
_ => Line::from(format!("📖 Exploring {count} folders")),
|
||||
}];
|
||||
|
||||
for name in paths {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" L ", Style::default().fg(Color::Gray)),
|
||||
Span::styled(name.clone(), Style::default().fg(LIGHT_BLUE)),
|
||||
]));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
|
||||
HistoryCell::CompletedExecCommand {
|
||||
view: TextBlock::new(lines),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_search_command(search_commands: Vec<ParsedCommand>) -> Self {
|
||||
let file_names: HashSet<&String> = search_commands
|
||||
.iter()
|
||||
.flat_map(|c| match c {
|
||||
ParsedCommand::Read { name, .. } => Some(name),
|
||||
_ => None,
|
||||
})
|
||||
.collect();
|
||||
|
||||
let count = file_names.len();
|
||||
let mut lines: Vec<Line> = vec![match count {
|
||||
0 => Line::from("🔎 Searching files"),
|
||||
1 => Line::from("🔎 Searching 1 file"),
|
||||
_ => Line::from(format!("🔎 Searching {count} files")),
|
||||
}];
|
||||
|
||||
for name in file_names {
|
||||
lines.push(Line::from(vec![
|
||||
Span::styled(" L ", Style::default().fg(Color::Gray)),
|
||||
Span::styled(name.clone(), Style::default().fg(LIGHT_BLUE)),
|
||||
]));
|
||||
}
|
||||
lines.push(Line::from(""));
|
||||
|
||||
HistoryCell::CompletedExecCommand {
|
||||
view: TextBlock::new(lines),
|
||||
}
|
||||
}
|
||||
|
||||
fn new_completed_exec_command_generic(command: Vec<String>, output: CommandOutput) -> Self {
|
||||
let CommandOutput {
|
||||
exit_code,
|
||||
stdout,
|
||||
stderr,
|
||||
} = output;
|
||||
|
||||
let mut lines: Vec<Line<'static>> = Vec::new();
|
||||
let command_escaped = strip_bash_lc_and_escape(&command);
|
||||
lines.push(Line::from(vec![
|
||||
|
||||
@@ -247,7 +247,7 @@ impl UserApprovalWidget<'_> {
|
||||
match decision {
|
||||
ReviewDecision::Approved => {
|
||||
lines.push(Line::from(vec![
|
||||
"✓ ".fg(Color::Green),
|
||||
"✔ ".fg(Color::Green),
|
||||
"You ".into(),
|
||||
"approved".bold(),
|
||||
" codex to run ".into(),
|
||||
@@ -258,7 +258,7 @@ impl UserApprovalWidget<'_> {
|
||||
}
|
||||
ReviewDecision::ApprovedForSession => {
|
||||
lines.push(Line::from(vec![
|
||||
"✓ ".fg(Color::Green),
|
||||
"✔ ".fg(Color::Green),
|
||||
"You ".into(),
|
||||
"approved".bold(),
|
||||
" codex to run ".into(),
|
||||
|
||||
Reference in New Issue
Block a user