Fix agent rules files for remote project by loading via buffer (#29440)

When using the agent with a project shared by a collaborator, rules file
loading didn't work as it was trying to read from the client's
filesystem

Release Notes:

- Fixed rules file loading when using the agent with a project shared by
a collaborator.
This commit is contained in:
Michael Sloan 2025-04-25 14:06:40 -06:00 committed by GitHub
parent 7623fce4b4
commit cfb7a30724
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 67 additions and 41 deletions

View file

@ -30,7 +30,7 @@ use language_model::{
}; };
use markdown::parser::{CodeBlockKind, CodeBlockMetadata}; use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown}; use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown};
use project::ProjectItem as _; use project::{ProjectEntryId, ProjectItem as _};
use rope::Point; use rope::Point;
use settings::{Settings as _, update_settings_file}; use settings::{Settings as _, update_settings_file};
use std::path::Path; use std::path::Path;
@ -44,7 +44,7 @@ use ui::{
}; };
use util::ResultExt as _; use util::ResultExt as _;
use util::markdown::MarkdownString; use util::markdown::MarkdownString;
use workspace::{OpenOptions, Workspace}; use workspace::Workspace;
use zed_actions::assistant::OpenRulesLibrary; use zed_actions::assistant::OpenRulesLibrary;
pub struct ActiveThread { pub struct ActiveThread {
@ -3039,21 +3039,30 @@ impl ActiveThread {
return; return;
}; };
let abs_paths = project_context let project_entry_ids = project_context
.worktrees .worktrees
.iter() .iter()
.flat_map(|worktree| worktree.rules_file.as_ref()) .flat_map(|worktree| worktree.rules_file.as_ref())
.map(|rules_file| rules_file.abs_path.to_path_buf()) .map(|rules_file| ProjectEntryId::from_usize(rules_file.project_entry_id))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if let Ok(task) = self.workspace.update(cx, move |workspace, cx| { self.workspace
// TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules .update(cx, move |workspace, cx| {
// files clear. For example, if rules file 1 is already open but rules file 2 is not, // TODO: Open a multibuffer instead? In some cases this doesn't make the set of rules
// this would open and focus rules file 2 in a tab that is not next to rules file 1. // files clear. For example, if rules file 1 is already open but rules file 2 is not,
workspace.open_paths(abs_paths, OpenOptions::default(), None, window, cx) // this would open and focus rules file 2 in a tab that is not next to rules file 1.
}) { let project = workspace.project().read(cx);
task.detach(); let project_paths = project_entry_ids
} .into_iter()
.flat_map(|entry_id| project.path_for_entry(entry_id, cx))
.collect::<Vec<_>>();
for project_path in project_paths {
workspace
.open_path(project_path, None, true, window, cx)
.detach_and_log_err(cx);
}
})
.ok();
} }
fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) { fn dismiss_notifications(&mut self, cx: &mut Context<ActiveThread>) {

View file

@ -11,7 +11,6 @@ use chrono::{DateTime, Utc};
use collections::HashMap; use collections::HashMap;
use context_server::manager::ContextServerManager; use context_server::manager::ContextServerManager;
use context_server::{ContextServerFactoryRegistry, ContextServerTool}; use context_server::{ContextServerFactoryRegistry, ContextServerTool};
use fs::Fs;
use futures::channel::{mpsc, oneshot}; use futures::channel::{mpsc, oneshot};
use futures::future::{self, BoxFuture, Shared}; use futures::future::{self, BoxFuture, Shared};
use futures::{FutureExt as _, StreamExt as _}; use futures::{FutureExt as _, StreamExt as _};
@ -22,7 +21,7 @@ use gpui::{
use heed::Database; use heed::Database;
use heed::types::SerdeBincode; use heed::types::SerdeBincode;
use language_model::{LanguageModelToolUseId, Role, TokenUsage}; use language_model::{LanguageModelToolUseId, Role, TokenUsage};
use project::{Project, Worktree}; use project::{Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::{ use prompt_store::{
ProjectContext, PromptBuilder, PromptId, PromptStore, PromptsUpdatedEvent, RulesFileContext, ProjectContext, PromptBuilder, PromptId, PromptStore, PromptsUpdatedEvent, RulesFileContext,
UserRulesContext, WorktreeContext, UserRulesContext, WorktreeContext,
@ -207,15 +206,15 @@ impl ThreadStore {
prompt_store: Option<Entity<PromptStore>>, prompt_store: Option<Entity<PromptStore>>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Task<()> { ) -> Task<()> {
let project = self.project.read(cx); let worktrees = self
let worktree_tasks = project .project
.read(cx)
.visible_worktrees(cx) .visible_worktrees(cx)
.collect::<Vec<_>>();
let worktree_tasks = worktrees
.into_iter()
.map(|worktree| { .map(|worktree| {
Self::load_worktree_info_for_system_prompt( Self::load_worktree_info_for_system_prompt(worktree, self.project.clone(), cx)
project.fs().clone(),
worktree.read(cx),
cx,
)
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let default_user_rules_task = match prompt_store { let default_user_rules_task = match prompt_store {
@ -276,13 +275,13 @@ impl ThreadStore {
} }
fn load_worktree_info_for_system_prompt( fn load_worktree_info_for_system_prompt(
fs: Arc<dyn Fs>, worktree: Entity<Worktree>,
worktree: &Worktree, project: Entity<Project>,
cx: &App, cx: &mut App,
) -> Task<(WorktreeContext, Option<RulesLoadingError>)> { ) -> Task<(WorktreeContext, Option<RulesLoadingError>)> {
let root_name = worktree.root_name().into(); let root_name = worktree.read(cx).root_name().into();
let rules_task = Self::load_worktree_rules_file(fs, worktree, cx); let rules_task = Self::load_worktree_rules_file(worktree, project, cx);
let Some(rules_task) = rules_task else { let Some(rules_task) = rules_task else {
return Task::ready(( return Task::ready((
WorktreeContext { WorktreeContext {
@ -312,33 +311,44 @@ impl ThreadStore {
} }
fn load_worktree_rules_file( fn load_worktree_rules_file(
fs: Arc<dyn Fs>, worktree: Entity<Worktree>,
worktree: &Worktree, project: Entity<Project>,
cx: &App, cx: &mut App,
) -> Option<Task<Result<RulesFileContext>>> { ) -> Option<Task<Result<RulesFileContext>>> {
let worktree_ref = worktree.read(cx);
let worktree_id = worktree_ref.id();
let selected_rules_file = RULES_FILE_NAMES let selected_rules_file = RULES_FILE_NAMES
.into_iter() .into_iter()
.filter_map(|name| { .filter_map(|name| {
worktree worktree_ref
.entry_for_path(name) .entry_for_path(name)
.filter(|entry| entry.is_file()) .filter(|entry| entry.is_file())
.map(|entry| (entry.path.clone(), worktree.absolutize(&entry.path))) .map(|entry| entry.path.clone())
}) })
.next(); .next();
// Note that Cline supports `.clinerules` being a directory, but that is not currently // Note that Cline supports `.clinerules` being a directory, but that is not currently
// supported. This doesn't seem to occur often in GitHub repositories. // supported. This doesn't seem to occur often in GitHub repositories.
selected_rules_file.map(|(path_in_worktree, abs_path)| { selected_rules_file.map(|path_in_worktree| {
let fs = fs.clone(); let project_path = ProjectPath {
worktree_id,
path: path_in_worktree.clone(),
};
let buffer_task =
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
let rope_task = cx.spawn(async move |cx| {
buffer_task.await?.read_with(cx, |buffer, cx| {
let project_entry_id = buffer.entry_id(cx).context("buffer has no file")?;
anyhow::Ok((project_entry_id, buffer.as_rope().clone()))
})?
});
// Build a string from the rope on a background thread.
cx.background_spawn(async move { cx.background_spawn(async move {
let abs_path = abs_path?; let (project_entry_id, rope) = rope_task.await?;
let text = fs.load(&abs_path).await.with_context(|| {
format!("Failed to load assistant rules file {:?}", abs_path)
})?;
anyhow::Ok(RulesFileContext { anyhow::Ok(RulesFileContext {
path_in_worktree, path_in_worktree,
abs_path: abs_path.into(), text: rope.to_string().trim().to_string(),
text: text.trim().to_string(), project_entry_id: project_entry_id.to_usize(),
}) })
}) })
}) })

View file

@ -64,8 +64,11 @@ pub struct WorktreeContext {
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]
pub struct RulesFileContext { pub struct RulesFileContext {
pub path_in_worktree: Arc<Path>, pub path_in_worktree: Arc<Path>,
pub abs_path: Arc<Path>,
pub text: String, pub text: String,
// This used for opening rules files. TODO: Since it isn't related to prompt templating, this
// should be moved elsewhere.
#[serde(skip)]
pub project_entry_id: usize,
} }
#[derive(Serialize)] #[derive(Serialize)]
@ -403,8 +406,8 @@ mod test {
root_name: "path".into(), root_name: "path".into(),
rules_file: Some(RulesFileContext { rules_file: Some(RulesFileContext {
path_in_worktree: Path::new(".rules").into(), path_in_worktree: Path::new(".rules").into(),
abs_path: Path::new("/some/path/.rules").into(),
text: "".into(), text: "".into(),
project_entry_id: 0,
}), }),
}]; }];
let default_user_rules = vec![UserRulesContext { let default_user_rules = vec![UserRulesContext {

View file

@ -5549,6 +5549,10 @@ impl ProjectEntryId {
self.0 as u64 self.0 as u64
} }
pub fn from_usize(id: usize) -> Self {
ProjectEntryId(id)
}
pub fn to_usize(&self) -> usize { pub fn to_usize(&self) -> usize {
self.0 self.0
} }