From ae743d56b09f29b3a28a57a6c8aff7565e7b075a Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 30 May 2025 14:41:55 -0700 Subject: [PATCH 1/2] feat: for `codex exec`, if PROMPT is not specified, read from stdin if not a TTY (#1178) This attempts to make `codex exec` more flexible in how the prompt can be passed: * as before, it can be passed as a single string argument * if `-` is passed as the value, the prompt is read from stdin * if no argument is passed _and stdin is a tty_, prints a warning to stderr that no prompt was specified an exits non-zero. * if no argument is passed _and stdin is NOT a tty_, prints `Reading prompt from stdin...` to stderr to let the user know that Codex will wait until it reads EOF from stdin to proceed. (You can repro this case by doing `yes | just exec` since stdin is not a TTY in that case but it also never reaches EOF). --- codex-rs/exec/src/cli.rs | 6 ++++-- codex-rs/exec/src/lib.rs | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index 1c2a9eb8aa..413fd23cb7 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -45,8 +45,10 @@ pub struct Cli { #[arg(long = "output-last-message")] pub last_message_file: Option, - /// Initial instructions for the agent. - pub prompt: String, + /// Initial instructions for the agent. If not provided as an argument (or + /// if `-` is used), instructions are read from stdin. + #[arg(value_name = "PROMPT")] + pub prompt: Option, } #[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)] diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 8c94fe5dc9..6602213b14 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -2,6 +2,7 @@ mod cli; mod event_processor; use std::io::IsTerminal; +use std::io::Read; use std::path::Path; use std::path::PathBuf; use std::sync::Arc; @@ -40,6 +41,41 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any config_overrides, } = cli; + // Determine the prompt based on CLI arg and/or stdin. + let prompt = match prompt { + Some(p) if p != "-" => p, + // Either `-` was passed or no positional arg. + maybe_dash => { + // When no arg (None) **and** stdin is a TTY, bail out early – unless the + // user explicitly forced reading via `-`. + let force_stdin = matches!(maybe_dash.as_deref(), Some("-")); + + if std::io::stdin().is_terminal() && !force_stdin { + eprintln!( + "No prompt provided. Either specify one as an argument or pipe the prompt into stdin." + ); + std::process::exit(1); + } + + // Ensure the user knows we are waiting on stdin, as they may + // have gotten into this state by mistake. If so, and they are not + // writing to stdin, Codex will hang indefinitely, so this should + // help them debug in that case. + if !force_stdin { + eprintln!("Reading prompt from stdin..."); + } + let mut buffer = String::new(); + if let Err(e) = std::io::stdin().read_to_string(&mut buffer) { + eprintln!("Failed to read prompt from stdin: {e}"); + std::process::exit(1); + } else if buffer.trim().is_empty() { + eprintln!("No prompt provided via stdin."); + std::process::exit(1); + } + buffer + } + }; + let (stdout_with_ansi, stderr_with_ansi) = match color { cli::Color::Always => (true, true), cli::Color::Never => (false, false), From 4b0578b5e4ccdb68e1cbae0a7f156240ed7c421f Mon Sep 17 00:00:00 2001 From: Michael Bolin Date: Fri, 30 May 2025 16:04:59 -0700 Subject: [PATCH 2/2] feat: grab-bag of improvements to `exec` output --- codex-rs/exec/src/event_processor.rs | 71 +++++++++++++++++----------- codex-rs/exec/src/lib.rs | 8 ++-- 2 files changed, 47 insertions(+), 32 deletions(-) diff --git a/codex-rs/exec/src/event_processor.rs b/codex-rs/exec/src/event_processor.rs index 352275bf43..8005980d8e 100644 --- a/codex-rs/exec/src/event_processor.rs +++ b/codex-rs/exec/src/event_processor.rs @@ -1,4 +1,3 @@ -use chrono::Utc; use codex_common::elapsed::format_elapsed; use codex_core::config::Config; use codex_core::protocol::AgentMessageEvent; @@ -37,11 +36,13 @@ pub(crate) struct EventProcessor { // using .style() with one of these fields. If you need a new style, add a // new field here. bold: Style, + italic: Style, dimmed: Style, magenta: Style, red: Style, green: Style, + cyan: Style, } impl EventProcessor { @@ -55,10 +56,12 @@ impl EventProcessor { call_id_to_command, call_id_to_patch, bold: Style::new().bold(), + italic: Style::new().italic(), dimmed: Style::new().dimmed(), magenta: Style::new().magenta(), red: Style::new().red(), green: Style::new().green(), + cyan: Style::new().cyan(), call_id_to_tool_call, } } else { @@ -66,10 +69,12 @@ impl EventProcessor { call_id_to_command, call_id_to_patch, bold: Style::new(), + italic: Style::new(), dimmed: Style::new(), magenta: Style::new(), red: Style::new(), green: Style::new(), + cyan: Style::new(), call_id_to_tool_call, } } @@ -94,43 +99,47 @@ struct PatchApplyBegin { auto_approved: bool, } +#[macro_export] macro_rules! ts_println { ($($arg:tt)*) => {{ - let now = Utc::now(); + let now = chrono::Utc::now(); let formatted = now.format("%Y-%m-%dT%H:%M:%S").to_string(); print!("[{}] ", formatted); println!($($arg)*); }}; } -/// Print a concise summary of the effective configuration that will be used -/// for the session. This mirrors the information shown in the TUI welcome -/// screen. -pub(crate) fn print_config_summary(config: &Config, with_ansi: bool) { - let bold = if with_ansi { - Style::new().bold() - } else { - Style::new() - }; +impl EventProcessor { + /// Print a concise summary of the effective configuration that will be used + /// for the session. This mirrors the information shown in the TUI welcome + /// screen. + pub(crate) fn print_config_summary(&mut self, config: &Config, prompt: &str) { + ts_println!("OpenAI Codex (research preview)\n--------"); - ts_println!("OpenAI Codex (research preview)\n--------"); + let entries = vec![ + ("workdir", config.cwd.display().to_string()), + ("model", config.model.clone()), + ("provider", config.model_provider_id.clone()), + ("approval", format!("{:?}", config.approval_policy)), + ("sandbox", format!("{:?}", config.sandbox_policy)), + ]; - let entries = vec![ - ("workdir", config.cwd.display().to_string()), - ("model", config.model.clone()), - ("provider", config.model_provider_id.clone()), - ("approval", format!("{:?}", config.approval_policy)), - ("sandbox", format!("{:?}", config.sandbox_policy)), - ]; + for (key, value) in entries { + println!("{} {}", format!("{key}: ").style(self.bold), value); + } - for (key, value) in entries { - println!("{} {}", format!("{key}: ").style(bold), value); + println!("--------"); + + // Echo the prompt that will be sent to the agent so it is visible in the + // transcript/logs before any events come in. Note the prompt may have been + // read from stdin, so it may not be visible in the terminal otherwise. + ts_println!( + "{}\n{}", + "User instructions:".style(self.bold).style(self.cyan), + prompt + ); } - println!("--------\n"); -} - -impl EventProcessor { pub(crate) fn process_event(&mut self, event: Event) { let Event { id: _, msg } = event; match msg { @@ -145,8 +154,10 @@ impl EventProcessor { // Ignore. } EventMsg::AgentMessage(AgentMessageEvent { message }) => { - let prefix = "Agent message:".style(self.bold); - ts_println!("{prefix} {message}"); + ts_println!( + "{}\n{message}", + "codex".style(self.bold).style(self.magenta) + ); } EventMsg::ExecCommandBegin(ExecCommandBeginEvent { call_id, @@ -394,7 +405,11 @@ impl EventProcessor { // Should we exit? } EventMsg::AgentReasoning(agent_reasoning_event) => { - println!("thinking: {}", agent_reasoning_event.text); + ts_println!( + "{}\n{}", + "thinking".style(self.italic).style(self.magenta), + agent_reasoning_event.text + ); } EventMsg::SessionConfigured(session_configured_event) => { let SessionConfiguredEvent { diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 6602213b14..e203c2f161 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -20,7 +20,6 @@ use codex_core::protocol::SandboxPolicy; use codex_core::protocol::TaskCompleteEvent; use codex_core::util::is_inside_git_repo; use event_processor::EventProcessor; -use event_processor::print_config_summary; use tracing::debug; use tracing::error; use tracing::info; @@ -113,8 +112,10 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any }; let config = Config::load_with_cli_overrides(cli_kv_overrides, overrides)?; - // Print the effective configuration so users can see what Codex is using. - print_config_summary(&config, stdout_with_ansi); + let mut event_processor = EventProcessor::create_with_ansi(stdout_with_ansi); + // Print the effective configuration and prompt so users can see what Codex + // is using. + event_processor.print_config_summary(&config, &prompt); if !skip_git_repo_check && !is_inside_git_repo(&config) { eprintln!("Not inside a Git repo and --skip-git-repo-check was not specified."); @@ -204,7 +205,6 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any info!("Sent prompt with event ID: {initial_prompt_task_id}"); // Run the loop until the task is complete. - let mut event_processor = EventProcessor::create_with_ansi(stdout_with_ansi); while let Some(event) = rx.recv().await { let (is_last_event, last_assistant_message) = match &event.msg { EventMsg::TaskComplete(TaskCompleteEvent { last_agent_message }) => {