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
This commit is contained in:
Michael Sloan 2025-04-08 19:31:56 -06:00 committed by GitHub
parent 020a1071d5
commit 301fc7cd7b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 125 additions and 64 deletions

14
Cargo.lock generated
View File

@ -52,6 +52,7 @@ dependencies = [
name = "agent" name = "agent"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"agent_rules",
"anyhow", "anyhow",
"assistant_context_editor", "assistant_context_editor",
"assistant_settings", "assistant_settings",
@ -161,6 +162,19 @@ dependencies = [
"workspace-hack", "workspace-hack",
] ]
[[package]]
name = "agent_rules"
version = "0.1.0"
dependencies = [
"anyhow",
"fs",
"gpui",
"indoc",
"prompt_store",
"util",
"worktree",
]
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.7.8" version = "0.7.8"

View File

@ -3,6 +3,7 @@ resolver = "2"
members = [ members = [
"crates/activity_indicator", "crates/activity_indicator",
"crates/agent", "crates/agent",
"crates/agent_rules",
"crates/anthropic", "crates/anthropic",
"crates/askpass", "crates/askpass",
"crates/assets", "crates/assets",
@ -209,6 +210,7 @@ edition = "2024"
activity_indicator = { path = "crates/activity_indicator" } activity_indicator = { path = "crates/activity_indicator" }
agent = { path = "crates/agent" } agent = { path = "crates/agent" }
agent_rules = { path = "crates/agent_rules" }
ai = { path = "crates/ai" } ai = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" } anthropic = { path = "crates/anthropic" }
askpass = { path = "crates/askpass" } askpass = { path = "crates/askpass" }

View File

@ -155,7 +155,7 @@ There are rules that apply to these root directories:
{{#each worktrees}} {{#each worktrees}}
{{#if rules_file}} {{#if rules_file}}
`{{root_name}}/{{rules_file.rel_path}}`: `{{root_name}}/{{rules_file.path_in_worktree}}`:
`````` ``````
{{{rules_file.text}}} {{{rules_file.text}}}

View File

@ -19,6 +19,7 @@ test-support = [
] ]
[dependencies] [dependencies]
agent_rules.workspace = true
anyhow.workspace = true anyhow.workspace = true
assistant_context_editor.workspace = true assistant_context_editor.workspace = true
assistant_settings.workspace = true assistant_settings.workspace = true

View File

@ -2581,7 +2581,7 @@ impl ActiveThread {
let label_text = match rules_files.as_slice() { let label_text = match rules_files.as_slice() {
&[] => return div().into_any(), &[] => return div().into_any(),
&[rules_file] => { &[rules_file] => {
format!("Using {:?} file", rules_file.rel_path) format!("Using {:?} file", rules_file.path_in_worktree)
} }
rules_files => { rules_files => {
format!("Using {} rules files", rules_files.len()) format!("Using {} rules files", rules_files.len())

View File

@ -3,6 +3,7 @@ use std::io::Write;
use std::ops::Range; use std::ops::Range;
use std::sync::Arc; use std::sync::Arc;
use agent_rules::load_worktree_rules_file;
use anyhow::{Context as _, Result, anyhow}; use anyhow::{Context as _, Result, anyhow};
use assistant_settings::AssistantSettings; use assistant_settings::AssistantSettings;
use assistant_tool::{ActionLog, Tool, ToolWorkingSet}; use assistant_tool::{ActionLog, Tool, ToolWorkingSet};
@ -21,13 +22,11 @@ use language_model::{
}; };
use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState}; use project::git_store::{GitStore, GitStoreCheckpoint, RepositoryState};
use project::{Project, Worktree}; use project::{Project, Worktree};
use prompt_store::{ use prompt_store::{AssistantSystemPromptContext, PromptBuilder, WorktreeInfoForSystemPrompt};
AssistantSystemPromptContext, PromptBuilder, RulesFile, WorktreeInfoForSystemPrompt,
};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::Settings; use settings::Settings;
use util::{ResultExt as _, TryFutureExt as _, maybe, post_inc}; use util::{ResultExt as _, TryFutureExt as _, post_inc};
use uuid::Uuid; use uuid::Uuid;
use crate::context::{AssistantContext, ContextId, format_context_as_string}; use crate::context::{AssistantContext, ContextId, format_context_as_string};
@ -854,41 +853,20 @@ impl Thread {
let root_name = worktree.root_name().into(); let root_name = worktree.root_name().into();
let abs_path = worktree.abs_path(); let abs_path = worktree.abs_path();
// Note that Cline supports `.clinerules` being a directory, but that is not currently let rules_task = load_worktree_rules_file(fs, worktree, cx);
// supported. This doesn't seem to occur often in GitHub repositories. let Some(rules_task) = rules_task else {
const RULES_FILE_NAMES: [&'static str; 6] = [ return Task::ready((
".rules", WorktreeInfoForSystemPrompt {
".cursorrules", root_name,
".windsurfrules", abs_path,
".clinerules", rules_file: None,
".github/copilot-instructions.md", },
"CLAUDE.md", None,
]; ));
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 |_| { cx.spawn(async move |_| {
let rules_file_result = maybe!(async move { let (rules_file, rules_file_error) = match rules_task.await {
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), Ok(rules_file) => (Some(rules_file), None),
Err(err) => ( Err(err) => (
None, None,
@ -905,16 +883,6 @@ impl Thread {
}; };
(worktree_info, rules_file_error) (worktree_info, rules_file_error)
}) })
} else {
Task::ready((
WorktreeInfoForSystemPrompt {
root_name,
abs_path,
rules_file: None,
},
None,
))
}
} }
pub fn send_to_model( pub fn send_to_model(

View File

@ -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

View File

@ -0,0 +1 @@
../../LICENSE-GPL

View File

@ -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<dyn Fs>,
worktree: &Worktree,
cx: &App,
) -> Option<Task<Result<SystemPromptRulesFile>>> {
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(),
})
}))
})
}

View File

@ -38,12 +38,12 @@ impl AssistantSystemPromptContext {
pub struct WorktreeInfoForSystemPrompt { pub struct WorktreeInfoForSystemPrompt {
pub root_name: String, pub root_name: String,
pub abs_path: Arc<Path>, pub abs_path: Arc<Path>,
pub rules_file: Option<RulesFile>, pub rules_file: Option<SystemPromptRulesFile>,
} }
#[derive(Serialize)] #[derive(Serialize)]
pub struct RulesFile { pub struct SystemPromptRulesFile {
pub rel_path: Arc<Path>, pub path_in_worktree: Arc<Path>,
pub abs_path: Arc<Path>, pub abs_path: Arc<Path>,
pub text: String, pub text: String,
} }