mirror of
https://github.com/openai/codex.git
synced 2026-02-02 06:57:03 +00:00
Compare commits
1 Commits
update_age
...
fix-cli
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e352f37940 |
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -773,6 +773,7 @@ dependencies = [
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-subscriber",
|
||||
"uuid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -55,6 +55,14 @@ In the transcript preview, the footer shows an `Esc edit prev` hint while editin
|
||||
|
||||
Sometimes it is not convenient to `cd` to the directory you want Codex to use as the "working root" before running Codex. Fortunately, `codex` supports a `--cd` option so you can specify whatever folder you want. You can confirm that Codex is honoring `--cd` by double-checking the **workdir** it reports in the TUI at the start of a new session.
|
||||
|
||||
### Resuming sessions
|
||||
|
||||
When you use `codex resume`, provide any follow-up prompt *before* an optional session id. This keeps combinations like `codex resume --last "fix the tests"` working while still letting you resume a specific session when needed:
|
||||
|
||||
- `codex resume --last "kick off linting"` — resume the most recent session and immediately send a new prompt.
|
||||
- `codex resume "draft release notes" d9b7b8b8-3a1f-4a4d-b0a2-4f04bb8d58df` — resume a specific session and send a follow-up prompt.
|
||||
- `codex resume d9b7b8b8-3a1f-4a4d-b0a2-4f04bb8d58df` — resume a session without sending a prompt (the CLI treats lone UUIDs as session ids).
|
||||
|
||||
### Shell completions
|
||||
|
||||
Generate shell completion scripts via:
|
||||
|
||||
@@ -36,6 +36,7 @@ ctor = { workspace = true }
|
||||
owo-colors = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
supports-color = { workspace = true }
|
||||
uuid = { workspace = true }
|
||||
tokio = { workspace = true, features = [
|
||||
"io-std",
|
||||
"macros",
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use clap::CommandFactory;
|
||||
use clap::Parser;
|
||||
use clap::error::Error as ClapError;
|
||||
use clap::error::ErrorKind as ClapErrorKind;
|
||||
use clap_complete::Shell;
|
||||
use clap_complete::generate;
|
||||
use codex_arg0::arg0_dispatch_or_else;
|
||||
@@ -22,6 +24,7 @@ use codex_tui::Cli as TuiCli;
|
||||
use owo_colors::OwoColorize;
|
||||
use std::path::PathBuf;
|
||||
use supports_color::Stream;
|
||||
use uuid::Uuid;
|
||||
|
||||
mod mcp_cmd;
|
||||
|
||||
@@ -112,17 +115,17 @@ struct CompletionCommand {
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct ResumeCommand {
|
||||
/// 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")]
|
||||
session_id: Option<String>,
|
||||
|
||||
/// Continue the most recent session without showing the picker.
|
||||
#[arg(long = "last", default_value_t = false, conflicts_with = "session_id")]
|
||||
last: bool,
|
||||
|
||||
#[clap(flatten)]
|
||||
config_overrides: TuiCli,
|
||||
|
||||
/// Continue the most recent session without showing the picker.
|
||||
#[arg(long = "last", default_value_t = false)]
|
||||
last: bool,
|
||||
|
||||
/// 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)]
|
||||
session_id: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
@@ -286,11 +289,15 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
|
||||
Some(Subcommand::AppServer) => {
|
||||
codex_app_server::run_main(codex_linux_sandbox_exe, root_config_overrides).await?;
|
||||
}
|
||||
Some(Subcommand::Resume(ResumeCommand {
|
||||
session_id,
|
||||
last,
|
||||
config_overrides,
|
||||
})) => {
|
||||
Some(Subcommand::Resume(mut resume_cmd)) => {
|
||||
if let Err(err) = resume_cmd.normalize() {
|
||||
err.exit();
|
||||
}
|
||||
let ResumeCommand {
|
||||
config_overrides,
|
||||
last,
|
||||
session_id,
|
||||
} = resume_cmd;
|
||||
interactive = finalize_resume_interactive(
|
||||
interactive,
|
||||
root_config_overrides.clone(),
|
||||
@@ -491,14 +498,16 @@ mod tests {
|
||||
subcommand,
|
||||
} = cli;
|
||||
|
||||
let Subcommand::Resume(ResumeCommand {
|
||||
session_id,
|
||||
last,
|
||||
config_overrides: resume_cli,
|
||||
}) = subcommand.expect("resume present")
|
||||
else {
|
||||
unreachable!()
|
||||
let mut resume_cmd = match subcommand.expect("resume present") {
|
||||
Subcommand::Resume(cmd) => cmd,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
resume_cmd.normalize().expect("normalize resume args");
|
||||
let ResumeCommand {
|
||||
config_overrides: resume_cli,
|
||||
last,
|
||||
session_id,
|
||||
} = resume_cmd;
|
||||
|
||||
finalize_resume_interactive(interactive, root_overrides, session_id, last, resume_cli)
|
||||
}
|
||||
@@ -575,12 +584,45 @@ mod tests {
|
||||
assert_eq!(interactive.resume_session_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_last_accepts_follow_up_prompt() {
|
||||
let interactive = finalize_from_args(["codex", "resume", "--last", "hi there"].as_ref());
|
||||
assert!(interactive.resume_last);
|
||||
assert_eq!(interactive.prompt.as_deref(), Some("hi there"));
|
||||
assert_eq!(interactive.resume_session_id, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_prompt_before_session_id() {
|
||||
let interactive = finalize_from_args(
|
||||
[
|
||||
"codex",
|
||||
"resume",
|
||||
"summarize progress",
|
||||
"123e4567-e89b-12d3-a456-426614174000",
|
||||
]
|
||||
.as_ref(),
|
||||
);
|
||||
assert_eq!(interactive.prompt.as_deref(), Some("summarize progress"));
|
||||
assert_eq!(
|
||||
interactive.resume_session_id.as_deref(),
|
||||
Some("123e4567-e89b-12d3-a456-426614174000"),
|
||||
);
|
||||
assert!(!interactive.resume_last);
|
||||
assert!(!interactive.resume_picker);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resume_picker_logic_with_session_id() {
|
||||
let interactive = finalize_from_args(["codex", "resume", "1234"].as_ref());
|
||||
let interactive = finalize_from_args(
|
||||
["codex", "resume", "123e4567-e89b-12d3-a456-426614174000"].as_ref(),
|
||||
);
|
||||
assert!(!interactive.resume_picker);
|
||||
assert!(!interactive.resume_last);
|
||||
assert_eq!(interactive.resume_session_id.as_deref(), Some("1234"));
|
||||
assert_eq!(
|
||||
interactive.resume_session_id.as_deref(),
|
||||
Some("123e4567-e89b-12d3-a456-426614174000")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -589,7 +631,7 @@ mod tests {
|
||||
[
|
||||
"codex",
|
||||
"resume",
|
||||
"sid",
|
||||
"123e4567-e89b-12d3-a456-426614174000",
|
||||
"--oss",
|
||||
"--full-auto",
|
||||
"--search",
|
||||
@@ -637,7 +679,10 @@ mod tests {
|
||||
assert!(has_a && has_b);
|
||||
assert!(!interactive.resume_picker);
|
||||
assert!(!interactive.resume_last);
|
||||
assert_eq!(interactive.resume_session_id.as_deref(), Some("sid"));
|
||||
assert_eq!(
|
||||
interactive.resume_session_id.as_deref(),
|
||||
Some("123e4567-e89b-12d3-a456-426614174000")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -656,3 +701,45 @@ mod tests {
|
||||
assert_eq!(interactive.resume_session_id, None);
|
||||
}
|
||||
}
|
||||
|
||||
impl ResumeCommand {
|
||||
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.config_overrides.prompt {
|
||||
if !existing.is_empty() {
|
||||
existing.push(' ');
|
||||
}
|
||||
existing.push_str(&value);
|
||||
} else {
|
||||
self.config_overrides.prompt = Some(value);
|
||||
}
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.session_id.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if let Some(prompt) = self.config_overrides.prompt.take() {
|
||||
if Self::looks_like_session_id(&prompt) {
|
||||
self.session_id = Some(prompt);
|
||||
} else {
|
||||
self.config_overrides.prompt = Some(prompt);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn looks_like_session_id(value: &str) -> bool {
|
||||
Uuid::parse_str(value).is_ok()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)]
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()?;
|
||||
|
||||
@@ -7,7 +7,7 @@ use std::path::PathBuf;
|
||||
#[command(version)]
|
||||
pub struct Cli {
|
||||
/// Optional user prompt to start the session.
|
||||
#[arg(value_name = "PROMPT")]
|
||||
#[arg(value_name = "PROMPT", index = 1)]
|
||||
pub prompt: Option<String>,
|
||||
|
||||
/// Optional image(s) to attach to the initial prompt.
|
||||
|
||||
Reference in New Issue
Block a user