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:
parent
2444321756
commit
44953375cc
9 changed files with 630 additions and 231 deletions
|
@ -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
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
122
crates/acp_thread/src/mention.rs
Normal file
122
crates/acp_thread/src/mention.rs
Normal 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());
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue