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:
parent
7623fce4b4
commit
cfb7a30724
4 changed files with 67 additions and 41 deletions
|
@ -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>) {
|
||||||
|
|
|
@ -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(),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue