agent_ui: Fix panic in message editor (#51918)

Bennet Bo Fenner created

## Context

Fixes ZED-59M

We could end panicking because of a double lease in message editor. This
could happen when pasting text and this line was executed:
`PromptCompletion::try_parse(line, offset_to_line,
&self.source.supported_modes(cx))`

Since self.source is the Entity<MessageEditor> in this case, we will try
to read message editor while it's being updated.

I took this as an opportunity to refactor `prompt_capabilities` and
`available_commands` which were both passed around as Rc<RefCell<...>>.
Now we have a single struct called `SessionCapabilities` which maintains
both and we pass that around wrapped in an `Arc<RwLock<...>>`
(`SharedSessionCapabilities`)

## Self-Review Checklist

<!-- Check before requesting review: -->
- [x] I've reviewed my own diff for quality, security, and reliability
- [x] Unsafe blocks (if any) have justifying comments
- [x] The content is consistent with the [UI/UX
checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist)
- [x] Tests cover the new/changed behavior
- [x] Performance impact has been considered and is acceptable

Release Notes:

- Fixed a crash when pasting text into the prompt editor in the agent
panel

Change summary

crates/agent_ui/src/conversation_view.rs             |  42 
crates/agent_ui/src/conversation_view/thread_view.rs |  25 
crates/agent_ui/src/entry_view_state.rs              |  24 
crates/agent_ui/src/message_editor.rs                | 361 +++++++++----
4 files changed, 299 insertions(+), 153 deletions(-)

Detailed changes

crates/agent_ui/src/conversation_view.rs 🔗

@@ -7,7 +7,7 @@ use acp_thread::{
 use acp_thread::{AgentConnection, Plan};
 use action_log::{ActionLog, ActionLogTelemetry, DiffStats};
 use agent::{NativeAgentServer, NativeAgentSessionList, SharedThread, ThreadStore};
-use agent_client_protocol::{self as acp, PromptCapabilities};
+use agent_client_protocol as acp;
 use agent_servers::AgentServer;
 #[cfg(test)]
 use agent_servers::AgentServerDelegate;
@@ -36,11 +36,13 @@ use gpui::{
 use language::Buffer;
 use language_model::LanguageModelRegistry;
 use markdown::{Markdown, MarkdownElement, MarkdownFont, MarkdownStyle};
+use parking_lot::RwLock;
 use project::{AgentId, AgentServerStore, Project, ProjectEntryId};
 use prompt_store::{PromptId, PromptStore};
+
+use crate::message_editor::SessionCapabilities;
 use rope::Point;
 use settings::{NotifyWhenAgentWaiting, Settings as _, SettingsStore};
-use std::cell::RefCell;
 use std::path::Path;
 use std::sync::Arc;
 use std::time::Instant;
@@ -588,11 +590,7 @@ impl ConversationView {
         if let Some(view) = self.active_thread() {
             view.update(cx, |this, cx| {
                 this.message_editor.update(cx, |editor, cx| {
-                    editor.set_command_state(
-                        this.prompt_capabilities.clone(),
-                        this.available_commands.clone(),
-                        cx,
-                    );
+                    editor.set_session_capabilities(this.session_capabilities.clone(), cx);
                 });
             });
         }
@@ -821,13 +819,13 @@ impl ConversationView {
         cx: &mut Context<Self>,
     ) -> Entity<ThreadView> {
         let agent_id = self.agent.agent_id();
-        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
-        let available_commands = Rc::new(RefCell::new(vec![]));
+        let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::new(
+            thread.read(cx).prompt_capabilities(),
+            vec![],
+        )));
 
         let action_log = thread.read(cx).action_log().clone();
 
-        prompt_capabilities.replace(thread.read(cx).prompt_capabilities());
-
         let entry_view_state = cx.new(|_| {
             EntryViewState::new(
                 self.workspace.clone(),
@@ -835,8 +833,7 @@ impl ConversationView {
                 self.thread_store.clone(),
                 history.as_ref().map(|h| h.downgrade()),
                 self.prompt_store.clone(),
-                prompt_capabilities.clone(),
-                available_commands.clone(),
+                session_capabilities.clone(),
                 self.agent.agent_id(),
             )
         });
@@ -995,8 +992,7 @@ impl ConversationView {
                 model_selector,
                 profile_selector,
                 list_state,
-                prompt_capabilities,
-                available_commands,
+                session_capabilities,
                 resumed_without_history,
                 self.project.downgrade(),
                 self.thread_store.clone(),
@@ -1411,8 +1407,9 @@ impl ConversationView {
                 if let Some(active) = self.thread_view(&thread_id) {
                     active.update(cx, |active, _cx| {
                         active
-                            .prompt_capabilities
-                            .replace(thread.read(_cx).prompt_capabilities());
+                            .session_capabilities
+                            .write()
+                            .set_prompt_capabilities(thread.read(_cx).prompt_capabilities());
                     });
                 }
             }
@@ -1437,7 +1434,10 @@ impl ConversationView {
                 let has_commands = !available_commands.is_empty();
                 if let Some(active) = self.active_thread() {
                     active.update(cx, |active, _cx| {
-                        active.available_commands.replace(available_commands);
+                        active
+                            .session_capabilities
+                            .write()
+                            .set_available_commands(available_commands);
                     });
                 }
 
@@ -2217,8 +2217,7 @@ impl ConversationView {
         let Some(thread) = connected.active_view() else {
             return;
         };
-        let prompt_capabilities = thread.read(cx).prompt_capabilities.clone();
-        let available_commands = thread.read(cx).available_commands.clone();
+        let session_capabilities = thread.read(cx).session_capabilities.clone();
 
         let current_count = thread.read(cx).queued_message_editors.len();
         let last_synced = thread.read(cx).last_synced_queue_length;
@@ -2257,8 +2256,7 @@ impl ConversationView {
                     None,
                     history.clone(),
                     None,
-                    prompt_capabilities.clone(),
-                    available_commands.clone(),
+                    session_capabilities.clone(),
                     agent_name.clone(),
                     "",
                     EditorMode::AutoHeight {

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

@@ -1,8 +1,11 @@
+use std::cell::RefCell;
+
 use acp_thread::ContentBlock;
 use cloud_api_types::{SubmitAgentThreadFeedbackBody, SubmitAgentThreadFeedbackCommentsBody};
 use editor::actions::OpenExcerpts;
 
 use crate::StartThreadIn;
+use crate::message_editor::SharedSessionCapabilities;
 use gpui::{Corner, List};
 use language_model::{LanguageModelEffortLevel, Speed};
 use settings::update_settings_file;
@@ -187,8 +190,7 @@ pub struct ThreadView {
     pub last_token_limit_telemetry: Option<acp_thread::TokenUsageRatio>,
     thread_feedback: ThreadFeedbackState,
     pub list_state: ListState,
-    pub prompt_capabilities: Rc<RefCell<PromptCapabilities>>,
-    pub available_commands: Rc<RefCell<Vec<agent_client_protocol::AvailableCommand>>>,
+    pub session_capabilities: SharedSessionCapabilities,
     /// Tracks which tool calls have their content/output expanded.
     /// Used for showing/hiding tool call results, terminal output, etc.
     pub expanded_tool_calls: HashSet<agent_client_protocol::ToolCallId>,
@@ -268,8 +270,7 @@ impl ThreadView {
         model_selector: Option<Entity<ModelSelectorPopover>>,
         profile_selector: Option<Entity<ProfileSelector>>,
         list_state: ListState,
-        prompt_capabilities: Rc<RefCell<PromptCapabilities>>,
-        available_commands: Rc<RefCell<Vec<agent_client_protocol::AvailableCommand>>>,
+        session_capabilities: SharedSessionCapabilities,
         resumed_without_history: bool,
         project: WeakEntity<Project>,
         thread_store: Option<Entity<ThreadStore>>,
@@ -300,8 +301,7 @@ impl ThreadView {
                 thread_store,
                 history.as_ref().map(|h| h.downgrade()),
                 prompt_store,
-                prompt_capabilities.clone(),
-                available_commands.clone(),
+                session_capabilities.clone(),
                 agent_id.clone(),
                 &placeholder,
                 editor::EditorMode::AutoHeight {
@@ -417,8 +417,7 @@ impl ThreadView {
             model_selector,
             profile_selector,
             list_state,
-            prompt_capabilities,
-            available_commands,
+            session_capabilities,
             resumed_without_history,
             _subscriptions: subscriptions,
             permission_dropdown_handle: PopoverMenuHandle::default(),
@@ -874,8 +873,9 @@ impl ThreadView {
             // Does the agent have a specific logout command? Prefer that in case they need to reset internal state.
             let logout_supported = text == "/logout"
                 && self
-                    .available_commands
-                    .borrow()
+                    .session_capabilities
+                    .read()
+                    .available_commands()
                     .iter()
                     .any(|command| command.name == "logout");
             if can_login && !logout_supported {
@@ -3575,8 +3575,9 @@ impl ThreadView {
     ) -> Entity<ContextMenu> {
         let message_editor = self.message_editor.clone();
         let workspace = self.workspace.clone();
-        let supports_images = self.prompt_capabilities.borrow().image;
-        let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context;
+        let session_capabilities = self.session_capabilities.read();
+        let supports_images = session_capabilities.supports_images();
+        let supports_embedded_context = session_capabilities.supports_embedded_context();
 
         let has_editor_selection = workspace
             .upgrade()

crates/agent_ui/src/entry_view_state.rs 🔗

@@ -1,9 +1,9 @@
-use std::{cell::RefCell, ops::Range, rc::Rc};
+use std::ops::Range;
 
 use super::thread_history::ThreadHistory;
 use acp_thread::{AcpThread, AgentThreadEntry};
 use agent::ThreadStore;
-use agent_client_protocol::{self as acp, ToolCallId};
+use agent_client_protocol::ToolCallId;
 use collections::HashMap;
 use editor::{Editor, EditorEvent, EditorMode, MinimapVisibility, SizingBehavior};
 use gpui::{
@@ -20,7 +20,7 @@ use theme::ThemeSettings;
 use ui::{Context, TextSize};
 use workspace::Workspace;
 
-use crate::message_editor::{MessageEditor, MessageEditorEvent};
+use crate::message_editor::{MessageEditor, MessageEditorEvent, SharedSessionCapabilities};
 
 pub struct EntryViewState {
     workspace: WeakEntity<Workspace>,
@@ -29,8 +29,7 @@ pub struct EntryViewState {
     history: Option<WeakEntity<ThreadHistory>>,
     prompt_store: Option<Entity<PromptStore>>,
     entries: Vec<Entry>,
-    prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
-    available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
+    session_capabilities: SharedSessionCapabilities,
     agent_id: AgentId,
 }
 
@@ -41,8 +40,7 @@ impl EntryViewState {
         thread_store: Option<Entity<ThreadStore>>,
         history: Option<WeakEntity<ThreadHistory>>,
         prompt_store: Option<Entity<PromptStore>>,
-        prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
-        available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
+        session_capabilities: SharedSessionCapabilities,
         agent_id: AgentId,
     ) -> Self {
         Self {
@@ -52,8 +50,7 @@ impl EntryViewState {
             history,
             prompt_store,
             entries: Vec::new(),
-            prompt_capabilities,
-            available_commands,
+            session_capabilities,
             agent_id,
         }
     }
@@ -94,8 +91,7 @@ impl EntryViewState {
                             self.thread_store.clone(),
                             self.history.clone(),
                             self.prompt_store.clone(),
-                            self.prompt_capabilities.clone(),
-                            self.available_commands.clone(),
+                            self.session_capabilities.clone(),
                             self.agent_id.clone(),
                             "Edit message - @ to include context",
                             editor::EditorMode::AutoHeight {
@@ -458,6 +454,7 @@ fn diff_editor_text_style_refinement(cx: &mut App) -> TextStyleRefinement {
 mod tests {
     use std::path::Path;
     use std::rc::Rc;
+    use std::sync::Arc;
 
     use acp_thread::{AgentConnection, StubAgentConnection};
     use agent_client_protocol as acp;
@@ -465,8 +462,10 @@ mod tests {
     use editor::RowInfo;
     use fs::FakeFs;
     use gpui::{AppContext as _, TestAppContext};
+    use parking_lot::RwLock;
 
     use crate::entry_view_state::EntryViewState;
+    use crate::message_editor::SessionCapabilities;
     use multi_buffer::MultiBufferRow;
     use pretty_assertions::assert_matches;
     use project::Project;
@@ -524,8 +523,7 @@ mod tests {
                 thread_store,
                 history,
                 None,
-                Default::default(),
-                Default::default(),
+                Arc::new(RwLock::new(SessionCapabilities::default())),
                 "Test Agent".into(),
             )
         });

crates/agent_ui/src/message_editor.rs 🔗

@@ -26,12 +26,13 @@ use gpui::{
     KeyContext, SharedString, Subscription, Task, TextStyle, WeakEntity,
 };
 use language::{Buffer, Language, language_settings::InlayHintKind};
+use parking_lot::RwLock;
 use project::AgentId;
 use project::{CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, Worktree};
 use prompt_store::PromptStore;
 use rope::Point;
 use settings::Settings;
-use std::{cell::RefCell, fmt::Write, ops::Range, rc::Rc, sync::Arc};
+use std::{fmt::Write, ops::Range, rc::Rc, sync::Arc};
 use theme::ThemeSettings;
 use ui::{ContextMenu, Disclosure, ElevationIndex, prelude::*};
 use util::paths::PathStyle;
@@ -39,41 +40,39 @@ use util::{ResultExt, debug_panic};
 use workspace::{CollaboratorId, Workspace};
 use zed_actions::agent::{Chat, PasteRaw};
 
-pub struct MessageEditor {
-    mention_set: Entity<MentionSet>,
-    editor: Entity<Editor>,
-    workspace: WeakEntity<Workspace>,
-    prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
-    available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
-    agent_id: AgentId,
-    thread_store: Option<Entity<ThreadStore>>,
-    _subscriptions: Vec<Subscription>,
-    _parse_slash_command_task: Task<()>,
+#[derive(Default)]
+pub struct SessionCapabilities {
+    prompt_capabilities: acp::PromptCapabilities,
+    available_commands: Vec<acp::AvailableCommand>,
 }
 
-#[derive(Clone, Debug)]
-pub enum MessageEditorEvent {
-    Send,
-    SendImmediately,
-    Cancel,
-    Focus,
-    LostFocus,
-    InputAttempted(Arc<str>),
-}
+impl SessionCapabilities {
+    pub fn new(
+        prompt_capabilities: acp::PromptCapabilities,
+        available_commands: Vec<acp::AvailableCommand>,
+    ) -> Self {
+        Self {
+            prompt_capabilities,
+            available_commands,
+        }
+    }
 
-impl EventEmitter<MessageEditorEvent> for MessageEditor {}
+    pub fn supports_images(&self) -> bool {
+        self.prompt_capabilities.image
+    }
 
-const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0);
+    pub fn supports_embedded_context(&self) -> bool {
+        self.prompt_capabilities.embedded_context
+    }
 
-impl PromptCompletionProviderDelegate for Entity<MessageEditor> {
-    fn supports_images(&self, cx: &App) -> bool {
-        self.read(cx).prompt_capabilities.borrow().image
+    pub fn available_commands(&self) -> &[acp::AvailableCommand] {
+        &self.available_commands
     }
 
-    fn supported_modes(&self, cx: &App) -> Vec<PromptContextType> {
+    fn supported_modes(&self, has_thread_store: bool) -> Vec<PromptContextType> {
         let mut supported = vec![PromptContextType::File, PromptContextType::Symbol];
-        if self.read(cx).prompt_capabilities.borrow().embedded_context {
-            if self.read(cx).thread_store.is_some() {
+        if self.prompt_capabilities.embedded_context {
+            if has_thread_store {
                 supported.push(PromptContextType::Thread);
             }
             supported.extend(&[
@@ -86,10 +85,8 @@ impl PromptCompletionProviderDelegate for Entity<MessageEditor> {
         supported
     }
 
-    fn available_commands(&self, cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
-        self.read(cx)
-            .available_commands
-            .borrow()
+    pub fn completion_commands(&self) -> Vec<crate::completion_provider::AvailableCommand> {
+        self.available_commands
             .iter()
             .map(|cmd| crate::completion_provider::AvailableCommand {
                 name: cmd.name.clone().into(),
@@ -99,11 +96,68 @@ impl PromptCompletionProviderDelegate for Entity<MessageEditor> {
             .collect()
     }
 
+    pub fn set_prompt_capabilities(&mut self, prompt_capabilities: acp::PromptCapabilities) {
+        self.prompt_capabilities = prompt_capabilities;
+    }
+
+    pub fn set_available_commands(&mut self, available_commands: Vec<acp::AvailableCommand>) {
+        self.available_commands = available_commands;
+    }
+}
+
+pub type SharedSessionCapabilities = Arc<RwLock<SessionCapabilities>>;
+
+struct MessageEditorCompletionDelegate {
+    session_capabilities: SharedSessionCapabilities,
+    has_thread_store: bool,
+    message_editor: WeakEntity<MessageEditor>,
+}
+
+impl PromptCompletionProviderDelegate for MessageEditorCompletionDelegate {
+    fn supports_images(&self, _cx: &App) -> bool {
+        self.session_capabilities.read().supports_images()
+    }
+
+    fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {
+        self.session_capabilities
+            .read()
+            .supported_modes(self.has_thread_store)
+    }
+
+    fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
+        self.session_capabilities.read().completion_commands()
+    }
+
     fn confirm_command(&self, cx: &mut App) {
-        self.update(cx, |this, cx| this.send(cx));
+        let _ = self.message_editor.update(cx, |this, cx| this.send(cx));
     }
 }
 
+pub struct MessageEditor {
+    mention_set: Entity<MentionSet>,
+    editor: Entity<Editor>,
+    workspace: WeakEntity<Workspace>,
+    session_capabilities: SharedSessionCapabilities,
+    agent_id: AgentId,
+    thread_store: Option<Entity<ThreadStore>>,
+    _subscriptions: Vec<Subscription>,
+    _parse_slash_command_task: Task<()>,
+}
+
+#[derive(Clone, Debug)]
+pub enum MessageEditorEvent {
+    Send,
+    SendImmediately,
+    Cancel,
+    Focus,
+    LostFocus,
+    InputAttempted(Arc<str>),
+}
+
+impl EventEmitter<MessageEditorEvent> for MessageEditor {}
+
+const COMMAND_HINT_INLAY_ID: InlayId = InlayId::Hint(0);
+
 impl MessageEditor {
     pub fn new(
         workspace: WeakEntity<Workspace>,
@@ -111,8 +165,7 @@ impl MessageEditor {
         thread_store: Option<Entity<ThreadStore>>,
         history: Option<WeakEntity<ThreadHistory>>,
         prompt_store: Option<Entity<PromptStore>>,
-        prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
-        available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
+        session_capabilities: SharedSessionCapabilities,
         agent_id: AgentId,
         placeholder: &str,
         mode: EditorMode,
@@ -164,7 +217,11 @@ impl MessageEditor {
         let mention_set =
             cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone()));
         let completion_provider = Rc::new(PromptCompletionProvider::new(
-            cx.entity(),
+            MessageEditorCompletionDelegate {
+                session_capabilities: session_capabilities.clone(),
+                has_thread_store: thread_store.is_some(),
+                message_editor: cx.weak_entity(),
+            },
             editor.downgrade(),
             mention_set.clone(),
             history,
@@ -234,8 +291,7 @@ impl MessageEditor {
             editor,
             mention_set,
             workspace,
-            prompt_capabilities,
-            available_commands,
+            session_capabilities,
             agent_id,
             thread_store,
             _subscriptions: subscriptions,
@@ -243,18 +299,17 @@ impl MessageEditor {
         }
     }
 
-    pub fn set_command_state(
+    pub fn set_session_capabilities(
         &mut self,
-        prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
-        available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
+        session_capabilities: SharedSessionCapabilities,
         _cx: &mut Context<Self>,
     ) {
-        self.prompt_capabilities = prompt_capabilities;
-        self.available_commands = available_commands;
+        self.session_capabilities = session_capabilities;
     }
 
     fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option<Inlay> {
-        let available_commands = self.available_commands.borrow();
+        let session_capabilities = self.session_capabilities.read();
+        let available_commands = session_capabilities.available_commands();
         if available_commands.is_empty() {
             return None;
         }
@@ -334,7 +389,7 @@ impl MessageEditor {
                 .text_anchor
         });
 
-        let supports_images = self.prompt_capabilities.borrow().image;
+        let supports_images = self.session_capabilities.read().supports_images();
 
         self.mention_set
             .update(cx, |mention_set, cx| {
@@ -415,7 +470,11 @@ impl MessageEditor {
         cx: &mut Context<Self>,
     ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
         let text = self.editor.read(cx).text(cx);
-        let available_commands = self.available_commands.borrow().clone();
+        let available_commands = self
+            .session_capabilities
+            .read()
+            .available_commands()
+            .to_vec();
         let agent_id = self.agent_id.clone();
         let build_task = self.build_content_blocks(full_mention_content, cx);
 
@@ -442,7 +501,8 @@ impl MessageEditor {
             .mention_set
             .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;
+        let supports_embedded_context =
+            self.session_capabilities.read().supports_embedded_context();
 
         cx.spawn(async move |_, cx| {
             let contents = contents.await?;
@@ -822,7 +882,7 @@ impl MessageEditor {
                 }
 
                 if !all_mentions.is_empty() {
-                    let supports_images = self.prompt_capabilities.borrow().image;
+                    let supports_images = self.session_capabilities.read().supports_images();
                     let http_client = workspace.read(cx).client().http_client();
 
                     for (anchor, content_len, mention_uri) in all_mentions {
@@ -881,7 +941,7 @@ impl MessageEditor {
             })
             .unwrap_or(false);
 
-        if self.prompt_capabilities.borrow().image
+        if self.session_capabilities.read().supports_images()
             && has_non_text_content
             && let Some(task) = paste_images_as_context(
                 self.editor.clone(),
@@ -958,7 +1018,7 @@ impl MessageEditor {
                     cx,
                 );
             });
-            let supports_images = self.prompt_capabilities.borrow().image;
+            let supports_images = self.session_capabilities.read().supports_images();
             tasks.push(self.mention_set.update(cx, |mention_set, cx| {
                 mention_set.confirm_mention_completion(
                     file_name,
@@ -1213,7 +1273,7 @@ impl MessageEditor {
             return;
         };
         let Some(completion) =
-            PromptCompletionProvider::<Entity<MessageEditor>>::completion_for_action(
+            PromptCompletionProvider::<MessageEditorCompletionDelegate>::completion_for_action(
                 PromptContextAction::AddSelections,
                 anchor..anchor,
                 self.editor.downgrade(),
@@ -1235,7 +1295,7 @@ impl MessageEditor {
     }
 
     pub fn add_images_from_picker(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if !self.prompt_capabilities.borrow().image {
+        if !self.session_capabilities.read().supports_images() {
             return;
         }
 
@@ -1662,7 +1722,7 @@ fn find_matching_bracket(text: &str, open: char, close: char) -> Option<usize> {
 
 #[cfg(test)]
 mod tests {
-    use std::{cell::RefCell, ops::Range, path::Path, rc::Rc, sync::Arc};
+    use std::{ops::Range, path::Path, sync::Arc};
 
     use acp_thread::MentionUri;
     use agent::{ThreadStore, outline};
@@ -1680,6 +1740,7 @@ mod tests {
     };
     use language_model::LanguageModelRegistry;
     use lsp::{CompletionContext, CompletionTriggerKind};
+    use parking_lot::RwLock;
     use project::{CompletionIntent, Project, ProjectPath};
     use serde_json::json;
 
@@ -1688,10 +1749,10 @@ mod tests {
     use util::{path, paths::PathStyle, rel_path::rel_path};
     use workspace::{AppState, Item, MultiWorkspace};
 
-    use crate::completion_provider::{PromptCompletionProviderDelegate, PromptContextType};
+    use crate::completion_provider::PromptContextType;
     use crate::{
         conversation_view::tests::init_test,
-        message_editor::{Mention, MessageEditor, parse_mention_links},
+        message_editor::{Mention, MessageEditor, SessionCapabilities, parse_mention_links},
     };
 
     #[test]
@@ -1809,7 +1870,6 @@ mod tests {
                     None,
                     None,
                     Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -1904,9 +1964,10 @@ mod tests {
 
         let project = Project::test(fs.clone(), ["/test".as_ref()], cx).await;
         let thread_store = None;
-        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
-        // Start with no available commands - simulating Claude which doesn't support slash commands
-        let available_commands = Rc::new(RefCell::new(vec![]));
+        let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::new(
+            acp::PromptCapabilities::default(),
+            vec![],
+        )));
 
         let (multi_workspace, cx) =
             cx.add_window_view(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
@@ -1920,8 +1981,7 @@ mod tests {
                     thread_store.clone(),
                     None,
                     None,
-                    prompt_capabilities.clone(),
-                    available_commands.clone(),
+                    session_capabilities.clone(),
                     "Claude Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -1951,7 +2011,9 @@ mod tests {
         assert!(error_message.contains("Available commands: none"));
 
         // Now simulate Claude providing its list of available commands (which doesn't include file)
-        available_commands.replace(vec![acp::AvailableCommand::new("help", "Get help")]);
+        session_capabilities
+            .write()
+            .set_available_commands(vec![acp::AvailableCommand::new("help", "Get help")]);
 
         // Test that unsupported slash commands trigger an error when we have a list of available commands
         editor.update_in(cx, |editor, window, cx| {
@@ -2065,15 +2127,17 @@ mod tests {
         let mut cx = VisualTestContext::from_window(window.into(), cx);
 
         let thread_store = None;
-        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
-        let available_commands = Rc::new(RefCell::new(vec![
-            acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
-            acp::AvailableCommand::new("say-hello", "Say hello to whoever you want").input(
-                acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput::new(
-                    "<name>",
-                )),
-            ),
-        ]));
+        let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::new(
+            acp::PromptCapabilities::default(),
+            vec![
+                acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
+                acp::AvailableCommand::new("say-hello", "Say hello to whoever you want").input(
+                    acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput::new(
+                        "<name>",
+                    )),
+                ),
+            ],
+        )));
 
         let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
             let workspace_handle = cx.weak_entity();
@@ -2084,8 +2148,7 @@ mod tests {
                     thread_store.clone(),
                     None,
                     None,
-                    prompt_capabilities.clone(),
-                    available_commands.clone(),
+                    session_capabilities.clone(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -2298,7 +2361,10 @@ mod tests {
         }
 
         let thread_store = cx.new(|cx| ThreadStore::new(cx));
-        let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
+        let session_capabilities = Arc::new(RwLock::new(SessionCapabilities::new(
+            acp::PromptCapabilities::default(),
+            vec![],
+        )));
 
         let (message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
             let workspace_handle = cx.weak_entity();
@@ -2309,8 +2375,7 @@ mod tests {
                     Some(thread_store),
                     None,
                     None,
-                    prompt_capabilities.clone(),
-                    Default::default(),
+                    session_capabilities.clone(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -2356,12 +2421,14 @@ mod tests {
             editor.set_text("", window, cx);
         });
 
-        prompt_capabilities.replace(
-            acp::PromptCapabilities::new()
-                .image(true)
-                .audio(true)
-                .embedded_context(true),
-        );
+        message_editor.update(&mut cx, |editor, _cx| {
+            editor.session_capabilities.write().set_prompt_capabilities(
+                acp::PromptCapabilities::new()
+                    .image(true)
+                    .audio(true)
+                    .embedded_context(true),
+            );
+        });
 
         cx.simulate_input("Lorem ");
 
@@ -2802,7 +2869,6 @@ mod tests {
                     None,
                     None,
                     Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -2814,8 +2880,9 @@ mod tests {
                 );
                 // Enable embedded context so files are actually included
                 editor
-                    .prompt_capabilities
-                    .replace(acp::PromptCapabilities::new().embedded_context(true));
+                    .session_capabilities
+                    .write()
+                    .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true));
                 editor
             })
         });
@@ -2904,7 +2971,6 @@ mod tests {
                     None,
                     None,
                     Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -2975,7 +3041,6 @@ mod tests {
                     None,
                     None,
                     Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -3030,7 +3095,6 @@ mod tests {
                     None,
                     None,
                     Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -3045,13 +3109,19 @@ mod tests {
 
         message_editor.update(cx, |editor, _cx| {
             editor
-                .prompt_capabilities
-                .replace(acp::PromptCapabilities::new().embedded_context(true));
+                .session_capabilities
+                .write()
+                .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true));
         });
 
         let supported_modes = {
             let app = cx.app.borrow();
-            message_editor.supported_modes(&app)
+            let _ = &app;
+            message_editor
+                .read(&app)
+                .session_capabilities
+                .read()
+                .supported_modes(false)
         };
 
         assert!(
@@ -3083,7 +3153,6 @@ mod tests {
                     None,
                     None,
                     Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -3098,13 +3167,19 @@ mod tests {
 
         message_editor.update(cx, |editor, _cx| {
             editor
-                .prompt_capabilities
-                .replace(acp::PromptCapabilities::new().embedded_context(true));
+                .session_capabilities
+                .write()
+                .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true));
         });
 
         let supported_modes = {
             let app = cx.app.borrow();
-            message_editor.supported_modes(&app)
+            let _ = &app;
+            message_editor
+                .read(&app)
+                .session_capabilities
+                .read()
+                .supported_modes(true)
         };
 
         assert!(
@@ -3137,7 +3212,6 @@ mod tests {
                     None,
                     None,
                     Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -3201,12 +3275,11 @@ mod tests {
                     None,
                     None,
                     Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
-                        max_lines: None,
                         min_lines: 1,
+                        max_lines: None,
                     },
                     window,
                     cx,
@@ -3258,8 +3331,9 @@ mod tests {
 
         message_editor.update(cx, |editor, _cx| {
             editor
-                .prompt_capabilities
-                .replace(acp::PromptCapabilities::new().embedded_context(true))
+                .session_capabilities
+                .write()
+                .set_prompt_capabilities(acp::PromptCapabilities::new().embedded_context(true))
         });
 
         let content = message_editor
@@ -3362,7 +3436,6 @@ mod tests {
                     None,
                     None,
                     Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::full(),
@@ -3474,11 +3547,10 @@ mod tests {
                 MessageEditor::new(
                     workspace_handle,
                     project.downgrade(),
-                    Some(thread_store),
+                    Some(thread_store.clone()),
                     None,
                     None,
                     Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -3559,7 +3631,6 @@ mod tests {
                     None,
                     None,
                     Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -3619,6 +3690,86 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_paste_mention_link_with_completion_trigger_does_not_panic(
+        cx: &mut TestAppContext,
+    ) {
+        init_test(cx);
+
+        let app_state = cx.update(AppState::test);
+
+        cx.update(|cx| {
+            editor::init(cx);
+            workspace::init(app_state.clone(), cx);
+        });
+
+        app_state
+            .fs
+            .as_fake()
+            .insert_tree(path!("/project"), json!({"file.txt": "content"}))
+            .await;
+
+        let project = Project::test(app_state.fs.clone(), [path!("/project").as_ref()], cx).await;
+        let window =
+            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
+        let workspace = window
+            .read_with(cx, |mw, _| mw.workspace().clone())
+            .unwrap();
+
+        let mut cx = VisualTestContext::from_window(window.into(), cx);
+
+        let thread_store = cx.new(|cx| ThreadStore::new(cx));
+
+        let (_message_editor, editor) = workspace.update_in(&mut cx, |workspace, window, cx| {
+            let workspace_handle = cx.weak_entity();
+            let message_editor = cx.new(|cx| {
+                MessageEditor::new(
+                    workspace_handle,
+                    project.downgrade(),
+                    Some(thread_store),
+                    None,
+                    None,
+                    Default::default(),
+                    "Test Agent".into(),
+                    "Test",
+                    EditorMode::AutoHeight {
+                        max_lines: None,
+                        min_lines: 1,
+                    },
+                    window,
+                    cx,
+                )
+            });
+            workspace.active_pane().update(cx, |pane, cx| {
+                pane.add_item(
+                    Box::new(cx.new(|_| MessageEditorItem(message_editor.clone()))),
+                    true,
+                    true,
+                    None,
+                    window,
+                    cx,
+                );
+            });
+            message_editor.read(cx).focus_handle(cx).focus(window, cx);
+            let editor = message_editor.read(cx).editor().clone();
+            (message_editor, editor)
+        });
+
+        cx.simulate_input("@");
+
+        editor.update(&mut cx, |editor, cx| {
+            assert_eq!(editor.text(cx), "@");
+            assert!(editor.has_visible_completions_menu());
+        });
+
+        cx.write_to_clipboard(ClipboardItem::new_string("[@f](file:///test.txt) @".into()));
+        cx.dispatch_action(Paste);
+
+        editor.update(&mut cx, |editor, cx| {
+            assert!(editor.text(cx).contains("[@f](file:///test.txt)"));
+        });
+    }
+
     // Helper that creates a minimal MessageEditor inside a window, returning both
     // the entity and the underlying VisualTestContext so callers can drive updates.
     async fn setup_message_editor(
@@ -3641,7 +3792,6 @@ mod tests {
                     None,
                     None,
                     Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -3792,7 +3942,6 @@ mod tests {
                     None,
                     None,
                     Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {