Remove local on-device model support and related IPC commands, consolidating commit-generation to cloud providers and simplifying the AI crate surface. Local-specific types, generation profiles, caching, and the local prompt builder were removed while message sanitization and diff-echo detection were preserved. Also harden repository handling and runtime: unborn HEADs are handled gracefully so empty repos still report files, Git LFS sync is skipped for repositories without commits, and tokio runtime features were enabled. - Remove local model engine, load/status commands, and local profile code - Handle unborn HEAD and skip LFS sync for repos without commits - Enable tokio runtime features and route AI generation to cloud only
120 lines
5.4 KiB
Rust
120 lines
5.4 KiB
Rust
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 (<type>(<scope>): <subject>) 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))
|
|
}
|