Support @-mentions in inline assists and when editing old agent panel messages (#29734)

Closes #ISSUE

Co-authored-by: Bennet <bennet@zed.dev>

Release Notes:

- Added support for context `@mentions` in the inline prompt editor and
when editing past messages in the agent panel.

---------

Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
This commit is contained in:
Cole Miller 2025-05-02 16:08:53 -04:00 committed by GitHub
parent c918f6cde1
commit 9547d42b15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 499 additions and 156 deletions

View file

@ -5,7 +5,7 @@ use std::sync::Arc;
use anyhow::{Result, anyhow};
use collections::{HashSet, IndexSet};
use futures::{self, FutureExt};
use gpui::{App, Context, Entity, Image, SharedString, Task, WeakEntity};
use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
use language::Buffer;
use language_model::LanguageModelImage;
use project::image_store::is_image_file;
@ -31,6 +31,12 @@ pub struct ContextStore {
context_thread_ids: HashSet<ThreadId>,
}
pub enum ContextStoreEvent {
ContextRemoved(AgentContextKey),
}
impl EventEmitter<ContextStoreEvent> for ContextStore {}
impl ContextStore {
pub fn new(
project: WeakEntity<Project>,
@ -82,7 +88,7 @@ impl ContextStore {
project_path: ProjectPath,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Task<Result<()>> {
) -> Task<Result<Option<AgentContextHandle>>> {
let Some(project) = self.project.upgrade() else {
return Task::ready(Err(anyhow!("failed to read project")));
};
@ -108,21 +114,22 @@ impl ContextStore {
buffer: Entity<Buffer>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) {
) -> Option<AgentContextHandle> {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::File(FileContextHandle { buffer, context_id });
let already_included = if self.has_context(&context) {
if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(key.as_ref().clone())
}
true
} else if self.path_included_in_directory(project_path, cx).is_some() {
None
} else {
self.path_included_in_directory(project_path, cx).is_some()
};
if !already_included {
self.insert_context(context, cx);
self.insert_context(context.clone(), cx);
Some(context)
}
}
@ -131,7 +138,7 @@ impl ContextStore {
project_path: &ProjectPath,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> Result<()> {
) -> Result<Option<AgentContextHandle>> {
let Some(project) = self.project.upgrade() else {
return Err(anyhow!("failed to read project"));
};
@ -150,15 +157,20 @@ impl ContextStore {
context_id,
});
if self.has_context(&context) {
if remove_if_exists {
self.remove_context(&context, cx);
}
} else if self.path_included_in_directory(project_path, cx).is_none() {
self.insert_context(context, cx);
}
let context =
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(existing.as_ref().clone())
}
} else {
self.insert_context(context.clone(), cx);
Some(context)
};
anyhow::Ok(())
anyhow::Ok(context)
}
pub fn add_symbol(
@ -169,7 +181,7 @@ impl ContextStore {
enclosing_range: Range<Anchor>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) -> bool {
) -> (Option<AgentContextHandle>, bool) {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Symbol(SymbolContextHandle {
buffer,
@ -179,14 +191,18 @@ impl ContextStore {
context_id,
});
if self.has_context(&context) {
if remove_if_exists {
if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
let handle = if remove_if_exists {
self.remove_context(&context, cx);
}
return false;
None
} else {
Some(key.as_ref().clone())
};
return (handle, false);
}
self.insert_context(context, cx)
let included = self.insert_context(context.clone(), cx);
(Some(context), included)
}
pub fn add_thread(
@ -194,16 +210,20 @@ impl ContextStore {
thread: Entity<Thread>,
remove_if_exists: bool,
cx: &mut Context<Self>,
) {
) -> Option<AgentContextHandle> {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Thread(ThreadContextHandle { thread, context_id });
if self.has_context(&context) {
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(existing.as_ref().clone())
}
} else {
self.insert_context(context, cx);
self.insert_context(context.clone(), cx);
Some(context)
}
}
@ -212,19 +232,23 @@ impl ContextStore {
prompt_id: UserPromptId,
remove_if_exists: bool,
cx: &mut Context<ContextStore>,
) {
) -> Option<AgentContextHandle> {
let context_id = self.next_context_id.post_inc();
let context = AgentContextHandle::Rules(RulesContextHandle {
prompt_id,
context_id,
});
if self.has_context(&context) {
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
if remove_if_exists {
self.remove_context(&context, cx);
None
} else {
Some(existing.as_ref().clone())
}
} else {
self.insert_context(context, cx);
self.insert_context(context.clone(), cx);
Some(context)
}
}
@ -233,14 +257,15 @@ impl ContextStore {
url: String,
text: impl Into<SharedString>,
cx: &mut Context<ContextStore>,
) {
) -> AgentContextHandle {
let context = AgentContextHandle::FetchedUrl(FetchedUrlContext {
url: url.into(),
text: text.into(),
context_id: self.next_context_id.post_inc(),
});
self.insert_context(context, cx);
self.insert_context(context.clone(), cx);
context
}
pub fn add_image_from_path(
@ -248,7 +273,7 @@ impl ContextStore {
project_path: ProjectPath,
remove_if_exists: bool,
cx: &mut Context<ContextStore>,
) -> Task<Result<()>> {
) -> Task<Result<Option<AgentContextHandle>>> {
let project = self.project.clone();
cx.spawn(async move |this, cx| {
let open_image_task = project.update(cx, |project, cx| {
@ -262,7 +287,7 @@ impl ContextStore {
image,
remove_if_exists,
cx,
);
)
})
})
}
@ -277,7 +302,7 @@ impl ContextStore {
image: Arc<Image>,
remove_if_exists: bool,
cx: &mut Context<ContextStore>,
) {
) -> Option<AgentContextHandle> {
let image_task = LanguageModelImage::from_image(image.clone(), cx).shared();
let context = AgentContextHandle::Image(ImageContext {
project_path,
@ -288,11 +313,12 @@ impl ContextStore {
if self.has_context(&context) {
if remove_if_exists {
self.remove_context(&context, cx);
return;
return None;
}
}
self.insert_context(context, cx);
self.insert_context(context.clone(), cx);
Some(context)
}
pub fn add_selection(
@ -364,9 +390,9 @@ impl ContextStore {
}
pub fn remove_context(&mut self, context: &AgentContextHandle, cx: &mut Context<Self>) {
if self
if let Some((_, key)) = self
.context_set
.shift_remove(AgentContextKey::ref_cast(context))
.shift_remove_full(AgentContextKey::ref_cast(context))
{
match context {
AgentContextHandle::Thread(thread_context) => {
@ -375,6 +401,7 @@ impl ContextStore {
}
_ => {}
}
cx.emit(ContextStoreEvent::ContextRemoved(key));
cx.notify();
}
}
@ -451,6 +478,12 @@ impl ContextStore {
.contains(&FetchedUrlContext::lookup_key(url.into()))
}
pub fn get_url_context(&self, url: SharedString) -> Option<AgentContextHandle> {
self.context_set
.get(&FetchedUrlContext::lookup_key(url))
.map(|key| key.as_ref().clone())
}
pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
self.context()
.filter_map(|context| match context {