Initial support for AI assistant rules files (#27168)
Release Notes: - N/A --------- Co-authored-by: Danilo <danilo@zed.dev> Co-authored-by: Nathan <nathan@zed.dev> Co-authored-by: Thomas <thomas@zed.dev>
This commit is contained in:
parent
14920ab910
commit
1180b6fbc7
8 changed files with 322 additions and 51 deletions
|
@ -6,6 +6,7 @@ use anyhow::{Context as _, Result};
|
|||
use assistant_tool::{ActionLog, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::{BTreeMap, HashMap, HashSet};
|
||||
use fs::Fs;
|
||||
use futures::future::Shared;
|
||||
use futures::{FutureExt, StreamExt as _};
|
||||
use git;
|
||||
|
@ -17,11 +18,13 @@ use language_model::{
|
|||
Role, StopReason, TokenUsage,
|
||||
};
|
||||
use project::git::GitStoreCheckpoint;
|
||||
use project::Project;
|
||||
use prompt_store::{AssistantSystemPromptWorktree, PromptBuilder};
|
||||
use project::{Project, Worktree};
|
||||
use prompt_store::{
|
||||
AssistantSystemPromptContext, PromptBuilder, RulesFile, WorktreeInfoForSystemPrompt,
|
||||
};
|
||||
use scripting_tool::{ScriptingSession, ScriptingTool};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use util::{post_inc, ResultExt, TryFutureExt as _};
|
||||
use util::{maybe, post_inc, ResultExt as _, TryFutureExt as _};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::context::{attach_context_to_message, ContextId, ContextSnapshot};
|
||||
|
@ -106,6 +109,7 @@ pub struct Thread {
|
|||
next_message_id: MessageId,
|
||||
context: BTreeMap<ContextId, ContextSnapshot>,
|
||||
context_by_message: HashMap<MessageId, Vec<ContextId>>,
|
||||
system_prompt_context: Option<AssistantSystemPromptContext>,
|
||||
checkpoints_by_message: HashMap<MessageId, GitStoreCheckpoint>,
|
||||
completion_count: usize,
|
||||
pending_completions: Vec<PendingCompletion>,
|
||||
|
@ -136,6 +140,7 @@ impl Thread {
|
|||
next_message_id: MessageId(0),
|
||||
context: BTreeMap::default(),
|
||||
context_by_message: HashMap::default(),
|
||||
system_prompt_context: None,
|
||||
checkpoints_by_message: HashMap::default(),
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
|
@ -197,6 +202,7 @@ impl Thread {
|
|||
next_message_id,
|
||||
context: BTreeMap::default(),
|
||||
context_by_message: HashMap::default(),
|
||||
system_prompt_context: None,
|
||||
checkpoints_by_message: HashMap::default(),
|
||||
completion_count: 0,
|
||||
pending_completions: Vec::new(),
|
||||
|
@ -478,6 +484,116 @@ impl Thread {
|
|||
})
|
||||
}
|
||||
|
||||
pub fn set_system_prompt_context(&mut self, context: AssistantSystemPromptContext) {
|
||||
self.system_prompt_context = Some(context);
|
||||
}
|
||||
|
||||
pub fn system_prompt_context(&self) -> &Option<AssistantSystemPromptContext> {
|
||||
&self.system_prompt_context
|
||||
}
|
||||
|
||||
pub fn load_system_prompt_context(
|
||||
&self,
|
||||
cx: &App,
|
||||
) -> Task<(AssistantSystemPromptContext, Option<ThreadError>)> {
|
||||
let project = self.project.read(cx);
|
||||
let tasks = project
|
||||
.visible_worktrees(cx)
|
||||
.map(|worktree| {
|
||||
Self::load_worktree_info_for_system_prompt(
|
||||
project.fs().clone(),
|
||||
worktree.read(cx),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.spawn(async |_cx| {
|
||||
let results = futures::future::join_all(tasks).await;
|
||||
let mut first_err = None;
|
||||
let worktrees = results
|
||||
.into_iter()
|
||||
.map(|(worktree, err)| {
|
||||
if first_err.is_none() && err.is_some() {
|
||||
first_err = err;
|
||||
}
|
||||
worktree
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
(AssistantSystemPromptContext::new(worktrees), first_err)
|
||||
})
|
||||
}
|
||||
|
||||
fn load_worktree_info_for_system_prompt(
|
||||
fs: Arc<dyn Fs>,
|
||||
worktree: &Worktree,
|
||||
cx: &App,
|
||||
) -> Task<(WorktreeInfoForSystemPrompt, Option<ThreadError>)> {
|
||||
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; 5] = [
|
||||
".rules",
|
||||
".cursorrules",
|
||||
".windsurfrules",
|
||||
".clinerules",
|
||||
"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((
|
||||
WorktreeInfoForSystemPrompt {
|
||||
root_name,
|
||||
abs_path,
|
||||
rules_file: None,
|
||||
},
|
||||
None,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_to_model(
|
||||
&mut self,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
|
@ -515,36 +631,30 @@ impl Thread {
|
|||
request_kind: RequestKind,
|
||||
cx: &App,
|
||||
) -> LanguageModelRequest {
|
||||
let worktree_root_names = self
|
||||
.project
|
||||
.read(cx)
|
||||
.visible_worktrees(cx)
|
||||
.map(|worktree| {
|
||||
let worktree = worktree.read(cx);
|
||||
AssistantSystemPromptWorktree {
|
||||
root_name: worktree.root_name().into(),
|
||||
abs_path: worktree.abs_path(),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let system_prompt = self
|
||||
.prompt_builder
|
||||
.generate_assistant_system_prompt(worktree_root_names)
|
||||
.context("failed to generate assistant system prompt")
|
||||
.log_err()
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut request = LanguageModelRequest {
|
||||
messages: vec![LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![MessageContent::Text(system_prompt)],
|
||||
cache: true,
|
||||
}],
|
||||
messages: vec![],
|
||||
tools: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
temperature: None,
|
||||
};
|
||||
|
||||
if let Some(system_prompt_context) = self.system_prompt_context.as_ref() {
|
||||
if let Some(system_prompt) = self
|
||||
.prompt_builder
|
||||
.generate_assistant_system_prompt(system_prompt_context)
|
||||
.context("failed to generate assistant system prompt")
|
||||
.log_err()
|
||||
{
|
||||
request.messages.push(LanguageModelRequestMessage {
|
||||
role: Role::System,
|
||||
content: vec![MessageContent::Text(system_prompt)],
|
||||
cache: true,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log::error!("system_prompt_context not set.")
|
||||
}
|
||||
|
||||
let mut referenced_context_ids = HashSet::default();
|
||||
|
||||
for message in &self.messages {
|
||||
|
@ -757,9 +867,10 @@ impl Thread {
|
|||
.map(|err| err.to_string())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message(
|
||||
SharedString::from(error_message.clone()),
|
||||
)));
|
||||
cx.emit(ThreadEvent::ShowError(ThreadError::Message {
|
||||
header: "Error interacting with language model".into(),
|
||||
message: SharedString::from(error_message.clone()),
|
||||
}));
|
||||
}
|
||||
|
||||
thread.cancel_last_completion(cx);
|
||||
|
@ -1204,7 +1315,10 @@ impl Thread {
|
|||
pub enum ThreadError {
|
||||
PaymentRequired,
|
||||
MaxMonthlySpendReached,
|
||||
Message(SharedString),
|
||||
Message {
|
||||
header: SharedString,
|
||||
message: SharedString,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue