diff --git a/Cargo.lock b/Cargo.lock index 9aa5a6f277..cfbd5b653f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -392,6 +392,7 @@ dependencies = [ "ui", "ui_input", "unindent", + "url", "urlencoding", "util", "uuid", diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 2c7c1ba437..3e9d93d633 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -32,6 +32,9 @@ pub enum MentionUri { path: PathBuf, line_range: Range, }, + Fetch { + url: Url, + }, } impl MentionUri { @@ -97,6 +100,7 @@ impl MentionUri { bail!("invalid zed url: {:?}", input); } } + "http" | "https" => Ok(MentionUri::Fetch { url }), other => bail!("unrecognized scheme {:?}", other), } } @@ -115,6 +119,7 @@ impl MentionUri { MentionUri::Selection { path, line_range, .. } => selection_name(path, line_range), + MentionUri::Fetch { url } => url.to_string(), } } @@ -172,6 +177,7 @@ impl MentionUri { url.query_pairs_mut().append_pair("name", name); url } + MentionUri::Fetch { url } => url.clone(), } } } @@ -289,11 +295,37 @@ mod tests { assert_eq!(parsed.to_uri().to_string(), rule_uri); } + #[test] + fn test_parse_fetch_http_uri() { + let http_uri = "http://example.com/path?query=value#fragment"; + let parsed = MentionUri::parse(http_uri).unwrap(); + match &parsed { + MentionUri::Fetch { url } => { + assert_eq!(url.to_string(), http_uri); + } + _ => panic!("Expected Fetch variant"), + } + assert_eq!(parsed.to_uri().to_string(), http_uri); + } + + #[test] + fn test_parse_fetch_https_uri() { + let https_uri = "https://example.com/api/endpoint"; + let parsed = MentionUri::parse(https_uri).unwrap(); + match &parsed { + MentionUri::Fetch { url } => { + assert_eq!(url.to_string(), https_uri); + } + _ => panic!("Expected Fetch variant"), + } + assert_eq!(parsed.to_uri().to_string(), https_uri); + } + #[test] fn test_invalid_scheme() { - assert!(MentionUri::parse("http://example.com").is_err()); - assert!(MentionUri::parse("https://example.com").is_err()); assert!(MentionUri::parse("ftp://example.com").is_err()); + assert!(MentionUri::parse("ssh://example.com").is_err()); + assert!(MentionUri::parse("unknown://example.com").is_err()); } #[test] diff --git a/crates/agent2/src/thread.rs b/crates/agent2/src/thread.rs index 246df2cac5..f8b2366e8a 100644 --- a/crates/agent2/src/thread.rs +++ b/crates/agent2/src/thread.rs @@ -1151,12 +1151,14 @@ impl AgentMessage { const OPEN_FILES_TAG: &str = ""; const OPEN_SYMBOLS_TAG: &str = ""; const OPEN_THREADS_TAG: &str = ""; + const OPEN_FETCH_TAG: &str = ""; const OPEN_RULES_TAG: &str = "\nThe user has specified the following rules that should be applied:\n"; let mut file_context = OPEN_FILES_TAG.to_string(); let mut symbol_context = OPEN_SYMBOLS_TAG.to_string(); let mut thread_context = OPEN_THREADS_TAG.to_string(); + let mut fetch_context = OPEN_FETCH_TAG.to_string(); let mut rules_context = OPEN_RULES_TAG.to_string(); for chunk in &self.content { @@ -1226,6 +1228,17 @@ impl AgentMessage { ) .ok(); } + MentionUri::Fetch { url } => { + write!( + &mut fetch_context, + "\n{}", + MarkdownCodeBlock { + tag: &format!("md {url}"), + text: &content + } + ) + .ok(); + } } language_model::MessageContent::Text(uri.as_link().to_string()) @@ -1258,6 +1271,13 @@ impl AgentMessage { .push(language_model::MessageContent::Text(thread_context)); } + if fetch_context.len() > OPEN_FETCH_TAG.len() { + fetch_context.push_str("\n"); + message + .content + .push(language_model::MessageContent::Text(fetch_context)); + } + if rules_context.len() > OPEN_RULES_TAG.len() { rules_context.push_str("\n"); message diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index fa75a7707c..b6a5710aa4 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -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 diff --git a/crates/agent_ui/src/acp/completion_provider.rs b/crates/agent_ui/src/acp/completion_provider.rs index 315ab9346d..0636975392 100644 --- a/crates/agent_ui/src/acp/completion_provider.rs +++ b/crates/agent_ui/src/acp/completion_provider.rs @@ -12,6 +12,7 @@ use file_icons::FileIcons; use futures::future::try_join_all; use fuzzy::{StringMatch, StringMatchCandidate}; use gpui::{App, Entity, Task, WeakEntity}; +use http_client::HttpClientWithUrl; use itertools::Itertools as _; use language::{Buffer, CodeLabel, HighlightId}; use lsp::CompletionContext; @@ -23,6 +24,7 @@ use prompt_store::PromptStore; use rope::Point; use text::{Anchor, OffsetRangeExt as _, ToPoint as _}; use ui::prelude::*; +use url::Url; use workspace::Workspace; use agent::{ @@ -45,6 +47,7 @@ use crate::context_picker::{ #[derive(Default)] pub struct MentionSet { uri_by_crease_id: HashMap, + fetch_results: HashMap, } impl MentionSet { @@ -67,95 +70,110 @@ impl MentionSet { let contents = self .uri_by_crease_id .iter() - .map(|(&crease_id, uri)| match uri { - MentionUri::File(path) => { - let uri = uri.clone(); - let path = path.to_path_buf(); - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(path, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); + .map(|(&crease_id, uri)| { + match uri { + MentionUri::File(path) => { + let uri = uri.clone(); + let path = path.to_path_buf(); + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(path, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| buffer.text())?; - anyhow::Ok((crease_id, Mention { uri, content })) - }) - } - MentionUri::Symbol { - path, line_range, .. - } - | MentionUri::Selection { - path, line_range, .. - } => { - let uri = uri.clone(); - let path_buf = path.clone(); - let line_range = line_range.clone(); + anyhow::Ok((crease_id, Mention { uri, content })) + }) + } + MentionUri::Symbol { + path, line_range, .. + } + | MentionUri::Selection { + path, line_range, .. + } => { + let uri = uri.clone(); + let path_buf = path.clone(); + let line_range = line_range.clone(); - let buffer_task = project.update(cx, |project, cx| { - let path = project - .find_project_path(&path_buf, cx) - .context("Failed to find project path")?; - anyhow::Ok(project.open_buffer(path, cx)) - }); + let buffer_task = project.update(cx, |project, cx| { + let path = project + .find_project_path(&path_buf, cx) + .context("Failed to find project path")?; + anyhow::Ok(project.open_buffer(path, cx)) + }); - cx.spawn(async move |cx| { - let buffer = buffer_task?.await?; - let content = buffer.read_with(cx, |buffer, _cx| { - buffer - .text_for_range( - Point::new(line_range.start, 0) - ..Point::new( - line_range.end, - buffer.line_len(line_range.end), - ), - ) - .collect() - })?; + cx.spawn(async move |cx| { + let buffer = buffer_task?.await?; + let content = buffer.read_with(cx, |buffer, _cx| { + buffer + .text_for_range( + Point::new(line_range.start, 0) + ..Point::new( + line_range.end, + buffer.line_len(line_range.end), + ), + ) + .collect() + })?; - anyhow::Ok((crease_id, Mention { uri, content })) - }) - } - MentionUri::Thread { id: thread_id, .. } => { - let open_task = thread_store.update(cx, |thread_store, cx| { - thread_store.open_thread(&thread_id, window, cx) - }); + anyhow::Ok((crease_id, Mention { uri, content })) + }) + } + 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() - })?; + 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 })) - }) + 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 })) + }) + } + MentionUri::Fetch { url } => { + let Some(content) = self.fetch_results.get(&url) else { + return Task::ready(Err(anyhow!("missing fetch result"))); + }; + Task::ready(Ok(( + crease_id, + Mention { + uri: uri.clone(), + content: content.clone(), + }, + ))) + } } }) .collect::>(); @@ -177,6 +195,7 @@ pub(crate) enum Match { File(FileMatch), Symbol(SymbolMatch), Thread(ThreadMatch), + Fetch(SharedString), Rules(RulesContextEntry), Entry(EntryMatch), } @@ -194,6 +213,7 @@ impl Match { Match::Thread(_) => 1., Match::Symbol(_) => 1., Match::Rules(_) => 1., + Match::Fetch(_) => 1., } } } @@ -721,6 +741,40 @@ impl ContextPickerCompletionProvider { )), }) } + + fn completion_for_fetch( + source_range: Range, + url_to_fetch: SharedString, + excerpt_id: ExcerptId, + editor: Entity, + mention_set: Arc>, + http_client: Arc, + ) -> Option { + let url = url::Url::parse(url_to_fetch.as_ref()).ok()?; + let mention_uri = MentionUri::Fetch { url }; + let new_text = format!("{} ", mention_uri.as_link()); + let new_text_len = new_text.len(); + Some(Completion { + replace_range: source_range.clone(), + new_text, + label: CodeLabel::plain(url_to_fetch.to_string(), None), + documentation: None, + source: project::CompletionSource::Custom, + icon_path: Some(IconName::ToolWeb.path().into()), + insert_text_mode: None, + // todo! custom callback to fetch + confirm: Some(confirm_completion_callback( + IconName::ToolWeb.path().into(), + url_to_fetch.clone(), + excerpt_id, + source_range.start, + new_text_len - 1, + editor.clone(), + mention_set, + mention_uri, + )), + }) + } } fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel { @@ -885,6 +939,15 @@ impl CompletionProvider for ContextPickerCompletionProvider { mention_set.clone(), )), + Match::Fetch(url) => Self::completion_for_fetch( + source_range.clone(), + url, + excerpt_id, + editor.clone(), + mention_set.clone(), + todo!(), + ), + Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry( entry, excerpt_id, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index df2ae07574..940ac7135f 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -2701,6 +2701,9 @@ impl AcpThreadView { cx, ) } + MentionUri::Fetch { url } => { + cx.open_url(url.as_str()); + } }) } else { cx.open_url(&url);