acp_thread: Fix @mention file path format (#41310)

Bennet Fenner and Cole Miller created

After #38882 we were always including file/directory mentions as
`zed:///agent/file?path=a/b/c.rs`.
However, for most resource links (files/directories/symbols/selections)
we want to use a common format, so that ACP servers don't have to
implement custom handling for parsing `ResourceLink`s coming from Zed.

This is what it looks like now:
```
[@index.js](file:///Users/.../projects/reqwest/examples/wasm_github_fetch/index.js) 
[@wasm](file:///Users/.../projects/reqwest/src/wasm) 
[@Error](file:///Users/.../projects/reqwest/src/async_impl/client.rs?symbol=Error#L2661:2661) 
[@error.rs (23:27)](file:///Users/.../projects/reqwest/src/error.rs#L23:27) 
```

Release Notes:

- N/A

---------

Co-authored-by: Cole Miller <cole@zed.dev>

Change summary

crates/acp_thread/src/acp_thread.rs       |  70 +++++++---
crates/acp_thread/src/mention.rs          | 174 ++++++++++++------------
crates/agent/src/agent.rs                 |   5 
crates/agent/src/thread.rs                |  10 
crates/agent_ui/src/acp/message_editor.rs |  25 ++
crates/agent_ui/src/acp/thread_view.rs    |   3 
6 files changed, 168 insertions(+), 119 deletions(-)

Detailed changes

crates/acp_thread/src/acp_thread.rs 🔗

@@ -35,7 +35,7 @@ use std::rc::Rc;
 use std::time::{Duration, Instant};
 use std::{fmt::Display, mem, path::PathBuf, sync::Arc};
 use ui::App;
-use util::{ResultExt, get_default_system_shell_preferring_bash};
+use util::{ResultExt, get_default_system_shell_preferring_bash, paths::PathStyle};
 use uuid::Uuid;
 
 #[derive(Debug)]
@@ -95,9 +95,14 @@ pub enum AssistantMessageChunk {
 }
 
 impl AssistantMessageChunk {
-    pub fn from_str(chunk: &str, language_registry: &Arc<LanguageRegistry>, cx: &mut App) -> Self {
+    pub fn from_str(
+        chunk: &str,
+        language_registry: &Arc<LanguageRegistry>,
+        path_style: PathStyle,
+        cx: &mut App,
+    ) -> Self {
         Self::Message {
-            block: ContentBlock::new(chunk.into(), language_registry, cx),
+            block: ContentBlock::new(chunk.into(), language_registry, path_style, cx),
         }
     }
 
@@ -186,6 +191,7 @@ impl ToolCall {
         tool_call: acp::ToolCall,
         status: ToolCallStatus,
         language_registry: Arc<LanguageRegistry>,
+        path_style: PathStyle,
         terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
         cx: &mut App,
     ) -> Result<Self> {
@@ -199,6 +205,7 @@ impl ToolCall {
             content.push(ToolCallContent::from_acp(
                 item,
                 language_registry.clone(),
+                path_style,
                 terminals,
                 cx,
             )?);
@@ -223,6 +230,7 @@ impl ToolCall {
         &mut self,
         fields: acp::ToolCallUpdateFields,
         language_registry: Arc<LanguageRegistry>,
+        path_style: PathStyle,
         terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
         cx: &mut App,
     ) -> Result<()> {
@@ -260,12 +268,13 @@ impl ToolCall {
 
             // Reuse existing content if we can
             for (old, new) in self.content.iter_mut().zip(content.by_ref()) {
-                old.update_from_acp(new, language_registry.clone(), terminals, cx)?;
+                old.update_from_acp(new, language_registry.clone(), path_style, terminals, cx)?;
             }
             for new in content {
                 self.content.push(ToolCallContent::from_acp(
                     new,
                     language_registry.clone(),
+                    path_style,
                     terminals,
                     cx,
                 )?)
@@ -450,21 +459,23 @@ impl ContentBlock {
     pub fn new(
         block: acp::ContentBlock,
         language_registry: &Arc<LanguageRegistry>,
+        path_style: PathStyle,
         cx: &mut App,
     ) -> Self {
         let mut this = Self::Empty;
-        this.append(block, language_registry, cx);
+        this.append(block, language_registry, path_style, cx);
         this
     }
 
     pub fn new_combined(
         blocks: impl IntoIterator<Item = acp::ContentBlock>,
         language_registry: Arc<LanguageRegistry>,
+        path_style: PathStyle,
         cx: &mut App,
     ) -> Self {
         let mut this = Self::Empty;
         for block in blocks {
-            this.append(block, &language_registry, cx);
+            this.append(block, &language_registry, path_style, cx);
         }
         this
     }
@@ -473,6 +484,7 @@ impl ContentBlock {
         &mut self,
         block: acp::ContentBlock,
         language_registry: &Arc<LanguageRegistry>,
+        path_style: PathStyle,
         cx: &mut App,
     ) {
         if matches!(self, ContentBlock::Empty)
@@ -482,7 +494,7 @@ impl ContentBlock {
             return;
         }
 
-        let new_content = self.block_string_contents(block);
+        let new_content = self.block_string_contents(block, path_style);
 
         match self {
             ContentBlock::Empty => {
@@ -492,7 +504,7 @@ impl ContentBlock {
                 markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx));
             }
             ContentBlock::ResourceLink { resource_link } => {
-                let existing_content = Self::resource_link_md(&resource_link.uri);
+                let existing_content = Self::resource_link_md(&resource_link.uri, path_style);
                 let combined = format!("{}\n{}", existing_content, new_content);
 
                 *self = Self::create_markdown_block(combined, language_registry, cx);
@@ -511,11 +523,11 @@ impl ContentBlock {
         }
     }
 
-    fn block_string_contents(&self, block: acp::ContentBlock) -> String {
+    fn block_string_contents(&self, block: acp::ContentBlock, path_style: PathStyle) -> String {
         match block {
             acp::ContentBlock::Text(text_content) => text_content.text,
             acp::ContentBlock::ResourceLink(resource_link) => {
-                Self::resource_link_md(&resource_link.uri)
+                Self::resource_link_md(&resource_link.uri, path_style)
             }
             acp::ContentBlock::Resource(acp::EmbeddedResource {
                 resource:
@@ -524,14 +536,14 @@ impl ContentBlock {
                         ..
                     }),
                 ..
-            }) => Self::resource_link_md(&uri),
+            }) => Self::resource_link_md(&uri, path_style),
             acp::ContentBlock::Image(image) => Self::image_md(&image),
             acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => String::new(),
         }
     }
 
-    fn resource_link_md(uri: &str) -> String {
-        if let Some(uri) = MentionUri::parse(uri).log_err() {
+    fn resource_link_md(uri: &str, path_style: PathStyle) -> String {
+        if let Some(uri) = MentionUri::parse(uri, path_style).log_err() {
             uri.as_link().to_string()
         } else {
             uri.to_string()
@@ -577,6 +589,7 @@ impl ToolCallContent {
     pub fn from_acp(
         content: acp::ToolCallContent,
         language_registry: Arc<LanguageRegistry>,
+        path_style: PathStyle,
         terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
         cx: &mut App,
     ) -> Result<Self> {
@@ -584,6 +597,7 @@ impl ToolCallContent {
             acp::ToolCallContent::Content { content } => Ok(Self::ContentBlock(ContentBlock::new(
                 content,
                 &language_registry,
+                path_style,
                 cx,
             ))),
             acp::ToolCallContent::Diff { diff } => Ok(Self::Diff(cx.new(|cx| {
@@ -607,6 +621,7 @@ impl ToolCallContent {
         &mut self,
         new: acp::ToolCallContent,
         language_registry: Arc<LanguageRegistry>,
+        path_style: PathStyle,
         terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
         cx: &mut App,
     ) -> Result<()> {
@@ -622,7 +637,7 @@ impl ToolCallContent {
         };
 
         if needs_update {
-            *self = Self::from_acp(new, language_registry, terminals, cx)?;
+            *self = Self::from_acp(new, language_registry, path_style, terminals, cx)?;
         }
         Ok(())
     }
@@ -1142,6 +1157,7 @@ impl AcpThread {
         cx: &mut Context<Self>,
     ) {
         let language_registry = self.project.read(cx).languages().clone();
+        let path_style = self.project.read(cx).path_style(cx);
         let entries_len = self.entries.len();
 
         if let Some(last_entry) = self.entries.last_mut()
@@ -1153,12 +1169,12 @@ impl AcpThread {
             }) = last_entry
         {
             *id = message_id.or(id.take());
-            content.append(chunk.clone(), &language_registry, cx);
+            content.append(chunk.clone(), &language_registry, path_style, cx);
             chunks.push(chunk);
             let idx = entries_len - 1;
             cx.emit(AcpThreadEvent::EntryUpdated(idx));
         } else {
-            let content = ContentBlock::new(chunk.clone(), &language_registry, cx);
+            let content = ContentBlock::new(chunk.clone(), &language_registry, path_style, cx);
             self.push_entry(
                 AgentThreadEntry::UserMessage(UserMessage {
                     id: message_id,
@@ -1178,6 +1194,7 @@ impl AcpThread {
         cx: &mut Context<Self>,
     ) {
         let language_registry = self.project.read(cx).languages().clone();
+        let path_style = self.project.read(cx).path_style(cx);
         let entries_len = self.entries.len();
         if let Some(last_entry) = self.entries.last_mut()
             && let AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) = last_entry
@@ -1187,10 +1204,10 @@ impl AcpThread {
             match (chunks.last_mut(), is_thought) {
                 (Some(AssistantMessageChunk::Message { block }), false)
                 | (Some(AssistantMessageChunk::Thought { block }), true) => {
-                    block.append(chunk, &language_registry, cx)
+                    block.append(chunk, &language_registry, path_style, cx)
                 }
                 _ => {
-                    let block = ContentBlock::new(chunk, &language_registry, cx);
+                    let block = ContentBlock::new(chunk, &language_registry, path_style, cx);
                     if is_thought {
                         chunks.push(AssistantMessageChunk::Thought { block })
                     } else {
@@ -1199,7 +1216,7 @@ impl AcpThread {
                 }
             }
         } else {
-            let block = ContentBlock::new(chunk, &language_registry, cx);
+            let block = ContentBlock::new(chunk, &language_registry, path_style, cx);
             let chunk = if is_thought {
                 AssistantMessageChunk::Thought { block }
             } else {
@@ -1251,6 +1268,7 @@ impl AcpThread {
     ) -> Result<()> {
         let update = update.into();
         let languages = self.project.read(cx).languages().clone();
+        let path_style = self.project.read(cx).path_style(cx);
 
         let ix = match self.index_for_tool_call(update.id()) {
             Some(ix) => ix,
@@ -1267,6 +1285,7 @@ impl AcpThread {
                             meta: None,
                         }),
                         &languages,
+                        path_style,
                         cx,
                     ))],
                     status: ToolCallStatus::Failed,
@@ -1286,7 +1305,7 @@ impl AcpThread {
         match update {
             ToolCallUpdate::UpdateFields(update) => {
                 let location_updated = update.fields.locations.is_some();
-                call.update_fields(update.fields, languages, &self.terminals, cx)?;
+                call.update_fields(update.fields, languages, path_style, &self.terminals, cx)?;
                 if location_updated {
                     self.resolve_locations(update.id, cx);
                 }
@@ -1325,6 +1344,7 @@ impl AcpThread {
         cx: &mut Context<Self>,
     ) -> Result<(), acp::Error> {
         let language_registry = self.project.read(cx).languages().clone();
+        let path_style = self.project.read(cx).path_style(cx);
         let id = update.id.clone();
 
         if let Some(ix) = self.index_for_tool_call(&id) {
@@ -1332,7 +1352,13 @@ impl AcpThread {
                 unreachable!()
             };
 
-            call.update_fields(update.fields, language_registry, &self.terminals, cx)?;
+            call.update_fields(
+                update.fields,
+                language_registry,
+                path_style,
+                &self.terminals,
+                cx,
+            )?;
             call.status = status;
 
             cx.emit(AcpThreadEvent::EntryUpdated(ix));
@@ -1341,6 +1367,7 @@ impl AcpThread {
                 update.try_into()?,
                 status,
                 language_registry,
+                self.project.read(cx).path_style(cx),
                 &self.terminals,
                 cx,
             )?;
@@ -1620,6 +1647,7 @@ impl AcpThread {
         let block = ContentBlock::new_combined(
             message.clone(),
             self.project.read(cx).languages().clone(),
+            self.project.read(cx).path_style(cx),
             cx,
         );
         let request = acp::PromptRequest {

crates/acp_thread/src/mention.rs 🔗

@@ -7,10 +7,10 @@ use std::{
     fmt,
     ops::RangeInclusive,
     path::{Path, PathBuf},
-    str::FromStr,
 };
 use ui::{App, IconName, SharedString};
 use url::Url;
+use util::paths::PathStyle;
 
 #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Hash)]
 pub enum MentionUri {
@@ -49,7 +49,7 @@ pub enum MentionUri {
 }
 
 impl MentionUri {
-    pub fn parse(input: &str) -> Result<Self> {
+    pub fn parse(input: &str, path_style: PathStyle) -> Result<Self> {
         fn parse_line_range(fragment: &str) -> Result<RangeInclusive<u32>> {
             let range = fragment
                 .strip_prefix("L")
@@ -74,25 +74,34 @@ impl MentionUri {
         let path = url.path();
         match url.scheme() {
             "file" => {
-                let path = url.to_file_path().ok().context("Extracting file path")?;
+                let path = if path_style.is_windows() {
+                    path.trim_start_matches("/")
+                } else {
+                    path
+                };
+
                 if let Some(fragment) = url.fragment() {
                     let line_range = parse_line_range(fragment)?;
                     if let Some(name) = single_query_param(&url, "symbol")? {
                         Ok(Self::Symbol {
                             name,
-                            abs_path: path,
+                            abs_path: path.into(),
                             line_range,
                         })
                     } else {
                         Ok(Self::Selection {
-                            abs_path: Some(path),
+                            abs_path: Some(path.into()),
                             line_range,
                         })
                     }
                 } else if input.ends_with("/") {
-                    Ok(Self::Directory { abs_path: path })
+                    Ok(Self::Directory {
+                        abs_path: path.into(),
+                    })
                 } else {
-                    Ok(Self::File { abs_path: path })
+                    Ok(Self::File {
+                        abs_path: path.into(),
+                    })
                 }
             }
             "zed" => {
@@ -213,18 +222,14 @@ impl MentionUri {
     pub fn to_uri(&self) -> Url {
         match self {
             MentionUri::File { abs_path } => {
-                let mut url = Url::parse("zed:///").unwrap();
-                url.set_path("/agent/file");
-                url.query_pairs_mut()
-                    .append_pair("path", &abs_path.to_string_lossy());
+                let mut url = Url::parse("file:///").unwrap();
+                url.set_path(&abs_path.to_string_lossy());
                 url
             }
             MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(),
             MentionUri::Directory { abs_path } => {
-                let mut url = Url::parse("zed:///").unwrap();
-                url.set_path("/agent/directory");
-                url.query_pairs_mut()
-                    .append_pair("path", &abs_path.to_string_lossy());
+                let mut url = Url::parse("file:///").unwrap();
+                url.set_path(&abs_path.to_string_lossy());
                 url
             }
             MentionUri::Symbol {
@@ -232,10 +237,9 @@ impl MentionUri {
                 name,
                 line_range,
             } => {
-                let mut url = Url::parse("zed:///").unwrap();
-                url.set_path(&format!("/agent/symbol/{name}"));
-                url.query_pairs_mut()
-                    .append_pair("path", &abs_path.to_string_lossy());
+                let mut url = Url::parse("file:///").unwrap();
+                url.set_path(&abs_path.to_string_lossy());
+                url.query_pairs_mut().append_pair("symbol", name);
                 url.set_fragment(Some(&format!(
                     "L{}:{}",
                     line_range.start() + 1,
@@ -247,13 +251,14 @@ impl MentionUri {
                 abs_path,
                 line_range,
             } => {
-                let mut url = Url::parse("zed:///").unwrap();
-                if let Some(abs_path) = abs_path {
-                    url.set_path("/agent/selection");
-                    url.query_pairs_mut()
-                        .append_pair("path", &abs_path.to_string_lossy());
+                let mut url = if let Some(path) = abs_path {
+                    let mut url = Url::parse("file:///").unwrap();
+                    url.set_path(&path.to_string_lossy());
+                    url
                 } else {
+                    let mut url = Url::parse("zed:///").unwrap();
                     url.set_path("/agent/untitled-buffer");
+                    url
                 };
                 url.set_fragment(Some(&format!(
                     "L{}:{}",
@@ -288,14 +293,6 @@ impl MentionUri {
     }
 }
 
-impl FromStr for MentionUri {
-    type Err = anyhow::Error;
-
-    fn from_str(s: &str) -> anyhow::Result<Self> {
-        Self::parse(s)
-    }
-}
-
 pub struct MentionLink<'a>(&'a MentionUri);
 
 impl fmt::Display for MentionLink<'_> {
@@ -338,93 +335,81 @@ mod tests {
 
     #[test]
     fn test_parse_file_uri() {
-        let old_uri = uri!("file:///path/to/file.rs");
-        let parsed = MentionUri::parse(old_uri).unwrap();
+        let file_uri = uri!("file:///path/to/file.rs");
+        let parsed = MentionUri::parse(file_uri, PathStyle::local()).unwrap();
         match &parsed {
             MentionUri::File { abs_path } => {
-                assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/file.rs"));
+                assert_eq!(abs_path, Path::new(path!("/path/to/file.rs")));
             }
             _ => panic!("Expected File variant"),
         }
-        let new_uri = parsed.to_uri().to_string();
-        assert!(new_uri.starts_with("zed:///agent/file"));
-        assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
+        assert_eq!(parsed.to_uri().to_string(), file_uri);
     }
 
     #[test]
     fn test_parse_directory_uri() {
-        let old_uri = uri!("file:///path/to/dir/");
-        let parsed = MentionUri::parse(old_uri).unwrap();
+        let file_uri = uri!("file:///path/to/dir/");
+        let parsed = MentionUri::parse(file_uri, PathStyle::local()).unwrap();
         match &parsed {
             MentionUri::Directory { abs_path } => {
-                assert_eq!(abs_path.to_str().unwrap(), path!("/path/to/dir/"));
+                assert_eq!(abs_path, Path::new(path!("/path/to/dir/")));
             }
             _ => panic!("Expected Directory variant"),
         }
-        let new_uri = parsed.to_uri().to_string();
-        assert!(new_uri.starts_with("zed:///agent/directory"));
-        assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
+        assert_eq!(parsed.to_uri().to_string(), file_uri);
     }
 
     #[test]
     fn test_to_directory_uri_without_slash() {
         let uri = MentionUri::Directory {
-            abs_path: PathBuf::from(path!("/path/to/dir")),
+            abs_path: PathBuf::from(path!("/path/to/dir/")),
         };
-        let uri_string = uri.to_uri().to_string();
-        assert!(uri_string.starts_with("zed:///agent/directory"));
-        assert_eq!(MentionUri::parse(&uri_string).unwrap(), uri);
+        let expected = uri!("file:///path/to/dir/");
+        assert_eq!(uri.to_uri().to_string(), expected);
     }
 
     #[test]
     fn test_parse_symbol_uri() {
-        let old_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20");
-        let parsed = MentionUri::parse(old_uri).unwrap();
+        let symbol_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20");
+        let parsed = MentionUri::parse(symbol_uri, PathStyle::local()).unwrap();
         match &parsed {
             MentionUri::Symbol {
                 abs_path: path,
                 name,
                 line_range,
             } => {
-                assert_eq!(path.to_str().unwrap(), path!("/path/to/file.rs"));
+                assert_eq!(path, Path::new(path!("/path/to/file.rs")));
                 assert_eq!(name, "MySymbol");
                 assert_eq!(line_range.start(), &9);
                 assert_eq!(line_range.end(), &19);
             }
             _ => panic!("Expected Symbol variant"),
         }
-        let new_uri = parsed.to_uri().to_string();
-        assert!(new_uri.starts_with("zed:///agent/symbol/MySymbol"));
-        assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
+        assert_eq!(parsed.to_uri().to_string(), symbol_uri);
     }
 
     #[test]
     fn test_parse_selection_uri() {
-        let old_uri = uri!("file:///path/to/file.rs#L5:15");
-        let parsed = MentionUri::parse(old_uri).unwrap();
+        let selection_uri = uri!("file:///path/to/file.rs#L5:15");
+        let parsed = MentionUri::parse(selection_uri, PathStyle::local()).unwrap();
         match &parsed {
             MentionUri::Selection {
                 abs_path: path,
                 line_range,
             } => {
-                assert_eq!(
-                    path.as_ref().unwrap().to_str().unwrap(),
-                    path!("/path/to/file.rs")
-                );
+                assert_eq!(path.as_ref().unwrap(), Path::new(path!("/path/to/file.rs")));
                 assert_eq!(line_range.start(), &4);
                 assert_eq!(line_range.end(), &14);
             }
             _ => panic!("Expected Selection variant"),
         }
-        let new_uri = parsed.to_uri().to_string();
-        assert!(new_uri.starts_with("zed:///agent/selection"));
-        assert_eq!(MentionUri::parse(&new_uri).unwrap(), parsed);
+        assert_eq!(parsed.to_uri().to_string(), selection_uri);
     }
 
     #[test]
     fn test_parse_untitled_selection_uri() {
         let selection_uri = uri!("zed:///agent/untitled-buffer#L1:10");
-        let parsed = MentionUri::parse(selection_uri).unwrap();
+        let parsed = MentionUri::parse(selection_uri, PathStyle::local()).unwrap();
         match &parsed {
             MentionUri::Selection {
                 abs_path: None,
@@ -441,7 +426,7 @@ mod tests {
     #[test]
     fn test_parse_thread_uri() {
         let thread_uri = "zed:///agent/thread/session123?name=Thread+name";
-        let parsed = MentionUri::parse(thread_uri).unwrap();
+        let parsed = MentionUri::parse(thread_uri, PathStyle::local()).unwrap();
         match &parsed {
             MentionUri::Thread {
                 id: thread_id,
@@ -458,7 +443,7 @@ mod tests {
     #[test]
     fn test_parse_rule_uri() {
         let rule_uri = "zed:///agent/rule/d8694ff2-90d5-4b6f-be33-33c1763acd52?name=Some+rule";
-        let parsed = MentionUri::parse(rule_uri).unwrap();
+        let parsed = MentionUri::parse(rule_uri, PathStyle::local()).unwrap();
         match &parsed {
             MentionUri::Rule { id, name } => {
                 assert_eq!(id.to_string(), "d8694ff2-90d5-4b6f-be33-33c1763acd52");
@@ -472,7 +457,7 @@ mod tests {
     #[test]
     fn test_parse_fetch_http_uri() {
         let http_uri = "http://example.com/path?query=value#fragment";
-        let parsed = MentionUri::parse(http_uri).unwrap();
+        let parsed = MentionUri::parse(http_uri, PathStyle::local()).unwrap();
         match &parsed {
             MentionUri::Fetch { url } => {
                 assert_eq!(url.to_string(), http_uri);
@@ -485,7 +470,7 @@ mod tests {
     #[test]
     fn test_parse_fetch_https_uri() {
         let https_uri = "https://example.com/api/endpoint";
-        let parsed = MentionUri::parse(https_uri).unwrap();
+        let parsed = MentionUri::parse(https_uri, PathStyle::local()).unwrap();
         match &parsed {
             MentionUri::Fetch { url } => {
                 assert_eq!(url.to_string(), https_uri);
@@ -497,40 +482,55 @@ mod tests {
 
     #[test]
     fn test_invalid_scheme() {
-        assert!(MentionUri::parse("ftp://example.com").is_err());
-        assert!(MentionUri::parse("ssh://example.com").is_err());
-        assert!(MentionUri::parse("unknown://example.com").is_err());
+        assert!(MentionUri::parse("ftp://example.com", PathStyle::local()).is_err());
+        assert!(MentionUri::parse("ssh://example.com", PathStyle::local()).is_err());
+        assert!(MentionUri::parse("unknown://example.com", PathStyle::local()).is_err());
     }
 
     #[test]
     fn test_invalid_zed_path() {
-        assert!(MentionUri::parse("zed:///invalid/path").is_err());
-        assert!(MentionUri::parse("zed:///agent/unknown/test").is_err());
+        assert!(MentionUri::parse("zed:///invalid/path", PathStyle::local()).is_err());
+        assert!(MentionUri::parse("zed:///agent/unknown/test", PathStyle::local()).is_err());
     }
 
     #[test]
     fn test_invalid_line_range_format() {
         // Missing L prefix
-        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#10:20")).is_err());
+        assert!(
+            MentionUri::parse(uri!("file:///path/to/file.rs#10:20"), PathStyle::local()).is_err()
+        );
 
         // Missing colon separator
-        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L1020")).is_err());
+        assert!(
+            MentionUri::parse(uri!("file:///path/to/file.rs#L1020"), PathStyle::local()).is_err()
+        );
 
         // Invalid numbers
-        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L10:abc")).is_err());
-        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#Labc:20")).is_err());
+        assert!(
+            MentionUri::parse(uri!("file:///path/to/file.rs#L10:abc"), PathStyle::local()).is_err()
+        );
+        assert!(
+            MentionUri::parse(uri!("file:///path/to/file.rs#Labc:20"), PathStyle::local()).is_err()
+        );
     }
 
     #[test]
     fn test_invalid_query_parameters() {
         // Invalid query parameter name
-        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L10:20?invalid=test")).is_err());
+        assert!(
+            MentionUri::parse(
+                uri!("file:///path/to/file.rs#L10:20?invalid=test"),
+                PathStyle::local()
+            )
+            .is_err()
+        );
 
         // Too many query parameters
         assert!(
-            MentionUri::parse(uri!(
-                "file:///path/to/file.rs#L10:20?symbol=test&another=param"
-            ))
+            MentionUri::parse(
+                uri!("file:///path/to/file.rs#L10:20?symbol=test&another=param"),
+                PathStyle::local()
+            )
             .is_err()
         );
     }
@@ -538,8 +538,14 @@ mod tests {
     #[test]
     fn test_zero_based_line_numbers() {
         // Test that 0-based line numbers are rejected (should be 1-based)
-        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L0:10")).is_err());
-        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L1:0")).is_err());
-        assert!(MentionUri::parse(uri!("file:///path/to/file.rs#L0:0")).is_err());
+        assert!(
+            MentionUri::parse(uri!("file:///path/to/file.rs#L0:10"), PathStyle::local()).is_err()
+        );
+        assert!(
+            MentionUri::parse(uri!("file:///path/to/file.rs#L1:0"), PathStyle::local()).is_err()
+        );
+        assert!(
+            MentionUri::parse(uri!("file:///path/to/file.rs#L0:0"), PathStyle::local()).is_err()
+        );
     }
 }

crates/agent/src/agent.rs 🔗

@@ -1035,12 +1035,13 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
         let session_id = params.session_id.clone();
         log::info!("Received prompt request for session: {}", session_id);
         log::debug!("Prompt blocks count: {}", params.prompt.len());
+        let path_style = self.0.read(cx).project.read(cx).path_style(cx);
 
-        self.run_turn(session_id, cx, |thread, cx| {
+        self.run_turn(session_id, cx, move |thread, cx| {
             let content: Vec<UserMessageContent> = params
                 .prompt
                 .into_iter()
-                .map(Into::into)
+                .map(|block| UserMessageContent::from_content_block(block, path_style))
                 .collect::<Vec<_>>();
             log::debug!("Converted prompt to message: {} chars", content.len());
             log::debug!("Message id: {:?}", id);

crates/agent/src/thread.rs 🔗

@@ -50,7 +50,7 @@ use std::{
     time::{Duration, Instant},
 };
 use std::{fmt::Write, path::PathBuf};
-use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock};
+use util::{ResultExt, debug_panic, markdown::MarkdownCodeBlock, paths::PathStyle};
 use uuid::Uuid;
 
 const TOOL_CANCELED_MESSAGE: &str = "Tool canceled by user";
@@ -2538,8 +2538,8 @@ impl From<&str> for UserMessageContent {
     }
 }
 
-impl From<acp::ContentBlock> for UserMessageContent {
-    fn from(value: acp::ContentBlock) -> Self {
+impl UserMessageContent {
+    pub fn from_content_block(value: acp::ContentBlock, path_style: PathStyle) -> Self {
         match value {
             acp::ContentBlock::Text(text_content) => Self::Text(text_content.text),
             acp::ContentBlock::Image(image_content) => Self::Image(convert_image(image_content)),
@@ -2548,7 +2548,7 @@ impl From<acp::ContentBlock> for UserMessageContent {
                 Self::Text("[audio]".to_string())
             }
             acp::ContentBlock::ResourceLink(resource_link) => {
-                match MentionUri::parse(&resource_link.uri) {
+                match MentionUri::parse(&resource_link.uri, path_style) {
                     Ok(uri) => Self::Mention {
                         uri,
                         content: String::new(),
@@ -2561,7 +2561,7 @@ impl From<acp::ContentBlock> for UserMessageContent {
             }
             acp::ContentBlock::Resource(resource) => match resource.resource {
                 acp::EmbeddedResourceResource::TextResourceContents(resource) => {
-                    match MentionUri::parse(&resource.uri) {
+                    match MentionUri::parse(&resource.uri, path_style) {
                         Ok(uri) => Self::Mention {
                             uri,
                             content: resource.text,

crates/agent_ui/src/acp/message_editor.rs 🔗

@@ -1062,6 +1062,7 @@ impl MessageEditor {
     ) {
         self.clear(window, cx);
 
+        let path_style = self.project.read(cx).path_style(cx);
         let mut text = String::new();
         let mut mentions = Vec::new();
 
@@ -1074,7 +1075,8 @@ impl MessageEditor {
                     resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
                     ..
                 }) => {
-                    let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() else {
+                    let Some(mention_uri) = MentionUri::parse(&resource.uri, path_style).log_err()
+                    else {
                         continue;
                     };
                     let start = text.len();
@@ -1090,7 +1092,9 @@ impl MessageEditor {
                     ));
                 }
                 acp::ContentBlock::ResourceLink(resource) => {
-                    if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() {
+                    if let Some(mention_uri) =
+                        MentionUri::parse(&resource.uri, path_style).log_err()
+                    {
                         let start = text.len();
                         write!(&mut text, "{}", mention_uri.as_link()).ok();
                         let end = text.len();
@@ -1105,7 +1109,7 @@ impl MessageEditor {
                     meta: _,
                 }) => {
                     let mention_uri = if let Some(uri) = uri {
-                        MentionUri::parse(&uri)
+                        MentionUri::parse(&uri, path_style)
                     } else {
                         Ok(MentionUri::PastedImage)
                     };
@@ -2293,7 +2297,10 @@ mod tests {
                 panic!("Unexpected mentions");
             };
             pretty_assertions::assert_eq!(content, "1");
-            pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap());
+            pretty_assertions::assert_eq!(
+                uri,
+                &MentionUri::parse(&url_one, PathStyle::local()).unwrap()
+            );
         }
 
         let contents = message_editor
@@ -2314,7 +2321,10 @@ mod tests {
             let [(uri, Mention::UriOnly)] = contents.as_slice() else {
                 panic!("Unexpected mentions");
             };
-            pretty_assertions::assert_eq!(uri, &url_one.parse::<MentionUri>().unwrap());
+            pretty_assertions::assert_eq!(
+                uri,
+                &MentionUri::parse(&url_one, PathStyle::local()).unwrap()
+            );
         }
 
         cx.simulate_input(" ");
@@ -2375,7 +2385,10 @@ mod tests {
                 panic!("Unexpected mentions");
             };
             pretty_assertions::assert_eq!(content, "8");
-            pretty_assertions::assert_eq!(uri, &url_eight.parse::<MentionUri>().unwrap());
+            pretty_assertions::assert_eq!(
+                uri,
+                &MentionUri::parse(&url_eight, PathStyle::local()).unwrap()
+            );
         }
 
         editor.update(&mut cx, |editor, cx| {

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -4305,7 +4305,8 @@ impl AcpThreadView {
             return;
         };
 
-        if let Some(mention) = MentionUri::parse(&url).log_err() {
+        if let Some(mention) = MentionUri::parse(&url, workspace.read(cx).path_style(cx)).log_err()
+        {
             workspace.update(cx, |workspace, cx| match mention {
                 MentionUri::File { abs_path } => {
                     let project = workspace.project();