Merge remote-tracking branch 'upstream/dev/codex/add-fork-option-to-codex-exec' into repair/collab-stack-refresh-20260402

This commit is contained in:
Friel
2026-04-02 11:25:00 +00:00

View File

@@ -122,6 +122,18 @@ enum InitialOperation {
},
}
enum StdinPromptBehavior {
/// Read stdin only when there is no positional prompt, which is the legacy
/// `codex exec` behavior for `codex exec` with piped input.
RequiredIfPiped,
/// Always treat stdin as the prompt, used for the explicit `codex exec -`
/// sentinel and similar forced-stdin call sites.
Forced,
/// If stdin is piped alongside a positional prompt, treat stdin as
/// additional context to append rather than as the primary prompt.
OptionalAppend,
}
struct RequestIdSequencer {
next: i64,
}
@@ -640,7 +652,7 @@ async fn run_exec_session(args: ExecRunArgs) -> anyhow::Result<()> {
)
}
(None, root_prompt, imgs) => {
let prompt_text = resolve_prompt(root_prompt);
let prompt_text = resolve_root_prompt(root_prompt);
let mut items: Vec<UserInput> = imgs
.into_iter()
.map(|path| UserInput::LocalImage { path })
@@ -1588,46 +1600,92 @@ fn decode_utf16(
String::from_utf16(&units).map_err(|_| PromptDecodeError::InvalidUtf16 { encoding })
}
fn read_prompt_from_stdin(behavior: StdinPromptBehavior) -> Option<String> {
let stdin_is_terminal = std::io::stdin().is_terminal();
match behavior {
StdinPromptBehavior::RequiredIfPiped if stdin_is_terminal => {
eprintln!(
"No prompt provided. Either specify one as an argument or pipe the prompt into stdin."
);
std::process::exit(1);
}
StdinPromptBehavior::RequiredIfPiped => {
eprintln!("Reading prompt from stdin...");
}
StdinPromptBehavior::Forced => {}
StdinPromptBehavior::OptionalAppend if stdin_is_terminal => return None,
StdinPromptBehavior::OptionalAppend => {
eprintln!("Reading additional input from stdin...");
}
}
let mut bytes = Vec::new();
if let Err(e) = std::io::stdin().read_to_end(&mut bytes) {
eprintln!("Failed to read prompt from stdin: {e}");
std::process::exit(1);
}
let buffer = match decode_prompt_bytes(&bytes) {
Ok(s) => s,
Err(e) => {
eprintln!("Failed to read prompt from stdin: {e}");
std::process::exit(1);
}
};
if buffer.trim().is_empty() {
match behavior {
StdinPromptBehavior::OptionalAppend => None,
StdinPromptBehavior::RequiredIfPiped | StdinPromptBehavior::Forced => {
eprintln!("No prompt provided via stdin.");
std::process::exit(1);
}
}
} else {
Some(buffer)
}
}
fn prompt_with_stdin_context(prompt: &str, stdin_text: &str) -> String {
let mut combined = format!("{prompt}\n\n<stdin>\n{stdin_text}");
if !stdin_text.ends_with('\n') {
combined.push('\n');
}
combined.push_str("</stdin>");
combined
}
fn resolve_prompt(prompt_arg: Option<String>) -> String {
match prompt_arg {
Some(p) if p != "-" => p,
maybe_dash => {
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);
}
if !force_stdin {
eprintln!("Reading prompt from stdin...");
}
let mut bytes = Vec::new();
if let Err(e) = std::io::stdin().read_to_end(&mut bytes) {
eprintln!("Failed to read prompt from stdin: {e}");
std::process::exit(1);
}
let buffer = match decode_prompt_bytes(&bytes) {
Ok(s) => s,
Err(e) => {
eprintln!("Failed to read prompt from stdin: {e}");
std::process::exit(1);
}
let behavior = if matches!(maybe_dash.as_deref(), Some("-")) {
StdinPromptBehavior::Forced
} else {
StdinPromptBehavior::RequiredIfPiped
};
if buffer.trim().is_empty() {
eprintln!("No prompt provided via stdin.");
std::process::exit(1);
}
buffer
let Some(prompt) = read_prompt_from_stdin(behavior) else {
unreachable!("required stdin prompt should produce content");
};
prompt
}
}
}
fn resolve_root_prompt(prompt_arg: Option<String>) -> String {
match prompt_arg {
Some(prompt) if prompt != "-" => {
if let Some(stdin_text) = read_prompt_from_stdin(StdinPromptBehavior::OptionalAppend) {
prompt_with_stdin_context(&prompt, &stdin_text)
} else {
prompt
}
}
maybe_dash => resolve_prompt(maybe_dash),
}
}
fn build_review_request(args: &ReviewArgs) -> anyhow::Result<ReviewRequest> {
let target = if args.uncommitted {
ReviewTarget::UncommittedChanges