Load threads and rule contents

Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
Agus Zubiaga 2025-08-12 18:34:58 -03:00
parent b4d97c437d
commit 98ba2d9acd
7 changed files with 185 additions and 60 deletions

View file

@ -18,6 +18,7 @@ test-support = ["gpui/test-support", "project/test-support"]
[dependencies]
action_log.workspace = true
agent-client-protocol.workspace = true
agent.workspace = true
anyhow.workspace = true
buffer_diff.workspace = true
editor.workspace = true
@ -28,6 +29,7 @@ language.workspace = true
language_model.workspace = true
markdown.workspace = true
project.workspace = true
prompt_store.workspace = true
serde.workspace = true
serde_json.workspace = true
settings.workspace = true
@ -36,6 +38,7 @@ terminal.workspace = true
ui.workspace = true
url.workspace = true
util.workspace = true
uuid.workspace = true
workspace-hack.workspace = true
[dev-dependencies]

View file

@ -1,8 +1,11 @@
use agent::ThreadId;
use anyhow::{Context as _, Result, bail};
use prompt_store::{PromptId, UserPromptId};
use std::{
ops::Range,
path::{Path, PathBuf},
};
use url::Url;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MentionUri {
@ -12,9 +15,18 @@ pub enum MentionUri {
name: String,
line_range: Range<u32>,
},
Thread(String),
TextThread(PathBuf),
Rule(String),
Thread {
id: ThreadId,
name: String,
},
TextThread {
path: PathBuf,
name: String,
},
Rule {
id: PromptId,
name: String,
},
Selection {
path: PathBuf,
line_range: Range<u32>,
@ -44,33 +56,42 @@ impl MentionUri {
.context("Parsing line range end")?
.checked_sub(1)
.context("Line numbers should be 1-based")?;
let pairs = url.query_pairs().collect::<Vec<_>>();
match pairs.as_slice() {
[] => Ok(Self::Selection {
if let Some(name) = single_query_param(&url, "symbol")? {
Ok(Self::Symbol {
name,
path: path.into(),
line_range,
}),
[(k, v)] => {
if k != "symbol" {
bail!("invalid query parameter")
}
Ok(Self::Symbol {
name: v.to_string(),
path: path.into(),
line_range,
})
}
_ => bail!("too many query pairs"),
})
} else {
Ok(Self::Selection {
path: path.into(),
line_range,
})
}
} else {
Ok(Self::File(path.into()))
}
}
"zed" => {
if let Some(thread) = path.strip_prefix("/agent/thread/") {
Ok(Self::Thread(thread.into()))
} else if let Some(rule) = path.strip_prefix("/agent/rule/") {
Ok(Self::Rule(rule.into()))
if let Some(thread_id) = path.strip_prefix("/agent/thread/") {
let name = single_query_param(&url, "name")?.context("Missing thread name")?;
Ok(Self::Thread {
id: thread_id.into(),
name,
})
} else if let Some(path) = path.strip_prefix("/agent/text-thread/") {
let name = single_query_param(&url, "name")?.context("Missing thread name")?;
Ok(Self::TextThread {
path: path.into(),
name,
})
} else if let Some(rule_id) = path.strip_prefix("/agent/rule/") {
let name = single_query_param(&url, "name")?.context("Missing rule name")?;
let rule_id = UserPromptId(rule_id.parse()?);
Ok(Self::Rule {
id: rule_id.into(),
name,
})
} else {
bail!("invalid zed url: {:?}", input);
}
@ -79,7 +100,7 @@ impl MentionUri {
}
}
pub fn name(&self) -> String {
fn name(&self) -> String {
match self {
MentionUri::File(path) => path
.file_name()
@ -87,9 +108,10 @@ impl MentionUri {
.to_string_lossy()
.into_owned(),
MentionUri::Symbol { name, .. } => name.clone(),
MentionUri::Thread(thread) => thread.to_string(),
MentionUri::TextThread(thread) => thread.display().to_string(),
MentionUri::Rule(rule) => rule.clone(),
// todo! better names
MentionUri::Thread { name, .. } => name.clone(),
MentionUri::TextThread { name, .. } => name.clone(),
MentionUri::Rule { name, .. } => name.clone(),
MentionUri::Selection {
path, line_range, ..
} => selection_name(path, line_range),
@ -129,19 +151,34 @@ impl MentionUri {
line_range.end + 1,
)
}
MentionUri::Thread(thread) => {
format!("zed:///agent/thread/{}", thread)
MentionUri::Thread { name, id } => {
format!("zed:///agent/thread/{id}?name={name}")
}
MentionUri::TextThread(path) => {
format!("zed:///agent/text-thread/{}", path.display())
MentionUri::TextThread { path, name } => {
format!("zed:///agent/text-thread/{}?name={name}", path.display())
}
MentionUri::Rule(rule) => {
format!("zed:///agent/rule/{}", rule)
MentionUri::Rule { name, id } => {
format!("zed:///agent/rule/{id}?name={name}")
}
}
}
}
fn single_query_param(url: &Url, name: &'static str) -> Result<Option<String>> {
let pairs = url.query_pairs().collect::<Vec<_>>();
match pairs.as_slice() {
[] => Ok(None),
[(k, v)] => {
if k != name {
bail!("invalid query parameter")
}
Ok(Some(v.to_string()))
}
_ => bail!("too many query pairs"),
}
}
pub fn selection_name(path: &Path, line_range: &Range<u32>) -> String {
format!(
"{} ({}:{})",
@ -203,10 +240,16 @@ mod tests {
#[test]
fn test_parse_thread_uri() {
let thread_uri = "zed:///agent/thread/session123";
let thread_uri = "zed:///agent/thread/session123?name=Thread%20name";
let parsed = MentionUri::parse(thread_uri).unwrap();
match &parsed {
MentionUri::Thread(thread_id) => assert_eq!(thread_id, "session123"),
MentionUri::Thread {
id: thread_id,
name,
} => {
assert_eq!(thread_id.to_string(), "session123");
assert_eq!(name, "Thread name");
}
_ => panic!("Expected Thread variant"),
}
assert_eq!(parsed.to_uri(), thread_uri);
@ -214,10 +257,13 @@ mod tests {
#[test]
fn test_parse_rule_uri() {
let rule_uri = "zed:///agent/rule/my_rule";
let rule_uri = "zed:///agent/rule/d8694ff2-90d5-4b6f-be33-33c1763acd52?name=Some%20rule";
let parsed = MentionUri::parse(rule_uri).unwrap();
match &parsed {
MentionUri::Rule(rule) => assert_eq!(rule, "my_rule"),
MentionUri::Rule { id, name } => {
assert_eq!(id.to_string(), "d8694ff2-90d5-4b6f-be33-33c1763acd52");
assert_eq!(name, "Some rule");
}
_ => panic!("Expected Rule variant"),
}
assert_eq!(parsed.to_uri(), rule_uri);

View file

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

View file

@ -4,10 +4,10 @@ use std::sync::Arc;
use std::sync::atomic::AtomicBool;
use acp_thread::{MentionUri, selection_name};
use anyhow::{Context as _, Result};
use anyhow::{Context as _, Result, anyhow};
use collections::{HashMap, HashSet};
use editor::display_map::CreaseId;
use editor::{AnchorRangeExt, CompletionProvider, Editor, ExcerptId, ToOffset as _, ToPoint};
use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _};
use file_icons::FileIcons;
use futures::future::try_join_all;
use fuzzy::{StringMatch, StringMatchCandidate};
@ -21,7 +21,7 @@ use project::{
};
use prompt_store::PromptStore;
use rope::Point;
use text::{Anchor, ToPoint as _};
use text::{Anchor, OffsetRangeExt as _, ToPoint as _};
use ui::prelude::*;
use workspace::Workspace;
@ -59,14 +59,16 @@ impl MentionSet {
pub fn contents(
&self,
project: Entity<Project>,
thread_store: Entity<ThreadStore>,
text_thread_store: Entity<TextThreadStore>,
window: &mut Window,
cx: &mut App,
) -> Task<Result<HashMap<CreaseId, Mention>>> {
let contents = self
.uri_by_crease_id
.iter()
.map(|(crease_id, uri)| match uri {
.map(|(&crease_id, uri)| match uri {
MentionUri::File(path) => {
let crease_id = *crease_id;
let uri = uri.clone();
let path = path.to_path_buf();
let buffer_task = project.update(cx, |project, cx| {
@ -89,7 +91,6 @@ impl MentionSet {
| MentionUri::Selection {
path, line_range, ..
} => {
let crease_id = *crease_id;
let uri = uri.clone();
let path_buf = path.clone();
let line_range = line_range.clone();
@ -118,9 +119,44 @@ impl MentionSet {
anyhow::Ok((crease_id, Mention { uri, content }))
})
}
MentionUri::Thread(_) => todo!(),
MentionUri::TextThread(path_buf) => todo!(),
MentionUri::Rule(_) => todo!(),
MentionUri::Thread { id: thread_id, .. } => {
let open_task = thread_store.update(cx, |thread_store, cx| {
thread_store.open_thread(&thread_id, window, cx)
});
let uri = uri.clone();
cx.spawn(async move |cx| {
let thread = open_task.await?;
let content = thread.read_with(cx, |thread, _cx| {
thread.latest_detailed_summary_or_text().to_string()
})?;
anyhow::Ok((crease_id, Mention { uri, content }))
})
}
MentionUri::TextThread { path, .. } => {
let context = text_thread_store.update(cx, |text_thread_store, cx| {
text_thread_store.open_local_context(path.as_path().into(), cx)
});
let uri = uri.clone();
cx.spawn(async move |cx| {
let context = context.await?;
let xml = context.update(cx, |context, cx| context.to_xml(cx))?;
anyhow::Ok((crease_id, Mention { uri, content: xml }))
})
}
MentionUri::Rule { id: prompt_id, .. } => {
let Some(prompt_store) = thread_store.read(cx).prompt_store().clone() else {
return Task::ready(Err(anyhow!("missing prompt store")));
};
let text_task = prompt_store.read(cx).load(prompt_id.clone(), cx);
let uri = uri.clone();
cx.spawn(async move |_| {
// TODO: report load errors instead of just logging
let text = text_task.await?;
anyhow::Ok((crease_id, Mention { uri, content: text }))
})
}
})
.collect::<Vec<_>>();
@ -434,7 +470,12 @@ impl ContextPickerCompletionProvider {
file.path().to_path_buf()
});
let point_range = range.to_point(&snapshot);
let point_range = snapshot
.as_singleton()
.map(|(_, _, snapshot)| {
selection_range.to_point(&snapshot)
})
.unwrap_or_default();
let line_range = point_range.start.row..point_range.end.row;
let crease = crate::context_picker::crease_for_mention(
selection_name(&path, &line_range).into(),
@ -505,8 +546,14 @@ impl ContextPickerCompletionProvider {
};
let uri = match &thread_entry {
ThreadContextEntry::Thread { id, .. } => MentionUri::Thread(id.to_string()),
ThreadContextEntry::Context { path, .. } => MentionUri::TextThread(path.to_path_buf()),
ThreadContextEntry::Thread { id, title } => MentionUri::Thread {
id: id.clone(),
name: title.to_string(),
},
ThreadContextEntry::Context { path, title } => MentionUri::TextThread {
path: path.to_path_buf(),
name: title.to_string(),
},
};
let new_text = format!("{} ", uri.to_link());
@ -533,26 +580,29 @@ impl ContextPickerCompletionProvider {
}
fn completion_for_rules(
rules: RulesContextEntry,
rule: RulesContextEntry,
excerpt_id: ExcerptId,
source_range: Range<Anchor>,
editor: Entity<Editor>,
mention_set: Arc<Mutex<MentionSet>>,
) -> Completion {
let uri = MentionUri::Rule(rules.prompt_id.0.to_string());
let uri = MentionUri::Rule {
id: rule.prompt_id.into(),
name: rule.title.to_string(),
};
let new_text = format!("{} ", uri.to_link());
let new_text_len = new_text.len();
Completion {
replace_range: source_range.clone(),
new_text,
label: CodeLabel::plain(rules.title.to_string(), None),
label: CodeLabel::plain(rule.title.to_string(), None),
documentation: None,
insert_text_mode: None,
source: project::CompletionSource::Custom,
icon_path: Some(RULES_ICON.path().into()),
confirm: Some(confirm_completion_callback(
RULES_ICON.path().into(),
rules.title.clone(),
rule.title.clone(),
excerpt_id,
source_range.start,
new_text_len - 1,
@ -657,8 +707,7 @@ impl ContextPickerCompletionProvider {
file_name.to_string()
};
let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
let mut label = CodeLabel::plain(symbol.name.clone(), None);
let label = CodeLabel::plain(symbol.name.clone(), None);
let uri = MentionUri::Symbol {
path: full_path.into(),
@ -754,8 +803,8 @@ impl CompletionProvider for ContextPickerCompletionProvider {
MentionUri::File(path) => {
excluded_paths.insert(path.clone());
}
MentionUri::Thread(thread) => {
excluded_threads.insert(thread.as_str().into());
MentionUri::Thread { id, .. } => {
excluded_threads.insert(id.clone());
}
_ => {}
}

View file

@ -59,6 +59,8 @@ pub struct AcpThreadView {
agent: Rc<dyn AgentServer>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
thread_store: WeakEntity<ThreadStore>,
text_thread_store: WeakEntity<TextThreadStore>,
thread_state: ThreadState,
diff_editors: HashMap<EntityId, Entity<Editor>>,
terminal_views: HashMap<EntityId, Entity<TerminalView>>,
@ -189,6 +191,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,
message_set_from_history: None,
@ -383,7 +387,17 @@ 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 Some(thread_store) = self.thread_store.upgrade() else {
return;
};
let Some(text_thread_store) = self.text_thread_store.upgrade() else {
return;
};
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 {

View file

@ -90,6 +90,16 @@ impl From<Uuid> for UserPromptId {
}
}
// todo! remove me
impl std::fmt::Display for PromptId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PromptId::User { uuid } => write!(f, "{}", uuid.0),
PromptId::EditWorkflow => write!(f, "Edit workflow"),
}
}
}
pub struct PromptStore {
env: heed::Env,
metadata_cache: RwLock<MetadataCache>,