Include mention context in acp-based native agent (#36006)

Also adds data-layer support for symbols, thread, and rules.

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>
This commit is contained in:
Agus Zubiaga 2025-08-12 10:12:58 -03:00 committed by GitHub
parent 2444321756
commit 44953375cc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 630 additions and 231 deletions

View file

@ -34,6 +34,7 @@ settings.workspace = true
smol.workspace = true
terminal.workspace = true
ui.workspace = true
url.workspace = true
util.workspace = true
workspace-hack.workspace = true

View file

@ -1,13 +1,15 @@
mod connection;
mod diff;
mod mention;
mod terminal;
pub use connection::*;
pub use diff::*;
pub use mention::*;
pub use terminal::*;
use action_log::ActionLog;
use agent_client_protocol as acp;
use agent_client_protocol::{self as acp};
use anyhow::{Context as _, Result};
use editor::Bias;
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
@ -21,12 +23,7 @@ use std::error::Error;
use std::fmt::Formatter;
use std::process::ExitStatus;
use std::rc::Rc;
use std::{
fmt::Display,
mem,
path::{Path, PathBuf},
sync::Arc,
};
use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
use ui::App;
use util::ResultExt;
@ -53,38 +50,6 @@ impl UserMessage {
}
}
#[derive(Debug)]
pub struct MentionPath<'a>(&'a Path);
impl<'a> MentionPath<'a> {
const PREFIX: &'static str = "@file:";
pub fn new(path: &'a Path) -> Self {
MentionPath(path)
}
pub fn try_parse(url: &'a str) -> Option<Self> {
let path = url.strip_prefix(Self::PREFIX)?;
Some(MentionPath(Path::new(path)))
}
pub fn path(&self) -> &Path {
self.0
}
}
impl Display for MentionPath<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"[@{}]({}{})",
self.0.file_name().unwrap_or_default().display(),
Self::PREFIX,
self.0.display()
)
}
}
#[derive(Debug, PartialEq)]
pub struct AssistantMessage {
pub chunks: Vec<AssistantMessageChunk>,
@ -367,16 +332,24 @@ impl ContentBlock {
) {
let new_content = match block {
acp::ContentBlock::Text(text_content) => text_content.text.clone(),
acp::ContentBlock::ResourceLink(resource_link) => {
if let Some(path) = resource_link.uri.strip_prefix("file://") {
format!("{}", MentionPath(path.as_ref()))
acp::ContentBlock::Resource(acp::EmbeddedResource {
resource:
acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents {
uri,
..
}),
..
}) => {
if let Some(uri) = MentionUri::parse(&uri).log_err() {
uri.to_link()
} else {
resource_link.uri.clone()
uri.clone()
}
}
acp::ContentBlock::Image(_)
| acp::ContentBlock::Audio(_)
| acp::ContentBlock::Resource(_) => String::new(),
| acp::ContentBlock::Resource(acp::EmbeddedResource { .. })
| acp::ContentBlock::ResourceLink(_) => String::new(),
};
match self {
@ -1329,7 +1302,7 @@ mod tests {
use serde_json::json;
use settings::SettingsStore;
use smol::stream::StreamExt as _;
use std::{cell::RefCell, rc::Rc, time::Duration};
use std::{cell::RefCell, path::Path, rc::Rc, time::Duration};
use util::path;

View file

@ -0,0 +1,122 @@
use agent_client_protocol as acp;
use anyhow::{Result, bail};
use std::path::PathBuf;
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MentionUri {
File(PathBuf),
Symbol(PathBuf, String),
Thread(acp::SessionId),
Rule(String),
}
impl MentionUri {
pub fn parse(input: &str) -> Result<Self> {
let url = url::Url::parse(input)?;
let path = url.path();
match url.scheme() {
"file" => {
if let Some(fragment) = url.fragment() {
Ok(Self::Symbol(path.into(), fragment.into()))
} else {
Ok(Self::File(path.into()))
}
}
"zed" => {
if let Some(thread) = path.strip_prefix("/agent/thread/") {
Ok(Self::Thread(acp::SessionId(thread.into())))
} else if let Some(rule) = path.strip_prefix("/agent/rule/") {
Ok(Self::Rule(rule.into()))
} else {
bail!("invalid zed url: {:?}", input);
}
}
other => bail!("unrecognized scheme {:?}", other),
}
}
pub fn name(&self) -> String {
match self {
MentionUri::File(path) => path.file_name().unwrap().to_string_lossy().into_owned(),
MentionUri::Symbol(_path, name) => name.clone(),
MentionUri::Thread(thread) => thread.to_string(),
MentionUri::Rule(rule) => rule.clone(),
}
}
pub fn to_link(&self) -> String {
let name = self.name();
let uri = self.to_uri();
format!("[{name}]({uri})")
}
pub fn to_uri(&self) -> String {
match self {
MentionUri::File(path) => {
format!("file://{}", path.display())
}
MentionUri::Symbol(path, name) => {
format!("file://{}#{}", path.display(), name)
}
MentionUri::Thread(thread) => {
format!("zed:///agent/thread/{}", thread.0)
}
MentionUri::Rule(rule) => {
format!("zed:///agent/rule/{}", rule)
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_mention_uri_parse_and_display() {
// Test file URI
let file_uri = "file:///path/to/file.rs";
let parsed = MentionUri::parse(file_uri).unwrap();
match &parsed {
MentionUri::File(path) => assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"),
_ => panic!("Expected File variant"),
}
assert_eq!(parsed.to_uri(), file_uri);
// Test symbol URI
let symbol_uri = "file:///path/to/file.rs#MySymbol";
let parsed = MentionUri::parse(symbol_uri).unwrap();
match &parsed {
MentionUri::Symbol(path, symbol) => {
assert_eq!(path.to_str().unwrap(), "/path/to/file.rs");
assert_eq!(symbol, "MySymbol");
}
_ => panic!("Expected Symbol variant"),
}
assert_eq!(parsed.to_uri(), symbol_uri);
// Test thread URI
let thread_uri = "zed:///agent/thread/session123";
let parsed = MentionUri::parse(thread_uri).unwrap();
match &parsed {
MentionUri::Thread(session_id) => assert_eq!(session_id.0.as_ref(), "session123"),
_ => panic!("Expected Thread variant"),
}
assert_eq!(parsed.to_uri(), thread_uri);
// Test rule URI
let rule_uri = "zed:///agent/rule/my_rule";
let parsed = MentionUri::parse(rule_uri).unwrap();
match &parsed {
MentionUri::Rule(rule) => assert_eq!(rule, "my_rule"),
_ => panic!("Expected Rule variant"),
}
assert_eq!(parsed.to_uri(), rule_uri);
// Test invalid scheme
assert!(MentionUri::parse("http://example.com").is_err());
// Test invalid zed path
assert!(MentionUri::parse("zed:///invalid/path").is_err());
}
}