Cargo.lock 🔗
@@ -7077,6 +7077,7 @@ dependencies = [
"picker",
"pretty_assertions",
"project",
+ "prompt_store",
"rand 0.9.2",
"recent_projects",
"remote",
Richard Feldman and Claude Haiku 4.5 created
Closes #38027
Release Notes:
- AI-generated commit messages now respect rules files (e.g.
`AGENTS.md`) if present
---------
Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Cargo.lock | 1
crates/agent/src/agent.rs | 15 -----
crates/git_ui/Cargo.toml | 1
crates/git_ui/src/git_panel.rs | 79 +++++++++++++++++++++++++++++--
crates/prompt_store/src/prompts.rs | 12 ++++
5 files changed, 89 insertions(+), 19 deletions(-)
@@ -7077,6 +7077,7 @@ dependencies = [
"picker",
"pretty_assertions",
"project",
+ "prompt_store",
"rand 0.9.2",
"recent_projects",
"remote",
@@ -33,7 +33,8 @@ use gpui::{
use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
use project::{Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::{
- ProjectContext, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext,
+ ProjectContext, PromptStore, RULES_FILE_NAMES, RulesFileContext, UserRulesContext,
+ WorktreeContext,
};
use serde::{Deserialize, Serialize};
use settings::{LanguageModelSelection, update_settings_file};
@@ -51,18 +52,6 @@ pub struct ProjectSnapshot {
pub timestamp: DateTime<Utc>,
}
-const RULES_FILE_NAMES: [&str; 9] = [
- ".rules",
- ".cursorrules",
- ".windsurfrules",
- ".clinerules",
- ".github/copilot-instructions.md",
- "CLAUDE.md",
- "AGENT.md",
- "AGENTS.md",
- "GEMINI.md",
-];
-
pub struct RulesLoadingError {
pub message: SharedString,
}
@@ -43,6 +43,7 @@ notifications.workspace = true
panel.workspace = true
picker.workspace = true
project.workspace = true
+prompt_store.workspace = true
recent_projects.workspace = true
remote.workspace = true
schemars.workspace = true
@@ -57,6 +57,7 @@ use project::{
git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op},
project_settings::{GitPathStyle, ProjectSettings},
};
+use prompt_store::RULES_FILE_NAMES;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore, StatusStyle};
use std::future::Future;
@@ -71,7 +72,7 @@ use ui::{
prelude::*,
};
use util::paths::PathStyle;
-use util::{ResultExt, TryFutureExt, maybe};
+use util::{ResultExt, TryFutureExt, maybe, rel_path::RelPath};
use workspace::SERIALIZATION_THROTTLE_TIME;
use workspace::{
Workspace,
@@ -2325,6 +2326,56 @@ impl GitPanel {
compressed
}
+ async fn load_project_rules(
+ project: &Entity<Project>,
+ repo_work_dir: &Arc<Path>,
+ cx: &mut AsyncApp,
+ ) -> Option<String> {
+ let rules_path = cx
+ .update(|cx| {
+ for worktree in project.read(cx).worktrees(cx) {
+ let worktree_abs_path = worktree.read(cx).abs_path();
+ if !worktree_abs_path.starts_with(&repo_work_dir) {
+ continue;
+ }
+
+ let worktree_snapshot = worktree.read(cx).snapshot();
+ for rules_name in RULES_FILE_NAMES {
+ if let Ok(rel_path) = RelPath::unix(rules_name) {
+ if let Some(entry) = worktree_snapshot.entry_for_path(rel_path) {
+ if entry.is_file() {
+ return Some(ProjectPath {
+ worktree_id: worktree.read(cx).id(),
+ path: entry.path.clone(),
+ });
+ }
+ }
+ }
+ }
+ }
+ None
+ })
+ .ok()??;
+
+ let buffer = project
+ .update(cx, |project, cx| project.open_buffer(rules_path, cx))
+ .ok()?
+ .await
+ .ok()?;
+
+ let content = buffer
+ .read_with(cx, |buffer, _| buffer.text())
+ .ok()?
+ .trim()
+ .to_string();
+
+ if content.is_empty() {
+ None
+ } else {
+ Some(content)
+ }
+ }
+
/// Generates a commit message using an LLM.
pub fn generate_commit_message(&mut self, cx: &mut Context<Self>) {
if !self.can_commit() || !AgentSettings::get_global(cx).enabled(cx) {
@@ -2352,8 +2403,10 @@ impl GitPanel {
});
let temperature = AgentSettings::temperature_for_model(&model, cx);
+ let project = self.project.clone();
+ let repo_work_dir = repo.read(cx).work_directory_abs_path.clone();
- self.generate_commit_message_task = Some(cx.spawn(async move |this, cx| {
+ self.generate_commit_message_task = Some(cx.spawn(async move |this, mut cx| {
async move {
let _defer = cx.on_drop(&this, |this, _cx| {
this.generate_commit_message_task.take();
@@ -2386,19 +2439,33 @@ impl GitPanel {
const MAX_DIFF_BYTES: usize = 20_000;
diff_text = Self::compress_commit_diff(&diff_text, MAX_DIFF_BYTES);
+ let rules_content = Self::load_project_rules(&project, &repo_work_dir, &mut cx).await;
+
let subject = this.update(cx, |this, cx| {
this.commit_editor.read(cx).text(cx).lines().next().map(ToOwned::to_owned).unwrap_or_default()
})?;
let text_empty = subject.trim().is_empty();
- let content = if text_empty {
- format!("{PROMPT}\nHere are the changes in this commit:\n{diff_text}")
+ const PROMPT: &str = include_str!("commit_message_prompt.txt");
+
+ let rules_section = match &rules_content {
+ Some(rules) => format!(
+ "\n\nThe user has provided the following project rules that you should follow when writing the commit message:\n\
+ <project_rules>\n{rules}\n</project_rules>\n"
+ ),
+ None => String::new(),
+ };
+
+ let subject_section = if text_empty {
+ String::new()
} else {
- format!("{PROMPT}\nHere is the user's subject line:\n{subject}\nHere are the changes in this commit:\n{diff_text}\n")
+ format!("\nHere is the user's subject line:\n{subject}")
};
- const PROMPT: &str = include_str!("commit_message_prompt.txt");
+ let content = format!(
+ "{PROMPT}{rules_section}{subject_section}\nHere are the changes in this commit:\n{diff_text}"
+ );
let request = LanguageModelRequest {
thread_id: None,
@@ -20,6 +20,18 @@ use util::{
use crate::UserPromptId;
+pub const RULES_FILE_NAMES: &[&str] = &[
+ ".rules",
+ ".cursorrules",
+ ".windsurfrules",
+ ".clinerules",
+ ".github/copilot-instructions.md",
+ "CLAUDE.md",
+ "AGENT.md",
+ "AGENTS.md",
+ "GEMINI.md",
+];
+
#[derive(Default, Debug, Clone, Serialize)]
pub struct ProjectContext {
pub worktrees: Vec<WorktreeContext>,