Checkpoint: Adding fetch

This commit is contained in:
Agus Zubiaga 2025-08-13 12:28:17 -03:00
parent 5d5c419fa9
commit 2f6c9e3a2b
6 changed files with 204 additions and 84 deletions

1
Cargo.lock generated
View file

@ -392,6 +392,7 @@ dependencies = [
"ui",
"ui_input",
"unindent",
"url",
"urlencoding",
"util",
"uuid",

View file

@ -32,6 +32,9 @@ pub enum MentionUri {
path: PathBuf,
line_range: Range<u32>,
},
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]

View file

@ -1151,12 +1151,14 @@ impl AgentMessage {
const OPEN_FILES_TAG: &str = "<files>";
const OPEN_SYMBOLS_TAG: &str = "<symbols>";
const OPEN_THREADS_TAG: &str = "<threads>";
const OPEN_FETCH_TAG: &str = "<fetched_urls>";
const OPEN_RULES_TAG: &str =
"<rules>\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("</fetched_urls>\n");
message
.content
.push(language_model::MessageContent::Text(fetch_context));
}
if rules_context.len() > OPEN_RULES_TAG.len() {
rules_context.push_str("</user_rules>\n");
message

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

View file

@ -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<CreaseId, MentionUri>,
fetch_results: HashMap<Url, String>,
}
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::<Vec<_>>();
@ -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<Anchor>,
url_to_fetch: SharedString,
excerpt_id: ExcerptId,
editor: Entity<Editor>,
mention_set: Arc<Mutex<MentionSet>>,
http_client: Arc<HttpClientWithUrl>,
) -> Option<Completion> {
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,

View file

@ -2701,6 +2701,9 @@ impl AcpThreadView {
cx,
)
}
MentionUri::Fetch { url } => {
cx.open_url(url.as_str());
}
})
} else {
cx.open_url(&url);