Include paths when including image mentions

Conrad Irwin created

Fixes #50658

Change summary

crates/agent_ui/src/mention_set.rs    | 67 ++++++++++++++++------------
crates/agent_ui/src/message_editor.rs | 42 +++++++++++++++++
2 files changed, 79 insertions(+), 30 deletions(-)

Detailed changes

crates/agent_ui/src/mention_set.rs 🔗

@@ -654,7 +654,7 @@ mod tests {
 /// Inserts a list of images into the editor as context mentions.
 /// This is the shared implementation used by both paste and file picker operations.
 pub(crate) async fn insert_images_as_context(
-    images: Vec<gpui::Image>,
+    images: Vec<(gpui::Image, Option<PathBuf>)>,
     editor: Entity<Editor>,
     mention_set: Entity<MentionSet>,
     workspace: WeakEntity<Workspace>,
@@ -664,9 +664,15 @@ pub(crate) async fn insert_images_as_context(
         return;
     }
 
-    let replacement_text = MentionUri::PastedImage.as_link().to_string();
+    for (image, path) in images {
+        let mention_uri = match &path {
+            Some(abs_path) => MentionUri::File {
+                abs_path: abs_path.clone(),
+            },
+            None => MentionUri::PastedImage,
+        };
+        let replacement_text = mention_uri.as_link().to_string();
 
-    for image in images {
         let Some((excerpt_id, text_anchor, multibuffer_anchor)) = editor
             .update_in(cx, |editor, window, cx| {
                 let snapshot = editor.snapshot(window, cx);
@@ -700,11 +706,11 @@ pub(crate) async fn insert_images_as_context(
                 excerpt_id,
                 text_anchor,
                 content_len,
-                MentionUri::PastedImage.name().into(),
-                IconName::Image.path().into(),
-                None,
-                None,
-                None,
+                mention_uri.name().into(),
+                mention_uri.icon_path(cx),
+                mention_uri.tooltip_text(),
+                Some(mention_uri.clone()),
+                Some(workspace.clone()),
                 Some(Task::ready(Ok(image.clone())).shared()),
                 editor.clone(),
                 window,
@@ -732,7 +738,7 @@ pub(crate) async fn insert_images_as_context(
             .shared();
 
         mention_set.update(cx, |mention_set, _cx| {
-            mention_set.insert_mention(crease_id, MentionUri::PastedImage, task.clone())
+            mention_set.insert_mention(crease_id, mention_uri, task.clone())
         });
 
         if task
@@ -760,7 +766,7 @@ pub(crate) fn paste_images_as_context(
     let clipboard = cx.read_from_clipboard()?;
     Some(window.spawn(cx, async move |mut cx| {
         use itertools::Itertools;
-        let (mut images, paths) = clipboard
+        let (clipboard_images, paths) = clipboard
             .into_entries()
             .filter_map(|entry| match entry {
                 ClipboardEntry::Image(image) => Some(Either::Left(image)),
@@ -769,35 +775,40 @@ pub(crate) fn paste_images_as_context(
             })
             .partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);
 
+        // Clipboard images don't have a file path
+        let mut images: Vec<(gpui::Image, Option<PathBuf>)> = clipboard_images
+            .into_iter()
+            .map(|img| (img, None))
+            .collect();
+
+        // Images from external paths preserve their file path
         if !paths.is_empty() {
-            images.extend(
-                cx.background_spawn(async move {
+            let path_images = cx
+                .background_spawn(async move {
                     let mut images = vec![];
                     for path in paths.into_iter().flat_map(|paths| paths.paths().to_owned()) {
-                        let Ok(content) = async_fs::read(path).await else {
+                        let Ok(content) = async_fs::read(&path).await else {
                             continue;
                         };
                         let Ok(format) = image::guess_format(&content) else {
                             continue;
                         };
-                        images.push(gpui::Image::from_bytes(
-                            match format {
-                                image::ImageFormat::Png => gpui::ImageFormat::Png,
-                                image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
-                                image::ImageFormat::WebP => gpui::ImageFormat::Webp,
-                                image::ImageFormat::Gif => gpui::ImageFormat::Gif,
-                                image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
-                                image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
-                                image::ImageFormat::Ico => gpui::ImageFormat::Ico,
-                                _ => continue,
-                            },
-                            content,
-                        ));
+                        let gpui_format = match format {
+                            image::ImageFormat::Png => gpui::ImageFormat::Png,
+                            image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
+                            image::ImageFormat::WebP => gpui::ImageFormat::Webp,
+                            image::ImageFormat::Gif => gpui::ImageFormat::Gif,
+                            image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
+                            image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
+                            image::ImageFormat::Ico => gpui::ImageFormat::Ico,
+                            _ => continue,
+                        };
+                        images.push((gpui::Image::from_bytes(gpui_format, content), Some(path)));
                     }
                     images
                 })
-                .await,
-            );
+                .await;
+            images.extend(path_images);
         }
 
         cx.update(|_window, cx| {

crates/agent_ui/src/message_editor.rs 🔗

@@ -829,6 +829,44 @@ impl MessageEditor {
                     let http_client = workspace.read(cx).client().http_client();
 
                     for (anchor, content_len, mention_uri) in all_mentions {
+                        // For image files, load a preview image
+                        let image_preview = if let MentionUri::File { ref abs_path } = mention_uri {
+                            let extension = abs_path
+                                .extension()
+                                .and_then(|e| e.to_str())
+                                .unwrap_or_default();
+                            if gpui::Img::extensions().contains(&extension)
+                                && !extension.contains("svg")
+                            {
+                                let path = abs_path.clone();
+                                Some(
+                                    cx.spawn(async move |_, _| {
+                                        let content = async_fs::read(&path)
+                                            .await
+                                            .map_err(|e| e.to_string())?;
+                                        let format = image::guess_format(&content)
+                                            .map_err(|e| e.to_string())?;
+                                        let gpui_format = match format {
+                                            image::ImageFormat::Png => gpui::ImageFormat::Png,
+                                            image::ImageFormat::Jpeg => gpui::ImageFormat::Jpeg,
+                                            image::ImageFormat::WebP => gpui::ImageFormat::Webp,
+                                            image::ImageFormat::Gif => gpui::ImageFormat::Gif,
+                                            image::ImageFormat::Bmp => gpui::ImageFormat::Bmp,
+                                            image::ImageFormat::Tiff => gpui::ImageFormat::Tiff,
+                                            image::ImageFormat::Ico => gpui::ImageFormat::Ico,
+                                            _ => return Err("Unsupported image format".to_string()),
+                                        };
+                                        Ok(Arc::new(gpui::Image::from_bytes(gpui_format, content)))
+                                    })
+                                    .shared(),
+                                )
+                            } else {
+                                None
+                            }
+                        } else {
+                            None
+                        };
+
                         let Some((crease_id, tx)) = insert_crease_for_mention(
                             anchor.excerpt_id,
                             anchor.text_anchor,
@@ -838,7 +876,7 @@ impl MessageEditor {
                             mention_uri.tooltip_text(),
                             Some(mention_uri.clone()),
                             Some(self.workspace.clone()),
-                            None,
+                            image_preview,
                             self.editor.clone(),
                             window,
                             cx,
@@ -1198,7 +1236,7 @@ impl MessageEditor {
                         continue;
                     };
 
-                    images.push(gpui::Image::from_bytes(format, content));
+                    images.push((gpui::Image::from_bytes(format, content), Some(path)));
                 }
 
                 crate::mention_set::insert_images_as_context(