use std::io::{self, IsTerminal, Read, Write};
use std::process::Command;
use std::path::Path;
use anyhow::{Result, bail};
use tempfile::NamedTempFile;
use zagens_core::chat::LlmClient;
use crate::cli::args::{ApplyArgs, ReviewArgs};
use crate::cli::auto_route_cli::resolve_cli_auto_route;
use crate::cli::context::CliContext;
use crate::client::DeepSeekClient;
use crate::models::{ContentBlock, Message, MessageRequest, SystemPrompt};
use crate::utils::truncate_with_ellipsis;
pub async fn run_review(ctx: &CliContext, args: ReviewArgs) -> Result<()> {
let diff = collect_diff(&args)?;
if diff.trim().is_empty() {
bail!("No diff to review.");
}
let model = args
.model
.or_else(|| ctx.config.default_text_model.clone())
.unwrap_or_else(|| ctx.config.default_model());
let route = resolve_cli_auto_route(&ctx.config, &model, &diff).await;
let model = route.model;
let reasoning_effort = route
.reasoning_effort
.map(|effort| effort.as_setting().to_string());
let system = SystemPrompt::Text(
"You are a senior code reviewer. Focus on bugs, risks, behavioral regressions, and missing tests. \
Provide findings ordered by severity with file references, then open questions, then a brief summary."
.to_string(),
);
let user_prompt =
format!("Review the following diff and provide feedback:\n\n{diff}\n\nEnd of diff.");
let client = DeepSeekClient::new(&ctx.config)?;
let request = MessageRequest {
model: model.clone(),
messages: vec![Message {
role: "user".to_string(),
content: vec![ContentBlock::Text {
text: user_prompt,
cache_control: None,
}],
}],
max_tokens: 4096,
system: Some(system),
tools: None,
tool_choice: None,
metadata: None,
thinking: None,
reasoning_effort,
stream: Some(false),
temperature: Some(0.2),
top_p: Some(0.9),
};
let response = client.create_message(request).await?;
let mut output = String::new();
for block in response.content {
if let ContentBlock::Text { text, .. } = block {
output.push_str(&text);
}
}
if args.json {
println!(
"{}",
serde_json::to_string_pretty(&serde_json::json!({
"mode": "review",
"model": model,
"success": true,
"content": output
}))?
);
} else {
println!("{output}");
}
Ok(())
}
pub fn run_apply(workspace: &Path, args: ApplyArgs) -> Result<()> {
let patch = if let Some(path) = args.patch_file {
std::fs::read_to_string(&path)
.map_err(|e| anyhow::anyhow!("Failed to read patch {}: {e}", path.display()))?
} else {
read_patch_from_stdin()?
};
if patch.trim().is_empty() {
bail!("Patch is empty.");
}
let mut tmp = NamedTempFile::new()?;
tmp.write_all(patch.as_bytes())?;
let tmp_path = tmp.path().to_path_buf();
let output = Command::new("git")
.current_dir(workspace)
.arg("apply")
.arg("--whitespace=nowarn")
.arg(&tmp_path)
.output()
.map_err(|e| anyhow::anyhow!("Failed to run git apply: {e}"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git apply failed: {}", stderr.trim());
}
println!("Applied patch successfully.");
Ok(())
}
fn collect_diff(args: &ReviewArgs) -> Result<String> {
let mut cmd = Command::new("git");
cmd.arg("diff");
if args.staged {
cmd.arg("--cached");
}
if let Some(base) = &args.base {
cmd.arg(format!("{base}...HEAD"));
}
if let Some(path) = &args.path {
cmd.arg("--").arg(path);
}
let output = cmd
.output()
.map_err(|e| anyhow::anyhow!("Failed to run git diff. Is git installed? ({e})"))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("git diff failed: {}", stderr.trim());
}
let mut diff = String::from_utf8_lossy(&output.stdout).to_string();
if diff.len() > args.max_chars {
diff = truncate_with_ellipsis(&diff, args.max_chars, "\n...[truncated]\n");
}
Ok(diff)
}
fn read_patch_from_stdin() -> Result<String> {
let mut stdin = io::stdin();
if stdin.is_terminal() {
bail!("No patch file provided and stdin is empty.");
}
let mut buffer = String::new();
stdin.read_to_string(&mut buffer)?;
Ok(buffer)
}