mod cloud; pub use cloud::{ generate_anthropic, generate_custom, generate_openai, review_anthropic, review_custom, review_openai, split_anthropic, split_custom, split_openai, }; /// Strip a wrapping code fence and wrapping quotes so the result can go straight into /// the commit-message box. pub(crate) fn sanitize_message(raw: &str) -> String { let mut text = raw.trim().to_string(); if text.starts_with("```") { text = match text.split_once('\n') { // Drop the opening fence line (which may carry a language tag) and the closing fence. Some((_fence, rest)) => rest.trim_end().trim_end_matches("```").trim().to_string(), None => text.trim_matches('`').trim().to_string(), }; } let trimmed = text.trim(); if trimmed.len() >= 2 && trimmed.starts_with('"') && trimmed.ends_with('"') { return trimmed[1..trimmed.len() - 1].trim().to_string(); } trimmed.to_string() } /// Some models echo the prompt's diff sections instead of writing a commit message. /// Catch that so the UI can show a clear error instead of dumping raw diff text into /// the commit-message box. pub(crate) fn looks_like_diff_echo(message: &str) -> bool { let lower = message.to_ascii_lowercase(); lower.contains("diff --git") || lower.contains("staged files:") || lower.contains("staged changes:") || lower.contains("diff stat:") || lower.contains("detailed diff:") || message.lines().any(|line| line.starts_with("@@ ")) } fn truncate_at_char_boundary(input: &str, max_chars: usize) -> String { if input.len() <= max_chars { return input.to_string(); } let mut cut = max_chars; while !input.is_char_boundary(cut) { cut -= 1; } format!("{}\n\n[... diff truncated ...]", &input[..cut]) } pub(crate) fn build_messages(diff: &str, notes: Option<&str>) -> Result<(String, String), String> { if diff.trim().is_empty() { return Err("No staged changes available for a commit message.".to_string()); } // Rough token estimate to keep requests within common context windows. const MAX_CHARS: usize = 24_000; let diff = truncate_at_char_boundary(diff, MAX_CHARS); let system = "You are a tool that writes a Git commit message describing a staged diff. \ Output ONLY the commit message text. Never quote, restate, or paraphrase the diff itself: \ do not include lines starting with 'diff --git', '@@', '+', '-', 'index ', 'Staged files:', \ or 'Diff stat:' anywhere in your answer. \ Format: a Conventional Commits header ((): ) in imperative mood, \ max. 72 characters, then a blank line, then a body. \ The body is required: a short, general paragraph (2-4 sentences) summarizing what changed \ and why at a high level — do NOT enumerate every changed file individually. \ You may optionally add up to 3 bullet points (- ...) afterward, but only for the most \ significant changes overall, never one bullet or heading per file. \ Never use bold text, backticks, or markdown headings for file names. \ Lines in the body max. 72 characters. \ No preamble, no explanation, no code fences, answer in English.\n\n\ Example:\n\ Diff:\n\ diff --git a/src/auth.py b/src/auth.py\n\ +def hash_password(pw):\n\ + return bcrypt.hash(pw)\n\ diff --git a/src/routes.py b/src/routes.py\n\ -if password == stored_password:\n\ +if bcrypt.check(password, stored_password):\n\n\ Commit message:\n\ feat(auth): hash and verify passwords with bcrypt\n\n\ Passwords were previously compared as plain text. This adds a bcrypt-based\n\ hashing helper and updates the login check to verify against the hash\n\ instead of a direct string comparison.\n\n\ - Hash passwords on write, verify with bcrypt on login" .to_string(); let mut user = String::new(); if let Some(n) = notes.filter(|n| !n.trim().is_empty()) { user.push_str(&format!("Developer notes:\n{n}\n\n")); } user.push_str(&format!("Staged diff:\n{diff}")); Ok((system, user)) } pub(crate) fn build_review_messages(diff: &str) -> Result<(String, String), String> { if diff.trim().is_empty() { return Err("No staged changes available for review.".to_string()); } const MAX_CHARS: usize = 36_000; let diff = truncate_at_char_boundary(diff, MAX_CHARS); let system = r#"You are a senior software engineer performing a focused pre-commit review. Review only the supplied staged Git diff. Look for concrete correctness bugs, security issues, data loss, regressions, broken edge cases, unsafe error handling, and meaningful performance or maintainability risks. Do not report formatting preferences or speculative nitpicks. Return ONLY valid JSON with this exact shape: {"summary":"one concise overall assessment","risk":"low|medium|high","findings":[{"severity":"critical|warning|info","title":"short title","description":"clear evidence and impact","file":"path or null","line":123,"suggestion":"specific safe next step"}]} Use the new-file line number from the diff when it is known; otherwise use null. Use null for file when the issue is repository-wide. Maximum 12 findings, ordered critical then warning then info. If no actionable issue exists, return an empty findings array and risk low. Never use markdown, code fences, commentary outside the JSON, or claim that tests were executed."# .to_string(); let user = format!("Staged diff to review:\n{diff}"); Ok((system, user)) }