From f838eb1248e4eac041a200a95dfa070cabf98fa1 Mon Sep 17 00:00:00 2001 From: Bennet Bo Fenner Date: Wed, 27 May 2026 16:08:15 +0200 Subject: [PATCH] git_ui: Respect global `AGENTS.md` when generating commit message (#57827) Release Notes: - agent: Fixed an issue where commit message generation would not respect instructions from global `AGENTS.md` --------- Co-authored-by: Richard Feldman --- crates/agent/src/agent.rs | 2 - crates/agent/src/thread.rs | 4 +- crates/agent_settings/Cargo.toml | 2 +- crates/agent_settings/src/agent_settings.rs | 2 + .../src/user_agents_md.rs | 0 crates/agent_ui/src/agent_panel.rs | 3 +- crates/agent_ui/src/agent_ui.rs | 1 - .../src/conversation_view/thread_view.rs | 3 +- crates/git_ui/src/git_panel.rs | 113 ++++++++++++++---- crates/zed/src/zed.rs | 6 +- 10 files changed, 101 insertions(+), 35 deletions(-) rename crates/{agent => agent_settings}/src/user_agents_md.rs (100%) diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index df3ae91682d..b2561f17072 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -10,7 +10,6 @@ mod thread; mod thread_store; mod tool_permissions; mod tools; -mod user_agents_md; use context_server::ContextServerId; pub use db::*; @@ -23,7 +22,6 @@ pub use thread::*; pub use thread_store::*; pub use tool_permissions::*; pub use tools::*; -pub use user_agents_md::{UserAgentsMd, UserAgentsMdState, init as init_user_agents_md}; use acp_thread::{ AcpThread, AgentModelSelector, AgentSessionInfo, AgentSessionList, AgentSessionListRequest, diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 458f3c1d2a7..593a9f6de4c 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -4,11 +4,11 @@ use crate::{ FindPathTool, FindReferencesTool, GetCodeActionsTool, GoToDefinitionTool, GrepTool, ListDirectoryTool, MovePathTool, ProjectSnapshot, ReadFileTool, RenameTool, SpawnAgentTool, SystemPromptTemplate, Template, Templates, TerminalTool, ToolPermissionDecision, - UpdatePlanTool, UpdateTitleTool, UserAgentsMd, WebSearchTool, WriteFileTool, - decide_permission_from_settings, + UpdatePlanTool, UpdateTitleTool, WebSearchTool, WriteFileTool, decide_permission_from_settings, }; use acp_thread::{MentionUri, UserMessageId}; use action_log::ActionLog; +use agent_settings::UserAgentsMd; use feature_flags::{ FeatureFlagAppExt as _, LspToolFeatureFlag, RenameToolFeatureFlag, UpdatePlanToolFeatureFlag, UpdateTitleToolFeatureFlag, diff --git a/crates/agent_settings/Cargo.toml b/crates/agent_settings/Cargo.toml index 985c0309afb..fd6f18f12af 100644 --- a/crates/agent_settings/Cargo.toml +++ b/crates/agent_settings/Cargo.toml @@ -21,6 +21,7 @@ futures.workspace = true gpui.workspace = true language_model.workspace = true log.workspace = true +paths.workspace = true project.workspace = true regex.workspace = true schemars.workspace = true @@ -31,7 +32,6 @@ util.workspace = true [dev-dependencies] fs.workspace = true gpui = { workspace = true, features = ["test-support"] } -paths.workspace = true serde_json_lenient.workspace = true serde_json.workspace = true diff --git a/crates/agent_settings/src/agent_settings.rs b/crates/agent_settings/src/agent_settings.rs index 7453d60e48d..d7b9d0ed018 100644 --- a/crates/agent_settings/src/agent_settings.rs +++ b/crates/agent_settings/src/agent_settings.rs @@ -1,4 +1,5 @@ mod agent_profile; +mod user_agents_md; use std::path::{Component, Path}; use std::sync::{Arc, LazyLock}; @@ -20,6 +21,7 @@ use settings::{ }; pub use crate::agent_profile::*; +pub use crate::user_agents_md::{UserAgentsMd, UserAgentsMdState, init as init_user_agents_md}; pub const SUMMARIZE_THREAD_PROMPT: &str = include_str!("prompts/summarize_thread_prompt.txt"); pub const SUMMARIZE_THREAD_DETAILED_PROMPT: &str = diff --git a/crates/agent/src/user_agents_md.rs b/crates/agent_settings/src/user_agents_md.rs similarity index 100% rename from crates/agent/src/user_agents_md.rs rename to crates/agent_settings/src/user_agents_md.rs diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index c3bc3088360..d19c8ef0120 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -10,9 +10,10 @@ use std::{ }; use acp_thread::{AcpThread, AcpThreadEvent, MentionUri, ThreadStatus}; -use agent::{ContextServerRegistry, SharedThread, ThreadStore, UserAgentsMd}; +use agent::{ContextServerRegistry, SharedThread, ThreadStore}; use agent_client_protocol::schema as acp; use agent_servers::AgentServer; +use agent_settings::UserAgentsMd; use collections::HashSet; use db::kvp::{Dismissable, KeyValueStore}; use itertools::Itertools; diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index acc45fa0bb5..88f0bcb34b9 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -688,7 +688,6 @@ fn update_command_palette_filter(cx: &mut App) { TypeId::of::(), TypeId::of::(), TypeId::of::(), - TypeId::of::(), TypeId::of::(), TypeId::of::(), TypeId::of::(), diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 01bef136c0d..5fabb8f4569 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -8,7 +8,8 @@ use agent_client_protocol::schema as acp; use std::cell::RefCell; use acp_thread::{ContentBlock, PlanEntry}; -use agent::{SkillLoadingError, SkillLoadingErrorsUpdated, UserAgentsMd}; +use agent::{SkillLoadingError, SkillLoadingErrorsUpdated}; +use agent_settings::UserAgentsMd; use cloud_api_types::{SubmitAgentThreadFeedbackBody, SubmitAgentThreadFeedbackCommentsBody}; use editor::actions::OpenExcerpts; use feature_flags::AcpBetaFeatureFlag; diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index f60737af586..8a7f3c5a134 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -9,7 +9,7 @@ use crate::{branch_picker, picker_prompt, render_remote_button}; use crate::{ git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector, }; -use agent_settings::AgentSettings; +use agent_settings::{AgentSettings, UserAgentsMd}; use alacritty_terminal::vte::ansi; use anyhow::Context as _; use askpass::AskPassDelegate; @@ -2699,6 +2699,40 @@ impl GitPanel { .unwrap_or_else(|| BuiltInPrompt::CommitMessage.default_content().to_string()) } + fn build_commit_message_prompt( + prompt: &str, + user_agents_md: Option<&str>, + rules_content: Option<&str>, + subject: &str, + diff_text: &str, + ) -> String { + let user_agents_md_section = match user_agents_md { + Some(user_agents_md) => format!( + "\n\nThe user has provided the following rules that you should follow when writing the commit message. Project-specific rules may override these instructions when they conflict:\n\ + \n{user_agents_md}\n\n" + ), + None => String::new(), + }; + + let rules_section = match rules_content { + Some(rules) => format!( + "\n\nThe user has provided the following rules specific to this project that you should follow when writing the commit message:\n\ + \n{rules}\n\n" + ), + None => String::new(), + }; + + let subject_section = if subject.trim().is_empty() { + String::new() + } else { + format!("\nHere is the user's subject line:\n{subject}") + }; + + format!( + "{prompt}{user_agents_md_section}{rules_section}{subject_section}\nHere are the changes in this commit:\n{diff_text}" + ) + } + /// Generates a commit message using an LLM. pub fn generate_commit_message(&mut self, cx: &mut Context) { if !self.can_commit() || !AgentSettings::get_global(cx).enabled(cx) { @@ -2730,7 +2764,7 @@ impl GitPanel { let repo_work_dir = repo.read(cx).work_directory_abs_path.clone(); self.generate_commit_message_task = Some(cx.spawn(async move |this, mut cx| { - async move { + async move { let _defer = cx.on_drop(&this, |this, _cx| { this.generate_commit_message_task.take(); }); @@ -2762,32 +2796,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 rules_content = + Self::load_project_rules(&project, &repo_work_dir, &mut cx).await; + let user_agents_md = cx.update(|cx| { + UserAgentsMd::global(cx) + .and_then(|user_agents_md| user_agents_md.content().cloned()) + }); let prompt = Self::load_commit_message_prompt(&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() + this.commit_editor + .read(cx) + .text(cx) + .lines() + .next() + .map(ToOwned::to_owned) + .unwrap_or_default() })?; let text_empty = subject.trim().is_empty(); - 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\ - \n{rules}\n\n" - ), - None => String::new(), - }; - - let subject_section = if text_empty { - String::new() - } else { - format!("\nHere is the user's subject line:\n{subject}") - }; - - let content = format!( - "{prompt}{rules_section}{subject_section}\nHere are the changes in this commit:\n{diff_text}" + let content = Self::build_commit_message_prompt( + &prompt, + user_agents_md.as_deref(), + rules_content.as_deref(), + &subject, + &diff_text, ); let request = LanguageModelRequest { @@ -2816,7 +2851,11 @@ impl GitPanel { this.update(cx, |this, cx| { this.commit_message_buffer(cx).update(cx, |buffer, cx| { let insert_position = buffer.anchor_before(buffer.len()); - buffer.edit([(insert_position..insert_position, "\n")], None, cx) + buffer.edit( + [(insert_position..insert_position, "\n")], + None, + cx, + ) }); })?; } @@ -2826,8 +2865,13 @@ impl GitPanel { Ok(text) => { this.update(cx, |this, cx| { this.commit_message_buffer(cx).update(cx, |buffer, cx| { - let insert_position = buffer.anchor_before(buffer.len()); - buffer.edit([(insert_position..insert_position, text)], None, cx); + let insert_position = + buffer.anchor_before(buffer.len()); + buffer.edit( + [(insert_position..insert_position, text)], + None, + cx, + ); }); })?; } @@ -2845,7 +2889,8 @@ impl GitPanel { anyhow::Ok(()) } - .log_err().await + .log_err() + .await })); } @@ -8726,6 +8771,26 @@ mod tests { assert_eq!(result, expected); } + #[test] + fn test_commit_message_prompt_includes_user_agents_md_before_project_rules() { + let prompt = GitPanel::build_commit_message_prompt( + "Write a commit message.", + Some("Use terse commit messages."), + Some("Use the git_ui prefix."), + "Update generated message", + "diff --git a/file b/file", + ); + + assert!(prompt.contains("Use terse commit messages.")); + assert!(prompt.contains("Use the git_ui prefix.")); + assert!(prompt.contains("Update generated message")); + assert!(prompt.contains("diff --git a/file b/file")); + + let user_agents_md_index = prompt.find("").unwrap(); + let project_rules_index = prompt.find("").unwrap(); + assert!(user_agents_md_index < project_rules_index); + } + #[gpui::test] async fn test_suggest_commit_message(cx: &mut TestAppContext) { init_test(cx); diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 3be90dbc039..a4771d0e08c 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -15,7 +15,7 @@ pub mod visual_tests; #[cfg(target_os = "windows")] pub(crate) mod windows_only_instance; -use agent::{UserAgentsMdState, init_user_agents_md}; +use agent_settings::{UserAgentsMdState, init_user_agents_md}; use agent_ui::AgentDiffToolbar; use anyhow::Context as _; pub use app_menus::*; @@ -1882,8 +1882,8 @@ fn init_cursor_hide_mode(cx: &mut App) { /// Starts watching `~/.config/zed/AGENTS.md` (or the platform equivalent) and /// surfaces any read errors using the same notification UI as settings errors. /// -/// The file itself is loaded into [`agent::UserAgentsMd`] for inclusion in the -/// native agent's system prompt. +/// The file itself is loaded into [`agent_settings::UserAgentsMd`] for inclusion +/// in prompts. pub fn watch_user_agents_md(fs: Arc, cx: &mut App) { struct UserAgentsMdParseError; let notification_id = NotificationId::unique::();