Threads and rule support

Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
Agus Zubiaga 2025-08-12 15:47:27 -03:00
parent e2973998ad
commit 91e22597a8
5 changed files with 68 additions and 98 deletions

View file

@ -1,4 +1,3 @@
use agent_client_protocol as acp;
use anyhow::{Result, bail}; use anyhow::{Result, bail};
use std::path::PathBuf; use std::path::PathBuf;
@ -6,7 +5,8 @@ use std::path::PathBuf;
pub enum MentionUri { pub enum MentionUri {
File(PathBuf), File(PathBuf),
Symbol(PathBuf, String), Symbol(PathBuf, String),
Thread(acp::SessionId), Thread(String),
TextThread(PathBuf),
Rule(String), Rule(String),
} }
@ -24,7 +24,7 @@ impl MentionUri {
} }
"zed" => { "zed" => {
if let Some(thread) = path.strip_prefix("/agent/thread/") { if let Some(thread) = path.strip_prefix("/agent/thread/") {
Ok(Self::Thread(acp::SessionId(thread.into()))) Ok(Self::Thread(thread.into()))
} else if let Some(rule) = path.strip_prefix("/agent/rule/") { } else if let Some(rule) = path.strip_prefix("/agent/rule/") {
Ok(Self::Rule(rule.into())) Ok(Self::Rule(rule.into()))
} else { } else {
@ -40,6 +40,7 @@ impl MentionUri {
MentionUri::File(path) => path.file_name().unwrap().to_string_lossy().into_owned(), MentionUri::File(path) => path.file_name().unwrap().to_string_lossy().into_owned(),
MentionUri::Symbol(_path, name) => name.clone(), MentionUri::Symbol(_path, name) => name.clone(),
MentionUri::Thread(thread) => thread.to_string(), MentionUri::Thread(thread) => thread.to_string(),
MentionUri::TextThread(thread) => thread.display().to_string(),
MentionUri::Rule(rule) => rule.clone(), MentionUri::Rule(rule) => rule.clone(),
} }
} }
@ -60,7 +61,10 @@ impl MentionUri {
format!("file://{}#{}", path.display(), name) format!("file://{}#{}", path.display(), name)
} }
MentionUri::Thread(thread) => { MentionUri::Thread(thread) => {
format!("zed:///agent/thread/{}", thread.0) format!("zed:///agent/thread/{}", thread)
}
MentionUri::TextThread(path) => {
format!("zed:///agent/text-thread/{}", path.display())
} }
MentionUri::Rule(rule) => { MentionUri::Rule(rule) => {
format!("zed:///agent/rule/{}", rule) format!("zed:///agent/rule/{}", rule)
@ -100,7 +104,7 @@ mod tests {
let thread_uri = "zed:///agent/thread/session123"; let thread_uri = "zed:///agent/thread/session123";
let parsed = MentionUri::parse(thread_uri).unwrap(); let parsed = MentionUri::parse(thread_uri).unwrap();
match &parsed { match &parsed {
MentionUri::Thread(session_id) => assert_eq!(session_id.0.as_ref(), "session123"), MentionUri::Thread(thread_id) => assert_eq!(thread_id, "session123"),
_ => panic!("Expected Thread variant"), _ => panic!("Expected Thread variant"),
} }
assert_eq!(parsed.to_uri(), thread_uri); assert_eq!(parsed.to_uri(), thread_uri);

View file

@ -1196,6 +1196,9 @@ impl AgentMessage {
MentionUri::Thread(_session_id) => { MentionUri::Thread(_session_id) => {
write!(&mut thread_context, "\n{}\n", content).ok(); write!(&mut thread_context, "\n{}\n", content).ok();
} }
MentionUri::TextThread(_session_id) => {
write!(&mut thread_context, "\n{}\n", content).ok();
}
MentionUri::Rule(_user_prompt_id) => { MentionUri::Rule(_user_prompt_id) => {
write!( write!(
&mut rules_context, &mut rules_context,

View file

@ -306,8 +306,8 @@ fn search(
pub struct ContextPickerCompletionProvider { pub struct ContextPickerCompletionProvider {
mention_set: Arc<Mutex<MentionSet>>, mention_set: Arc<Mutex<MentionSet>>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>, thread_store: WeakEntity<ThreadStore>,
text_thread_store: Option<WeakEntity<TextThreadStore>>, text_thread_store: WeakEntity<TextThreadStore>,
editor: WeakEntity<Editor>, editor: WeakEntity<Editor>,
} }
@ -315,8 +315,8 @@ impl ContextPickerCompletionProvider {
pub fn new( pub fn new(
mention_set: Arc<Mutex<MentionSet>>, mention_set: Arc<Mutex<MentionSet>>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
thread_store: Option<WeakEntity<ThreadStore>>, thread_store: WeakEntity<ThreadStore>,
text_thread_store: Option<WeakEntity<TextThreadStore>>, text_thread_store: WeakEntity<TextThreadStore>,
editor: WeakEntity<Editor>, editor: WeakEntity<Editor>,
) -> Self { ) -> Self {
Self { Self {
@ -474,77 +474,39 @@ impl ContextPickerCompletionProvider {
recent: bool, recent: bool,
editor: Entity<Editor>, editor: Entity<Editor>,
mention_set: Arc<Mutex<MentionSet>>, mention_set: Arc<Mutex<MentionSet>>,
thread_store: Entity<ThreadStore>,
text_thread_store: Entity<TextThreadStore>,
) -> Completion { ) -> Completion {
todo!(); let icon_for_completion = if recent {
// let icon_for_completion = if recent { IconName::HistoryRerun
// IconName::HistoryRerun } else {
// } else { IconName::Thread
// IconName::Thread };
// };
// let new_text = format!("{} ", MentionUri::Thread(thread_id)); let uri = match &thread_entry {
ThreadContextEntry::Thread { id, .. } => MentionUri::Thread(id.to_string()),
ThreadContextEntry::Context { path, .. } => MentionUri::TextThread(path.to_path_buf()),
};
let new_text = format!("{} ", uri.to_link());
// let new_text_len = new_text.len(); let new_text_len = new_text.len();
// Completion { Completion {
// replace_range: source_range.clone(), replace_range: source_range.clone(),
// new_text, new_text,
// label: CodeLabel::plain(thread_entry.title().to_string(), None), label: CodeLabel::plain(thread_entry.title().to_string(), None),
// documentation: None, documentation: None,
// insert_text_mode: None, insert_text_mode: None,
// source: project::CompletionSource::Custom, source: project::CompletionSource::Custom,
// icon_path: Some(icon_for_completion.path().into()), icon_path: Some(icon_for_completion.path().into()),
// confirm: Some(confirm_completion_callback( confirm: Some(confirm_completion_callback(
// IconName::Thread.path().into(), IconName::Thread.path().into(),
// thread_entry.title().clone(), thread_entry.title().clone(),
// excerpt_id, excerpt_id,
// source_range.start, source_range.start,
// new_text_len - 1, new_text_len - 1,
// editor.clone(), editor.clone(),
// context_store.clone(), mention_set,
// move |window, cx| match &thread_entry { uri,
// ThreadContextEntry::Thread { id, .. } => { )),
// let thread_id = id.clone(); }
// let context_store = context_store.clone();
// let thread_store = thread_store.clone();
// window.spawn::<_, Option<_>>(cx, async move |cx| {
// let thread: Entity<Thread> = thread_store
// .update_in(cx, |thread_store, window, cx| {
// thread_store.open_thread(&thread_id, window, cx)
// })
// .ok()?
// .await
// .log_err()?;
// let context = context_store
// .update(cx, |context_store, cx| {
// context_store.add_thread(thread, false, cx)
// })
// .ok()??;
// Some(context)
// })
// }
// ThreadContextEntry::Context { path, .. } => {
// let path = path.clone();
// let context_store = context_store.clone();
// let text_thread_store = text_thread_store.clone();
// cx.spawn::<_, Option<_>>(async move |cx| {
// let thread = text_thread_store
// .update(cx, |store, cx| store.open_local_context(path, cx))
// .ok()?
// .await
// .log_err()?;
// let context = context_store
// .update(cx, |context_store, cx| {
// context_store.add_text_thread(thread, false, cx)
// })
// .ok()??;
// Some(context)
// })
// }
// },
// )),
// }
} }
fn completion_for_rules( fn completion_for_rules(
@ -555,7 +517,7 @@ impl ContextPickerCompletionProvider {
mention_set: Arc<Mutex<MentionSet>>, mention_set: Arc<Mutex<MentionSet>>,
) -> Completion { ) -> Completion {
let uri = MentionUri::Rule(rules.prompt_id.0.to_string()); let uri = MentionUri::Rule(rules.prompt_id.0.to_string());
let new_text = uri.to_link(); let new_text = format!("{} ", uri.to_link());
let new_text_len = new_text.len(); let new_text_len = new_text.len();
Completion { Completion {
replace_range: source_range.clone(), replace_range: source_range.clone(),
@ -769,7 +731,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
excluded_paths.insert(path.clone()); excluded_paths.insert(path.clone());
} }
MentionUri::Thread(thread) => { MentionUri::Thread(thread) => {
excluded_threads.insert(thread.0.as_ref().into()); excluded_threads.insert(thread.as_str().into());
} }
_ => {} _ => {}
} }
@ -850,21 +812,14 @@ impl CompletionProvider for ContextPickerCompletionProvider {
Match::Thread(ThreadMatch { Match::Thread(ThreadMatch {
thread, is_recent, .. thread, is_recent, ..
}) => { }) => Some(Self::completion_for_thread(
let thread_store = thread_store.as_ref().and_then(|t| t.upgrade())?; thread,
let text_thread_store = excerpt_id,
text_thread_store.as_ref().and_then(|t| t.upgrade())?; source_range.clone(),
Some(Self::completion_for_thread( is_recent,
thread, editor.clone(),
excerpt_id, mention_set.clone(),
source_range.clone(), )),
is_recent,
editor.clone(),
mention_set.clone(),
thread_store,
text_thread_store,
))
}
Match::Rules(user_rules) => Some(Self::completion_for_rules( Match::Rules(user_rules) => Some(Self::completion_for_rules(
user_rules, user_rules,

View file

@ -4,6 +4,7 @@ use acp_thread::{
}; };
use acp_thread::{AgentConnection, Plan}; use acp_thread::{AgentConnection, Plan};
use action_log::ActionLog; use action_log::ActionLog;
use agent::{TextThreadStore, ThreadStore};
use agent_client_protocol as acp; use agent_client_protocol as acp;
use agent_servers::AgentServer; use agent_servers::AgentServer;
use agent_settings::{AgentSettings, NotifyWhenAgentWaiting}; use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
@ -104,6 +105,8 @@ impl AcpThreadView {
agent: Rc<dyn AgentServer>, agent: Rc<dyn AgentServer>,
workspace: WeakEntity<Workspace>, workspace: WeakEntity<Workspace>,
project: Entity<Project>, project: Entity<Project>,
thread_store: WeakEntity<ThreadStore>,
text_thread_store: WeaksEntity<TextThreadStore>,
message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>, message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
min_lines: usize, min_lines: usize,
max_lines: Option<usize>, max_lines: Option<usize>,
@ -141,11 +144,9 @@ impl AcpThreadView {
editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new( editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
mention_set.clone(), mention_set.clone(),
workspace.clone(), workspace.clone(),
// todo! provide thread stores thread_store.clone(),
None, text_thread_store.clone(),
None,
cx.weak_entity(), cx.weak_entity(),
// None,
)))); ))));
editor.set_context_menu_options(ContextMenuOptions { editor.set_context_menu_options(ContextMenuOptions {
min_entries_visible: 12, min_entries_visible: 12,
@ -3579,6 +3580,8 @@ mod tests {
Rc::new(agent), Rc::new(agent),
workspace.downgrade(), workspace.downgrade(),
project, project,
WeakEntity::new_invalid(),
WeakEntity::new_invalid(),
Rc::new(RefCell::new(MessageHistory::default())), Rc::new(RefCell::new(MessageHistory::default())),
1, 1,
None, None,

View file

@ -924,6 +924,9 @@ impl AgentPanel {
agent: crate::ExternalAgent, 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| { cx.spawn_in(window, async move |this, cx| {
let server: Rc<dyn AgentServer> = match agent_choice { let server: Rc<dyn AgentServer> = match agent_choice {
Some(agent) => { Some(agent) => {
@ -962,6 +965,8 @@ impl AgentPanel {
server, server,
workspace.clone(), workspace.clone(),
project, project,
thread_store.clone(),
text_thread_store.clone(),
message_history, message_history,
MIN_EDITOR_LINES, MIN_EDITOR_LINES,
Some(MAX_EDITOR_LINES), Some(MAX_EDITOR_LINES),