feat(commit-ai): enhance commit message generation and caching

Improve the commit message generation process by adding a caching mechanism and refining the input handling. This change aims to enhance performance and prevent redundant computations when generating commit messages based on staged changes.

- **src-tauri/crates/commit_ai/src/cloud.rs**:
  - Introduced `looks_like_diff_echo` function to detect if the model's output is a diff instead of a commit message.
  - Updated `openai_compatible_request` and `generate_anthropic` to utilize the new function for error handling.

- **src-tauri/crates/commit_ai/src/lib.rs**:
  - Added `LocalGenerationProfile` enum for managing different generation profiles.
  - Implemented caching for generated messages to avoid redundant processing.
  - Updated `generate_commit_message` to incorporate caching logic.

- **src-tauri/src/git.rs**:
  - Added `staged_diff_local` function to handle local profile generation and exclude specific lock files from the diff.
  - Modified `commit_ai_generate` to accept and process the local generation profile.

- **src/App.svelte**:
  - Added `lastLocalAiGeneratedMessage` state to track the last generated message and prevent unnecessary updates.

- **.claude/settings.local.json**:
  - Updated settings to include additional commands for better functionality.
This commit is contained in:
Christoph Brandau
2026-07-03 07:03:35 +02:00
parent 70de45e1ba
commit 791d686c48
9 changed files with 395 additions and 64 deletions
+31 -12
View File
@@ -2,7 +2,7 @@ use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::{build_messages, sanitize_message};
use crate::{build_messages, looks_like_diff_echo, sanitize_message};
// Generous sizing so a detailed body with bullet points isn't cut off.
const DEFAULT_MAX_TOKENS: u32 = 1500;
@@ -56,8 +56,14 @@ async fn openai_compatible_request(
let body = OpenAiRequest {
model: model.to_string(),
messages: vec![
OpenAiMessage { role: "system", content: system },
OpenAiMessage { role: "user", content: user },
OpenAiMessage {
role: "system",
content: system,
},
OpenAiMessage {
role: "user",
content: user,
},
],
temperature: 0.3,
};
@@ -82,17 +88,22 @@ async fn openai_compatible_request(
return Err(format!("API error ({status}): {text}"));
}
let parsed: OpenAiResponse = serde_json::from_str(&text)
.map_err(|err| format!("Could not process response: {err}"))?;
let parsed: OpenAiResponse =
serde_json::from_str(&text).map_err(|err| format!("Could not process response: {err}"))?;
parsed
let message = parsed
.choices
.into_iter()
.next()
.and_then(|choice| choice.message.content)
.map(|content| sanitize_message(&content))
.filter(|content| !content.is_empty())
.ok_or_else(|| "The model did not return a response.".to_string())
.ok_or_else(|| "The model did not return a response.".to_string())?;
if looks_like_diff_echo(&message) {
return Err("The model returned the diff instead of a commit message.".to_string());
}
Ok(message)
}
pub async fn generate_openai(
@@ -168,7 +179,10 @@ pub async fn generate_anthropic(
model: model.to_string(),
max_tokens: DEFAULT_MAX_TOKENS,
system,
messages: vec![AnthropicMessage { role: "user", content: user }],
messages: vec![AnthropicMessage {
role: "user",
content: user,
}],
};
let client = http_client()?;
@@ -190,14 +204,19 @@ pub async fn generate_anthropic(
return Err(format!("API error ({status}): {text}"));
}
let parsed: AnthropicResponse = serde_json::from_str(&text)
.map_err(|err| format!("Could not process response: {err}"))?;
let parsed: AnthropicResponse =
serde_json::from_str(&text).map_err(|err| format!("Could not process response: {err}"))?;
parsed
let message = parsed
.content
.into_iter()
.find_map(|block| block.text)
.map(|text| sanitize_message(&text))
.filter(|text| !text.is_empty())
.ok_or_else(|| "The model did not return a response.".to_string())
.ok_or_else(|| "The model did not return a response.".to_string())?;
if looks_like_diff_echo(&message) {
return Err("The model returned the diff instead of a commit message.".to_string());
}
Ok(message)
}