Fully support all mention kinds (#36134)

Feature parity with the agent1 @mention kinds:
- File
- Symbols
- Selections
- Threads
- Rules
- Fetch


Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
Agus Zubiaga 2025-08-13 17:11:32 -03:00 committed by GitHub
parent 389d382f42
commit 389d24d7e5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 1787 additions and 324 deletions

View file

@ -93,6 +93,7 @@ time.workspace = true
time_format.workspace = true
ui.workspace = true
ui_input.workspace = true
url.workspace = true
urlencoding.workspace = true
util.workspace = true
uuid.workspace = true
@ -102,6 +103,8 @@ workspace.workspace = true
zed_actions.workspace = true
[dev-dependencies]
agent = { workspace = true, features = ["test-support"] }
assistant_context = { workspace = true, features = ["test-support"] }
assistant_tools.workspace = true
buffer_diff = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }

File diff suppressed because it is too large Load diff

View file

@ -45,12 +45,8 @@ impl<T> MessageHistory<T> {
None
})
}
#[cfg(test)]
pub fn items(&self) -> &[T] {
&self.items
}
}
#[cfg(test)]
mod tests {
use super::*;

View file

@ -4,15 +4,17 @@ use acp_thread::{
};
use acp_thread::{AgentConnection, Plan};
use action_log::ActionLog;
use agent::{TextThreadStore, ThreadStore};
use agent_client_protocol as acp;
use agent_servers::AgentServer;
use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
use audio::{Audio, Sound};
use buffer_diff::BufferDiff;
use collections::{HashMap, HashSet};
use editor::scroll::Autoscroll;
use editor::{
AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
EditorStyle, MinimapVisibility, MultiBuffer, PathKey, SelectionEffects,
};
use file_icons::FileIcons;
use gpui::{
@ -27,8 +29,10 @@ use language::{Buffer, Language};
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
use parking_lot::Mutex;
use project::{CompletionIntent, Project};
use prompt_store::PromptId;
use rope::Point;
use settings::{Settings as _, SettingsStore};
use std::fmt::Write as _;
use std::path::PathBuf;
use std::{
cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc,
@ -44,6 +48,7 @@ use ui::{
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage, ToggleModelSelector};
use zed_actions::assistant::OpenRulesLibrary;
use crate::acp::AcpModelSelectorPopover;
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
@ -61,6 +66,8 @@ pub struct AcpThreadView {
agent: Rc<dyn AgentServer>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
thread_store: Entity<ThreadStore>,
text_thread_store: Entity<TextThreadStore>,
thread_state: ThreadState,
diff_editors: HashMap<EntityId, Entity<Editor>>,
terminal_views: HashMap<EntityId, Entity<TerminalView>>,
@ -108,6 +115,8 @@ impl AcpThreadView {
agent: Rc<dyn AgentServer>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
thread_store: Entity<ThreadStore>,
text_thread_store: Entity<TextThreadStore>,
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
min_lines: usize,
max_lines: Option<usize>,
@ -145,6 +154,8 @@ impl AcpThreadView {
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
mention_set.clone(),
workspace.clone(),
thread_store.downgrade(),
text_thread_store.downgrade(),
cx.weak_entity(),
))));
editor.set_context_menu_options(ContextMenuOptions {
@ -188,6 +199,8 @@ impl AcpThreadView {
agent: agent.clone(),
workspace: workspace.clone(),
project: project.clone(),
thread_store,
text_thread_store,
thread_state: Self::initial_state(agent, workspace, project, window, cx),
message_editor,
model_selector: None,
@ -401,7 +414,13 @@ impl AcpThreadView {
let mut chunks: Vec<acp::ContentBlock> = Vec::new();
let project = self.project.clone();
let contents = self.mention_set.lock().contents(project, cx);
let thread_store = self.thread_store.clone();
let text_thread_store = self.text_thread_store.clone();
let contents =
self.mention_set
.lock()
.contents(project, thread_store, text_thread_store, window, cx);
cx.spawn_in(window, async move |this, cx| {
let contents = match contents.await {
@ -439,7 +458,7 @@ impl AcpThreadView {
acp::TextResourceContents {
mime_type: None,
text: mention.content.clone(),
uri: mention.uri.to_uri(),
uri: mention.uri.to_uri().to_string(),
},
),
}));
@ -614,8 +633,7 @@ impl AcpThreadView {
let path = PathBuf::from(&resource.uri);
let project_path = project.read(cx).project_path_for_absolute_path(&path, cx);
let start = text.len();
let content = MentionUri::File(path).to_uri();
text.push_str(&content);
let _ = write!(&mut text, "{}", MentionUri::File(path).to_uri());
let end = text.len();
if let Some(project_path) = project_path {
let filename: SharedString = project_path
@ -663,7 +681,9 @@ impl AcpThreadView {
);
if let Some(crease_id) = crease_id {
mention_set.lock().insert(crease_id, project_path);
mention_set
.lock()
.insert(crease_id, MentionUri::File(project_path));
}
}
}
@ -2698,9 +2718,72 @@ impl AcpThreadView {
.detach_and_log_err(cx);
}
}
_ => {
// TODO
unimplemented!()
MentionUri::Symbol {
path, line_range, ..
}
| MentionUri::Selection { path, line_range } => {
let project = workspace.project();
let Some((path, _)) = project.update(cx, |project, cx| {
let path = project.find_project_path(path, cx)?;
let entry = project.entry_for_path(&path, cx)?;
Some((path, entry))
}) else {
return;
};
let item = workspace.open_path(path, None, true, window, cx);
window
.spawn(cx, async move |cx| {
let Some(editor) = item.await?.downcast::<Editor>() else {
return Ok(());
};
let range =
Point::new(line_range.start, 0)..Point::new(line_range.start, 0);
editor
.update_in(cx, |editor, window, cx| {
editor.change_selections(
SelectionEffects::scroll(Autoscroll::center()),
window,
cx,
|s| s.select_ranges(vec![range]),
);
})
.ok();
anyhow::Ok(())
})
.detach_and_log_err(cx);
}
MentionUri::Thread { id, .. } => {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel
.open_thread_by_id(&id, window, cx)
.detach_and_log_err(cx)
});
}
}
MentionUri::TextThread { path, .. } => {
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
panel.update(cx, |panel, cx| {
panel
.open_saved_prompt_editor(path.as_path().into(), window, cx)
.detach_and_log_err(cx);
});
}
}
MentionUri::Rule { id, .. } => {
let PromptId::User { uuid } = id else {
return;
};
window.dispatch_action(
Box::new(OpenRulesLibrary {
prompt_to_select: Some(uuid.0),
}),
cx,
)
}
MentionUri::Fetch { url } => {
cx.open_url(url.as_str());
}
})
} else {
@ -3090,7 +3173,7 @@ impl AcpThreadView {
.unwrap_or(path.path.as_os_str())
.display()
.to_string();
let completion = ContextPickerCompletionProvider::completion_for_path(
let Some(completion) = ContextPickerCompletionProvider::completion_for_path(
path,
&path_prefix,
false,
@ -3101,7 +3184,9 @@ impl AcpThreadView {
self.mention_set.clone(),
self.project.clone(),
cx,
);
) else {
continue;
};
self.message_editor.update(cx, |message_editor, cx| {
message_editor.edit(
@ -3431,17 +3516,14 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
#[cfg(test)]
mod tests {
use agent::{TextThreadStore, ThreadStore};
use agent_client_protocol::SessionId;
use editor::EditorSettings;
use fs::FakeFs;
use futures::future::try_join_all;
use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
use lsp::{CompletionContext, CompletionTriggerKind};
use project::CompletionIntent;
use rand::Rng;
use serde_json::json;
use settings::SettingsStore;
use util::path;
use super::*;
@ -3554,109 +3636,6 @@ mod tests {
);
}
#[gpui::test]
async fn test_crease_removal(cx: &mut TestAppContext) {
init_test(cx);
let fs = FakeFs::new(cx.executor());
fs.insert_tree("/project", json!({"file": ""})).await;
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
let agent = StubAgentServer::default();
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let thread_view = cx.update(|window, cx| {
cx.new(|cx| {
AcpThreadView::new(
Rc::new(agent),
workspace.downgrade(),
project,
Rc::new(RefCell::new(MessageHistory::default())),
1,
None,
window,
cx,
)
})
});
cx.run_until_parked();
let message_editor = cx.read(|cx| thread_view.read(cx).message_editor.clone());
let excerpt_id = message_editor.update(cx, |editor, cx| {
editor
.buffer()
.read(cx)
.excerpt_ids()
.into_iter()
.next()
.unwrap()
});
let completions = message_editor.update_in(cx, |editor, window, cx| {
editor.set_text("Hello @", window, cx);
let buffer = editor.buffer().read(cx).as_singleton().unwrap();
let completion_provider = editor.completion_provider().unwrap();
completion_provider.completions(
excerpt_id,
&buffer,
Anchor::MAX,
CompletionContext {
trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
trigger_character: Some("@".into()),
},
window,
cx,
)
});
let [_, completion]: [_; 2] = completions
.await
.unwrap()
.into_iter()
.flat_map(|response| response.completions)
.collect::<Vec<_>>()
.try_into()
.unwrap();
message_editor.update_in(cx, |editor, window, cx| {
let snapshot = editor.buffer().read(cx).snapshot(cx);
let start = snapshot
.anchor_in_excerpt(excerpt_id, completion.replace_range.start)
.unwrap();
let end = snapshot
.anchor_in_excerpt(excerpt_id, completion.replace_range.end)
.unwrap();
editor.edit([(start..end, completion.new_text)], cx);
(completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
});
cx.run_until_parked();
// Backspace over the inserted crease (and the following space).
message_editor.update_in(cx, |editor, window, cx| {
editor.backspace(&Default::default(), window, cx);
editor.backspace(&Default::default(), window, cx);
});
thread_view.update_in(cx, |thread_view, window, cx| {
thread_view.chat(&Chat, window, cx);
});
cx.run_until_parked();
let content = thread_view.update_in(cx, |thread_view, _window, _cx| {
thread_view
.message_history
.borrow()
.items()
.iter()
.flatten()
.cloned()
.collect::<Vec<_>>()
});
// We don't send a resource link for the deleted crease.
pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
}
async fn setup_thread_view(
agent: impl AgentServer + 'static,
cx: &mut TestAppContext,
@ -3666,12 +3645,19 @@ mod tests {
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let thread_store =
cx.update(|_window, cx| cx.new(|cx| ThreadStore::fake(project.clone(), cx)));
let text_thread_store =
cx.update(|_window, cx| cx.new(|cx| TextThreadStore::fake(project.clone(), cx)));
let thread_view = cx.update(|window, cx| {
cx.new(|cx| {
AcpThreadView::new(
Rc::new(agent),
workspace.downgrade(),
project,
thread_store.clone(),
text_thread_store.clone(),
Rc::new(RefCell::new(MessageHistory::default())),
1,
None,

View file

@ -973,6 +973,9 @@ impl AgentPanel {
agent: crate::ExternalAgent,
}
let thread_store = self.thread_store.clone();
let text_thread_store = self.context_store.clone();
cx.spawn_in(window, async move |this, cx| {
let server: Rc<dyn AgentServer> = match agent_choice {
Some(agent) => {
@ -1011,6 +1014,8 @@ impl AgentPanel {
server,
workspace.clone(),
project,
thread_store.clone(),
text_thread_store.clone(),
message_history,
MIN_EDITOR_LINES,
Some(MAX_EDITOR_LINES),

View file

@ -1,15 +1,16 @@
mod completion_provider;
mod fetch_context_picker;
pub(crate) mod fetch_context_picker;
pub(crate) mod file_context_picker;
mod rules_context_picker;
mod symbol_context_picker;
mod thread_context_picker;
pub(crate) mod rules_context_picker;
pub(crate) mod symbol_context_picker;
pub(crate) mod thread_context_picker;
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{Result, anyhow};
use collections::HashSet;
pub use completion_provider::ContextPickerCompletionProvider;
use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};
use editor::{Anchor, AnchorRangeExt as _, Editor, ExcerptId, FoldPlaceholder, ToOffset};
@ -45,7 +46,7 @@ use agent::{
};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ContextPickerEntry {
pub(crate) enum ContextPickerEntry {
Mode(ContextPickerMode),
Action(ContextPickerAction),
}
@ -74,7 +75,7 @@ impl ContextPickerEntry {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ContextPickerMode {
pub(crate) enum ContextPickerMode {
File,
Symbol,
Fetch,
@ -83,7 +84,7 @@ enum ContextPickerMode {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ContextPickerAction {
pub(crate) enum ContextPickerAction {
AddSelections,
}
@ -531,7 +532,7 @@ impl ContextPicker {
return vec![];
};
recent_context_picker_entries(
recent_context_picker_entries_with_store(
context_store,
self.thread_store.clone(),
self.text_thread_store.clone(),
@ -585,7 +586,8 @@ impl Render for ContextPicker {
})
}
}
enum RecentEntry {
pub(crate) enum RecentEntry {
File {
project_path: ProjectPath,
path_prefix: Arc<str>,
@ -593,7 +595,7 @@ enum RecentEntry {
Thread(ThreadContextEntry),
}
fn available_context_picker_entries(
pub(crate) fn available_context_picker_entries(
prompt_store: &Option<Entity<PromptStore>>,
thread_store: &Option<WeakEntity<ThreadStore>>,
workspace: &Entity<Workspace>,
@ -630,24 +632,56 @@ fn available_context_picker_entries(
entries
}
fn recent_context_picker_entries(
fn recent_context_picker_entries_with_store(
context_store: Entity<ContextStore>,
thread_store: Option<WeakEntity<ThreadStore>>,
text_thread_store: Option<WeakEntity<TextThreadStore>>,
workspace: Entity<Workspace>,
exclude_path: Option<ProjectPath>,
cx: &App,
) -> Vec<RecentEntry> {
let project = workspace.read(cx).project();
let mut exclude_paths = context_store.read(cx).file_paths(cx);
exclude_paths.extend(exclude_path);
let exclude_paths = exclude_paths
.into_iter()
.filter_map(|project_path| project.read(cx).absolute_path(&project_path, cx))
.collect();
let exclude_threads = context_store.read(cx).thread_ids();
recent_context_picker_entries(
thread_store,
text_thread_store,
workspace,
&exclude_paths,
exclude_threads,
cx,
)
}
pub(crate) fn recent_context_picker_entries(
thread_store: Option<WeakEntity<ThreadStore>>,
text_thread_store: Option<WeakEntity<TextThreadStore>>,
workspace: Entity<Workspace>,
exclude_paths: &HashSet<PathBuf>,
exclude_threads: &HashSet<ThreadId>,
cx: &App,
) -> Vec<RecentEntry> {
let mut recent = Vec::with_capacity(6);
let mut current_files = context_store.read(cx).file_paths(cx);
current_files.extend(exclude_path);
let workspace = workspace.read(cx);
let project = workspace.project().read(cx);
recent.extend(
workspace
.recent_navigation_history_iter(cx)
.filter(|(path, _)| !current_files.contains(path))
.filter(|(_, abs_path)| {
abs_path
.as_ref()
.map_or(true, |path| !exclude_paths.contains(path.as_path()))
})
.take(4)
.filter_map(|(project_path, _)| {
project
@ -659,8 +693,6 @@ fn recent_context_picker_entries(
}),
);
let current_threads = context_store.read(cx).thread_ids();
let active_thread_id = workspace
.panel::<AgentPanel>(cx)
.and_then(|panel| Some(panel.read(cx).active_thread(cx)?.read(cx).id()));
@ -672,7 +704,7 @@ fn recent_context_picker_entries(
let mut threads = unordered_thread_entries(thread_store, text_thread_store, cx)
.filter(|(_, thread)| match thread {
ThreadContextEntry::Thread { id, .. } => {
Some(id) != active_thread_id && !current_threads.contains(id)
Some(id) != active_thread_id && !exclude_threads.contains(id)
}
ThreadContextEntry::Context { .. } => true,
})
@ -710,7 +742,7 @@ fn add_selections_as_context(
})
}
fn selection_ranges(
pub(crate) fn selection_ranges(
workspace: &Entity<Workspace>,
cx: &mut App,
) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {

View file

@ -35,7 +35,7 @@ use super::symbol_context_picker::search_symbols;
use super::thread_context_picker::{ThreadContextEntry, ThreadMatch, search_threads};
use super::{
ContextPickerAction, ContextPickerEntry, ContextPickerMode, MentionLink, RecentEntry,
available_context_picker_entries, recent_context_picker_entries, selection_ranges,
available_context_picker_entries, recent_context_picker_entries_with_store, selection_ranges,
};
use crate::message_editor::ContextCreasesAddon;
@ -787,7 +787,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
.and_then(|b| b.read(cx).file())
.map(|file| ProjectPath::from_file(file.as_ref(), cx));
let recent_entries = recent_context_picker_entries(
let recent_entries = recent_context_picker_entries_with_store(
context_store.clone(),
thread_store.clone(),
text_thread_store.clone(),