This commit is contained in:
Ahmed Ibrahim
2025-09-30 15:09:26 -07:00
parent 516acc030b
commit e352f37940
9 changed files with 242 additions and 38 deletions

View File

@@ -30,6 +30,7 @@ codex-protocol = { workspace = true }
owo-colors = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
uuid = { workspace = true }
shlex = { workspace = true }
tokio = { workspace = true, features = [
"io-std",

View File

@@ -1,7 +1,10 @@
use clap::Parser;
use clap::ValueEnum;
use clap::error::Error as ClapError;
use clap::error::ErrorKind as ClapErrorKind;
use codex_common::CliConfigOverrides;
use std::path::PathBuf;
use uuid::Uuid;
#[derive(Parser, Debug)]
#[command(version)]
@@ -100,18 +103,59 @@ pub enum Command {
#[derive(Parser, Debug)]
pub struct ResumeArgs {
/// Conversation/session id (UUID). When provided, resumes this session.
/// If omitted, use --last to pick the most recent recorded session.
#[arg(value_name = "SESSION_ID")]
pub session_id: Option<String>,
/// Prompt to send after resuming the session. If `-` is used, read from stdin.
#[arg(value_name = "PROMPT", index = 1)]
pub prompt: Option<String>,
/// Resume the most recent recorded session (newest) without specifying an id.
#[arg(long = "last", default_value_t = false, conflicts_with = "session_id")]
#[arg(long = "last", default_value_t = false)]
pub last: bool,
/// Prompt to send after resuming the session. If `-` is used, read from stdin.
#[arg(value_name = "PROMPT")]
pub prompt: Option<String>,
/// Conversation/session id (UUID). When provided, resumes this session.
/// If omitted, use --last to pick the most recent recorded session.
#[arg(value_name = "SESSION_ID", index = 2)]
pub session_id: Option<String>,
}
impl ResumeArgs {
pub fn normalize(&mut self) -> Result<(), ClapError> {
if self.last {
if let Some(value) = self.session_id.take() {
if Self::looks_like_session_id(&value) {
return Err(ClapError::raw(
ClapErrorKind::ArgumentConflict,
"The argument '--last' cannot be used with '[SESSION_ID]'",
));
}
if let Some(existing) = &mut self.prompt {
if !existing.is_empty() {
existing.push(' ');
}
existing.push_str(&value);
} else {
self.prompt = Some(value);
}
}
return Ok(());
}
if self.session_id.is_some() {
return Ok(());
}
if let Some(value) = self.prompt.take() {
if Self::looks_like_session_id(&value) {
self.session_id = Some(value);
} else {
self.prompt = Some(value);
}
}
Ok(())
}
fn looks_like_session_id(value: &str) -> bool {
Uuid::parse_str(value).is_ok()
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]

View File

@@ -61,18 +61,24 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
json: json_mode,
experimental_json,
sandbox_mode: sandbox_mode_cli_arg,
prompt,
prompt: parent_prompt,
output_schema: output_schema_path,
include_plan_tool,
config_overrides,
} = cli;
// Determine the prompt source (parent or subcommand) and read from stdin if needed.
let prompt_arg = match &command {
let mut command = command;
let prompt_arg = match &mut command {
// Allow prompt before the subcommand by falling back to the parent-level prompt
// when the Resume subcommand did not provide its own prompt.
Some(ExecCommand::Resume(args)) => args.prompt.clone().or(prompt),
None => prompt,
Some(ExecCommand::Resume(args)) => {
if let Err(err) = args.normalize() {
err.exit();
}
args.prompt.clone().or_else(|| parent_prompt.clone())
}
None => parent_prompt,
};
let prompt = match prompt_arg {

View File

@@ -130,6 +130,62 @@ fn exec_resume_last_appends_to_existing_file() -> anyhow::Result<()> {
Ok(())
}
#[test]
fn exec_resume_last_accepts_prompt_after_flag() -> anyhow::Result<()> {
let home = TempDir::new()?;
let fixture = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/cli_responses_fixture.sse");
let marker = format!("resume-last-flag-{}", Uuid::new_v4());
let prompt = format!("echo {marker}");
Command::cargo_bin("codex-exec")
.context("should find binary for codex-exec")?
.env("CODEX_HOME", home.path())
.env("OPENAI_API_KEY", "dummy")
.env("CODEX_RS_SSE_FIXTURE", &fixture)
.env("OPENAI_BASE_URL", "http://unused.local")
.arg("--skip-git-repo-check")
.arg("-C")
.arg(env!("CARGO_MANIFEST_DIR"))
.arg(&prompt)
.assert()
.success();
let sessions_dir = home.path().join("sessions");
let path = find_session_file_containing_marker(&sessions_dir, &marker)
.expect("no session file found after first run");
let marker2 = format!("resume-last-flag-2-{}", Uuid::new_v4());
let prompt2 = format!("echo {marker2}");
Command::cargo_bin("codex-exec")
.context("should find binary for codex-exec")?
.env("CODEX_HOME", home.path())
.env("OPENAI_API_KEY", "dummy")
.env("CODEX_RS_SSE_FIXTURE", &fixture)
.env("OPENAI_BASE_URL", "http://unused.local")
.arg("--skip-git-repo-check")
.arg("-C")
.arg(env!("CARGO_MANIFEST_DIR"))
.arg("resume")
.arg("--last")
.arg(&prompt2)
.assert()
.success();
let resumed_path = find_session_file_containing_marker(&sessions_dir, &marker2)
.expect("no resumed session file containing marker2");
assert_eq!(
resumed_path, path,
"resume --last should reuse the existing file",
);
let content = std::fs::read_to_string(&resumed_path)?;
assert!(content.contains(&marker));
assert!(content.contains(&marker2));
Ok(())
}
#[test]
fn exec_resume_by_id_appends_to_existing_file() -> anyhow::Result<()> {
let home = TempDir::new()?;