Compare commits

...

1 Commits

Author SHA1 Message Date
Gabriel Peal
3be407fcbe Works 2025-08-07 13:39:55 -07:00
10 changed files with 837 additions and 8 deletions

View 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);
}
}

View File

@@ -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 {

View File

@@ -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;

View 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,
},
]
);
}
}

View File

@@ -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)]

View File

@@ -255,6 +255,7 @@ impl EventProcessor for EventProcessorWithHumanOutput {
call_id,
command,
cwd,
parsed_cmd: _,
}) => {
self.call_id_to_command.insert(
call_id.clone(),

View File

@@ -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": []
}
}
});

View File

@@ -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,

View File

@@ -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![

View File

@@ -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(),