mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-25 03:32:32 +00:00
feat(exec): route slash commands and render review output
This commit is contained in:
@@ -629,8 +629,64 @@ impl EventProcessor for EventProcessorWithHumanOutput {
|
||||
EventMsg::ShutdownComplete => return CodexStatus::Shutdown,
|
||||
EventMsg::ConversationPath(_) => {}
|
||||
EventMsg::UserMessage(_) => {}
|
||||
EventMsg::EnteredReviewMode(_) => {}
|
||||
EventMsg::ExitedReviewMode(_) => {}
|
||||
EventMsg::EnteredReviewMode(request) => {
|
||||
ts_println!(
|
||||
self,
|
||||
"{} {}",
|
||||
"review".style(self.magenta),
|
||||
"started".style(self.bold),
|
||||
);
|
||||
if let Some(scope) = request
|
||||
.metadata
|
||||
.as_ref()
|
||||
.and_then(|m| m.scope.as_ref())
|
||||
{
|
||||
println!("{} {}", "scope:".style(self.dimmed), scope.style(self.dimmed));
|
||||
}
|
||||
if !request.user_facing_hint.trim().is_empty() {
|
||||
println!("{}", request.user_facing_hint.trim().style(self.dimmed));
|
||||
}
|
||||
}
|
||||
EventMsg::ExitedReviewMode(output_opt) => {
|
||||
ts_println!(
|
||||
self,
|
||||
"{} {}",
|
||||
"review".style(self.magenta),
|
||||
"finished".style(self.bold),
|
||||
);
|
||||
match output_opt {
|
||||
Some(output) => {
|
||||
if !output.overall_explanation.trim().is_empty() {
|
||||
println!("{}", output.overall_explanation.trim());
|
||||
}
|
||||
|
||||
if output.findings.is_empty() {
|
||||
println!("{}", "no findings reported".style(self.dimmed));
|
||||
} else {
|
||||
for (idx, finding) in output.findings.iter().enumerate() {
|
||||
println!(
|
||||
"{} {}",
|
||||
format!("#{}", idx + 1).style(self.bold),
|
||||
finding.title.trim(),
|
||||
);
|
||||
if !finding.body.trim().is_empty() {
|
||||
for line in finding.body.lines() {
|
||||
println!(" {}", line.trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if output.overall_confidence_score > 0.0 {
|
||||
println!(
|
||||
"confidence: {:.1}",
|
||||
output.overall_confidence_score,
|
||||
);
|
||||
}
|
||||
}
|
||||
None => println!("{}", "review ended without results".style(self.dimmed)),
|
||||
}
|
||||
}
|
||||
EventMsg::CompactionCheckpointWarning(_) => {}
|
||||
}
|
||||
CodexStatus::Running
|
||||
|
||||
+44
-7
@@ -2,6 +2,7 @@ mod cli;
|
||||
mod event_processor;
|
||||
mod event_processor_with_human_output;
|
||||
mod event_processor_with_json_output;
|
||||
mod slash;
|
||||
|
||||
pub use cli::Cli;
|
||||
use code_auto_drive_core::start_auto_coordinator;
|
||||
@@ -28,6 +29,7 @@ use code_core::protocol::Event;
|
||||
use code_core::protocol::EventMsg;
|
||||
use code_core::protocol::InputItem;
|
||||
use code_core::protocol::Op;
|
||||
use code_core::protocol::ReviewRequest;
|
||||
use code_core::protocol::TaskCompleteEvent;
|
||||
use code_protocol::models::ContentItem;
|
||||
use code_protocol::models::ResponseItem;
|
||||
@@ -52,6 +54,7 @@ use anyhow::Context;
|
||||
use crate::cli::Command as ExecCommand;
|
||||
use crate::event_processor::CodexStatus;
|
||||
use crate::event_processor::EventProcessor;
|
||||
use crate::slash::{process_exec_slash_command, SlashContext, SlashDispatch};
|
||||
use code_core::{entry_to_rollout_path, SessionCatalog, SessionQuery};
|
||||
|
||||
const AUTO_DRIVE_TEST_SUFFIX: &str = "After planning, but before you start, please ensure you can test the outcome of your changes. Test first to ensure it's failing, then again at the end to ensure it passes. Do not use work arounds or mock code to pass - solve the underlying issue. Create new tests as you work if needed. Once done, clean up your tests unless added to an existing test suite.";
|
||||
@@ -157,11 +160,13 @@ pub async fn run_main(cli: Cli, code_linux_sandbox_exe: Option<PathBuf>) -> anyh
|
||||
*goal = append_auto_drive_test_suffix(goal);
|
||||
}
|
||||
|
||||
let summary_prompt = if let Some(goal) = auto_drive_goal.as_ref() {
|
||||
let mut prompt_to_send = prompt.clone();
|
||||
let mut summary_prompt = if let Some(goal) = auto_drive_goal.as_ref() {
|
||||
format!("/auto {goal}")
|
||||
} else {
|
||||
prompt.clone()
|
||||
};
|
||||
let mut review_request: Option<ReviewRequest> = None;
|
||||
|
||||
let _output_schema = load_output_schema(output_schema_path);
|
||||
|
||||
@@ -248,6 +253,31 @@ pub async fn run_main(cli: Cli, code_linux_sandbox_exe: Option<PathBuf>) -> anyh
|
||||
};
|
||||
|
||||
let config = Config::load_with_cli_overrides(cli_kv_overrides, overrides)?;
|
||||
let slash_context = SlashContext {
|
||||
agents: &config.agents,
|
||||
subagent_commands: &config.subagent_commands,
|
||||
};
|
||||
|
||||
match process_exec_slash_command(prompt_to_send.trim(), slash_context) {
|
||||
Ok(SlashDispatch::NotSlash) => {}
|
||||
Ok(SlashDispatch::ExpandedPrompt { prompt, summary }) => {
|
||||
prompt_to_send = prompt;
|
||||
if auto_drive_goal.is_none() {
|
||||
summary_prompt = summary;
|
||||
}
|
||||
}
|
||||
Ok(SlashDispatch::Review { request, summary }) => {
|
||||
review_request = Some(request);
|
||||
if auto_drive_goal.is_none() {
|
||||
summary_prompt = summary;
|
||||
}
|
||||
}
|
||||
Err(msg) => {
|
||||
eprintln!("{msg}");
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
let stop_on_task_complete = auto_drive_goal.is_none();
|
||||
let mut event_processor: Box<dyn EventProcessor> = if json_mode {
|
||||
Box::new(EventProcessorWithJsonOutput::new(last_message_file.clone()))
|
||||
@@ -472,12 +502,19 @@ pub async fn run_main(cli: Cli, code_linux_sandbox_exe: Option<PathBuf>) -> anyh
|
||||
}
|
||||
|
||||
// Send the prompt.
|
||||
let items: Vec<InputItem> = vec![InputItem::Text { text: prompt }];
|
||||
// Fallback for older core protocol: send only user input items.
|
||||
let initial_prompt_task_id = conversation
|
||||
.submit(Op::UserInput { items })
|
||||
.await?;
|
||||
info!("Sent prompt with event ID: {initial_prompt_task_id}");
|
||||
let _initial_prompt_task_id = if let Some(review_request) = review_request {
|
||||
let event_id = conversation.submit(Op::Review { review_request }).await?;
|
||||
info!("Sent /review with event ID: {event_id}");
|
||||
event_id
|
||||
} else {
|
||||
let items: Vec<InputItem> = vec![InputItem::Text { text: prompt_to_send }];
|
||||
// Fallback for older core protocol: send only user input items.
|
||||
let event_id = conversation
|
||||
.submit(Op::UserInput { items })
|
||||
.await?;
|
||||
info!("Sent prompt with event ID: {event_id}");
|
||||
event_id
|
||||
};
|
||||
|
||||
// Run the loop until the task is complete.
|
||||
// Track whether a fatal error was reported by the server so we can
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
use code_core::config_types::{AgentConfig, SubagentCommandConfig};
|
||||
use code_core::protocol::{ReviewContextMetadata, ReviewRequest};
|
||||
|
||||
use code_core::slash_commands::format_subagent_command;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SlashContext<'a> {
|
||||
pub agents: &'a [AgentConfig],
|
||||
pub subagent_commands: &'a [SubagentCommandConfig],
|
||||
}
|
||||
|
||||
pub enum SlashDispatch {
|
||||
NotSlash,
|
||||
ExpandedPrompt { prompt: String, summary: String },
|
||||
Review { request: ReviewRequest, summary: String },
|
||||
}
|
||||
|
||||
pub fn process_exec_slash_command(message: &str, ctx: SlashContext<'_>) -> Result<SlashDispatch, String> {
|
||||
let trimmed = message.trim();
|
||||
if trimmed.is_empty() {
|
||||
return Ok(SlashDispatch::NotSlash);
|
||||
}
|
||||
|
||||
if !trimmed.starts_with('/') {
|
||||
return Ok(SlashDispatch::NotSlash);
|
||||
}
|
||||
|
||||
let command_portion = trimmed.trim_start_matches('/');
|
||||
let mut parts = command_portion.splitn(2, |c: char| c.is_whitespace());
|
||||
let command_raw = parts.next().unwrap_or("");
|
||||
let args_raw = parts.next().unwrap_or("").trim();
|
||||
let command = command_raw.to_ascii_lowercase();
|
||||
|
||||
if command.is_empty() {
|
||||
return Ok(SlashDispatch::NotSlash);
|
||||
}
|
||||
|
||||
// Allow existing exec auto-drive handling to continue unchanged.
|
||||
if command == "auto" {
|
||||
return Ok(SlashDispatch::NotSlash);
|
||||
}
|
||||
|
||||
match command.as_str() {
|
||||
"plan" | "solve" | "code" => handle_subagent(command.as_str(), args_raw, ctx),
|
||||
"review" => handle_review(args_raw),
|
||||
other => {
|
||||
// Custom subagents
|
||||
if ctx
|
||||
.subagent_commands
|
||||
.iter()
|
||||
.any(|c| c.name.eq_ignore_ascii_case(other))
|
||||
{
|
||||
return handle_subagent(other, args_raw, ctx);
|
||||
}
|
||||
|
||||
Err(format!("Command '/{}' is not supported in exec mode.", other))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_subagent(
|
||||
name: &str,
|
||||
args: &str,
|
||||
ctx: SlashContext<'_>,
|
||||
) -> Result<SlashDispatch, String> {
|
||||
if args.is_empty() {
|
||||
return Err(format!(
|
||||
"Error: /{name} requires a task description. Usage: /{name} <task>",
|
||||
name = name
|
||||
));
|
||||
}
|
||||
|
||||
let formatted = format_subagent_command(name, args, Some(ctx.agents), Some(ctx.subagent_commands));
|
||||
let summary_args = args.replace('\n', " ").trim().to_string();
|
||||
let summary = if summary_args.is_empty() {
|
||||
format!("/{name}")
|
||||
} else {
|
||||
format!("/{name} {summary_args}")
|
||||
};
|
||||
|
||||
Ok(SlashDispatch::ExpandedPrompt {
|
||||
prompt: formatted.prompt,
|
||||
summary,
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_review(args_raw: &str) -> Result<SlashDispatch, String> {
|
||||
let (prompt, hint, metadata) = if args_raw.is_empty() {
|
||||
(
|
||||
"Review the current workspace changes and highlight bugs, regressions, risky patterns, and missing tests before merge.".to_string(),
|
||||
"current workspace changes".to_string(),
|
||||
ReviewContextMetadata {
|
||||
scope: Some("workspace".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
} else {
|
||||
let text = args_raw.trim().to_string();
|
||||
(
|
||||
text.clone(),
|
||||
text.clone(),
|
||||
ReviewContextMetadata {
|
||||
scope: Some("custom".to_string()),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
};
|
||||
|
||||
let summary = if args_raw.is_empty() {
|
||||
"/review".to_string()
|
||||
} else {
|
||||
let hint_clean = hint.replace('\n', " ").trim().to_string();
|
||||
if hint_clean.is_empty() {
|
||||
"/review".to_string()
|
||||
} else {
|
||||
format!("/review {hint_clean}")
|
||||
}
|
||||
};
|
||||
|
||||
Ok(SlashDispatch::Review {
|
||||
request: ReviewRequest {
|
||||
prompt,
|
||||
user_facing_hint: hint,
|
||||
metadata: Some(metadata),
|
||||
},
|
||||
summary,
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use code_core::config_types::{AgentConfig, SubagentCommandConfig};
|
||||
|
||||
fn ctx<'a>(agents: &'a [AgentConfig], subagents: &'a [SubagentCommandConfig]) -> SlashContext<'a> {
|
||||
SlashContext { agents, subagent_commands: subagents }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_expands_to_prompt() {
|
||||
let result = process_exec_slash_command("/plan ship it", ctx(&[], &[])).unwrap();
|
||||
match result {
|
||||
SlashDispatch::ExpandedPrompt { prompt, summary } => {
|
||||
assert!(prompt.contains("Task for /plan"));
|
||||
assert_eq!(summary, "/plan ship it");
|
||||
}
|
||||
_ => panic!("expected expansion"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn custom_subagent_is_supported() {
|
||||
let subagent = SubagentCommandConfig { name: "audit".to_string(), ..Default::default() };
|
||||
let result = process_exec_slash_command("/audit security pass", ctx(&[], &[subagent])).unwrap();
|
||||
match result {
|
||||
SlashDispatch::ExpandedPrompt { prompt, summary } => {
|
||||
assert!(prompt.contains("Task for /audit"));
|
||||
assert_eq!(summary, "/audit security pass");
|
||||
}
|
||||
other => panic!("unexpected result: {:?}", other),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn review_without_args_defaults_to_workspace_scope() {
|
||||
let result = process_exec_slash_command("/review", ctx(&[], &[])).unwrap();
|
||||
match result {
|
||||
SlashDispatch::Review { request, summary } => {
|
||||
assert_eq!(summary, "/review");
|
||||
assert_eq!(request.user_facing_hint, "current workspace changes");
|
||||
assert_eq!(request.metadata.unwrap().scope.unwrap(), "workspace");
|
||||
}
|
||||
_ => panic!("expected review"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_command_returns_error() {
|
||||
let result = process_exec_slash_command("/theme", ctx(&[], &[]));
|
||||
assert!(result.is_err());
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,6 @@
|
||||
// alternate‑screen mode starts; that file opts‑out locally via `allow`.
|
||||
#![deny(clippy::print_stdout, clippy::print_stderr)]
|
||||
#![deny(clippy::disallowed_methods)]
|
||||
compile_error!("intentional failure for auto-review test");
|
||||
use app::App;
|
||||
use code_common::model_presets::{
|
||||
all_model_presets,
|
||||
|
||||
Reference in New Issue
Block a user