Compare commits

...

7 Commits

Author SHA1 Message Date
jif-oai
a76582a8c9 Merge branch 'origin/main' 2025-11-24 15:46:21 +00:00
jif-oai
15023e4728 A few more 2025-11-24 15:26:55 +00:00
jif-oai
132cfe97a5 Update with main 2025-11-24 15:14:46 +00:00
jif-oai
d619a0dc05 Merge branch 'main' into jif/codex-exec-review 2025-11-18 14:01:06 +00:00
jif-oai
8970cccbb1 Merge branch 'main' into jif/codex-exec-review 2025-11-13 09:43:51 +00:00
jif-oai
67b66d1e08 NIT 2025-11-12 13:19:51 +00:00
jif-oai
50b8e856ab feat: add review to codex exec 2025-11-12 13:09:13 +00:00
11 changed files with 538 additions and 93 deletions

View File

@@ -52,6 +52,23 @@ You can enable notifications by configuring a script that is run whenever the ag
To run Codex non-interactively, run `codex exec PROMPT` (you can also pass the prompt via `stdin`) and Codex will work on your task until it decides that it is done and exits. Output is printed to the terminal directly. You can set the `RUST_LOG` environment variable to see more about what's going on.
You can also run the review flow headlessly:
```
# Review uncommitted changes
codex exec review --uncommitted
# Review changes against a base branch
codex exec review --branch main
# Review a specific commit
codex exec review --commit abcd123
# Custom review instructions (from arg or stdin)
codex exec review "Review changes in src/ with focus on error handling"
echo "Review all TODOs left in code" | codex exec review -
```
### Experimenting with the Codex Sandbox
To test to see what happens when a command is run under the sandbox provided by Codex, we provide the following subcommands in Codex CLI:

View File

@@ -58,6 +58,7 @@ pub use model_provider_info::create_oss_provider_with_base_url;
mod conversation_manager;
mod event_mapping;
pub mod review_format;
pub mod review_prompts;
pub use codex_protocol::protocol::InitialHistory;
pub use conversation_manager::ConversationManager;
pub use conversation_manager::NewConversation;

View File

@@ -0,0 +1,209 @@
use codex_protocol::protocol::ReviewRequest;
use std::cmp::min;
/// Target to review against.
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ReviewTarget {
/// Review the working tree: staged, unstaged, and untracked files.
UncommittedChanges,
/// Review changes between the current branch and the given base branch.
BaseBranch { branch: String },
/// Review the changes introduced by a specific commit.
Commit {
sha: String,
/// Optional human-readable label (e.g., commit subject) for UIs.
title: Option<String>,
},
/// Arbitrary instructions, equivalent to the old free-form prompt.
Custom { instructions: String },
}
/// Validation errors for review targets.
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum ReviewTargetError {
#[error("branch must not be empty")]
EmptyBranch,
#[error("sha must not be empty")]
EmptySha,
#[error("instructions must not be empty")]
EmptyInstructions,
}
/// Built review request plus a user-visible description of the target.
#[derive(Clone, Debug, PartialEq)]
pub struct BuiltReviewRequest {
pub review_request: ReviewRequest,
pub display_text: String,
}
/// Build the review prompt and user-facing hint for uncommitted changes.
pub fn review_uncommitted_prompt() -> (String, String) {
let prompt = "Review the current code changes (staged, unstaged, and untracked files) and provide prioritized findings.".to_string();
let hint = "current changes".to_string();
(prompt, hint)
}
/// Build the review prompt and hint for reviewing against a base branch.
pub fn review_branch_prompt(branch: &str) -> (String, String) {
let prompt = format!(
"Review the code changes against the base branch '{branch}'. Start by finding the merge diff between the current branch and {branch}'s upstream e.g. (`git merge-base HEAD \"$(git rev-parse --abbrev-ref \"{branch}@{{upstream}}\")\"`), then run `git diff` against that SHA to see what changes we would merge into the {branch} branch. Provide prioritized, actionable findings."
);
let hint = format!("changes against '{branch}'");
(prompt, hint)
}
/// Build the review prompt and hint for a specific commit.
/// If `subject_opt` is provided, it will be included in the prompt; otherwise it is omitted.
pub fn review_commit_prompt(sha: &str, subject_opt: Option<&str>) -> (String, String) {
let short: String = sha.chars().take(min(7, sha.len())).collect();
let prompt = if let Some(subject) = subject_opt {
format!(
"Review the code changes introduced by commit {sha} (\"{subject}\"). Provide prioritized, actionable findings."
)
} else {
format!(
"Review the code changes introduced by commit {sha}. Provide prioritized, actionable findings."
)
};
let hint = format!("commit {short}");
(prompt, hint)
}
/// Build the review prompt and hint for custom free-form instructions.
pub fn review_custom_prompt(custom: &str) -> (String, String) {
let prompt = custom.trim().to_string();
(prompt.clone(), prompt)
}
pub fn review_request_from_target(
target: ReviewTarget,
append_to_original_thread: bool,
) -> Result<BuiltReviewRequest, ReviewTargetError> {
match target {
ReviewTarget::UncommittedChanges => {
let (prompt, hint) = review_uncommitted_prompt();
Ok(BuiltReviewRequest {
review_request: ReviewRequest {
prompt,
user_facing_hint: hint,
append_to_original_thread,
},
display_text: "Review uncommitted changes".to_string(),
})
}
ReviewTarget::BaseBranch { branch } => {
let branch = branch.trim().to_string();
if branch.is_empty() {
return Err(ReviewTargetError::EmptyBranch);
}
let (prompt, hint) = review_branch_prompt(&branch);
Ok(BuiltReviewRequest {
review_request: ReviewRequest {
prompt,
user_facing_hint: hint,
append_to_original_thread,
},
display_text: format!("Review changes against base branch '{branch}'"),
})
}
ReviewTarget::Commit { sha, title } => {
let sha = sha.trim().to_string();
if sha.is_empty() {
return Err(ReviewTargetError::EmptySha);
}
let title = title
.map(|t| t.trim().to_string())
.filter(|t| !t.is_empty());
let (prompt, hint) = review_commit_prompt(&sha, title.as_deref());
let short_sha: String = sha.chars().take(min(7, sha.len())).collect();
let display_text = if let Some(title) = title {
format!("Review commit {short_sha}: {title}")
} else {
format!("Review commit {short_sha}")
};
Ok(BuiltReviewRequest {
review_request: ReviewRequest {
prompt,
user_facing_hint: hint,
append_to_original_thread,
},
display_text,
})
}
ReviewTarget::Custom { instructions } => {
let trimmed = instructions.trim().to_string();
if trimmed.is_empty() {
return Err(ReviewTargetError::EmptyInstructions);
}
let (prompt, hint) = review_custom_prompt(&trimmed);
Ok(BuiltReviewRequest {
review_request: ReviewRequest {
prompt,
user_facing_hint: hint,
append_to_original_thread,
},
display_text: trimmed,
})
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn commit_target_includes_title_and_short_sha() {
let built = review_request_from_target(
ReviewTarget::Commit {
sha: "123456789".to_string(),
title: Some("Refactor colors".to_string()),
},
true,
)
.expect("valid commit target");
assert!(built.display_text.contains("1234567"));
assert!(built.display_text.contains("Refactor colors"));
assert_eq!(built.review_request.user_facing_hint, "commit 1234567");
assert!(built.review_request.append_to_original_thread);
}
#[test]
fn empty_inputs_reject() {
assert_eq!(
review_request_from_target(
ReviewTarget::BaseBranch {
branch: " ".to_string()
},
false
)
.unwrap_err(),
ReviewTargetError::EmptyBranch
);
assert_eq!(
review_request_from_target(
ReviewTarget::Commit {
sha: "".to_string(),
title: None
},
false
)
.unwrap_err(),
ReviewTargetError::EmptySha
);
assert_eq!(
review_request_from_target(
ReviewTarget::Custom {
instructions: "\n".to_string()
},
false
)
.unwrap_err(),
ReviewTargetError::EmptyInstructions
);
}
}

View File

@@ -91,6 +91,9 @@ pub struct Cli {
pub enum Command {
/// Resume a previous session by id or pick the most recent with --last.
Resume(ResumeArgs),
/// Run a code review.
Review(ReviewArgs),
}
#[derive(Parser, Debug)]
@@ -109,6 +112,33 @@ pub struct ResumeArgs {
pub prompt: Option<String>,
}
#[derive(Parser, Debug)]
pub struct ReviewArgs {
/// Select uncommitted (staged, unstaged, untracked) changes.
#[arg(long = "uncommitted", default_value_t = false, conflicts_with_all = ["branch", "commit", "prompt"])]
pub uncommitted: bool,
/// Review against the given base branch (PR-style).
#[arg(long = "branch", value_name = "BRANCH", conflicts_with_all = ["commit", "uncommitted", "prompt"])]
pub branch: Option<String>,
/// Review a specific commit by SHA.
#[arg(long = "commit", value_name = "SHA", conflicts_with_all = ["branch", "uncommitted", "prompt"])]
pub commit: Option<String>,
/// Optional override for the user-facing hint recorded in history.
#[arg(long = "hint", value_name = "TEXT")]
pub hint: Option<String>,
/// Optional override for the review model (defaults to config.review_model).
#[arg(long = "review-model", value_name = "MODEL")]
pub review_model: Option<String>,
/// Custom review instructions. If `-` is used, read from stdin.
#[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)]
pub prompt: Option<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum Color {

View File

@@ -10,12 +10,14 @@ use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
use codex_core::protocol::ExecCommandBeginEvent;
use codex_core::protocol::ExecCommandEndEvent;
use codex_core::protocol::ExitedReviewModeEvent;
use codex_core::protocol::FileChange;
use codex_core::protocol::McpInvocation;
use codex_core::protocol::McpToolCallBeginEvent;
use codex_core::protocol::McpToolCallEndEvent;
use codex_core::protocol::PatchApplyBeginEvent;
use codex_core::protocol::PatchApplyEndEvent;
use codex_core::protocol::ReviewRequest;
use codex_core::protocol::SessionConfiguredEvent;
use codex_core::protocol::StreamErrorEvent;
use codex_core::protocol::TaskCompleteEvent;
@@ -559,6 +561,31 @@ impl EventProcessor for EventProcessorWithHumanOutput {
ts_msg!(self, "task aborted: review ended");
}
},
EventMsg::EnteredReviewMode(ReviewRequest {
user_facing_hint, ..
}) => {
let banner = format!(">> Code review started: {user_facing_hint} <<");
ts_msg!(self, "{banner}");
}
EventMsg::ExitedReviewMode(ExitedReviewModeEvent { review_output }) => {
if let Some(output) = review_output {
if output.findings.is_empty() {
let explanation = output.overall_explanation.trim();
if !explanation.is_empty() {
ts_msg!(self, "{explanation}");
} else {
ts_msg!(self, "Reviewer failed to output a response.");
}
} else {
let block = codex_core::review_format::format_review_findings_block(
&output.findings,
None,
);
ts_msg!(self, "{block}");
}
}
ts_msg!(self, "{}", "<< Code review finished >>".style(self.dimmed));
}
EventMsg::ShutdownComplete => return CodexStatus::Shutdown,
EventMsg::WebSearchBegin(_)
| EventMsg::ExecApprovalRequest(_)
@@ -569,8 +596,6 @@ impl EventProcessor for EventProcessorWithHumanOutput {
| EventMsg::ListCustomPromptsResponse(_)
| EventMsg::RawResponseItem(_)
| EventMsg::UserMessage(_)
| EventMsg::EnteredReviewMode(_)
| EventMsg::ExitedReviewMode(_)
| EventMsg::AgentMessageDelta(_)
| EventMsg::AgentReasoningDelta(_)
| EventMsg::AgentReasoningRawContentDelta(_)

View File

@@ -143,6 +143,7 @@ impl EventProcessorWithJsonOutput {
message: ev.message.clone(),
})],
EventMsg::PlanUpdate(ev) => self.handle_plan_update(ev),
EventMsg::ExitedReviewMode(ev) => self.handle_exited_review_mode(ev),
_ => Vec::new(),
}
}
@@ -184,6 +185,31 @@ impl EventProcessorWithJsonOutput {
vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { item })]
}
fn handle_exited_review_mode(
&self,
ev: &codex_core::protocol::ExitedReviewModeEvent,
) -> Vec<ThreadEvent> {
// Convert review output into a synthetic agent message so JSONL clients
// can observe review results without bespoke item types for now.
if let Some(output) = &ev.review_output {
let text = if output.findings.is_empty() {
output.overall_explanation.clone()
} else {
codex_core::review_format::format_review_findings_block(&output.findings, None)
};
if text.is_empty() {
return Vec::new();
}
let item = ThreadItem {
id: self.get_next_item_id(),
details: ThreadItemDetails::AgentMessage(AgentMessageItem { text }),
};
vec![ThreadEvent::ItemCompleted(ItemCompletedEvent { item })]
} else {
Vec::new()
}
}
fn handle_reasoning_event(&self, ev: &AgentReasoningEvent) -> Vec<ThreadEvent> {
let item = ThreadItem {
id: self.get_next_item_id(),

View File

@@ -30,6 +30,8 @@ use codex_core::protocol::Event;
use codex_core::protocol::EventMsg;
use codex_core::protocol::Op;
use codex_core::protocol::SessionSource;
use codex_core::review_prompts::ReviewTarget;
use codex_core::review_prompts::review_request_from_target;
use codex_protocol::approvals::ElicitationAction;
use codex_protocol::config_types::SandboxMode;
use codex_protocol::user_input::UserInput;
@@ -48,6 +50,7 @@ use tracing_subscriber::EnvFilter;
use tracing_subscriber::prelude::*;
use crate::cli::Command as ExecCommand;
use crate::cli::ReviewArgs;
use crate::event_processor::CodexStatus;
use crate::event_processor::EventProcessor;
use codex_core::default_client::set_default_originator;
@@ -79,6 +82,28 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
config_overrides,
} = cli;
// Helper: read a prompt from stdin when '-' was passed or when required.
fn read_prompt_from_stdin_or_exit(force: bool) -> String {
if !force && std::io::stdin().is_terminal() {
eprintln!(
"No prompt provided. Either specify one as an argument or pipe the prompt into stdin."
);
std::process::exit(1);
}
if !force {
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
}
// Determine the prompt source (parent or subcommand) and read from stdin if needed.
let prompt_arg = match &command {
// Allow prompt before the subcommand by falling back to the parent-level prompt
@@ -96,30 +121,25 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
None
}
});
resume_prompt.or(prompt)
resume_prompt.or(prompt.clone())
}
None => prompt,
Some(ExecCommand::Review(_)) => prompt.clone(),
None => prompt.clone(),
};
let prompt = match prompt_arg {
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 `-`.
// Determine the regular prompt. If explicitly "-", read from stdin.
// If omitted, read from stdin when nonTTY; otherwise exit with a message.
let regular_prompt_opt = match (&command, prompt_arg) {
(Some(ExecCommand::Review(_)), _) => None,
(_, Some(p)) if p != "-" => Some(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);
}
// 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...");
}
@@ -131,7 +151,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
eprintln!("No prompt provided via stdin.");
std::process::exit(1);
}
buffer
Some(buffer)
}
};
@@ -228,9 +248,15 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
};
// Load configuration and determine approval policy
// Allow --review-model override only when review subcommand is present.
let review_model_override: Option<String> = match &command {
Some(ExecCommand::Review(ReviewArgs { review_model, .. })) => review_model.clone(),
_ => None,
};
let overrides = ConfigOverrides {
model,
review_model: None,
review_model: review_model_override,
config_profile,
// Default to never ask for approvals in headless mode. Feature flags can override.
approval_policy: Some(AskForApproval::Never),
@@ -329,8 +355,8 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
conversation_id: _,
conversation,
session_configured,
} = if let Some(ExecCommand::Resume(args)) = command {
let resume_path = resolve_resume_path(&config, &args).await?;
} = if let Some(ExecCommand::Resume(args)) = &command {
let resume_path = resolve_resume_path(&config, args).await?;
if let Some(path) = resume_path {
conversation_manager
@@ -346,9 +372,12 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
.new_conversation(config.clone())
.await?
};
// Print the effective configuration and prompt so users can see what Codex
// is using.
event_processor.print_config_summary(&config, &prompt, &session_configured);
// Echo the effective configuration and the text that will be sent first.
let mut echo_prompt = String::new();
if let Some(p) = &regular_prompt_opt {
echo_prompt = p.clone();
}
event_processor.print_config_summary(&config, &echo_prompt, &session_configured);
info!("Codex initialized with event: {session_configured:?}");
@@ -391,25 +420,84 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
});
}
// Package images and prompt into a single user input turn.
let mut items: Vec<UserInput> = images
.into_iter()
.map(|path| UserInput::LocalImage { path })
.collect();
items.push(UserInput::Text { text: prompt });
let initial_prompt_task_id = conversation
.submit(Op::UserTurn {
items,
cwd: default_cwd,
approval_policy: default_approval_policy,
sandbox_policy: default_sandbox_policy,
model: default_model,
effort: default_effort,
summary: default_summary,
final_output_json_schema: output_schema,
})
.await?;
info!("Sent prompt with event ID: {initial_prompt_task_id}");
let _initial_prompt_task_id = match &command {
Some(ExecCommand::Review(args)) => {
let append_to_original_thread = true;
let target = if let Some(custom) = args.prompt.as_deref() {
let text = if custom == "-" {
read_prompt_from_stdin_or_exit(true)
} else {
custom.to_string()
};
ReviewTarget::Custom { instructions: text }
} else if let Some(branch) = &args.branch {
ReviewTarget::BaseBranch {
branch: branch.clone(),
}
} else if let Some(sha) = &args.commit {
// Try to enrich with subject; fall back gracefully.
let mut title: Option<String> = None;
let entries = codex_core::git_info::recent_commits(&default_cwd, 200).await;
for entry in entries {
if entry.sha.starts_with(sha) {
if !entry.subject.trim().is_empty() {
title = Some(entry.subject);
}
break;
}
}
ReviewTarget::Commit {
sha: sha.clone(),
title,
}
} else {
// Default to reviewing uncommitted changes if nothing else specified.
ReviewTarget::UncommittedChanges
};
let mut built_request =
review_request_from_target(target, append_to_original_thread)
.map_err(|err| anyhow::anyhow!("invalid review target: {err}"))?;
if let Some(hint_override) = &args.hint {
built_request.review_request.user_facing_hint = hint_override.clone();
}
let id = conversation
.submit(Op::Review {
review_request: built_request.review_request,
})
.await?;
info!("Sent review request with event ID: {id}");
id
}
_ => {
// Package images and prompt into a single user input turn.
let mut items: Vec<UserInput> = images
.into_iter()
.map(|path| UserInput::LocalImage { path })
.collect();
let text = match regular_prompt_opt {
Some(t) => t,
None => read_prompt_from_stdin_or_exit(false),
};
items.push(UserInput::Text { text });
let id = conversation
.submit(Op::UserTurn {
items,
cwd: default_cwd,
approval_policy: default_approval_policy,
sandbox_policy: default_sandbox_policy,
model: default_model,
effort: default_effort,
summary: default_summary,
final_output_json_schema: output_schema,
})
.await?;
info!("Sent prompt with event ID: {id}");
id
}
};
// Run the loop until the task is complete.
// Track whether a fatal error was reported by the server so we can

View File

@@ -5,5 +5,6 @@ mod auth_env;
mod originator;
mod output_schema;
mod resume;
mod review;
mod sandbox;
mod server_error_exit;

View File

@@ -0,0 +1,59 @@
#![cfg(not(target_os = "windows"))]
#![allow(clippy::expect_used, clippy::unwrap_used)]
use core_test_support::responses;
use core_test_support::test_codex_exec::test_codex_exec;
use predicates::prelude::*;
/// Verify that `codex exec review` triggers the review flow and renders
/// a formatted finding block when the reviewer returns a structured JSON result.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn exec_review_uncommitted_renders_findings() -> anyhow::Result<()> {
let test = test_codex_exec();
// Structured review output returned by the reviewer model (as a JSON string).
let review_json = serde_json::json!({
"findings": [
{
"title": "Prefer Stylize helpers",
"body": "Use .dim()/.bold() chaining instead of manual Style where possible.",
"confidence_score": 0.9,
"priority": 1,
"code_location": {
"absolute_file_path": "/tmp/file.rs",
"line_range": {"start": 10, "end": 20}
}
}
],
"overall_correctness": "good",
"overall_explanation": "All good with some improvements suggested.",
"overall_confidence_score": 0.8
})
.to_string();
let server = responses::start_mock_server().await;
let body = responses::sse(vec![
responses::ev_response_created("resp-1"),
responses::ev_assistant_message("m-1", &review_json),
responses::ev_completed("resp-1"),
]);
responses::mount_sse_once(&server, body).await;
test.cmd_with_server(&server)
.arg("--skip-git-repo-check")
.arg("-C")
.arg(test.cwd_path())
.arg("review")
.arg("--uncommitted")
.assert()
.success()
.stderr(predicate::str::contains(
">> Code review started: current changes <<",
))
.stderr(predicate::str::contains(
"- Prefer Stylize helpers — /tmp/file.rs:10-20",
))
.stderr(predicate::str::contains("<< Code review finished >>"));
Ok(())
}

2
codex-rs/tui/config.toml Normal file
View File

@@ -0,0 +1,2 @@
[projects."/Users/jif/code/codex"]
trust_level = "untrusted"

View File

@@ -52,6 +52,7 @@ use codex_core::protocol::ViewImageToolCallEvent;
use codex_core::protocol::WarningEvent;
use codex_core::protocol::WebSearchBeginEvent;
use codex_core::protocol::WebSearchEndEvent;
use codex_core::review_prompts::ReviewTarget;
use codex_protocol::ConversationId;
use codex_protocol::approvals::ElicitationRequestEvent;
use codex_protocol::parse_command::ParsedCommand;
@@ -144,6 +145,15 @@ const RATE_LIMIT_WARNING_THRESHOLDS: [f64; 3] = [75.0, 90.0, 95.0];
const NUDGE_MODEL_SLUG: &str = "gpt-5.1-codex-mini";
const RATE_LIMIT_SWITCH_PROMPT_THRESHOLD: f64 = 90.0;
fn build_review_request(target: ReviewTarget) -> Option<ReviewRequest> {
codex_core::review_prompts::review_request_from_target(target, true)
.map(|built| built.review_request)
.map_err(|err| {
debug!(?err, "invalid review target");
})
.ok()
}
#[derive(Default)]
struct RateLimitWarningState {
secondary_index: usize,
@@ -2808,19 +2818,14 @@ impl ChatWidget {
..Default::default()
});
let uncommitted_request = build_review_request(ReviewTarget::UncommittedChanges);
items.push(SelectionItem {
name: "Review uncommitted changes".to_string(),
actions: vec![Box::new(
move |tx: &AppEventSender| {
tx.send(AppEvent::CodexOp(Op::Review {
review_request: ReviewRequest {
prompt: "Review the current code changes (staged, unstaged, and untracked files) and provide prioritized findings.".to_string(),
user_facing_hint: "current changes".to_string(),
append_to_original_thread: true,
},
}));
},
)],
actions: vec![Box::new(move |tx: &AppEventSender| {
if let Some(review_request) = uncommitted_request.clone() {
tx.send(AppEvent::CodexOp(Op::Review { review_request }));
}
})],
dismiss_on_select: true,
..Default::default()
});
@@ -2867,15 +2872,11 @@ impl ChatWidget {
items.push(SelectionItem {
name: format!("{current_branch} -> {branch}"),
actions: vec![Box::new(move |tx3: &AppEventSender| {
tx3.send(AppEvent::CodexOp(Op::Review {
review_request: ReviewRequest {
prompt: format!(
"Review the code changes against the base branch '{branch}'. Start by finding the merge diff between the current branch and {branch}'s upstream e.g. (`git merge-base HEAD \"$(git rev-parse --abbrev-ref \"{branch}@{{upstream}}\")\"`), then run `git diff` against that SHA to see what changes we would merge into the {branch} branch. Provide prioritized, actionable findings."
),
user_facing_hint: format!("changes against '{branch}'"),
append_to_original_thread: true,
},
}));
if let Some(review_request) = build_review_request(ReviewTarget::BaseBranch {
branch: branch.clone(),
}) {
tx3.send(AppEvent::CodexOp(Op::Review { review_request }));
}
})],
dismiss_on_select: true,
search_value: Some(option),
@@ -2900,23 +2901,17 @@ impl ChatWidget {
for entry in commits {
let subject = entry.subject.clone();
let sha = entry.sha.clone();
let short = sha.chars().take(7).collect::<String>();
let search_val = format!("{subject} {sha}");
items.push(SelectionItem {
name: subject.clone(),
actions: vec![Box::new(move |tx3: &AppEventSender| {
let hint = format!("commit {short}");
let prompt = format!(
"Review the code changes introduced by commit {sha} (\"{subject}\"). Provide prioritized, actionable findings."
);
tx3.send(AppEvent::CodexOp(Op::Review {
review_request: ReviewRequest {
prompt,
user_facing_hint: hint,
append_to_original_thread: true,
},
}));
if let Some(review_request) = build_review_request(ReviewTarget::Commit {
sha: sha.clone(),
title: Some(subject.clone()),
}) {
tx3.send(AppEvent::CodexOp(Op::Review { review_request }));
}
})],
dismiss_on_select: true,
search_value: Some(search_val),
@@ -2945,13 +2940,11 @@ impl ChatWidget {
if trimmed.is_empty() {
return;
}
tx.send(AppEvent::CodexOp(Op::Review {
review_request: ReviewRequest {
prompt: trimmed.clone(),
user_facing_hint: trimmed,
append_to_original_thread: true,
},
}));
if let Some(review_request) = build_review_request(ReviewTarget::Custom {
instructions: trimmed,
}) {
tx.send(AppEvent::CodexOp(Op::Review { review_request }));
}
}),
);
self.bottom_pane.show_view(Box::new(view));
@@ -3151,23 +3144,17 @@ pub(crate) fn show_review_commit_picker_with_entries(
for entry in entries {
let subject = entry.subject.clone();
let sha = entry.sha.clone();
let short = sha.chars().take(7).collect::<String>();
let search_val = format!("{subject} {sha}");
items.push(SelectionItem {
name: subject.clone(),
actions: vec![Box::new(move |tx3: &AppEventSender| {
let hint = format!("commit {short}");
let prompt = format!(
"Review the code changes introduced by commit {sha} (\"{subject}\"). Provide prioritized, actionable findings."
);
tx3.send(AppEvent::CodexOp(Op::Review {
review_request: ReviewRequest {
prompt,
user_facing_hint: hint,
append_to_original_thread: true,
},
}));
if let Some(review_request) = build_review_request(ReviewTarget::Commit {
sha: sha.clone(),
title: Some(subject.clone()),
}) {
tx3.send(AppEvent::CodexOp(Op::Review { review_request }));
}
})],
dismiss_on_select: true,
search_value: Some(search_val),