agent_ui: Fix `@fetch` mention if URL contains `@` character (#51953)

Bennet Bo Fenner and Marshall Bowers created

## Context

Closes #47111

Release Notes:

- Fixed an issue where using `@fetch` in the agent panel would break if
URL contained an `@` character

Co-authored-by: Marshall Bowers <git@maxdeviant.com>

Change summary

crates/agent_ui/src/completion_provider.rs | 75 +++++++++++++++++++++--
1 file changed, 67 insertions(+), 8 deletions(-)

Detailed changes

crates/agent_ui/src/completion_provider.rs 🔗

@@ -1691,17 +1691,34 @@ impl MentionCompletion {
         offset_to_line: usize,
         supported_modes: &[PromptContextType],
     ) -> Option<Self> {
-        let last_mention_start = line.rfind('@')?;
+        // Find the rightmost '@' that has a word boundary before it and no whitespace immediately after
+        let mut last_mention_start = None;
+        for (idx, _) in line.rmatch_indices('@') {
+            // No whitespace immediately after '@'
+            if line[idx + 1..]
+                .chars()
+                .next()
+                .is_some_and(|c| c.is_whitespace())
+            {
+                continue;
+            }
 
-        // No whitespace immediately after '@'
-        if line[last_mention_start + 1..]
-            .chars()
-            .next()
-            .is_some_and(|c| c.is_whitespace())
-        {
-            return None;
+            // Must be a word boundary before '@'
+            if idx > 0
+                && line[..idx]
+                    .chars()
+                    .last()
+                    .is_some_and(|c| !c.is_whitespace())
+            {
+                continue;
+            }
+
+            last_mention_start = Some(idx);
+            break;
         }
 
+        let last_mention_start = last_mention_start?;
+
         //  Must be a word boundary before '@'
         if last_mention_start > 0
             && line[..last_mention_start]
@@ -2488,6 +2505,48 @@ mod tests {
             None,
             "Should not parse with a space after @ at the start of the line"
         );
+
+        assert_eq!(
+            MentionCompletion::try_parse(
+                "@fetch https://www.npmjs.com/package/@matterport/sdk",
+                0,
+                &[PromptContextType::Fetch]
+            ),
+            Some(MentionCompletion {
+                source_range: 0..52,
+                mode: Some(PromptContextType::Fetch),
+                argument: Some("https://www.npmjs.com/package/@matterport/sdk".to_string()),
+            }),
+            "Should handle URLs with @ in the path"
+        );
+
+        assert_eq!(
+            MentionCompletion::try_parse(
+                "@fetch https://example.com/@org/@repo/file",
+                0,
+                &[PromptContextType::Fetch]
+            ),
+            Some(MentionCompletion {
+                source_range: 0..42,
+                mode: Some(PromptContextType::Fetch),
+                argument: Some("https://example.com/@org/@repo/file".to_string()),
+            }),
+            "Should handle URLs with multiple @ characters"
+        );
+
+        assert_eq!(
+            MentionCompletion::try_parse(
+                "@fetch https://example.com/@",
+                0,
+                &[PromptContextType::Fetch]
+            ),
+            Some(MentionCompletion {
+                source_range: 0..28,
+                mode: Some(PromptContextType::Fetch),
+                argument: Some("https://example.com/@".to_string()),
+            }),
+            "Should parse URL ending with @ (even if URL is incomplete)"
+        );
     }
 
     #[gpui::test]