Load threads and rule contents
Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
parent
b4d97c437d
commit
98ba2d9acd
7 changed files with 185 additions and 60 deletions
|
@ -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]
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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{}",
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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>,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue