chore: review everywhere (#7444)
This commit is contained in:
parent
85e2fabc9f
commit
4b78e2ab09
14 changed files with 560 additions and 210 deletions
|
|
@ -62,6 +62,7 @@ use codex_core::protocol::ReviewDecision;
|
|||
use codex_core::protocol::TokenCountEvent;
|
||||
use codex_core::protocol::TurnDiffEvent;
|
||||
use codex_core::review_format::format_review_findings_block;
|
||||
use codex_core::review_prompts;
|
||||
use codex_protocol::ConversationId;
|
||||
use codex_protocol::plan_tool::UpdatePlanArgs;
|
||||
use codex_protocol::protocol::ReviewOutputEvent;
|
||||
|
|
@ -374,7 +375,9 @@ pub(crate) async fn apply_bespoke_event_handling(
|
|||
.await;
|
||||
}
|
||||
EventMsg::EnteredReviewMode(review_request) => {
|
||||
let review = review_request.user_facing_hint;
|
||||
let review = review_request
|
||||
.user_facing_hint
|
||||
.unwrap_or_else(|| review_prompts::user_facing_hint(&review_request.target));
|
||||
let item = ThreadItem::EnteredReviewMode {
|
||||
id: event_turn_id.clone(),
|
||||
review,
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ use codex_app_server_protocol::ResumeConversationResponse;
|
|||
use codex_app_server_protocol::ReviewDelivery as ApiReviewDelivery;
|
||||
use codex_app_server_protocol::ReviewStartParams;
|
||||
use codex_app_server_protocol::ReviewStartResponse;
|
||||
use codex_app_server_protocol::ReviewTarget;
|
||||
use codex_app_server_protocol::ReviewTarget as ApiReviewTarget;
|
||||
use codex_app_server_protocol::SandboxMode;
|
||||
use codex_app_server_protocol::SendUserMessageParams;
|
||||
use codex_app_server_protocol::SendUserMessageResponse;
|
||||
|
|
@ -124,6 +124,7 @@ use codex_core::protocol::EventMsg;
|
|||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::ReviewDelivery as CoreReviewDelivery;
|
||||
use codex_core::protocol::ReviewRequest;
|
||||
use codex_core::protocol::ReviewTarget as CoreReviewTarget;
|
||||
use codex_core::protocol::SessionConfiguredEvent;
|
||||
use codex_core::read_head_for_summary;
|
||||
use codex_feedback::CodexFeedback;
|
||||
|
|
@ -255,7 +256,7 @@ impl CodexMessageProcessor {
|
|||
}
|
||||
|
||||
fn review_request_from_target(
|
||||
target: ReviewTarget,
|
||||
target: ApiReviewTarget,
|
||||
) -> Result<(ReviewRequest, String), JSONRPCErrorError> {
|
||||
fn invalid_request(message: String) -> JSONRPCErrorError {
|
||||
JSONRPCErrorError {
|
||||
|
|
@ -265,73 +266,52 @@ impl CodexMessageProcessor {
|
|||
}
|
||||
}
|
||||
|
||||
match target {
|
||||
// TODO(jif) those messages will be extracted in a follow-up PR.
|
||||
ReviewTarget::UncommittedChanges => Ok((
|
||||
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(),
|
||||
},
|
||||
"Review uncommitted changes".to_string(),
|
||||
)),
|
||||
ReviewTarget::BaseBranch { branch } => {
|
||||
let cleaned_target = match target {
|
||||
ApiReviewTarget::UncommittedChanges => ApiReviewTarget::UncommittedChanges,
|
||||
ApiReviewTarget::BaseBranch { branch } => {
|
||||
let branch = branch.trim().to_string();
|
||||
if branch.is_empty() {
|
||||
return Err(invalid_request("branch must not be empty".to_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}'");
|
||||
let display = format!("Review changes against base branch '{branch}'");
|
||||
Ok((
|
||||
ReviewRequest {
|
||||
prompt,
|
||||
user_facing_hint: hint,
|
||||
},
|
||||
display,
|
||||
))
|
||||
ApiReviewTarget::BaseBranch { branch }
|
||||
}
|
||||
ReviewTarget::Commit { sha, title } => {
|
||||
ApiReviewTarget::Commit { sha, title } => {
|
||||
let sha = sha.trim().to_string();
|
||||
if sha.is_empty() {
|
||||
return Err(invalid_request("sha must not be empty".to_string()));
|
||||
}
|
||||
let brief_title = title
|
||||
let title = title
|
||||
.map(|t| t.trim().to_string())
|
||||
.filter(|t| !t.is_empty());
|
||||
let prompt = if let Some(title) = brief_title.clone() {
|
||||
format!("Review the code changes introduced by commit {sha} (\"{title}\"). Provide prioritized, actionable findings.")
|
||||
} else {
|
||||
format!("Review the code changes introduced by commit {sha}. Provide prioritized, actionable findings.")
|
||||
};
|
||||
let short_sha = sha.chars().take(7).collect::<String>();
|
||||
let hint = format!("commit {short_sha}");
|
||||
let display = if let Some(title) = brief_title {
|
||||
format!("Review commit {short_sha}: {title}")
|
||||
} else {
|
||||
format!("Review commit {short_sha}")
|
||||
};
|
||||
Ok((
|
||||
ReviewRequest {
|
||||
prompt,
|
||||
user_facing_hint: hint,
|
||||
},
|
||||
display,
|
||||
))
|
||||
ApiReviewTarget::Commit { sha, title }
|
||||
}
|
||||
ReviewTarget::Custom { instructions } => {
|
||||
ApiReviewTarget::Custom { instructions } => {
|
||||
let trimmed = instructions.trim().to_string();
|
||||
if trimmed.is_empty() {
|
||||
return Err(invalid_request("instructions must not be empty".to_string()));
|
||||
return Err(invalid_request(
|
||||
"instructions must not be empty".to_string(),
|
||||
));
|
||||
}
|
||||
ApiReviewTarget::Custom {
|
||||
instructions: trimmed,
|
||||
}
|
||||
Ok((
|
||||
ReviewRequest {
|
||||
prompt: trimmed.clone(),
|
||||
user_facing_hint: trimmed.clone(),
|
||||
},
|
||||
trimmed,
|
||||
))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let core_target = match cleaned_target {
|
||||
ApiReviewTarget::UncommittedChanges => CoreReviewTarget::UncommittedChanges,
|
||||
ApiReviewTarget::BaseBranch { branch } => CoreReviewTarget::BaseBranch { branch },
|
||||
ApiReviewTarget::Commit { sha, title } => CoreReviewTarget::Commit { sha, title },
|
||||
ApiReviewTarget::Custom { instructions } => CoreReviewTarget::Custom { instructions },
|
||||
};
|
||||
|
||||
let hint = codex_core::review_prompts::user_facing_hint(&core_target);
|
||||
let review_request = ReviewRequest {
|
||||
target: core_target,
|
||||
user_facing_hint: Some(hint.clone()),
|
||||
};
|
||||
|
||||
Ok((review_request, hint))
|
||||
}
|
||||
|
||||
pub async fn process_request(&mut self, request: ClientRequest) {
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ async fn review_start_runs_review_turn_and_emits_code_review_item() -> Result<()
|
|||
match started.item {
|
||||
ThreadItem::EnteredReviewMode { id, review } => {
|
||||
assert_eq!(id, turn_id);
|
||||
assert_eq!(review, "commit 1234567");
|
||||
assert_eq!(review, "commit 1234567: Tidy UI colors");
|
||||
saw_entered_review_mode = true;
|
||||
break;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ use codex_cli::login::run_logout;
|
|||
use codex_cloud_tasks::Cli as CloudTasksCli;
|
||||
use codex_common::CliConfigOverrides;
|
||||
use codex_exec::Cli as ExecCli;
|
||||
use codex_exec::Command as ExecCommand;
|
||||
use codex_exec::ReviewArgs;
|
||||
use codex_execpolicy::ExecPolicyCheckCommand;
|
||||
use codex_responses_api_proxy::Args as ResponsesApiProxyArgs;
|
||||
use codex_tui::AppExitInfo;
|
||||
|
|
@ -72,6 +74,9 @@ enum Subcommand {
|
|||
#[clap(visible_alias = "e")]
|
||||
Exec(ExecCli),
|
||||
|
||||
/// Run a code review non-interactively.
|
||||
Review(ReviewArgs),
|
||||
|
||||
/// Manage login.
|
||||
Login(LoginCommand),
|
||||
|
||||
|
|
@ -449,6 +454,15 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
|
|||
);
|
||||
codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?;
|
||||
}
|
||||
Some(Subcommand::Review(review_args)) => {
|
||||
let mut exec_cli = ExecCli::try_parse_from(["codex", "exec"])?;
|
||||
exec_cli.command = Some(ExecCommand::Review(review_args));
|
||||
prepend_config_flags(
|
||||
&mut exec_cli.config_overrides,
|
||||
root_config_overrides.clone(),
|
||||
);
|
||||
codex_exec::run_main(exec_cli, codex_linux_sandbox_exe).await?;
|
||||
}
|
||||
Some(Subcommand::McpServer) => {
|
||||
codex_mcp_server::run_main(codex_linux_sandbox_exe, root_config_overrides).await?;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1476,6 +1476,7 @@ mod handlers {
|
|||
use crate::codex::spawn_review_thread;
|
||||
use crate::config::Config;
|
||||
use crate::mcp::auth::compute_auth_statuses;
|
||||
use crate::review_prompts::resolve_review_request;
|
||||
use crate::tasks::CompactTask;
|
||||
use crate::tasks::RegularTask;
|
||||
use crate::tasks::UndoTask;
|
||||
|
|
@ -1784,14 +1785,28 @@ mod handlers {
|
|||
let turn_context = sess
|
||||
.new_turn_with_sub_id(sub_id.clone(), SessionSettingsUpdate::default())
|
||||
.await;
|
||||
spawn_review_thread(
|
||||
Arc::clone(sess),
|
||||
Arc::clone(config),
|
||||
turn_context.clone(),
|
||||
sub_id,
|
||||
review_request,
|
||||
)
|
||||
.await;
|
||||
match resolve_review_request(review_request, config.cwd.as_path()) {
|
||||
Ok(resolved) => {
|
||||
spawn_review_thread(
|
||||
Arc::clone(sess),
|
||||
Arc::clone(config),
|
||||
turn_context.clone(),
|
||||
sub_id,
|
||||
resolved,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Err(err) => {
|
||||
let event = Event {
|
||||
id: sub_id,
|
||||
msg: EventMsg::Error(ErrorEvent {
|
||||
message: err.to_string(),
|
||||
codex_error_info: Some(CodexErrorInfo::Other),
|
||||
}),
|
||||
};
|
||||
sess.send_event(&turn_context, event.msg).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -1801,7 +1816,7 @@ async fn spawn_review_thread(
|
|||
config: Arc<Config>,
|
||||
parent_turn_context: Arc<TurnContext>,
|
||||
sub_id: String,
|
||||
review_request: ReviewRequest,
|
||||
resolved: crate::review_prompts::ResolvedReviewRequest,
|
||||
) {
|
||||
let model = config.review_model.clone();
|
||||
let review_model_family = find_family_for_model(&model)
|
||||
|
|
@ -1817,7 +1832,7 @@ async fn spawn_review_thread(
|
|||
});
|
||||
|
||||
let base_instructions = REVIEW_PROMPT.to_string();
|
||||
let review_prompt = review_request.prompt.clone();
|
||||
let review_prompt = resolved.prompt.clone();
|
||||
let provider = parent_turn_context.client.get_provider();
|
||||
let auth_manager = parent_turn_context.client.get_auth_manager();
|
||||
let model_family = review_model_family.clone();
|
||||
|
|
@ -1879,6 +1894,10 @@ async fn spawn_review_thread(
|
|||
sess.spawn_task(tc.clone(), input, ReviewTask::new()).await;
|
||||
|
||||
// Announce entering review mode so UIs can switch modes.
|
||||
let review_request = ReviewRequest {
|
||||
target: resolved.target,
|
||||
user_facing_hint: Some(resolved.user_facing_hint),
|
||||
};
|
||||
sess.send_event(&tc, EventMsg::EnteredReviewMode(review_request))
|
||||
.await;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
93
codex-rs/core/src/review_prompts.rs
Normal file
93
codex-rs/core/src/review_prompts.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
use codex_git::merge_base_with_head;
|
||||
use codex_protocol::protocol::ReviewRequest;
|
||||
use codex_protocol::protocol::ReviewTarget;
|
||||
use std::path::Path;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ResolvedReviewRequest {
|
||||
pub target: ReviewTarget,
|
||||
pub prompt: String,
|
||||
pub user_facing_hint: String,
|
||||
}
|
||||
|
||||
const UNCOMMITTED_PROMPT: &str = "Review the current code changes (staged, unstaged, and untracked files) and provide prioritized findings.";
|
||||
|
||||
const BASE_BRANCH_PROMPT_BACKUP: &str = "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.";
|
||||
const BASE_BRANCH_PROMPT: &str = "Review the code changes against the base branch '{baseBranch}'. The merge base commit for this comparison is {mergeBaseSha}. Run `git diff {mergeBaseSha}` to inspect the changes relative to {baseBranch}. Provide prioritized, actionable findings.";
|
||||
|
||||
const COMMIT_PROMPT_WITH_TITLE: &str = "Review the code changes introduced by commit {sha} (\"{title}\"). Provide prioritized, actionable findings.";
|
||||
const COMMIT_PROMPT: &str =
|
||||
"Review the code changes introduced by commit {sha}. Provide prioritized, actionable findings.";
|
||||
|
||||
pub fn resolve_review_request(
|
||||
request: ReviewRequest,
|
||||
cwd: &Path,
|
||||
) -> anyhow::Result<ResolvedReviewRequest> {
|
||||
let target = request.target;
|
||||
let prompt = review_prompt(&target, cwd)?;
|
||||
let user_facing_hint = request
|
||||
.user_facing_hint
|
||||
.unwrap_or_else(|| user_facing_hint(&target));
|
||||
|
||||
Ok(ResolvedReviewRequest {
|
||||
target,
|
||||
prompt,
|
||||
user_facing_hint,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn review_prompt(target: &ReviewTarget, cwd: &Path) -> anyhow::Result<String> {
|
||||
match target {
|
||||
ReviewTarget::UncommittedChanges => Ok(UNCOMMITTED_PROMPT.to_string()),
|
||||
ReviewTarget::BaseBranch { branch } => {
|
||||
if let Some(commit) = merge_base_with_head(cwd, branch)? {
|
||||
Ok(BASE_BRANCH_PROMPT
|
||||
.replace("{baseBranch}", branch)
|
||||
.replace("{mergeBaseSha}", &commit))
|
||||
} else {
|
||||
Ok(BASE_BRANCH_PROMPT_BACKUP.replace("{branch}", branch))
|
||||
}
|
||||
}
|
||||
ReviewTarget::Commit { sha, title } => {
|
||||
if let Some(title) = title {
|
||||
Ok(COMMIT_PROMPT_WITH_TITLE
|
||||
.replace("{sha}", sha)
|
||||
.replace("{title}", title))
|
||||
} else {
|
||||
Ok(COMMIT_PROMPT.replace("{sha}", sha))
|
||||
}
|
||||
}
|
||||
ReviewTarget::Custom { instructions } => {
|
||||
let prompt = instructions.trim();
|
||||
if prompt.is_empty() {
|
||||
anyhow::bail!("Review prompt cannot be empty");
|
||||
}
|
||||
Ok(prompt.to_string())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn user_facing_hint(target: &ReviewTarget) -> String {
|
||||
match target {
|
||||
ReviewTarget::UncommittedChanges => "current changes".to_string(),
|
||||
ReviewTarget::BaseBranch { branch } => format!("changes against '{branch}'"),
|
||||
ReviewTarget::Commit { sha, title } => {
|
||||
let short_sha: String = sha.chars().take(7).collect();
|
||||
if let Some(title) = title {
|
||||
format!("commit {short_sha}: {title}")
|
||||
} else {
|
||||
format!("commit {short_sha}")
|
||||
}
|
||||
}
|
||||
ReviewTarget::Custom { instructions } => instructions.trim().to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResolvedReviewRequest> for ReviewRequest {
|
||||
fn from(resolved: ResolvedReviewRequest) -> Self {
|
||||
ReviewRequest {
|
||||
target: resolved.target,
|
||||
user_facing_hint: Some(resolved.user_facing_hint),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ use codex_core::protocol::EventMsg;
|
|||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::ReviewDecision;
|
||||
use codex_core::protocol::ReviewRequest;
|
||||
use codex_core::protocol::ReviewTarget;
|
||||
use codex_core::protocol::SandboxPolicy;
|
||||
use core_test_support::responses::ev_apply_patch_function_call;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
|
|
@ -68,8 +69,10 @@ async fn codex_delegate_forwards_exec_approval_and_proceeds_on_approval() {
|
|||
test.codex
|
||||
.submit(Op::Review {
|
||||
review_request: ReviewRequest {
|
||||
prompt: "Please review".to_string(),
|
||||
user_facing_hint: "review".to_string(),
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "Please review".to_string(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
|
|
@ -143,8 +146,10 @@ async fn codex_delegate_forwards_patch_approval_and_proceeds_on_decision() {
|
|||
test.codex
|
||||
.submit(Op::Review {
|
||||
review_request: ReviewRequest {
|
||||
prompt: "Please review".to_string(),
|
||||
user_facing_hint: "review".to_string(),
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "Please review".to_string(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
|
|
@ -197,8 +202,10 @@ async fn codex_delegate_ignores_legacy_deltas() {
|
|||
test.codex
|
||||
.submit(Op::Review {
|
||||
review_request: ReviewRequest {
|
||||
prompt: "Please review".to_string(),
|
||||
user_facing_hint: "review".to_string(),
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "Please review".to_string(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ use codex_core::protocol::ReviewFinding;
|
|||
use codex_core::protocol::ReviewLineRange;
|
||||
use codex_core::protocol::ReviewOutputEvent;
|
||||
use codex_core::protocol::ReviewRequest;
|
||||
use codex_core::protocol::ReviewTarget;
|
||||
use codex_core::protocol::RolloutItem;
|
||||
use codex_core::protocol::RolloutLine;
|
||||
use codex_core::review_format::render_review_output_text;
|
||||
|
|
@ -81,8 +82,10 @@ async fn review_op_emits_lifecycle_and_review_output() {
|
|||
codex
|
||||
.submit(Op::Review {
|
||||
review_request: ReviewRequest {
|
||||
prompt: "Please review my changes".to_string(),
|
||||
user_facing_hint: "my changes".to_string(),
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "Please review my changes".to_string(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
|
|
@ -199,8 +202,10 @@ async fn review_op_with_plain_text_emits_review_fallback() {
|
|||
codex
|
||||
.submit(Op::Review {
|
||||
review_request: ReviewRequest {
|
||||
prompt: "Plain text review".to_string(),
|
||||
user_facing_hint: "plain text review".to_string(),
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "Plain text review".to_string(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
|
|
@ -257,8 +262,10 @@ async fn review_filters_agent_message_related_events() {
|
|||
codex
|
||||
.submit(Op::Review {
|
||||
review_request: ReviewRequest {
|
||||
prompt: "Filter streaming events".to_string(),
|
||||
user_facing_hint: "Filter streaming events".to_string(),
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "Filter streaming events".to_string(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
|
|
@ -336,8 +343,10 @@ async fn review_does_not_emit_agent_message_on_structured_output() {
|
|||
codex
|
||||
.submit(Op::Review {
|
||||
review_request: ReviewRequest {
|
||||
prompt: "check structured".to_string(),
|
||||
user_facing_hint: "check structured".to_string(),
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "check structured".to_string(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
|
|
@ -393,8 +402,10 @@ async fn review_uses_custom_review_model_from_config() {
|
|||
codex
|
||||
.submit(Op::Review {
|
||||
review_request: ReviewRequest {
|
||||
prompt: "use custom model".to_string(),
|
||||
user_facing_hint: "use custom model".to_string(),
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "use custom model".to_string(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
|
|
@ -510,8 +521,10 @@ async fn review_input_isolated_from_parent_history() {
|
|||
codex
|
||||
.submit(Op::Review {
|
||||
review_request: ReviewRequest {
|
||||
prompt: review_prompt.clone(),
|
||||
user_facing_hint: review_prompt.clone(),
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: review_prompt.clone(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
|
|
@ -621,8 +634,10 @@ async fn review_history_surfaces_in_parent_session() {
|
|||
codex
|
||||
.submit(Op::Review {
|
||||
review_request: ReviewRequest {
|
||||
prompt: "Start a review".to_string(),
|
||||
user_facing_hint: "Start a review".to_string(),
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "Start a review".to_string(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
})
|
||||
.await
|
||||
|
|
|
|||
|
|
@ -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 against the current repository.
|
||||
Review(ReviewArgs),
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
|
|
@ -109,6 +112,41 @@ pub struct ResumeArgs {
|
|||
pub prompt: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
pub struct ReviewArgs {
|
||||
/// Review staged, unstaged, and untracked changes.
|
||||
#[arg(
|
||||
long = "uncommitted",
|
||||
default_value_t = false,
|
||||
conflicts_with_all = ["base", "commit", "prompt"]
|
||||
)]
|
||||
pub uncommitted: bool,
|
||||
|
||||
/// Review changes against the given base branch.
|
||||
#[arg(
|
||||
long = "base",
|
||||
value_name = "BRANCH",
|
||||
conflicts_with_all = ["uncommitted", "commit", "prompt"]
|
||||
)]
|
||||
pub base: Option<String>,
|
||||
|
||||
/// Review the changes introduced by a commit.
|
||||
#[arg(
|
||||
long = "commit",
|
||||
value_name = "SHA",
|
||||
conflicts_with_all = ["uncommitted", "base", "prompt"]
|
||||
)]
|
||||
pub commit: Option<String>,
|
||||
|
||||
/// Optional commit title to display in the review summary.
|
||||
#[arg(long = "title", value_name = "TITLE", requires = "commit")]
|
||||
pub commit_title: 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 {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ pub mod event_processor_with_jsonl_output;
|
|||
pub mod exec_events;
|
||||
|
||||
pub use cli::Cli;
|
||||
pub use cli::Command;
|
||||
pub use cli::ReviewArgs;
|
||||
use codex_common::oss::ensure_oss_provider_ready;
|
||||
use codex_common::oss::get_default_model_for_oss_provider;
|
||||
use codex_core::AuthManager;
|
||||
|
|
@ -29,6 +31,8 @@ use codex_core::protocol::AskForApproval;
|
|||
use codex_core::protocol::Event;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::protocol::ReviewRequest;
|
||||
use codex_core::protocol::ReviewTarget;
|
||||
use codex_core::protocol::SessionSource;
|
||||
use codex_protocol::approvals::ElicitationAction;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
|
|
@ -53,6 +57,16 @@ use crate::event_processor::EventProcessor;
|
|||
use codex_core::default_client::set_default_originator;
|
||||
use codex_core::find_conversation_path_by_id_str;
|
||||
|
||||
enum InitialOperation {
|
||||
UserTurn {
|
||||
items: Vec<UserInput>,
|
||||
output_schema: Option<Value>,
|
||||
},
|
||||
Review {
|
||||
review_request: ReviewRequest,
|
||||
},
|
||||
}
|
||||
|
||||
pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()> {
|
||||
if let Err(err) = set_default_originator("codex_exec".to_string()) {
|
||||
tracing::warn!(?err, "Failed to set codex exec originator override {err:?}");
|
||||
|
|
@ -79,64 +93,6 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option<PathBuf>) -> any
|
|||
config_overrides,
|
||||
} = cli;
|
||||
|
||||
// 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
|
||||
// when the Resume subcommand did not provide its own prompt.
|
||||
Some(ExecCommand::Resume(args)) => {
|
||||
let resume_prompt = args
|
||||
.prompt
|
||||
.clone()
|
||||
// When using `resume --last <PROMPT>`, clap still parses the first positional
|
||||
// as `session_id`. Reinterpret it as the prompt so the flag works with JSON mode.
|
||||
.or_else(|| {
|
||||
if args.last {
|
||||
args.session_id.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
resume_prompt.or(prompt)
|
||||
}
|
||||
None => prompt,
|
||||
};
|
||||
|
||||
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 `-`.
|
||||
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...");
|
||||
}
|
||||
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
|
||||
}
|
||||
};
|
||||
|
||||
let output_schema = load_output_schema(output_schema_path);
|
||||
|
||||
let (stdout_with_ansi, stderr_with_ansi) = match color {
|
||||
cli::Color::Always => (true, true),
|
||||
cli::Color::Never => (false, false),
|
||||
|
|
@ -329,8 +285,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.as_ref() {
|
||||
let resume_path = resolve_resume_path(&config, args).await?;
|
||||
|
||||
if let Some(path) = resume_path {
|
||||
conversation_manager
|
||||
|
|
@ -346,9 +302,64 @@ 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
|
||||
let (initial_operation, prompt_summary) = match (command, prompt, images) {
|
||||
(Some(ExecCommand::Review(review_cli)), _, _) => {
|
||||
let review_request = build_review_request(review_cli)?;
|
||||
let summary = codex_core::review_prompts::user_facing_hint(&review_request.target);
|
||||
(InitialOperation::Review { review_request }, summary)
|
||||
}
|
||||
(Some(ExecCommand::Resume(args)), root_prompt, imgs) => {
|
||||
let prompt_arg = args
|
||||
.prompt
|
||||
.clone()
|
||||
.or_else(|| {
|
||||
if args.last {
|
||||
args.session_id.clone()
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.or(root_prompt);
|
||||
let prompt_text = resolve_prompt(prompt_arg);
|
||||
let mut items: Vec<UserInput> = imgs
|
||||
.into_iter()
|
||||
.map(|path| UserInput::LocalImage { path })
|
||||
.collect();
|
||||
items.push(UserInput::Text {
|
||||
text: prompt_text.clone(),
|
||||
});
|
||||
let output_schema = load_output_schema(output_schema_path.clone());
|
||||
(
|
||||
InitialOperation::UserTurn {
|
||||
items,
|
||||
output_schema,
|
||||
},
|
||||
prompt_text,
|
||||
)
|
||||
}
|
||||
(None, root_prompt, imgs) => {
|
||||
let prompt_text = resolve_prompt(root_prompt);
|
||||
let mut items: Vec<UserInput> = imgs
|
||||
.into_iter()
|
||||
.map(|path| UserInput::LocalImage { path })
|
||||
.collect();
|
||||
items.push(UserInput::Text {
|
||||
text: prompt_text.clone(),
|
||||
});
|
||||
let output_schema = load_output_schema(output_schema_path);
|
||||
(
|
||||
InitialOperation::UserTurn {
|
||||
items,
|
||||
output_schema,
|
||||
},
|
||||
prompt_text,
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// Print the effective configuration and initial request so users can see what Codex
|
||||
// is using.
|
||||
event_processor.print_config_summary(&config, &prompt, &session_configured);
|
||||
event_processor.print_config_summary(&config, &prompt_summary, &session_configured);
|
||||
|
||||
info!("Codex initialized with event: {session_configured:?}");
|
||||
|
||||
|
|
@ -391,25 +402,32 @@ 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 {
|
||||
match initial_operation {
|
||||
InitialOperation::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}");
|
||||
output_schema,
|
||||
} => {
|
||||
let 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: {task_id}");
|
||||
task_id
|
||||
}
|
||||
InitialOperation::Review { review_request } => {
|
||||
let task_id = conversation.submit(Op::Review { review_request }).await?;
|
||||
info!("Sent review request with event ID: {task_id}");
|
||||
task_id
|
||||
}
|
||||
};
|
||||
|
||||
// Run the loop until the task is complete.
|
||||
// Track whether a fatal error was reported by the server so we can
|
||||
|
|
@ -503,3 +521,130 @@ fn load_output_schema(path: Option<PathBuf>) -> Option<Value> {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_review_request(args: ReviewArgs) -> anyhow::Result<ReviewRequest> {
|
||||
let target = if args.uncommitted {
|
||||
ReviewTarget::UncommittedChanges
|
||||
} else if let Some(branch) = args.base {
|
||||
ReviewTarget::BaseBranch { branch }
|
||||
} else if let Some(sha) = args.commit {
|
||||
ReviewTarget::Commit {
|
||||
sha,
|
||||
title: args.commit_title,
|
||||
}
|
||||
} else if let Some(prompt_arg) = args.prompt {
|
||||
let prompt = resolve_prompt(Some(prompt_arg)).trim().to_string();
|
||||
if prompt.is_empty() {
|
||||
anyhow::bail!("Review prompt cannot be empty");
|
||||
}
|
||||
ReviewTarget::Custom {
|
||||
instructions: prompt,
|
||||
}
|
||||
} else {
|
||||
anyhow::bail!(
|
||||
"Specify --uncommitted, --base, --commit, or provide custom review instructions"
|
||||
);
|
||||
};
|
||||
|
||||
Ok(ReviewRequest {
|
||||
target,
|
||||
user_facing_hint: None,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn builds_uncommitted_review_request() {
|
||||
let request = build_review_request(ReviewArgs {
|
||||
uncommitted: true,
|
||||
base: None,
|
||||
commit: None,
|
||||
commit_title: None,
|
||||
prompt: None,
|
||||
})
|
||||
.expect("builds uncommitted review request");
|
||||
|
||||
let expected = ReviewRequest {
|
||||
target: ReviewTarget::UncommittedChanges,
|
||||
user_facing_hint: None,
|
||||
};
|
||||
|
||||
assert_eq!(request, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_commit_review_request_with_title() {
|
||||
let request = build_review_request(ReviewArgs {
|
||||
uncommitted: false,
|
||||
base: None,
|
||||
commit: Some("123456789".to_string()),
|
||||
commit_title: Some("Add review command".to_string()),
|
||||
prompt: None,
|
||||
})
|
||||
.expect("builds commit review request");
|
||||
|
||||
let expected = ReviewRequest {
|
||||
target: ReviewTarget::Commit {
|
||||
sha: "123456789".to_string(),
|
||||
title: Some("Add review command".to_string()),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
};
|
||||
|
||||
assert_eq!(request, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn builds_custom_review_request_trims_prompt() {
|
||||
let request = build_review_request(ReviewArgs {
|
||||
uncommitted: false,
|
||||
base: None,
|
||||
commit: None,
|
||||
commit_title: None,
|
||||
prompt: Some(" custom review instructions ".to_string()),
|
||||
})
|
||||
.expect("builds custom review request");
|
||||
|
||||
let expected = ReviewRequest {
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "custom review instructions".to_string(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
};
|
||||
|
||||
assert_eq!(request, expected);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1263,11 +1263,40 @@ pub enum ReviewDelivery {
|
|||
Detached,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema, TS)]
|
||||
#[serde(tag = "type", rename_all = "camelCase")]
|
||||
#[ts(tag = "type")]
|
||||
pub enum ReviewTarget {
|
||||
/// Review the working tree: staged, unstaged, and untracked files.
|
||||
UncommittedChanges,
|
||||
|
||||
/// Review changes between the current branch and the given base branch.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
BaseBranch { branch: String },
|
||||
|
||||
/// Review the changes introduced by a specific commit.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Commit {
|
||||
sha: String,
|
||||
/// Optional human-readable label (e.g., commit subject) for UIs.
|
||||
title: Option<String>,
|
||||
},
|
||||
|
||||
/// Arbitrary instructions provided by the user.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(rename_all = "camelCase")]
|
||||
Custom { instructions: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, JsonSchema, TS)]
|
||||
/// Review request sent to the review session.
|
||||
pub struct ReviewRequest {
|
||||
pub prompt: String,
|
||||
pub user_facing_hint: String,
|
||||
pub target: ReviewTarget,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub user_facing_hint: Option<String>,
|
||||
}
|
||||
|
||||
/// Structured review result produced by a child review session.
|
||||
|
|
|
|||
|
|
@ -40,6 +40,7 @@ use codex_core::protocol::Op;
|
|||
use codex_core::protocol::PatchApplyBeginEvent;
|
||||
use codex_core::protocol::RateLimitSnapshot;
|
||||
use codex_core::protocol::ReviewRequest;
|
||||
use codex_core::protocol::ReviewTarget;
|
||||
use codex_core::protocol::StreamErrorEvent;
|
||||
use codex_core::protocol::TaskCompleteEvent;
|
||||
use codex_core::protocol::TokenUsage;
|
||||
|
|
@ -1812,7 +1813,10 @@ impl ChatWidget {
|
|||
self.pre_review_token_info = Some(self.token_info.clone());
|
||||
}
|
||||
self.is_review_mode = true;
|
||||
let banner = format!(">> Code review started: {} <<", review.user_facing_hint);
|
||||
let hint = review
|
||||
.user_facing_hint
|
||||
.unwrap_or_else(|| codex_core::review_prompts::user_facing_hint(&review.target));
|
||||
let banner = format!(">> Code review started: {hint} <<");
|
||||
self.add_to_history(history_cell::new_review_status_line(banner));
|
||||
self.request_redraw();
|
||||
}
|
||||
|
|
@ -2889,16 +2893,14 @@ impl ChatWidget {
|
|||
|
||||
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(),
|
||||
},
|
||||
}));
|
||||
},
|
||||
)],
|
||||
actions: vec![Box::new(move |tx: &AppEventSender| {
|
||||
tx.send(AppEvent::CodexOp(Op::Review {
|
||||
review_request: ReviewRequest {
|
||||
target: ReviewTarget::UncommittedChanges,
|
||||
user_facing_hint: None,
|
||||
},
|
||||
}));
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
});
|
||||
|
|
@ -2947,10 +2949,10 @@ impl ChatWidget {
|
|||
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}'"),
|
||||
target: ReviewTarget::BaseBranch {
|
||||
branch: branch.clone(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
}));
|
||||
})],
|
||||
|
|
@ -2977,20 +2979,18 @@ 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,
|
||||
target: ReviewTarget::Commit {
|
||||
sha: sha.clone(),
|
||||
title: Some(subject.clone()),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
}));
|
||||
})],
|
||||
|
|
@ -3023,8 +3023,10 @@ impl ChatWidget {
|
|||
}
|
||||
tx.send(AppEvent::CodexOp(Op::Review {
|
||||
review_request: ReviewRequest {
|
||||
prompt: trimmed.clone(),
|
||||
user_facing_hint: trimmed,
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: trimmed,
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
}));
|
||||
}),
|
||||
|
|
@ -3226,20 +3228,18 @@ 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,
|
||||
target: ReviewTarget::Commit {
|
||||
sha: sha.clone(),
|
||||
title: Some(subject.clone()),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
},
|
||||
}));
|
||||
})],
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ use codex_core::protocol::ReviewFinding;
|
|||
use codex_core::protocol::ReviewLineRange;
|
||||
use codex_core::protocol::ReviewOutputEvent;
|
||||
use codex_core::protocol::ReviewRequest;
|
||||
use codex_core::protocol::ReviewTarget;
|
||||
use codex_core::protocol::StreamErrorEvent;
|
||||
use codex_core::protocol::TaskCompleteEvent;
|
||||
use codex_core::protocol::TaskStartedEvent;
|
||||
|
|
@ -153,8 +154,10 @@ fn entered_review_mode_uses_request_hint() {
|
|||
chat.handle_codex_event(Event {
|
||||
id: "review-start".into(),
|
||||
msg: EventMsg::EnteredReviewMode(ReviewRequest {
|
||||
prompt: "Review the latest changes".to_string(),
|
||||
user_facing_hint: "feature branch".to_string(),
|
||||
target: ReviewTarget::BaseBranch {
|
||||
branch: "feature".to_string(),
|
||||
},
|
||||
user_facing_hint: Some("feature branch".to_string()),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -172,8 +175,8 @@ fn entered_review_mode_defaults_to_current_changes_banner() {
|
|||
chat.handle_codex_event(Event {
|
||||
id: "review-start".into(),
|
||||
msg: EventMsg::EnteredReviewMode(ReviewRequest {
|
||||
prompt: "Review the current changes".to_string(),
|
||||
user_facing_hint: "current changes".to_string(),
|
||||
target: ReviewTarget::UncommittedChanges,
|
||||
user_facing_hint: None,
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -239,8 +242,10 @@ fn review_restores_context_window_indicator() {
|
|||
chat.handle_codex_event(Event {
|
||||
id: "review-start".into(),
|
||||
msg: EventMsg::EnteredReviewMode(ReviewRequest {
|
||||
prompt: "Review the latest changes".to_string(),
|
||||
user_facing_hint: "feature branch".to_string(),
|
||||
target: ReviewTarget::BaseBranch {
|
||||
branch: "feature".to_string(),
|
||||
},
|
||||
user_facing_hint: Some("feature branch".to_string()),
|
||||
}),
|
||||
});
|
||||
|
||||
|
|
@ -1312,12 +1317,13 @@ fn custom_prompt_submit_sends_review_op() {
|
|||
match evt {
|
||||
AppEvent::CodexOp(Op::Review { review_request }) => {
|
||||
assert_eq!(
|
||||
review_request.prompt,
|
||||
"please audit dependencies".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
review_request.user_facing_hint,
|
||||
"please audit dependencies".to_string()
|
||||
review_request,
|
||||
ReviewRequest {
|
||||
target: ReviewTarget::Custom {
|
||||
instructions: "please audit dependencies".to_string(),
|
||||
},
|
||||
user_facing_hint: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
other => panic!("unexpected app event: {other:?}"),
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue