From 301fc7cd7b3259963efbc60f4dba788b45a6988b Mon Sep 17 00:00:00 2001 From: Michael Sloan Date: Tue, 8 Apr 2025 19:31:56 -0600 Subject: [PATCH] Pull out plain rules file loading code into a new `agent_rules` crate (#28383) Also renames for rules file templated into the system prompt Release Notes: - N/A --- Cargo.lock | 14 ++++ Cargo.toml | 2 + assets/prompts/assistant_system_prompt.hbs | 2 +- crates/agent/Cargo.toml | 1 + crates/agent/src/active_thread.rs | 2 +- crates/agent/src/thread.rs | 86 +++++++--------------- crates/agent_rules/Cargo.toml | 24 ++++++ crates/agent_rules/LICENSE-GPL | 1 + crates/agent_rules/src/agent_rules.rs | 51 +++++++++++++ crates/prompt_store/src/prompts.rs | 6 +- 10 files changed, 125 insertions(+), 64 deletions(-) create mode 100644 crates/agent_rules/Cargo.toml create mode 120000 crates/agent_rules/LICENSE-GPL create mode 100644 crates/agent_rules/src/agent_rules.rs diff --git a/Cargo.lock b/Cargo.lock index e9ed24ff14..4936af8d08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -52,6 +52,7 @@ dependencies = [ name = "agent" version = "0.1.0" dependencies = [ + "agent_rules", "anyhow", "assistant_context_editor", "assistant_settings", @@ -161,6 +162,19 @@ dependencies = [ "workspace-hack", ] +[[package]] +name = "agent_rules" +version = "0.1.0" +dependencies = [ + "anyhow", + "fs", + "gpui", + "indoc", + "prompt_store", + "util", + "worktree", +] + [[package]] name = "ahash" version = "0.7.8" diff --git a/Cargo.toml b/Cargo.toml index ae24db801d..c696c6ebe6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,6 +3,7 @@ resolver = "2" members = [ "crates/activity_indicator", "crates/agent", + "crates/agent_rules", "crates/anthropic", "crates/askpass", "crates/assets", @@ -209,6 +210,7 @@ edition = "2024" activity_indicator = { path = "crates/activity_indicator" } agent = { path = "crates/agent" } +agent_rules = { path = "crates/agent_rules" } ai = { path = "crates/ai" } anthropic = { path = "crates/anthropic" } askpass = { path = "crates/askpass" } diff --git a/assets/prompts/assistant_system_prompt.hbs b/assets/prompts/assistant_system_prompt.hbs index 1462a5e516..7425f9ba60 100644 --- a/assets/prompts/assistant_system_prompt.hbs +++ b/assets/prompts/assistant_system_prompt.hbs @@ -155,7 +155,7 @@ There are rules that apply to these root directories: {{#each worktrees}} {{#if rules_file}} -`{{root_name}}/{{rules_file.rel_path}}`: +`{{root_name}}/{{rules_file.path_in_worktree}}`: `````` {{{rules_file.text}}} diff --git a/crates/agent/Cargo.toml b/crates/agent/Cargo.toml index f72fc60203..deaee66d49 100644 --- a/crates/agent/Cargo.toml +++ b/crates/agent/Cargo.toml @@ -19,6 +19,7 @@ test-support = [ ] [dependencies] +agent_rules.workspace = true anyhow.workspace = true assistant_context_editor.workspace = true assistant_settings.workspace = true diff --git a/crates/agent/src/active_thread.rs b/crates/agent/src/active_thread.rs index 92dc8ab0e0..45bea67f62 100644 --- a/crates/agent/src/active_thread.rs +++ b/crates/agent/src/active_thread.rs @@ -2581,7 +2581,7 @@ impl ActiveThread { let label_text = match rules_files.as_slice() { &[] => return div().into_any(), &[rules_file] => { - format!("Using {:?} file", rules_file.rel_path) + format!("Using {:?} file", rules_file.path_in_worktree) } rules_files => { format!("Using {} rules files", rules_files.len()) diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index 3b5cd98d09..cdd88c714b 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -3,6 +3,7 @@ use std::io::Write; use std::ops::Range; use std::sync::Arc; +use agent_rules::load_worktree_rules_file; use anyhow::{Context as _, Result, anyhow}; use assistant_settings::AssistantSettings; use assistant_tool::{ActionLog, Tool, ToolWorkingSet}; @@ -21,13 +22,11 @@ use language_model::{ }; use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState}; use project::{Project, Worktree}; -use prompt_store::{ - AssistantSystemPromptContext, PromptBuilder, RulesFile, WorktreeInfoForSystemPrompt, -}; +use prompt_store::{AssistantSystemPromptContext, PromptBuilder, WorktreeInfoForSystemPrompt}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Settings; -use util::{ResultExt as _, TryFutureExt as _, maybe, post_inc}; +use util::{ResultExt as _, TryFutureExt as _, post_inc}; use uuid::Uuid; use crate::context::{AssistantContext, ContextId, format_context_as_string}; @@ -854,67 +853,36 @@ impl Thread { let root_name = worktree.root_name().into(); let abs_path = worktree.abs_path(); - // Note that Cline supports `.clinerules` being a directory, but that is not currently - // supported. This doesn't seem to occur often in GitHub repositories. - const RULES_FILE_NAMES: [&'static str; 6] = [ - ".rules", - ".cursorrules", - ".windsurfrules", - ".clinerules", - ".github/copilot-instructions.md", - "CLAUDE.md", - ]; - let selected_rules_file = RULES_FILE_NAMES - .into_iter() - .filter_map(|name| { - worktree - .entry_for_path(name) - .filter(|entry| entry.is_file()) - .map(|entry| (entry.path.clone(), worktree.absolutize(&entry.path))) - }) - .next(); - - if let Some((rel_rules_path, abs_rules_path)) = selected_rules_file { - cx.spawn(async move |_| { - let rules_file_result = maybe!(async move { - let abs_rules_path = abs_rules_path?; - let text = fs.load(&abs_rules_path).await.with_context(|| { - format!("Failed to load assistant rules file {:?}", abs_rules_path) - })?; - anyhow::Ok(RulesFile { - rel_path: rel_rules_path, - abs_path: abs_rules_path.into(), - text: text.trim().to_string(), - }) - }) - .await; - let (rules_file, rules_file_error) = match rules_file_result { - Ok(rules_file) => (Some(rules_file), None), - Err(err) => ( - None, - Some(ThreadError::Message { - header: "Error loading rules file".into(), - message: format!("{err}").into(), - }), - ), - }; - let worktree_info = WorktreeInfoForSystemPrompt { - root_name, - abs_path, - rules_file, - }; - (worktree_info, rules_file_error) - }) - } else { - Task::ready(( + let rules_task = load_worktree_rules_file(fs, worktree, cx); + let Some(rules_task) = rules_task else { + return Task::ready(( WorktreeInfoForSystemPrompt { root_name, abs_path, rules_file: None, }, None, - )) - } + )); + }; + + cx.spawn(async move |_| { + let (rules_file, rules_file_error) = match rules_task.await { + Ok(rules_file) => (Some(rules_file), None), + Err(err) => ( + None, + Some(ThreadError::Message { + header: "Error loading rules file".into(), + message: format!("{err}").into(), + }), + ), + }; + let worktree_info = WorktreeInfoForSystemPrompt { + root_name, + abs_path, + rules_file, + }; + (worktree_info, rules_file_error) + }) } pub fn send_to_model( diff --git a/crates/agent_rules/Cargo.toml b/crates/agent_rules/Cargo.toml new file mode 100644 index 0000000000..f8d5c32d06 --- /dev/null +++ b/crates/agent_rules/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "agent_rules" +version = "0.1.0" +edition.workspace = true +publish.workspace = true +license = "GPL-3.0-or-later" + +[lints] +workspace = true + +[lib] +path = "src/agent_rules.rs" +doctest = false + +[dependencies] +anyhow.workspace = true +fs.workspace = true +gpui.workspace = true +prompt_store.workspace = true +util.workspace = true +worktree.workspace = true + +[dev-dependencies] +indoc.workspace = true diff --git a/crates/agent_rules/LICENSE-GPL b/crates/agent_rules/LICENSE-GPL new file mode 120000 index 0000000000..89e542f750 --- /dev/null +++ b/crates/agent_rules/LICENSE-GPL @@ -0,0 +1 @@ +../../LICENSE-GPL \ No newline at end of file diff --git a/crates/agent_rules/src/agent_rules.rs b/crates/agent_rules/src/agent_rules.rs new file mode 100644 index 0000000000..faae6a086a --- /dev/null +++ b/crates/agent_rules/src/agent_rules.rs @@ -0,0 +1,51 @@ +use std::sync::Arc; + +use anyhow::{Context as _, Result}; +use fs::Fs; +use gpui::{App, AppContext, Task}; +use prompt_store::SystemPromptRulesFile; +use util::maybe; +use worktree::Worktree; + +const RULES_FILE_NAMES: [&'static str; 6] = [ + ".rules", + ".cursorrules", + ".windsurfrules", + ".clinerules", + ".github/copilot-instructions.md", + "CLAUDE.md", +]; + +pub fn load_worktree_rules_file( + fs: Arc, + worktree: &Worktree, + cx: &App, +) -> Option>> { + let selected_rules_file = RULES_FILE_NAMES + .into_iter() + .filter_map(|name| { + worktree + .entry_for_path(name) + .filter(|entry| entry.is_file()) + .map(|entry| (entry.path.clone(), worktree.absolutize(&entry.path))) + }) + .next(); + + // Note that Cline supports `.clinerules` being a directory, but that is not currently + // supported. This doesn't seem to occur often in GitHub repositories. + selected_rules_file.map(|(path_in_worktree, abs_path)| { + let fs = fs.clone(); + cx.background_spawn(maybe!(async move { + let abs_path = abs_path?; + let text = fs + .load(&abs_path) + .await + .with_context(|| format!("Failed to load assistant rules file {:?}", abs_path))?; + anyhow::Ok(SystemPromptRulesFile { + path_in_worktree, + abs_path: abs_path.into(), + text: text.trim().to_string(), + }) + })) + }) +} diff --git a/crates/prompt_store/src/prompts.rs b/crates/prompt_store/src/prompts.rs index f0b8fce68e..8577a21a1e 100644 --- a/crates/prompt_store/src/prompts.rs +++ b/crates/prompt_store/src/prompts.rs @@ -38,12 +38,12 @@ impl AssistantSystemPromptContext { pub struct WorktreeInfoForSystemPrompt { pub root_name: String, pub abs_path: Arc, - pub rules_file: Option, + pub rules_file: Option, } #[derive(Serialize)] -pub struct RulesFile { - pub rel_path: Arc, +pub struct SystemPromptRulesFile { + pub path_in_worktree: Arc, pub abs_path: Arc, pub text: String, }