agent_ui: Remove context strip from inline assistant (#42633)

Bennet Bo Fenner and Danilo Leal created

TODO
- [x] Implement PromptEditor::paste
- [x] Fix creases on unlink
- [x] PromptCompletionProviderDelegate::supports_images
- [ ] Fix highlighting in completion menu

Release Notes:

- N/A

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

Cargo.lock                                                  |    2 
Cargo.toml                                                  |    1 
assets/keymaps/default-linux.json                           |   16 
assets/keymaps/default-macos.json                           |   16 
assets/keymaps/default-windows.json                         |   17 
crates/agent_ui/Cargo.toml                                  |    2 
crates/agent_ui/src/acp.rs                                  |    1 
crates/agent_ui/src/acp/message_editor.rs                   |  886 ----
crates/agent_ui/src/agent_panel.rs                          |   27 
crates/agent_ui/src/agent_ui.rs                             |   20 
crates/agent_ui/src/buffer_codegen.rs                       |   90 
crates/agent_ui/src/completion_provider.rs                  |  816 +++-
crates/agent_ui/src/context.rs                              | 1160 ------
crates/agent_ui/src/context_picker.rs                       |  931 -----
crates/agent_ui/src/context_picker/fetch_context_picker.rs  |  252 -
crates/agent_ui/src/context_picker/file_context_picker.rs   |  392 --
crates/agent_ui/src/context_picker/rules_context_picker.rs  |  224 -
crates/agent_ui/src/context_picker/symbol_context_picker.rs |  415 --
crates/agent_ui/src/context_picker/thread_context_picker.rs |  280 -
crates/agent_ui/src/context_store.rs                        |  614 ---
crates/agent_ui/src/context_strip.rs                        |  619 ---
crates/agent_ui/src/inline_assistant.rs                     |   75 
crates/agent_ui/src/inline_prompt_editor.rs                 |  453 +-
crates/agent_ui/src/mention_set.rs                          | 1112 ++++++
crates/agent_ui/src/terminal_inline_assistant.rs            |   55 
crates/agent_ui/src/ui.rs                                   |    2 
crates/agent_ui/src/ui/context_pill.rs                      |  858 -----
27 files changed, 2,179 insertions(+), 7,157 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -368,7 +368,6 @@ dependencies = [
  "prompt_store",
  "proto",
  "rand 0.9.2",
- "ref-cast",
  "release_channel",
  "rope",
  "rules_library",
@@ -394,7 +393,6 @@ dependencies = [
  "ui_input",
  "unindent",
  "url",
- "urlencoding",
  "util",
  "watch",
  "workspace",

Cargo.toml πŸ”—

@@ -607,7 +607,6 @@ pulldown-cmark = { version = "0.12.0", default-features = false }
 quote = "1.0.9"
 rand = "0.9"
 rayon = "1.8"
-ref-cast = "1.0.24"
 regex = "1.5"
 # WARNING: If you change this, you must also publish a new version of zed-reqwest to crates.io
 reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "c15662463bda39148ba154100dd44d3fba5873a4", default-features = false, features = [

assets/keymaps/default-linux.json πŸ”—

@@ -239,13 +239,11 @@
       "ctrl-alt-l": "agent::OpenRulesLibrary",
       "ctrl-i": "agent::ToggleProfileSelector",
       "ctrl-alt-/": "agent::ToggleModelSelector",
-      "ctrl-shift-a": "agent::ToggleContextPicker",
       "ctrl-shift-j": "agent::ToggleNavigationMenu",
       "ctrl-alt-i": "agent::ToggleOptionsMenu",
       "ctrl-alt-shift-n": "agent::ToggleNewThreadMenu",
       "shift-alt-escape": "agent::ExpandMessageEditor",
       "ctrl->": "agent::AddSelectionToThread",
-      "ctrl-alt-e": "agent::RemoveAllContext",
       "ctrl-shift-e": "project_panel::ToggleFocus",
       "ctrl-shift-enter": "agent::ContinueThread",
       "super-ctrl-b": "agent::ToggleBurnMode",
@@ -322,17 +320,6 @@
       "alt-enter": "editor::Newline"
     }
   },
-  {
-    "context": "ContextStrip",
-    "bindings": {
-      "up": "agent::FocusUp",
-      "right": "agent::FocusRight",
-      "left": "agent::FocusLeft",
-      "down": "agent::FocusDown",
-      "backspace": "agent::RemoveFocusedContext",
-      "enter": "agent::AcceptSuggestedContext"
-    }
-  },
   {
     "context": "AcpThread > ModeSelector",
     "bindings": {
@@ -824,8 +811,7 @@
     "context": "PromptEditor",
     "bindings": {
       "ctrl-[": "agent::CyclePreviousInlineAssist",
-      "ctrl-]": "agent::CycleNextInlineAssist",
-      "ctrl-alt-e": "agent::RemoveAllContext"
+      "ctrl-]": "agent::CycleNextInlineAssist"
     }
   },
   {

assets/keymaps/default-macos.json πŸ”—

@@ -278,13 +278,11 @@
       "cmd-alt-p": "agent::ManageProfiles",
       "cmd-i": "agent::ToggleProfileSelector",
       "cmd-alt-/": "agent::ToggleModelSelector",
-      "cmd-shift-a": "agent::ToggleContextPicker",
       "cmd-shift-j": "agent::ToggleNavigationMenu",
       "cmd-alt-m": "agent::ToggleOptionsMenu",
       "cmd-alt-shift-n": "agent::ToggleNewThreadMenu",
       "shift-alt-escape": "agent::ExpandMessageEditor",
       "cmd->": "agent::AddSelectionToThread",
-      "cmd-alt-e": "agent::RemoveAllContext",
       "cmd-shift-e": "project_panel::ToggleFocus",
       "cmd-ctrl-b": "agent::ToggleBurnMode",
       "cmd-shift-enter": "agent::ContinueThread",
@@ -365,18 +363,6 @@
       "alt-enter": "editor::Newline"
     }
   },
-  {
-    "context": "ContextStrip",
-    "use_key_equivalents": true,
-    "bindings": {
-      "up": "agent::FocusUp",
-      "right": "agent::FocusRight",
-      "left": "agent::FocusLeft",
-      "down": "agent::FocusDown",
-      "backspace": "agent::RemoveFocusedContext",
-      "enter": "agent::AcceptSuggestedContext"
-    }
-  },
   {
     "context": "AgentConfiguration",
     "bindings": {
@@ -889,9 +875,7 @@
     "context": "PromptEditor",
     "use_key_equivalents": true,
     "bindings": {
-      "cmd-shift-a": "agent::ToggleContextPicker",
       "cmd-alt-/": "agent::ToggleModelSelector",
-      "cmd-alt-e": "agent::RemoveAllContext",
       "ctrl-[": "agent::CyclePreviousInlineAssist",
       "ctrl-]": "agent::CycleNextInlineAssist"
     }

assets/keymaps/default-windows.json πŸ”—

@@ -240,13 +240,11 @@
       "shift-alt-p": "agent::ManageProfiles",
       "ctrl-i": "agent::ToggleProfileSelector",
       "shift-alt-/": "agent::ToggleModelSelector",
-      "ctrl-shift-a": "agent::ToggleContextPicker",
       "ctrl-shift-j": "agent::ToggleNavigationMenu",
       "ctrl-alt-i": "agent::ToggleOptionsMenu",
       // "ctrl-shift-alt-n": "agent::ToggleNewThreadMenu",
       "shift-alt-escape": "agent::ExpandMessageEditor",
       "ctrl-shift-.": "agent::AddSelectionToThread",
-      "shift-alt-e": "agent::RemoveAllContext",
       "ctrl-shift-e": "project_panel::ToggleFocus",
       "ctrl-shift-enter": "agent::ContinueThread",
       "super-ctrl-b": "agent::ToggleBurnMode",
@@ -328,18 +326,6 @@
       "alt-enter": "editor::Newline"
     }
   },
-  {
-    "context": "ContextStrip",
-    "use_key_equivalents": true,
-    "bindings": {
-      "up": "agent::FocusUp",
-      "right": "agent::FocusRight",
-      "left": "agent::FocusLeft",
-      "down": "agent::FocusDown",
-      "backspace": "agent::RemoveFocusedContext",
-      "enter": "agent::AcceptSuggestedContext"
-    }
-  },
   {
     "context": "AcpThread > ModeSelector",
     "bindings": {
@@ -837,8 +823,7 @@
     "use_key_equivalents": true,
     "bindings": {
       "ctrl-[": "agent::CyclePreviousInlineAssist",
-      "ctrl-]": "agent::CycleNextInlineAssist",
-      "shift-alt-e": "agent::RemoveAllContext"
+      "ctrl-]": "agent::CycleNextInlineAssist"
     }
   },
   {

crates/agent_ui/Cargo.toml πŸ”—

@@ -69,7 +69,6 @@ postage.workspace = true
 project.workspace = true
 prompt_store.workspace = true
 proto.workspace = true
-ref-cast.workspace = true
 release_channel.workspace = true
 rope.workspace = true
 rules_library.workspace = true
@@ -93,7 +92,6 @@ time_format.workspace = true
 ui.workspace = true
 ui_input.workspace = true
 url.workspace = true
-urlencoding.workspace = true
 util.workspace = true
 watch.workspace = true
 workspace.workspace = true

crates/agent_ui/src/acp/message_editor.rs πŸ”—

@@ -1,68 +1,45 @@
 use crate::{
     ChatWithFollow,
-    acp::completion_provider::{ContextPickerCompletionProvider, SlashCommandCompletion},
-    context_picker::{ContextPickerAction, fetch_context_picker::fetch_url_content},
+    completion_provider::{
+        PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextAction,
+        PromptContextType, SlashCommandCompletion,
+    },
+    mention_set::{
+        Mention, MentionImage, MentionSet, insert_crease_for_mention, paste_images_as_context,
+    },
 };
-use acp_thread::{MentionUri, selection_name};
-use agent::{HistoryStore, outline};
+use acp_thread::MentionUri;
+use agent::HistoryStore;
 use agent_client_protocol as acp;
-use agent_servers::{AgentServer, AgentServerDelegate};
 use anyhow::{Result, anyhow};
-use assistant_slash_commands::codeblock_fence_for_path;
-use collections::{HashMap, HashSet};
+use collections::HashSet;
 use editor::{
-    Addon, Anchor, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
-    EditorEvent, EditorMode, EditorSnapshot, EditorStyle, ExcerptId, FoldPlaceholder, Inlay,
-    MultiBuffer, MultiBufferOffset, ToOffset,
-    actions::Paste,
-    code_context_menus::CodeContextMenu,
-    display_map::{Crease, CreaseId, FoldId},
-    scroll::Autoscroll,
-};
-use futures::{
-    FutureExt as _,
-    future::{Shared, join_all},
+    Addon, AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement,
+    EditorEvent, EditorMode, EditorStyle, Inlay, MultiBuffer, MultiBufferOffset, ToOffset,
+    actions::Paste, code_context_menus::CodeContextMenu, scroll::Autoscroll,
 };
+use futures::{FutureExt as _, future::join_all};
 use gpui::{
-    Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Entity, EntityId,
-    EventEmitter, FocusHandle, Focusable, Image, ImageFormat, Img, KeyContext, SharedString,
-    Subscription, Task, TextStyle, WeakEntity, pulsating_between,
+    AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat, KeyContext,
+    SharedString, Subscription, Task, TextStyle, WeakEntity,
 };
-use itertools::Either;
 use language::{Buffer, Language, language_settings::InlayHintKind};
-use language_model::LanguageModelImage;
-use postage::stream::Stream as _;
-use project::{
-    CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectItem, ProjectPath,
-    Worktree,
-};
-use prompt_store::{PromptId, PromptStore};
+use project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree};
+use prompt_store::PromptStore;
 use rope::Point;
 use settings::Settings;
-use std::{
-    cell::RefCell,
-    ffi::OsStr,
-    fmt::Write,
-    ops::{Range, RangeInclusive},
-    path::{Path, PathBuf},
-    rc::Rc,
-    sync::Arc,
-    time::Duration,
-};
-use text::OffsetRangeExt;
+use std::{cell::RefCell, fmt::Write, rc::Rc, sync::Arc};
 use theme::ThemeSettings;
-use ui::{ButtonLike, TintColor, Toggleable, prelude::*};
-use util::{ResultExt, debug_panic, rel_path::RelPath};
-use workspace::{CollaboratorId, Workspace, notifications::NotifyResultExt as _};
+use ui::prelude::*;
+use util::{ResultExt, debug_panic};
+use workspace::{CollaboratorId, Workspace};
 use zed_actions::agent::Chat;
 
 pub struct MessageEditor {
-    mention_set: MentionSet,
+    mention_set: Entity<MentionSet>,
     editor: Entity<Editor>,
     project: Entity<Project>,
     workspace: WeakEntity<Workspace>,
-    history_store: Entity<HistoryStore>,
-    prompt_store: Option<Entity<PromptStore>>,
     prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
     available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
     agent_name: SharedString,
@@ -82,6 +59,41 @@ impl EventEmitter<MessageEditorEvent> for MessageEditor {}
 
 const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0);
 
+impl PromptCompletionProviderDelegate for Entity<MessageEditor> {
+    fn supports_images(&self, cx: &App) -> bool {
+        self.read(cx).prompt_capabilities.borrow().image
+    }
+
+    fn supported_modes(&self, cx: &App) -> Vec<PromptContextType> {
+        let mut supported = vec![PromptContextType::File, PromptContextType::Symbol];
+        if self.read(cx).prompt_capabilities.borrow().embedded_context {
+            supported.extend(&[
+                PromptContextType::Thread,
+                PromptContextType::Fetch,
+                PromptContextType::Rules,
+            ]);
+        }
+        supported
+    }
+
+    fn available_commands(&self, cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
+        self.read(cx)
+            .available_commands
+            .borrow()
+            .iter()
+            .map(|cmd| crate::completion_provider::AvailableCommand {
+                name: cmd.name.clone().into(),
+                description: cmd.description.clone().into(),
+                requires_argument: cmd.input.is_some(),
+            })
+            .collect()
+    }
+
+    fn confirm_command(&self, cx: &mut App) {
+        self.update(cx, |this, cx| this.send(cx));
+    }
+}
+
 impl MessageEditor {
     pub fn new(
         workspace: WeakEntity<Workspace>,
@@ -103,15 +115,7 @@ impl MessageEditor {
             },
             None,
         );
-        let completion_provider = Rc::new(ContextPickerCompletionProvider::new(
-            cx.weak_entity(),
-            workspace.clone(),
-            history_store.clone(),
-            prompt_store.clone(),
-            prompt_capabilities.clone(),
-            available_commands.clone(),
-        ));
-        let mention_set = MentionSet::default();
+
         let editor = cx.new(|cx| {
             let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
             let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
@@ -121,7 +125,6 @@ impl MessageEditor {
             editor.set_show_indent_guides(false, cx);
             editor.set_soft_wrap();
             editor.set_use_modal_editing(true);
-            editor.set_completion_provider(Some(completion_provider.clone()));
             editor.set_context_menu_options(ContextMenuOptions {
                 min_entries_visible: 12,
                 max_entries_visible: 12,
@@ -130,6 +133,26 @@ impl MessageEditor {
             editor.register_addon(MessageEditorAddon::new());
             editor
         });
+        let mention_set = cx.new(|cx| {
+            MentionSet::new(
+                editor.clone(),
+                project.downgrade(),
+                history_store.clone(),
+                prompt_store.clone(),
+                window,
+                cx,
+            )
+        });
+        let completion_provider = Rc::new(PromptCompletionProvider::new(
+            cx.entity(),
+            mention_set.clone(),
+            history_store.clone(),
+            prompt_store.clone(),
+            workspace.clone(),
+        ));
+        editor.update(cx, |editor, _cx| {
+            editor.set_completion_provider(Some(completion_provider.clone()))
+        });
 
         cx.on_focus_in(&editor.focus_handle(cx), window, |_, _, cx| {
             cx.emit(MessageEditorEvent::Focus)
@@ -143,12 +166,12 @@ impl MessageEditor {
         let mut has_hint = false;
         let mut subscriptions = Vec::new();
 
-        subscriptions.push(cx.subscribe_in(&editor, window, {
-            move |this, editor, event, window, cx| {
+        subscriptions.push(cx.subscribe(&editor, {
+            move |this, editor, event, cx| {
                 if let EditorEvent::Edited { .. } = event
                     && !editor.read(cx).read_only(cx)
                 {
-                    let snapshot = editor.update(cx, |editor, cx| {
+                    editor.update(cx, |editor, cx| {
                         let new_hints = this
                             .command_hint(editor.buffer(), cx)
                             .into_iter()
@@ -164,11 +187,7 @@ impl MessageEditor {
                             cx,
                         );
                         has_hint = has_new_hint;
-
-                        editor.snapshot(window, cx)
                     });
-                    this.mention_set.remove_invalid(snapshot);
-
                     cx.notify();
                 }
             }
@@ -179,8 +198,6 @@ impl MessageEditor {
             project,
             mention_set,
             workspace,
-            history_store,
-            prompt_store,
             prompt_capabilities,
             available_commands,
             agent_name,
@@ -238,6 +255,9 @@ impl MessageEditor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        let Some(workspace) = self.workspace.upgrade() else {
+            return;
+        };
         let uri = MentionUri::Thread {
             id: thread.id.clone(),
             name: thread.title.to_string(),
@@ -256,7 +276,21 @@ impl MessageEditor {
                 .text_anchor
         });
 
-        self.confirm_mention_completion(thread.title, start, content_len, uri, window, cx)
+        let supports_images = self.prompt_capabilities.borrow().image;
+
+        self.mention_set
+            .update(cx, |mention_set, cx| {
+                mention_set.confirm_mention_completion(
+                    thread.title,
+                    start,
+                    content_len,
+                    uri,
+                    supports_images,
+                    &workspace,
+                    window,
+                    cx,
+                )
+            })
             .detach();
     }
 
@@ -265,11 +299,6 @@ impl MessageEditor {
         &self.editor
     }
 
-    #[cfg(test)]
-    pub(crate) fn mention_set(&mut self) -> &mut MentionSet {
-        &mut self.mention_set
-    }
-
     pub fn is_empty(&self, cx: &App) -> bool {
         self.editor.read(cx).is_empty(cx)
     }
@@ -283,388 +312,9 @@ impl MessageEditor {
             .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
     }
 
-    pub fn mentions(&self) -> HashSet<MentionUri> {
-        self.mention_set
-            .mentions
-            .values()
-            .map(|(uri, _)| uri.clone())
-            .collect()
-    }
-
-    pub fn confirm_mention_completion(
-        &mut self,
-        crease_text: SharedString,
-        start: text::Anchor,
-        content_len: usize,
-        mention_uri: MentionUri,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Task<()> {
-        let snapshot = self
-            .editor
-            .update(cx, |editor, cx| editor.snapshot(window, cx));
-        let Some(start_anchor) = snapshot.buffer_snapshot().as_singleton_anchor(start) else {
-            return Task::ready(());
-        };
-        let excerpt_id = start_anchor.excerpt_id;
-        let end_anchor = snapshot.buffer_snapshot().anchor_before(
-            start_anchor.to_offset(&snapshot.buffer_snapshot()) + content_len + 1usize,
-        );
-
-        let crease = if let MentionUri::File { abs_path } = &mention_uri
-            && let Some(extension) = abs_path.extension()
-            && let Some(extension) = extension.to_str()
-            && Img::extensions().contains(&extension)
-            && !extension.contains("svg")
-        {
-            let Some(project_path) = self
-                .project
-                .read(cx)
-                .project_path_for_absolute_path(&abs_path, cx)
-            else {
-                log::error!("project path not found");
-                return Task::ready(());
-            };
-            let image = self
-                .project
-                .update(cx, |project, cx| project.open_image(project_path, cx));
-            let image = cx
-                .spawn(async move |_, cx| {
-                    let image = image.await.map_err(|e| e.to_string())?;
-                    let image = image
-                        .update(cx, |image, _| image.image.clone())
-                        .map_err(|e| e.to_string())?;
-                    Ok(image)
-                })
-                .shared();
-            insert_crease_for_mention(
-                excerpt_id,
-                start,
-                content_len,
-                mention_uri.name().into(),
-                IconName::Image.path().into(),
-                Some(image),
-                self.editor.clone(),
-                window,
-                cx,
-            )
-        } else {
-            insert_crease_for_mention(
-                excerpt_id,
-                start,
-                content_len,
-                crease_text,
-                mention_uri.icon_path(cx),
-                None,
-                self.editor.clone(),
-                window,
-                cx,
-            )
-        };
-        let Some((crease_id, tx)) = crease else {
-            return Task::ready(());
-        };
-
-        let task = match mention_uri.clone() {
-            MentionUri::Fetch { url } => self.confirm_mention_for_fetch(url, cx),
-            MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
-            MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
-            MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx),
-            MentionUri::File { abs_path } => self.confirm_mention_for_file(abs_path, cx),
-            MentionUri::Symbol {
-                abs_path,
-                line_range,
-                ..
-            } => self.confirm_mention_for_symbol(abs_path, line_range, cx),
-            MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
-            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"
-                )))
-            }
-            MentionUri::Selection { .. } => {
-                debug_panic!("unexpected selection URI");
-                Task::ready(Err(anyhow!("unexpected selection URI")))
-            }
-        };
-        let task = cx
-            .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
-            .shared();
-        self.mention_set
-            .mentions
-            .insert(crease_id, (mention_uri, task.clone()));
-
-        // Notify the user if we failed to load the mentioned context
-        cx.spawn_in(window, async move |this, cx| {
-            let result = task.await.notify_async_err(cx);
-            drop(tx);
-            if result.is_none() {
-                this.update(cx, |this, cx| {
-                    this.editor.update(cx, |editor, cx| {
-                        // Remove mention
-                        editor.edit([(start_anchor..end_anchor, "")], cx);
-                    });
-                    this.mention_set.mentions.remove(&crease_id);
-                })
-                .ok();
-            }
-        })
-    }
-
-    fn confirm_mention_for_file(
-        &mut self,
-        abs_path: PathBuf,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Mention>> {
-        let Some(project_path) = self
-            .project
-            .read(cx)
-            .project_path_for_absolute_path(&abs_path, cx)
-        else {
-            return Task::ready(Err(anyhow!("project path not found")));
-        };
-        let extension = abs_path
-            .extension()
-            .and_then(OsStr::to_str)
-            .unwrap_or_default();
-
-        if Img::extensions().contains(&extension) && !extension.contains("svg") {
-            if !self.prompt_capabilities.borrow().image {
-                return Task::ready(Err(anyhow!("This model does not support images yet")));
-            }
-            let task = self
-                .project
-                .update(cx, |project, cx| project.open_image(project_path, cx));
-            return cx.spawn(async move |_, cx| {
-                let image = task.await?;
-                let image = image.update(cx, |image, _| image.image.clone())?;
-                let format = image.format;
-                let image = cx
-                    .update(|cx| LanguageModelImage::from_image(image, cx))?
-                    .await;
-                if let Some(image) = image {
-                    Ok(Mention::Image(MentionImage {
-                        data: image.source,
-                        format,
-                    }))
-                } else {
-                    Err(anyhow!("Failed to convert image"))
-                }
-            });
-        }
-
-        let buffer = self
-            .project
-            .update(cx, |project, cx| project.open_buffer(project_path, cx));
-        cx.spawn(async move |_, cx| {
-            let buffer = buffer.await?;
-            let buffer_content = outline::get_buffer_content_or_outline(
-                buffer.clone(),
-                Some(&abs_path.to_string_lossy()),
-                &cx,
-            )
-            .await?;
-
-            Ok(Mention::Text {
-                content: buffer_content.text,
-                tracked_buffers: vec![buffer],
-            })
-        })
-    }
-
-    fn confirm_mention_for_fetch(
-        &mut self,
-        url: url::Url,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Mention>> {
-        let http_client = match self
-            .workspace
-            .update(cx, |workspace, _| workspace.client().http_client())
-        {
-            Ok(http_client) => http_client,
-            Err(e) => return Task::ready(Err(e)),
-        };
-        cx.background_executor().spawn(async move {
-            let content = fetch_url_content(http_client, url.to_string()).await?;
-            Ok(Mention::Text {
-                content,
-                tracked_buffers: Vec::new(),
-            })
-        })
-    }
-
-    fn confirm_mention_for_symbol(
-        &mut self,
-        abs_path: PathBuf,
-        line_range: RangeInclusive<u32>,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Mention>> {
-        let Some(project_path) = self
-            .project
-            .read(cx)
-            .project_path_for_absolute_path(&abs_path, cx)
-        else {
-            return Task::ready(Err(anyhow!("project path not found")));
-        };
-        let buffer = self
-            .project
-            .update(cx, |project, cx| project.open_buffer(project_path, cx));
-        cx.spawn(async move |_, cx| {
-            let buffer = buffer.await?;
-            let mention = buffer.update(cx, |buffer, cx| {
-                let start = Point::new(*line_range.start(), 0).min(buffer.max_point());
-                let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point());
-                let content = buffer.text_for_range(start..end).collect();
-                Mention::Text {
-                    content,
-                    tracked_buffers: vec![cx.entity()],
-                }
-            })?;
-            anyhow::Ok(mention)
-        })
-    }
-
-    fn confirm_mention_for_rule(
-        &mut self,
-        id: PromptId,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Mention>> {
-        let Some(prompt_store) = self.prompt_store.clone() else {
-            return Task::ready(Err(anyhow!("missing prompt store")));
-        };
-        let prompt = prompt_store.read(cx).load(id, cx);
-        cx.spawn(async move |_, _| {
-            let prompt = prompt.await?;
-            Ok(Mention::Text {
-                content: prompt,
-                tracked_buffers: Vec::new(),
-            })
-        })
-    }
-
-    pub fn confirm_mention_for_selection(
-        &mut self,
-        source_range: Range<text::Anchor>,
-        selections: Vec<(Entity<Buffer>, Range<text::Anchor>, Range<usize>)>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
-        let Some(start) = snapshot.as_singleton_anchor(source_range.start) else {
-            return;
-        };
-
-        let offset = start.to_offset(&snapshot);
-
-        for (buffer, selection_range, range_to_fold) in selections {
-            let range = snapshot.anchor_after(offset + range_to_fold.start)
-                ..snapshot.anchor_after(offset + range_to_fold.end);
-
-            let abs_path = buffer
-                .read(cx)
-                .project_path(cx)
-                .and_then(|project_path| self.project.read(cx).absolute_path(&project_path, cx));
-            let snapshot = buffer.read(cx).snapshot();
-
-            let text = snapshot
-                .text_for_range(selection_range.clone())
-                .collect::<String>();
-            let point_range = selection_range.to_point(&snapshot);
-            let line_range = point_range.start.row..=point_range.end.row;
-
-            let uri = MentionUri::Selection {
-                abs_path: abs_path.clone(),
-                line_range: line_range.clone(),
-            };
-            let crease = crate::context_picker::crease_for_mention(
-                selection_name(abs_path.as_deref(), &line_range).into(),
-                uri.icon_path(cx),
-                range,
-                self.editor.downgrade(),
-            );
-
-            let crease_id = self.editor.update(cx, |editor, cx| {
-                let crease_ids = editor.insert_creases(vec![crease.clone()], cx);
-                editor.fold_creases(vec![crease], false, window, cx);
-                crease_ids.first().copied().unwrap()
-            });
-
-            self.mention_set.mentions.insert(
-                crease_id,
-                (
-                    uri,
-                    Task::ready(Ok(Mention::Text {
-                        content: text,
-                        tracked_buffers: vec![buffer],
-                    }))
-                    .shared(),
-                ),
-            );
-        }
-
-        // Take this explanation with a grain of salt but, with creases being
-        // inserted, GPUI's recomputes the editor layout in the next frames, so
-        // directly calling `editor.request_autoscroll` wouldn't work as
-        // expected. We're leveraging `cx.on_next_frame` to wait 2 frames and
-        // ensure that the layout has been recalculated so that the autoscroll
-        // request actually shows the cursor's new position.
-        let editor = self.editor.clone();
-        cx.on_next_frame(window, move |_, window, cx| {
-            cx.on_next_frame(window, move |_, _, cx| {
-                editor.update(cx, |editor, cx| {
-                    editor.request_autoscroll(Autoscroll::fit(), cx)
-                });
-            });
-        });
-    }
-
-    fn confirm_mention_for_thread(
-        &mut self,
-        id: acp::SessionId,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Mention>> {
-        let server = Rc::new(agent::NativeAgentServer::new(
-            self.project.read(cx).fs().clone(),
-            self.history_store.clone(),
-        ));
-        let delegate = AgentServerDelegate::new(
-            self.project.read(cx).agent_server_store().clone(),
-            self.project.clone(),
-            None,
-            None,
-        );
-        let connection = server.connect(None, delegate, cx);
-        cx.spawn(async move |_, cx| {
-            let (agent, _) = connection.await?;
-            let agent = agent.downcast::<agent::NativeAgentConnection>().unwrap();
-            let summary = agent
-                .0
-                .update(cx, |agent, cx| agent.thread_summary(id, cx))?
-                .await?;
-            anyhow::Ok(Mention::Text {
-                content: summary.to_string(),
-                tracked_buffers: Vec::new(),
-            })
-        })
-    }
-
-    fn confirm_mention_for_text_thread(
-        &mut self,
-        path: PathBuf,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Mention>> {
-        let text_thread_task = self.history_store.update(cx, |store, cx| {
-            store.load_text_thread(path.as_path().into(), cx)
-        });
-        cx.spawn(async move |_, cx| {
-            let text_thread = text_thread_task.await?;
-            let xml = text_thread.update(cx, |text_thread, cx| text_thread.to_xml(cx))?;
-            Ok(Mention::Text {
-                content: xml,
-                tracked_buffers: Vec::new(),
-            })
-        })
+    #[cfg(test)]
+    pub fn mention_set(&self) -> &Entity<MentionSet> {
+        &self.mention_set
     }
 
     fn validate_slash_commands(
@@ -716,7 +366,7 @@ impl MessageEditor {
 
         let contents = self
             .mention_set
-            .contents(full_mention_content, self.project.clone(), cx);
+            .update(cx, |store, cx| store.contents(full_mention_content, cx));
         let editor = self.editor.clone();
         let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context;
 
@@ -828,10 +478,12 @@ impl MessageEditor {
         self.editor.update(cx, |editor, cx| {
             editor.clear(window, cx);
             editor.remove_creases(
-                self.mention_set
-                    .mentions
-                    .drain()
-                    .map(|(crease_id, _)| crease_id),
+                self.mention_set.update(cx, |mention_set, _cx| {
+                    mention_set
+                        .remove_all()
+                        .map(|(crease_id, _)| crease_id)
+                        .collect::<Vec<_>>()
+                }),
                 cx,
             )
         });
@@ -910,153 +562,12 @@ impl MessageEditor {
     }
 
     fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
-        if !self.prompt_capabilities.borrow().image {
-            return;
+        if self.prompt_capabilities.borrow().image
+            && let Some(task) =
+                paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
+        {
+            task.detach();
         }
-        let Some(clipboard) = cx.read_from_clipboard() else {
-            return;
-        };
-        cx.spawn_in(window, async move |this, cx| {
-            use itertools::Itertools;
-            let (mut images, paths) = clipboard
-                .into_entries()
-                .filter_map(|entry| match entry {
-                    ClipboardEntry::Image(image) => Some(Either::Left(image)),
-                    ClipboardEntry::ExternalPaths(paths) => Some(Either::Right(paths)),
-                    _ => None,
-                })
-                .partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);
-
-            if !paths.is_empty() {
-                images.extend(
-                    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 {
-                                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,
-                            ));
-                        }
-                        images
-                    })
-                    .await,
-                );
-            }
-
-            if images.is_empty() {
-                return;
-            }
-
-            let replacement_text = MentionUri::PastedImage.as_link().to_string();
-            let Ok(editor) = this.update(cx, |this, cx| {
-                cx.stop_propagation();
-                this.editor.clone()
-            }) else {
-                return;
-            };
-            for image in images {
-                let Ok((excerpt_id, text_anchor, multibuffer_anchor)) =
-                    editor.update_in(cx, |message_editor, window, cx| {
-                        let snapshot = message_editor.snapshot(window, cx);
-                        let (excerpt_id, _, buffer_snapshot) =
-                            snapshot.buffer_snapshot().as_singleton().unwrap();
-
-                        let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
-                        let multibuffer_anchor = snapshot
-                            .buffer_snapshot()
-                            .anchor_in_excerpt(*excerpt_id, text_anchor);
-                        message_editor.edit(
-                            [(
-                                multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
-                                format!("{replacement_text} "),
-                            )],
-                            cx,
-                        );
-                        (*excerpt_id, text_anchor, multibuffer_anchor)
-                    })
-                else {
-                    break;
-                };
-
-                let content_len = replacement_text.len();
-                let Some(start_anchor) = multibuffer_anchor else {
-                    continue;
-                };
-                let Ok(end_anchor) = editor.update(cx, |editor, cx| {
-                    let snapshot = editor.buffer().read(cx).snapshot(cx);
-                    snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
-                }) else {
-                    continue;
-                };
-                let image = Arc::new(image);
-                let Ok(Some((crease_id, tx))) = cx.update(|window, cx| {
-                    insert_crease_for_mention(
-                        excerpt_id,
-                        text_anchor,
-                        content_len,
-                        MentionUri::PastedImage.name().into(),
-                        IconName::Image.path().into(),
-                        Some(Task::ready(Ok(image.clone())).shared()),
-                        editor.clone(),
-                        window,
-                        cx,
-                    )
-                }) else {
-                    continue;
-                };
-                let task = cx
-                    .spawn(async move |cx| {
-                        let format = image.format;
-                        let image = cx
-                            .update(|_, cx| LanguageModelImage::from_image(image, cx))
-                            .map_err(|e| e.to_string())?
-                            .await;
-                        drop(tx);
-                        if let Some(image) = image {
-                            Ok(Mention::Image(MentionImage {
-                                data: image.source,
-                                format,
-                            }))
-                        } else {
-                            Err("Failed to convert image".into())
-                        }
-                    })
-                    .shared();
-
-                this.update(cx, |this, _| {
-                    this.mention_set
-                        .mentions
-                        .insert(crease_id, (MentionUri::PastedImage, task.clone()))
-                })
-                .ok();
-
-                if task.await.notify_async_err(cx).is_none() {
-                    this.update(cx, |this, cx| {
-                        this.editor.update(cx, |editor, cx| {
-                            editor.edit([(start_anchor..end_anchor, "")], cx);
-                        });
-                        this.mention_set.mentions.remove(&crease_id);
-                    })
-                    .ok();
-                }
-            }
-        })
-        .detach();
     }
 
     pub fn insert_dragged_files(
@@ -1066,6 +577,9 @@ impl MessageEditor {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        let Some(workspace) = self.workspace.upgrade() else {
+            return;
+        };
         let path_style = self.project.read(cx).path_style(cx);
         let buffer = self.editor.read(cx).buffer().clone();
         let Some(buffer) = buffer.read(cx).as_singleton() else {
@@ -1080,12 +594,11 @@ impl MessageEditor {
                 continue;
             };
             let abs_path = worktree.read(cx).absolutize(&path.path);
-            let (file_name, _) =
-                crate::context_picker::file_context_picker::extract_file_name_and_directory(
-                    &path.path,
-                    worktree.read(cx).root_name(),
-                    path_style,
-                );
+            let (file_name, _) = crate::completion_provider::extract_file_name_and_directory(
+                &path.path,
+                worktree.read(cx).root_name(),
+                path_style,
+            );
 
             let uri = if entry.is_dir() {
                 MentionUri::Directory { abs_path }
@@ -1107,14 +620,19 @@ impl MessageEditor {
                     cx,
                 );
             });
-            tasks.push(self.confirm_mention_completion(
-                file_name,
-                anchor,
-                content_len,
-                uri,
-                window,
-                cx,
-            ));
+            let supports_images = self.prompt_capabilities.borrow().image;
+            tasks.push(self.mention_set.update(cx, |mention_set, cx| {
+                mention_set.confirm_mention_completion(
+                    file_name,
+                    anchor,
+                    content_len,
+                    uri,
+                    supports_images,
+                    &workspace,
+                    window,
+                    cx,
+                )
+            }));
         }
         cx.spawn(async move |_, _| {
             join_all(tasks).await;
@@ -1137,13 +655,15 @@ impl MessageEditor {
         let Some(workspace) = self.workspace.upgrade() else {
             return;
         };
-        let Some(completion) = ContextPickerCompletionProvider::completion_for_action(
-            ContextPickerAction::AddSelections,
-            anchor..anchor,
-            cx.weak_entity(),
-            &workspace,
-            cx,
-        ) else {
+        let Some(completion) =
+            PromptCompletionProvider::<Entity<MessageEditor>>::completion_for_action(
+                PromptContextAction::AddSelections,
+                anchor..anchor,
+                self.mention_set.downgrade(),
+                &workspace,
+                cx,
+            )
+        else {
             return;
         };
 
@@ -1274,10 +794,13 @@ impl MessageEditor {
             };
             drop(tx);
 
-            self.mention_set.mentions.insert(
-                crease_id,
-                (mention_uri.clone(), Task::ready(Ok(mention)).shared()),
-            );
+            self.mention_set.update(cx, |mention_set, _cx| {
+                mention_set.insert_mention(
+                    crease_id,
+                    mention_uri.clone(),
+                    Task::ready(Ok(mention)).shared(),
+                )
+            });
         }
         cx.notify();
     }
@@ -1305,111 +828,6 @@ impl MessageEditor {
     }
 }
 
-fn full_mention_for_directory(
-    project: &Entity<Project>,
-    abs_path: &Path,
-    cx: &mut App,
-) -> Task<Result<Mention>> {
-    fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, String)> {
-        let mut files = Vec::new();
-
-        for entry in worktree.child_entries(path) {
-            if entry.is_dir() {
-                files.extend(collect_files_in_path(worktree, &entry.path));
-            } else if entry.is_file() {
-                files.push((
-                    entry.path.clone(),
-                    worktree
-                        .full_path(&entry.path)
-                        .to_string_lossy()
-                        .to_string(),
-                ));
-            }
-        }
-
-        files
-    }
-
-    let Some(project_path) = project
-        .read(cx)
-        .project_path_for_absolute_path(&abs_path, cx)
-    else {
-        return Task::ready(Err(anyhow!("project path not found")));
-    };
-    let Some(entry) = project.read(cx).entry_for_path(&project_path, cx) else {
-        return Task::ready(Err(anyhow!("project entry not found")));
-    };
-    let directory_path = entry.path.clone();
-    let worktree_id = project_path.worktree_id;
-    let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
-        return Task::ready(Err(anyhow!("worktree not found")));
-    };
-    let project = project.clone();
-    cx.spawn(async move |cx| {
-        let file_paths = worktree.read_with(cx, |worktree, _cx| {
-            collect_files_in_path(worktree, &directory_path)
-        })?;
-        let descendants_future = cx.update(|cx| {
-            join_all(file_paths.into_iter().map(|(worktree_path, full_path)| {
-                let rel_path = worktree_path
-                    .strip_prefix(&directory_path)
-                    .log_err()
-                    .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
-
-                let open_task = project.update(cx, |project, cx| {
-                    project.buffer_store().update(cx, |buffer_store, cx| {
-                        let project_path = ProjectPath {
-                            worktree_id,
-                            path: worktree_path,
-                        };
-                        buffer_store.open_buffer(project_path, cx)
-                    })
-                });
-
-                cx.spawn(async move |cx| {
-                    let buffer = open_task.await.log_err()?;
-                    let buffer_content = outline::get_buffer_content_or_outline(
-                        buffer.clone(),
-                        Some(&full_path),
-                        &cx,
-                    )
-                    .await
-                    .ok()?;
-
-                    Some((rel_path, full_path, buffer_content.text, buffer))
-                })
-            }))
-        })?;
-
-        let contents = cx
-            .background_spawn(async move {
-                let (contents, tracked_buffers) = descendants_future
-                    .await
-                    .into_iter()
-                    .flatten()
-                    .map(|(rel_path, full_path, rope, buffer)| {
-                        ((rel_path, full_path, rope), buffer)
-                    })
-                    .unzip();
-                Mention::Text {
-                    content: render_directory_contents(contents),
-                    tracked_buffers,
-                }
-            })
-            .await;
-        anyhow::Ok(contents)
-    })
-}
-
-fn render_directory_contents(entries: Vec<(Arc<RelPath>, String, String)>) -> String {
-    let mut output = String::new();
-    for (_relative_path, full_path, content) in entries {
-        let fence = codeblock_fence_for_path(Some(&full_path), None);
-        write!(output, "\n{fence}\n{content}\n```").unwrap();
-    }
-    output
-}
-
 impl Focusable for MessageEditor {
     fn focus_handle(&self, cx: &App) -> FocusHandle {
         self.editor.focus_handle(cx)

crates/agent_ui/src/agent_panel.rs πŸ”—

@@ -17,6 +17,7 @@ use settings::{
 
 use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
 
+use crate::ManageProfiles;
 use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
 use crate::{
     AddContextServer, AgentDiffPane, DeleteRecentlyOpenThread, Follow, InlineAssistant,
@@ -34,7 +35,6 @@ use crate::{
     acp::{AcpThreadHistory, ThreadHistoryEvent},
 };
 use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary};
-use crate::{ManageProfiles, context_store::ContextStore};
 use agent_settings::AgentSettings;
 use ai_onboarding::AgentPanelOnboarding;
 use anyhow::{Result, anyhow};
@@ -431,7 +431,6 @@ pub struct AgentPanel {
     text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
     prompt_store: Option<Entity<PromptStore>>,
     context_server_registry: Entity<ContextServerRegistry>,
-    inline_assist_context_store: Entity<ContextStore>,
     configuration: Option<Entity<AgentConfiguration>>,
     configuration_subscription: Option<Subscription>,
     active_view: ActiveView,
@@ -543,7 +542,6 @@ impl AgentPanel {
         let client = workspace.client().clone();
         let workspace = workspace.weak_handle();
 
-        let inline_assist_context_store = cx.new(|_cx| ContextStore::new(project.downgrade()));
         let context_server_registry =
             cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
 
@@ -680,7 +678,6 @@ impl AgentPanel {
             configuration: None,
             configuration_subscription: None,
             context_server_registry,
-            inline_assist_context_store,
             previous_view: None,
             new_thread_menu_handle: PopoverMenuHandle::default(),
             agent_panel_menu_handle: PopoverMenuHandle::default(),
@@ -721,10 +718,6 @@ impl AgentPanel {
         &self.prompt_store
     }
 
-    pub(crate) fn inline_assist_context_store(&self) -> &Entity<ContextStore> {
-        &self.inline_assist_context_store
-    }
-
     pub(crate) fn thread_store(&self) -> &Entity<HistoryStore> {
         &self.history_store
     }
@@ -2664,23 +2657,19 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
         cx: &mut Context<RulesLibrary>,
     ) {
         InlineAssistant::update_global(cx, |assistant, cx| {
-            let Some(project) = self
-                .workspace
-                .upgrade()
-                .map(|workspace| workspace.read(cx).project().downgrade())
-            else {
+            let Some(workspace) = self.workspace.upgrade() else {
+                return;
+            };
+            let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
                 return;
             };
-            let prompt_store = None;
-            let thread_store = None;
-            let context_store = cx.new(|_| ContextStore::new(project.clone()));
+            let project = workspace.read(cx).project().downgrade();
             assistant.assist(
                 prompt_editor,
                 self.workspace.clone(),
-                context_store,
                 project,
-                prompt_store,
-                thread_store,
+                panel.read(cx).thread_store().clone(),
+                None,
                 initial_prompt,
                 window,
                 cx,

crates/agent_ui/src/agent_ui.rs πŸ”—

@@ -4,14 +4,13 @@ mod agent_diff;
 mod agent_model_selector;
 mod agent_panel;
 mod buffer_codegen;
+mod completion_provider;
 mod context;
-mod context_picker;
 mod context_server_configuration;
-mod context_store;
-mod context_strip;
 mod inline_assistant;
 mod inline_prompt_editor;
 mod language_model_selector;
+mod mention_set;
 mod profile_selector;
 mod slash_command;
 mod slash_command_picker;
@@ -35,7 +34,7 @@ use language::{
     language_settings::{AllLanguageSettings, EditPredictionProvider},
 };
 use language_model::{
-    ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
+    ConfiguredModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
 };
 use project::DisableAiSettings;
 use prompt_store::PromptBuilder;
@@ -56,8 +55,6 @@ actions!(
     [
         /// Creates a new text-based conversation thread.
         NewTextThread,
-        /// Toggles the context picker interface for adding files, symbols, or other context.
-        ToggleContextPicker,
         /// Toggles the menu to create new agent threads.
         ToggleNewThreadMenu,
         /// Toggles the navigation menu for switching between threads and views.
@@ -70,8 +67,6 @@ actions!(
         ToggleProfileSelector,
         /// Cycles through available session modes.
         CycleModeSelector,
-        /// Removes all added context from the current conversation.
-        RemoveAllContext,
         /// Expands the message editor to full size.
         ExpandMessageEditor,
         /// Opens the conversation history view.
@@ -94,10 +89,6 @@ actions!(
         FocusLeft,
         /// Moves focus right in the interface.
         FocusRight,
-        /// Removes the currently focused context item.
-        RemoveFocusedContext,
-        /// Accepts the suggested context item.
-        AcceptSuggestedContext,
         /// Opens the active thread as a markdown file.
         OpenActiveThreadAsMarkdown,
         /// Opens the agent diff view to review changes.
@@ -220,11 +211,6 @@ impl ModelUsageContext {
             }
         }
     }
-
-    pub fn language_model(&self, cx: &App) -> Option<Arc<dyn LanguageModel>> {
-        self.configured_model(cx)
-            .map(|configured_model| configured_model.model)
-    }
 }
 
 /// Initializes the `agent` crate.

crates/agent_ui/src/buffer_codegen.rs πŸ”—

@@ -1,6 +1,4 @@
-use crate::{
-    context::load_context, context_store::ContextStore, inline_prompt_editor::CodegenStatus,
-};
+use crate::{context::LoadedContext, inline_prompt_editor::CodegenStatus};
 use agent_settings::AgentSettings;
 use anyhow::{Context as _, Result};
 use client::telemetry::Telemetry;
@@ -8,9 +6,12 @@ use cloud_llm_client::CompletionIntent;
 use collections::HashSet;
 use editor::{Anchor, AnchorRangeExt, MultiBuffer, MultiBufferSnapshot, ToOffset as _, ToPoint};
 use futures::{
-    SinkExt, Stream, StreamExt, TryStreamExt as _, channel::mpsc, future::LocalBoxFuture, join,
+    SinkExt, Stream, StreamExt, TryStreamExt as _,
+    channel::mpsc,
+    future::{LocalBoxFuture, Shared},
+    join,
 };
-use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task, WeakEntity};
+use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Subscription, Task};
 use language::{Buffer, IndentKind, Point, TransactionId, line_diff};
 use language_model::{
     LanguageModel, LanguageModelRegistry, LanguageModelRequest, LanguageModelRequestMessage,
@@ -18,8 +19,7 @@ use language_model::{
 };
 use multi_buffer::MultiBufferRow;
 use parking_lot::Mutex;
-use project::Project;
-use prompt_store::{PromptBuilder, PromptStore};
+use prompt_store::PromptBuilder;
 use rope::Rope;
 use smol::future::FutureExt;
 use std::{
@@ -43,9 +43,6 @@ pub struct BufferCodegen {
     buffer: Entity<MultiBuffer>,
     range: Range<Anchor>,
     initial_transaction_id: Option<TransactionId>,
-    context_store: Entity<ContextStore>,
-    project: WeakEntity<Project>,
-    prompt_store: Option<Entity<PromptStore>>,
     telemetry: Arc<Telemetry>,
     builder: Arc<PromptBuilder>,
     pub is_insertion: bool,
@@ -56,9 +53,6 @@ impl BufferCodegen {
         buffer: Entity<MultiBuffer>,
         range: Range<Anchor>,
         initial_transaction_id: Option<TransactionId>,
-        context_store: Entity<ContextStore>,
-        project: WeakEntity<Project>,
-        prompt_store: Option<Entity<PromptStore>>,
         telemetry: Arc<Telemetry>,
         builder: Arc<PromptBuilder>,
         cx: &mut Context<Self>,
@@ -68,9 +62,6 @@ impl BufferCodegen {
                 buffer.clone(),
                 range.clone(),
                 false,
-                Some(context_store.clone()),
-                project.clone(),
-                prompt_store.clone(),
                 Some(telemetry.clone()),
                 builder.clone(),
                 cx,
@@ -85,9 +76,6 @@ impl BufferCodegen {
             buffer,
             range,
             initial_transaction_id,
-            context_store,
-            project,
-            prompt_store,
             telemetry,
             builder,
         };
@@ -148,6 +136,7 @@ impl BufferCodegen {
         &mut self,
         primary_model: Arc<dyn LanguageModel>,
         user_prompt: String,
+        context_task: Shared<Task<Option<LoadedContext>>>,
         cx: &mut Context<Self>,
     ) -> Result<()> {
         let alternative_models = LanguageModelRegistry::read_global(cx)
@@ -165,9 +154,6 @@ impl BufferCodegen {
                     self.buffer.clone(),
                     self.range.clone(),
                     false,
-                    Some(self.context_store.clone()),
-                    self.project.clone(),
-                    self.prompt_store.clone(),
                     Some(self.telemetry.clone()),
                     self.builder.clone(),
                     cx,
@@ -180,7 +166,7 @@ impl BufferCodegen {
             .zip(&self.alternatives)
         {
             alternative.update(cx, |alternative, cx| {
-                alternative.start(user_prompt.clone(), model.clone(), cx)
+                alternative.start(user_prompt.clone(), context_task.clone(), model.clone(), cx)
             })?;
         }
 
@@ -243,9 +229,6 @@ pub struct CodegenAlternative {
     status: CodegenStatus,
     generation: Task<()>,
     diff: Diff,
-    context_store: Option<Entity<ContextStore>>,
-    project: WeakEntity<Project>,
-    prompt_store: Option<Entity<PromptStore>>,
     telemetry: Option<Arc<Telemetry>>,
     _subscription: gpui::Subscription,
     builder: Arc<PromptBuilder>,
@@ -264,9 +247,6 @@ impl CodegenAlternative {
         buffer: Entity<MultiBuffer>,
         range: Range<Anchor>,
         active: bool,
-        context_store: Option<Entity<ContextStore>>,
-        project: WeakEntity<Project>,
-        prompt_store: Option<Entity<PromptStore>>,
         telemetry: Option<Arc<Telemetry>>,
         builder: Arc<PromptBuilder>,
         cx: &mut Context<Self>,
@@ -307,9 +287,6 @@ impl CodegenAlternative {
             status: CodegenStatus::Idle,
             generation: Task::ready(()),
             diff: Diff::default(),
-            context_store,
-            project,
-            prompt_store,
             telemetry,
             _subscription: cx.subscribe(&buffer, Self::handle_buffer_event),
             builder,
@@ -366,6 +343,7 @@ impl CodegenAlternative {
     pub fn start(
         &mut self,
         user_prompt: String,
+        context_task: Shared<Task<Option<LoadedContext>>>,
         model: Arc<dyn LanguageModel>,
         cx: &mut Context<Self>,
     ) -> Result<()> {
@@ -384,7 +362,7 @@ impl CodegenAlternative {
             if user_prompt.trim().to_lowercase() == "delete" {
                 async { Ok(LanguageModelTextStream::default()) }.boxed_local()
             } else {
-                let request = self.build_request(&model, user_prompt, cx)?;
+                let request = self.build_request(&model, user_prompt, context_task, cx)?;
                 cx.spawn(async move |_, cx| {
                     Ok(model.stream_completion_text(request.await, cx).await?)
                 })
@@ -398,6 +376,7 @@ impl CodegenAlternative {
         &self,
         model: &Arc<dyn LanguageModel>,
         user_prompt: String,
+        context_task: Shared<Task<Option<LoadedContext>>>,
         cx: &mut App,
     ) -> Result<Task<LanguageModelRequest>> {
         let buffer = self.buffer.read(cx).snapshot(cx);
@@ -437,19 +416,6 @@ impl CodegenAlternative {
             )
             .context("generating content prompt")?;
 
-        let context_task = self.context_store.as_ref().and_then(|context_store| {
-            if let Some(project) = self.project.upgrade() {
-                let context = context_store
-                    .read(cx)
-                    .context()
-                    .cloned()
-                    .collect::<Vec<_>>();
-                Some(load_context(context, &project, &self.prompt_store, cx))
-            } else {
-                None
-            }
-        });
-
         let temperature = AgentSettings::temperature_for_model(model, cx);
 
         Ok(cx.spawn(async move |_cx| {
@@ -459,10 +425,8 @@ impl CodegenAlternative {
                 cache: false,
             };
 
-            if let Some(context_task) = context_task {
-                context_task
-                    .await
-                    .add_to_request_message(&mut request_message);
+            if let Some(context) = context_task.await {
+                context.add_to_request_message(&mut request_message);
             }
 
             request_message.content.push(prompt.into());
@@ -1088,7 +1052,6 @@ impl Diff {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use fs::FakeFs;
     use futures::{
         Stream,
         stream::{self},
@@ -1120,17 +1083,12 @@ mod tests {
             snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(4, 5))
         });
         let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
-        let fs = FakeFs::new(cx.executor());
-        let project = Project::test(fs, vec![], cx).await;
         let codegen = cx.new(|cx| {
             CodegenAlternative::new(
                 buffer.clone(),
                 range.clone(),
                 true,
                 None,
-                project.downgrade(),
-                None,
-                None,
                 prompt_builder,
                 cx,
             )
@@ -1187,17 +1145,12 @@ mod tests {
             snapshot.anchor_before(Point::new(1, 6))..snapshot.anchor_after(Point::new(1, 6))
         });
         let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
-        let fs = FakeFs::new(cx.executor());
-        let project = Project::test(fs, vec![], cx).await;
         let codegen = cx.new(|cx| {
             CodegenAlternative::new(
                 buffer.clone(),
                 range.clone(),
                 true,
                 None,
-                project.downgrade(),
-                None,
-                None,
                 prompt_builder,
                 cx,
             )
@@ -1256,17 +1209,12 @@ mod tests {
             snapshot.anchor_before(Point::new(1, 2))..snapshot.anchor_after(Point::new(1, 2))
         });
         let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
-        let fs = FakeFs::new(cx.executor());
-        let project = Project::test(fs, vec![], cx).await;
         let codegen = cx.new(|cx| {
             CodegenAlternative::new(
                 buffer.clone(),
                 range.clone(),
                 true,
                 None,
-                project.downgrade(),
-                None,
-                None,
                 prompt_builder,
                 cx,
             )
@@ -1325,17 +1273,12 @@ mod tests {
             snapshot.anchor_before(Point::new(0, 0))..snapshot.anchor_after(Point::new(4, 2))
         });
         let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
-        let fs = FakeFs::new(cx.executor());
-        let project = Project::test(fs, vec![], cx).await;
         let codegen = cx.new(|cx| {
             CodegenAlternative::new(
                 buffer.clone(),
                 range.clone(),
                 true,
                 None,
-                project.downgrade(),
-                None,
-                None,
                 prompt_builder,
                 cx,
             )
@@ -1382,17 +1325,12 @@ mod tests {
             snapshot.anchor_before(Point::new(1, 0))..snapshot.anchor_after(Point::new(1, 14))
         });
         let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
-        let fs = FakeFs::new(cx.executor());
-        let project = Project::test(fs, vec![], cx).await;
         let codegen = cx.new(|cx| {
             CodegenAlternative::new(
                 buffer.clone(),
                 range.clone(),
                 false,
                 None,
-                project.downgrade(),
-                None,
-                None,
                 prompt_builder,
                 cx,
             )

crates/agent_ui/src/acp/completion_provider.rs β†’ crates/agent_ui/src/completion_provider.rs πŸ”—

@@ -1,41 +1,130 @@
-use std::cell::RefCell;
+use std::cmp::Reverse;
 use std::ops::Range;
 use std::path::PathBuf;
-use std::rc::Rc;
 use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
 
 use acp_thread::MentionUri;
 use agent::{HistoryEntry, HistoryStore};
-use agent_client_protocol as acp;
 use anyhow::Result;
 use editor::{CompletionProvider, Editor, ExcerptId};
-use fuzzy::{StringMatch, StringMatchCandidate};
+use fuzzy::{PathMatch, StringMatch, StringMatchCandidate};
 use gpui::{App, Entity, Task, WeakEntity};
 use language::{Buffer, CodeLabel, CodeLabelBuilder, HighlightId};
 use lsp::CompletionContext;
+use ordered_float::OrderedFloat;
 use project::lsp_store::{CompletionDocumentation, SymbolLocation};
 use project::{
-    Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, Project,
-    ProjectPath, Symbol, WorktreeId,
+    Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse,
+    PathMatchCandidateSet, Project, ProjectPath, Symbol, WorktreeId,
 };
-use prompt_store::PromptStore;
+use prompt_store::{PromptId, PromptStore, UserPromptId};
 use rope::Point;
 use text::{Anchor, ToPoint as _};
 use ui::prelude::*;
+use util::ResultExt as _;
+use util::paths::PathStyle;
 use util::rel_path::RelPath;
 use workspace::Workspace;
 
 use crate::AgentPanel;
-use crate::acp::message_editor::MessageEditor;
-use crate::context_picker::file_context_picker::{FileMatch, search_files};
-use crate::context_picker::rules_context_picker::{RulesContextEntry, search_rules};
-use crate::context_picker::symbol_context_picker::SymbolMatch;
-use crate::context_picker::symbol_context_picker::search_symbols;
-use crate::context_picker::thread_context_picker::search_threads;
-use crate::context_picker::{
-    ContextPickerAction, ContextPickerEntry, ContextPickerMode, selection_ranges,
-};
+use crate::mention_set::MentionSet;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum PromptContextEntry {
+    Mode(PromptContextType),
+    Action(PromptContextAction),
+}
+
+impl PromptContextEntry {
+    pub fn keyword(&self) -> &'static str {
+        match self {
+            Self::Mode(mode) => mode.keyword(),
+            Self::Action(action) => action.keyword(),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum PromptContextType {
+    File,
+    Symbol,
+    Fetch,
+    Thread,
+    Rules,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum PromptContextAction {
+    AddSelections,
+}
+
+impl PromptContextAction {
+    pub fn keyword(&self) -> &'static str {
+        match self {
+            Self::AddSelections => "selection",
+        }
+    }
+
+    pub fn label(&self) -> &'static str {
+        match self {
+            Self::AddSelections => "Selection",
+        }
+    }
+
+    pub fn icon(&self) -> IconName {
+        match self {
+            Self::AddSelections => IconName::Reader,
+        }
+    }
+}
+
+impl TryFrom<&str> for PromptContextType {
+    type Error = String;
+
+    fn try_from(value: &str) -> Result<Self, Self::Error> {
+        match value {
+            "file" => Ok(Self::File),
+            "symbol" => Ok(Self::Symbol),
+            "fetch" => Ok(Self::Fetch),
+            "thread" => Ok(Self::Thread),
+            "rule" => Ok(Self::Rules),
+            _ => Err(format!("Invalid context picker mode: {}", value)),
+        }
+    }
+}
+
+impl PromptContextType {
+    pub fn keyword(&self) -> &'static str {
+        match self {
+            Self::File => "file",
+            Self::Symbol => "symbol",
+            Self::Fetch => "fetch",
+            Self::Thread => "thread",
+            Self::Rules => "rule",
+        }
+    }
+
+    pub fn label(&self) -> &'static str {
+        match self {
+            Self::File => "Files & Directories",
+            Self::Symbol => "Symbols",
+            Self::Fetch => "Fetch",
+            Self::Thread => "Threads",
+            Self::Rules => "Rules",
+        }
+    }
+
+    pub fn icon(&self) -> IconName {
+        match self {
+            Self::File => IconName::File,
+            Self::Symbol => IconName::Code,
+            Self::Fetch => IconName::ToolWeb,
+            Self::Thread => IconName::Thread,
+            Self::Rules => IconName::Reader,
+        }
+    }
+}
 
 pub(crate) enum Match {
     File(FileMatch),
@@ -47,11 +136,6 @@ pub(crate) enum Match {
     Entry(EntryMatch),
 }
 
-pub struct EntryMatch {
-    mat: Option<StringMatch>,
-    entry: ContextPickerEntry,
-}
-
 impl Match {
     pub fn score(&self) -> f64 {
         match self {
@@ -66,43 +150,69 @@ impl Match {
     }
 }
 
-pub struct ContextPickerCompletionProvider {
-    message_editor: WeakEntity<MessageEditor>,
-    workspace: WeakEntity<Workspace>,
+pub struct EntryMatch {
+    mat: Option<StringMatch>,
+    entry: PromptContextEntry,
+}
+
+#[derive(Debug, Clone)]
+pub struct RulesContextEntry {
+    pub prompt_id: UserPromptId,
+    pub title: SharedString,
+}
+
+#[derive(Debug, Clone)]
+pub struct AvailableCommand {
+    pub name: Arc<str>,
+    pub description: Arc<str>,
+    pub requires_argument: bool,
+}
+
+pub trait PromptCompletionProviderDelegate: Send + Sync + 'static {
+    fn supports_context(&self, mode: PromptContextType, cx: &App) -> bool {
+        self.supported_modes(cx).contains(&mode)
+    }
+    fn supported_modes(&self, cx: &App) -> Vec<PromptContextType>;
+    fn supports_images(&self, cx: &App) -> bool;
+
+    fn available_commands(&self, cx: &App) -> Vec<AvailableCommand>;
+    fn confirm_command(&self, cx: &mut App);
+}
+
+pub struct PromptCompletionProvider<T: PromptCompletionProviderDelegate> {
+    source: Arc<T>,
+    mention_set: Entity<MentionSet>,
     history_store: Entity<HistoryStore>,
     prompt_store: Option<Entity<PromptStore>>,
-    prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
-    available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
+    workspace: WeakEntity<Workspace>,
 }
 
-impl ContextPickerCompletionProvider {
+impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
     pub fn new(
-        message_editor: WeakEntity<MessageEditor>,
-        workspace: WeakEntity<Workspace>,
+        source: T,
+        mention_set: Entity<MentionSet>,
         history_store: Entity<HistoryStore>,
         prompt_store: Option<Entity<PromptStore>>,
-        prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
-        available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
+        workspace: WeakEntity<Workspace>,
     ) -> Self {
         Self {
-            message_editor,
+            source: Arc::new(source),
+            mention_set,
             workspace,
             history_store,
             prompt_store,
-            prompt_capabilities,
-            available_commands,
         }
     }
 
     fn completion_for_entry(
-        entry: ContextPickerEntry,
+        entry: PromptContextEntry,
         source_range: Range<Anchor>,
-        message_editor: WeakEntity<MessageEditor>,
+        mention_set: WeakEntity<MentionSet>,
         workspace: &Entity<Workspace>,
         cx: &mut App,
     ) -> Option<Completion> {
         match entry {
-            ContextPickerEntry::Mode(mode) => Some(Completion {
+            PromptContextEntry::Mode(mode) => Some(Completion {
                 replace_range: source_range,
                 new_text: format!("@{} ", mode.keyword()),
                 label: CodeLabel::plain(mode.label().to_string(), None),
@@ -117,8 +227,8 @@ impl ContextPickerCompletionProvider {
                 // inserted
                 confirm: Some(Arc::new(|_, _, _| true)),
             }),
-            ContextPickerEntry::Action(action) => {
-                Self::completion_for_action(action, source_range, message_editor, workspace, cx)
+            PromptContextEntry::Action(action) => {
+                Self::completion_for_action(action, source_range, mention_set, workspace, cx)
             }
         }
     }
@@ -127,7 +237,9 @@ impl ContextPickerCompletionProvider {
         thread_entry: HistoryEntry,
         source_range: Range<Anchor>,
         recent: bool,
-        editor: WeakEntity<MessageEditor>,
+        source: Arc<T>,
+        mention_set: WeakEntity<MentionSet>,
+        workspace: Entity<Workspace>,
         cx: &mut App,
     ) -> Completion {
         let uri = thread_entry.mention_uri();
@@ -155,8 +267,10 @@ impl ContextPickerCompletionProvider {
                 thread_entry.title().clone(),
                 source_range.start,
                 new_text_len - 1,
-                editor,
                 uri,
+                source,
+                mention_set,
+                workspace,
             )),
         }
     }
@@ -164,7 +278,9 @@ impl ContextPickerCompletionProvider {
     fn completion_for_rules(
         rule: RulesContextEntry,
         source_range: Range<Anchor>,
-        editor: WeakEntity<MessageEditor>,
+        source: Arc<T>,
+        mention_set: WeakEntity<MentionSet>,
+        workspace: Entity<Workspace>,
         cx: &mut App,
     ) -> Completion {
         let uri = MentionUri::Rule {
@@ -188,8 +304,10 @@ impl ContextPickerCompletionProvider {
                 rule.title,
                 source_range.start,
                 new_text_len - 1,
-                editor,
                 uri,
+                source,
+                mention_set,
+                workspace,
             )),
         }
     }
@@ -200,20 +318,18 @@ impl ContextPickerCompletionProvider {
         is_recent: bool,
         is_directory: bool,
         source_range: Range<Anchor>,
-        message_editor: WeakEntity<MessageEditor>,
+        source: Arc<T>,
+        mention_set: WeakEntity<MentionSet>,
+        workspace: Entity<Workspace>,
         project: Entity<Project>,
         cx: &mut App,
     ) -> Option<Completion> {
         let path_style = project.read(cx).path_style(cx);
         let (file_name, directory) =
-            crate::context_picker::file_context_picker::extract_file_name_and_directory(
-                &project_path.path,
-                path_prefix,
-                path_style,
-            );
+            extract_file_name_and_directory(&project_path.path, path_prefix, path_style);
 
         let label =
-            build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
+            build_code_label_for_path(&file_name, directory.as_ref().map(|s| s.as_ref()), None, cx);
 
         let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
 
@@ -246,8 +362,10 @@ impl ContextPickerCompletionProvider {
                 file_name,
                 source_range.start,
                 new_text_len - 1,
-                message_editor,
                 uri,
+                source,
+                mention_set,
+                workspace,
             )),
         })
     }
@@ -255,7 +373,8 @@ impl ContextPickerCompletionProvider {
     fn completion_for_symbol(
         symbol: Symbol,
         source_range: Range<Anchor>,
-        message_editor: WeakEntity<MessageEditor>,
+        source: Arc<T>,
+        mention_set: WeakEntity<MentionSet>,
         workspace: Entity<Workspace>,
         cx: &mut App,
     ) -> Option<Completion> {
@@ -275,7 +394,12 @@ impl ContextPickerCompletionProvider {
             ),
         };
 
-        let label = build_symbol_label(&symbol.name, &file_name, symbol.range.start.0.row + 1, cx);
+        let label = build_code_label_for_path(
+            &symbol.name,
+            Some(&file_name),
+            Some(symbol.range.start.0.row + 1),
+            cx,
+        );
 
         let uri = MentionUri::Symbol {
             abs_path,
@@ -299,8 +423,10 @@ impl ContextPickerCompletionProvider {
                 symbol.name.into(),
                 source_range.start,
                 new_text_len - 1,
-                message_editor,
                 uri,
+                source,
+                mention_set,
+                workspace,
             )),
         })
     }
@@ -308,7 +434,9 @@ impl ContextPickerCompletionProvider {
     fn completion_for_fetch(
         source_range: Range<Anchor>,
         url_to_fetch: SharedString,
-        message_editor: WeakEntity<MessageEditor>,
+        source: Arc<T>,
+        mention_set: WeakEntity<MentionSet>,
+        workspace: Entity<Workspace>,
         cx: &mut App,
     ) -> Option<Completion> {
         let new_text = format!("@fetch {} ", url_to_fetch);
@@ -333,21 +461,23 @@ impl ContextPickerCompletionProvider {
                 url_to_fetch.to_string().into(),
                 source_range.start,
                 new_text.len() - 1,
-                message_editor,
                 mention_uri,
+                source,
+                mention_set,
+                workspace,
             )),
         })
     }
 
     pub(crate) fn completion_for_action(
-        action: ContextPickerAction,
+        action: PromptContextAction,
         source_range: Range<Anchor>,
-        message_editor: WeakEntity<MessageEditor>,
+        mention_set: WeakEntity<MentionSet>,
         workspace: &Entity<Workspace>,
         cx: &mut App,
     ) -> Option<Completion> {
         let (new_text, on_action) = match action {
-            ContextPickerAction::AddSelections => {
+            PromptContextAction::AddSelections => {
                 const PLACEHOLDER: &str = "selection ";
                 let selections = selection_ranges(workspace, cx)
                     .into_iter()
@@ -367,12 +497,12 @@ impl ContextPickerCompletionProvider {
                     let source_range = source_range.clone();
                     move |_, window: &mut Window, cx: &mut App| {
                         let selections = selections.clone();
-                        let message_editor = message_editor.clone();
+                        let mention_set = mention_set.clone();
                         let source_range = source_range.clone();
                         window.defer(cx, move |window, cx| {
-                            message_editor
-                                .update(cx, |message_editor, cx| {
-                                    message_editor.confirm_mention_for_selection(
+                            mention_set
+                                .update(cx, |store, cx| {
+                                    store.confirm_mention_for_selection(
                                         source_range,
                                         selections,
                                         window,
@@ -406,12 +536,8 @@ impl ContextPickerCompletionProvider {
         })
     }
 
-    fn search_slash_commands(
-        &self,
-        query: String,
-        cx: &mut App,
-    ) -> Task<Vec<acp::AvailableCommand>> {
-        let commands = self.available_commands.borrow().clone();
+    fn search_slash_commands(&self, query: String, cx: &mut App) -> Task<Vec<AvailableCommand>> {
+        let commands = self.source.available_commands(cx);
         if commands.is_empty() {
             return Task::ready(Vec::new());
         }
@@ -443,7 +569,7 @@ impl ContextPickerCompletionProvider {
 
     fn search_mentions(
         &self,
-        mode: Option<ContextPickerMode>,
+        mode: Option<PromptContextType>,
         query: String,
         cancellation_flag: Arc<AtomicBool>,
         cx: &mut App,
@@ -452,7 +578,7 @@ impl ContextPickerCompletionProvider {
             return Task::ready(Vec::default());
         };
         match mode {
-            Some(ContextPickerMode::File) => {
+            Some(PromptContextType::File) => {
                 let search_files_task = search_files(query, cancellation_flag, &workspace, cx);
                 cx.background_spawn(async move {
                     search_files_task
@@ -463,7 +589,7 @@ impl ContextPickerCompletionProvider {
                 })
             }
 
-            Some(ContextPickerMode::Symbol) => {
+            Some(PromptContextType::Symbol) => {
                 let search_symbols_task = search_symbols(query, cancellation_flag, &workspace, cx);
                 cx.background_spawn(async move {
                     search_symbols_task
@@ -474,7 +600,7 @@ impl ContextPickerCompletionProvider {
                 })
             }
 
-            Some(ContextPickerMode::Thread) => {
+            Some(PromptContextType::Thread) => {
                 let search_threads_task =
                     search_threads(query, cancellation_flag, &self.history_store, cx);
                 cx.background_spawn(async move {
@@ -486,7 +612,7 @@ impl ContextPickerCompletionProvider {
                 })
             }
 
-            Some(ContextPickerMode::Fetch) => {
+            Some(PromptContextType::Fetch) => {
                 if !query.is_empty() {
                     Task::ready(vec![Match::Fetch(query.into())])
                 } else {
@@ -494,7 +620,7 @@ impl ContextPickerCompletionProvider {
                 }
             }
 
-            Some(ContextPickerMode::Rules) => {
+            Some(PromptContextType::Rules) => {
                 if let Some(prompt_store) = self.prompt_store.as_ref() {
                     let search_rules_task =
                         search_rules(query, cancellation_flag, prompt_store, cx);
@@ -584,9 +710,8 @@ impl ContextPickerCompletionProvider {
         let mut recent = Vec::with_capacity(6);
 
         let mut mentions = self
-            .message_editor
-            .read_with(cx, |message_editor, _cx| message_editor.mentions())
-            .unwrap_or_default();
+            .mention_set
+            .read_with(cx, |store, _cx| store.mentions());
         let workspace = workspace.read(cx);
         let project = workspace.project().read(cx);
         let include_root_name = workspace.visible_worktrees(cx).count() > 1;
@@ -637,7 +762,7 @@ impl ContextPickerCompletionProvider {
                 }),
         );
 
-        if self.prompt_capabilities.borrow().embedded_context {
+        if self.source.supports_context(PromptContextType::Thread, cx) {
             const RECENT_COUNT: usize = 2;
             let threads = self
                 .history_store
@@ -658,15 +783,14 @@ impl ContextPickerCompletionProvider {
         &self,
         workspace: &Entity<Workspace>,
         cx: &mut App,
-    ) -> Vec<ContextPickerEntry> {
-        let embedded_context = self.prompt_capabilities.borrow().embedded_context;
+    ) -> Vec<PromptContextEntry> {
         let mut entries = vec![
-            ContextPickerEntry::Mode(ContextPickerMode::File),
-            ContextPickerEntry::Mode(ContextPickerMode::Symbol),
+            PromptContextEntry::Mode(PromptContextType::File),
+            PromptContextEntry::Mode(PromptContextType::Symbol),
         ];
 
-        if embedded_context {
-            entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
+        if self.source.supports_context(PromptContextType::Thread, cx) {
+            entries.push(PromptContextEntry::Mode(PromptContextType::Thread));
         }
 
         let has_selection = workspace
@@ -679,53 +803,25 @@ impl ContextPickerCompletionProvider {
                 })
             });
         if has_selection {
-            entries.push(ContextPickerEntry::Action(
-                ContextPickerAction::AddSelections,
+            entries.push(PromptContextEntry::Action(
+                PromptContextAction::AddSelections,
             ));
         }
 
-        if embedded_context {
-            if self.prompt_store.is_some() {
-                entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
-            }
+        if self.prompt_store.is_some() && self.source.supports_context(PromptContextType::Rules, cx)
+        {
+            entries.push(PromptContextEntry::Mode(PromptContextType::Rules));
+        }
 
-            entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
+        if self.source.supports_context(PromptContextType::Fetch, cx) {
+            entries.push(PromptContextEntry::Mode(PromptContextType::Fetch));
         }
 
         entries
     }
 }
 
-fn build_symbol_label(symbol_name: &str, file_name: &str, line: u32, cx: &App) -> CodeLabel {
-    let comment_id = cx.theme().syntax().highlight_id("comment").map(HighlightId);
-    let mut label = CodeLabelBuilder::default();
-
-    label.push_str(symbol_name, None);
-    label.push_str(" ", None);
-    label.push_str(&format!("{} L{}", file_name, line), comment_id);
-
-    label.build()
-}
-
-fn build_code_label_for_full_path(file_name: &str, directory: Option<&str>, cx: &App) -> CodeLabel {
-    let path = cx
-        .theme()
-        .syntax()
-        .highlight_id("variable")
-        .map(HighlightId);
-    let mut label = CodeLabelBuilder::default();
-
-    label.push_str(file_name, None);
-    label.push_str(" ", None);
-
-    if let Some(directory) = directory {
-        label.push_str(directory, path);
-    }
-
-    label.build()
-}
-
-impl CompletionProvider for ContextPickerCompletionProvider {
+impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletionProvider<T> {
     fn completions(
         &self,
         _excerpt_id: ExcerptId,
@@ -735,17 +831,13 @@ impl CompletionProvider for ContextPickerCompletionProvider {
         _window: &mut Window,
         cx: &mut Context<Editor>,
     ) -> Task<Result<Vec<CompletionResponse>>> {
-        let state = buffer.update(cx, |buffer, _cx| {
+        let state = buffer.update(cx, |buffer, cx| {
             let position = buffer_position.to_point(buffer);
             let line_start = Point::new(position.row, 0);
             let offset_to_line = buffer.point_to_offset(line_start);
             let mut lines = buffer.text_for_range(line_start..position).lines();
             let line = lines.next()?;
-            ContextCompletion::try_parse(
-                line,
-                offset_to_line,
-                self.prompt_capabilities.borrow().embedded_context,
-            )
+            ContextCompletion::try_parse(line, offset_to_line, &self.source.supported_modes(cx))
         });
         let Some(state) = state else {
             return Task::ready(Ok(Vec::new()));
@@ -760,8 +852,8 @@ impl CompletionProvider for ContextPickerCompletionProvider {
         let source_range = snapshot.anchor_before(state.source_range().start)
             ..snapshot.anchor_after(state.source_range().end);
 
-        let editor = self.message_editor.clone();
-
+        let source = self.source.clone();
+        let mention_set = self.mention_set.downgrade();
         match state {
             ContextCompletion::SlashCommand(SlashCommandCompletion {
                 command, argument, ..
@@ -778,7 +870,8 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                                 format!("/{} ", command.name)
                             };
 
-                            let is_missing_argument = argument.is_none() && command.input.is_some();
+                            let is_missing_argument =
+                                command.requires_argument && argument.is_none();
                             Completion {
                                 replace_range: source_range.clone(),
                                 new_text,
@@ -792,26 +885,18 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                                 snippet_deduplication_key: None,
                                 insert_text_mode: None,
                                 confirm: Some(Arc::new({
-                                    let editor = editor.clone();
+                                    let source = source.clone();
                                     move |intent, _window, cx| {
                                         if !is_missing_argument {
                                             cx.defer({
-                                                let editor = editor.clone();
-                                                move |cx| {
-                                                    editor
-                                                        .update(cx, |editor, cx| {
-                                                            match intent {
-                                                                CompletionIntent::Complete
-                                                                | CompletionIntent::CompleteWithInsert
-                                                                | CompletionIntent::CompleteWithReplace => {
-                                                                    if !is_missing_argument {
-                                                                        editor.send(cx);
-                                                                    }
-                                                                }
-                                                                CompletionIntent::Compose => {}
-                                                            }
-                                                        })
-                                                        .ok();
+                                                let source = source.clone();
+                                                move |cx| match intent {
+                                                    CompletionIntent::Complete
+                                                    | CompletionIntent::CompleteWithInsert
+                                                    | CompletionIntent::CompleteWithReplace => {
+                                                        source.confirm_command(cx);
+                                                    }
+                                                    CompletionIntent::Compose => {}
                                                 }
                                             });
                                         }
@@ -869,7 +954,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                                         is_recent,
                                         mat.is_dir,
                                         source_range.clone(),
-                                        editor.clone(),
+                                        source.clone(),
+                                        mention_set.clone(),
+                                        workspace.clone(),
                                         project.clone(),
                                         cx,
                                     )
@@ -879,7 +966,8 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                                     Self::completion_for_symbol(
                                         symbol,
                                         source_range.clone(),
-                                        editor.clone(),
+                                        source.clone(),
+                                        mention_set.clone(),
                                         workspace.clone(),
                                         cx,
                                     )
@@ -889,7 +977,9 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                                     thread,
                                     source_range.clone(),
                                     false,
-                                    editor.clone(),
+                                    source.clone(),
+                                    mention_set.clone(),
+                                    workspace.clone(),
                                     cx,
                                 )),
 
@@ -897,21 +987,27 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                                     thread,
                                     source_range.clone(),
                                     true,
-                                    editor.clone(),
+                                    source.clone(),
+                                    mention_set.clone(),
+                                    workspace.clone(),
                                     cx,
                                 )),
 
                                 Match::Rules(user_rules) => Some(Self::completion_for_rules(
                                     user_rules,
                                     source_range.clone(),
-                                    editor.clone(),
+                                    source.clone(),
+                                    mention_set.clone(),
+                                    workspace.clone(),
                                     cx,
                                 )),
 
                                 Match::Fetch(url) => Self::completion_for_fetch(
                                     source_range.clone(),
                                     url,
-                                    editor.clone(),
+                                    source.clone(),
+                                    mention_set.clone(),
+                                    workspace.clone(),
                                     cx,
                                 ),
 
@@ -919,7 +1015,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                                     Self::completion_for_entry(
                                         entry,
                                         source_range.clone(),
-                                        editor.clone(),
+                                        mention_set.clone(),
                                         &workspace,
                                         cx,
                                     )
@@ -957,27 +1053,24 @@ impl CompletionProvider for ContextPickerCompletionProvider {
         let offset_to_line = buffer.point_to_offset(line_start);
         let mut lines = buffer.text_for_range(line_start..position).lines();
         if let Some(line) = lines.next() {
-            ContextCompletion::try_parse(
-                line,
-                offset_to_line,
-                self.prompt_capabilities.borrow().embedded_context,
-            )
-            .filter(|completion| {
-                // Right now we don't support completing arguments of slash commands
-                let is_slash_command_with_argument = matches!(
-                    completion,
-                    ContextCompletion::SlashCommand(SlashCommandCompletion {
-                        argument: Some(_),
-                        ..
-                    })
-                );
-                !is_slash_command_with_argument
-            })
-            .map(|completion| {
-                completion.source_range().start <= offset_to_line + position.column as usize
-                    && completion.source_range().end >= offset_to_line + position.column as usize
-            })
-            .unwrap_or(false)
+            ContextCompletion::try_parse(line, offset_to_line, &self.source.supported_modes(cx))
+                .filter(|completion| {
+                    // Right now we don't support completing arguments of slash commands
+                    let is_slash_command_with_argument = matches!(
+                        completion,
+                        ContextCompletion::SlashCommand(SlashCommandCompletion {
+                            argument: Some(_),
+                            ..
+                        })
+                    );
+                    !is_slash_command_with_argument
+                })
+                .map(|completion| {
+                    completion.source_range().start <= offset_to_line + position.column as usize
+                        && completion.source_range().end
+                            >= offset_to_line + position.column as usize
+                })
+                .unwrap_or(false)
         } else {
             false
         }
@@ -992,27 +1085,33 @@ impl CompletionProvider for ContextPickerCompletionProvider {
     }
 }
 
-fn confirm_completion_callback(
+fn confirm_completion_callback<T: PromptCompletionProviderDelegate>(
     crease_text: SharedString,
     start: Anchor,
     content_len: usize,
-    message_editor: WeakEntity<MessageEditor>,
     mention_uri: MentionUri,
+    source: Arc<T>,
+    mention_set: WeakEntity<MentionSet>,
+    workspace: Entity<Workspace>,
 ) -> Arc<dyn Fn(CompletionIntent, &mut Window, &mut App) -> bool + Send + Sync> {
     Arc::new(move |_, window, cx| {
-        let message_editor = message_editor.clone();
+        let source = source.clone();
+        let mention_set = mention_set.clone();
         let crease_text = crease_text.clone();
         let mention_uri = mention_uri.clone();
+        let workspace = workspace.clone();
         window.defer(cx, move |window, cx| {
-            message_editor
+            mention_set
                 .clone()
-                .update(cx, |message_editor, cx| {
-                    message_editor
+                .update(cx, |mention_set, cx| {
+                    mention_set
                         .confirm_mention_completion(
                             crease_text,
                             start,
                             content_len,
                             mention_uri,
+                            source.supports_images(cx),
+                            &workspace,
                             window,
                             cx,
                         )
@@ -1037,11 +1136,15 @@ impl ContextCompletion {
         }
     }
 
-    fn try_parse(line: &str, offset_to_line: usize, allow_non_file_mentions: bool) -> Option<Self> {
+    fn try_parse(
+        line: &str,
+        offset_to_line: usize,
+        supported_modes: &[PromptContextType],
+    ) -> Option<Self> {
         if let Some(command) = SlashCommandCompletion::try_parse(line, offset_to_line) {
             Some(Self::SlashCommand(command))
         } else if let Some(mention) =
-            MentionCompletion::try_parse(allow_non_file_mentions, line, offset_to_line)
+            MentionCompletion::try_parse(line, offset_to_line, supported_modes)
         {
             Some(Self::Mention(mention))
         } else {
@@ -1098,12 +1201,16 @@ impl SlashCommandCompletion {
 #[derive(Debug, Default, PartialEq)]
 struct MentionCompletion {
     source_range: Range<usize>,
-    mode: Option<ContextPickerMode>,
+    mode: Option<PromptContextType>,
     argument: Option<String>,
 }
 
 impl MentionCompletion {
-    fn try_parse(allow_non_file_mentions: bool, line: &str, offset_to_line: usize) -> Option<Self> {
+    fn try_parse(
+        line: &str,
+        offset_to_line: usize,
+        supported_modes: &[PromptContextType],
+    ) -> Option<Self> {
         let last_mention_start = line.rfind('@')?;
 
         // No whitespace immediately after '@'
@@ -1137,8 +1244,8 @@ impl MentionCompletion {
             // Safe since we check no leading whitespace above
             end += mode_text.len();
 
-            if let Some(parsed_mode) = ContextPickerMode::try_from(mode_text).ok()
-                && (allow_non_file_mentions || matches!(parsed_mode, ContextPickerMode::File))
+            if let Some(parsed_mode) = PromptContextType::try_from(mode_text).ok()
+                && supported_modes.contains(&parsed_mode)
             {
                 mode = Some(parsed_mode);
             } else {
@@ -1172,6 +1279,339 @@ impl MentionCompletion {
     }
 }
 
+pub(crate) fn search_files(
+    query: String,
+    cancellation_flag: Arc<AtomicBool>,
+    workspace: &Entity<Workspace>,
+    cx: &App,
+) -> Task<Vec<FileMatch>> {
+    if query.is_empty() {
+        let workspace = workspace.read(cx);
+        let project = workspace.project().read(cx);
+        let visible_worktrees = workspace.visible_worktrees(cx).collect::<Vec<_>>();
+        let include_root_name = visible_worktrees.len() > 1;
+
+        let recent_matches = workspace
+            .recent_navigation_history(Some(10), cx)
+            .into_iter()
+            .map(|(project_path, _)| {
+                let path_prefix = if include_root_name {
+                    project
+                        .worktree_for_id(project_path.worktree_id, cx)
+                        .map(|wt| wt.read(cx).root_name().into())
+                        .unwrap_or_else(|| RelPath::empty().into())
+                } else {
+                    RelPath::empty().into()
+                };
+
+                FileMatch {
+                    mat: PathMatch {
+                        score: 0.,
+                        positions: Vec::new(),
+                        worktree_id: project_path.worktree_id.to_usize(),
+                        path: project_path.path,
+                        path_prefix,
+                        distance_to_relative_ancestor: 0,
+                        is_dir: false,
+                    },
+                    is_recent: true,
+                }
+            });
+
+        let file_matches = visible_worktrees.into_iter().flat_map(|worktree| {
+            let worktree = worktree.read(cx);
+            let path_prefix: Arc<RelPath> = if include_root_name {
+                worktree.root_name().into()
+            } else {
+                RelPath::empty().into()
+            };
+            worktree.entries(false, 0).map(move |entry| FileMatch {
+                mat: PathMatch {
+                    score: 0.,
+                    positions: Vec::new(),
+                    worktree_id: worktree.id().to_usize(),
+                    path: entry.path.clone(),
+                    path_prefix: path_prefix.clone(),
+                    distance_to_relative_ancestor: 0,
+                    is_dir: entry.is_dir(),
+                },
+                is_recent: false,
+            })
+        });
+
+        Task::ready(recent_matches.chain(file_matches).collect())
+    } else {
+        let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
+        let include_root_name = worktrees.len() > 1;
+        let candidate_sets = worktrees
+            .into_iter()
+            .map(|worktree| {
+                let worktree = worktree.read(cx);
+
+                PathMatchCandidateSet {
+                    snapshot: worktree.snapshot(),
+                    include_ignored: worktree.root_entry().is_some_and(|entry| entry.is_ignored),
+                    include_root_name,
+                    candidates: project::Candidates::Entries,
+                }
+            })
+            .collect::<Vec<_>>();
+
+        let executor = cx.background_executor().clone();
+        cx.foreground_executor().spawn(async move {
+            fuzzy::match_path_sets(
+                candidate_sets.as_slice(),
+                query.as_str(),
+                &None,
+                false,
+                100,
+                &cancellation_flag,
+                executor,
+            )
+            .await
+            .into_iter()
+            .map(|mat| FileMatch {
+                mat,
+                is_recent: false,
+            })
+            .collect::<Vec<_>>()
+        })
+    }
+}
+
+pub(crate) fn search_symbols(
+    query: String,
+    cancellation_flag: Arc<AtomicBool>,
+    workspace: &Entity<Workspace>,
+    cx: &mut App,
+) -> Task<Vec<SymbolMatch>> {
+    let symbols_task = workspace.update(cx, |workspace, cx| {
+        workspace
+            .project()
+            .update(cx, |project, cx| project.symbols(&query, cx))
+    });
+    let project = workspace.read(cx).project().clone();
+    cx.spawn(async move |cx| {
+        let Some(symbols) = symbols_task.await.log_err() else {
+            return Vec::new();
+        };
+        let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> =
+            project
+                .update(cx, |project, cx| {
+                    symbols
+                        .iter()
+                        .enumerate()
+                        .map(|(id, symbol)| {
+                            StringMatchCandidate::new(id, symbol.label.filter_text())
+                        })
+                        .partition(|candidate| match &symbols[candidate.id].path {
+                            SymbolLocation::InProject(project_path) => project
+                                .entry_for_path(project_path, cx)
+                                .is_some_and(|e| !e.is_ignored),
+                            SymbolLocation::OutsideProject { .. } => false,
+                        })
+                })
+                .log_err()
+        else {
+            return Vec::new();
+        };
+
+        const MAX_MATCHES: usize = 100;
+        let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
+            &visible_match_candidates,
+            &query,
+            false,
+            true,
+            MAX_MATCHES,
+            &cancellation_flag,
+            cx.background_executor().clone(),
+        ));
+        let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
+            &external_match_candidates,
+            &query,
+            false,
+            true,
+            MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
+            &cancellation_flag,
+            cx.background_executor().clone(),
+        ));
+        let sort_key_for_match = |mat: &StringMatch| {
+            let symbol = &symbols[mat.candidate_id];
+            (Reverse(OrderedFloat(mat.score)), symbol.label.filter_text())
+        };
+
+        visible_matches.sort_unstable_by_key(sort_key_for_match);
+        external_matches.sort_unstable_by_key(sort_key_for_match);
+        let mut matches = visible_matches;
+        matches.append(&mut external_matches);
+
+        matches
+            .into_iter()
+            .map(|mut mat| {
+                let symbol = symbols[mat.candidate_id].clone();
+                let filter_start = symbol.label.filter_range.start;
+                for position in &mut mat.positions {
+                    *position += filter_start;
+                }
+                SymbolMatch { symbol }
+            })
+            .collect()
+    })
+}
+
+pub(crate) fn search_threads(
+    query: String,
+    cancellation_flag: Arc<AtomicBool>,
+    thread_store: &Entity<HistoryStore>,
+    cx: &mut App,
+) -> Task<Vec<HistoryEntry>> {
+    let threads = thread_store.read(cx).entries().collect();
+    if query.is_empty() {
+        return Task::ready(threads);
+    }
+
+    let executor = cx.background_executor().clone();
+    cx.background_spawn(async move {
+        let candidates = threads
+            .iter()
+            .enumerate()
+            .map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
+            .collect::<Vec<_>>();
+        let matches = fuzzy::match_strings(
+            &candidates,
+            &query,
+            false,
+            true,
+            100,
+            &cancellation_flag,
+            executor,
+        )
+        .await;
+
+        matches
+            .into_iter()
+            .map(|mat| threads[mat.candidate_id].clone())
+            .collect()
+    })
+}
+
+pub(crate) fn search_rules(
+    query: String,
+    cancellation_flag: Arc<AtomicBool>,
+    prompt_store: &Entity<PromptStore>,
+    cx: &mut App,
+) -> Task<Vec<RulesContextEntry>> {
+    let search_task = prompt_store.read(cx).search(query, cancellation_flag, cx);
+    cx.background_spawn(async move {
+        search_task
+            .await
+            .into_iter()
+            .flat_map(|metadata| {
+                // Default prompts are filtered out as they are automatically included.
+                if metadata.default {
+                    None
+                } else {
+                    match metadata.id {
+                        PromptId::EditWorkflow => None,
+                        PromptId::User { uuid } => Some(RulesContextEntry {
+                            prompt_id: uuid,
+                            title: metadata.title?,
+                        }),
+                    }
+                }
+            })
+            .collect::<Vec<_>>()
+    })
+}
+
+pub struct SymbolMatch {
+    pub symbol: Symbol,
+}
+
+pub struct FileMatch {
+    pub mat: PathMatch,
+    pub is_recent: bool,
+}
+
+pub fn extract_file_name_and_directory(
+    path: &RelPath,
+    path_prefix: &RelPath,
+    path_style: PathStyle,
+) -> (SharedString, Option<SharedString>) {
+    // If path is empty, this means we're matching with the root directory itself
+    // so we use the path_prefix as the name
+    if path.is_empty() && !path_prefix.is_empty() {
+        return (path_prefix.display(path_style).to_string().into(), None);
+    }
+
+    let full_path = path_prefix.join(path);
+    let file_name = full_path.file_name().unwrap_or_default();
+    let display_path = full_path.display(path_style);
+    let (directory, file_name) = display_path.split_at(display_path.len() - file_name.len());
+    (
+        file_name.to_string().into(),
+        Some(SharedString::new(directory)).filter(|dir| !dir.is_empty()),
+    )
+}
+
+fn build_code_label_for_path(
+    file: &str,
+    directory: Option<&str>,
+    line_number: Option<u32>,
+    cx: &App,
+) -> CodeLabel {
+    let variable_highlight_id = cx
+        .theme()
+        .syntax()
+        .highlight_id("variable")
+        .map(HighlightId);
+    let mut label = CodeLabelBuilder::default();
+
+    label.push_str(file, None);
+    label.push_str(" ", None);
+
+    if let Some(directory) = directory {
+        label.push_str(directory, variable_highlight_id);
+    }
+    if let Some(line_number) = line_number {
+        label.push_str(&format!(" L{}", line_number), variable_highlight_id);
+    }
+    label.build()
+}
+
+fn selection_ranges(
+    workspace: &Entity<Workspace>,
+    cx: &mut App,
+) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
+    let Some(editor) = workspace
+        .read(cx)
+        .active_item(cx)
+        .and_then(|item| item.act_as::<Editor>(cx))
+    else {
+        return Vec::new();
+    };
+
+    editor.update(cx, |editor, cx| {
+        let selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
+
+        let buffer = editor.buffer().clone().read(cx);
+        let snapshot = buffer.snapshot(cx);
+
+        selections
+            .into_iter()
+            .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
+            .flat_map(|range| {
+                let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
+                let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
+                if start_buffer != end_buffer {
+                    return None;
+                }
+                Some((start_buffer, start..end))
+            })
+            .collect::<Vec<_>>()
+    })
+}
+
 #[cfg(test)]
 mod tests {
     use super::*;

crates/agent_ui/src/context.rs πŸ”—

@@ -1,764 +1,10 @@
-use agent::outline;
-use assistant_text_thread::TextThread;
-use futures::future;
-use futures::{FutureExt, future::Shared};
-use gpui::{App, AppContext as _, ElementId, Entity, SharedString, Task};
-use language::Buffer;
+use crate::mention_set::Mention;
+use gpui::{AppContext as _, Entity, Task};
 use language_model::{LanguageModelImage, LanguageModelRequestMessage, MessageContent};
-use project::{Project, ProjectEntryId, ProjectPath, Worktree};
-use prompt_store::{PromptStore, UserPromptId};
-use ref_cast::RefCast;
-use rope::Point;
-use std::fmt::{self, Display, Formatter, Write as _};
-use std::hash::{Hash, Hasher};
-use std::path::PathBuf;
-use std::{ops::Range, path::Path, sync::Arc};
-use text::{Anchor, OffsetRangeExt as _};
-use ui::IconName;
-use util::markdown::MarkdownCodeBlock;
-use util::rel_path::RelPath;
-use util::{ResultExt as _, post_inc};
+use ui::App;
+use util::ResultExt as _;
 
-pub const RULES_ICON: IconName = IconName::Reader;
-
-pub enum ContextKind {
-    File,
-    Directory,
-    Symbol,
-    Selection,
-    FetchedUrl,
-    Thread,
-    TextThread,
-    Rules,
-    Image,
-}
-
-impl ContextKind {
-    pub fn icon(&self) -> IconName {
-        match self {
-            ContextKind::File => IconName::File,
-            ContextKind::Directory => IconName::Folder,
-            ContextKind::Symbol => IconName::Code,
-            ContextKind::Selection => IconName::Reader,
-            ContextKind::FetchedUrl => IconName::ToolWeb,
-            ContextKind::Thread => IconName::Thread,
-            ContextKind::TextThread => IconName::TextThread,
-            ContextKind::Rules => RULES_ICON,
-            ContextKind::Image => IconName::Image,
-        }
-    }
-}
-
-/// Handle for context that can be attached to a user message.
-///
-/// This uses IDs that are stable enough for tracking renames and identifying when context has
-/// already been added to the thread. To use this in a set, wrap it in `AgentContextKey` to opt in
-/// to `PartialEq` and `Hash` impls that use the subset of the fields used for this stable identity.
-#[derive(Debug, Clone)]
-pub enum AgentContextHandle {
-    File(FileContextHandle),
-    Directory(DirectoryContextHandle),
-    Symbol(SymbolContextHandle),
-    Selection(SelectionContextHandle),
-    FetchedUrl(FetchedUrlContext),
-    Thread(ThreadContextHandle),
-    TextThread(TextThreadContextHandle),
-    Rules(RulesContextHandle),
-    Image(ImageContext),
-}
-
-impl AgentContextHandle {
-    pub fn id(&self) -> ContextId {
-        match self {
-            Self::File(context) => context.context_id,
-            Self::Directory(context) => context.context_id,
-            Self::Symbol(context) => context.context_id,
-            Self::Selection(context) => context.context_id,
-            Self::FetchedUrl(context) => context.context_id,
-            Self::Thread(context) => context.context_id,
-            Self::TextThread(context) => context.context_id,
-            Self::Rules(context) => context.context_id,
-            Self::Image(context) => context.context_id,
-        }
-    }
-
-    pub fn element_id(&self, name: SharedString) -> ElementId {
-        ElementId::NamedInteger(name, self.id().0)
-    }
-}
-
-/// Loaded context that can be attached to a user message. This can be thought of as a
-/// snapshot of the context along with an `AgentContextHandle`.
-#[derive(Debug, Clone)]
-pub enum AgentContext {
-    File(FileContext),
-    Directory(DirectoryContext),
-    Symbol(SymbolContext),
-    Selection(SelectionContext),
-    FetchedUrl(FetchedUrlContext),
-    Thread(ThreadContext),
-    TextThread(TextThreadContext),
-    Rules(RulesContext),
-    Image(ImageContext),
-}
-
-impl AgentContext {
-    pub fn handle(&self) -> AgentContextHandle {
-        match self {
-            AgentContext::File(context) => AgentContextHandle::File(context.handle.clone()),
-            AgentContext::Directory(context) => {
-                AgentContextHandle::Directory(context.handle.clone())
-            }
-            AgentContext::Symbol(context) => AgentContextHandle::Symbol(context.handle.clone()),
-            AgentContext::Selection(context) => {
-                AgentContextHandle::Selection(context.handle.clone())
-            }
-            AgentContext::FetchedUrl(context) => AgentContextHandle::FetchedUrl(context.clone()),
-            AgentContext::Thread(context) => AgentContextHandle::Thread(context.handle.clone()),
-            AgentContext::TextThread(context) => {
-                AgentContextHandle::TextThread(context.handle.clone())
-            }
-            AgentContext::Rules(context) => AgentContextHandle::Rules(context.handle.clone()),
-            AgentContext::Image(context) => AgentContextHandle::Image(context.clone()),
-        }
-    }
-}
-
-/// ID created at time of context add, for use in ElementId. This is not the stable identity of a
-/// context, instead that's handled by the `PartialEq` and `Hash` impls of `AgentContextKey`.
-#[derive(Debug, Copy, Clone)]
-pub struct ContextId(u64);
-
-impl ContextId {
-    pub fn zero() -> Self {
-        ContextId(0)
-    }
-
-    fn for_lookup() -> Self {
-        ContextId(u64::MAX)
-    }
-
-    pub fn post_inc(&mut self) -> Self {
-        Self(post_inc(&mut self.0))
-    }
-}
-
-/// File context provides the entire contents of a file.
-///
-/// This holds an `Entity<Buffer>` so that file path renames affect its display and so that it can
-/// be opened even if the file has been deleted. An alternative might be to use `ProjectEntryId`,
-/// but then when deleted there is no path info or ability to open.
-#[derive(Debug, Clone)]
-pub struct FileContextHandle {
-    pub buffer: Entity<Buffer>,
-    pub context_id: ContextId,
-}
-
-#[derive(Debug, Clone)]
-pub struct FileContext {
-    pub handle: FileContextHandle,
-    pub full_path: String,
-    pub text: SharedString,
-    pub is_outline: bool,
-}
-
-impl FileContextHandle {
-    pub fn eq_for_key(&self, other: &Self) -> bool {
-        self.buffer == other.buffer
-    }
-
-    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
-        self.buffer.hash(state)
-    }
-
-    pub fn project_path(&self, cx: &App) -> Option<ProjectPath> {
-        let file = self.buffer.read(cx).file()?;
-        Some(ProjectPath {
-            worktree_id: file.worktree_id(cx),
-            path: file.path().clone(),
-        })
-    }
-
-    fn load(self, cx: &App) -> Task<Option<AgentContext>> {
-        let buffer_ref = self.buffer.read(cx);
-        let Some(file) = buffer_ref.file() else {
-            log::error!("file context missing path");
-            return Task::ready(None);
-        };
-        let full_path = file.full_path(cx).to_string_lossy().into_owned();
-        let rope = buffer_ref.as_rope().clone();
-        let buffer = self.buffer.clone();
-
-        cx.spawn(async move |cx| {
-            let buffer_content =
-                outline::get_buffer_content_or_outline(buffer.clone(), Some(&full_path), &cx)
-                    .await
-                    .unwrap_or_else(|_| outline::BufferContent {
-                        text: rope.to_string(),
-                        is_outline: false,
-                    });
-
-            let context = AgentContext::File(FileContext {
-                handle: self,
-                full_path,
-                text: buffer_content.text.into(),
-                is_outline: buffer_content.is_outline,
-            });
-            Some(context)
-        })
-    }
-}
-
-impl Display for FileContext {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(
-            f,
-            "{}",
-            MarkdownCodeBlock {
-                tag: &codeblock_tag(&self.full_path, None),
-                text: &self.text,
-            }
-        )
-    }
-}
-
-/// Directory contents provides the entire contents of text files in a directory.
-///
-/// This has a `ProjectEntryId` so that it follows renames.
-#[derive(Debug, Clone)]
-pub struct DirectoryContextHandle {
-    pub entry_id: ProjectEntryId,
-    pub context_id: ContextId,
-}
-
-#[derive(Debug, Clone)]
-pub struct DirectoryContext {
-    pub handle: DirectoryContextHandle,
-    pub full_path: String,
-    pub descendants: Vec<DirectoryContextDescendant>,
-}
-
-#[derive(Debug, Clone)]
-pub struct DirectoryContextDescendant {
-    /// Path within the directory.
-    pub rel_path: Arc<RelPath>,
-    pub fenced_codeblock: SharedString,
-}
-
-impl DirectoryContextHandle {
-    pub fn eq_for_key(&self, other: &Self) -> bool {
-        self.entry_id == other.entry_id
-    }
-
-    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
-        self.entry_id.hash(state)
-    }
-
-    fn load(self, project: Entity<Project>, cx: &mut App) -> Task<Option<AgentContext>> {
-        let Some(worktree) = project.read(cx).worktree_for_entry(self.entry_id, cx) else {
-            return Task::ready(None);
-        };
-        let worktree_ref = worktree.read(cx);
-        let Some(entry) = worktree_ref.entry_for_id(self.entry_id) else {
-            return Task::ready(None);
-        };
-        if entry.is_file() {
-            log::error!("DirectoryContext unexpectedly refers to a file.");
-            return Task::ready(None);
-        }
-
-        let directory_path = entry.path.clone();
-        let directory_full_path = worktree_ref
-            .full_path(&directory_path)
-            .to_string_lossy()
-            .to_string();
-
-        let file_paths = collect_files_in_path(worktree_ref, &directory_path);
-        let descendants_future = future::join_all(file_paths.into_iter().map(|path| {
-            let worktree_ref = worktree.read(cx);
-            let worktree_id = worktree_ref.id();
-            let full_path = worktree_ref.full_path(&path).to_string_lossy().into_owned();
-
-            let rel_path = path
-                .strip_prefix(&directory_path)
-                .log_err()
-                .map_or_else(|| path.clone(), |rel_path| rel_path.into());
-
-            let open_task = project.update(cx, |project, cx| {
-                project.buffer_store().update(cx, |buffer_store, cx| {
-                    let project_path = ProjectPath { worktree_id, path };
-                    buffer_store.open_buffer(project_path, cx)
-                })
-            });
-
-            // TODO: report load errors instead of just logging
-            let rope_task = cx.spawn(async move |cx| {
-                let buffer = open_task.await.log_err()?;
-                let rope = buffer
-                    .read_with(cx, |buffer, _cx| buffer.as_rope().clone())
-                    .log_err()?;
-                Some((rope, buffer))
-            });
-
-            cx.background_spawn(async move {
-                let (rope, _buffer) = rope_task.await?;
-                let fenced_codeblock = MarkdownCodeBlock {
-                    tag: &codeblock_tag(&full_path, None),
-                    text: &rope.to_string(),
-                }
-                .to_string()
-                .into();
-                let descendant = DirectoryContextDescendant {
-                    rel_path,
-                    fenced_codeblock,
-                };
-                Some(descendant)
-            })
-        }));
-
-        cx.background_spawn(async move {
-            let descendants = descendants_future
-                .await
-                .into_iter()
-                .flatten()
-                .collect::<Vec<_>>();
-            let context = AgentContext::Directory(DirectoryContext {
-                handle: self,
-                full_path: directory_full_path,
-                descendants,
-            });
-            Some(context)
-        })
-    }
-}
-
-impl Display for DirectoryContext {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let mut is_first = true;
-        for descendant in &self.descendants {
-            if !is_first {
-                writeln!(f)?;
-            } else {
-                is_first = false;
-            }
-            write!(f, "{}", descendant.fenced_codeblock)?;
-        }
-        Ok(())
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct SymbolContextHandle {
-    pub buffer: Entity<Buffer>,
-    pub symbol: SharedString,
-    pub range: Range<Anchor>,
-    /// The range that fully contains the symbol. e.g. for function symbol, this will include not
-    /// only the signature, but also the body. Not used by `PartialEq` or `Hash` for
-    /// `AgentContextKey`.
-    pub enclosing_range: Range<Anchor>,
-    pub context_id: ContextId,
-}
-
-#[derive(Debug, Clone)]
-pub struct SymbolContext {
-    pub handle: SymbolContextHandle,
-    pub full_path: String,
-    pub line_range: Range<Point>,
-    pub text: SharedString,
-}
-
-impl SymbolContextHandle {
-    pub fn eq_for_key(&self, other: &Self) -> bool {
-        self.buffer == other.buffer && self.symbol == other.symbol && self.range == other.range
-    }
-
-    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
-        self.buffer.hash(state);
-        self.symbol.hash(state);
-        self.range.hash(state);
-    }
-
-    pub fn full_path(&self, cx: &App) -> Option<PathBuf> {
-        Some(self.buffer.read(cx).file()?.full_path(cx))
-    }
-
-    pub fn enclosing_line_range(&self, cx: &App) -> Range<Point> {
-        self.enclosing_range
-            .to_point(&self.buffer.read(cx).snapshot())
-    }
-
-    pub fn text(&self, cx: &App) -> SharedString {
-        self.buffer
-            .read(cx)
-            .text_for_range(self.enclosing_range.clone())
-            .collect::<String>()
-            .into()
-    }
-
-    fn load(self, cx: &App) -> Task<Option<AgentContext>> {
-        let buffer_ref = self.buffer.read(cx);
-        let Some(file) = buffer_ref.file() else {
-            log::error!("symbol context's file has no path");
-            return Task::ready(None);
-        };
-        let full_path = file.full_path(cx).to_string_lossy().into_owned();
-        let line_range = self.enclosing_range.to_point(&buffer_ref.snapshot());
-        let text = self.text(cx);
-        let context = AgentContext::Symbol(SymbolContext {
-            handle: self,
-            full_path,
-            line_range,
-            text,
-        });
-        Task::ready(Some(context))
-    }
-}
-
-impl Display for SymbolContext {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let code_block = MarkdownCodeBlock {
-            tag: &codeblock_tag(&self.full_path, Some(self.line_range.clone())),
-            text: &self.text,
-        };
-        write!(f, "{code_block}",)
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct SelectionContextHandle {
-    pub buffer: Entity<Buffer>,
-    pub range: Range<Anchor>,
-    pub context_id: ContextId,
-}
-
-#[derive(Debug, Clone)]
-pub struct SelectionContext {
-    pub handle: SelectionContextHandle,
-    pub full_path: String,
-    pub line_range: Range<Point>,
-    pub text: SharedString,
-}
-
-impl SelectionContextHandle {
-    pub fn eq_for_key(&self, other: &Self) -> bool {
-        self.buffer == other.buffer && self.range == other.range
-    }
-
-    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
-        self.buffer.hash(state);
-        self.range.hash(state);
-    }
-
-    pub fn full_path(&self, cx: &App) -> Option<PathBuf> {
-        Some(self.buffer.read(cx).file()?.full_path(cx))
-    }
-
-    pub fn line_range(&self, cx: &App) -> Range<Point> {
-        self.range.to_point(&self.buffer.read(cx).snapshot())
-    }
-
-    pub fn text(&self, cx: &App) -> SharedString {
-        self.buffer
-            .read(cx)
-            .text_for_range(self.range.clone())
-            .collect::<String>()
-            .into()
-    }
-
-    fn load(self, cx: &App) -> Task<Option<AgentContext>> {
-        let Some(full_path) = self.full_path(cx) else {
-            log::error!("selection context's file has no path");
-            return Task::ready(None);
-        };
-        let text = self.text(cx);
-        let context = AgentContext::Selection(SelectionContext {
-            full_path: full_path.to_string_lossy().into_owned(),
-            line_range: self.line_range(cx),
-            text,
-            handle: self,
-        });
-
-        Task::ready(Some(context))
-    }
-}
-
-impl Display for SelectionContext {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let code_block = MarkdownCodeBlock {
-            tag: &codeblock_tag(&self.full_path, Some(self.line_range.clone())),
-            text: &self.text,
-        };
-        write!(f, "{code_block}",)
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct FetchedUrlContext {
-    pub url: SharedString,
-    /// Text contents of the fetched url. Unlike other context types, the contents of this gets
-    /// populated when added rather than when sending the message. Not used by `PartialEq` or `Hash`
-    /// for `AgentContextKey`.
-    pub text: SharedString,
-    pub context_id: ContextId,
-}
-
-impl FetchedUrlContext {
-    pub fn eq_for_key(&self, other: &Self) -> bool {
-        self.url == other.url
-    }
-
-    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
-        self.url.hash(state);
-    }
-
-    pub fn lookup_key(url: SharedString) -> AgentContextKey {
-        AgentContextKey(AgentContextHandle::FetchedUrl(FetchedUrlContext {
-            url,
-            text: "".into(),
-            context_id: ContextId::for_lookup(),
-        }))
-    }
-
-    pub fn load(self) -> Task<Option<AgentContext>> {
-        Task::ready(Some(AgentContext::FetchedUrl(self)))
-    }
-}
-
-impl Display for FetchedUrlContext {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        // TODO: Better format - url and contents are not delimited.
-        write!(f, "{}\n{}\n", self.url, self.text)
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct ThreadContextHandle {
-    pub thread: Entity<agent::Thread>,
-    pub context_id: ContextId,
-}
-
-#[derive(Debug, Clone)]
-pub struct ThreadContext {
-    pub handle: ThreadContextHandle,
-    pub title: SharedString,
-    pub text: SharedString,
-}
-
-impl ThreadContextHandle {
-    pub fn eq_for_key(&self, other: &Self) -> bool {
-        self.thread == other.thread
-    }
-
-    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
-        self.thread.hash(state)
-    }
-
-    pub fn title(&self, cx: &App) -> SharedString {
-        self.thread.read(cx).title()
-    }
-
-    fn load(self, cx: &mut App) -> Task<Option<AgentContext>> {
-        let task = self.thread.update(cx, |thread, cx| thread.summary(cx));
-        let title = self.title(cx);
-        cx.background_spawn(async move {
-            let text = task.await?;
-            let context = AgentContext::Thread(ThreadContext {
-                title,
-                text,
-                handle: self,
-            });
-            Some(context)
-        })
-    }
-}
-
-impl Display for ThreadContext {
-    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
-        // TODO: Better format for this - doesn't distinguish title and contents.
-        write!(f, "{}\n{}\n", &self.title, &self.text.trim())
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct TextThreadContextHandle {
-    pub text_thread: Entity<TextThread>,
-    pub context_id: ContextId,
-}
-
-#[derive(Debug, Clone)]
-pub struct TextThreadContext {
-    pub handle: TextThreadContextHandle,
-    pub title: SharedString,
-    pub text: SharedString,
-}
-
-impl TextThreadContextHandle {
-    // pub fn lookup_key() ->
-    pub fn eq_for_key(&self, other: &Self) -> bool {
-        self.text_thread == other.text_thread
-    }
-
-    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
-        self.text_thread.hash(state)
-    }
-
-    pub fn title(&self, cx: &App) -> SharedString {
-        self.text_thread.read(cx).summary().or_default()
-    }
-
-    fn load(self, cx: &App) -> Task<Option<AgentContext>> {
-        let title = self.title(cx);
-        let text = self.text_thread.read(cx).to_xml(cx);
-        let context = AgentContext::TextThread(TextThreadContext {
-            title,
-            text: text.into(),
-            handle: self,
-        });
-        Task::ready(Some(context))
-    }
-}
-
-impl Display for TextThreadContext {
-    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
-        write!(f, "<text_thread title=\"")?;
-        for c in self.title.chars() {
-            match c {
-                '&' => write!(f, "&amp;")?,
-                '<' => write!(f, "&lt;")?,
-                '>' => write!(f, "&gt;")?,
-                '"' => write!(f, "&quot;")?,
-                '\'' => write!(f, "&apos;")?,
-                _ => write!(f, "{}", c)?,
-            }
-        }
-        writeln!(f, "\">")?;
-        write!(f, "{}", self.text.trim())?;
-        write!(f, "\n</text_thread>")
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct RulesContextHandle {
-    pub prompt_id: UserPromptId,
-    pub context_id: ContextId,
-}
-
-#[derive(Debug, Clone)]
-pub struct RulesContext {
-    pub handle: RulesContextHandle,
-    pub title: Option<SharedString>,
-    pub text: SharedString,
-}
-
-impl RulesContextHandle {
-    pub fn eq_for_key(&self, other: &Self) -> bool {
-        self.prompt_id == other.prompt_id
-    }
-
-    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
-        self.prompt_id.hash(state)
-    }
-
-    pub fn lookup_key(prompt_id: UserPromptId) -> AgentContextKey {
-        AgentContextKey(AgentContextHandle::Rules(RulesContextHandle {
-            prompt_id,
-            context_id: ContextId::for_lookup(),
-        }))
-    }
-
-    pub fn load(
-        self,
-        prompt_store: &Option<Entity<PromptStore>>,
-        cx: &App,
-    ) -> Task<Option<AgentContext>> {
-        let Some(prompt_store) = prompt_store.as_ref() else {
-            return Task::ready(None);
-        };
-        let prompt_store = prompt_store.read(cx);
-        let prompt_id = self.prompt_id.into();
-        let Some(metadata) = prompt_store.metadata(prompt_id) else {
-            return Task::ready(None);
-        };
-        let title = metadata.title;
-        let text_task = prompt_store.load(prompt_id, cx);
-        cx.background_spawn(async move {
-            // TODO: report load errors instead of just logging
-            let text = text_task.await.log_err()?.into();
-            let context = AgentContext::Rules(RulesContext {
-                handle: self,
-                title,
-                text,
-            });
-            Some(context)
-        })
-    }
-}
-
-impl Display for RulesContext {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        if let Some(title) = &self.title {
-            writeln!(f, "Rules title: {}", title)?;
-        }
-        let code_block = MarkdownCodeBlock {
-            tag: "",
-            text: self.text.trim(),
-        };
-        write!(f, "{code_block}")
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct ImageContext {
-    pub project_path: Option<ProjectPath>,
-    pub full_path: Option<String>,
-    pub original_image: Arc<gpui::Image>,
-    // TODO: handle this elsewhere and remove `ignore-interior-mutability` opt-out in clippy.toml
-    // needed due to a false positive of `clippy::mutable_key_type`.
-    pub image_task: Shared<Task<Option<LanguageModelImage>>>,
-    pub context_id: ContextId,
-}
-
-pub enum ImageStatus {
-    Loading,
-    Error,
-    Warning,
-    Ready,
-}
-
-impl ImageContext {
-    pub fn eq_for_key(&self, other: &Self) -> bool {
-        self.original_image.id() == other.original_image.id()
-    }
-
-    pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
-        self.original_image.id().hash(state);
-    }
-
-    pub fn image(&self) -> Option<LanguageModelImage> {
-        self.image_task.clone().now_or_never().flatten()
-    }
-
-    pub fn status(&self, model: Option<&Arc<dyn language_model::LanguageModel>>) -> ImageStatus {
-        match self.image_task.clone().now_or_never() {
-            None => ImageStatus::Loading,
-            Some(None) => ImageStatus::Error,
-            Some(Some(_)) => {
-                if model.is_some_and(|model| !model.supports_images()) {
-                    ImageStatus::Warning
-                } else {
-                    ImageStatus::Ready
-                }
-            }
-        }
-    }
-
-    pub fn load(self, cx: &App) -> Task<Option<AgentContext>> {
-        cx.background_spawn(async move {
-            self.image_task.clone().await;
-            Some(AgentContext::Image(self))
-        })
-    }
-}
+use crate::mention_set::MentionSet;
 
 #[derive(Debug, Clone, Default)]
 pub struct LoadedContext {
@@ -792,382 +38,26 @@ impl LoadedContext {
 }
 
 /// Loads and formats a collection of contexts.
-pub fn load_context(
-    contexts: Vec<AgentContextHandle>,
-    project: &Entity<Project>,
-    prompt_store: &Option<Entity<PromptStore>>,
-    cx: &mut App,
-) -> Task<LoadedContext> {
-    let load_tasks: Vec<_> = contexts
-        .into_iter()
-        .map(|context| match context {
-            AgentContextHandle::File(context) => context.load(cx),
-            AgentContextHandle::Directory(context) => context.load(project.clone(), cx),
-            AgentContextHandle::Symbol(context) => context.load(cx),
-            AgentContextHandle::Selection(context) => context.load(cx),
-            AgentContextHandle::FetchedUrl(context) => context.load(),
-            AgentContextHandle::Thread(context) => context.load(cx),
-            AgentContextHandle::TextThread(context) => context.load(cx),
-            AgentContextHandle::Rules(context) => context.load(prompt_store, cx),
-            AgentContextHandle::Image(context) => context.load(cx),
-        })
-        .collect();
-
+pub fn load_context(mention_set: &Entity<MentionSet>, cx: &mut App) -> Task<Option<LoadedContext>> {
+    let task = mention_set.update(cx, |mention_set, cx| mention_set.contents(true, cx));
     cx.background_spawn(async move {
-        let load_results = future::join_all(load_tasks).await;
-
-        let mut text = String::new();
-
-        let mut file_context = Vec::new();
-        let mut directory_context = Vec::new();
-        let mut symbol_context = Vec::new();
-        let mut selection_context = Vec::new();
-        let mut fetched_url_context = Vec::new();
-        let mut thread_context = Vec::new();
-        let mut text_thread_context = Vec::new();
-        let mut rules_context = Vec::new();
-        let mut images = Vec::new();
-        for context in load_results.into_iter().flatten() {
-            match context {
-                AgentContext::File(context) => file_context.push(context),
-                AgentContext::Directory(context) => directory_context.push(context),
-                AgentContext::Symbol(context) => symbol_context.push(context),
-                AgentContext::Selection(context) => selection_context.push(context),
-                AgentContext::FetchedUrl(context) => fetched_url_context.push(context),
-                AgentContext::Thread(context) => thread_context.push(context),
-                AgentContext::TextThread(context) => text_thread_context.push(context),
-                AgentContext::Rules(context) => rules_context.push(context),
-                AgentContext::Image(context) => images.extend(context.image()),
-            }
-        }
-
-        // Use empty text if there are no contexts that contribute to text (everything but image
-        // context).
-        if file_context.is_empty()
-            && directory_context.is_empty()
-            && symbol_context.is_empty()
-            && selection_context.is_empty()
-            && fetched_url_context.is_empty()
-            && thread_context.is_empty()
-            && text_thread_context.is_empty()
-            && rules_context.is_empty()
-        {
-            return LoadedContext { text, images };
-        }
-
-        text.push_str(
-            "\n<context>\n\
-            The following items were attached by the user. \
-            They are up-to-date and don't need to be re-read.\n\n",
-        );
-
-        if !file_context.is_empty() {
-            text.push_str("<files>");
-            for context in file_context {
-                text.push('\n');
-                let _ = write!(text, "{context}");
-            }
-            text.push_str("</files>\n");
-        }
-
-        if !directory_context.is_empty() {
-            text.push_str("<directories>");
-            for context in directory_context {
-                text.push('\n');
-                let _ = write!(text, "{context}");
-            }
-            text.push_str("</directories>\n");
-        }
-
-        if !symbol_context.is_empty() {
-            text.push_str("<symbols>");
-            for context in symbol_context {
-                text.push('\n');
-                let _ = write!(text, "{context}");
-            }
-            text.push_str("</symbols>\n");
-        }
-
-        if !selection_context.is_empty() {
-            text.push_str("<selections>");
-            for context in selection_context {
-                text.push('\n');
-                let _ = write!(text, "{context}");
-            }
-            text.push_str("</selections>\n");
-        }
-
-        if !fetched_url_context.is_empty() {
-            text.push_str("<fetched_urls>");
-            for context in fetched_url_context {
-                text.push('\n');
-                let _ = write!(text, "{context}");
-            }
-            text.push_str("</fetched_urls>\n");
-        }
-
-        if !thread_context.is_empty() {
-            text.push_str("<conversation_threads>");
-            for context in thread_context {
-                text.push('\n');
-                let _ = write!(text, "{context}");
-            }
-            text.push_str("</conversation_threads>\n");
-        }
-
-        if !text_thread_context.is_empty() {
-            text.push_str("<text_threads>");
-            for context in text_thread_context {
-                text.push('\n');
-                let _ = writeln!(text, "{context}");
-            }
-            text.push_str("<text_threads>");
-        }
-
-        if !rules_context.is_empty() {
-            text.push_str(
-                "<user_rules>\n\
-                The user has specified the following rules that should be applied:\n",
-            );
-            for context in rules_context {
-                text.push('\n');
-                let _ = write!(text, "{context}");
-            }
-            text.push_str("</user_rules>\n");
-        }
-
-        text.push_str("</context>\n");
-
-        LoadedContext { text, images }
+        let mentions = task.await.log_err()?;
+        let mut loaded_context = LoadedContext::default();
+        loaded_context
+            .text
+            .push_str("The following items were attached by the user.\n");
+        for (_, (_, mention)) in mentions {
+            match mention {
+                Mention::Text { content, .. } => {
+                    loaded_context.text.push_str(&content);
+                }
+                Mention::Image(mention_image) => loaded_context.images.push(LanguageModelImage {
+                    source: mention_image.data,
+                    ..LanguageModelImage::empty()
+                }),
+                Mention::Link => {}
+            }
+        }
+        Some(loaded_context)
     })
 }
-
-fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<Arc<RelPath>> {
-    let mut files = Vec::new();
-
-    for entry in worktree.child_entries(path) {
-        if entry.is_dir() {
-            files.extend(collect_files_in_path(worktree, &entry.path));
-        } else if entry.is_file() {
-            files.push(entry.path.clone());
-        }
-    }
-
-    files
-}
-
-fn codeblock_tag(full_path: &str, line_range: Option<Range<Point>>) -> String {
-    let mut result = String::new();
-
-    if let Some(extension) = Path::new(full_path)
-        .extension()
-        .and_then(|ext| ext.to_str())
-    {
-        let _ = write!(result, "{} ", extension);
-    }
-
-    let _ = write!(result, "{}", full_path);
-
-    if let Some(range) = line_range {
-        if range.start.row == range.end.row {
-            let _ = write!(result, ":{}", range.start.row + 1);
-        } else {
-            let _ = write!(result, ":{}-{}", range.start.row + 1, range.end.row + 1);
-        }
-    }
-
-    result
-}
-
-/// Wraps `AgentContext` to opt-in to `PartialEq` and `Hash` impls which use a subset of fields
-/// needed for stable context identity.
-#[derive(Debug, Clone, RefCast)]
-#[repr(transparent)]
-pub struct AgentContextKey(pub AgentContextHandle);
-
-impl AsRef<AgentContextHandle> for AgentContextKey {
-    fn as_ref(&self) -> &AgentContextHandle {
-        &self.0
-    }
-}
-
-impl Eq for AgentContextKey {}
-
-impl PartialEq for AgentContextKey {
-    fn eq(&self, other: &Self) -> bool {
-        match &self.0 {
-            AgentContextHandle::File(context) => {
-                if let AgentContextHandle::File(other_context) = &other.0 {
-                    return context.eq_for_key(other_context);
-                }
-            }
-            AgentContextHandle::Directory(context) => {
-                if let AgentContextHandle::Directory(other_context) = &other.0 {
-                    return context.eq_for_key(other_context);
-                }
-            }
-            AgentContextHandle::Symbol(context) => {
-                if let AgentContextHandle::Symbol(other_context) = &other.0 {
-                    return context.eq_for_key(other_context);
-                }
-            }
-            AgentContextHandle::Selection(context) => {
-                if let AgentContextHandle::Selection(other_context) = &other.0 {
-                    return context.eq_for_key(other_context);
-                }
-            }
-            AgentContextHandle::FetchedUrl(context) => {
-                if let AgentContextHandle::FetchedUrl(other_context) = &other.0 {
-                    return context.eq_for_key(other_context);
-                }
-            }
-            AgentContextHandle::Thread(context) => {
-                if let AgentContextHandle::Thread(other_context) = &other.0 {
-                    return context.eq_for_key(other_context);
-                }
-            }
-            AgentContextHandle::Rules(context) => {
-                if let AgentContextHandle::Rules(other_context) = &other.0 {
-                    return context.eq_for_key(other_context);
-                }
-            }
-            AgentContextHandle::Image(context) => {
-                if let AgentContextHandle::Image(other_context) = &other.0 {
-                    return context.eq_for_key(other_context);
-                }
-            }
-            AgentContextHandle::TextThread(context) => {
-                if let AgentContextHandle::TextThread(other_context) = &other.0 {
-                    return context.eq_for_key(other_context);
-                }
-            }
-        }
-        false
-    }
-}
-
-impl Hash for AgentContextKey {
-    fn hash<H: Hasher>(&self, state: &mut H) {
-        match &self.0 {
-            AgentContextHandle::File(context) => context.hash_for_key(state),
-            AgentContextHandle::Directory(context) => context.hash_for_key(state),
-            AgentContextHandle::Symbol(context) => context.hash_for_key(state),
-            AgentContextHandle::Selection(context) => context.hash_for_key(state),
-            AgentContextHandle::FetchedUrl(context) => context.hash_for_key(state),
-            AgentContextHandle::Thread(context) => context.hash_for_key(state),
-            AgentContextHandle::TextThread(context) => context.hash_for_key(state),
-            AgentContextHandle::Rules(context) => context.hash_for_key(state),
-            AgentContextHandle::Image(context) => context.hash_for_key(state),
-        }
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use gpui::TestAppContext;
-    use project::{FakeFs, Project};
-    use serde_json::json;
-    use settings::SettingsStore;
-    use util::path;
-
-    fn init_test_settings(cx: &mut TestAppContext) {
-        cx.update(|cx| {
-            let settings_store = SettingsStore::test(cx);
-            cx.set_global(settings_store);
-        });
-    }
-
-    // Helper to create a test project with test files
-    async fn create_test_project(
-        cx: &mut TestAppContext,
-        files: serde_json::Value,
-    ) -> Entity<Project> {
-        let fs = FakeFs::new(cx.background_executor.clone());
-        fs.insert_tree(path!("/test"), files).await;
-        Project::test(fs, [path!("/test").as_ref()], cx).await
-    }
-
-    #[gpui::test]
-    async fn test_large_file_uses_fallback(cx: &mut TestAppContext) {
-        init_test_settings(cx);
-
-        // Create a large file that exceeds AUTO_OUTLINE_SIZE
-        const LINE: &str = "Line with some text\n";
-        let large_content = LINE.repeat(2 * (outline::AUTO_OUTLINE_SIZE / LINE.len()));
-        let content_len = large_content.len();
-
-        assert!(content_len > outline::AUTO_OUTLINE_SIZE);
-
-        let file_context = load_context_for("file.txt", large_content, cx).await;
-
-        // Should contain some of the actual file content
-        assert!(
-            file_context.text.contains(LINE),
-            "Should contain some of the file content"
-        );
-
-        // Should be much smaller than original
-        assert!(
-            file_context.text.len() < content_len / 10,
-            "Should be significantly smaller than original content"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_small_file_uses_full_content(cx: &mut TestAppContext) {
-        init_test_settings(cx);
-
-        let small_content = "This is a small file.\n";
-        let content_len = small_content.len();
-
-        assert!(content_len < outline::AUTO_OUTLINE_SIZE);
-
-        let file_context = load_context_for("file.txt", small_content.to_string(), cx).await;
-
-        assert!(
-            !file_context
-                .text
-                .contains(&format!("# File outline for {}", path!("test/file.txt"))),
-            "Small files should not get an outline"
-        );
-
-        assert!(
-            file_context.text.contains(small_content),
-            "Small files should use full content"
-        );
-    }
-
-    async fn load_context_for(
-        filename: &str,
-        content: String,
-        cx: &mut TestAppContext,
-    ) -> LoadedContext {
-        // Create a test project with the file
-        let project = create_test_project(
-            cx,
-            json!({
-                filename: content,
-            }),
-        )
-        .await;
-
-        // Open the buffer
-        let buffer_path = project
-            .read_with(cx, |project, cx| project.find_project_path(filename, cx))
-            .unwrap();
-
-        let buffer = project
-            .update(cx, |project, cx| project.open_buffer(buffer_path, cx))
-            .await
-            .unwrap();
-
-        let context_handle = AgentContextHandle::File(FileContextHandle {
-            buffer: buffer.clone(),
-            context_id: ContextId::zero(),
-        });
-
-        cx.update(|cx| load_context(vec![context_handle], &project, &None, cx))
-            .await
-    }
-}

crates/agent_ui/src/context_picker.rs πŸ”—

@@ -1,931 +0,0 @@
-mod completion_provider;
-pub(crate) mod fetch_context_picker;
-pub(crate) mod file_context_picker;
-pub(crate) mod rules_context_picker;
-pub(crate) mod symbol_context_picker;
-pub(crate) mod thread_context_picker;
-
-use std::ops::Range;
-use std::path::PathBuf;
-use std::sync::Arc;
-
-use agent::{HistoryEntry, HistoryEntryId, HistoryStore};
-use agent_client_protocol as acp;
-use anyhow::{Result, anyhow};
-use collections::HashSet;
-pub use completion_provider::ContextPickerCompletionProvider;
-use editor::display_map::{Crease, CreaseId, CreaseMetadata, FoldId};
-use editor::{Anchor, Editor, ExcerptId, FoldPlaceholder, ToOffset};
-use fetch_context_picker::FetchContextPicker;
-use file_context_picker::FileContextPicker;
-use file_context_picker::render_file_context_entry;
-use gpui::{
-    App, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
-    WeakEntity,
-};
-use language::Buffer;
-use multi_buffer::MultiBufferRow;
-use project::ProjectPath;
-use prompt_store::PromptStore;
-use rules_context_picker::{RulesContextEntry, RulesContextPicker};
-use symbol_context_picker::SymbolContextPicker;
-use thread_context_picker::render_thread_context_entry;
-use ui::{
-    ButtonLike, ContextMenu, ContextMenuEntry, ContextMenuItem, Disclosure, TintColor, prelude::*,
-};
-use util::paths::PathStyle;
-use util::rel_path::RelPath;
-use workspace::{Workspace, notifications::NotifyResultExt};
-
-use crate::context_picker::thread_context_picker::ThreadContextPicker;
-use crate::{context::RULES_ICON, context_store::ContextStore};
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum ContextPickerEntry {
-    Mode(ContextPickerMode),
-    Action(ContextPickerAction),
-}
-
-impl ContextPickerEntry {
-    pub fn keyword(&self) -> &'static str {
-        match self {
-            Self::Mode(mode) => mode.keyword(),
-            Self::Action(action) => action.keyword(),
-        }
-    }
-
-    pub fn label(&self) -> &'static str {
-        match self {
-            Self::Mode(mode) => mode.label(),
-            Self::Action(action) => action.label(),
-        }
-    }
-
-    pub fn icon(&self) -> IconName {
-        match self {
-            Self::Mode(mode) => mode.icon(),
-            Self::Action(action) => action.icon(),
-        }
-    }
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum ContextPickerMode {
-    File,
-    Symbol,
-    Fetch,
-    Thread,
-    Rules,
-}
-
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub(crate) enum ContextPickerAction {
-    AddSelections,
-}
-
-impl ContextPickerAction {
-    pub fn keyword(&self) -> &'static str {
-        match self {
-            Self::AddSelections => "selection",
-        }
-    }
-
-    pub fn label(&self) -> &'static str {
-        match self {
-            Self::AddSelections => "Selection",
-        }
-    }
-
-    pub fn icon(&self) -> IconName {
-        match self {
-            Self::AddSelections => IconName::Reader,
-        }
-    }
-}
-
-impl TryFrom<&str> for ContextPickerMode {
-    type Error = String;
-
-    fn try_from(value: &str) -> Result<Self, Self::Error> {
-        match value {
-            "file" => Ok(Self::File),
-            "symbol" => Ok(Self::Symbol),
-            "fetch" => Ok(Self::Fetch),
-            "thread" => Ok(Self::Thread),
-            "rule" => Ok(Self::Rules),
-            _ => Err(format!("Invalid context picker mode: {}", value)),
-        }
-    }
-}
-
-impl ContextPickerMode {
-    pub fn keyword(&self) -> &'static str {
-        match self {
-            Self::File => "file",
-            Self::Symbol => "symbol",
-            Self::Fetch => "fetch",
-            Self::Thread => "thread",
-            Self::Rules => "rule",
-        }
-    }
-
-    pub fn label(&self) -> &'static str {
-        match self {
-            Self::File => "Files & Directories",
-            Self::Symbol => "Symbols",
-            Self::Fetch => "Fetch",
-            Self::Thread => "Threads",
-            Self::Rules => "Rules",
-        }
-    }
-
-    pub fn icon(&self) -> IconName {
-        match self {
-            Self::File => IconName::File,
-            Self::Symbol => IconName::Code,
-            Self::Fetch => IconName::ToolWeb,
-            Self::Thread => IconName::Thread,
-            Self::Rules => RULES_ICON,
-        }
-    }
-}
-
-#[derive(Debug, Clone)]
-enum ContextPickerState {
-    Default(Entity<ContextMenu>),
-    File(Entity<FileContextPicker>),
-    Symbol(Entity<SymbolContextPicker>),
-    Fetch(Entity<FetchContextPicker>),
-    Thread(Entity<ThreadContextPicker>),
-    Rules(Entity<RulesContextPicker>),
-}
-
-pub(super) struct ContextPicker {
-    mode: ContextPickerState,
-    workspace: WeakEntity<Workspace>,
-    context_store: WeakEntity<ContextStore>,
-    thread_store: Option<WeakEntity<HistoryStore>>,
-    prompt_store: Option<WeakEntity<PromptStore>>,
-    _subscriptions: Vec<Subscription>,
-}
-
-impl ContextPicker {
-    pub fn new(
-        workspace: WeakEntity<Workspace>,
-        thread_store: Option<WeakEntity<HistoryStore>>,
-        prompt_store: Option<WeakEntity<PromptStore>>,
-        context_store: WeakEntity<ContextStore>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let subscriptions = context_store
-            .upgrade()
-            .map(|context_store| {
-                cx.observe(&context_store, |this, _, cx| this.notify_current_picker(cx))
-            })
-            .into_iter()
-            .chain(
-                thread_store
-                    .as_ref()
-                    .and_then(|thread_store| thread_store.upgrade())
-                    .map(|thread_store| {
-                        cx.observe(&thread_store, |this, _, cx| this.notify_current_picker(cx))
-                    }),
-            )
-            .collect::<Vec<Subscription>>();
-
-        ContextPicker {
-            mode: ContextPickerState::Default(ContextMenu::build(
-                window,
-                cx,
-                |menu, _window, _cx| menu,
-            )),
-            workspace,
-            context_store,
-            thread_store,
-            prompt_store,
-            _subscriptions: subscriptions,
-        }
-    }
-
-    pub fn init(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        self.mode = ContextPickerState::Default(self.build_menu(window, cx));
-        cx.notify();
-    }
-
-    fn build_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) -> Entity<ContextMenu> {
-        let context_picker = cx.entity();
-
-        let menu = ContextMenu::build(window, cx, move |menu, _window, cx| {
-            let Some(workspace) = self.workspace.upgrade() else {
-                return menu;
-            };
-            let path_style = workspace.read(cx).path_style(cx);
-            let recent = self.recent_entries(cx);
-            let has_recent = !recent.is_empty();
-            let recent_entries = recent
-                .into_iter()
-                .enumerate()
-                .map(|(ix, entry)| {
-                    self.recent_menu_item(context_picker.clone(), ix, entry, path_style)
-                })
-                .collect::<Vec<_>>();
-
-            let entries = self
-                .workspace
-                .upgrade()
-                .map(|workspace| {
-                    available_context_picker_entries(
-                        &self.prompt_store,
-                        &self.thread_store,
-                        &workspace,
-                        cx,
-                    )
-                })
-                .unwrap_or_default();
-
-            menu.when(has_recent, |menu| {
-                menu.custom_row(|_, _| {
-                    div()
-                        .mb_1()
-                        .child(
-                            Label::new("Recent")
-                                .color(Color::Muted)
-                                .size(LabelSize::Small),
-                        )
-                        .into_any_element()
-                })
-            })
-            .extend(recent_entries)
-            .when(has_recent, |menu| menu.separator())
-            .extend(entries.into_iter().map(|entry| {
-                let context_picker = context_picker.clone();
-
-                ContextMenuEntry::new(entry.label())
-                    .icon(entry.icon())
-                    .icon_size(IconSize::XSmall)
-                    .icon_color(Color::Muted)
-                    .handler(move |window, cx| {
-                        context_picker.update(cx, |this, cx| this.select_entry(entry, window, cx))
-                    })
-            }))
-            .keep_open_on_confirm(true)
-        });
-
-        cx.subscribe(&menu, move |_, _, _: &DismissEvent, cx| {
-            cx.emit(DismissEvent);
-        })
-        .detach();
-
-        menu
-    }
-
-    /// Whether threads are allowed as context.
-    pub fn allow_threads(&self) -> bool {
-        self.thread_store.is_some()
-    }
-
-    fn select_entry(
-        &mut self,
-        entry: ContextPickerEntry,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let context_picker = cx.entity().downgrade();
-
-        match entry {
-            ContextPickerEntry::Mode(mode) => match mode {
-                ContextPickerMode::File => {
-                    self.mode = ContextPickerState::File(cx.new(|cx| {
-                        FileContextPicker::new(
-                            context_picker.clone(),
-                            self.workspace.clone(),
-                            self.context_store.clone(),
-                            window,
-                            cx,
-                        )
-                    }));
-                }
-                ContextPickerMode::Symbol => {
-                    self.mode = ContextPickerState::Symbol(cx.new(|cx| {
-                        SymbolContextPicker::new(
-                            context_picker.clone(),
-                            self.workspace.clone(),
-                            self.context_store.clone(),
-                            window,
-                            cx,
-                        )
-                    }));
-                }
-                ContextPickerMode::Rules => {
-                    if let Some(prompt_store) = self.prompt_store.as_ref() {
-                        self.mode = ContextPickerState::Rules(cx.new(|cx| {
-                            RulesContextPicker::new(
-                                prompt_store.clone(),
-                                context_picker.clone(),
-                                self.context_store.clone(),
-                                window,
-                                cx,
-                            )
-                        }));
-                    }
-                }
-                ContextPickerMode::Fetch => {
-                    self.mode = ContextPickerState::Fetch(cx.new(|cx| {
-                        FetchContextPicker::new(
-                            context_picker.clone(),
-                            self.workspace.clone(),
-                            self.context_store.clone(),
-                            window,
-                            cx,
-                        )
-                    }));
-                }
-                ContextPickerMode::Thread => {
-                    if let Some(thread_store) = self.thread_store.clone() {
-                        self.mode = ContextPickerState::Thread(cx.new(|cx| {
-                            ThreadContextPicker::new(
-                                thread_store,
-                                context_picker.clone(),
-                                self.context_store.clone(),
-                                self.workspace.clone(),
-                                window,
-                                cx,
-                            )
-                        }));
-                    }
-                }
-            },
-            ContextPickerEntry::Action(action) => match action {
-                ContextPickerAction::AddSelections => {
-                    if let Some((context_store, workspace)) =
-                        self.context_store.upgrade().zip(self.workspace.upgrade())
-                    {
-                        add_selections_as_context(&context_store, &workspace, cx);
-                    }
-
-                    cx.emit(DismissEvent);
-                }
-            },
-        }
-
-        cx.notify();
-        cx.focus_self(window);
-    }
-
-    pub fn select_first(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        // Other variants already select their first entry on open automatically
-        if let ContextPickerState::Default(entity) = &self.mode {
-            entity.update(cx, |entity, cx| {
-                entity.select_first(&Default::default(), window, cx)
-            })
-        }
-    }
-
-    fn recent_menu_item(
-        &self,
-        context_picker: Entity<ContextPicker>,
-        ix: usize,
-        entry: RecentEntry,
-        path_style: PathStyle,
-    ) -> ContextMenuItem {
-        match entry {
-            RecentEntry::File {
-                project_path,
-                path_prefix,
-            } => {
-                let context_store = self.context_store.clone();
-                let worktree_id = project_path.worktree_id;
-                let path = project_path.path.clone();
-
-                ContextMenuItem::custom_entry(
-                    move |_window, cx| {
-                        render_file_context_entry(
-                            ElementId::named_usize("ctx-recent", ix),
-                            worktree_id,
-                            &path,
-                            &path_prefix,
-                            false,
-                            path_style,
-                            context_store.clone(),
-                            cx,
-                        )
-                        .into_any()
-                    },
-                    move |window, cx| {
-                        context_picker.update(cx, |this, cx| {
-                            this.add_recent_file(project_path.clone(), window, cx);
-                        })
-                    },
-                    None,
-                )
-            }
-            RecentEntry::Thread(thread) => {
-                let context_store = self.context_store.clone();
-                let view_thread = thread.clone();
-
-                ContextMenuItem::custom_entry(
-                    move |_window, cx| {
-                        render_thread_context_entry(&view_thread, context_store.clone(), cx)
-                            .into_any()
-                    },
-                    move |window, cx| {
-                        context_picker.update(cx, |this, cx| {
-                            this.add_recent_thread(thread.clone(), window, cx)
-                                .detach_and_log_err(cx);
-                        })
-                    },
-                    None,
-                )
-            }
-        }
-    }
-
-    fn add_recent_file(
-        &self,
-        project_path: ProjectPath,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let Some(context_store) = self.context_store.upgrade() else {
-            return;
-        };
-
-        let task = context_store.update(cx, |context_store, cx| {
-            context_store.add_file_from_path(project_path.clone(), true, cx)
-        });
-
-        cx.spawn_in(window, async move |_, cx| task.await.notify_async_err(cx))
-            .detach();
-
-        cx.notify();
-    }
-
-    fn add_recent_thread(
-        &self,
-        entry: HistoryEntry,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<()>> {
-        let Some(context_store) = self.context_store.upgrade() else {
-            return Task::ready(Err(anyhow!("context store not available")));
-        };
-        let Some(project) = self
-            .workspace
-            .upgrade()
-            .map(|workspace| workspace.read(cx).project().clone())
-        else {
-            return Task::ready(Err(anyhow!("project not available")));
-        };
-
-        match entry {
-            HistoryEntry::AcpThread(thread) => {
-                let Some(thread_store) = self
-                    .thread_store
-                    .as_ref()
-                    .and_then(|thread_store| thread_store.upgrade())
-                else {
-                    return Task::ready(Err(anyhow!("thread store not available")));
-                };
-                let load_thread_task =
-                    agent::load_agent_thread(thread.id, thread_store, project, cx);
-                cx.spawn(async move |this, cx| {
-                    let thread = load_thread_task.await?;
-                    context_store.update(cx, |context_store, cx| {
-                        context_store.add_thread(thread, true, cx);
-                    })?;
-                    this.update(cx, |_this, cx| cx.notify())
-                })
-            }
-            HistoryEntry::TextThread(thread) => {
-                let Some(thread_store) = self
-                    .thread_store
-                    .as_ref()
-                    .and_then(|thread_store| thread_store.upgrade())
-                else {
-                    return Task::ready(Err(anyhow!("text thread store not available")));
-                };
-
-                let task = thread_store.update(cx, |this, cx| {
-                    this.load_text_thread(thread.path.clone(), cx)
-                });
-                cx.spawn(async move |this, cx| {
-                    let thread = task.await?;
-                    context_store.update(cx, |context_store, cx| {
-                        context_store.add_text_thread(thread, true, cx);
-                    })?;
-                    this.update(cx, |_this, cx| cx.notify())
-                })
-            }
-        }
-    }
-
-    fn recent_entries(&self, cx: &mut App) -> Vec<RecentEntry> {
-        let Some(workspace) = self.workspace.upgrade() else {
-            return vec![];
-        };
-
-        let Some(context_store) = self.context_store.upgrade() else {
-            return vec![];
-        };
-
-        recent_context_picker_entries_with_store(
-            context_store,
-            self.thread_store.clone(),
-            workspace,
-            None,
-            cx,
-        )
-    }
-
-    fn notify_current_picker(&mut self, cx: &mut Context<Self>) {
-        match &self.mode {
-            ContextPickerState::Default(entity) => entity.update(cx, |_, cx| cx.notify()),
-            ContextPickerState::File(entity) => entity.update(cx, |_, cx| cx.notify()),
-            ContextPickerState::Symbol(entity) => entity.update(cx, |_, cx| cx.notify()),
-            ContextPickerState::Fetch(entity) => entity.update(cx, |_, cx| cx.notify()),
-            ContextPickerState::Thread(entity) => entity.update(cx, |_, cx| cx.notify()),
-            ContextPickerState::Rules(entity) => entity.update(cx, |_, cx| cx.notify()),
-        }
-    }
-}
-
-impl EventEmitter<DismissEvent> for ContextPicker {}
-
-impl Focusable for ContextPicker {
-    fn focus_handle(&self, cx: &App) -> FocusHandle {
-        match &self.mode {
-            ContextPickerState::Default(menu) => menu.focus_handle(cx),
-            ContextPickerState::File(file_picker) => file_picker.focus_handle(cx),
-            ContextPickerState::Symbol(symbol_picker) => symbol_picker.focus_handle(cx),
-            ContextPickerState::Fetch(fetch_picker) => fetch_picker.focus_handle(cx),
-            ContextPickerState::Thread(thread_picker) => thread_picker.focus_handle(cx),
-            ContextPickerState::Rules(user_rules_picker) => user_rules_picker.focus_handle(cx),
-        }
-    }
-}
-
-impl Render for ContextPicker {
-    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-        v_flex()
-            .w(px(400.))
-            .min_w(px(400.))
-            .map(|parent| match &self.mode {
-                ContextPickerState::Default(menu) => parent.child(menu.clone()),
-                ContextPickerState::File(file_picker) => parent.child(file_picker.clone()),
-                ContextPickerState::Symbol(symbol_picker) => parent.child(symbol_picker.clone()),
-                ContextPickerState::Fetch(fetch_picker) => parent.child(fetch_picker.clone()),
-                ContextPickerState::Thread(thread_picker) => parent.child(thread_picker.clone()),
-                ContextPickerState::Rules(user_rules_picker) => {
-                    parent.child(user_rules_picker.clone())
-                }
-            })
-    }
-}
-
-pub(crate) enum RecentEntry {
-    File {
-        project_path: ProjectPath,
-        path_prefix: Arc<RelPath>,
-    },
-    Thread(HistoryEntry),
-}
-
-pub(crate) fn available_context_picker_entries(
-    prompt_store: &Option<WeakEntity<PromptStore>>,
-    thread_store: &Option<WeakEntity<HistoryStore>>,
-    workspace: &Entity<Workspace>,
-    cx: &mut App,
-) -> Vec<ContextPickerEntry> {
-    let mut entries = vec![
-        ContextPickerEntry::Mode(ContextPickerMode::File),
-        ContextPickerEntry::Mode(ContextPickerMode::Symbol),
-    ];
-
-    let has_selection = workspace
-        .read(cx)
-        .active_item(cx)
-        .and_then(|item| item.downcast::<Editor>())
-        .is_some_and(|editor| {
-            editor.update(cx, |editor, cx| {
-                editor.has_non_empty_selection(&editor.display_snapshot(cx))
-            })
-        });
-    if has_selection {
-        entries.push(ContextPickerEntry::Action(
-            ContextPickerAction::AddSelections,
-        ));
-    }
-
-    if thread_store.is_some() {
-        entries.push(ContextPickerEntry::Mode(ContextPickerMode::Thread));
-    }
-
-    if prompt_store.is_some() {
-        entries.push(ContextPickerEntry::Mode(ContextPickerMode::Rules));
-    }
-
-    entries.push(ContextPickerEntry::Mode(ContextPickerMode::Fetch));
-
-    entries
-}
-
-fn recent_context_picker_entries_with_store(
-    context_store: Entity<ContextStore>,
-    thread_store: Option<WeakEntity<HistoryStore>>,
-    workspace: Entity<Workspace>,
-    exclude_path: Option<ProjectPath>,
-    cx: &App,
-) -> Vec<RecentEntry> {
-    let project = workspace.read(cx).project();
-
-    let mut exclude_paths = context_store.read(cx).file_paths(cx);
-    exclude_paths.extend(exclude_path);
-
-    let exclude_paths = exclude_paths
-        .into_iter()
-        .filter_map(|project_path| project.read(cx).absolute_path(&project_path, cx))
-        .collect();
-
-    let exclude_threads = context_store.read(cx).thread_ids();
-
-    recent_context_picker_entries(thread_store, workspace, &exclude_paths, exclude_threads, cx)
-}
-
-pub(crate) fn recent_context_picker_entries(
-    thread_store: Option<WeakEntity<HistoryStore>>,
-    workspace: Entity<Workspace>,
-    exclude_paths: &HashSet<PathBuf>,
-    exclude_threads: &HashSet<acp::SessionId>,
-    cx: &App,
-) -> Vec<RecentEntry> {
-    let mut recent = Vec::with_capacity(6);
-    let workspace = workspace.read(cx);
-    let project = workspace.project().read(cx);
-    let include_root_name = workspace.visible_worktrees(cx).count() > 1;
-
-    recent.extend(
-        workspace
-            .recent_navigation_history_iter(cx)
-            .filter(|(_, abs_path)| {
-                abs_path
-                    .as_ref()
-                    .is_none_or(|path| !exclude_paths.contains(path.as_path()))
-            })
-            .take(4)
-            .filter_map(|(project_path, _)| {
-                project
-                    .worktree_for_id(project_path.worktree_id, cx)
-                    .map(|worktree| {
-                        let path_prefix = if include_root_name {
-                            worktree.read(cx).root_name().into()
-                        } else {
-                            RelPath::empty().into()
-                        };
-                        RecentEntry::File {
-                            project_path,
-                            path_prefix,
-                        }
-                    })
-            }),
-    );
-
-    if let Some(thread_store) = thread_store.and_then(|store| store.upgrade()) {
-        const RECENT_THREADS_COUNT: usize = 2;
-        recent.extend(
-            thread_store
-                .read(cx)
-                .recently_opened_entries(cx)
-                .iter()
-                .filter(|e| match e.id() {
-                    HistoryEntryId::AcpThread(session_id) => !exclude_threads.contains(&session_id),
-                    HistoryEntryId::TextThread(path) => {
-                        !exclude_paths.contains(&path.to_path_buf())
-                    }
-                })
-                .take(RECENT_THREADS_COUNT)
-                .map(|thread| RecentEntry::Thread(thread.clone())),
-        );
-    }
-
-    recent
-}
-
-fn add_selections_as_context(
-    context_store: &Entity<ContextStore>,
-    workspace: &Entity<Workspace>,
-    cx: &mut App,
-) {
-    let selection_ranges = selection_ranges(workspace, cx);
-    context_store.update(cx, |context_store, cx| {
-        for (buffer, range) in selection_ranges {
-            context_store.add_selection(buffer, range, cx);
-        }
-    })
-}
-
-pub(crate) fn selection_ranges(
-    workspace: &Entity<Workspace>,
-    cx: &mut App,
-) -> Vec<(Entity<Buffer>, Range<text::Anchor>)> {
-    let Some(editor) = workspace
-        .read(cx)
-        .active_item(cx)
-        .and_then(|item| item.act_as::<Editor>(cx))
-    else {
-        return Vec::new();
-    };
-
-    editor.update(cx, |editor, cx| {
-        let selections = editor.selections.all_adjusted(&editor.display_snapshot(cx));
-
-        let buffer = editor.buffer().clone().read(cx);
-        let snapshot = buffer.snapshot(cx);
-
-        selections
-            .into_iter()
-            .map(|s| snapshot.anchor_after(s.start)..snapshot.anchor_before(s.end))
-            .flat_map(|range| {
-                let (start_buffer, start) = buffer.text_anchor_for_position(range.start, cx)?;
-                let (end_buffer, end) = buffer.text_anchor_for_position(range.end, cx)?;
-                if start_buffer != end_buffer {
-                    return None;
-                }
-                Some((start_buffer, start..end))
-            })
-            .collect::<Vec<_>>()
-    })
-}
-
-pub(crate) fn insert_crease_for_mention(
-    excerpt_id: ExcerptId,
-    crease_start: text::Anchor,
-    content_len: usize,
-    crease_label: SharedString,
-    crease_icon_path: SharedString,
-    editor_entity: Entity<Editor>,
-    window: &mut Window,
-    cx: &mut App,
-) -> Option<CreaseId> {
-    editor_entity.update(cx, |editor, cx| {
-        let snapshot = editor.buffer().read(cx).snapshot(cx);
-
-        let start = snapshot.anchor_in_excerpt(excerpt_id, crease_start)?;
-
-        let start = start.bias_right(&snapshot);
-        let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
-
-        let crease = crease_for_mention(
-            crease_label,
-            crease_icon_path,
-            start..end,
-            editor_entity.downgrade(),
-        );
-
-        let ids = editor.insert_creases(vec![crease.clone()], cx);
-        editor.fold_creases(vec![crease], false, window, cx);
-
-        Some(ids[0])
-    })
-}
-
-pub fn crease_for_mention(
-    label: SharedString,
-    icon_path: SharedString,
-    range: Range<Anchor>,
-    editor_entity: WeakEntity<Editor>,
-) -> Crease<Anchor> {
-    let placeholder = FoldPlaceholder {
-        render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
-        merge_adjacent: false,
-        ..Default::default()
-    };
-
-    let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
-
-    Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
-        .with_metadata(CreaseMetadata { icon_path, label })
-}
-
-fn render_fold_icon_button(
-    icon_path: SharedString,
-    label: SharedString,
-    editor: WeakEntity<Editor>,
-) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
-    Arc::new({
-        move |fold_id, fold_range, cx| {
-            let is_in_text_selection = editor
-                .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
-                .unwrap_or_default();
-
-            ButtonLike::new(fold_id)
-                .style(ButtonStyle::Filled)
-                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
-                .toggle_state(is_in_text_selection)
-                .child(
-                    h_flex()
-                        .gap_1()
-                        .child(
-                            Icon::from_path(icon_path.clone())
-                                .size(IconSize::XSmall)
-                                .color(Color::Muted),
-                        )
-                        .child(
-                            Label::new(label.clone())
-                                .size(LabelSize::Small)
-                                .buffer_font(cx)
-                                .single_line(),
-                        ),
-                )
-                .into_any_element()
-        }
-    })
-}
-
-fn fold_toggle(
-    name: &'static str,
-) -> impl Fn(
-    MultiBufferRow,
-    bool,
-    Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
-    &mut Window,
-    &mut App,
-) -> AnyElement {
-    move |row, is_folded, fold, _window, _cx| {
-        Disclosure::new((name, row.0 as u64), !is_folded)
-            .toggle_state(is_folded)
-            .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
-            .into_any_element()
-    }
-}
-
-pub struct MentionLink;
-
-impl MentionLink {
-    const FILE: &str = "@file";
-    const SYMBOL: &str = "@symbol";
-    const SELECTION: &str = "@selection";
-    const THREAD: &str = "@thread";
-    const FETCH: &str = "@fetch";
-    const RULE: &str = "@rule";
-
-    const TEXT_THREAD_URL_PREFIX: &str = "text-thread://";
-
-    pub fn for_file(file_name: &str, full_path: &str) -> String {
-        format!("[@{}]({}:{})", file_name, Self::FILE, full_path)
-    }
-
-    pub fn for_symbol(symbol_name: &str, full_path: &str) -> String {
-        format!(
-            "[@{}]({}:{}:{})",
-            symbol_name,
-            Self::SYMBOL,
-            full_path,
-            symbol_name
-        )
-    }
-
-    pub fn for_selection(file_name: &str, full_path: &str, line_range: Range<usize>) -> String {
-        format!(
-            "[@{} ({}-{})]({}:{}:{}-{})",
-            file_name,
-            line_range.start + 1,
-            line_range.end + 1,
-            Self::SELECTION,
-            full_path,
-            line_range.start,
-            line_range.end
-        )
-    }
-
-    pub fn for_thread(thread: &HistoryEntry) -> String {
-        match thread {
-            HistoryEntry::AcpThread(thread) => {
-                format!("[@{}]({}:{})", thread.title, Self::THREAD, thread.id)
-            }
-            HistoryEntry::TextThread(thread) => {
-                let filename = thread
-                    .path
-                    .file_name()
-                    .unwrap_or_default()
-                    .to_string_lossy();
-                let escaped_filename = urlencoding::encode(&filename);
-                format!(
-                    "[@{}]({}:{}{})",
-                    thread.title,
-                    Self::THREAD,
-                    Self::TEXT_THREAD_URL_PREFIX,
-                    escaped_filename
-                )
-            }
-        }
-    }
-
-    pub fn for_fetch(url: &str) -> String {
-        format!("[@{}]({}:{})", url, Self::FETCH, url)
-    }
-
-    pub fn for_rule(rule: &RulesContextEntry) -> String {
-        format!("[@{}]({}:{})", rule.title, Self::RULE, rule.prompt_id.0)
-    }
-}

crates/agent_ui/src/context_picker/fetch_context_picker.rs πŸ”—

@@ -1,252 +0,0 @@
-use std::cell::RefCell;
-use std::rc::Rc;
-use std::sync::Arc;
-
-use anyhow::{Context as _, Result, bail};
-use futures::AsyncReadExt as _;
-use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
-use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
-use http_client::{AsyncBody, HttpClientWithUrl};
-use picker::{Picker, PickerDelegate};
-use ui::{Context, ListItem, Window, prelude::*};
-use workspace::Workspace;
-
-use crate::{context_picker::ContextPicker, context_store::ContextStore};
-
-pub struct FetchContextPicker {
-    picker: Entity<Picker<FetchContextPickerDelegate>>,
-}
-
-impl FetchContextPicker {
-    pub fn new(
-        context_picker: WeakEntity<ContextPicker>,
-        workspace: WeakEntity<Workspace>,
-        context_store: WeakEntity<ContextStore>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let delegate = FetchContextPickerDelegate::new(context_picker, workspace, context_store);
-        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
-
-        Self { picker }
-    }
-}
-
-impl Focusable for FetchContextPicker {
-    fn focus_handle(&self, cx: &App) -> FocusHandle {
-        self.picker.focus_handle(cx)
-    }
-}
-
-impl Render for FetchContextPicker {
-    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-        self.picker.clone()
-    }
-}
-
-#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
-enum ContentType {
-    Html,
-    Plaintext,
-    Json,
-}
-
-pub struct FetchContextPickerDelegate {
-    context_picker: WeakEntity<ContextPicker>,
-    workspace: WeakEntity<Workspace>,
-    context_store: WeakEntity<ContextStore>,
-    url: String,
-}
-
-impl FetchContextPickerDelegate {
-    pub fn new(
-        context_picker: WeakEntity<ContextPicker>,
-        workspace: WeakEntity<Workspace>,
-        context_store: WeakEntity<ContextStore>,
-    ) -> Self {
-        FetchContextPickerDelegate {
-            context_picker,
-            workspace,
-            context_store,
-            url: String::new(),
-        }
-    }
-}
-
-pub(crate) async fn fetch_url_content(
-    http_client: Arc<HttpClientWithUrl>,
-    url: String,
-) -> Result<String> {
-    let url = if !url.starts_with("https://") && !url.starts_with("http://") {
-        format!("https://{url}")
-    } else {
-        url
-    };
-
-    let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
-
-    let mut body = Vec::new();
-    response
-        .body_mut()
-        .read_to_end(&mut body)
-        .await
-        .context("error reading response body")?;
-
-    if response.status().is_client_error() {
-        let text = String::from_utf8_lossy(body.as_slice());
-        bail!(
-            "status error {}, response: {text:?}",
-            response.status().as_u16()
-        );
-    }
-
-    let Some(content_type) = response.headers().get("content-type") else {
-        bail!("missing Content-Type header");
-    };
-    let content_type = content_type
-        .to_str()
-        .context("invalid Content-Type header")?;
-    let content_type = match content_type {
-        "text/html" => ContentType::Html,
-        "text/plain" => ContentType::Plaintext,
-        "application/json" => ContentType::Json,
-        _ => ContentType::Html,
-    };
-
-    match content_type {
-        ContentType::Html => {
-            let mut handlers: Vec<TagHandler> = vec![
-                Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
-                Rc::new(RefCell::new(markdown::ParagraphHandler)),
-                Rc::new(RefCell::new(markdown::HeadingHandler)),
-                Rc::new(RefCell::new(markdown::ListHandler)),
-                Rc::new(RefCell::new(markdown::TableHandler::new())),
-                Rc::new(RefCell::new(markdown::StyledTextHandler)),
-            ];
-            if url.contains("wikipedia.org") {
-                use html_to_markdown::structure::wikipedia;
-
-                handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
-                handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
-                handlers.push(Rc::new(
-                    RefCell::new(wikipedia::WikipediaCodeHandler::new()),
-                ));
-            } else {
-                handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
-            }
-
-            convert_html_to_markdown(&body[..], &mut handlers)
-        }
-        ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
-        ContentType::Json => {
-            let json: serde_json::Value = serde_json::from_slice(&body)?;
-
-            Ok(format!(
-                "```json\n{}\n```",
-                serde_json::to_string_pretty(&json)?
-            ))
-        }
-    }
-}
-
-impl PickerDelegate for FetchContextPickerDelegate {
-    type ListItem = ListItem;
-
-    fn match_count(&self) -> usize {
-        if self.url.is_empty() { 0 } else { 1 }
-    }
-
-    fn no_matches_text(&self, _window: &mut Window, _cx: &mut App) -> Option<SharedString> {
-        Some("Enter the URL that you would like to fetch".into())
-    }
-
-    fn selected_index(&self) -> usize {
-        0
-    }
-
-    fn set_selected_index(
-        &mut self,
-        _ix: usize,
-        _window: &mut Window,
-        _cx: &mut Context<Picker<Self>>,
-    ) {
-    }
-
-    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
-        "Enter a URL…".into()
-    }
-
-    fn update_matches(
-        &mut self,
-        query: String,
-        _window: &mut Window,
-        _cx: &mut Context<Picker<Self>>,
-    ) -> Task<()> {
-        self.url = query;
-
-        Task::ready(())
-    }
-
-    fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        let Some(workspace) = self.workspace.upgrade() else {
-            return;
-        };
-
-        let http_client = workspace.read(cx).client().http_client();
-        let url = self.url.clone();
-        cx.spawn_in(window, async move |this, cx| {
-            let text = cx
-                .background_spawn(fetch_url_content(http_client, url.clone()))
-                .await?;
-
-            this.update(cx, |this, cx| {
-                this.delegate.context_store.update(cx, |context_store, cx| {
-                    context_store.add_fetched_url(url, text, cx)
-                })
-            })??;
-
-            anyhow::Ok(())
-        })
-        .detach_and_log_err(cx);
-    }
-
-    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        self.context_picker
-            .update(cx, |_, cx| {
-                cx.emit(DismissEvent);
-            })
-            .ok();
-    }
-
-    fn render_match(
-        &self,
-        ix: usize,
-        selected: bool,
-        _window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Option<Self::ListItem> {
-        let added = self
-            .context_store
-            .upgrade()
-            .is_some_and(|context_store| context_store.read(cx).includes_url(&self.url));
-
-        Some(
-            ListItem::new(ix)
-                .inset(true)
-                .toggle_state(selected)
-                .child(Label::new(self.url.clone()))
-                .when(added, |child| {
-                    child.disabled(true).end_slot(
-                        h_flex()
-                            .gap_1()
-                            .child(
-                                Icon::new(IconName::Check)
-                                    .size(IconSize::Small)
-                                    .color(Color::Success),
-                            )
-                            .child(Label::new("Added").size(LabelSize::Small)),
-                    )
-                }),
-        )
-    }
-}

crates/agent_ui/src/context_picker/file_context_picker.rs πŸ”—

@@ -1,392 +0,0 @@
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-
-use file_icons::FileIcons;
-use fuzzy::PathMatch;
-use gpui::{
-    App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
-};
-use picker::{Picker, PickerDelegate};
-use project::{PathMatchCandidateSet, ProjectPath, WorktreeId};
-use ui::{ListItem, Tooltip, prelude::*};
-use util::{ResultExt as _, paths::PathStyle, rel_path::RelPath};
-use workspace::Workspace;
-
-use crate::{
-    context_picker::ContextPicker,
-    context_store::{ContextStore, FileInclusion},
-};
-
-pub struct FileContextPicker {
-    picker: Entity<Picker<FileContextPickerDelegate>>,
-}
-
-impl FileContextPicker {
-    pub fn new(
-        context_picker: WeakEntity<ContextPicker>,
-        workspace: WeakEntity<Workspace>,
-        context_store: WeakEntity<ContextStore>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let delegate = FileContextPickerDelegate::new(context_picker, workspace, context_store);
-        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
-
-        Self { picker }
-    }
-}
-
-impl Focusable for FileContextPicker {
-    fn focus_handle(&self, cx: &App) -> FocusHandle {
-        self.picker.focus_handle(cx)
-    }
-}
-
-impl Render for FileContextPicker {
-    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-        self.picker.clone()
-    }
-}
-
-pub struct FileContextPickerDelegate {
-    context_picker: WeakEntity<ContextPicker>,
-    workspace: WeakEntity<Workspace>,
-    context_store: WeakEntity<ContextStore>,
-    matches: Vec<FileMatch>,
-    selected_index: usize,
-}
-
-impl FileContextPickerDelegate {
-    pub fn new(
-        context_picker: WeakEntity<ContextPicker>,
-        workspace: WeakEntity<Workspace>,
-        context_store: WeakEntity<ContextStore>,
-    ) -> Self {
-        Self {
-            context_picker,
-            workspace,
-            context_store,
-            matches: Vec::new(),
-            selected_index: 0,
-        }
-    }
-}
-
-impl PickerDelegate for FileContextPickerDelegate {
-    type ListItem = ListItem;
-
-    fn match_count(&self) -> usize {
-        self.matches.len()
-    }
-
-    fn selected_index(&self) -> usize {
-        self.selected_index
-    }
-
-    fn set_selected_index(
-        &mut self,
-        ix: usize,
-        _window: &mut Window,
-        _cx: &mut Context<Picker<Self>>,
-    ) {
-        self.selected_index = ix;
-    }
-
-    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
-        "Search files & directories…".into()
-    }
-
-    fn update_matches(
-        &mut self,
-        query: String,
-        window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Task<()> {
-        let Some(workspace) = self.workspace.upgrade() else {
-            return Task::ready(());
-        };
-
-        let search_task = search_files(query, Arc::<AtomicBool>::default(), &workspace, cx);
-
-        cx.spawn_in(window, async move |this, cx| {
-            // TODO: This should be probably be run in the background.
-            let paths = search_task.await;
-
-            this.update(cx, |this, _cx| {
-                this.delegate.matches = paths;
-            })
-            .log_err();
-        })
-    }
-
-    fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        let Some(FileMatch { mat, .. }) = self.matches.get(self.selected_index) else {
-            return;
-        };
-
-        let project_path = ProjectPath {
-            worktree_id: WorktreeId::from_usize(mat.worktree_id),
-            path: mat.path.clone(),
-        };
-
-        let is_directory = mat.is_dir;
-
-        self.context_store
-            .update(cx, |context_store, cx| {
-                if is_directory {
-                    context_store
-                        .add_directory(&project_path, true, cx)
-                        .log_err();
-                } else {
-                    context_store
-                        .add_file_from_path(project_path.clone(), true, cx)
-                        .detach_and_log_err(cx);
-                }
-            })
-            .ok();
-    }
-
-    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
-        self.context_picker
-            .update(cx, |_, cx| {
-                cx.emit(DismissEvent);
-            })
-            .ok();
-    }
-
-    fn render_match(
-        &self,
-        ix: usize,
-        selected: bool,
-        _window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Option<Self::ListItem> {
-        let FileMatch { mat, .. } = &self.matches.get(ix)?;
-        let workspace = self.workspace.upgrade()?;
-        let path_style = workspace.read(cx).path_style(cx);
-
-        Some(
-            ListItem::new(ix)
-                .inset(true)
-                .toggle_state(selected)
-                .child(render_file_context_entry(
-                    ElementId::named_usize("file-ctx-picker", ix),
-                    WorktreeId::from_usize(mat.worktree_id),
-                    &mat.path,
-                    &mat.path_prefix,
-                    mat.is_dir,
-                    path_style,
-                    self.context_store.clone(),
-                    cx,
-                )),
-        )
-    }
-}
-
-pub struct FileMatch {
-    pub mat: PathMatch,
-    pub is_recent: bool,
-}
-
-pub(crate) fn search_files(
-    query: String,
-    cancellation_flag: Arc<AtomicBool>,
-    workspace: &Entity<Workspace>,
-    cx: &App,
-) -> Task<Vec<FileMatch>> {
-    if query.is_empty() {
-        let workspace = workspace.read(cx);
-        let project = workspace.project().read(cx);
-        let visible_worktrees = workspace.visible_worktrees(cx).collect::<Vec<_>>();
-        let include_root_name = visible_worktrees.len() > 1;
-
-        let recent_matches = workspace
-            .recent_navigation_history(Some(10), cx)
-            .into_iter()
-            .map(|(project_path, _)| {
-                let path_prefix = if include_root_name {
-                    project
-                        .worktree_for_id(project_path.worktree_id, cx)
-                        .map(|wt| wt.read(cx).root_name().into())
-                        .unwrap_or_else(|| RelPath::empty().into())
-                } else {
-                    RelPath::empty().into()
-                };
-
-                FileMatch {
-                    mat: PathMatch {
-                        score: 0.,
-                        positions: Vec::new(),
-                        worktree_id: project_path.worktree_id.to_usize(),
-                        path: project_path.path,
-                        path_prefix,
-                        distance_to_relative_ancestor: 0,
-                        is_dir: false,
-                    },
-                    is_recent: true,
-                }
-            });
-
-        let file_matches = visible_worktrees.into_iter().flat_map(|worktree| {
-            let worktree = worktree.read(cx);
-            let path_prefix: Arc<RelPath> = if include_root_name {
-                worktree.root_name().into()
-            } else {
-                RelPath::empty().into()
-            };
-            worktree.entries(false, 0).map(move |entry| FileMatch {
-                mat: PathMatch {
-                    score: 0.,
-                    positions: Vec::new(),
-                    worktree_id: worktree.id().to_usize(),
-                    path: entry.path.clone(),
-                    path_prefix: path_prefix.clone(),
-                    distance_to_relative_ancestor: 0,
-                    is_dir: entry.is_dir(),
-                },
-                is_recent: false,
-            })
-        });
-
-        Task::ready(recent_matches.chain(file_matches).collect())
-    } else {
-        let worktrees = workspace.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
-        let include_root_name = worktrees.len() > 1;
-        let candidate_sets = worktrees
-            .into_iter()
-            .map(|worktree| {
-                let worktree = worktree.read(cx);
-
-                PathMatchCandidateSet {
-                    snapshot: worktree.snapshot(),
-                    include_ignored: worktree.root_entry().is_some_and(|entry| entry.is_ignored),
-                    include_root_name,
-                    candidates: project::Candidates::Entries,
-                }
-            })
-            .collect::<Vec<_>>();
-
-        let executor = cx.background_executor().clone();
-        cx.foreground_executor().spawn(async move {
-            fuzzy::match_path_sets(
-                candidate_sets.as_slice(),
-                query.as_str(),
-                &None,
-                false,
-                100,
-                &cancellation_flag,
-                executor,
-            )
-            .await
-            .into_iter()
-            .map(|mat| FileMatch {
-                mat,
-                is_recent: false,
-            })
-            .collect::<Vec<_>>()
-        })
-    }
-}
-
-pub fn extract_file_name_and_directory(
-    path: &RelPath,
-    path_prefix: &RelPath,
-    path_style: PathStyle,
-) -> (SharedString, Option<SharedString>) {
-    // If path is empty, this means we're matching with the root directory itself
-    // so we use the path_prefix as the name
-    if path.is_empty() && !path_prefix.is_empty() {
-        return (path_prefix.display(path_style).to_string().into(), None);
-    }
-
-    let full_path = path_prefix.join(path);
-    let file_name = full_path.file_name().unwrap_or_default();
-    let display_path = full_path.display(path_style);
-    let (directory, file_name) = display_path.split_at(display_path.len() - file_name.len());
-    (
-        file_name.to_string().into(),
-        Some(SharedString::new(directory)).filter(|dir| !dir.is_empty()),
-    )
-}
-
-pub fn render_file_context_entry(
-    id: ElementId,
-    worktree_id: WorktreeId,
-    path: &Arc<RelPath>,
-    path_prefix: &Arc<RelPath>,
-    is_directory: bool,
-    path_style: PathStyle,
-    context_store: WeakEntity<ContextStore>,
-    cx: &App,
-) -> Stateful<Div> {
-    let (file_name, directory) = extract_file_name_and_directory(path, path_prefix, path_style);
-
-    let added = context_store.upgrade().and_then(|context_store| {
-        let project_path = ProjectPath {
-            worktree_id,
-            path: path.clone(),
-        };
-        if is_directory {
-            context_store
-                .read(cx)
-                .path_included_in_directory(&project_path, cx)
-        } else {
-            context_store.read(cx).file_path_included(&project_path, cx)
-        }
-    });
-
-    let file_icon = if is_directory {
-        FileIcons::get_folder_icon(false, path.as_std_path(), cx)
-    } else {
-        FileIcons::get_icon(path.as_std_path(), cx)
-    }
-    .map(Icon::from_path)
-    .unwrap_or_else(|| Icon::new(IconName::File));
-
-    h_flex()
-        .id(id)
-        .gap_1p5()
-        .w_full()
-        .child(file_icon.size(IconSize::Small).color(Color::Muted))
-        .child(
-            h_flex()
-                .gap_1()
-                .child(Label::new(file_name))
-                .children(directory.map(|directory| {
-                    Label::new(directory)
-                        .size(LabelSize::Small)
-                        .color(Color::Muted)
-                })),
-        )
-        .when_some(added, |el, added| match added {
-            FileInclusion::Direct => el.child(
-                h_flex()
-                    .w_full()
-                    .justify_end()
-                    .gap_0p5()
-                    .child(
-                        Icon::new(IconName::Check)
-                            .size(IconSize::Small)
-                            .color(Color::Success),
-                    )
-                    .child(Label::new("Added").size(LabelSize::Small)),
-            ),
-            FileInclusion::InDirectory { full_path } => {
-                let directory_full_path = full_path.to_string_lossy().into_owned();
-
-                el.child(
-                    h_flex()
-                        .w_full()
-                        .justify_end()
-                        .gap_0p5()
-                        .child(
-                            Icon::new(IconName::Check)
-                                .size(IconSize::Small)
-                                .color(Color::Success),
-                        )
-                        .child(Label::new("Included").size(LabelSize::Small)),
-                )
-                .tooltip(Tooltip::text(format!("in {directory_full_path}")))
-            }
-        })
-}

crates/agent_ui/src/context_picker/rules_context_picker.rs πŸ”—

@@ -1,224 +0,0 @@
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-
-use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
-use picker::{Picker, PickerDelegate};
-use prompt_store::{PromptId, PromptStore, UserPromptId};
-use ui::{ListItem, prelude::*};
-use util::ResultExt as _;
-
-use crate::{
-    context::RULES_ICON,
-    context_picker::ContextPicker,
-    context_store::{self, ContextStore},
-};
-
-pub struct RulesContextPicker {
-    picker: Entity<Picker<RulesContextPickerDelegate>>,
-}
-
-impl RulesContextPicker {
-    pub fn new(
-        prompt_store: WeakEntity<PromptStore>,
-        context_picker: WeakEntity<ContextPicker>,
-        context_store: WeakEntity<context_store::ContextStore>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let delegate = RulesContextPickerDelegate::new(prompt_store, context_picker, context_store);
-        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
-
-        RulesContextPicker { picker }
-    }
-}
-
-impl Focusable for RulesContextPicker {
-    fn focus_handle(&self, cx: &App) -> FocusHandle {
-        self.picker.focus_handle(cx)
-    }
-}
-
-impl Render for RulesContextPicker {
-    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-        self.picker.clone()
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct RulesContextEntry {
-    pub prompt_id: UserPromptId,
-    pub title: SharedString,
-}
-
-pub struct RulesContextPickerDelegate {
-    prompt_store: WeakEntity<PromptStore>,
-    context_picker: WeakEntity<ContextPicker>,
-    context_store: WeakEntity<context_store::ContextStore>,
-    matches: Vec<RulesContextEntry>,
-    selected_index: usize,
-}
-
-impl RulesContextPickerDelegate {
-    pub fn new(
-        prompt_store: WeakEntity<PromptStore>,
-        context_picker: WeakEntity<ContextPicker>,
-        context_store: WeakEntity<context_store::ContextStore>,
-    ) -> Self {
-        RulesContextPickerDelegate {
-            prompt_store,
-            context_picker,
-            context_store,
-            matches: Vec::new(),
-            selected_index: 0,
-        }
-    }
-}
-
-impl PickerDelegate for RulesContextPickerDelegate {
-    type ListItem = ListItem;
-
-    fn match_count(&self) -> usize {
-        self.matches.len()
-    }
-
-    fn selected_index(&self) -> usize {
-        self.selected_index
-    }
-
-    fn set_selected_index(
-        &mut self,
-        ix: usize,
-        _window: &mut Window,
-        _cx: &mut Context<Picker<Self>>,
-    ) {
-        self.selected_index = ix;
-    }
-
-    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
-        "Search available rules…".into()
-    }
-
-    fn update_matches(
-        &mut self,
-        query: String,
-        window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Task<()> {
-        let Some(prompt_store) = self.prompt_store.upgrade() else {
-            return Task::ready(());
-        };
-        let search_task = search_rules(query, Arc::new(AtomicBool::default()), &prompt_store, cx);
-        cx.spawn_in(window, async move |this, cx| {
-            let matches = search_task.await;
-            this.update(cx, |this, cx| {
-                this.delegate.matches = matches;
-                this.delegate.selected_index = 0;
-                cx.notify();
-            })
-            .ok();
-        })
-    }
-
-    fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        let Some(entry) = self.matches.get(self.selected_index) else {
-            return;
-        };
-
-        self.context_store
-            .update(cx, |context_store, cx| {
-                context_store.add_rules(entry.prompt_id, true, cx)
-            })
-            .log_err();
-    }
-
-    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        self.context_picker
-            .update(cx, |_, cx| {
-                cx.emit(DismissEvent);
-            })
-            .ok();
-    }
-
-    fn render_match(
-        &self,
-        ix: usize,
-        selected: bool,
-        _window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Option<Self::ListItem> {
-        let thread = &self.matches.get(ix)?;
-
-        Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
-            render_thread_context_entry(thread, self.context_store.clone(), cx),
-        ))
-    }
-}
-
-pub fn render_thread_context_entry(
-    user_rules: &RulesContextEntry,
-    context_store: WeakEntity<ContextStore>,
-    cx: &mut App,
-) -> Div {
-    let added = context_store.upgrade().is_some_and(|context_store| {
-        context_store
-            .read(cx)
-            .includes_user_rules(user_rules.prompt_id)
-    });
-
-    h_flex()
-        .gap_1p5()
-        .w_full()
-        .justify_between()
-        .child(
-            h_flex()
-                .gap_1p5()
-                .max_w_72()
-                .child(
-                    Icon::new(RULES_ICON)
-                        .size(IconSize::XSmall)
-                        .color(Color::Muted),
-                )
-                .child(Label::new(user_rules.title.clone()).truncate()),
-        )
-        .when(added, |el| {
-            el.child(
-                h_flex()
-                    .gap_1()
-                    .child(
-                        Icon::new(IconName::Check)
-                            .size(IconSize::Small)
-                            .color(Color::Success),
-                    )
-                    .child(Label::new("Added").size(LabelSize::Small)),
-            )
-        })
-}
-
-pub(crate) fn search_rules(
-    query: String,
-    cancellation_flag: Arc<AtomicBool>,
-    prompt_store: &Entity<PromptStore>,
-    cx: &mut App,
-) -> Task<Vec<RulesContextEntry>> {
-    let search_task = prompt_store.read(cx).search(query, cancellation_flag, cx);
-    cx.background_spawn(async move {
-        search_task
-            .await
-            .into_iter()
-            .flat_map(|metadata| {
-                // Default prompts are filtered out as they are automatically included.
-                if metadata.default {
-                    None
-                } else {
-                    match metadata.id {
-                        PromptId::EditWorkflow => None,
-                        PromptId::User { uuid } => Some(RulesContextEntry {
-                            prompt_id: uuid,
-                            title: metadata.title?,
-                        }),
-                    }
-                }
-            })
-            .collect::<Vec<_>>()
-    })
-}

crates/agent_ui/src/context_picker/symbol_context_picker.rs πŸ”—

@@ -1,415 +0,0 @@
-use std::cmp::Reverse;
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-
-use anyhow::{Result, anyhow};
-use fuzzy::{StringMatch, StringMatchCandidate};
-use gpui::{
-    App, AppContext, DismissEvent, Entity, FocusHandle, Focusable, Stateful, Task, WeakEntity,
-};
-use ordered_float::OrderedFloat;
-use picker::{Picker, PickerDelegate};
-use project::lsp_store::SymbolLocation;
-use project::{DocumentSymbol, Symbol};
-use ui::{ListItem, prelude::*};
-use util::ResultExt as _;
-use workspace::Workspace;
-
-use crate::{
-    context::AgentContextHandle, context_picker::ContextPicker, context_store::ContextStore,
-};
-
-pub struct SymbolContextPicker {
-    picker: Entity<Picker<SymbolContextPickerDelegate>>,
-}
-
-impl SymbolContextPicker {
-    pub fn new(
-        context_picker: WeakEntity<ContextPicker>,
-        workspace: WeakEntity<Workspace>,
-        context_store: WeakEntity<ContextStore>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let delegate = SymbolContextPickerDelegate::new(context_picker, workspace, context_store);
-        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
-
-        Self { picker }
-    }
-}
-
-impl Focusable for SymbolContextPicker {
-    fn focus_handle(&self, cx: &App) -> FocusHandle {
-        self.picker.focus_handle(cx)
-    }
-}
-
-impl Render for SymbolContextPicker {
-    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-        self.picker.clone()
-    }
-}
-
-pub struct SymbolContextPickerDelegate {
-    context_picker: WeakEntity<ContextPicker>,
-    workspace: WeakEntity<Workspace>,
-    context_store: WeakEntity<ContextStore>,
-    matches: Vec<SymbolEntry>,
-    selected_index: usize,
-}
-
-impl SymbolContextPickerDelegate {
-    pub fn new(
-        context_picker: WeakEntity<ContextPicker>,
-        workspace: WeakEntity<Workspace>,
-        context_store: WeakEntity<ContextStore>,
-    ) -> Self {
-        Self {
-            context_picker,
-            workspace,
-            context_store,
-            matches: Vec::new(),
-            selected_index: 0,
-        }
-    }
-}
-
-impl PickerDelegate for SymbolContextPickerDelegate {
-    type ListItem = ListItem;
-
-    fn match_count(&self) -> usize {
-        self.matches.len()
-    }
-
-    fn selected_index(&self) -> usize {
-        self.selected_index
-    }
-
-    fn set_selected_index(
-        &mut self,
-        ix: usize,
-        _window: &mut Window,
-        _cx: &mut Context<Picker<Self>>,
-    ) {
-        self.selected_index = ix;
-    }
-
-    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
-        "Search symbols…".into()
-    }
-
-    fn update_matches(
-        &mut self,
-        query: String,
-        window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Task<()> {
-        let Some(workspace) = self.workspace.upgrade() else {
-            return Task::ready(());
-        };
-
-        let search_task = search_symbols(query, Arc::<AtomicBool>::default(), &workspace, cx);
-        let context_store = self.context_store.clone();
-        cx.spawn_in(window, async move |this, cx| {
-            let symbols = search_task.await;
-
-            let symbol_entries = context_store
-                .read_with(cx, |context_store, cx| {
-                    compute_symbol_entries(symbols, context_store, cx)
-                })
-                .log_err()
-                .unwrap_or_default();
-
-            this.update(cx, |this, _cx| {
-                this.delegate.matches = symbol_entries;
-            })
-            .log_err();
-        })
-    }
-
-    fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        let Some(mat) = self.matches.get(self.selected_index) else {
-            return;
-        };
-        let Some(workspace) = self.workspace.upgrade() else {
-            return;
-        };
-
-        let add_symbol_task = add_symbol(
-            mat.symbol.clone(),
-            true,
-            workspace,
-            self.context_store.clone(),
-            cx,
-        );
-
-        let selected_index = self.selected_index;
-        cx.spawn(async move |this, cx| {
-            let (_, included) = add_symbol_task.await?;
-            this.update(cx, |this, _| {
-                if let Some(mat) = this.delegate.matches.get_mut(selected_index) {
-                    mat.is_included = included;
-                }
-            })
-        })
-        .detach_and_log_err(cx);
-    }
-
-    fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
-        self.context_picker
-            .update(cx, |_, cx| {
-                cx.emit(DismissEvent);
-            })
-            .ok();
-    }
-
-    fn render_match(
-        &self,
-        ix: usize,
-        selected: bool,
-        _window: &mut Window,
-        _: &mut Context<Picker<Self>>,
-    ) -> Option<Self::ListItem> {
-        let mat = &self.matches.get(ix)?;
-
-        Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
-            render_symbol_context_entry(ElementId::named_usize("symbol-ctx-picker", ix), mat),
-        ))
-    }
-}
-
-pub(crate) struct SymbolEntry {
-    pub symbol: Symbol,
-    pub is_included: bool,
-}
-
-pub(crate) fn add_symbol(
-    symbol: Symbol,
-    remove_if_exists: bool,
-    workspace: Entity<Workspace>,
-    context_store: WeakEntity<ContextStore>,
-    cx: &mut App,
-) -> Task<Result<(Option<AgentContextHandle>, bool)>> {
-    let project = workspace.read(cx).project().clone();
-    let open_buffer_task = project.update(cx, |project, cx| {
-        let SymbolLocation::InProject(symbol_path) = &symbol.path else {
-            return Task::ready(Err(anyhow!("can't add symbol from outside of project")));
-        };
-        project.open_buffer(symbol_path.clone(), cx)
-    });
-    cx.spawn(async move |cx| {
-        let buffer = open_buffer_task.await?;
-        let document_symbols = project
-            .update(cx, |project, cx| project.document_symbols(&buffer, cx))?
-            .await?;
-
-        // Try to find a matching document symbol. Document symbols include
-        // not only the symbol itself (e.g. function name), but they also
-        // include the context that they contain (e.g. function body).
-        let (name, range, enclosing_range) = if let Some(DocumentSymbol {
-            name,
-            range,
-            selection_range,
-            ..
-        }) =
-            find_matching_symbol(&symbol, document_symbols.as_slice())
-        {
-            (name, selection_range, range)
-        } else {
-            // If we do not find a matching document symbol, fall back to
-            // just the symbol itself
-            (symbol.name, symbol.range.clone(), symbol.range)
-        };
-
-        let (range, enclosing_range) = buffer.read_with(cx, |buffer, _| {
-            (
-                buffer.anchor_after(range.start)..buffer.anchor_before(range.end),
-                buffer.anchor_after(enclosing_range.start)
-                    ..buffer.anchor_before(enclosing_range.end),
-            )
-        })?;
-
-        context_store.update(cx, move |context_store, cx| {
-            context_store.add_symbol(
-                buffer,
-                name.into(),
-                range,
-                enclosing_range,
-                remove_if_exists,
-                cx,
-            )
-        })
-    })
-}
-
-fn find_matching_symbol(symbol: &Symbol, candidates: &[DocumentSymbol]) -> Option<DocumentSymbol> {
-    let mut candidates = candidates.iter();
-    let mut candidate = candidates.next()?;
-
-    loop {
-        if candidate.range.start > symbol.range.end {
-            return None;
-        }
-        if candidate.range.end < symbol.range.start {
-            candidate = candidates.next()?;
-            continue;
-        }
-        if candidate.selection_range == symbol.range {
-            return Some(candidate.clone());
-        }
-        if candidate.range.start <= symbol.range.start && symbol.range.end <= candidate.range.end {
-            candidates = candidate.children.iter();
-            candidate = candidates.next()?;
-            continue;
-        }
-        return None;
-    }
-}
-
-pub struct SymbolMatch {
-    pub symbol: Symbol,
-}
-
-pub(crate) fn search_symbols(
-    query: String,
-    cancellation_flag: Arc<AtomicBool>,
-    workspace: &Entity<Workspace>,
-    cx: &mut App,
-) -> Task<Vec<SymbolMatch>> {
-    let symbols_task = workspace.update(cx, |workspace, cx| {
-        workspace
-            .project()
-            .update(cx, |project, cx| project.symbols(&query, cx))
-    });
-    let project = workspace.read(cx).project().clone();
-    cx.spawn(async move |cx| {
-        let Some(symbols) = symbols_task.await.log_err() else {
-            return Vec::new();
-        };
-        let Some((visible_match_candidates, external_match_candidates)): Option<(Vec<_>, Vec<_>)> =
-            project
-                .update(cx, |project, cx| {
-                    symbols
-                        .iter()
-                        .enumerate()
-                        .map(|(id, symbol)| {
-                            StringMatchCandidate::new(id, symbol.label.filter_text())
-                        })
-                        .partition(|candidate| match &symbols[candidate.id].path {
-                            SymbolLocation::InProject(project_path) => project
-                                .entry_for_path(project_path, cx)
-                                .is_some_and(|e| !e.is_ignored),
-                            SymbolLocation::OutsideProject { .. } => false,
-                        })
-                })
-                .log_err()
-        else {
-            return Vec::new();
-        };
-
-        const MAX_MATCHES: usize = 100;
-        let mut visible_matches = cx.background_executor().block(fuzzy::match_strings(
-            &visible_match_candidates,
-            &query,
-            false,
-            true,
-            MAX_MATCHES,
-            &cancellation_flag,
-            cx.background_executor().clone(),
-        ));
-        let mut external_matches = cx.background_executor().block(fuzzy::match_strings(
-            &external_match_candidates,
-            &query,
-            false,
-            true,
-            MAX_MATCHES - visible_matches.len().min(MAX_MATCHES),
-            &cancellation_flag,
-            cx.background_executor().clone(),
-        ));
-        let sort_key_for_match = |mat: &StringMatch| {
-            let symbol = &symbols[mat.candidate_id];
-            (Reverse(OrderedFloat(mat.score)), symbol.label.filter_text())
-        };
-
-        visible_matches.sort_unstable_by_key(sort_key_for_match);
-        external_matches.sort_unstable_by_key(sort_key_for_match);
-        let mut matches = visible_matches;
-        matches.append(&mut external_matches);
-
-        matches
-            .into_iter()
-            .map(|mut mat| {
-                let symbol = symbols[mat.candidate_id].clone();
-                let filter_start = symbol.label.filter_range.start;
-                for position in &mut mat.positions {
-                    *position += filter_start;
-                }
-                SymbolMatch { symbol }
-            })
-            .collect()
-    })
-}
-
-fn compute_symbol_entries(
-    symbols: Vec<SymbolMatch>,
-    context_store: &ContextStore,
-    cx: &App,
-) -> Vec<SymbolEntry> {
-    symbols
-        .into_iter()
-        .map(|SymbolMatch { symbol, .. }| SymbolEntry {
-            is_included: context_store.includes_symbol(&symbol, cx),
-            symbol,
-        })
-        .collect::<Vec<_>>()
-}
-
-pub fn render_symbol_context_entry(id: ElementId, entry: &SymbolEntry) -> Stateful<Div> {
-    let path = match &entry.symbol.path {
-        SymbolLocation::InProject(project_path) => {
-            project_path.path.file_name().unwrap_or_default().into()
-        }
-        SymbolLocation::OutsideProject {
-            abs_path,
-            signature: _,
-        } => abs_path
-            .file_name()
-            .map(|f| f.to_string_lossy())
-            .unwrap_or_default(),
-    };
-    let symbol_location = format!("{} L{}", path, entry.symbol.range.start.0.row + 1);
-
-    h_flex()
-        .id(id)
-        .gap_1p5()
-        .w_full()
-        .child(
-            Icon::new(IconName::Code)
-                .size(IconSize::Small)
-                .color(Color::Muted),
-        )
-        .child(
-            h_flex()
-                .gap_1()
-                .child(Label::new(&entry.symbol.name))
-                .child(
-                    Label::new(symbol_location)
-                        .size(LabelSize::Small)
-                        .color(Color::Muted),
-                ),
-        )
-        .when(entry.is_included, |el| {
-            el.child(
-                h_flex()
-                    .w_full()
-                    .justify_end()
-                    .gap_0p5()
-                    .child(
-                        Icon::new(IconName::Check)
-                            .size(IconSize::Small)
-                            .color(Color::Success),
-                    )
-                    .child(Label::new("Added").size(LabelSize::Small)),
-            )
-        })
-}

crates/agent_ui/src/context_picker/thread_context_picker.rs πŸ”—

@@ -1,280 +0,0 @@
-use std::sync::Arc;
-use std::sync::atomic::AtomicBool;
-
-use crate::{
-    context_picker::ContextPicker,
-    context_store::{self, ContextStore},
-};
-use agent::{HistoryEntry, HistoryStore};
-use fuzzy::StringMatchCandidate;
-use gpui::{App, DismissEvent, Entity, FocusHandle, Focusable, Task, WeakEntity};
-use picker::{Picker, PickerDelegate};
-use ui::{ListItem, prelude::*};
-use workspace::Workspace;
-
-pub struct ThreadContextPicker {
-    picker: Entity<Picker<ThreadContextPickerDelegate>>,
-}
-
-impl ThreadContextPicker {
-    pub fn new(
-        thread_store: WeakEntity<HistoryStore>,
-        context_picker: WeakEntity<ContextPicker>,
-        context_store: WeakEntity<context_store::ContextStore>,
-        workspace: WeakEntity<Workspace>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let delegate = ThreadContextPickerDelegate::new(
-            thread_store,
-            context_picker,
-            context_store,
-            workspace,
-        );
-        let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
-
-        ThreadContextPicker { picker }
-    }
-}
-
-impl Focusable for ThreadContextPicker {
-    fn focus_handle(&self, cx: &App) -> FocusHandle {
-        self.picker.focus_handle(cx)
-    }
-}
-
-impl Render for ThreadContextPicker {
-    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
-        self.picker.clone()
-    }
-}
-
-pub struct ThreadContextPickerDelegate {
-    thread_store: WeakEntity<HistoryStore>,
-    context_picker: WeakEntity<ContextPicker>,
-    context_store: WeakEntity<context_store::ContextStore>,
-    workspace: WeakEntity<Workspace>,
-    matches: Vec<HistoryEntry>,
-    selected_index: usize,
-}
-
-impl ThreadContextPickerDelegate {
-    pub fn new(
-        thread_store: WeakEntity<HistoryStore>,
-        context_picker: WeakEntity<ContextPicker>,
-        context_store: WeakEntity<context_store::ContextStore>,
-        workspace: WeakEntity<Workspace>,
-    ) -> Self {
-        ThreadContextPickerDelegate {
-            thread_store,
-            context_picker,
-            context_store,
-            workspace,
-            matches: Vec::new(),
-            selected_index: 0,
-        }
-    }
-}
-
-impl PickerDelegate for ThreadContextPickerDelegate {
-    type ListItem = ListItem;
-
-    fn match_count(&self) -> usize {
-        self.matches.len()
-    }
-
-    fn selected_index(&self) -> usize {
-        self.selected_index
-    }
-
-    fn set_selected_index(
-        &mut self,
-        ix: usize,
-        _window: &mut Window,
-        _cx: &mut Context<Picker<Self>>,
-    ) {
-        self.selected_index = ix;
-    }
-
-    fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
-        "Search threads…".into()
-    }
-
-    fn update_matches(
-        &mut self,
-        query: String,
-        window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Task<()> {
-        let Some(thread_store) = self.thread_store.upgrade() else {
-            return Task::ready(());
-        };
-
-        let search_task = search_threads(query, Arc::new(AtomicBool::default()), &thread_store, cx);
-        cx.spawn_in(window, async move |this, cx| {
-            let matches = search_task.await;
-            this.update(cx, |this, cx| {
-                this.delegate.matches = matches;
-                this.delegate.selected_index = 0;
-                cx.notify();
-            })
-            .ok();
-        })
-    }
-
-    fn confirm(&mut self, _secondary: bool, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        let Some(project) = self
-            .workspace
-            .upgrade()
-            .map(|w| w.read(cx).project().clone())
-        else {
-            return;
-        };
-        let Some((entry, thread_store)) = self
-            .matches
-            .get(self.selected_index)
-            .zip(self.thread_store.upgrade())
-        else {
-            return;
-        };
-
-        match entry {
-            HistoryEntry::AcpThread(thread) => {
-                let load_thread_task =
-                    agent::load_agent_thread(thread.id.clone(), thread_store, project, cx);
-
-                cx.spawn(async move |this, cx| {
-                    let thread = load_thread_task.await?;
-                    this.update(cx, |this, cx| {
-                        this.delegate
-                            .context_store
-                            .update(cx, |context_store, cx| {
-                                context_store.add_thread(thread, true, cx)
-                            })
-                            .ok();
-                    })
-                })
-                .detach_and_log_err(cx);
-            }
-            HistoryEntry::TextThread(thread) => {
-                let task = thread_store.update(cx, |this, cx| {
-                    this.load_text_thread(thread.path.clone(), cx)
-                });
-
-                cx.spawn(async move |this, cx| {
-                    let thread = task.await?;
-                    this.update(cx, |this, cx| {
-                        this.delegate
-                            .context_store
-                            .update(cx, |context_store, cx| {
-                                context_store.add_text_thread(thread, true, cx)
-                            })
-                            .ok();
-                    })
-                })
-                .detach_and_log_err(cx);
-            }
-        }
-    }
-
-    fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        self.context_picker
-            .update(cx, |_, cx| {
-                cx.emit(DismissEvent);
-            })
-            .ok();
-    }
-
-    fn render_match(
-        &self,
-        ix: usize,
-        selected: bool,
-        _window: &mut Window,
-        cx: &mut Context<Picker<Self>>,
-    ) -> Option<Self::ListItem> {
-        let thread = &self.matches.get(ix)?;
-
-        Some(ListItem::new(ix).inset(true).toggle_state(selected).child(
-            render_thread_context_entry(thread, self.context_store.clone(), cx),
-        ))
-    }
-}
-
-pub fn render_thread_context_entry(
-    entry: &HistoryEntry,
-    context_store: WeakEntity<ContextStore>,
-    cx: &mut App,
-) -> Div {
-    let is_added = match entry {
-        HistoryEntry::AcpThread(thread) => context_store
-            .upgrade()
-            .is_some_and(|ctx_store| ctx_store.read(cx).includes_thread(&thread.id)),
-        HistoryEntry::TextThread(thread) => context_store
-            .upgrade()
-            .is_some_and(|ctx_store| ctx_store.read(cx).includes_text_thread(&thread.path)),
-    };
-
-    h_flex()
-        .gap_1p5()
-        .w_full()
-        .justify_between()
-        .child(
-            h_flex()
-                .gap_1p5()
-                .max_w_72()
-                .child(
-                    Icon::new(IconName::Thread)
-                        .size(IconSize::XSmall)
-                        .color(Color::Muted),
-                )
-                .child(Label::new(entry.title().clone()).truncate()),
-        )
-        .when(is_added, |el| {
-            el.child(
-                h_flex()
-                    .gap_1()
-                    .child(
-                        Icon::new(IconName::Check)
-                            .size(IconSize::Small)
-                            .color(Color::Success),
-                    )
-                    .child(Label::new("Added").size(LabelSize::Small)),
-            )
-        })
-}
-
-pub(crate) fn search_threads(
-    query: String,
-    cancellation_flag: Arc<AtomicBool>,
-    thread_store: &Entity<HistoryStore>,
-    cx: &mut App,
-) -> Task<Vec<HistoryEntry>> {
-    let threads = thread_store.read(cx).entries().collect();
-    if query.is_empty() {
-        return Task::ready(threads);
-    }
-
-    let executor = cx.background_executor().clone();
-    cx.background_spawn(async move {
-        let candidates = threads
-            .iter()
-            .enumerate()
-            .map(|(id, thread)| StringMatchCandidate::new(id, thread.title()))
-            .collect::<Vec<_>>();
-        let matches = fuzzy::match_strings(
-            &candidates,
-            &query,
-            false,
-            true,
-            100,
-            &cancellation_flag,
-            executor,
-        )
-        .await;
-
-        matches
-            .into_iter()
-            .map(|mat| threads[mat.candidate_id].clone())
-            .collect()
-    })
-}

crates/agent_ui/src/context_store.rs πŸ”—

@@ -1,614 +0,0 @@
-use crate::context::{
-    AgentContextHandle, AgentContextKey, ContextId, ContextKind, DirectoryContextHandle,
-    FetchedUrlContext, FileContextHandle, ImageContext, RulesContextHandle, SelectionContextHandle,
-    SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
-};
-use agent_client_protocol as acp;
-use anyhow::{Context as _, Result, anyhow};
-use assistant_text_thread::TextThread;
-use collections::{HashSet, IndexSet};
-use futures::{self, FutureExt};
-use gpui::{App, Context, Entity, EventEmitter, Image, SharedString, Task, WeakEntity};
-use language::{Buffer, File as _};
-use language_model::LanguageModelImage;
-use project::{
-    Project, ProjectItem, ProjectPath, Symbol, image_store::is_image_file,
-    lsp_store::SymbolLocation,
-};
-use prompt_store::UserPromptId;
-use ref_cast::RefCast as _;
-use std::{
-    ops::Range,
-    path::{Path, PathBuf},
-    sync::Arc,
-};
-use text::{Anchor, OffsetRangeExt};
-
-pub struct ContextStore {
-    project: WeakEntity<Project>,
-    next_context_id: ContextId,
-    context_set: IndexSet<AgentContextKey>,
-    context_thread_ids: HashSet<acp::SessionId>,
-    context_text_thread_paths: HashSet<Arc<Path>>,
-}
-
-pub enum ContextStoreEvent {
-    ContextRemoved(AgentContextKey),
-}
-
-impl EventEmitter<ContextStoreEvent> for ContextStore {}
-
-impl ContextStore {
-    pub fn new(project: WeakEntity<Project>) -> Self {
-        Self {
-            project,
-            next_context_id: ContextId::zero(),
-            context_set: IndexSet::default(),
-            context_thread_ids: HashSet::default(),
-            context_text_thread_paths: HashSet::default(),
-        }
-    }
-
-    pub fn context(&self) -> impl Iterator<Item = &AgentContextHandle> {
-        self.context_set.iter().map(|entry| entry.as_ref())
-    }
-
-    pub fn clear(&mut self, cx: &mut Context<Self>) {
-        self.context_set.clear();
-        self.context_thread_ids.clear();
-        cx.notify();
-    }
-
-    pub fn add_file_from_path(
-        &mut self,
-        project_path: ProjectPath,
-        remove_if_exists: bool,
-        cx: &mut Context<Self>,
-    ) -> Task<Result<Option<AgentContextHandle>>> {
-        let Some(project) = self.project.upgrade() else {
-            return Task::ready(Err(anyhow!("failed to read project")));
-        };
-
-        if is_image_file(&project, &project_path, cx) {
-            self.add_image_from_path(project_path, remove_if_exists, cx)
-        } else {
-            cx.spawn(async move |this, cx| {
-                let open_buffer_task = project.update(cx, |project, cx| {
-                    project.open_buffer(project_path.clone(), cx)
-                })?;
-                let buffer = open_buffer_task.await?;
-                this.update(cx, |this, cx| {
-                    this.add_file_from_buffer(&project_path, buffer, remove_if_exists, cx)
-                })
-            })
-        }
-    }
-
-    pub fn add_file_from_buffer(
-        &mut self,
-        project_path: &ProjectPath,
-        buffer: Entity<Buffer>,
-        remove_if_exists: bool,
-        cx: &mut Context<Self>,
-    ) -> Option<AgentContextHandle> {
-        let context_id = self.next_context_id.post_inc();
-        let context = AgentContextHandle::File(FileContextHandle { buffer, context_id });
-
-        if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
-            if remove_if_exists {
-                self.remove_context(&context, cx);
-                None
-            } else {
-                Some(key.as_ref().clone())
-            }
-        } else if self.path_included_in_directory(project_path, cx).is_some() {
-            None
-        } else {
-            self.insert_context(context.clone(), cx);
-            Some(context)
-        }
-    }
-
-    pub fn add_directory(
-        &mut self,
-        project_path: &ProjectPath,
-        remove_if_exists: bool,
-        cx: &mut Context<Self>,
-    ) -> Result<Option<AgentContextHandle>> {
-        let project = self.project.upgrade().context("failed to read project")?;
-        let entry_id = project
-            .read(cx)
-            .entry_for_path(project_path, cx)
-            .map(|entry| entry.id)
-            .context("no entry found for directory context")?;
-
-        let context_id = self.next_context_id.post_inc();
-        let context = AgentContextHandle::Directory(DirectoryContextHandle {
-            entry_id,
-            context_id,
-        });
-
-        let context =
-            if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
-                if remove_if_exists {
-                    self.remove_context(&context, cx);
-                    None
-                } else {
-                    Some(existing.as_ref().clone())
-                }
-            } else {
-                self.insert_context(context.clone(), cx);
-                Some(context)
-            };
-
-        anyhow::Ok(context)
-    }
-
-    pub fn add_symbol(
-        &mut self,
-        buffer: Entity<Buffer>,
-        symbol: SharedString,
-        range: Range<Anchor>,
-        enclosing_range: Range<Anchor>,
-        remove_if_exists: bool,
-        cx: &mut Context<Self>,
-    ) -> (Option<AgentContextHandle>, bool) {
-        let context_id = self.next_context_id.post_inc();
-        let context = AgentContextHandle::Symbol(SymbolContextHandle {
-            buffer,
-            symbol,
-            range,
-            enclosing_range,
-            context_id,
-        });
-
-        if let Some(key) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
-            let handle = if remove_if_exists {
-                self.remove_context(&context, cx);
-                None
-            } else {
-                Some(key.as_ref().clone())
-            };
-            return (handle, false);
-        }
-
-        let included = self.insert_context(context.clone(), cx);
-        (Some(context), included)
-    }
-
-    pub fn add_thread(
-        &mut self,
-        thread: Entity<agent::Thread>,
-        remove_if_exists: bool,
-        cx: &mut Context<Self>,
-    ) -> Option<AgentContextHandle> {
-        let context_id = self.next_context_id.post_inc();
-        let context = AgentContextHandle::Thread(ThreadContextHandle { thread, context_id });
-
-        if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
-            if remove_if_exists {
-                self.remove_context(&context, cx);
-                None
-            } else {
-                Some(existing.as_ref().clone())
-            }
-        } else {
-            self.insert_context(context.clone(), cx);
-            Some(context)
-        }
-    }
-
-    pub fn add_text_thread(
-        &mut self,
-        text_thread: Entity<TextThread>,
-        remove_if_exists: bool,
-        cx: &mut Context<Self>,
-    ) -> Option<AgentContextHandle> {
-        let context_id = self.next_context_id.post_inc();
-        let context = AgentContextHandle::TextThread(TextThreadContextHandle {
-            text_thread,
-            context_id,
-        });
-
-        if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
-            if remove_if_exists {
-                self.remove_context(&context, cx);
-                None
-            } else {
-                Some(existing.as_ref().clone())
-            }
-        } else {
-            self.insert_context(context.clone(), cx);
-            Some(context)
-        }
-    }
-
-    pub fn add_rules(
-        &mut self,
-        prompt_id: UserPromptId,
-        remove_if_exists: bool,
-        cx: &mut Context<ContextStore>,
-    ) -> Option<AgentContextHandle> {
-        let context_id = self.next_context_id.post_inc();
-        let context = AgentContextHandle::Rules(RulesContextHandle {
-            prompt_id,
-            context_id,
-        });
-
-        if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
-            if remove_if_exists {
-                self.remove_context(&context, cx);
-                None
-            } else {
-                Some(existing.as_ref().clone())
-            }
-        } else {
-            self.insert_context(context.clone(), cx);
-            Some(context)
-        }
-    }
-
-    pub fn add_fetched_url(
-        &mut self,
-        url: String,
-        text: impl Into<SharedString>,
-        cx: &mut Context<ContextStore>,
-    ) -> AgentContextHandle {
-        let context = AgentContextHandle::FetchedUrl(FetchedUrlContext {
-            url: url.into(),
-            text: text.into(),
-            context_id: self.next_context_id.post_inc(),
-        });
-
-        self.insert_context(context.clone(), cx);
-        context
-    }
-
-    pub fn add_image_from_path(
-        &mut self,
-        project_path: ProjectPath,
-        remove_if_exists: bool,
-        cx: &mut Context<ContextStore>,
-    ) -> Task<Result<Option<AgentContextHandle>>> {
-        let project = self.project.clone();
-        cx.spawn(async move |this, cx| {
-            let open_image_task = project.update(cx, |project, cx| {
-                project.open_image(project_path.clone(), cx)
-            })?;
-            let image_item = open_image_task.await?;
-
-            this.update(cx, |this, cx| {
-                let item = image_item.read(cx);
-                this.insert_image(
-                    Some(item.project_path(cx)),
-                    Some(item.file.full_path(cx).to_string_lossy().into_owned()),
-                    item.image.clone(),
-                    remove_if_exists,
-                    cx,
-                )
-            })
-        })
-    }
-
-    pub fn add_image_instance(&mut self, image: Arc<Image>, cx: &mut Context<ContextStore>) {
-        self.insert_image(None, None, image, false, cx);
-    }
-
-    fn insert_image(
-        &mut self,
-        project_path: Option<ProjectPath>,
-        full_path: Option<String>,
-        image: Arc<Image>,
-        remove_if_exists: bool,
-        cx: &mut Context<ContextStore>,
-    ) -> Option<AgentContextHandle> {
-        let image_task = LanguageModelImage::from_image(image.clone(), cx).shared();
-        let context = AgentContextHandle::Image(ImageContext {
-            project_path,
-            full_path,
-            original_image: image,
-            image_task,
-            context_id: self.next_context_id.post_inc(),
-        });
-        if self.has_context(&context) && remove_if_exists {
-            self.remove_context(&context, cx);
-            return None;
-        }
-
-        self.insert_context(context.clone(), cx);
-        Some(context)
-    }
-
-    pub fn add_selection(
-        &mut self,
-        buffer: Entity<Buffer>,
-        range: Range<Anchor>,
-        cx: &mut Context<ContextStore>,
-    ) {
-        let context_id = self.next_context_id.post_inc();
-        let context = AgentContextHandle::Selection(SelectionContextHandle {
-            buffer,
-            range,
-            context_id,
-        });
-        self.insert_context(context, cx);
-    }
-
-    pub fn add_suggested_context(
-        &mut self,
-        suggested: &SuggestedContext,
-        cx: &mut Context<ContextStore>,
-    ) {
-        match suggested {
-            SuggestedContext::File {
-                buffer,
-                icon_path: _,
-                name: _,
-            } => {
-                if let Some(buffer) = buffer.upgrade() {
-                    let context_id = self.next_context_id.post_inc();
-                    self.insert_context(
-                        AgentContextHandle::File(FileContextHandle { buffer, context_id }),
-                        cx,
-                    );
-                };
-            }
-            SuggestedContext::TextThread {
-                text_thread,
-                name: _,
-            } => {
-                if let Some(text_thread) = text_thread.upgrade() {
-                    let context_id = self.next_context_id.post_inc();
-                    self.insert_context(
-                        AgentContextHandle::TextThread(TextThreadContextHandle {
-                            text_thread,
-                            context_id,
-                        }),
-                        cx,
-                    );
-                }
-            }
-        }
-    }
-
-    fn insert_context(&mut self, context: AgentContextHandle, cx: &mut Context<Self>) -> bool {
-        match &context {
-            // AgentContextHandle::Thread(thread_context) => {
-            //     if let Some(thread_store) = self.thread_store.clone() {
-            //         thread_context.thread.update(cx, |thread, cx| {
-            //             thread.start_generating_detailed_summary_if_needed(thread_store, cx);
-            //         });
-            //         self.context_thread_ids
-            //             .insert(thread_context.thread.read(cx).id().clone());
-            //     } else {
-            //         return false;
-            //     }
-            // }
-            AgentContextHandle::TextThread(text_thread_context) => {
-                self.context_text_thread_paths
-                    .extend(text_thread_context.text_thread.read(cx).path().cloned());
-            }
-            _ => {}
-        }
-        let inserted = self.context_set.insert(AgentContextKey(context));
-        if inserted {
-            cx.notify();
-        }
-        inserted
-    }
-
-    pub fn remove_context(&mut self, context: &AgentContextHandle, cx: &mut Context<Self>) {
-        if let Some((_, key)) = self
-            .context_set
-            .shift_remove_full(AgentContextKey::ref_cast(context))
-        {
-            match context {
-                AgentContextHandle::Thread(thread_context) => {
-                    self.context_thread_ids
-                        .remove(thread_context.thread.read(cx).id());
-                }
-                AgentContextHandle::TextThread(text_thread_context) => {
-                    if let Some(path) = text_thread_context.text_thread.read(cx).path() {
-                        self.context_text_thread_paths.remove(path);
-                    }
-                }
-                _ => {}
-            }
-            cx.emit(ContextStoreEvent::ContextRemoved(key));
-            cx.notify();
-        }
-    }
-
-    pub fn has_context(&mut self, context: &AgentContextHandle) -> bool {
-        self.context_set
-            .contains(AgentContextKey::ref_cast(context))
-    }
-
-    /// Returns whether this file path is already included directly in the context, or if it will be
-    /// included in the context via a directory.
-    pub fn file_path_included(&self, path: &ProjectPath, cx: &App) -> Option<FileInclusion> {
-        let project = self.project.upgrade()?.read(cx);
-        self.context().find_map(|context| match context {
-            AgentContextHandle::File(file_context) => {
-                FileInclusion::check_file(file_context, path, cx)
-            }
-            AgentContextHandle::Image(image_context) => {
-                FileInclusion::check_image(image_context, path)
-            }
-            AgentContextHandle::Directory(directory_context) => {
-                FileInclusion::check_directory(directory_context, path, project, cx)
-            }
-            _ => None,
-        })
-    }
-
-    pub fn path_included_in_directory(
-        &self,
-        path: &ProjectPath,
-        cx: &App,
-    ) -> Option<FileInclusion> {
-        let project = self.project.upgrade()?.read(cx);
-        self.context().find_map(|context| match context {
-            AgentContextHandle::Directory(directory_context) => {
-                FileInclusion::check_directory(directory_context, path, project, cx)
-            }
-            _ => None,
-        })
-    }
-
-    pub fn includes_symbol(&self, symbol: &Symbol, cx: &App) -> bool {
-        self.context().any(|context| match context {
-            AgentContextHandle::Symbol(context) => {
-                if context.symbol != symbol.name {
-                    return false;
-                }
-                let buffer = context.buffer.read(cx);
-                let Some(context_path) = buffer.project_path(cx) else {
-                    return false;
-                };
-                if symbol.path != SymbolLocation::InProject(context_path) {
-                    return false;
-                }
-                let context_range = context.range.to_point_utf16(&buffer.snapshot());
-                context_range.start == symbol.range.start.0
-                    && context_range.end == symbol.range.end.0
-            }
-            _ => false,
-        })
-    }
-
-    pub fn includes_thread(&self, thread_id: &acp::SessionId) -> bool {
-        self.context_thread_ids.contains(thread_id)
-    }
-
-    pub fn includes_text_thread(&self, path: &Arc<Path>) -> bool {
-        self.context_text_thread_paths.contains(path)
-    }
-
-    pub fn includes_user_rules(&self, prompt_id: UserPromptId) -> bool {
-        self.context_set
-            .contains(&RulesContextHandle::lookup_key(prompt_id))
-    }
-
-    pub fn includes_url(&self, url: impl Into<SharedString>) -> bool {
-        self.context_set
-            .contains(&FetchedUrlContext::lookup_key(url.into()))
-    }
-
-    pub fn get_url_context(&self, url: SharedString) -> Option<AgentContextHandle> {
-        self.context_set
-            .get(&FetchedUrlContext::lookup_key(url))
-            .map(|key| key.as_ref().clone())
-    }
-
-    pub fn file_paths(&self, cx: &App) -> HashSet<ProjectPath> {
-        self.context()
-            .filter_map(|context| match context {
-                AgentContextHandle::File(file) => {
-                    let buffer = file.buffer.read(cx);
-                    buffer.project_path(cx)
-                }
-                AgentContextHandle::Directory(_)
-                | AgentContextHandle::Symbol(_)
-                | AgentContextHandle::Thread(_)
-                | AgentContextHandle::Selection(_)
-                | AgentContextHandle::FetchedUrl(_)
-                | AgentContextHandle::TextThread(_)
-                | AgentContextHandle::Rules(_)
-                | AgentContextHandle::Image(_) => None,
-            })
-            .collect()
-    }
-
-    pub fn thread_ids(&self) -> &HashSet<acp::SessionId> {
-        &self.context_thread_ids
-    }
-}
-
-#[derive(Clone)]
-pub enum SuggestedContext {
-    File {
-        name: SharedString,
-        icon_path: Option<SharedString>,
-        buffer: WeakEntity<Buffer>,
-    },
-    TextThread {
-        name: SharedString,
-        text_thread: WeakEntity<TextThread>,
-    },
-}
-
-impl SuggestedContext {
-    pub fn name(&self) -> &SharedString {
-        match self {
-            Self::File { name, .. } => name,
-            Self::TextThread { name, .. } => name,
-        }
-    }
-
-    pub fn icon_path(&self) -> Option<SharedString> {
-        match self {
-            Self::File { icon_path, .. } => icon_path.clone(),
-            Self::TextThread { .. } => None,
-        }
-    }
-
-    pub fn kind(&self) -> ContextKind {
-        match self {
-            Self::File { .. } => ContextKind::File,
-            Self::TextThread { .. } => ContextKind::TextThread,
-        }
-    }
-}
-
-pub enum FileInclusion {
-    Direct,
-    InDirectory { full_path: PathBuf },
-}
-
-impl FileInclusion {
-    fn check_file(file_context: &FileContextHandle, path: &ProjectPath, cx: &App) -> Option<Self> {
-        let file_path = file_context.buffer.read(cx).project_path(cx)?;
-        if path == &file_path {
-            Some(FileInclusion::Direct)
-        } else {
-            None
-        }
-    }
-
-    fn check_image(image_context: &ImageContext, path: &ProjectPath) -> Option<Self> {
-        let image_path = image_context.project_path.as_ref()?;
-        if path == image_path {
-            Some(FileInclusion::Direct)
-        } else {
-            None
-        }
-    }
-
-    fn check_directory(
-        directory_context: &DirectoryContextHandle,
-        path: &ProjectPath,
-        project: &Project,
-        cx: &App,
-    ) -> Option<Self> {
-        let worktree = project
-            .worktree_for_entry(directory_context.entry_id, cx)?
-            .read(cx);
-        let entry = worktree.entry_for_id(directory_context.entry_id)?;
-        let directory_path = ProjectPath {
-            worktree_id: worktree.id(),
-            path: entry.path.clone(),
-        };
-        if path.starts_with(&directory_path) {
-            if path == &directory_path {
-                Some(FileInclusion::Direct)
-            } else {
-                Some(FileInclusion::InDirectory {
-                    full_path: worktree.full_path(&entry.path),
-                })
-            }
-        } else {
-            None
-        }
-    }
-}

crates/agent_ui/src/context_strip.rs πŸ”—

@@ -1,619 +0,0 @@
-use crate::{
-    AcceptSuggestedContext, AgentPanel, FocusDown, FocusLeft, FocusRight, FocusUp,
-    ModelUsageContext, RemoveAllContext, RemoveFocusedContext, ToggleContextPicker,
-    context_picker::ContextPicker,
-    ui::{AddedContext, ContextPill},
-};
-use crate::{
-    context::AgentContextHandle,
-    context_store::{ContextStore, SuggestedContext},
-};
-use agent::HistoryStore;
-use collections::HashSet;
-use editor::Editor;
-use gpui::{
-    App, Bounds, ClickEvent, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable,
-    Subscription, Task, WeakEntity,
-};
-use itertools::Itertools;
-use project::ProjectItem;
-use prompt_store::PromptStore;
-use rope::Point;
-use std::rc::Rc;
-use text::ToPoint as _;
-use ui::{PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
-use util::ResultExt as _;
-use workspace::Workspace;
-use zed_actions::assistant::OpenRulesLibrary;
-
-pub struct ContextStrip {
-    context_store: Entity<ContextStore>,
-    context_picker: Entity<ContextPicker>,
-    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
-    focus_handle: FocusHandle,
-    suggest_context_kind: SuggestContextKind,
-    workspace: WeakEntity<Workspace>,
-    prompt_store: Option<WeakEntity<PromptStore>>,
-    _subscriptions: Vec<Subscription>,
-    focused_index: Option<usize>,
-    children_bounds: Option<Vec<Bounds<Pixels>>>,
-    model_usage_context: ModelUsageContext,
-}
-
-impl ContextStrip {
-    pub fn new(
-        context_store: Entity<ContextStore>,
-        workspace: WeakEntity<Workspace>,
-        thread_store: Option<WeakEntity<HistoryStore>>,
-        prompt_store: Option<WeakEntity<PromptStore>>,
-        context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
-        suggest_context_kind: SuggestContextKind,
-        model_usage_context: ModelUsageContext,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Self {
-        let context_picker = cx.new(|cx| {
-            ContextPicker::new(
-                workspace.clone(),
-                thread_store.clone(),
-                prompt_store.clone(),
-                context_store.downgrade(),
-                window,
-                cx,
-            )
-        });
-
-        let focus_handle = cx.focus_handle();
-
-        let subscriptions = vec![
-            cx.observe(&context_store, |_, _, cx| cx.notify()),
-            cx.subscribe_in(&context_picker, window, Self::handle_context_picker_event),
-            cx.on_focus(&focus_handle, window, Self::handle_focus),
-            cx.on_blur(&focus_handle, window, Self::handle_blur),
-        ];
-
-        Self {
-            context_store: context_store.clone(),
-            context_picker,
-            context_picker_menu_handle,
-            focus_handle,
-            suggest_context_kind,
-            workspace,
-            prompt_store,
-            _subscriptions: subscriptions,
-            focused_index: None,
-            children_bounds: None,
-            model_usage_context,
-        }
-    }
-
-    /// Whether or not the context strip has items to display
-    pub fn has_context_items(&self, cx: &App) -> bool {
-        self.context_store.read(cx).context().next().is_some()
-            || self.suggested_context(cx).is_some()
-    }
-
-    fn added_contexts(&self, cx: &App) -> Vec<AddedContext> {
-        if let Some(workspace) = self.workspace.upgrade() {
-            let project = workspace.read(cx).project().read(cx);
-            let prompt_store = self.prompt_store.as_ref().and_then(|p| p.upgrade());
-
-            let current_model = self.model_usage_context.language_model(cx);
-
-            self.context_store
-                .read(cx)
-                .context()
-                .flat_map(|context| {
-                    AddedContext::new_pending(
-                        context.clone(),
-                        prompt_store.as_ref(),
-                        project,
-                        current_model.as_ref(),
-                        cx,
-                    )
-                })
-                .collect::<Vec<_>>()
-        } else {
-            Vec::new()
-        }
-    }
-
-    fn suggested_context(&self, cx: &App) -> Option<SuggestedContext> {
-        match self.suggest_context_kind {
-            SuggestContextKind::Thread => self.suggested_thread(cx),
-        }
-    }
-
-    fn suggested_thread(&self, cx: &App) -> Option<SuggestedContext> {
-        if !self.context_picker.read(cx).allow_threads() {
-            return None;
-        }
-
-        let workspace = self.workspace.upgrade()?;
-        let panel = workspace.read(cx).panel::<AgentPanel>(cx)?.read(cx);
-
-        if let Some(active_text_thread_editor) = panel.active_text_thread_editor() {
-            let text_thread = active_text_thread_editor.read(cx).text_thread();
-            let weak_text_thread = text_thread.downgrade();
-            let text_thread = text_thread.read(cx);
-            let path = text_thread.path()?;
-
-            if self.context_store.read(cx).includes_text_thread(path) {
-                return None;
-            }
-
-            Some(SuggestedContext::TextThread {
-                name: text_thread.summary().or_default(),
-                text_thread: weak_text_thread,
-            })
-        } else {
-            None
-        }
-    }
-
-    fn handle_context_picker_event(
-        &mut self,
-        _picker: &Entity<ContextPicker>,
-        _event: &DismissEvent,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        cx.emit(ContextStripEvent::PickerDismissed);
-    }
-
-    fn handle_focus(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
-        self.focused_index = self.last_pill_index();
-        cx.notify();
-    }
-
-    fn handle_blur(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
-        self.focused_index = None;
-        cx.notify();
-    }
-
-    fn focus_left(&mut self, _: &FocusLeft, _window: &mut Window, cx: &mut Context<Self>) {
-        self.focused_index = match self.focused_index {
-            Some(index) if index > 0 => Some(index - 1),
-            _ => self.last_pill_index(),
-        };
-
-        cx.notify();
-    }
-
-    fn focus_right(&mut self, _: &FocusRight, _window: &mut Window, cx: &mut Context<Self>) {
-        let Some(last_index) = self.last_pill_index() else {
-            return;
-        };
-
-        self.focused_index = match self.focused_index {
-            Some(index) if index < last_index => Some(index + 1),
-            _ => Some(0),
-        };
-
-        cx.notify();
-    }
-
-    fn focus_up(&mut self, _: &FocusUp, _window: &mut Window, cx: &mut Context<Self>) {
-        let Some(focused_index) = self.focused_index else {
-            return;
-        };
-
-        if focused_index == 0 {
-            return cx.emit(ContextStripEvent::BlurredUp);
-        }
-
-        let Some((focused, pills)) = self.focused_bounds(focused_index) else {
-            return;
-        };
-
-        let iter = pills[..focused_index].iter().enumerate().rev();
-        self.focused_index = Self::find_best_horizontal_match(focused, iter).or(Some(0));
-        cx.notify();
-    }
-
-    fn focus_down(&mut self, _: &FocusDown, _window: &mut Window, cx: &mut Context<Self>) {
-        let Some(focused_index) = self.focused_index else {
-            return;
-        };
-
-        let last_index = self.last_pill_index();
-
-        if self.focused_index == last_index {
-            return cx.emit(ContextStripEvent::BlurredDown);
-        }
-
-        let Some((focused, pills)) = self.focused_bounds(focused_index) else {
-            return;
-        };
-
-        let iter = pills.iter().enumerate().skip(focused_index + 1);
-        self.focused_index = Self::find_best_horizontal_match(focused, iter).or(last_index);
-        cx.notify();
-    }
-
-    fn focused_bounds(&self, focused: usize) -> Option<(&Bounds<Pixels>, &[Bounds<Pixels>])> {
-        let pill_bounds = self.pill_bounds()?;
-        let focused = pill_bounds.get(focused)?;
-
-        Some((focused, pill_bounds))
-    }
-
-    fn pill_bounds(&self) -> Option<&[Bounds<Pixels>]> {
-        let bounds = self.children_bounds.as_ref()?;
-        let eraser = if bounds.len() < 3 { 0 } else { 1 };
-        let pills = &bounds[1..bounds.len() - eraser];
-
-        if pills.is_empty() { None } else { Some(pills) }
-    }
-
-    fn last_pill_index(&self) -> Option<usize> {
-        Some(self.pill_bounds()?.len() - 1)
-    }
-
-    fn find_best_horizontal_match<'a>(
-        focused: &'a Bounds<Pixels>,
-        iter: impl Iterator<Item = (usize, &'a Bounds<Pixels>)>,
-    ) -> Option<usize> {
-        let mut best = None;
-
-        let focused_left = focused.left();
-        let focused_right = focused.right();
-
-        for (index, probe) in iter {
-            if probe.origin.y == focused.origin.y {
-                continue;
-            }
-
-            let overlap = probe.right().min(focused_right) - probe.left().max(focused_left);
-
-            best = match best {
-                Some((_, prev_overlap, y)) if probe.origin.y != y || prev_overlap > overlap => {
-                    break;
-                }
-                Some(_) | None => Some((index, overlap, probe.origin.y)),
-            };
-        }
-
-        best.map(|(index, _, _)| index)
-    }
-
-    fn open_context(&mut self, context: &AgentContextHandle, window: &mut Window, cx: &mut App) {
-        let Some(workspace) = self.workspace.upgrade() else {
-            return;
-        };
-
-        match context {
-            AgentContextHandle::File(file_context) => {
-                if let Some(project_path) = file_context.project_path(cx) {
-                    workspace.update(cx, |workspace, cx| {
-                        workspace
-                            .open_path(project_path, None, true, window, cx)
-                            .detach_and_log_err(cx);
-                    });
-                }
-            }
-
-            AgentContextHandle::Directory(directory_context) => {
-                let entry_id = directory_context.entry_id;
-                workspace.update(cx, |workspace, cx| {
-                    workspace.project().update(cx, |_project, cx| {
-                        cx.emit(project::Event::RevealInProjectPanel(entry_id));
-                    })
-                })
-            }
-
-            AgentContextHandle::Symbol(symbol_context) => {
-                let buffer = symbol_context.buffer.read(cx);
-                if let Some(project_path) = buffer.project_path(cx) {
-                    let snapshot = buffer.snapshot();
-                    let target_position = symbol_context.range.start.to_point(&snapshot);
-                    open_editor_at_position(project_path, target_position, &workspace, window, cx)
-                        .detach();
-                }
-            }
-
-            AgentContextHandle::Selection(selection_context) => {
-                let buffer = selection_context.buffer.read(cx);
-                if let Some(project_path) = buffer.project_path(cx) {
-                    let snapshot = buffer.snapshot();
-                    let target_position = selection_context.range.start.to_point(&snapshot);
-
-                    open_editor_at_position(project_path, target_position, &workspace, window, cx)
-                        .detach();
-                }
-            }
-
-            AgentContextHandle::FetchedUrl(fetched_url_context) => {
-                cx.open_url(&fetched_url_context.url);
-            }
-
-            AgentContextHandle::Thread(_thread_context) => {}
-
-            AgentContextHandle::TextThread(text_thread_context) => {
-                workspace.update(cx, |workspace, cx| {
-                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
-                        let context = text_thread_context.text_thread.clone();
-                        window.defer(cx, move |window, cx| {
-                            panel.update(cx, |panel, cx| {
-                                panel.open_text_thread(context, window, cx)
-                            });
-                        });
-                    }
-                })
-            }
-
-            AgentContextHandle::Rules(rules_context) => window.dispatch_action(
-                Box::new(OpenRulesLibrary {
-                    prompt_to_select: Some(rules_context.prompt_id.0),
-                }),
-                cx,
-            ),
-
-            AgentContextHandle::Image(_) => {}
-        }
-    }
-
-    fn remove_focused_context(
-        &mut self,
-        _: &RemoveFocusedContext,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some(index) = self.focused_index {
-            let added_contexts = self.added_contexts(cx);
-            let Some(context) = added_contexts.get(index) else {
-                return;
-            };
-
-            self.context_store.update(cx, |this, cx| {
-                this.remove_context(&context.handle, cx);
-            });
-
-            let is_now_empty = added_contexts.len() == 1;
-            if is_now_empty {
-                cx.emit(ContextStripEvent::BlurredEmpty);
-            } else {
-                self.focused_index = Some(index.saturating_sub(1));
-                cx.notify();
-            }
-        }
-    }
-
-    fn is_suggested_focused(&self, added_contexts: &Vec<AddedContext>) -> bool {
-        // We only suggest one item after the actual context
-        self.focused_index == Some(added_contexts.len())
-    }
-
-    fn accept_suggested_context(
-        &mut self,
-        _: &AcceptSuggestedContext,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some(suggested) = self.suggested_context(cx)
-            && self.is_suggested_focused(&self.added_contexts(cx))
-        {
-            self.add_suggested_context(&suggested, cx);
-        }
-    }
-
-    fn add_suggested_context(&mut self, suggested: &SuggestedContext, cx: &mut Context<Self>) {
-        self.context_store.update(cx, |context_store, cx| {
-            context_store.add_suggested_context(suggested, cx)
-        });
-        cx.notify();
-    }
-}
-
-impl Focusable for ContextStrip {
-    fn focus_handle(&self, _cx: &App) -> FocusHandle {
-        self.focus_handle.clone()
-    }
-}
-
-impl Render for ContextStrip {
-    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        let context_picker = self.context_picker.clone();
-        let focus_handle = self.focus_handle.clone();
-
-        let added_contexts = self.added_contexts(cx);
-        let dupe_names = added_contexts
-            .iter()
-            .map(|c| c.name.clone())
-            .sorted()
-            .tuple_windows()
-            .filter(|(a, b)| a == b)
-            .map(|(a, _)| a)
-            .collect::<HashSet<SharedString>>();
-        let no_added_context = added_contexts.is_empty();
-
-        let suggested_context = self.suggested_context(cx).map(|suggested_context| {
-            (
-                suggested_context,
-                self.is_suggested_focused(&added_contexts),
-            )
-        });
-
-        h_flex()
-            .flex_wrap()
-            .gap_1()
-            .track_focus(&focus_handle)
-            .key_context("ContextStrip")
-            .on_action(cx.listener(Self::focus_up))
-            .on_action(cx.listener(Self::focus_right))
-            .on_action(cx.listener(Self::focus_down))
-            .on_action(cx.listener(Self::focus_left))
-            .on_action(cx.listener(Self::remove_focused_context))
-            .on_action(cx.listener(Self::accept_suggested_context))
-            .on_children_prepainted({
-                let entity = cx.entity().downgrade();
-                move |children_bounds, _window, cx| {
-                    entity
-                        .update(cx, |this, _| {
-                            this.children_bounds = Some(children_bounds);
-                        })
-                        .ok();
-                }
-            })
-            .child(
-                PopoverMenu::new("context-picker")
-                    .menu({
-                        let context_picker = context_picker.clone();
-                        move |window, cx| {
-                            context_picker.update(cx, |this, cx| {
-                                this.init(window, cx);
-                            });
-
-                            Some(context_picker.clone())
-                        }
-                    })
-                    .on_open({
-                        let context_picker = context_picker.downgrade();
-                        Rc::new(move |window, cx| {
-                            context_picker
-                                .update(cx, |context_picker, cx| {
-                                    context_picker.select_first(window, cx);
-                                })
-                                .ok();
-                        })
-                    })
-                    .trigger_with_tooltip(
-                        IconButton::new("add-context", IconName::Plus)
-                            .icon_size(IconSize::Small)
-                            .style(ui::ButtonStyle::Filled),
-                        {
-                            let focus_handle = focus_handle.clone();
-                            move |_window, cx| {
-                                Tooltip::for_action_in(
-                                    "Add Context",
-                                    &ToggleContextPicker,
-                                    &focus_handle,
-                                    cx,
-                                )
-                            }
-                        },
-                    )
-                    .attach(gpui::Corner::TopLeft)
-                    .anchor(gpui::Corner::BottomLeft)
-                    .offset(gpui::Point {
-                        x: px(0.0),
-                        y: px(-2.0),
-                    })
-                    .with_handle(self.context_picker_menu_handle.clone()),
-            )
-            .children(
-                added_contexts
-                    .into_iter()
-                    .enumerate()
-                    .map(|(i, added_context)| {
-                        let name = added_context.name.clone();
-                        let context = added_context.handle.clone();
-                        ContextPill::added(
-                            added_context,
-                            dupe_names.contains(&name),
-                            self.focused_index == Some(i),
-                            Some({
-                                let context = context.clone();
-                                let context_store = self.context_store.clone();
-                                Rc::new(cx.listener(move |_this, _event, _window, cx| {
-                                    context_store.update(cx, |this, cx| {
-                                        this.remove_context(&context, cx);
-                                    });
-                                    cx.notify();
-                                }))
-                            }),
-                        )
-                        .on_click({
-                            Rc::new(cx.listener(move |this, event: &ClickEvent, window, cx| {
-                                if event.click_count() > 1 {
-                                    this.open_context(&context, window, cx);
-                                } else {
-                                    this.focused_index = Some(i);
-                                }
-                                cx.notify();
-                            }))
-                        })
-                    }),
-            )
-            .when_some(suggested_context, |el, (suggested, focused)| {
-                el.child(
-                    ContextPill::suggested(
-                        suggested.name().clone(),
-                        suggested.icon_path(),
-                        suggested.kind(),
-                        focused,
-                    )
-                    .on_click(Rc::new(cx.listener(
-                        move |this, _event, _window, cx| {
-                            this.add_suggested_context(&suggested, cx);
-                        },
-                    ))),
-                )
-            })
-            .when(!no_added_context, {
-                move |parent| {
-                    parent.child(
-                        IconButton::new("remove-all-context", IconName::Eraser)
-                            .icon_size(IconSize::Small)
-                            .tooltip({
-                                let focus_handle = focus_handle.clone();
-                                move |_window, cx| {
-                                    Tooltip::for_action_in(
-                                        "Remove All Context",
-                                        &RemoveAllContext,
-                                        &focus_handle,
-                                        cx,
-                                    )
-                                }
-                            })
-                            .on_click(cx.listener({
-                                let focus_handle = focus_handle.clone();
-                                move |_this, _event, window, cx| {
-                                    focus_handle.dispatch_action(&RemoveAllContext, window, cx);
-                                }
-                            })),
-                    )
-                }
-            })
-            .into_any()
-    }
-}
-
-pub enum ContextStripEvent {
-    PickerDismissed,
-    BlurredEmpty,
-    BlurredDown,
-    BlurredUp,
-}
-
-impl EventEmitter<ContextStripEvent> for ContextStrip {}
-
-pub enum SuggestContextKind {
-    Thread,
-}
-
-fn open_editor_at_position(
-    project_path: project::ProjectPath,
-    target_position: Point,
-    workspace: &Entity<Workspace>,
-    window: &mut Window,
-    cx: &mut App,
-) -> Task<()> {
-    let open_task = workspace.update(cx, |workspace, cx| {
-        workspace.open_path(project_path, None, true, window, cx)
-    });
-    window.spawn(cx, async move |cx| {
-        if let Some(active_editor) = open_task
-            .await
-            .log_err()
-            .and_then(|item| item.downcast::<Editor>())
-        {
-            active_editor
-                .downgrade()
-                .update_in(cx, |editor, window, cx| {
-                    editor.go_to_singleton_buffer_point(target_position, window, cx);
-                })
-                .log_err();
-        }
-    })
-}

crates/agent_ui/src/inline_assistant.rs πŸ”—

@@ -4,10 +4,11 @@ use std::ops::Range;
 use std::rc::Rc;
 use std::sync::Arc;
 
+use crate::context::load_context;
+use crate::mention_set::MentionSet;
 use crate::{
     AgentPanel,
     buffer_codegen::{BufferCodegen, CodegenAlternative, CodegenEvent},
-    context_store::ContextStore,
     inline_prompt_editor::{CodegenStatus, InlineAssistId, PromptEditor, PromptEditorEvent},
     terminal_inline_assistant::TerminalInlineAssistant,
 };
@@ -31,6 +32,7 @@ use editor::{
     },
 };
 use fs::Fs;
+use futures::FutureExt;
 use gpui::{
     App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
     WeakEntity, Window, point,
@@ -214,16 +216,10 @@ impl InlineAssistant {
         if let Some(editor) = item.act_as::<Editor>(cx) {
             editor.update(cx, |editor, cx| {
                 if is_ai_enabled {
-                    let panel = workspace.read(cx).panel::<AgentPanel>(cx);
-                    let thread_store = panel
-                        .as_ref()
-                        .map(|agent_panel| agent_panel.read(cx).thread_store().downgrade());
-
                     editor.add_code_action_provider(
                         Rc::new(AssistantCodeActionProvider {
                             editor: cx.entity().downgrade(),
                             workspace: workspace.downgrade(),
-                            thread_store,
                         }),
                         window,
                         cx,
@@ -235,9 +231,6 @@ impl InlineAssistant {
                             editor.cancel(&Default::default(), window, cx);
                         }
                     }
-
-                    // Remove the Assistant1 code action provider, as it still might be registered.
-                    editor.remove_code_action_provider("assistant".into(), window, cx);
                 } else {
                     editor.remove_code_action_provider(
                         ASSISTANT_CODE_ACTION_PROVIDER_ID.into(),
@@ -279,8 +272,7 @@ impl InlineAssistant {
         let agent_panel = agent_panel.read(cx);
 
         let prompt_store = agent_panel.prompt_store().as_ref().cloned();
-        let thread_store = Some(agent_panel.thread_store().downgrade());
-        let context_store = agent_panel.inline_assist_context_store().clone();
+        let thread_store = agent_panel.thread_store().clone();
 
         let handle_assist =
             |window: &mut Window, cx: &mut Context<Workspace>| match inline_assist_target {
@@ -289,10 +281,9 @@ impl InlineAssistant {
                         assistant.assist(
                             &active_editor,
                             cx.entity().downgrade(),
-                            context_store,
                             workspace.project().downgrade(),
-                            prompt_store,
                             thread_store,
+                            prompt_store,
                             action.prompt.clone(),
                             window,
                             cx,
@@ -305,8 +296,8 @@ impl InlineAssistant {
                             &active_terminal,
                             cx.entity().downgrade(),
                             workspace.project().downgrade(),
-                            prompt_store,
                             thread_store,
+                            prompt_store,
                             action.prompt.clone(),
                             window,
                             cx,
@@ -477,10 +468,9 @@ impl InlineAssistant {
         &mut self,
         editor: &Entity<Editor>,
         workspace: WeakEntity<Workspace>,
-        context_store: Entity<ContextStore>,
         project: WeakEntity<Project>,
+        thread_store: Entity<HistoryStore>,
         prompt_store: Option<Entity<PromptStore>>,
-        thread_store: Option<WeakEntity<HistoryStore>>,
         initial_prompt: Option<String>,
         window: &mut Window,
         codegen_ranges: &[Range<Anchor>],
@@ -508,9 +498,6 @@ impl InlineAssistant {
                     editor.read(cx).buffer().clone(),
                     range.clone(),
                     initial_transaction_id,
-                    context_store.clone(),
-                    project.clone(),
-                    prompt_store.clone(),
                     self.telemetry.clone(),
                     self.prompt_builder.clone(),
                     cx,
@@ -526,10 +513,10 @@ impl InlineAssistant {
                     prompt_buffer.clone(),
                     codegen.clone(),
                     self.fs.clone(),
-                    context_store.clone(),
-                    workspace.clone(),
                     thread_store.clone(),
-                    prompt_store.as_ref().map(|s| s.downgrade()),
+                    prompt_store.clone(),
+                    project.clone(),
+                    workspace.clone(),
                     window,
                     cx,
                 )
@@ -606,10 +593,9 @@ impl InlineAssistant {
         &mut self,
         editor: &Entity<Editor>,
         workspace: WeakEntity<Workspace>,
-        context_store: Entity<ContextStore>,
         project: WeakEntity<Project>,
+        thread_store: Entity<HistoryStore>,
         prompt_store: Option<Entity<PromptStore>>,
-        thread_store: Option<WeakEntity<HistoryStore>>,
         initial_prompt: Option<String>,
         window: &mut Window,
         cx: &mut App,
@@ -625,10 +611,9 @@ impl InlineAssistant {
         let assist_to_focus = self.batch_assist(
             editor,
             workspace,
-            context_store,
             project,
-            prompt_store,
             thread_store,
+            prompt_store,
             initial_prompt,
             window,
             &codegen_ranges,
@@ -650,8 +635,8 @@ impl InlineAssistant {
         initial_transaction_id: Option<TransactionId>,
         focus: bool,
         workspace: Entity<Workspace>,
+        thread_store: Entity<HistoryStore>,
         prompt_store: Option<Entity<PromptStore>>,
-        thread_store: Option<WeakEntity<HistoryStore>>,
         window: &mut Window,
         cx: &mut App,
     ) -> InlineAssistId {
@@ -663,16 +648,14 @@ impl InlineAssistant {
         }
 
         let project = workspace.read(cx).project().downgrade();
-        let context_store = cx.new(|_cx| ContextStore::new(project.clone()));
 
         let assist_id = self
             .batch_assist(
                 editor,
                 workspace.downgrade(),
-                context_store,
                 project,
-                prompt_store,
                 thread_store,
+                prompt_store,
                 Some(initial_prompt),
                 window,
                 &[range],
@@ -1294,7 +1277,8 @@ impl InlineAssistant {
             return;
         }
 
-        let Some(user_prompt) = assist.user_prompt(cx) else {
+        let Some((user_prompt, mention_set)) = assist.user_prompt(cx).zip(assist.mention_set(cx))
+        else {
             return;
         };
 
@@ -1310,9 +1294,12 @@ impl InlineAssistant {
             return;
         };
 
+        let context_task = load_context(&mention_set, cx).shared();
         assist
             .codegen
-            .update(cx, |codegen, cx| codegen.start(model, user_prompt, cx))
+            .update(cx, |codegen, cx| {
+                codegen.start(model, user_prompt, context_task, cx)
+            })
             .log_err();
     }
 
@@ -1778,6 +1765,11 @@ impl InlineAssist {
         let decorations = self.decorations.as_ref()?;
         Some(decorations.prompt_editor.read(cx).prompt(cx))
     }
+
+    fn mention_set(&self, cx: &App) -> Option<Entity<MentionSet>> {
+        let decorations = self.decorations.as_ref()?;
+        Some(decorations.prompt_editor.read(cx).mention_set().clone())
+    }
 }
 
 struct InlineAssistDecorations {
@@ -1790,10 +1782,9 @@ struct InlineAssistDecorations {
 struct AssistantCodeActionProvider {
     editor: WeakEntity<Editor>,
     workspace: WeakEntity<Workspace>,
-    thread_store: Option<WeakEntity<HistoryStore>>,
 }
 
-const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant2";
+const ASSISTANT_CODE_ACTION_PROVIDER_ID: &str = "assistant";
 
 impl CodeActionProvider for AssistantCodeActionProvider {
     fn id(&self) -> Arc<str> {
@@ -1861,10 +1852,20 @@ impl CodeActionProvider for AssistantCodeActionProvider {
     ) -> Task<Result<ProjectTransaction>> {
         let editor = self.editor.clone();
         let workspace = self.workspace.clone();
-        let thread_store = self.thread_store.clone();
         let prompt_store = PromptStore::global(cx);
         window.spawn(cx, async move |cx| {
             let workspace = workspace.upgrade().context("workspace was released")?;
+            let thread_store = cx.update(|_window, cx| {
+                anyhow::Ok(
+                    workspace
+                        .read(cx)
+                        .panel::<AgentPanel>(cx)
+                        .context("missing agent panel")?
+                        .read(cx)
+                        .thread_store()
+                        .clone(),
+                )
+            })??;
             let editor = editor.upgrade().context("editor was released")?;
             let range = editor
                 .update(cx, |editor, cx| {
@@ -1907,8 +1908,8 @@ impl CodeActionProvider for AssistantCodeActionProvider {
                     None,
                     true,
                     workspace,
-                    prompt_store,
                     thread_store,
+                    prompt_store,
                     window,
                     cx,
                 );

crates/agent_ui/src/inline_prompt_editor.rs πŸ”—

@@ -1,19 +1,21 @@
 use agent::HistoryStore;
-use collections::{HashMap, VecDeque};
+use collections::VecDeque;
 use editor::actions::Paste;
-use editor::display_map::{CreaseId, EditorMargins};
-use editor::{Addon, AnchorRangeExt as _, MultiBufferOffset};
+use editor::code_context_menus::CodeContextMenu;
+use editor::display_map::EditorMargins;
+use editor::{AnchorRangeExt as _, MultiBufferOffset, ToOffset as _};
 use editor::{
     ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
     actions::{MoveDown, MoveUp},
 };
 use fs::Fs;
 use gpui::{
-    AnyElement, App, ClipboardEntry, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
-    Focusable, Subscription, TextStyle, WeakEntity, Window,
+    AnyElement, App, Context, CursorStyle, Entity, EventEmitter, FocusHandle, Focusable,
+    Subscription, TextStyle, WeakEntity, Window,
 };
 use language_model::{LanguageModel, LanguageModelRegistry};
 use parking_lot::Mutex;
+use project::Project;
 use prompt_store::PromptStore;
 use settings::Settings;
 use std::cmp;
@@ -28,22 +30,21 @@ use zed_actions::agent::ToggleModelSelector;
 
 use crate::agent_model_selector::AgentModelSelector;
 use crate::buffer_codegen::BufferCodegen;
-use crate::context::{AgentContextHandle, AgentContextKey};
-use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
-use crate::context_store::{ContextStore, ContextStoreEvent};
-use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
-use crate::terminal_codegen::TerminalCodegen;
-use crate::{
-    CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext, RemoveAllContext,
-    ToggleContextPicker,
+use crate::completion_provider::{
+    PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType,
 };
+use crate::mention_set::paste_images_as_context;
+use crate::mention_set::{MentionSet, crease_for_mention};
+use crate::terminal_codegen::TerminalCodegen;
+use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
 
 pub struct PromptEditor<T> {
     pub editor: Entity<Editor>,
     mode: PromptEditorMode,
-    context_store: Entity<ContextStore>,
-    context_strip: Entity<ContextStrip>,
-    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
+    mention_set: Entity<MentionSet>,
+    history_store: Entity<HistoryStore>,
+    prompt_store: Option<Entity<PromptStore>>,
+    workspace: WeakEntity<Workspace>,
     model_selector: Entity<AgentModelSelector>,
     edited_since_done: bool,
     prompt_history: VecDeque<String>,
@@ -51,7 +52,6 @@ pub struct PromptEditor<T> {
     pending_prompt: String,
     _codegen_subscription: Subscription,
     editor_subscriptions: Vec<Subscription>,
-    _context_strip_subscription: Subscription,
     show_rate_limit_notice: bool,
     _phantom: std::marker::PhantomData<T>,
 }
@@ -98,6 +98,19 @@ impl<T: 'static> Render for PromptEditor<T> {
 
         buttons.extend(self.render_buttons(window, cx));
 
+        let menu_visible = self.is_completions_menu_visible(cx);
+        let add_context_button = IconButton::new("add-context", IconName::AtSign)
+            .icon_size(IconSize::Small)
+            .icon_color(Color::Muted)
+            .when(!menu_visible, |this| {
+                this.tooltip(move |_window, cx| {
+                    Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx)
+                })
+            })
+            .on_click(cx.listener(move |this, _, window, cx| {
+                this.trigger_completion_menu(window, cx);
+            }));
+
         v_flex()
             .key_context("PromptEditor")
             .capture_action(cx.listener(Self::paste))
@@ -114,7 +127,6 @@ impl<T: 'static> Render for PromptEditor<T> {
                 h_flex()
                     .items_start()
                     .cursor(CursorStyle::Arrow)
-                    .on_action(cx.listener(Self::toggle_context_picker))
                     .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
                         this.model_selector
                             .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
@@ -123,7 +135,6 @@ impl<T: 'static> Render for PromptEditor<T> {
                     .on_action(cx.listener(Self::cancel))
                     .on_action(cx.listener(Self::move_up))
                     .on_action(cx.listener(Self::move_down))
-                    .on_action(cx.listener(Self::remove_all_context))
                     .capture_action(cx.listener(Self::cycle_prev))
                     .capture_action(cx.listener(Self::cycle_next))
                     .child(
@@ -182,7 +193,7 @@ impl<T: 'static> Render for PromptEditor<T> {
                             .pl_1()
                             .items_start()
                             .justify_between()
-                            .child(self.context_strip.clone())
+                            .child(add_context_button)
                             .child(self.model_selector.clone()),
                     ),
             )
@@ -214,6 +225,18 @@ impl<T: 'static> PromptEditor<T> {
         ));
     }
 
+    fn assign_completion_provider(&mut self, cx: &mut Context<Self>) {
+        self.editor.update(cx, |editor, _cx| {
+            editor.set_completion_provider(Some(Rc::new(PromptCompletionProvider::new(
+                PromptEditorCompletionProviderDelegate,
+                self.mention_set.clone(),
+                self.history_store.clone(),
+                self.prompt_store.clone(),
+                self.workspace.clone(),
+            ))));
+        });
+    }
+
     pub fn set_show_cursor_when_unfocused(
         &mut self,
         show_cursor_when_unfocused: bool,
@@ -226,27 +249,23 @@ impl<T: 'static> PromptEditor<T> {
 
     pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let prompt = self.prompt(cx);
-        let existing_creases = self.editor.update(cx, extract_message_creases);
-
+        let existing_creases = self.editor.update(cx, |editor, cx| {
+            extract_message_creases(editor, &self.mention_set, window, cx)
+        });
         let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
         self.editor = cx.new(|cx| {
             let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
             editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
             editor.set_placeholder_text("Add a prompt…", window, cx);
             editor.set_text(prompt, window, cx);
-            insert_message_creases(
-                &mut editor,
-                &existing_creases,
-                &self.context_store,
-                window,
-                cx,
-            );
+            insert_message_creases(&mut editor, &existing_creases, window, cx);
 
             if focus {
                 window.focus(&editor.focus_handle(cx));
             }
             editor
         });
+        self.assign_completion_provider(cx);
         self.subscribe_to_editor(window, cx);
     }
 
@@ -274,32 +293,13 @@ impl<T: 'static> PromptEditor<T> {
         self.editor.read(cx).text(cx)
     }
 
-    fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
-        let images = cx
-            .read_from_clipboard()
-            .map(|item| {
-                item.into_entries()
-                    .filter_map(|entry| {
-                        if let ClipboardEntry::Image(image) = entry {
-                            Some(image)
-                        } else {
-                            None
-                        }
-                    })
-                    .collect::<Vec<_>>()
-            })
-            .unwrap_or_default();
-
-        if images.is_empty() {
-            return;
+    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
+        if inline_assistant_model_supports_images(cx)
+            && let Some(task) =
+                paste_images_as_context(self.editor.clone(), self.mention_set.clone(), window, cx)
+        {
+            task.detach();
         }
-        cx.stop_propagation();
-
-        self.context_store.update(cx, |store, cx| {
-            for image in images {
-                store.add_image_instance(Arc::new(image), cx);
-            }
-        });
     }
 
     fn handle_prompt_editor_events(
@@ -343,23 +343,44 @@ impl<T: 'static> PromptEditor<T> {
         }
     }
 
-    fn toggle_context_picker(
-        &mut self,
-        _: &ToggleContextPicker,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.context_picker_menu_handle.toggle(window, cx);
+    pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
+        self.editor
+            .read(cx)
+            .context_menu()
+            .borrow()
+            .as_ref()
+            .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
     }
 
-    pub fn remove_all_context(
-        &mut self,
-        _: &RemoveAllContext,
-        _window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        self.context_store.update(cx, |store, cx| store.clear(cx));
-        cx.notify();
+    pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        self.editor.update(cx, |editor, cx| {
+            let menu_is_open = editor.context_menu().borrow().as_ref().is_some_and(|menu| {
+                matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
+            });
+
+            let has_at_sign = {
+                let snapshot = editor.display_snapshot(cx);
+                let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
+                let offset = cursor.to_offset(&snapshot);
+                if offset.0 > 0 {
+                    snapshot
+                        .buffer_snapshot()
+                        .reversed_chars_at(offset)
+                        .next()
+                        .map(|sign| sign == '@')
+                        .unwrap_or(false)
+                } else {
+                    false
+                }
+            };
+
+            if menu_is_open && has_at_sign {
+                return;
+            }
+
+            editor.insert("@", window, cx);
+            editor.show_completions(&editor::actions::ShowCompletions, window, cx);
+        });
     }
 
     fn cancel(
@@ -434,8 +455,6 @@ impl<T: 'static> PromptEditor<T> {
                     editor.move_to_end(&Default::default(), window, cx)
                 });
             }
-        } else if self.context_strip.read(cx).has_context_items(cx) {
-            self.context_strip.focus_handle(cx).focus(window);
         }
     }
 
@@ -709,6 +728,7 @@ impl<T: 'static> PromptEditor<T> {
                     EditorStyle {
                         background: colors.editor_background,
                         local_player: cx.theme().players().local(),
+                        syntax: cx.theme().syntax().clone(),
                         text: text_style,
                         ..Default::default()
                     },
@@ -716,21 +736,6 @@ impl<T: 'static> PromptEditor<T> {
             })
             .into_any_element()
     }
-
-    fn handle_context_strip_event(
-        &mut self,
-        _context_strip: &Entity<ContextStrip>,
-        event: &ContextStripEvent,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        match event {
-            ContextStripEvent::PickerDismissed
-            | ContextStripEvent::BlurredEmpty
-            | ContextStripEvent::BlurredUp => self.editor.focus_handle(cx).focus(window),
-            ContextStripEvent::BlurredDown => {}
-        }
-    }
 }
 
 pub enum PromptEditorMode {
@@ -765,6 +770,36 @@ impl InlineAssistId {
     }
 }
 
+struct PromptEditorCompletionProviderDelegate;
+
+fn inline_assistant_model_supports_images(cx: &App) -> bool {
+    LanguageModelRegistry::read_global(cx)
+        .inline_assistant_model()
+        .map_or(false, |m| m.model.supports_images())
+}
+
+impl PromptCompletionProviderDelegate for PromptEditorCompletionProviderDelegate {
+    fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {
+        vec![
+            PromptContextType::File,
+            PromptContextType::Symbol,
+            PromptContextType::Thread,
+            PromptContextType::Fetch,
+            PromptContextType::Rules,
+        ]
+    }
+
+    fn supports_images(&self, cx: &App) -> bool {
+        inline_assistant_model_supports_images(cx)
+    }
+
+    fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
+        Vec::new()
+    }
+
+    fn confirm_command(&self, _cx: &mut App) {}
+}
+
 impl PromptEditor<BufferCodegen> {
     pub fn new_buffer(
         id: InlineAssistId,
@@ -773,15 +808,14 @@ impl PromptEditor<BufferCodegen> {
         prompt_buffer: Entity<MultiBuffer>,
         codegen: Entity<BufferCodegen>,
         fs: Arc<dyn Fs>,
-        context_store: Entity<ContextStore>,
+        history_store: Entity<HistoryStore>,
+        prompt_store: Option<Entity<PromptStore>>,
+        project: WeakEntity<Project>,
         workspace: WeakEntity<Workspace>,
-        thread_store: Option<WeakEntity<HistoryStore>>,
-        prompt_store: Option<WeakEntity<PromptStore>>,
         window: &mut Window,
         cx: &mut Context<PromptEditor<BufferCodegen>>,
     ) -> PromptEditor<BufferCodegen> {
         let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
-        let codegen_buffer = codegen.read(cx).buffer(cx).read(cx).as_singleton();
         let mode = PromptEditorMode::Buffer {
             id,
             codegen,
@@ -805,7 +839,6 @@ impl PromptEditor<BufferCodegen> {
             // typing in one will make what you typed appear in all of them.
             editor.set_show_cursor_when_unfocused(true, cx);
             editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
-            editor.register_addon(ContextCreasesAddon::new());
             editor.set_context_menu_options(ContextMenuOptions {
                 min_entries_visible: 12,
                 max_entries_visible: 12,
@@ -815,43 +848,25 @@ impl PromptEditor<BufferCodegen> {
             editor
         });
 
-        let prompt_editor_entity = prompt_editor.downgrade();
-        prompt_editor.update(cx, |editor, _| {
-            editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
-                workspace.clone(),
-                context_store.downgrade(),
-                thread_store.clone(),
+        let mention_set = cx.new(|cx| {
+            MentionSet::new(
+                prompt_editor.clone(),
+                project,
+                history_store.clone(),
                 prompt_store.clone(),
-                prompt_editor_entity,
-                codegen_buffer.as_ref().map(Entity::downgrade),
-            ))));
-        });
-
-        let context_picker_menu_handle = PopoverMenuHandle::default();
-        let model_selector_menu_handle = PopoverMenuHandle::default();
-
-        let context_strip = cx.new(|cx| {
-            ContextStrip::new(
-                context_store.clone(),
-                workspace.clone(),
-                thread_store.clone(),
-                prompt_store,
-                context_picker_menu_handle.clone(),
-                SuggestContextKind::Thread,
-                ModelUsageContext::InlineAssistant,
                 window,
                 cx,
             )
         });
 
-        let context_strip_subscription =
-            cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
+        let model_selector_menu_handle = PopoverMenuHandle::default();
 
         let mut this: PromptEditor<BufferCodegen> = PromptEditor {
             editor: prompt_editor.clone(),
-            context_store,
-            context_strip,
-            context_picker_menu_handle,
+            mention_set,
+            history_store,
+            prompt_store,
+            workspace,
             model_selector: cx.new(|cx| {
                 AgentModelSelector::new(
                     fs,
@@ -868,12 +883,12 @@ impl PromptEditor<BufferCodegen> {
             pending_prompt: String::new(),
             _codegen_subscription: codegen_subscription,
             editor_subscriptions: Vec::new(),
-            _context_strip_subscription: context_strip_subscription,
             show_rate_limit_notice: false,
             mode,
             _phantom: Default::default(),
         };
 
+        this.assign_completion_provider(cx);
         this.subscribe_to_editor(window, cx);
         this
     }
@@ -919,6 +934,10 @@ impl PromptEditor<BufferCodegen> {
         }
     }
 
+    pub fn mention_set(&self) -> &Entity<MentionSet> {
+        &self.mention_set
+    }
+
     pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
         match &self.mode {
             PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
@@ -945,10 +964,10 @@ impl PromptEditor<TerminalCodegen> {
         prompt_buffer: Entity<MultiBuffer>,
         codegen: Entity<TerminalCodegen>,
         fs: Arc<dyn Fs>,
-        context_store: Entity<ContextStore>,
+        history_store: Entity<HistoryStore>,
+        prompt_store: Option<Entity<PromptStore>>,
+        project: WeakEntity<Project>,
         workspace: WeakEntity<Workspace>,
-        thread_store: Option<WeakEntity<HistoryStore>>,
-        prompt_store: Option<WeakEntity<PromptStore>>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
@@ -980,43 +999,25 @@ impl PromptEditor<TerminalCodegen> {
             editor
         });
 
-        let prompt_editor_entity = prompt_editor.downgrade();
-        prompt_editor.update(cx, |editor, _| {
-            editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
-                workspace.clone(),
-                context_store.downgrade(),
-                thread_store.clone(),
+        let mention_set = cx.new(|cx| {
+            MentionSet::new(
+                prompt_editor.clone(),
+                project,
+                history_store.clone(),
                 prompt_store.clone(),
-                prompt_editor_entity,
-                None,
-            ))));
-        });
-
-        let context_picker_menu_handle = PopoverMenuHandle::default();
-        let model_selector_menu_handle = PopoverMenuHandle::default();
-
-        let context_strip = cx.new(|cx| {
-            ContextStrip::new(
-                context_store.clone(),
-                workspace.clone(),
-                thread_store.clone(),
-                prompt_store.clone(),
-                context_picker_menu_handle.clone(),
-                SuggestContextKind::Thread,
-                ModelUsageContext::InlineAssistant,
                 window,
                 cx,
             )
         });
 
-        let context_strip_subscription =
-            cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
+        let model_selector_menu_handle = PopoverMenuHandle::default();
 
         let mut this = Self {
             editor: prompt_editor.clone(),
-            context_store,
-            context_strip,
-            context_picker_menu_handle,
+            mention_set,
+            history_store,
+            prompt_store,
+            workspace,
             model_selector: cx.new(|cx| {
                 AgentModelSelector::new(
                     fs,
@@ -1033,12 +1034,12 @@ impl PromptEditor<TerminalCodegen> {
             pending_prompt: String::new(),
             _codegen_subscription: codegen_subscription,
             editor_subscriptions: Vec::new(),
-            _context_strip_subscription: context_strip_subscription,
             mode,
             show_rate_limit_notice: false,
             _phantom: Default::default(),
         };
         this.count_lines(cx);
+        this.assign_completion_provider(cx);
         this.subscribe_to_editor(window, cx);
         this
     }
@@ -1085,6 +1086,10 @@ impl PromptEditor<TerminalCodegen> {
         }
     }
 
+    pub fn mention_set(&self) -> &Entity<MentionSet> {
+        &self.mention_set
+    }
+
     pub fn codegen(&self) -> &Entity<TerminalCodegen> {
         match &self.mode {
             PromptEditorMode::Buffer { .. } => unreachable!(),
@@ -1164,128 +1169,38 @@ impl GenerationMode {
 
 /// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
 #[derive(Clone, Debug)]
-pub struct MessageCrease {
-    pub range: Range<MultiBufferOffset>,
-    pub icon_path: SharedString,
-    pub label: SharedString,
-    /// None for a deserialized message, Some otherwise.
-    pub context: Option<AgentContextHandle>,
-}
-
-#[derive(Default)]
-pub struct ContextCreasesAddon {
-    creases: HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>>,
-    _subscription: Option<Subscription>,
+struct MessageCrease {
+    range: Range<MultiBufferOffset>,
+    icon_path: SharedString,
+    label: SharedString,
 }
 
-impl Addon for ContextCreasesAddon {
-    fn to_any(&self) -> &dyn std::any::Any {
-        self
-    }
-
-    fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
-        Some(self)
-    }
-}
-
-impl ContextCreasesAddon {
-    pub fn new() -> Self {
-        Self {
-            creases: HashMap::default(),
-            _subscription: None,
-        }
-    }
-
-    pub fn add_creases(
-        &mut self,
-        context_store: &Entity<ContextStore>,
-        key: AgentContextKey,
-        creases: impl IntoIterator<Item = (CreaseId, SharedString)>,
-        cx: &mut Context<Editor>,
-    ) {
-        self.creases.entry(key).or_default().extend(creases);
-        self._subscription = Some(
-            cx.subscribe(context_store, |editor, _, event, cx| match event {
-                ContextStoreEvent::ContextRemoved(key) => {
-                    let Some(this) = editor.addon_mut::<Self>() else {
-                        return;
-                    };
-                    let (crease_ids, replacement_texts): (Vec<_>, Vec<_>) = this
-                        .creases
-                        .remove(key)
-                        .unwrap_or_default()
-                        .into_iter()
-                        .unzip();
-                    let ranges = editor
-                        .remove_creases(crease_ids, cx)
-                        .into_iter()
-                        .map(|(_, range)| range)
-                        .collect::<Vec<_>>();
-                    editor.unfold_ranges(&ranges, false, false, cx);
-                    editor.edit(ranges.into_iter().zip(replacement_texts), cx);
-                    cx.notify();
-                }
-            }),
-        )
-    }
-
-    pub fn into_inner(self) -> HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>> {
-        self.creases
-    }
-}
-
-pub fn extract_message_creases(
+fn extract_message_creases(
     editor: &mut Editor,
+    mention_set: &Entity<MentionSet>,
+    window: &mut Window,
     cx: &mut Context<'_, Editor>,
 ) -> Vec<MessageCrease> {
-    let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
-    let mut contexts_by_crease_id = editor
-        .addon_mut::<ContextCreasesAddon>()
-        .map(std::mem::take)
-        .unwrap_or_default()
-        .into_inner()
-        .into_iter()
-        .flat_map(|(key, creases)| {
-            let context = key.0;
-            creases
-                .into_iter()
-                .map(move |(id, _)| (id, context.clone()))
-        })
-        .collect::<HashMap<_, _>>();
-    // Filter the addon's list of creases based on what the editor reports,
-    // since the addon might have removed creases in it.
-
-    editor.display_map.update(cx, |display_map, cx| {
-        display_map
-            .snapshot(cx)
-            .crease_snapshot
-            .creases()
-            .filter_map(|(id, crease)| {
-                Some((
-                    id,
-                    (
-                        crease.range().to_offset(&buffer_snapshot),
-                        crease.metadata()?.clone(),
-                    ),
-                ))
+    let creases = mention_set.read(cx).creases();
+    let snapshot = editor.snapshot(window, cx);
+    snapshot
+        .crease_snapshot
+        .creases()
+        .filter(|(id, _)| creases.contains(id))
+        .filter_map(|(_, crease)| {
+            let metadata = crease.metadata()?.clone();
+            Some(MessageCrease {
+                range: crease.range().to_offset(snapshot.buffer()),
+                label: metadata.label,
+                icon_path: metadata.icon_path,
             })
-            .map(|(id, (range, metadata))| {
-                let context = contexts_by_crease_id.remove(&id);
-                MessageCrease {
-                    range,
-                    context,
-                    label: metadata.label,
-                    icon_path: metadata.icon_path,
-                }
-            })
-            .collect()
-    })
+        })
+        .collect()
 }
 
-pub fn insert_message_creases(
+fn insert_message_creases(
     editor: &mut Editor,
     message_creases: &[MessageCrease],
-    context_store: &Entity<ContextStore>,
     window: &mut Window,
     cx: &mut Context<'_, Editor>,
 ) {
@@ -1303,14 +1218,6 @@ pub fn insert_message_creases(
             )
         })
         .collect::<Vec<_>>();
-    let ids = editor.insert_creases(creases.clone(), cx);
+    editor.insert_creases(creases.clone(), cx);
     editor.fold_creases(creases, false, window, cx);
-    if let Some(addon) = editor.addon_mut::<ContextCreasesAddon>() {
-        for (crease, id) in message_creases.iter().zip(ids) {
-            if let Some(context) = crease.context.as_ref() {
-                let key = AgentContextKey(context.clone());
-                addon.add_creases(context_store, key, vec![(id, crease.label.clone())], cx);
-            }
-        }
-    }
 }

crates/agent_ui/src/mention_set.rs πŸ”—

@@ -0,0 +1,1112 @@
+use acp_thread::{MentionUri, selection_name};
+use agent::{HistoryStore, outline};
+use agent_client_protocol as acp;
+use agent_servers::{AgentServer, AgentServerDelegate};
+use anyhow::{Context as _, Result, anyhow};
+use assistant_slash_commands::codeblock_fence_for_path;
+use collections::{HashMap, HashSet};
+use editor::{
+    Anchor, Editor, EditorEvent, EditorSnapshot, ExcerptId, FoldPlaceholder, ToOffset,
+    display_map::{Crease, CreaseId, CreaseMetadata, FoldId},
+    scroll::Autoscroll,
+};
+use futures::{AsyncReadExt as _, FutureExt as _, future::Shared};
+use gpui::{
+    Animation, AnimationExt as _, AppContext, ClipboardEntry, Context, Empty, Entity, EntityId,
+    Image, ImageFormat, Img, SharedString, Subscription, Task, WeakEntity, pulsating_between,
+};
+use http_client::{AsyncBody, HttpClientWithUrl};
+use itertools::Either;
+use language::Buffer;
+use language_model::LanguageModelImage;
+use multi_buffer::MultiBufferRow;
+use postage::stream::Stream as _;
+use project::{Project, ProjectItem, ProjectPath, Worktree};
+use prompt_store::{PromptId, PromptStore};
+use rope::Point;
+use std::{
+    cell::RefCell,
+    ffi::OsStr,
+    fmt::Write,
+    ops::{Range, RangeInclusive},
+    path::{Path, PathBuf},
+    rc::Rc,
+    sync::Arc,
+    time::Duration,
+};
+use text::OffsetRangeExt;
+use ui::{ButtonLike, Disclosure, TintColor, Toggleable, prelude::*};
+use util::{ResultExt, debug_panic, rel_path::RelPath};
+use workspace::{Workspace, notifications::NotifyResultExt as _};
+
+pub type MentionTask = Shared<Task<Result<Mention, String>>>;
+
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub enum Mention {
+    Text {
+        content: String,
+        tracked_buffers: Vec<Entity<Buffer>>,
+    },
+    Image(MentionImage),
+    Link,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct MentionImage {
+    pub data: SharedString,
+    pub format: ImageFormat,
+}
+
+pub struct MentionSet {
+    editor: Entity<Editor>,
+    project: WeakEntity<Project>,
+    history_store: Entity<HistoryStore>,
+    prompt_store: Option<Entity<PromptStore>>,
+    mentions: HashMap<CreaseId, (MentionUri, MentionTask)>,
+    _editor_subscription: Subscription,
+}
+
+impl MentionSet {
+    pub fn new(
+        editor: Entity<Editor>,
+        project: WeakEntity<Project>,
+        history_store: Entity<HistoryStore>,
+        prompt_store: Option<Entity<PromptStore>>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let editor_subscription =
+            cx.subscribe_in(&editor, window, move |this, editor, event, window, cx| {
+                if let EditorEvent::Edited { .. } = event
+                    && !editor.read(cx).read_only(cx)
+                {
+                    let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
+                    this.remove_invalid(snapshot);
+                }
+            });
+
+        Self {
+            editor,
+            project,
+            history_store,
+            prompt_store,
+            mentions: HashMap::default(),
+            _editor_subscription: editor_subscription,
+        }
+    }
+
+    pub fn contents(
+        &self,
+        full_mention_content: bool,
+        cx: &mut App,
+    ) -> Task<Result<HashMap<CreaseId, (MentionUri, Mention)>>> {
+        let Some(project) = self.project.upgrade() else {
+            return Task::ready(Err(anyhow!("Project not found")));
+        };
+        let mentions = self.mentions.clone();
+        cx.spawn(async move |cx| {
+            let mut contents = HashMap::default();
+            for (crease_id, (mention_uri, task)) in mentions {
+                let content = if full_mention_content
+                    && let MentionUri::Directory { abs_path } = &mention_uri
+                {
+                    cx.update(|cx| full_mention_for_directory(&project, abs_path, cx))?
+                        .await?
+                } else {
+                    task.await.map_err(|e| anyhow!("{e}"))?
+                };
+
+                contents.insert(crease_id, (mention_uri, content));
+            }
+            Ok(contents)
+        })
+    }
+
+    fn remove_invalid(&mut self, snapshot: EditorSnapshot) {
+        for (crease_id, crease) in snapshot.crease_snapshot.creases() {
+            if !crease.range().start.is_valid(&snapshot.buffer_snapshot()) {
+                self.mentions.remove(&crease_id);
+            }
+        }
+    }
+
+    pub fn insert_mention(&mut self, crease_id: CreaseId, uri: MentionUri, task: MentionTask) {
+        self.mentions.insert(crease_id, (uri, task));
+    }
+
+    pub fn remove_mention(&mut self, crease_id: &CreaseId) {
+        self.mentions.remove(crease_id);
+    }
+
+    pub fn creases(&self) -> HashSet<CreaseId> {
+        self.mentions.keys().cloned().collect()
+    }
+
+    pub fn mentions(&self) -> HashSet<MentionUri> {
+        self.mentions.values().map(|(uri, _)| uri.clone()).collect()
+    }
+
+    pub fn remove_all(&mut self) -> impl Iterator<Item = (CreaseId, (MentionUri, MentionTask))> {
+        self.mentions.drain()
+    }
+
+    pub fn confirm_mention_completion(
+        &mut self,
+        crease_text: SharedString,
+        start: text::Anchor,
+        content_len: usize,
+        mention_uri: MentionUri,
+        supports_images: bool,
+        workspace: &Entity<Workspace>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Task<()> {
+        let Some(project) = self.project.upgrade() else {
+            return Task::ready(());
+        };
+
+        let snapshot = self
+            .editor
+            .update(cx, |editor, cx| editor.snapshot(window, cx));
+        let Some(start_anchor) = snapshot.buffer_snapshot().as_singleton_anchor(start) else {
+            return Task::ready(());
+        };
+        let excerpt_id = start_anchor.excerpt_id;
+        let end_anchor = snapshot.buffer_snapshot().anchor_before(
+            start_anchor.to_offset(&snapshot.buffer_snapshot()) + content_len + 1usize,
+        );
+
+        let crease = if let MentionUri::File { abs_path } = &mention_uri
+            && let Some(extension) = abs_path.extension()
+            && let Some(extension) = extension.to_str()
+            && Img::extensions().contains(&extension)
+            && !extension.contains("svg")
+        {
+            let Some(project_path) = project
+                .read(cx)
+                .project_path_for_absolute_path(&abs_path, cx)
+            else {
+                log::error!("project path not found");
+                return Task::ready(());
+            };
+            let image_task = project.update(cx, |project, cx| project.open_image(project_path, cx));
+            let image = cx
+                .spawn(async move |_, cx| {
+                    let image = image_task.await.map_err(|e| e.to_string())?;
+                    let image = image
+                        .update(cx, |image, _| image.image.clone())
+                        .map_err(|e| e.to_string())?;
+                    Ok(image)
+                })
+                .shared();
+            insert_crease_for_mention(
+                excerpt_id,
+                start,
+                content_len,
+                mention_uri.name().into(),
+                IconName::Image.path().into(),
+                Some(image),
+                self.editor.clone(),
+                window,
+                cx,
+            )
+        } else {
+            insert_crease_for_mention(
+                excerpt_id,
+                start,
+                content_len,
+                crease_text,
+                mention_uri.icon_path(cx),
+                None,
+                self.editor.clone(),
+                window,
+                cx,
+            )
+        };
+        let Some((crease_id, tx)) = crease else {
+            return Task::ready(());
+        };
+
+        let task = match mention_uri.clone() {
+            MentionUri::Fetch { url } => {
+                self.confirm_mention_for_fetch(url, workspace.read(cx).client().http_client(), cx)
+            }
+            MentionUri::Directory { .. } => Task::ready(Ok(Mention::Link)),
+            MentionUri::Thread { id, .. } => self.confirm_mention_for_thread(id, cx),
+            MentionUri::TextThread { path, .. } => self.confirm_mention_for_text_thread(path, cx),
+            MentionUri::File { abs_path } => {
+                self.confirm_mention_for_file(abs_path, supports_images, cx)
+            }
+            MentionUri::Symbol {
+                abs_path,
+                line_range,
+                ..
+            } => self.confirm_mention_for_symbol(abs_path, line_range, cx),
+            MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
+            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"
+                )))
+            }
+            MentionUri::Selection { .. } => {
+                debug_panic!("unexpected selection URI");
+                Task::ready(Err(anyhow!("unexpected selection URI")))
+            }
+        };
+        let task = cx
+            .spawn(async move |_, _| task.await.map_err(|e| e.to_string()))
+            .shared();
+        self.mentions.insert(crease_id, (mention_uri, task.clone()));
+
+        // Notify the user if we failed to load the mentioned context
+        cx.spawn_in(window, async move |this, cx| {
+            let result = task.await.notify_async_err(cx);
+            drop(tx);
+            if result.is_none() {
+                this.update(cx, |this, cx| {
+                    this.editor.update(cx, |editor, cx| {
+                        // Remove mention
+                        editor.edit([(start_anchor..end_anchor, "")], cx);
+                    });
+                    this.mentions.remove(&crease_id);
+                })
+                .ok();
+            }
+        })
+    }
+
+    pub fn confirm_mention_for_file(
+        &self,
+        abs_path: PathBuf,
+        supports_images: bool,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<Mention>> {
+        let Some(project) = self.project.upgrade() else {
+            return Task::ready(Err(anyhow!("project not found")));
+        };
+
+        let Some(project_path) = project
+            .read(cx)
+            .project_path_for_absolute_path(&abs_path, cx)
+        else {
+            return Task::ready(Err(anyhow!("project path not found")));
+        };
+        let extension = abs_path
+            .extension()
+            .and_then(OsStr::to_str)
+            .unwrap_or_default();
+
+        if Img::extensions().contains(&extension) && !extension.contains("svg") {
+            if !supports_images {
+                return Task::ready(Err(anyhow!("This model does not support images yet")));
+            }
+            let task = project.update(cx, |project, cx| project.open_image(project_path, cx));
+            return cx.spawn(async move |_, cx| {
+                let image = task.await?;
+                let image = image.update(cx, |image, _| image.image.clone())?;
+                let format = image.format;
+                let image = cx
+                    .update(|cx| LanguageModelImage::from_image(image, cx))?
+                    .await;
+                if let Some(image) = image {
+                    Ok(Mention::Image(MentionImage {
+                        data: image.source,
+                        format,
+                    }))
+                } else {
+                    Err(anyhow!("Failed to convert image"))
+                }
+            });
+        }
+
+        let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
+        cx.spawn(async move |_, cx| {
+            let buffer = buffer.await?;
+            let buffer_content = outline::get_buffer_content_or_outline(
+                buffer.clone(),
+                Some(&abs_path.to_string_lossy()),
+                &cx,
+            )
+            .await?;
+
+            Ok(Mention::Text {
+                content: buffer_content.text,
+                tracked_buffers: vec![buffer],
+            })
+        })
+    }
+
+    fn confirm_mention_for_fetch(
+        &self,
+        url: url::Url,
+        http_client: Arc<HttpClientWithUrl>,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<Mention>> {
+        cx.background_executor().spawn(async move {
+            let content = fetch_url_content(http_client, url.to_string()).await?;
+            Ok(Mention::Text {
+                content,
+                tracked_buffers: Vec::new(),
+            })
+        })
+    }
+
+    fn confirm_mention_for_symbol(
+        &self,
+        abs_path: PathBuf,
+        line_range: RangeInclusive<u32>,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<Mention>> {
+        let Some(project) = self.project.upgrade() else {
+            return Task::ready(Err(anyhow!("project not found")));
+        };
+        let Some(project_path) = project
+            .read(cx)
+            .project_path_for_absolute_path(&abs_path, cx)
+        else {
+            return Task::ready(Err(anyhow!("project path not found")));
+        };
+        let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
+        cx.spawn(async move |_, cx| {
+            let buffer = buffer.await?;
+            let mention = buffer.update(cx, |buffer, cx| {
+                let start = Point::new(*line_range.start(), 0).min(buffer.max_point());
+                let end = Point::new(*line_range.end() + 1, 0).min(buffer.max_point());
+                let content = buffer.text_for_range(start..end).collect();
+                Mention::Text {
+                    content,
+                    tracked_buffers: vec![cx.entity()],
+                }
+            })?;
+            anyhow::Ok(mention)
+        })
+    }
+
+    fn confirm_mention_for_rule(
+        &mut self,
+        id: PromptId,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<Mention>> {
+        let Some(prompt_store) = self.prompt_store.as_ref() else {
+            return Task::ready(Err(anyhow!("Missing prompt store")));
+        };
+        let prompt = prompt_store.read(cx).load(id, cx);
+        cx.spawn(async move |_, _| {
+            let prompt = prompt.await?;
+            Ok(Mention::Text {
+                content: prompt,
+                tracked_buffers: Vec::new(),
+            })
+        })
+    }
+
+    pub fn confirm_mention_for_selection(
+        &mut self,
+        source_range: Range<text::Anchor>,
+        selections: Vec<(Entity<Buffer>, Range<text::Anchor>, Range<usize>)>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let Some(project) = self.project.upgrade() else {
+            return;
+        };
+
+        let snapshot = self.editor.read(cx).buffer().read(cx).snapshot(cx);
+        let Some(start) = snapshot.as_singleton_anchor(source_range.start) else {
+            return;
+        };
+
+        let offset = start.to_offset(&snapshot);
+
+        for (buffer, selection_range, range_to_fold) in selections {
+            let range = snapshot.anchor_after(offset + range_to_fold.start)
+                ..snapshot.anchor_after(offset + range_to_fold.end);
+
+            let abs_path = buffer
+                .read(cx)
+                .project_path(cx)
+                .and_then(|project_path| project.read(cx).absolute_path(&project_path, cx));
+            let snapshot = buffer.read(cx).snapshot();
+
+            let text = snapshot
+                .text_for_range(selection_range.clone())
+                .collect::<String>();
+            let point_range = selection_range.to_point(&snapshot);
+            let line_range = point_range.start.row..=point_range.end.row;
+
+            let uri = MentionUri::Selection {
+                abs_path: abs_path.clone(),
+                line_range: line_range.clone(),
+            };
+            let crease = crease_for_mention(
+                selection_name(abs_path.as_deref(), &line_range).into(),
+                uri.icon_path(cx),
+                range,
+                self.editor.downgrade(),
+            );
+
+            let crease_id = self.editor.update(cx, |editor, cx| {
+                let crease_ids = editor.insert_creases(vec![crease.clone()], cx);
+                editor.fold_creases(vec![crease], false, window, cx);
+                crease_ids.first().copied().unwrap()
+            });
+
+            self.mentions.insert(
+                crease_id,
+                (
+                    uri,
+                    Task::ready(Ok(Mention::Text {
+                        content: text,
+                        tracked_buffers: vec![buffer],
+                    }))
+                    .shared(),
+                ),
+            );
+        }
+
+        // Take this explanation with a grain of salt but, with creases being
+        // inserted, GPUI's recomputes the editor layout in the next frames, so
+        // directly calling `editor.request_autoscroll` wouldn't work as
+        // expected. We're leveraging `cx.on_next_frame` to wait 2 frames and
+        // ensure that the layout has been recalculated so that the autoscroll
+        // request actually shows the cursor's new position.
+        let editor = self.editor.clone();
+        cx.on_next_frame(window, move |_, window, cx| {
+            cx.on_next_frame(window, move |_, _, cx| {
+                editor.update(cx, |editor, cx| {
+                    editor.request_autoscroll(Autoscroll::fit(), cx)
+                });
+            });
+        });
+    }
+
+    fn confirm_mention_for_thread(
+        &mut self,
+        id: acp::SessionId,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<Mention>> {
+        let Some(project) = self.project.upgrade() else {
+            return Task::ready(Err(anyhow!("project not found")));
+        };
+
+        let server = Rc::new(agent::NativeAgentServer::new(
+            project.read(cx).fs().clone(),
+            self.history_store.clone(),
+        ));
+        let delegate = AgentServerDelegate::new(
+            project.read(cx).agent_server_store().clone(),
+            project.clone(),
+            None,
+            None,
+        );
+        let connection = server.connect(None, delegate, cx);
+        cx.spawn(async move |_, cx| {
+            let (agent, _) = connection.await?;
+            let agent = agent.downcast::<agent::NativeAgentConnection>().unwrap();
+            let summary = agent
+                .0
+                .update(cx, |agent, cx| agent.thread_summary(id, cx))?
+                .await?;
+            anyhow::Ok(Mention::Text {
+                content: summary.to_string(),
+                tracked_buffers: Vec::new(),
+            })
+        })
+    }
+
+    fn confirm_mention_for_text_thread(
+        &mut self,
+        path: PathBuf,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<Mention>> {
+        let text_thread_task = self.history_store.update(cx, |store, cx| {
+            store.load_text_thread(path.as_path().into(), cx)
+        });
+        cx.spawn(async move |_, cx| {
+            let text_thread = text_thread_task.await?;
+            let xml = text_thread.update(cx, |text_thread, cx| text_thread.to_xml(cx))?;
+            Ok(Mention::Text {
+                content: xml,
+                tracked_buffers: Vec::new(),
+            })
+        })
+    }
+}
+
+pub(crate) fn paste_images_as_context(
+    editor: Entity<Editor>,
+    mention_set: Entity<MentionSet>,
+    window: &mut Window,
+    cx: &mut App,
+) -> Option<Task<()>> {
+    let clipboard = cx.read_from_clipboard()?;
+    Some(window.spawn(cx, async move |cx| {
+        use itertools::Itertools;
+        let (mut images, paths) = clipboard
+            .into_entries()
+            .filter_map(|entry| match entry {
+                ClipboardEntry::Image(image) => Some(Either::Left(image)),
+                ClipboardEntry::ExternalPaths(paths) => Some(Either::Right(paths)),
+                _ => None,
+            })
+            .partition_map::<Vec<_>, Vec<_>, _, _, _>(std::convert::identity);
+
+        if !paths.is_empty() {
+            images.extend(
+                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 {
+                            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,
+                        ));
+                    }
+                    images
+                })
+                .await,
+            );
+        }
+
+        if images.is_empty() {
+            return;
+        }
+
+        let replacement_text = MentionUri::PastedImage.as_link().to_string();
+        cx.update(|_window, cx| {
+            cx.stop_propagation();
+        })
+        .ok();
+        for image in images {
+            let Ok((excerpt_id, text_anchor, multibuffer_anchor)) =
+                editor.update_in(cx, |message_editor, window, cx| {
+                    let snapshot = message_editor.snapshot(window, cx);
+                    let (excerpt_id, _, buffer_snapshot) =
+                        snapshot.buffer_snapshot().as_singleton().unwrap();
+
+                    let text_anchor = buffer_snapshot.anchor_before(buffer_snapshot.len());
+                    let multibuffer_anchor = snapshot
+                        .buffer_snapshot()
+                        .anchor_in_excerpt(*excerpt_id, text_anchor);
+                    message_editor.edit(
+                        [(
+                            multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
+                            format!("{replacement_text} "),
+                        )],
+                        cx,
+                    );
+                    (*excerpt_id, text_anchor, multibuffer_anchor)
+                })
+            else {
+                break;
+            };
+
+            let content_len = replacement_text.len();
+            let Some(start_anchor) = multibuffer_anchor else {
+                continue;
+            };
+            let Ok(end_anchor) = editor.update(cx, |editor, cx| {
+                let snapshot = editor.buffer().read(cx).snapshot(cx);
+                snapshot.anchor_before(start_anchor.to_offset(&snapshot) + content_len)
+            }) else {
+                continue;
+            };
+            let image = Arc::new(image);
+            let Ok(Some((crease_id, tx))) = cx.update(|window, cx| {
+                insert_crease_for_mention(
+                    excerpt_id,
+                    text_anchor,
+                    content_len,
+                    MentionUri::PastedImage.name().into(),
+                    IconName::Image.path().into(),
+                    Some(Task::ready(Ok(image.clone())).shared()),
+                    editor.clone(),
+                    window,
+                    cx,
+                )
+            }) else {
+                continue;
+            };
+            let task = cx
+                .spawn(async move |cx| {
+                    let format = image.format;
+                    let image = cx
+                        .update(|_, cx| LanguageModelImage::from_image(image, cx))
+                        .map_err(|e| e.to_string())?
+                        .await;
+                    drop(tx);
+                    if let Some(image) = image {
+                        Ok(Mention::Image(MentionImage {
+                            data: image.source,
+                            format,
+                        }))
+                    } else {
+                        Err("Failed to convert image".into())
+                    }
+                })
+                .shared();
+
+            mention_set
+                .update(cx, |mention_set, _cx| {
+                    mention_set.insert_mention(crease_id, MentionUri::PastedImage, task.clone())
+                })
+                .ok();
+
+            if task.await.notify_async_err(cx).is_none() {
+                editor
+                    .update(cx, |editor, cx| {
+                        editor.edit([(start_anchor..end_anchor, "")], cx);
+                    })
+                    .ok();
+                mention_set
+                    .update(cx, |mention_set, _cx| {
+                        mention_set.remove_mention(&crease_id)
+                    })
+                    .ok();
+            }
+        }
+    }))
+}
+
+pub(crate) fn insert_crease_for_mention(
+    excerpt_id: ExcerptId,
+    anchor: text::Anchor,
+    content_len: usize,
+    crease_label: SharedString,
+    crease_icon: SharedString,
+    // abs_path: Option<Arc<Path>>,
+    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
+    editor: Entity<Editor>,
+    window: &mut Window,
+    cx: &mut App,
+) -> Option<(CreaseId, postage::barrier::Sender)> {
+    let (tx, rx) = postage::barrier::channel();
+
+    let crease_id = editor.update(cx, |editor, cx| {
+        let snapshot = editor.buffer().read(cx).snapshot(cx);
+
+        let start = snapshot.anchor_in_excerpt(excerpt_id, anchor)?;
+
+        let start = start.bias_right(&snapshot);
+        let end = snapshot.anchor_before(start.to_offset(&snapshot) + content_len);
+
+        let placeholder = FoldPlaceholder {
+            render: render_mention_fold_button(
+                crease_label.clone(),
+                crease_icon.clone(),
+                start..end,
+                rx,
+                image,
+                cx.weak_entity(),
+                cx,
+            ),
+            merge_adjacent: false,
+            ..Default::default()
+        };
+
+        let crease = Crease::Inline {
+            range: start..end,
+            placeholder,
+            render_toggle: None,
+            render_trailer: None,
+            metadata: Some(CreaseMetadata {
+                label: crease_label,
+                icon_path: crease_icon,
+            }),
+        };
+
+        let ids = editor.insert_creases(vec![crease.clone()], cx);
+        editor.fold_creases(vec![crease], false, window, cx);
+
+        Some(ids[0])
+    })?;
+
+    Some((crease_id, tx))
+}
+
+pub(crate) fn crease_for_mention(
+    label: SharedString,
+    icon_path: SharedString,
+    range: Range<Anchor>,
+    editor_entity: WeakEntity<Editor>,
+) -> Crease<Anchor> {
+    let placeholder = FoldPlaceholder {
+        render: render_fold_icon_button(icon_path.clone(), label.clone(), editor_entity),
+        merge_adjacent: false,
+        ..Default::default()
+    };
+
+    let render_trailer = move |_row, _unfold, _window: &mut Window, _cx: &mut App| Empty.into_any();
+
+    Crease::inline(range, placeholder, fold_toggle("mention"), render_trailer)
+        .with_metadata(CreaseMetadata { icon_path, label })
+}
+
+fn render_fold_icon_button(
+    icon_path: SharedString,
+    label: SharedString,
+    editor: WeakEntity<Editor>,
+) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
+    Arc::new({
+        move |fold_id, fold_range, cx| {
+            let is_in_text_selection = editor
+                .update(cx, |editor, cx| editor.is_range_selected(&fold_range, cx))
+                .unwrap_or_default();
+
+            ButtonLike::new(fold_id)
+                .style(ButtonStyle::Filled)
+                .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+                .toggle_state(is_in_text_selection)
+                .child(
+                    h_flex()
+                        .gap_1()
+                        .child(
+                            Icon::from_path(icon_path.clone())
+                                .size(IconSize::XSmall)
+                                .color(Color::Muted),
+                        )
+                        .child(
+                            Label::new(label.clone())
+                                .size(LabelSize::Small)
+                                .buffer_font(cx)
+                                .single_line(),
+                        ),
+                )
+                .into_any_element()
+        }
+    })
+}
+
+fn fold_toggle(
+    name: &'static str,
+) -> impl Fn(
+    MultiBufferRow,
+    bool,
+    Arc<dyn Fn(bool, &mut Window, &mut App) + Send + Sync>,
+    &mut Window,
+    &mut App,
+) -> AnyElement {
+    move |row, is_folded, fold, _window, _cx| {
+        Disclosure::new((name, row.0 as u64), !is_folded)
+            .toggle_state(is_folded)
+            .on_click(move |_e, window, cx| fold(!is_folded, window, cx))
+            .into_any_element()
+    }
+}
+
+fn full_mention_for_directory(
+    project: &Entity<Project>,
+    abs_path: &Path,
+    cx: &mut App,
+) -> Task<Result<Mention>> {
+    fn collect_files_in_path(worktree: &Worktree, path: &RelPath) -> Vec<(Arc<RelPath>, String)> {
+        let mut files = Vec::new();
+
+        for entry in worktree.child_entries(path) {
+            if entry.is_dir() {
+                files.extend(collect_files_in_path(worktree, &entry.path));
+            } else if entry.is_file() {
+                files.push((
+                    entry.path.clone(),
+                    worktree
+                        .full_path(&entry.path)
+                        .to_string_lossy()
+                        .to_string(),
+                ));
+            }
+        }
+
+        files
+    }
+
+    let Some(project_path) = project
+        .read(cx)
+        .project_path_for_absolute_path(&abs_path, cx)
+    else {
+        return Task::ready(Err(anyhow!("project path not found")));
+    };
+    let Some(entry) = project.read(cx).entry_for_path(&project_path, cx) else {
+        return Task::ready(Err(anyhow!("project entry not found")));
+    };
+    let directory_path = entry.path.clone();
+    let worktree_id = project_path.worktree_id;
+    let Some(worktree) = project.read(cx).worktree_for_id(worktree_id, cx) else {
+        return Task::ready(Err(anyhow!("worktree not found")));
+    };
+    let project = project.clone();
+    cx.spawn(async move |cx| {
+        let file_paths = worktree.read_with(cx, |worktree, _cx| {
+            collect_files_in_path(worktree, &directory_path)
+        })?;
+        let descendants_future = cx.update(|cx| {
+            futures::future::join_all(file_paths.into_iter().map(|(worktree_path, full_path)| {
+                let rel_path = worktree_path
+                    .strip_prefix(&directory_path)
+                    .log_err()
+                    .map_or_else(|| worktree_path.clone(), |rel_path| rel_path.into());
+
+                let open_task = project.update(cx, |project, cx| {
+                    project.buffer_store().update(cx, |buffer_store, cx| {
+                        let project_path = ProjectPath {
+                            worktree_id,
+                            path: worktree_path,
+                        };
+                        buffer_store.open_buffer(project_path, cx)
+                    })
+                });
+
+                cx.spawn(async move |cx| {
+                    let buffer = open_task.await.log_err()?;
+                    let buffer_content = outline::get_buffer_content_or_outline(
+                        buffer.clone(),
+                        Some(&full_path),
+                        &cx,
+                    )
+                    .await
+                    .ok()?;
+
+                    Some((rel_path, full_path, buffer_content.text, buffer))
+                })
+            }))
+        })?;
+
+        let contents = cx
+            .background_spawn(async move {
+                let (contents, tracked_buffers) = descendants_future
+                    .await
+                    .into_iter()
+                    .flatten()
+                    .map(|(rel_path, full_path, rope, buffer)| {
+                        ((rel_path, full_path, rope), buffer)
+                    })
+                    .unzip();
+                Mention::Text {
+                    content: render_directory_contents(contents),
+                    tracked_buffers,
+                }
+            })
+            .await;
+        anyhow::Ok(contents)
+    })
+}
+
+fn render_directory_contents(entries: Vec<(Arc<RelPath>, String, String)>) -> String {
+    let mut output = String::new();
+    for (_relative_path, full_path, content) in entries {
+        let fence = codeblock_fence_for_path(Some(&full_path), None);
+        write!(output, "\n{fence}\n{content}\n```").unwrap();
+    }
+    output
+}
+
+fn render_mention_fold_button(
+    label: SharedString,
+    icon: SharedString,
+    range: Range<Anchor>,
+    mut loading_finished: postage::barrier::Receiver,
+    image_task: Option<Shared<Task<Result<Arc<Image>, String>>>>,
+    editor: WeakEntity<Editor>,
+    cx: &mut App,
+) -> Arc<dyn Send + Sync + Fn(FoldId, Range<Anchor>, &mut App) -> AnyElement> {
+    let loading = cx.new(|cx| {
+        let loading = cx.spawn(async move |this, cx| {
+            loading_finished.recv().await;
+            this.update(cx, |this: &mut LoadingContext, cx| {
+                this.loading = None;
+                cx.notify();
+            })
+            .ok();
+        });
+        LoadingContext {
+            id: cx.entity_id(),
+            label,
+            icon,
+            range,
+            editor,
+            loading: Some(loading),
+            image: image_task.clone(),
+        }
+    });
+    Arc::new(move |_fold_id, _fold_range, _cx| loading.clone().into_any_element())
+}
+
+struct LoadingContext {
+    id: EntityId,
+    label: SharedString,
+    icon: SharedString,
+    range: Range<Anchor>,
+    editor: WeakEntity<Editor>,
+    loading: Option<Task<()>>,
+    image: Option<Shared<Task<Result<Arc<Image>, String>>>>,
+}
+
+impl Render for LoadingContext {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let is_in_text_selection = self
+            .editor
+            .update(cx, |editor, cx| editor.is_range_selected(&self.range, cx))
+            .unwrap_or_default();
+        ButtonLike::new(("loading-context", self.id))
+            .style(ButtonStyle::Filled)
+            .selected_style(ButtonStyle::Tinted(TintColor::Accent))
+            .toggle_state(is_in_text_selection)
+            .when_some(self.image.clone(), |el, image_task| {
+                el.hoverable_tooltip(move |_, cx| {
+                    let image = image_task.peek().cloned().transpose().ok().flatten();
+                    let image_task = image_task.clone();
+                    cx.new::<ImageHover>(|cx| ImageHover {
+                        image,
+                        _task: cx.spawn(async move |this, cx| {
+                            if let Ok(image) = image_task.clone().await {
+                                this.update(cx, |this, cx| {
+                                    if this.image.replace(image).is_none() {
+                                        cx.notify();
+                                    }
+                                })
+                                .ok();
+                            }
+                        }),
+                    })
+                    .into()
+                })
+            })
+            .child(
+                h_flex()
+                    .gap_1()
+                    .child(
+                        Icon::from_path(self.icon.clone())
+                            .size(IconSize::XSmall)
+                            .color(Color::Muted),
+                    )
+                    .child(
+                        Label::new(self.label.clone())
+                            .size(LabelSize::Small)
+                            .buffer_font(cx)
+                            .single_line(),
+                    )
+                    .map(|el| {
+                        if self.loading.is_some() {
+                            el.with_animation(
+                                "loading-context-crease",
+                                Animation::new(Duration::from_secs(2))
+                                    .repeat()
+                                    .with_easing(pulsating_between(0.4, 0.8)),
+                                |label, delta| label.opacity(delta),
+                            )
+                            .into_any()
+                        } else {
+                            el.into_any()
+                        }
+                    }),
+            )
+    }
+}
+
+struct ImageHover {
+    image: Option<Arc<Image>>,
+    _task: Task<()>,
+}
+
+impl Render for ImageHover {
+    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
+        if let Some(image) = self.image.clone() {
+            gpui::img(image).max_w_96().max_h_96().into_any_element()
+        } else {
+            gpui::Empty.into_any_element()
+        }
+    }
+}
+
+async fn fetch_url_content(http_client: Arc<HttpClientWithUrl>, url: String) -> Result<String> {
+    #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
+    enum ContentType {
+        Html,
+        Plaintext,
+        Json,
+    }
+    use html_to_markdown::{TagHandler, convert_html_to_markdown, markdown};
+
+    let url = if !url.starts_with("https://") && !url.starts_with("http://") {
+        format!("https://{url}")
+    } else {
+        url
+    };
+
+    let mut response = http_client.get(&url, AsyncBody::default(), true).await?;
+    let mut body = Vec::new();
+    response
+        .body_mut()
+        .read_to_end(&mut body)
+        .await
+        .context("error reading response body")?;
+
+    if response.status().is_client_error() {
+        let text = String::from_utf8_lossy(body.as_slice());
+        anyhow::bail!(
+            "status error {}, response: {text:?}",
+            response.status().as_u16()
+        );
+    }
+
+    let Some(content_type) = response.headers().get("content-type") else {
+        anyhow::bail!("missing Content-Type header");
+    };
+    let content_type = content_type
+        .to_str()
+        .context("invalid Content-Type header")?;
+    let content_type = match content_type {
+        "text/html" => ContentType::Html,
+        "text/plain" => ContentType::Plaintext,
+        "application/json" => ContentType::Json,
+        _ => ContentType::Html,
+    };
+
+    match content_type {
+        ContentType::Html => {
+            let mut handlers: Vec<TagHandler> = vec![
+                Rc::new(RefCell::new(markdown::WebpageChromeRemover)),
+                Rc::new(RefCell::new(markdown::ParagraphHandler)),
+                Rc::new(RefCell::new(markdown::HeadingHandler)),
+                Rc::new(RefCell::new(markdown::ListHandler)),
+                Rc::new(RefCell::new(markdown::TableHandler::new())),
+                Rc::new(RefCell::new(markdown::StyledTextHandler)),
+            ];
+            if url.contains("wikipedia.org") {
+                use html_to_markdown::structure::wikipedia;
+
+                handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaChromeRemover)));
+                handlers.push(Rc::new(RefCell::new(wikipedia::WikipediaInfoboxHandler)));
+                handlers.push(Rc::new(
+                    RefCell::new(wikipedia::WikipediaCodeHandler::new()),
+                ));
+            } else {
+                handlers.push(Rc::new(RefCell::new(markdown::CodeHandler)));
+            }
+            convert_html_to_markdown(&body[..], &mut handlers)
+        }
+        ContentType::Plaintext => Ok(std::str::from_utf8(&body)?.to_owned()),
+        ContentType::Json => {
+            let json: serde_json::Value = serde_json::from_slice(&body)?;
+
+            Ok(format!(
+                "```json\n{}\n```",
+                serde_json::to_string_pretty(&json)?
+            ))
+        }
+    }
+}

crates/agent_ui/src/terminal_inline_assistant.rs πŸ”—

@@ -1,6 +1,5 @@
 use crate::{
     context::load_context,
-    context_store::ContextStore,
     inline_prompt_editor::{
         CodegenStatus, PromptEditor, PromptEditorEvent, TerminalInlineAssistId,
     },
@@ -73,8 +72,8 @@ impl TerminalInlineAssistant {
         terminal_view: &Entity<TerminalView>,
         workspace: WeakEntity<Workspace>,
         project: WeakEntity<Project>,
+        thread_store: Entity<HistoryStore>,
         prompt_store: Option<Entity<PromptStore>>,
-        thread_store: Option<WeakEntity<HistoryStore>>,
         initial_prompt: Option<String>,
         window: &mut Window,
         cx: &mut App,
@@ -87,7 +86,6 @@ impl TerminalInlineAssistant {
                 cx,
             )
         });
-        let context_store = cx.new(|_cx| ContextStore::new(project));
         let codegen = cx.new(|_| TerminalCodegen::new(terminal, self.telemetry.clone()));
 
         let prompt_editor = cx.new(|cx| {
@@ -97,10 +95,10 @@ impl TerminalInlineAssistant {
                 prompt_buffer.clone(),
                 codegen,
                 self.fs.clone(),
-                context_store.clone(),
-                workspace.clone(),
                 thread_store.clone(),
-                prompt_store.as_ref().map(|s| s.downgrade()),
+                prompt_store.clone(),
+                project.clone(),
+                workspace.clone(),
                 window,
                 cx,
             )
@@ -119,8 +117,6 @@ impl TerminalInlineAssistant {
             terminal_view,
             prompt_editor,
             workspace.clone(),
-            context_store,
-            prompt_store,
             window,
             cx,
         );
@@ -227,6 +223,10 @@ impl TerminalInlineAssistant {
         assist_id: TerminalInlineAssistId,
         cx: &mut App,
     ) -> Result<Task<LanguageModelRequest>> {
+        let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
+            .inline_assistant_model()
+            .context("No inline assistant model")?;
+
         let assist = self.assists.get(&assist_id).context("invalid assist")?;
 
         let shell = std::env::var("SHELL").ok();
@@ -243,35 +243,20 @@ impl TerminalInlineAssistant {
             .ok()
             .unwrap_or_default();
 
+        let prompt_editor = assist.prompt_editor.clone().context("invalid assist")?;
+
         let prompt = self.prompt_builder.generate_terminal_assistant_prompt(
-            &assist
-                .prompt_editor
-                .clone()
-                .context("invalid assist")?
-                .read(cx)
-                .prompt(cx),
+            &prompt_editor.read(cx).prompt(cx),
             shell.as_deref(),
             working_directory.as_deref(),
             &latest_output,
         )?;
 
-        let contexts = assist
-            .context_store
-            .read(cx)
-            .context()
-            .cloned()
-            .collect::<Vec<_>>();
-        let context_load_task = assist.workspace.update(cx, |workspace, cx| {
-            let project = workspace.project();
-            load_context(contexts, project, &assist.prompt_store, cx)
-        })?;
-
-        let ConfiguredModel { model, .. } = LanguageModelRegistry::read_global(cx)
-            .inline_assistant_model()
-            .context("No inline assistant model")?;
-
         let temperature = AgentSettings::temperature_for_model(&model, cx);
 
+        let mention_set = prompt_editor.read(cx).mention_set().clone();
+        let load_context_task = load_context(&mention_set, cx);
+
         Ok(cx.background_spawn(async move {
             let mut request_message = LanguageModelRequestMessage {
                 role: Role::User,
@@ -279,9 +264,9 @@ impl TerminalInlineAssistant {
                 cache: false,
             };
 
-            context_load_task
-                .await
-                .add_to_request_message(&mut request_message);
+            if let Some(context) = load_context_task.await {
+                context.add_to_request_message(&mut request_message);
+            }
 
             request_message.content.push(prompt.into());
 
@@ -409,8 +394,6 @@ struct TerminalInlineAssist {
     prompt_editor: Option<Entity<PromptEditor<TerminalCodegen>>>,
     codegen: Entity<TerminalCodegen>,
     workspace: WeakEntity<Workspace>,
-    context_store: Entity<ContextStore>,
-    prompt_store: Option<Entity<PromptStore>>,
     _subscriptions: Vec<Subscription>,
 }
 
@@ -420,8 +403,6 @@ impl TerminalInlineAssist {
         terminal: &Entity<TerminalView>,
         prompt_editor: Entity<PromptEditor<TerminalCodegen>>,
         workspace: WeakEntity<Workspace>,
-        context_store: Entity<ContextStore>,
-        prompt_store: Option<Entity<PromptStore>>,
         window: &mut Window,
         cx: &mut App,
     ) -> Self {
@@ -431,8 +412,6 @@ impl TerminalInlineAssist {
             prompt_editor: Some(prompt_editor.clone()),
             codegen: codegen.clone(),
             workspace,
-            context_store,
-            prompt_store,
             _subscriptions: vec![
                 window.subscribe(&prompt_editor, cx, |prompt_editor, event, window, cx| {
                     TerminalInlineAssistant::update_global(cx, |this, cx| {

crates/agent_ui/src/ui.rs πŸ”—

@@ -2,7 +2,6 @@ mod acp_onboarding_modal;
 mod agent_notification;
 mod burn_mode_tooltip;
 mod claude_code_onboarding_modal;
-mod context_pill;
 mod end_trial_upsell;
 mod hold_for_default;
 mod onboarding_modal;
@@ -13,7 +12,6 @@ pub use acp_onboarding_modal::*;
 pub use agent_notification::*;
 pub use burn_mode_tooltip::*;
 pub use claude_code_onboarding_modal::*;
-pub use context_pill::*;
 pub use end_trial_upsell::*;
 pub use hold_for_default::*;
 pub use onboarding_modal::*;

crates/agent_ui/src/ui/context_pill.rs πŸ”—

@@ -1,858 +0,0 @@
-use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
-
-use file_icons::FileIcons;
-use futures::FutureExt as _;
-use gpui::{
-    Animation, AnimationExt as _, AnyView, ClickEvent, Entity, Image, MouseButton, Task,
-    pulsating_between,
-};
-use language_model::LanguageModelImage;
-use project::Project;
-use prompt_store::PromptStore;
-use rope::Point;
-use ui::{IconButtonShape, Tooltip, prelude::*, tooltip_container};
-use util::paths::PathStyle;
-
-use crate::context::{
-    AgentContextHandle, ContextId, ContextKind, DirectoryContextHandle, FetchedUrlContext,
-    FileContextHandle, ImageContext, ImageStatus, RulesContextHandle, SelectionContextHandle,
-    SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
-};
-
-#[derive(IntoElement)]
-pub enum ContextPill {
-    Added {
-        context: AddedContext,
-        dupe_name: bool,
-        focused: bool,
-        on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
-        on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
-    },
-    Suggested {
-        name: SharedString,
-        icon_path: Option<SharedString>,
-        kind: ContextKind,
-        focused: bool,
-        on_click: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
-    },
-}
-
-impl ContextPill {
-    pub fn added(
-        context: AddedContext,
-        dupe_name: bool,
-        focused: bool,
-        on_remove: Option<Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>>,
-    ) -> Self {
-        Self::Added {
-            context,
-            dupe_name,
-            on_remove,
-            focused,
-            on_click: None,
-        }
-    }
-
-    pub fn suggested(
-        name: SharedString,
-        icon_path: Option<SharedString>,
-        kind: ContextKind,
-        focused: bool,
-    ) -> Self {
-        Self::Suggested {
-            name,
-            icon_path,
-            kind,
-            focused,
-            on_click: None,
-        }
-    }
-
-    pub fn on_click(mut self, listener: Rc<dyn Fn(&ClickEvent, &mut Window, &mut App)>) -> Self {
-        match &mut self {
-            ContextPill::Added { on_click, .. } => {
-                *on_click = Some(listener);
-            }
-            ContextPill::Suggested { on_click, .. } => {
-                *on_click = Some(listener);
-            }
-        }
-        self
-    }
-
-    pub fn id(&self) -> ElementId {
-        match self {
-            Self::Added { context, .. } => context.handle.element_id("context-pill".into()),
-            Self::Suggested { .. } => "suggested-context-pill".into(),
-        }
-    }
-
-    pub fn icon(&self) -> Icon {
-        match self {
-            Self::Suggested {
-                icon_path: Some(icon_path),
-                ..
-            } => Icon::from_path(icon_path),
-            Self::Suggested { kind, .. } => Icon::new(kind.icon()),
-            Self::Added { context, .. } => context.icon(),
-        }
-    }
-}
-
-impl RenderOnce for ContextPill {
-    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
-        let color = cx.theme().colors();
-
-        let base_pill = h_flex()
-            .id(self.id())
-            .pl_1()
-            .pb(px(1.))
-            .border_1()
-            .rounded_sm()
-            .gap_1()
-            .child(self.icon().size(IconSize::XSmall).color(Color::Muted));
-
-        match &self {
-            ContextPill::Added {
-                context,
-                dupe_name,
-                on_remove,
-                focused,
-                on_click,
-            } => {
-                let status_is_error = matches!(context.status, ContextStatus::Error { .. });
-                let status_is_warning = matches!(context.status, ContextStatus::Warning { .. });
-
-                base_pill
-                    .pr(if on_remove.is_some() { px(2.) } else { px(4.) })
-                    .map(|pill| {
-                        if status_is_error {
-                            pill.bg(cx.theme().status().error_background)
-                                .border_color(cx.theme().status().error_border)
-                        } else if status_is_warning {
-                            pill.bg(cx.theme().status().warning_background)
-                                .border_color(cx.theme().status().warning_border)
-                        } else if *focused {
-                            pill.bg(color.element_background)
-                                .border_color(color.border_focused)
-                        } else {
-                            pill.bg(color.element_background)
-                                .border_color(color.border.opacity(0.5))
-                        }
-                    })
-                    .child(
-                        h_flex()
-                            .id("context-data")
-                            .gap_1()
-                            .child(
-                                div().max_w_64().child(
-                                    Label::new(context.name.clone())
-                                        .size(LabelSize::Small)
-                                        .truncate(),
-                                ),
-                            )
-                            .when_some(context.parent.as_ref(), |element, parent_name| {
-                                if *dupe_name {
-                                    element.child(
-                                        Label::new(parent_name.clone())
-                                            .size(LabelSize::XSmall)
-                                            .color(Color::Muted),
-                                    )
-                                } else {
-                                    element
-                                }
-                            })
-                            .when_some(context.tooltip.as_ref(), |element, tooltip| {
-                                element.tooltip(Tooltip::text(tooltip.clone()))
-                            })
-                            .map(|element| match &context.status {
-                                ContextStatus::Ready => element
-                                    .when_some(
-                                        context.render_hover.as_ref(),
-                                        |element, render_hover| {
-                                            let render_hover = render_hover.clone();
-                                            element.hoverable_tooltip(move |window, cx| {
-                                                render_hover(window, cx)
-                                            })
-                                        },
-                                    )
-                                    .into_any(),
-                                ContextStatus::Loading { message } => element
-                                    .tooltip(ui::Tooltip::text(message.clone()))
-                                    .with_animation(
-                                        "pulsating-ctx-pill",
-                                        Animation::new(Duration::from_secs(2))
-                                            .repeat()
-                                            .with_easing(pulsating_between(0.4, 0.8)),
-                                        |label, delta| label.opacity(delta),
-                                    )
-                                    .into_any_element(),
-                                ContextStatus::Warning { message }
-                                | ContextStatus::Error { message } => element
-                                    .tooltip(ui::Tooltip::text(message.clone()))
-                                    .into_any_element(),
-                            }),
-                    )
-                    .when_some(on_remove.as_ref(), |element, on_remove| {
-                        element.child(
-                            IconButton::new(
-                                context.handle.element_id("remove".into()),
-                                IconName::Close,
-                            )
-                            .shape(IconButtonShape::Square)
-                            .icon_size(IconSize::XSmall)
-                            .tooltip(Tooltip::text("Remove Context"))
-                            .on_click({
-                                let on_remove = on_remove.clone();
-                                move |event, window, cx| on_remove(event, window, cx)
-                            }),
-                        )
-                    })
-                    .when_some(on_click.as_ref(), |element, on_click| {
-                        let on_click = on_click.clone();
-                        element.cursor_pointer().on_click(move |event, window, cx| {
-                            on_click(event, window, cx);
-                            cx.stop_propagation();
-                        })
-                    })
-                    .into_any_element()
-            }
-            ContextPill::Suggested {
-                name,
-                icon_path: _,
-                kind: _,
-                focused,
-                on_click,
-            } => base_pill
-                .cursor_pointer()
-                .pr_1()
-                .border_dashed()
-                .map(|pill| {
-                    if *focused {
-                        pill.border_color(color.border_focused)
-                            .bg(color.element_background.opacity(0.5))
-                    } else {
-                        pill.border_color(color.border)
-                    }
-                })
-                .hover(|style| style.bg(color.element_hover.opacity(0.5)))
-                .child(
-                    div().max_w_64().child(
-                        Label::new(name.clone())
-                            .size(LabelSize::Small)
-                            .color(Color::Muted)
-                            .truncate(),
-                    ),
-                )
-                .tooltip(|_window, cx| {
-                    Tooltip::with_meta("Suggested Context", None, "Click to add it", cx)
-                })
-                .when_some(on_click.as_ref(), |element, on_click| {
-                    let on_click = on_click.clone();
-                    element.on_click(move |event, window, cx| {
-                        on_click(event, window, cx);
-                        cx.stop_propagation();
-                    })
-                })
-                .into_any(),
-        }
-    }
-}
-
-pub enum ContextStatus {
-    Ready,
-    Loading { message: SharedString },
-    Error { message: SharedString },
-    Warning { message: SharedString },
-}
-
-#[derive(RegisterComponent)]
-pub struct AddedContext {
-    pub handle: AgentContextHandle,
-    pub kind: ContextKind,
-    pub name: SharedString,
-    pub parent: Option<SharedString>,
-    pub tooltip: Option<SharedString>,
-    pub icon_path: Option<SharedString>,
-    pub status: ContextStatus,
-    pub render_hover: Option<Rc<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
-}
-
-impl AddedContext {
-    pub fn icon(&self) -> Icon {
-        match &self.status {
-            ContextStatus::Warning { .. } => Icon::new(IconName::Warning).color(Color::Warning),
-            ContextStatus::Error { .. } => Icon::new(IconName::XCircle).color(Color::Error),
-            _ => {
-                if let Some(icon_path) = &self.icon_path {
-                    Icon::from_path(icon_path)
-                } else {
-                    Icon::new(self.kind.icon())
-                }
-            }
-        }
-    }
-    /// Creates an `AddedContext` by retrieving relevant details of `AgentContext`. This returns a
-    /// `None` if `DirectoryContext` or `RulesContext` no longer exist.
-    ///
-    /// TODO: `None` cases are unremovable from `ContextStore` and so are a very minor memory leak.
-    pub fn new_pending(
-        handle: AgentContextHandle,
-        prompt_store: Option<&Entity<PromptStore>>,
-        project: &Project,
-        model: Option<&Arc<dyn language_model::LanguageModel>>,
-        cx: &App,
-    ) -> Option<AddedContext> {
-        match handle {
-            AgentContextHandle::File(handle) => {
-                Self::pending_file(handle, project.path_style(cx), cx)
-            }
-            AgentContextHandle::Directory(handle) => Self::pending_directory(handle, project, cx),
-            AgentContextHandle::Symbol(handle) => {
-                Self::pending_symbol(handle, project.path_style(cx), cx)
-            }
-            AgentContextHandle::Selection(handle) => {
-                Self::pending_selection(handle, project.path_style(cx), cx)
-            }
-            AgentContextHandle::FetchedUrl(handle) => Some(Self::fetched_url(handle)),
-            AgentContextHandle::Thread(handle) => Some(Self::pending_thread(handle, cx)),
-            AgentContextHandle::TextThread(handle) => Some(Self::pending_text_thread(handle, cx)),
-            AgentContextHandle::Rules(handle) => Self::pending_rules(handle, prompt_store, cx),
-            AgentContextHandle::Image(handle) => {
-                Some(Self::image(handle, model, project.path_style(cx), cx))
-            }
-        }
-    }
-
-    fn pending_file(
-        handle: FileContextHandle,
-        path_style: PathStyle,
-        cx: &App,
-    ) -> Option<AddedContext> {
-        let full_path = handle
-            .buffer
-            .read(cx)
-            .file()?
-            .full_path(cx)
-            .to_string_lossy()
-            .to_string();
-        Some(Self::file(handle, &full_path, path_style, cx))
-    }
-
-    fn file(
-        handle: FileContextHandle,
-        full_path: &str,
-        path_style: PathStyle,
-        cx: &App,
-    ) -> AddedContext {
-        let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
-        AddedContext {
-            kind: ContextKind::File,
-            name,
-            parent,
-            tooltip: Some(SharedString::new(full_path)),
-            icon_path: FileIcons::get_icon(Path::new(full_path), cx),
-            status: ContextStatus::Ready,
-            render_hover: None,
-            handle: AgentContextHandle::File(handle),
-        }
-    }
-
-    fn pending_directory(
-        handle: DirectoryContextHandle,
-        project: &Project,
-        cx: &App,
-    ) -> Option<AddedContext> {
-        let worktree = project.worktree_for_entry(handle.entry_id, cx)?.read(cx);
-        let entry = worktree.entry_for_id(handle.entry_id)?;
-        let full_path = worktree
-            .full_path(&entry.path)
-            .to_string_lossy()
-            .to_string();
-        Some(Self::directory(handle, &full_path, project.path_style(cx)))
-    }
-
-    fn directory(
-        handle: DirectoryContextHandle,
-        full_path: &str,
-        path_style: PathStyle,
-    ) -> AddedContext {
-        let (name, parent) = extract_file_name_and_directory_from_full_path(full_path, path_style);
-        AddedContext {
-            kind: ContextKind::Directory,
-            name,
-            parent,
-            tooltip: Some(SharedString::new(full_path)),
-            icon_path: None,
-            status: ContextStatus::Ready,
-            render_hover: None,
-            handle: AgentContextHandle::Directory(handle),
-        }
-    }
-
-    fn pending_symbol(
-        handle: SymbolContextHandle,
-        path_style: PathStyle,
-        cx: &App,
-    ) -> Option<AddedContext> {
-        let excerpt = ContextFileExcerpt::new(
-            &handle.full_path(cx)?.to_string_lossy(),
-            handle.enclosing_line_range(cx),
-            path_style,
-            cx,
-        );
-        Some(AddedContext {
-            kind: ContextKind::Symbol,
-            name: handle.symbol.clone(),
-            parent: Some(excerpt.file_name_and_range.clone()),
-            tooltip: None,
-            icon_path: None,
-            status: ContextStatus::Ready,
-            render_hover: {
-                let handle = handle.clone();
-                Some(Rc::new(move |_, cx| {
-                    excerpt.hover_view(handle.text(cx), cx).into()
-                }))
-            },
-            handle: AgentContextHandle::Symbol(handle),
-        })
-    }
-
-    fn pending_selection(
-        handle: SelectionContextHandle,
-        path_style: PathStyle,
-        cx: &App,
-    ) -> Option<AddedContext> {
-        let excerpt = ContextFileExcerpt::new(
-            &handle.full_path(cx)?.to_string_lossy(),
-            handle.line_range(cx),
-            path_style,
-            cx,
-        );
-        Some(AddedContext {
-            kind: ContextKind::Selection,
-            name: excerpt.file_name_and_range.clone(),
-            parent: excerpt.parent_name.clone(),
-            tooltip: None,
-            icon_path: excerpt.icon_path.clone(),
-            status: ContextStatus::Ready,
-            render_hover: {
-                let handle = handle.clone();
-                Some(Rc::new(move |_, cx| {
-                    excerpt.hover_view(handle.text(cx), cx).into()
-                }))
-            },
-            handle: AgentContextHandle::Selection(handle),
-        })
-    }
-
-    fn fetched_url(context: FetchedUrlContext) -> AddedContext {
-        AddedContext {
-            kind: ContextKind::FetchedUrl,
-            name: context.url.clone(),
-            parent: None,
-            tooltip: None,
-            icon_path: None,
-            status: ContextStatus::Ready,
-            render_hover: None,
-            handle: AgentContextHandle::FetchedUrl(context),
-        }
-    }
-
-    fn pending_thread(handle: ThreadContextHandle, cx: &App) -> AddedContext {
-        AddedContext {
-            kind: ContextKind::Thread,
-            name: handle.title(cx),
-            parent: None,
-            tooltip: None,
-            icon_path: None,
-            status: if handle.thread.read(cx).is_generating_summary() {
-                ContextStatus::Loading {
-                    message: "Summarizing…".into(),
-                }
-            } else {
-                ContextStatus::Ready
-            },
-            render_hover: {
-                let thread = handle.thread.clone();
-                Some(Rc::new(move |_, cx| {
-                    let text = thread
-                        .update(cx, |thread, cx| thread.summary(cx))
-                        .now_or_never()
-                        .flatten()
-                        .unwrap_or_else(|| SharedString::from(thread.read(cx).to_markdown()));
-                    ContextPillHover::new_text(text, cx).into()
-                }))
-            },
-            handle: AgentContextHandle::Thread(handle),
-        }
-    }
-
-    fn pending_text_thread(handle: TextThreadContextHandle, cx: &App) -> AddedContext {
-        AddedContext {
-            kind: ContextKind::TextThread,
-            name: handle.title(cx),
-            parent: None,
-            tooltip: None,
-            icon_path: None,
-            status: ContextStatus::Ready,
-            render_hover: {
-                let text_thread = handle.text_thread.clone();
-                Some(Rc::new(move |_, cx| {
-                    let text = text_thread.read(cx).to_xml(cx);
-                    ContextPillHover::new_text(text.into(), cx).into()
-                }))
-            },
-            handle: AgentContextHandle::TextThread(handle),
-        }
-    }
-
-    fn pending_rules(
-        handle: RulesContextHandle,
-        prompt_store: Option<&Entity<PromptStore>>,
-        cx: &App,
-    ) -> Option<AddedContext> {
-        let title = prompt_store
-            .as_ref()?
-            .read(cx)
-            .metadata(handle.prompt_id.into())?
-            .title
-            .unwrap_or_else(|| "Unnamed Rule".into());
-        Some(AddedContext {
-            kind: ContextKind::Rules,
-            name: title,
-            parent: None,
-            tooltip: None,
-            icon_path: None,
-            status: ContextStatus::Ready,
-            render_hover: None,
-            handle: AgentContextHandle::Rules(handle),
-        })
-    }
-
-    fn image(
-        context: ImageContext,
-        model: Option<&Arc<dyn language_model::LanguageModel>>,
-        path_style: PathStyle,
-        cx: &App,
-    ) -> AddedContext {
-        let (name, parent, icon_path) = if let Some(full_path) = context.full_path.as_ref() {
-            let (name, parent) =
-                extract_file_name_and_directory_from_full_path(full_path, path_style);
-            let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
-            (name, parent, icon_path)
-        } else {
-            ("Image".into(), None, None)
-        };
-
-        let status = match context.status(model) {
-            ImageStatus::Loading => ContextStatus::Loading {
-                message: "Loading…".into(),
-            },
-            ImageStatus::Error => ContextStatus::Error {
-                message: "Failed to load Image".into(),
-            },
-            ImageStatus::Warning => ContextStatus::Warning {
-                message: format!(
-                    "{} doesn't support attaching Images as Context",
-                    model.map(|m| m.name().0).unwrap_or_else(|| "Model".into())
-                )
-                .into(),
-            },
-            ImageStatus::Ready => ContextStatus::Ready,
-        };
-
-        AddedContext {
-            kind: ContextKind::Image,
-            name,
-            parent,
-            tooltip: None,
-            icon_path,
-            status,
-            render_hover: Some(Rc::new({
-                let image = context.original_image.clone();
-                move |_, cx| {
-                    let image = image.clone();
-                    ContextPillHover::new(cx, move |_, _| {
-                        gpui::img(image.clone())
-                            .max_w_96()
-                            .max_h_96()
-                            .into_any_element()
-                    })
-                    .into()
-                }
-            })),
-            handle: AgentContextHandle::Image(context),
-        }
-    }
-}
-
-fn extract_file_name_and_directory_from_full_path(
-    path: &str,
-    path_style: PathStyle,
-) -> (SharedString, Option<SharedString>) {
-    let (parent, file_name) = path_style.split(path);
-    let parent = parent.and_then(|parent| {
-        let parent = parent.trim_end_matches(path_style.separator());
-        let (_, parent) = path_style.split(parent);
-        if parent.is_empty() {
-            None
-        } else {
-            Some(SharedString::new(parent))
-        }
-    });
-    (SharedString::new(file_name), parent)
-}
-
-#[derive(Debug, Clone)]
-struct ContextFileExcerpt {
-    pub file_name_and_range: SharedString,
-    pub full_path_and_range: SharedString,
-    pub parent_name: Option<SharedString>,
-    pub icon_path: Option<SharedString>,
-}
-
-impl ContextFileExcerpt {
-    pub fn new(full_path: &str, line_range: Range<Point>, path_style: PathStyle, cx: &App) -> Self {
-        let (parent, file_name) = path_style.split(full_path);
-        let line_range_text = format!(" ({}-{})", line_range.start.row + 1, line_range.end.row + 1);
-        let mut full_path_and_range = full_path.to_owned();
-        full_path_and_range.push_str(&line_range_text);
-        let mut file_name_and_range = file_name.to_owned();
-        file_name_and_range.push_str(&line_range_text);
-
-        let parent_name = parent.and_then(|parent| {
-            let parent = parent.trim_end_matches(path_style.separator());
-            let (_, parent) = path_style.split(parent);
-            if parent.is_empty() {
-                None
-            } else {
-                Some(SharedString::new(parent))
-            }
-        });
-
-        let icon_path = FileIcons::get_icon(Path::new(full_path), cx);
-
-        ContextFileExcerpt {
-            file_name_and_range: file_name_and_range.into(),
-            full_path_and_range: full_path_and_range.into(),
-            parent_name,
-            icon_path,
-        }
-    }
-
-    fn hover_view(&self, text: SharedString, cx: &mut App) -> Entity<ContextPillHover> {
-        let icon_path = self.icon_path.clone();
-        let full_path_and_range = self.full_path_and_range.clone();
-        ContextPillHover::new(cx, move |_, cx| {
-            v_flex()
-                .child(
-                    h_flex()
-                        .gap_0p5()
-                        .w_full()
-                        .max_w_full()
-                        .border_b_1()
-                        .border_color(cx.theme().colors().border.opacity(0.6))
-                        .children(
-                            icon_path
-                                .clone()
-                                .map(Icon::from_path)
-                                .map(|icon| icon.color(Color::Muted).size(IconSize::XSmall)),
-                        )
-                        .child(
-                            // TODO: make this truncate on the left.
-                            Label::new(full_path_and_range.clone())
-                                .size(LabelSize::Small)
-                                .ml_1(),
-                        ),
-                )
-                .child(
-                    div()
-                        .id("context-pill-hover-contents")
-                        .overflow_scroll()
-                        .max_w_128()
-                        .max_h_96()
-                        .child(Label::new(text.clone()).buffer_font(cx)),
-                )
-                .into_any_element()
-        })
-    }
-}
-
-struct ContextPillHover {
-    render_hover: Box<dyn Fn(&mut Window, &mut App) -> AnyElement>,
-}
-
-impl ContextPillHover {
-    fn new(
-        cx: &mut App,
-        render_hover: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
-    ) -> Entity<Self> {
-        cx.new(|_| Self {
-            render_hover: Box::new(render_hover),
-        })
-    }
-
-    fn new_text(content: SharedString, cx: &mut App) -> Entity<Self> {
-        Self::new(cx, move |_, _| {
-            div()
-                .id("context-pill-hover-contents")
-                .overflow_scroll()
-                .max_w_128()
-                .max_h_96()
-                .child(content.clone())
-                .into_any_element()
-        })
-    }
-}
-
-impl Render for ContextPillHover {
-    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
-        tooltip_container(cx, move |this, cx| {
-            this.occlude()
-                .on_mouse_move(|_, _, cx| cx.stop_propagation())
-                .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
-                .child((self.render_hover)(window, cx))
-        })
-    }
-}
-
-impl Component for AddedContext {
-    fn scope() -> ComponentScope {
-        ComponentScope::Agent
-    }
-
-    fn sort_name() -> &'static str {
-        "AddedContext"
-    }
-
-    fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
-        let mut next_context_id = ContextId::zero();
-        let image_ready = (
-            "Ready",
-            AddedContext::image(
-                ImageContext {
-                    context_id: next_context_id.post_inc(),
-                    project_path: None,
-                    full_path: None,
-                    original_image: Arc::new(Image::empty()),
-                    image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
-                },
-                None,
-                PathStyle::local(),
-                cx,
-            ),
-        );
-
-        let image_loading = (
-            "Loading",
-            AddedContext::image(
-                ImageContext {
-                    context_id: next_context_id.post_inc(),
-                    project_path: None,
-                    full_path: None,
-                    original_image: Arc::new(Image::empty()),
-                    image_task: cx
-                        .background_spawn(async move {
-                            smol::Timer::after(Duration::from_secs(60 * 5)).await;
-                            Some(LanguageModelImage::empty())
-                        })
-                        .shared(),
-                },
-                None,
-                PathStyle::local(),
-                cx,
-            ),
-        );
-
-        let image_error = (
-            "Error",
-            AddedContext::image(
-                ImageContext {
-                    context_id: next_context_id.post_inc(),
-                    project_path: None,
-                    full_path: None,
-                    original_image: Arc::new(Image::empty()),
-                    image_task: Task::ready(None).shared(),
-                },
-                None,
-                PathStyle::local(),
-                cx,
-            ),
-        );
-
-        Some(
-            v_flex()
-                .gap_6()
-                .children(
-                    vec![image_ready, image_loading, image_error]
-                        .into_iter()
-                        .map(|(text, context)| {
-                            single_example(
-                                text,
-                                ContextPill::added(context, false, false, None).into_any_element(),
-                            )
-                        }),
-                )
-                .into_any(),
-        )
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use gpui::App;
-    use language_model::{LanguageModel, fake_provider::FakeLanguageModel};
-    use std::sync::Arc;
-
-    #[gpui::test]
-    fn test_image_context_warning_for_unsupported_model(cx: &mut App) {
-        let model: Arc<dyn LanguageModel> = Arc::new(FakeLanguageModel::default());
-        assert!(!model.supports_images());
-
-        let image_context = ImageContext {
-            context_id: ContextId::zero(),
-            project_path: None,
-            original_image: Arc::new(Image::empty()),
-            image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
-            full_path: None,
-        };
-
-        let added_context =
-            AddedContext::image(image_context, Some(&model), PathStyle::local(), cx);
-
-        assert!(matches!(
-            added_context.status,
-            ContextStatus::Warning { .. }
-        ));
-
-        assert!(matches!(added_context.kind, ContextKind::Image));
-        assert_eq!(added_context.name.as_ref(), "Image");
-        assert!(added_context.parent.is_none());
-        assert!(added_context.icon_path.is_none());
-    }
-
-    #[gpui::test]
-    fn test_image_context_ready_for_no_model(cx: &mut App) {
-        let image_context = ImageContext {
-            context_id: ContextId::zero(),
-            project_path: None,
-            original_image: Arc::new(Image::empty()),
-            image_task: Task::ready(Some(LanguageModelImage::empty())).shared(),
-            full_path: None,
-        };
-
-        let added_context = AddedContext::image(image_context, None, PathStyle::local(), cx);
-
-        assert!(
-            matches!(added_context.status, ContextStatus::Ready),
-            "Expected ready status when no model provided"
-        );
-
-        assert!(matches!(added_context.kind, ContextKind::Image));
-        assert_eq!(added_context.name.as_ref(), "Image");
-        assert!(added_context.parent.is_none());
-        assert!(added_context.icon_path.is_none());
-    }
-}