diff --git a/codex-rs/cli/src/main.rs b/codex-rs/cli/src/main.rs index 583a2e32e8..faa6cf2fce 100644 --- a/codex-rs/cli/src/main.rs +++ b/codex-rs/cli/src/main.rs @@ -147,7 +147,7 @@ struct ResumeCommand { session_id: Option, /// Continue the most recent session without showing the picker. - #[arg(long = "last", default_value_t = false, conflicts_with = "session_id")] + #[arg(long = "last", default_value_t = false)] last: bool, /// Show all sessions (disables cwd filtering and shows CWD column). @@ -932,6 +932,24 @@ mod tests { finalize_fork_interactive(interactive, root_overrides, session_id, last, all, fork_cli) } + #[test] + fn exec_resume_last_accepts_prompt_positional() { + let cli = + MultitoolCli::try_parse_from(["codex", "exec", "--json", "resume", "--last", "2+2"]) + .expect("parse should succeed"); + + let Some(Subcommand::Exec(exec)) = cli.subcommand else { + panic!("expected exec subcommand"); + }; + let Some(codex_exec::Command::Resume(args)) = exec.command else { + panic!("expected exec resume"); + }; + + assert!(args.last); + assert_eq!(args.session_id, None); + assert_eq!(args.prompt.as_deref(), Some("2+2")); + } + fn app_server_from_args(args: &[&str]) -> AppServerCommand { let cli = MultitoolCli::try_parse_from(args).expect("parse"); let Subcommand::AppServer(app_server) = cli.subcommand.expect("app-server present") else { diff --git a/codex-rs/exec/src/cli.rs b/codex-rs/exec/src/cli.rs index 3d2001ae61..56ff805fa0 100644 --- a/codex-rs/exec/src/cli.rs +++ b/codex-rs/exec/src/cli.rs @@ -1,3 +1,5 @@ +use clap::Args; +use clap::FromArgMatches; use clap::Parser; use clap::ValueEnum; use codex_common::CliConfigOverrides; @@ -108,20 +110,22 @@ pub enum Command { Review(ReviewArgs), } -#[derive(Parser, Debug)] -pub struct ResumeArgs { +#[derive(Args, Debug)] +struct ResumeArgsRaw { + // Note: This is the direct clap shape. We reinterpret the positional when --last is set + // so "codex resume --last " treats the positional as a prompt, not a session id. /// 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, + session_id: Option, /// Resume the most recent recorded session (newest) without specifying an id. #[arg(long = "last", default_value_t = false)] - pub last: bool, + last: bool, /// Show all sessions (disables cwd filtering). #[arg(long = "all", default_value_t = false)] - pub all: bool, + all: bool, /// Optional image(s) to attach to the prompt sent after resuming. #[arg( @@ -131,13 +135,72 @@ pub struct ResumeArgs { value_delimiter = ',', num_args = 1 )] - pub images: Vec, + images: Vec, /// Prompt to send after resuming the session. If `-` is used, read from stdin. #[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)] + prompt: Option, +} + +#[derive(Debug)] +pub struct ResumeArgs { + /// Conversation/session id (UUID). When provided, resumes this session. + /// If omitted, use --last to pick the most recent recorded session. + pub session_id: Option, + + /// Resume the most recent recorded session (newest) without specifying an id. + pub last: bool, + + /// Show all sessions (disables cwd filtering). + pub all: bool, + + /// Optional image(s) to attach to the prompt sent after resuming. + pub images: Vec, + + /// Prompt to send after resuming the session. If `-` is used, read from stdin. pub prompt: Option, } +impl From for ResumeArgs { + fn from(raw: ResumeArgsRaw) -> Self { + // When --last is used without an explicit prompt, treat the positional as the prompt + // (clap can’t express this conditional positional meaning cleanly). + let (session_id, prompt) = if raw.last && raw.prompt.is_none() { + (None, raw.session_id) + } else { + (raw.session_id, raw.prompt) + }; + Self { + session_id, + last: raw.last, + all: raw.all, + images: raw.images, + prompt, + } + } +} + +impl Args for ResumeArgs { + fn augment_args(cmd: clap::Command) -> clap::Command { + ResumeArgsRaw::augment_args(cmd) + } + + fn augment_args_for_update(cmd: clap::Command) -> clap::Command { + ResumeArgsRaw::augment_args_for_update(cmd) + } +} + +impl FromArgMatches for ResumeArgs { + fn from_arg_matches(matches: &clap::ArgMatches) -> Result { + ResumeArgsRaw::from_arg_matches(matches).map(Self::from) + } + + fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> { + *self = ResumeArgsRaw::from_arg_matches(matches).map(Self::from)?; + Ok(()) + } +} + #[derive(Parser, Debug)] pub struct ReviewArgs { /// Review staged, unstaged, and untracked changes.