From 7e271711749cad0e1092c125d5f16bd22738bec6 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 6 Apr 2026 12:16:58 -0300 Subject: [PATCH] agent_ui: Fix label for image mentions (#52995) This PR fixes an issue where an image mention would have its label reset to just "Image", instead of persisting the original label, when the prompt got submitted. Closes #48564 Release Notes: - agent: Fixed image mention labels by persisting the file name after submitting the prompt - agent: Fixed directory mentions being incorrectly parsed as files when pasting into prompt editor --------- Co-authored-by: Bennet Bo Fenner --- crates/acp_thread/src/mention.rs | 39 ++++++++++++++++--- crates/agent/src/thread.rs | 2 +- .../src/conversation_view/thread_view.rs | 2 +- crates/agent_ui/src/mention_set.rs | 20 +++++++--- crates/agent_ui/src/message_editor.rs | 25 +++++++++--- crates/agent_ui/src/ui/mention_crease.rs | 2 +- 6 files changed, 69 insertions(+), 21 deletions(-) diff --git a/crates/acp_thread/src/mention.rs b/crates/acp_thread/src/mention.rs index 753838d3b98ed60dc02c3d9383c28fe4f848a29e..28038ecbc04c59d1c5107872210056f11b413141 100644 --- a/crates/acp_thread/src/mention.rs +++ b/crates/acp_thread/src/mention.rs @@ -19,7 +19,9 @@ pub enum MentionUri { File { abs_path: PathBuf, }, - PastedImage, + PastedImage { + name: String, + }, Directory { abs_path: PathBuf, }, @@ -155,7 +157,9 @@ impl MentionUri { include_warnings, }) } else if path.starts_with("/agent/pasted-image") { - Ok(Self::PastedImage) + let name = + single_query_param(&url, "name")?.unwrap_or_else(|| "Image".to_string()); + Ok(Self::PastedImage { name }) } else if path.starts_with("/agent/untitled-buffer") { let fragment = url .fragment() @@ -227,7 +231,7 @@ impl MentionUri { .unwrap_or_default() .to_string_lossy() .into_owned(), - MentionUri::PastedImage => "Image".to_string(), + MentionUri::PastedImage { name } => name.clone(), MentionUri::Symbol { name, .. } => name.clone(), MentionUri::Thread { name, .. } => name.clone(), MentionUri::Rule { name, .. } => name.clone(), @@ -296,7 +300,7 @@ impl MentionUri { MentionUri::File { abs_path } => { FileIcons::get_icon(abs_path, cx).unwrap_or_else(|| IconName::File.path().into()) } - MentionUri::PastedImage => IconName::Image.path().into(), + MentionUri::PastedImage { .. } => IconName::Image.path().into(), MentionUri::Directory { abs_path } => FileIcons::get_folder_icon(false, abs_path, cx) .unwrap_or_else(|| IconName::Folder.path().into()), MentionUri::Symbol { .. } => IconName::Code.path().into(), @@ -322,10 +326,18 @@ impl MentionUri { url.set_path(&abs_path.to_string_lossy()); url } - MentionUri::PastedImage => Url::parse("zed:///agent/pasted-image").unwrap(), + MentionUri::PastedImage { name } => { + let mut url = Url::parse("zed:///agent/pasted-image").unwrap(); + url.query_pairs_mut().append_pair("name", name); + url + } MentionUri::Directory { abs_path } => { let mut url = Url::parse("file:///").unwrap(); - url.set_path(&abs_path.to_string_lossy()); + let mut path = abs_path.to_string_lossy().into_owned(); + if !path.ends_with('/') && !path.ends_with('\\') { + path.push('/'); + } + url.set_path(&path); url } MentionUri::Symbol { @@ -490,6 +502,21 @@ mod tests { assert_eq!(uri.to_uri().to_string(), expected); } + #[test] + fn test_directory_uri_round_trip_without_trailing_slash() { + let uri = MentionUri::Directory { + abs_path: PathBuf::from(path!("/path/to/dir")), + }; + let serialized = uri.to_uri().to_string(); + assert!(serialized.ends_with('/'), "directory URI must end with /"); + let parsed = MentionUri::parse(&serialized, PathStyle::local()).unwrap(); + assert!( + matches!(parsed, MentionUri::Directory { .. }), + "expected Directory variant, got {:?}", + parsed + ); + } + #[test] fn test_parse_symbol_uri() { let symbol_uri = uri!("file:///path/to/file.rs?symbol=MySymbol#L10:20"); diff --git a/crates/agent/src/thread.rs b/crates/agent/src/thread.rs index b61df1b8af84d312d7f186fb85e5a1d04ab59dfd..bcb5b7b2d2f3eb8cffd5be8b70fc08fef8e9fe37 100644 --- a/crates/agent/src/thread.rs +++ b/crates/agent/src/thread.rs @@ -253,7 +253,7 @@ impl UserMessage { ) .ok(); } - MentionUri::PastedImage => { + MentionUri::PastedImage { .. } => { debug_panic!("pasted image URI should not be used in mention content") } MentionUri::Directory { .. } => { diff --git a/crates/agent_ui/src/conversation_view/thread_view.rs b/crates/agent_ui/src/conversation_view/thread_view.rs index 53e63268c51aa1aa5537a87b6055dea62ecd630e..886ac816c925067b6be6b4553361eb2425539ada 100644 --- a/crates/agent_ui/src/conversation_view/thread_view.rs +++ b/crates/agent_ui/src/conversation_view/thread_view.rs @@ -8819,7 +8819,7 @@ pub(crate) fn open_link( .open_path(path, None, true, window, cx) .detach_and_log_err(cx); } - MentionUri::PastedImage => {} + MentionUri::PastedImage { .. } => {} MentionUri::Directory { abs_path } => { let project = workspace.project(); let Some(entry_id) = project.update(cx, |project, cx| { diff --git a/crates/agent_ui/src/mention_set.rs b/crates/agent_ui/src/mention_set.rs index 4db856f9dd1e512a7b8b43eadcefccc22fe50188..1b2ec0ad2fd460b4eec5a8b757bdd3058d4a3704 100644 --- a/crates/agent_ui/src/mention_set.rs +++ b/crates/agent_ui/src/mention_set.rs @@ -154,7 +154,7 @@ impl MentionSet { MentionUri::Selection { abs_path: None, .. } => Task::ready(Err(anyhow!( "Untitled buffer selection mentions are not supported for paste" ))), - MentionUri::PastedImage + MentionUri::PastedImage { .. } | MentionUri::TerminalSelection { .. } | MentionUri::MergeConflict { .. } => { Task::ready(Err(anyhow!("Unsupported mention URI type for paste"))) @@ -283,7 +283,7 @@ impl MentionSet { include_errors, include_warnings, } => self.confirm_mention_for_diagnostics(include_errors, include_warnings, cx), - MentionUri::PastedImage => { + MentionUri::PastedImage { .. } => { debug_panic!("pasted image URI should not be included in completions"); Task::ready(Err(anyhow!( "pasted imaged URI should not be included in completions" @@ -739,9 +739,11 @@ pub(crate) async fn insert_images_as_context( return; } - let replacement_text = MentionUri::PastedImage.as_link().to_string(); - for (image, name) in images { + let mention_uri = MentionUri::PastedImage { + name: name.to_string(), + }; + let replacement_text = mention_uri.as_link().to_string(); let Some((text_anchor, multibuffer_anchor)) = editor .update_in(cx, |editor, window, cx| { let snapshot = editor.snapshot(window, cx); @@ -804,7 +806,13 @@ 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, + MentionUri::PastedImage { + name: name.to_string(), + }, + task.clone(), + ) }); if task @@ -873,7 +881,7 @@ pub(crate) fn paste_images_as_context( Some(window.spawn(cx, async move |mut cx| { use itertools::Itertools; - let default_name: SharedString = MentionUri::PastedImage.name().into(); + let default_name: SharedString = "Image".into(); let (mut images, paths): (Vec<(gpui::Image, SharedString)>, Vec<_>) = clipboard .into_entries() .filter_map(|entry| match entry { diff --git a/crates/agent_ui/src/message_editor.rs b/crates/agent_ui/src/message_editor.rs index 8660e792cd23bc418b1d2c204bfafb2a81ba48df..0f59441ab27b5074a710c46a683e72d003a8d5d7 100644 --- a/crates/agent_ui/src/message_editor.rs +++ b/crates/agent_ui/src/message_editor.rs @@ -261,7 +261,7 @@ async fn resolve_pasted_context_items( ) -> (Vec, Vec>) { let mut items = Vec::new(); let mut added_worktrees = Vec::new(); - let default_image_name: SharedString = MentionUri::PastedImage.name().into(); + let default_image_name: SharedString = "Image".into(); for entry in entries { match entry { @@ -812,7 +812,9 @@ impl MessageEditor { ) .uri(match uri { MentionUri::File { .. } => Some(uri.to_uri().to_string()), - MentionUri::PastedImage => None, + MentionUri::PastedImage { .. } => { + Some(uri.to_uri().to_string()) + } other => { debug_panic!( "unexpected mention uri for image: {:?}", @@ -1638,7 +1640,9 @@ impl MessageEditor { let mention_uri = if let Some(uri) = uri { MentionUri::parse(&uri, path_style) } else { - Ok(MentionUri::PastedImage) + Ok(MentionUri::PastedImage { + name: "Image".to_string(), + }) }; let Some(mention_uri) = mention_uri.log_err() else { continue; @@ -4074,6 +4078,11 @@ mod tests { &mut cx, ); + let image_name = temporary_image_path + .file_name() + .and_then(|n| n.to_str()) + .unwrap_or("Image") + .to_string(); std::fs::remove_file(&temporary_image_path).expect("remove temp png"); let expected_file_uri = MentionUri::File { @@ -4081,12 +4090,16 @@ mod tests { } .to_uri() .to_string(); - let expected_image_uri = MentionUri::PastedImage.to_uri().to_string(); + let expected_image_uri = MentionUri::PastedImage { + name: image_name.clone(), + } + .to_uri() + .to_string(); editor.update(&mut cx, |editor, cx| { assert_eq!( editor.text(cx), - format!("[@Image]({expected_image_uri}) [@file.txt]({expected_file_uri}) ") + format!("[@{image_name}]({expected_image_uri}) [@file.txt]({expected_file_uri}) ") ); }); @@ -4094,7 +4107,7 @@ mod tests { assert_eq!(contents.len(), 2); assert!(contents.iter().any(|(uri, mention)| { - *uri == MentionUri::PastedImage && matches!(mention, Mention::Image(_)) + matches!(uri, MentionUri::PastedImage { .. }) && matches!(mention, Mention::Image(_)) })); assert!(contents.iter().any(|(uri, mention)| { *uri == MentionUri::File { diff --git a/crates/agent_ui/src/ui/mention_crease.rs b/crates/agent_ui/src/ui/mention_crease.rs index 6e99647304d93fe91cd6b91dbd2bf3bfd82c7ab0..bd48a558f5d9b1f042f974dc6e174f8ba8078adf 100644 --- a/crates/agent_ui/src/ui/mention_crease.rs +++ b/crates/agent_ui/src/ui/mention_crease.rs @@ -184,7 +184,7 @@ fn open_mention_uri( MentionUri::Fetch { url } => { cx.open_url(url.as_str()); } - MentionUri::PastedImage + MentionUri::PastedImage { .. } | MentionUri::Selection { abs_path: None, .. } | MentionUri::Diagnostics { .. } | MentionUri::TerminalSelection { .. }