agent_ui: Refactor `AcpThreadView` (#48339)

Bennet Bo Fenner , cameron , Ben Brandt , and Smit Barmase created

Release Notes:

- N/A

---------

Co-authored-by: cameron <cameron.studdstreet@gmail.com>
Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com>
Co-authored-by: Smit Barmase <heysmitbarmase@gmail.com>

Change summary

Cargo.lock                                           |    2 
crates/agent_ui/Cargo.toml                           |    4 
crates/agent_ui/src/acp/entry_view_state.rs          |   13 
crates/agent_ui/src/acp/message_editor.rs            |  257 -
crates/agent_ui/src/acp/thread_view.rs               | 1002 +-----
crates/agent_ui/src/acp/thread_view/active_thread.rs |  664 +++
crates/agent_ui/src/agent_panel.rs                   |   47 
crates/agent_ui/src/agent_ui.rs                      |    1 
crates/agent_ui/src/completion_provider.rs           |  211 -
crates/agent_ui/src/text_thread_editor.rs            |  159 -
crates/agent_ui/src/user_slash_command.rs            | 2118 --------------
crates/agent_ui_v2/src/agent_thread_pane.rs          |    2 
crates/feature_flags/src/flags.rs                    |    6 
crates/zed/src/visual_test_runner.rs                 |    4 
14 files changed, 790 insertions(+), 3,700 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -369,7 +369,6 @@ dependencies = [
  "fs",
  "futures 0.3.31",
  "fuzzy",
- "git_ui",
  "gpui",
  "gpui_tokio",
  "html_to_markdown",
@@ -400,6 +399,7 @@ dependencies = [
  "rand 0.9.2",
  "recent_projects",
  "release_channel",
+ "remote_connection",
  "reqwest_client",
  "rope",
  "rules_library",

crates/agent_ui/Cargo.toml πŸ”—

@@ -18,7 +18,6 @@ test-support = [
     "acp_thread/test-support",
     "eval_utils",
     "gpui/test-support",
-    "git_ui/test-support",
     "language/test-support",
     "reqwest_client",
     "workspace/test-support",
@@ -59,7 +58,6 @@ feature_flags.workspace = true
 file_icons.workspace = true
 fs.workspace = true
 futures.workspace = true
-git_ui.workspace = true
 fuzzy.workspace = true
 gpui.workspace = true
 gpui_tokio.workspace = true
@@ -127,7 +125,6 @@ db = { workspace = true, features = ["test-support"] }
 editor = { workspace = true, features = ["test-support"] }
 eval_utils.workspace = true
 gpui = { workspace = true, "features" = ["test-support"] }
-git_ui = { workspace = true, features = ["test-support"] }
 indoc.workspace = true
 language = { workspace = true, "features" = ["test-support"] }
 languages = { workspace = true, features = ["test-support"] }
@@ -135,6 +132,7 @@ language_model = { workspace = true, "features" = ["test-support"] }
 pretty_assertions.workspace = true
 project = { workspace = true, features = ["test-support"] }
 recent_projects = { workspace = true, features = ["test-support"] }
+remote_connection = { workspace = true, features = ["test-support"] }
 title_bar = { workspace = true, features = ["test-support"] }
 semver.workspace = true
 reqwest_client.workspace = true

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

@@ -1,7 +1,6 @@
 use std::{cell::RefCell, ops::Range, rc::Rc};
 
 use super::thread_history::AcpThreadHistory;
-use crate::user_slash_command::{CommandLoadError, UserSlashCommand};
 use acp_thread::{AcpThread, AgentThreadEntry};
 use agent::ThreadStore;
 use agent_client_protocol::{self as acp, ToolCallId};
@@ -31,8 +30,6 @@ pub struct EntryViewState {
     entries: Vec<Entry>,
     prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
     available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
-    cached_user_commands: Rc<RefCell<HashMap<String, UserSlashCommand>>>,
-    cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
     agent_name: SharedString,
 }
 
@@ -45,8 +42,6 @@ impl EntryViewState {
         prompt_store: Option<Entity<PromptStore>>,
         prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
         available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
-        cached_user_commands: Rc<RefCell<HashMap<String, UserSlashCommand>>>,
-        cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
         agent_name: SharedString,
     ) -> Self {
         Self {
@@ -58,8 +53,6 @@ impl EntryViewState {
             entries: Vec::new(),
             prompt_capabilities,
             available_commands,
-            cached_user_commands,
-            cached_user_command_errors,
             agent_name,
         }
     }
@@ -93,7 +86,7 @@ impl EntryViewState {
                     }
                 } else {
                     let message_editor = cx.new(|cx| {
-                        let mut editor = MessageEditor::new_with_cache(
+                        let mut editor = MessageEditor::new(
                             self.workspace.clone(),
                             self.project.clone(),
                             self.thread_store.clone(),
@@ -101,8 +94,6 @@ impl EntryViewState {
                             self.prompt_store.clone(),
                             self.prompt_capabilities.clone(),
                             self.available_commands.clone(),
-                            self.cached_user_commands.clone(),
-                            self.cached_user_command_errors.clone(),
                             self.agent_name.clone(),
                             "Edit message - @ to include context",
                             editor::EditorMode::AutoHeight {
@@ -478,8 +469,6 @@ mod tests {
                 None,
                 Default::default(),
                 Default::default(),
-                Default::default(),
-                Default::default(),
                 "Test Agent".into(),
             )
         });

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

@@ -9,7 +9,6 @@ use crate::{
     mention_set::{
         Mention, MentionImage, MentionSet, insert_crease_for_mention, paste_images_as_context,
     },
-    user_slash_command::{self, CommandLoadError, UserSlashCommand},
 };
 use acp_thread::{AgentSessionInfo, MentionUri};
 use agent::ThreadStore;
@@ -22,7 +21,6 @@ use editor::{
     MultiBufferSnapshot, ToOffset, actions::Paste, code_context_menus::CodeContextMenu,
     scroll::Autoscroll,
 };
-use feature_flags::{FeatureFlagAppExt as _, UserSlashCommandsFeatureFlag};
 use futures::{FutureExt as _, future::join_all};
 use gpui::{
     AppContext, ClipboardEntry, Context, Entity, EventEmitter, FocusHandle, Focusable, ImageFormat,
@@ -40,25 +38,12 @@ use util::{ResultExt, debug_panic};
 use workspace::{CollaboratorId, Workspace};
 use zed_actions::agent::{Chat, PasteRaw};
 
-enum UserSlashCommands {
-    Cached {
-        commands: collections::HashMap<String, user_slash_command::UserSlashCommand>,
-        errors: Vec<user_slash_command::CommandLoadError>,
-    },
-    FromFs {
-        fs: Arc<dyn fs::Fs>,
-        worktree_roots: Vec<std::path::PathBuf>,
-    },
-}
-
 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>>>,
-    cached_user_commands: Rc<RefCell<collections::HashMap<String, UserSlashCommand>>>,
-    cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
     agent_name: SharedString,
     thread_store: Option<Entity<ThreadStore>>,
     _subscriptions: Vec<Subscription>,
@@ -107,7 +92,6 @@ impl PromptCompletionProviderDelegate for Entity<MessageEditor> {
                 name: cmd.name.clone().into(),
                 description: cmd.description.clone().into(),
                 requires_argument: cmd.input.is_some(),
-                source: crate::completion_provider::CommandSource::Server,
             })
             .collect()
     }
@@ -115,27 +99,6 @@ impl PromptCompletionProviderDelegate for Entity<MessageEditor> {
     fn confirm_command(&self, cx: &mut App) {
         self.update(cx, |this, cx| this.send(cx));
     }
-
-    fn cached_user_commands(
-        &self,
-        cx: &App,
-    ) -> Option<collections::HashMap<String, UserSlashCommand>> {
-        let commands = self.read(cx).cached_user_commands.borrow();
-        if commands.is_empty() {
-            None
-        } else {
-            Some(commands.clone())
-        }
-    }
-
-    fn cached_user_command_errors(&self, cx: &App) -> Option<Vec<CommandLoadError>> {
-        let errors = self.read(cx).cached_user_command_errors.borrow();
-        if errors.is_empty() {
-            None
-        } else {
-            Some(errors.clone())
-        }
-    }
 }
 
 impl MessageEditor {
@@ -152,42 +115,6 @@ impl MessageEditor {
         mode: EditorMode,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) -> Self {
-        let cached_user_commands = Rc::new(RefCell::new(collections::HashMap::default()));
-        let cached_user_command_errors = Rc::new(RefCell::new(Vec::new()));
-        Self::new_with_cache(
-            workspace,
-            project,
-            thread_store,
-            history,
-            prompt_store,
-            prompt_capabilities,
-            available_commands,
-            cached_user_commands,
-            cached_user_command_errors,
-            agent_name,
-            placeholder,
-            mode,
-            window,
-            cx,
-        )
-    }
-
-    pub fn new_with_cache(
-        workspace: WeakEntity<Workspace>,
-        project: WeakEntity<Project>,
-        thread_store: Option<Entity<ThreadStore>>,
-        history: WeakEntity<AcpThreadHistory>,
-        prompt_store: Option<Entity<PromptStore>>,
-        prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
-        available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
-        cached_user_commands: Rc<RefCell<collections::HashMap<String, UserSlashCommand>>>,
-        cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
-        agent_name: SharedString,
-        placeholder: &str,
-        mode: EditorMode,
-        window: &mut Window,
-        cx: &mut Context<Self>,
     ) -> Self {
         let language = Language::new(
             language::LanguageConfig {
@@ -293,8 +220,6 @@ impl MessageEditor {
             workspace,
             prompt_capabilities,
             available_commands,
-            cached_user_commands,
-            cached_user_command_errors,
             agent_name,
             thread_store,
             _subscriptions: subscriptions,
@@ -306,14 +231,10 @@ impl MessageEditor {
         &mut self,
         prompt_capabilities: Rc<RefCell<acp::PromptCapabilities>>,
         available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
-        cached_user_commands: Rc<RefCell<collections::HashMap<String, UserSlashCommand>>>,
-        cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
         _cx: &mut Context<Self>,
     ) {
         self.prompt_capabilities = prompt_capabilities;
         self.available_commands = available_commands;
-        self.cached_user_commands = cached_user_commands;
-        self.cached_user_command_errors = cached_user_command_errors;
     }
 
     fn command_hint(&self, snapshot: &MultiBufferSnapshot) -> Option<Inlay> {
@@ -477,48 +398,11 @@ impl MessageEditor {
         &self,
         full_mention_content: bool,
         cx: &mut Context<Self>,
-    ) -> Task<Result<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>> {
-        self.contents_with_cache(full_mention_content, None, None, cx)
-    }
-
-    pub fn contents_with_cache(
-        &self,
-        full_mention_content: bool,
-        cached_user_commands: Option<
-            collections::HashMap<String, user_slash_command::UserSlashCommand>,
-        >,
-        cached_user_command_errors: Option<Vec<user_slash_command::CommandLoadError>>,
-        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 agent_name = self.agent_name.clone();
 
-        let user_slash_commands = if !cx.has_flag::<UserSlashCommandsFeatureFlag>() {
-            UserSlashCommands::Cached {
-                commands: collections::HashMap::default(),
-                errors: Vec::new(),
-            }
-        } else if let Some(cached) = cached_user_commands {
-            UserSlashCommands::Cached {
-                commands: cached,
-                errors: cached_user_command_errors.unwrap_or_default(),
-            }
-        } else if let Some(workspace) = self.workspace.upgrade() {
-            let fs = workspace.read(cx).project().read(cx).fs().clone();
-            let worktree_roots: Vec<std::path::PathBuf> = workspace
-                .read(cx)
-                .visible_worktrees(cx)
-                .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
-                .collect();
-            UserSlashCommands::FromFs { fs, worktree_roots }
-        } else {
-            UserSlashCommands::Cached {
-                commands: collections::HashMap::default(),
-                errors: Vec::new(),
-            }
-        };
-
         let contents = self
             .mention_set
             .update(cx, |store, cx| store.contents(full_mention_content, cx));
@@ -526,58 +410,7 @@ impl MessageEditor {
         let supports_embedded_context = self.prompt_capabilities.borrow().embedded_context;
 
         cx.spawn(async move |_, cx| {
-            let (mut user_commands, mut user_command_errors) = match user_slash_commands {
-                UserSlashCommands::Cached { commands, errors } => (commands, errors),
-                UserSlashCommands::FromFs { fs, worktree_roots } => {
-                    let load_result =
-                        user_slash_command::load_all_commands_async(&fs, &worktree_roots).await;
-
-                    (
-                        user_slash_command::commands_to_map(&load_result.commands),
-                        load_result.errors,
-                    )
-                }
-            };
-
-            let server_command_names = available_commands
-                .iter()
-                .map(|command| command.name.clone())
-                .collect::<HashSet<_>>();
-            user_slash_command::apply_server_command_conflicts_to_map(
-                &mut user_commands,
-                &mut user_command_errors,
-                &server_command_names,
-            );
-
-            // Check if the user is trying to use an errored slash command.
-            // If so, report the error to the user.
-            if let Some(parsed) = user_slash_command::try_parse_user_command(&text) {
-                for error in &user_command_errors {
-                    if let Some(error_cmd_name) = error.command_name() {
-                        if error_cmd_name == parsed.name {
-                            return Err(anyhow::anyhow!(
-                                "Failed to load /{}: {}",
-                                parsed.name,
-                                error.message
-                            ));
-                        }
-                    }
-                }
-            }
-            // Errors for commands that don't match the user's input are silently ignored here,
-            // since the user will see them via the error callout in the thread view.
-
-            // Check if this is a user-defined slash command and expand it
-            match user_slash_command::try_expand_from_commands(&text, &user_commands) {
-                Ok(Some(expanded)) => return Ok((vec![expanded.into()], Vec::new())),
-                Err(err) => return Err(err),
-                Ok(None) => {} // Not a user command, continue with normal processing
-            }
-
-            if let Err(err) = Self::validate_slash_commands(&text, &available_commands, &agent_name)
-            {
-                return Err(err);
-            }
+            Self::validate_slash_commands(&text, &available_commands, &agent_name)?;
 
             let contents = contents.await?;
             let mut all_tracked_buffers = Vec::new();
@@ -1497,7 +1330,7 @@ mod tests {
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
-                MessageEditor::new_with_cache(
+                MessageEditor::new(
                     workspace.downgrade(),
                     project.downgrade(),
                     thread_store.clone(),
@@ -1505,8 +1338,6 @@ mod tests {
                     None,
                     Default::default(),
                     Default::default(),
-                    Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -1574,9 +1405,7 @@ mod tests {
         });
 
         let (content, _) = message_editor
-            .update(cx, |message_editor, cx| {
-                message_editor.contents_with_cache(false, None, None, cx)
-            })
+            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
             .await
             .unwrap();
 
@@ -1614,7 +1443,7 @@ mod tests {
         let workspace_handle = workspace.downgrade();
         let message_editor = workspace.update_in(cx, |_, window, cx| {
             cx.new(|cx| {
-                MessageEditor::new_with_cache(
+                MessageEditor::new(
                     workspace_handle.clone(),
                     project.downgrade(),
                     thread_store.clone(),
@@ -1622,8 +1451,6 @@ mod tests {
                     None,
                     prompt_capabilities.clone(),
                     available_commands.clone(),
-                    Default::default(),
-                    Default::default(),
                     "Claude Code".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -1643,9 +1470,7 @@ mod tests {
         });
 
         let contents_result = message_editor
-            .update(cx, |message_editor, cx| {
-                message_editor.contents_with_cache(false, None, None, cx)
-            })
+            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
             .await;
 
         // Should fail because available_commands is empty (no commands supported)
@@ -1663,9 +1488,7 @@ mod tests {
         });
 
         let contents_result = message_editor
-            .update(cx, |message_editor, cx| {
-                message_editor.contents_with_cache(false, None, None, cx)
-            })
+            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
             .await;
 
         assert!(contents_result.is_err());
@@ -1680,9 +1503,7 @@ mod tests {
         });
 
         let contents_result = message_editor
-            .update(cx, |message_editor, cx| {
-                message_editor.contents_with_cache(false, None, None, cx)
-            })
+            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
             .await;
 
         // Should succeed because /help is in available_commands
@@ -1694,9 +1515,7 @@ mod tests {
         });
 
         let (content, _) = message_editor
-            .update(cx, |message_editor, cx| {
-                message_editor.contents_with_cache(false, None, None, cx)
-            })
+            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
             .await
             .unwrap();
 
@@ -1714,9 +1533,7 @@ mod tests {
 
         // The @ mention functionality should not be affected
         let (content, _) = message_editor
-            .update(cx, |message_editor, cx| {
-                message_editor.contents_with_cache(false, None, None, cx)
-            })
+            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
             .await
             .unwrap();
 
@@ -1789,7 +1606,7 @@ mod tests {
         let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
             let workspace_handle = cx.weak_entity();
             let message_editor = cx.new(|cx| {
-                MessageEditor::new_with_cache(
+                MessageEditor::new(
                     workspace_handle,
                     project.downgrade(),
                     thread_store.clone(),
@@ -1797,8 +1614,6 @@ mod tests {
                     None,
                     prompt_capabilities.clone(),
                     available_commands.clone(),
-                    Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -2015,7 +1830,7 @@ mod tests {
         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_with_cache(
+                MessageEditor::new(
                     workspace_handle,
                     project.downgrade(),
                     Some(thread_store),
@@ -2023,8 +1838,6 @@ mod tests {
                     None,
                     prompt_capabilities.clone(),
                     Default::default(),
-                    Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -2510,7 +2323,7 @@ mod tests {
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
-                let editor = MessageEditor::new_with_cache(
+                let editor = MessageEditor::new(
                     workspace.downgrade(),
                     project.downgrade(),
                     thread_store.clone(),
@@ -2518,8 +2331,6 @@ mod tests {
                     None,
                     Default::default(),
                     Default::default(),
-                    Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -2621,7 +2432,7 @@ mod tests {
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
-                let mut editor = MessageEditor::new_with_cache(
+                let mut editor = MessageEditor::new(
                     workspace.downgrade(),
                     project.downgrade(),
                     thread_store.clone(),
@@ -2629,8 +2440,6 @@ mod tests {
                     None,
                     Default::default(),
                     Default::default(),
-                    Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -2703,7 +2512,7 @@ mod tests {
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
-                let mut editor = MessageEditor::new_with_cache(
+                let mut editor = MessageEditor::new(
                     workspace.downgrade(),
                     project.downgrade(),
                     thread_store.clone(),
@@ -2711,8 +2520,6 @@ mod tests {
                     None,
                     Default::default(),
                     Default::default(),
-                    Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -2756,7 +2563,7 @@ mod tests {
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
-                MessageEditor::new_with_cache(
+                MessageEditor::new(
                     workspace.downgrade(),
                     project.downgrade(),
                     thread_store.clone(),
@@ -2764,8 +2571,6 @@ mod tests {
                     None,
                     Default::default(),
                     Default::default(),
-                    Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -2812,7 +2617,7 @@ mod tests {
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
-                MessageEditor::new_with_cache(
+                MessageEditor::new(
                     workspace.downgrade(),
                     project.downgrade(),
                     thread_store.clone(),
@@ -2820,8 +2625,6 @@ mod tests {
                     None,
                     Default::default(),
                     Default::default(),
-                    Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -2869,7 +2672,7 @@ mod tests {
 
         let message_editor = cx.update(|window, cx| {
             cx.new(|cx| {
-                MessageEditor::new_with_cache(
+                MessageEditor::new(
                     workspace.downgrade(),
                     project.downgrade(),
                     thread_store.clone(),
@@ -2877,8 +2680,6 @@ mod tests {
                     None,
                     Default::default(),
                     Default::default(),
-                    Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -2899,9 +2700,7 @@ mod tests {
         });
 
         let (content, _) = message_editor
-            .update(cx, |message_editor, cx| {
-                message_editor.contents_with_cache(false, None, None, cx)
-            })
+            .update(cx, |message_editor, cx| message_editor.contents(false, cx))
             .await
             .unwrap();
 
@@ -2938,7 +2737,7 @@ mod tests {
         let (message_editor, editor) = workspace.update_in(cx, |workspace, window, cx| {
             let workspace_handle = cx.weak_entity();
             let message_editor = cx.new(|cx| {
-                MessageEditor::new_with_cache(
+                MessageEditor::new(
                     workspace_handle,
                     project.downgrade(),
                     thread_store.clone(),
@@ -2946,8 +2745,6 @@ mod tests {
                     None,
                     Default::default(),
                     Default::default(),
-                    Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {
@@ -2982,9 +2779,7 @@ mod tests {
         });
 
         let content = message_editor
-            .update(cx, |editor, cx| {
-                editor.contents_with_cache(false, None, None, cx)
-            })
+            .update(cx, |editor, cx| editor.contents(false, cx))
             .await
             .unwrap()
             .0;
@@ -3011,9 +2806,7 @@ mod tests {
         });
 
         let content = message_editor
-            .update(cx, |editor, cx| {
-                editor.contents_with_cache(false, None, None, cx)
-            })
+            .update(cx, |editor, cx| editor.contents(false, cx))
             .await
             .unwrap()
             .0;
@@ -3104,7 +2897,7 @@ mod tests {
         let message_editor = workspace.update_in(&mut cx, |workspace, window, cx| {
             let workspace_handle = cx.weak_entity();
             let message_editor = cx.new(|cx| {
-                MessageEditor::new_with_cache(
+                MessageEditor::new(
                     workspace_handle,
                     project.downgrade(),
                     thread_store.clone(),
@@ -3112,8 +2905,6 @@ mod tests {
                     None,
                     Default::default(),
                     Default::default(),
-                    Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::full(),
@@ -3221,7 +3012,7 @@ mod tests {
         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_with_cache(
+                MessageEditor::new(
                     workspace_handle,
                     project.downgrade(),
                     Some(thread_store),
@@ -3229,8 +3020,6 @@ mod tests {
                     None,
                     Default::default(),
                     Default::default(),
-                    Default::default(),
-                    Default::default(),
                     "Test Agent".into(),
                     "Test",
                     EditorMode::AutoHeight {

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

@@ -22,7 +22,7 @@ use editor::{
 };
 use feature_flags::{
     AgentSharingFeatureFlag, AgentV2FeatureFlag, CloudThinkingToggleFeatureFlag,
-    FeatureFlagAppExt as _, UserSlashCommandsFeatureFlag,
+    FeatureFlagAppExt as _,
 };
 use file_icons::FileIcons;
 use fs::Fs;
@@ -57,9 +57,7 @@ use ui::{
 };
 use util::defer;
 use util::{ResultExt, size::format_file_size, time::duration_alt_display};
-use workspace::{
-    CollaboratorId, NewTerminal, OpenOptions, Toast, Workspace, notifications::NotificationId,
-};
+use workspace::{CollaboratorId, NewTerminal, Toast, Workspace, notifications::NotificationId};
 use zed_actions::agent::{Chat, ToggleModelSelector};
 use zed_actions::assistant::OpenRulesLibrary;
 
@@ -73,9 +71,6 @@ use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
 use crate::agent_diff::AgentDiff;
 use crate::profile_selector::{ProfileProvider, ProfileSelector};
 use crate::ui::{AgentNotification, AgentNotificationEvent};
-use crate::user_slash_command::{
-    self, CommandLoadError, SlashCommandRegistry, SlashCommandRegistryEvent, UserSlashCommand,
-};
 use crate::{
     AgentDiffPane, AgentPanel, AllowAlways, AllowOnce, AuthorizeToolCall, ClearMessageQueue,
     CycleFavoriteModels, CycleModeSelector, EditFirstQueuedMessage, ExpandMessageEditor,
@@ -102,7 +97,7 @@ enum ThreadFeedback {
 }
 
 #[derive(Debug)]
-enum ThreadError {
+pub(crate) enum ThreadError {
     PaymentRequired,
     Refusal,
     AuthenticationRequired(SharedString),
@@ -113,7 +108,7 @@ enum ThreadError {
 }
 
 impl ThreadError {
-    fn from_err(error: anyhow::Error, agent: &Rc<dyn AgentServer>) -> Self {
+    fn from_err(error: anyhow::Error, agent_name: &str) -> Self {
         if error.is::<language_model::PaymentRequiredError>() {
             Self::PaymentRequired
         } else if let Some(acp_error) = error.downcast_ref::<acp::Error>()
@@ -129,7 +124,7 @@ impl ThreadError {
                 .map(|acp_error| SharedString::from(acp_error.code.to_string()));
 
             // TODO: we should have Gemini return better errors here.
-            if agent.clone().downcast::<agent_servers::Gemini>().is_some()
+            if agent_name == "Gemini CLI"
                 && message.contains("Could not load the default credentials")
                 || message.contains("API key not valid")
                 || message.contains("Request had invalid authentication credentials")
@@ -164,166 +159,6 @@ impl ProfileProvider for Entity<agent::Thread> {
     }
 }
 
-#[derive(Default)]
-struct ThreadFeedbackState {
-    feedback: Option<ThreadFeedback>,
-    comments_editor: Option<Entity<Editor>>,
-}
-
-impl ThreadFeedbackState {
-    pub fn submit(
-        &mut self,
-        thread: Entity<AcpThread>,
-        feedback: ThreadFeedback,
-        window: &mut Window,
-        cx: &mut App,
-    ) {
-        let Some(telemetry) = thread.read(cx).connection().telemetry() else {
-            return;
-        };
-
-        if self.feedback == Some(feedback) {
-            return;
-        }
-
-        self.feedback = Some(feedback);
-        match feedback {
-            ThreadFeedback::Positive => {
-                self.comments_editor = None;
-            }
-            ThreadFeedback::Negative => {
-                self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx));
-            }
-        }
-        let session_id = thread.read(cx).session_id().clone();
-        let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
-        let task = telemetry.thread_data(&session_id, cx);
-        let rating = match feedback {
-            ThreadFeedback::Positive => "positive",
-            ThreadFeedback::Negative => "negative",
-        };
-        cx.background_spawn(async move {
-            let thread = task.await?;
-            telemetry::event!(
-                "Agent Thread Rated",
-                agent = agent_telemetry_id,
-                session_id = session_id,
-                rating = rating,
-                thread = thread
-            );
-            anyhow::Ok(())
-        })
-        .detach_and_log_err(cx);
-    }
-
-    pub fn submit_comments(&mut self, thread: Entity<AcpThread>, cx: &mut App) {
-        let Some(telemetry) = thread.read(cx).connection().telemetry() else {
-            return;
-        };
-
-        let Some(comments) = self
-            .comments_editor
-            .as_ref()
-            .map(|editor| editor.read(cx).text(cx))
-            .filter(|text| !text.trim().is_empty())
-        else {
-            return;
-        };
-
-        self.comments_editor.take();
-
-        let session_id = thread.read(cx).session_id().clone();
-        let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
-        let task = telemetry.thread_data(&session_id, cx);
-        cx.background_spawn(async move {
-            let thread = task.await?;
-            telemetry::event!(
-                "Agent Thread Feedback Comments",
-                agent = agent_telemetry_id,
-                session_id = session_id,
-                comments = comments,
-                thread = thread
-            );
-            anyhow::Ok(())
-        })
-        .detach_and_log_err(cx);
-    }
-
-    pub fn clear(&mut self) {
-        *self = Self::default()
-    }
-
-    pub fn dismiss_comments(&mut self) {
-        self.comments_editor.take();
-    }
-
-    fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity<Editor> {
-        let buffer = cx.new(|cx| {
-            let empty_string = String::new();
-            MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
-        });
-
-        let editor = cx.new(|cx| {
-            let mut editor = Editor::new(
-                editor::EditorMode::AutoHeight {
-                    min_lines: 1,
-                    max_lines: Some(4),
-                },
-                buffer,
-                None,
-                window,
-                cx,
-            );
-            editor.set_placeholder_text(
-                "What went wrong? Share your feedback so we can improve.",
-                window,
-                cx,
-            );
-            editor
-        });
-
-        editor.read(cx).focus_handle(cx).focus(window, cx);
-        editor
-    }
-}
-
-#[derive(Default, Clone, Copy)]
-struct DiffStats {
-    lines_added: u32,
-    lines_removed: u32,
-}
-
-impl DiffStats {
-    fn single_file(buffer: &Buffer, diff: &BufferDiff, cx: &App) -> Self {
-        let mut stats = DiffStats::default();
-        let diff_snapshot = diff.snapshot(cx);
-        let buffer_snapshot = buffer.snapshot();
-        let base_text = diff_snapshot.base_text();
-
-        for hunk in diff_snapshot.hunks(&buffer_snapshot) {
-            let added_rows = hunk.range.end.row.saturating_sub(hunk.range.start.row);
-            stats.lines_added += added_rows;
-
-            let base_start = hunk.diff_base_byte_range.start.to_point(base_text).row;
-            let base_end = hunk.diff_base_byte_range.end.to_point(base_text).row;
-            let removed_rows = base_end.saturating_sub(base_start);
-            stats.lines_removed += removed_rows;
-        }
-
-        stats
-    }
-
-    fn all_files(changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>, cx: &App) -> Self {
-        let mut total = DiffStats::default();
-        for (buffer, diff) in changed_buffers {
-            let stats = DiffStats::single_file(buffer.read(cx), diff.read(cx), cx);
-            total.lines_added += stats.lines_added;
-            total.lines_removed += stats.lines_removed;
-        }
-        total
-    }
-}
-
 pub struct AcpServerView {
     agent: Rc<dyn AgentServer>,
     agent_server_store: Entity<AgentServerStore>,
@@ -333,33 +168,18 @@ pub struct AcpServerView {
     prompt_store: Option<Entity<PromptStore>>,
     server_state: ServerState,
     login: Option<task::SpawnInTerminal>, // is some <=> Active | Unauthenticated
-    recent_history_entries: Vec<AgentSessionInfo>,
     history: Entity<AcpThreadHistory>,
-    _history_subscription: Subscription,
-    hovered_recent_history_item: Option<usize>,
-    message_editor: Entity<MessageEditor>,
     focus_handle: FocusHandle,
     notifications: Vec<WindowHandle<AgentNotification>>,
     notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
-    slash_command_registry: Option<Entity<SlashCommandRegistry>>,
     auth_task: Option<Task<()>>,
     _subscriptions: Vec<Subscription>,
-    show_codex_windows_warning: bool,
-    in_flight_prompt: Option<Vec<acp::ContentBlock>>,
-    add_context_menu_handle: PopoverMenuHandle<ContextMenu>,
 }
 
 impl AcpServerView {
-    pub fn as_active_thread(&self) -> Option<&AcpThreadView> {
+    pub fn as_active_thread(&self) -> Option<Entity<AcpThreadView>> {
         match &self.server_state {
-            ServerState::Connected(connected) => Some(&connected.current),
-            _ => None,
-        }
-    }
-
-    pub fn as_active_thread_mut(&mut self) -> Option<&mut AcpThreadView> {
-        match &mut self.server_state {
-            ServerState::Connected(connected) => Some(&mut connected.current),
+            ServerState::Connected(connected) => Some(connected.current.clone()),
             _ => None,
         }
     }
@@ -389,7 +209,7 @@ enum ServerState {
 // hashmap of threads, current becomes session_id
 pub struct ConnectedServerState {
     auth_state: AuthState,
-    current: AcpThreadView,
+    current: Entity<AcpThreadView>,
     connection: Rc<dyn AgentConnection>,
 }
 
@@ -416,8 +236,8 @@ struct LoadingView {
 }
 
 impl ConnectedServerState {
-    pub fn has_thread_error(&self) -> bool {
-        self.current.thread_error.is_some()
+    pub fn has_thread_error(&self, cx: &App) -> bool {
+        self.current.read(cx).thread_error.is_some()
     }
 }
 
@@ -436,58 +256,11 @@ impl AcpServerView {
     ) -> Self {
         let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
         let available_commands = Rc::new(RefCell::new(vec![]));
-        let cached_user_commands = Rc::new(RefCell::new(collections::HashMap::default()));
-        let cached_user_command_errors = Rc::new(RefCell::new(Vec::new()));
 
         let agent_server_store = project.read(cx).agent_server_store().clone();
-        let agent_display_name = agent_server_store
-            .read(cx)
-            .agent_display_name(&ExternalAgentServerName(agent.name()))
-            .unwrap_or_else(|| agent.name());
-
-        let placeholder = placeholder_text(agent_display_name.as_ref(), false);
-
-        let message_editor = cx.new(|cx| {
-            let mut editor = MessageEditor::new_with_cache(
-                workspace.clone(),
-                project.downgrade(),
-                thread_store.clone(),
-                history.downgrade(),
-                prompt_store.clone(),
-                prompt_capabilities.clone(),
-                available_commands.clone(),
-                cached_user_commands.clone(),
-                cached_user_command_errors.clone(),
-                agent.name(),
-                &placeholder,
-                editor::EditorMode::AutoHeight {
-                    min_lines: AgentSettings::get_global(cx).message_editor_min_lines,
-                    max_lines: Some(AgentSettings::get_global(cx).set_message_editor_max_lines()),
-                },
-                window,
-                cx,
-            );
-            if let Some(content) = initial_content {
-                match content {
-                    ExternalAgentInitialContent::ThreadSummary(entry) => {
-                        editor.insert_thread_summary(entry, window, cx);
-                    }
-                    ExternalAgentInitialContent::Text(prompt) => {
-                        editor.set_message(
-                            vec![acp::ContentBlock::Text(acp::TextContent::new(prompt))],
-                            window,
-                            cx,
-                        );
-                    }
-                }
-            }
-            editor
-        });
-
         let subscriptions = vec![
             cx.observe_global_in::<SettingsStore>(window, Self::agent_ui_font_size_changed),
             cx.observe_global_in::<AgentFontSize>(window, Self::agent_ui_font_size_changed),
-            cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
             cx.subscribe_in(
                 &agent_server_store,
                 window,
@@ -506,125 +279,64 @@ impl AcpServerView {
         })
         .detach();
 
-        let show_codex_windows_warning = cfg!(windows)
-            && project.read(cx).is_local()
-            && agent.clone().downcast::<agent_servers::Codex>().is_some();
-
-        // Create SlashCommandRegistry to cache user-defined slash commands and watch for changes
-        let slash_command_registry = if cx.has_flag::<UserSlashCommandsFeatureFlag>() {
-            let fs = project.read(cx).fs().clone();
-            let worktree_roots: Vec<std::path::PathBuf> = project
-                .read(cx)
-                .visible_worktrees(cx)
-                .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
-                .collect();
-            let registry = cx.new(|cx| SlashCommandRegistry::new(fs, worktree_roots, cx));
-
-            // Subscribe to registry changes to update error display and cached commands
-            cx.subscribe(&registry, move |this, registry, event, cx| match event {
-                SlashCommandRegistryEvent::CommandsChanged => {
-                    this.refresh_cached_user_commands_from_registry(&registry, cx);
-                }
-            })
-            .detach();
-
-            // Initialize cached commands and errors from registry
-            let mut commands = registry.read(cx).commands().clone();
-            let mut errors = registry.read(cx).errors().to_vec();
-            let server_command_names = available_commands
-                .borrow()
-                .iter()
-                .map(|command| command.name.clone())
-                .collect::<HashSet<_>>();
-            user_slash_command::apply_server_command_conflicts_to_map(
-                &mut commands,
-                &mut errors,
-                &server_command_names,
-            );
-            *cached_user_commands.borrow_mut() = commands;
-            *cached_user_command_errors.borrow_mut() = errors;
-
-            Some(registry)
-        } else {
-            None
-        };
-
-        let recent_history_entries = history.read(cx).get_recent_sessions(3);
-        let history_subscription = cx.observe(&history, |this, history, cx| {
-            this.update_recent_history_from_cache(&history, cx);
-        });
+        let workspace_for_state = workspace.clone();
+        let project_for_state = project.clone();
 
         Self {
             agent: agent.clone(),
             agent_server_store,
-            workspace: workspace.clone(),
-            project: project.clone(),
+            workspace,
+            project,
             thread_store,
             prompt_store,
             server_state: Self::initial_state(
                 agent.clone(),
                 resume_thread,
-                workspace.clone(),
-                project.clone(),
+                workspace_for_state,
+                project_for_state,
                 prompt_capabilities,
                 available_commands,
-                cached_user_commands,
-                cached_user_command_errors,
+                initial_content,
                 window,
                 cx,
             ),
             login: None,
-            message_editor,
             notifications: Vec::new(),
             notification_subscriptions: HashMap::default(),
-            slash_command_registry,
             auth_task: None,
-            recent_history_entries,
             history,
-            _history_subscription: history_subscription,
-            hovered_recent_history_item: None,
             _subscriptions: subscriptions,
             focus_handle: cx.focus_handle(),
-            show_codex_windows_warning,
-            in_flight_prompt: None,
-            add_context_menu_handle: PopoverMenuHandle::default(),
         }
     }
 
     fn reset(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
         let available_commands = Rc::new(RefCell::new(vec![]));
-        let cached_user_commands = Rc::new(RefCell::new(collections::HashMap::default()));
-        let cached_user_command_errors = Rc::new(RefCell::new(Vec::new()));
 
         let resume_thread_metadata = self
             .as_active_thread()
-            .and_then(|thread| thread.resume_thread_metadata.clone());
-
-        self.message_editor.update(cx, |editor, cx| {
-            editor.set_command_state(
-                prompt_capabilities.clone(),
-                available_commands.clone(),
-                cached_user_commands.clone(),
-                cached_user_command_errors.clone(),
-                cx,
-            );
-        });
+            .and_then(|thread| thread.read(cx).resume_thread_metadata.clone());
 
         self.server_state = Self::initial_state(
             self.agent.clone(),
             resume_thread_metadata,
             self.workspace.clone(),
             self.project.clone(),
-            prompt_capabilities,
-            available_commands,
-            cached_user_commands,
-            cached_user_command_errors,
+            prompt_capabilities.clone(),
+            available_commands.clone(),
+            None,
             window,
             cx,
         );
-        self.refresh_cached_user_commands(cx);
-        self.recent_history_entries.clear();
+
+        if let Some(connected) = self.as_connected() {
+            connected.current.update(cx, |this, cx| {
+                this.message_editor.update(cx, |editor, cx| {
+                    editor.set_command_state(prompt_capabilities, available_commands, cx);
+                });
+            });
+        }
         cx.notify();
     }
 
@@ -635,8 +347,7 @@ impl AcpServerView {
         project: Entity<Project>,
         prompt_capabilities: Rc<RefCell<PromptCapabilities>>,
         available_commands: Rc<RefCell<Vec<acp::AvailableCommand>>>,
-        cached_user_commands: Rc<RefCell<HashMap<String, UserSlashCommand>>>,
-        cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
+        initial_content: Option<ExternalAgentInitialContent>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> ServerState {
@@ -688,8 +399,8 @@ impl AcpServerView {
                     this.update_in(cx, |this, window, cx| {
                         if err.downcast_ref::<LoadError>().is_some() {
                             this.handle_load_error(err, window, cx);
-                        } else {
-                            this.handle_thread_error(err, cx);
+                        } else if let Some(active) = this.as_active_thread() {
+                            active.update(cx, |active, cx| active.handle_any_thread_error(err, cx));
                         }
                         cx.notify();
                     })
@@ -772,8 +483,6 @@ impl AcpServerView {
                                 this.prompt_store.clone(),
                                 prompt_capabilities.clone(),
                                 available_commands.clone(),
-                                cached_user_commands.clone(),
-                                cached_user_command_errors.clone(),
                                 this.agent.name(),
                             )
                         });
@@ -861,11 +570,11 @@ impl AcpServerView {
                         let mut subscriptions = vec![
                             cx.subscribe_in(&thread, window, Self::handle_thread_event),
                             cx.observe(&action_log, |_, _, cx| cx.notify()),
-                            cx.subscribe_in(
-                                &entry_view_state,
-                                window,
-                                Self::handle_entry_view_event,
-                            ),
+                            // cx.subscribe_in(
+                            //     &entry_view_state,
+                            //     window,
+                            //     Self::handle_entry_view_event,
+                            // ),
                         ];
 
                         let title_editor =
@@ -900,11 +609,20 @@ impl AcpServerView {
                                 })
                             });
 
-                        this.server_state = ServerState::Connected(ConnectedServerState {
-                            connection,
-                            auth_state: AuthState::Ok,
-                            current: AcpThreadView::new(
+                        let agent_display_name = this
+                            .agent_server_store
+                            .read(cx)
+                            .agent_display_name(&ExternalAgentServerName(agent.name()))
+                            .unwrap_or_else(|| agent.name());
+
+                        let weak = cx.weak_entity();
+                        let current = cx.new(|cx| {
+                            AcpThreadView::new(
                                 thread,
+                                this.login.clone(),
+                                weak,
+                                agent.name(),
+                                agent_display_name,
                                 workspace.clone(),
                                 entry_view_state,
                                 title_editor,
@@ -915,19 +633,33 @@ impl AcpServerView {
                                 list_state,
                                 prompt_capabilities,
                                 available_commands,
-                                cached_user_commands,
-                                cached_user_command_errors,
                                 resumed_without_history,
                                 resume_thread.clone(),
+                                project.downgrade(),
+                                this.thread_store.clone(),
+                                this.history.clone(),
+                                this.prompt_store.clone(),
+                                initial_content,
                                 subscriptions,
+                                window,
                                 cx,
-                            ),
+                            )
                         });
 
                         if this.focus_handle.contains_focused(window, cx) {
-                            this.message_editor.focus_handle(cx).focus(window, cx);
+                            current
+                                .read(cx)
+                                .message_editor
+                                .focus_handle(cx)
+                                .focus(window, cx);
                         }
 
+                        this.server_state = ServerState::Connected(ConnectedServerState {
+                            connection,
+                            auth_state: AuthState::Ok,
+                            current,
+                        });
+
                         cx.notify();
                     }
                     Err(err) => {
@@ -942,8 +674,10 @@ impl AcpServerView {
             while let Ok(new_version) = new_version_available_rx.recv().await {
                 if let Some(new_version) = new_version {
                     this.update(cx, |this, cx| {
-                        if let Some(thread) = this.as_active_thread_mut() {
-                            thread.new_server_version_available = Some(new_version.into());
+                        if let Some(thread) = this.as_active_thread() {
+                            thread.update(cx, |thread, _cx| {
+                                thread.new_server_version_available = Some(new_version.into());
+                            });
                         }
                         cx.notify();
                     })
@@ -1028,9 +762,15 @@ impl AcpServerView {
                     description,
                     _subscription: subscription,
                 };
-            }
-            if this.message_editor.focus_handle(cx).is_focused(window) {
-                this.focus_handle.focus(window, cx)
+                if connected
+                    .current
+                    .read(cx)
+                    .message_editor
+                    .focus_handle(cx)
+                    .is_focused(window)
+                {
+                    this.focus_handle.focus(window, cx)
+                }
             }
             cx.notify();
         })
@@ -1043,15 +783,26 @@ impl AcpServerView {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        match &self.server_state {
+            ServerState::Connected(connected) => {
+                if connected
+                    .current
+                    .read(cx)
+                    .message_editor
+                    .focus_handle(cx)
+                    .is_focused(window)
+                {
+                    self.focus_handle.focus(window, cx)
+                }
+            }
+            _ => {}
+        }
         if let Some(load_err) = err.downcast_ref::<LoadError>() {
             self.server_state = ServerState::LoadError(load_err.clone());
         } else {
             self.server_state =
                 ServerState::LoadError(LoadError::Other(format!("{:#}", err).into()))
         }
-        if self.message_editor.focus_handle(cx).is_focused(window) {
-            self.focus_handle.focus(window, cx)
-        }
         cx.notify();
     }
 
@@ -1069,14 +820,15 @@ impl AcpServerView {
             ServerState::Loading(_) => false,
             ServerState::LoadError(_) => true,
             ServerState::Connected(connected) => {
-                connected.auth_state.is_ok() && connected.has_thread_error()
+                connected.auth_state.is_ok() && connected.has_thread_error(cx)
             }
         };
 
         if should_retry {
-            if let Some(active) = self.as_active_thread_mut() {
-                active.thread_error = None;
-                active.thread_error_markdown = None;
+            if let Some(active) = self.as_active_thread() {
+                active.update(cx, |active, cx| {
+                    active.clear_thread_error(cx);
+                });
             }
             self.reset(window, cx);
         }
@@ -1102,529 +854,111 @@ impl AcpServerView {
     }
 
     pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
-        if let Some(active) = self.as_active_thread_mut() {
-            active.cancel_generation(cx);
-        }
-    }
-
-    fn share_thread(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
-        let Some(thread) = self.as_native_thread(cx) else {
-            return;
-        };
-
-        let client = self.project.read(cx).client();
-        let workspace = self.workspace.clone();
-        let session_id = thread.read(cx).id().to_string();
-
-        let load_task = thread.read(cx).to_db(cx);
-
-        cx.spawn(async move |_this, cx| {
-            let db_thread = load_task.await;
-
-            let shared_thread = SharedThread::from_db_thread(&db_thread);
-            let thread_data = shared_thread.to_bytes()?;
-            let title = shared_thread.title.to_string();
-
-            client
-                .request(proto::ShareAgentThread {
-                    session_id: session_id.clone(),
-                    title,
-                    thread_data,
-                })
-                .await?;
-
-            let share_url = client::zed_urls::shared_agent_thread_url(&session_id);
-
-            cx.update(|cx| {
-                if let Some(workspace) = workspace.upgrade() {
-                    workspace.update(cx, |workspace, cx| {
-                        struct ThreadSharedToast;
-                        workspace.show_toast(
-                            Toast::new(
-                                NotificationId::unique::<ThreadSharedToast>(),
-                                "Thread shared!",
-                            )
-                            .on_click(
-                                "Copy URL",
-                                move |_window, cx| {
-                                    cx.write_to_clipboard(ClipboardItem::new_string(
-                                        share_url.clone(),
-                                    ));
-                                },
-                            ),
-                            cx,
-                        );
-                    });
-                }
+        if let Some(active) = self.as_active_thread() {
+            active.update(cx, |active, cx| {
+                active.cancel_generation(cx);
             });
-
-            anyhow::Ok(())
-        })
-        .detach_and_log_err(cx);
-    }
-
-    fn sync_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if !self.is_imported_thread(cx) {
-            return;
         }
-
-        let Some(active) = self.as_active_thread() else {
-            return;
-        };
-
-        let Some(session_list) = self
-            .as_native_connection(cx)
-            .and_then(|connection| connection.session_list(cx))
-            .and_then(|list| list.downcast::<NativeAgentSessionList>())
-        else {
-            return;
-        };
-        let thread_store = session_list.thread_store().clone();
-
-        let client = self.project.read(cx).client();
-        let session_id = active.thread.read(cx).session_id().clone();
-
-        cx.spawn_in(window, async move |this, cx| {
-            let response = client
-                .request(proto::GetSharedAgentThread {
-                    session_id: session_id.to_string(),
-                })
-                .await?;
-
-            let shared_thread = SharedThread::from_bytes(&response.thread_data)?;
-
-            let db_thread = shared_thread.to_db_thread();
-
-            thread_store
-                .update(&mut cx.clone(), |store, cx| {
-                    store.save_thread(session_id.clone(), db_thread, cx)
-                })
-                .await?;
-
-            let thread_metadata = AgentSessionInfo {
-                session_id,
-                cwd: None,
-                title: Some(format!("πŸ”— {}", response.title).into()),
-                updated_at: Some(chrono::Utc::now()),
-                meta: None,
-            };
-
-            this.update_in(cx, |this, window, cx| {
-                if let Some(active) = this.as_active_thread_mut() {
-                    active.resume_thread_metadata = Some(thread_metadata);
-                }
-                this.reset(window, cx);
-            })?;
-
-            this.update_in(cx, |this, _window, cx| {
-                if let Some(workspace) = this.workspace.upgrade() {
-                    workspace.update(cx, |workspace, cx| {
-                        struct ThreadSyncedToast;
-                        workspace.show_toast(
-                            Toast::new(
-                                NotificationId::unique::<ThreadSyncedToast>(),
-                                "Thread synced with latest version",
-                            )
-                            .autohide(),
-                            cx,
-                        );
-                    });
-                }
-            })?;
-
-            anyhow::Ok(())
-        })
-        .detach_and_log_err(cx);
     }
 
-    pub fn expand_message_editor(
+    pub fn handle_title_editor_event(
         &mut self,
-        _: &ExpandMessageEditor,
-        _window: &mut Window,
+        title_editor: &Entity<Editor>,
+        event: &EditorEvent,
+        window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let editor = self.message_editor.clone();
-        if let Some(active) = self.as_active_thread_mut() {
-            active.expand_message_editor(editor, cx);
+        if let Some(active) = self.as_active_thread() {
+            active.update(cx, |active, cx| {
+                active.handle_title_editor_event(title_editor, event, window, cx);
+            });
         }
     }
 
-    fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
-        let editor = self.message_editor.clone();
-        if let Some(active) = self.as_active_thread_mut() {
-            active.set_editor_is_expanded(is_expanded, editor, cx);
-        }
+    pub fn is_loading(&self) -> bool {
+        matches!(self.server_state, ServerState::Loading { .. })
     }
 
-    pub fn handle_title_editor_event(
-        &mut self,
-        title_editor: &Entity<Editor>,
-        event: &EditorEvent,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some(active) = self.as_active_thread_mut() {
-            active.handle_title_editor_event(title_editor, event, window, cx);
+    fn update_turn_tokens(&mut self, cx: &mut Context<Self>) {
+        if let Some(active) = self.as_active_thread() {
+            active.update(cx, |active, cx| {
+                active.update_turn_tokens(cx);
+            });
         }
     }
 
-    pub fn handle_message_editor_event(
+    fn send_queued_message_at_index(
         &mut self,
-        _: &Entity<MessageEditor>,
-        event: &MessageEditorEvent,
+        index: usize,
+        is_send_now: bool,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        match event {
-            MessageEditorEvent::Send => self.send(window, cx),
-            MessageEditorEvent::SendImmediately => self.interrupt_and_send(window, cx),
-            MessageEditorEvent::Cancel => self.cancel_generation(cx),
-            MessageEditorEvent::Focus => {
-                self.cancel_editing(&Default::default(), window, cx);
-            }
-            MessageEditorEvent::LostFocus => {}
+        if let Some(active) = self.as_active_thread() {
+            active.update(cx, |active, cx| {
+                active.send_queued_message_at_index(index, is_send_now, window, cx);
+            });
         }
     }
 
-    pub fn handle_entry_view_event(
+    fn handle_thread_event(
         &mut self,
-        _: &Entity<EntryViewState>,
-        event: &EntryViewEvent,
+        thread: &Entity<AcpThread>,
+        event: &AcpThreadEvent,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        match &event.view_event {
-            ViewEvent::NewDiff(tool_call_id) => {
-                if AgentSettings::get_global(cx).expand_edit_card {
-                    if let Some(active) = self.as_active_thread_mut() {
-                        active.expanded_tool_calls.insert(tool_call_id.clone());
-                    }
+        match event {
+            AcpThreadEvent::NewEntry => {
+                let len = thread.read(cx).entries().len();
+                let index = len - 1;
+                if let Some(active) = self.as_active_thread() {
+                    let entry_view_state = active.read(cx).entry_view_state.clone();
+                    let list_state = active.read(cx).list_state.clone();
+                    entry_view_state.update(cx, |view_state, cx| {
+                        view_state.sync_entry(index, thread, window, cx);
+                        list_state.splice_focusable(
+                            index..index,
+                            [view_state
+                                .entry(index)
+                                .and_then(|entry| entry.focus_handle(cx))],
+                        );
+                    });
                 }
             }
-            ViewEvent::NewTerminal(tool_call_id) => {
-                if AgentSettings::get_global(cx).expand_terminal_card {
-                    if let Some(active) = self.as_active_thread_mut() {
-                        active.expanded_tool_calls.insert(tool_call_id.clone());
-                    }
+            AcpThreadEvent::EntryUpdated(index) => {
+                if let Some(entry_view_state) = self
+                    .as_active_thread()
+                    .map(|active| active.read(cx).entry_view_state.clone())
+                {
+                    entry_view_state.update(cx, |view_state, cx| {
+                        view_state.sync_entry(*index, thread, window, cx)
+                    });
                 }
             }
-            ViewEvent::TerminalMovedToBackground(tool_call_id) => {
-                if let Some(active) = self.as_active_thread_mut() {
-                    active.expanded_tool_calls.remove(tool_call_id);
+            AcpThreadEvent::EntriesRemoved(range) => {
+                if let Some(active) = self.as_active_thread() {
+                    let entry_view_state = active.read(cx).entry_view_state.clone();
+                    let list_state = active.read(cx).list_state.clone();
+                    entry_view_state.update(cx, |view_state, _cx| view_state.remove(range.clone()));
+                    list_state.splice(range.clone(), 0);
                 }
             }
-            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
-                if let Some(active) = self.as_active_thread()
-                    && let Some(AgentThreadEntry::UserMessage(user_message)) =
-                        active.thread.read(cx).entries().get(event.entry_index)
-                    && user_message.id.is_some()
-                {
-                    if let Some(active) = self.as_active_thread_mut() {
-                        active.editing_message = Some(event.entry_index);
-                    }
-                    cx.notify();
-                }
-            }
-            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => {
-                if let Some(active) = self.as_active_thread()
-                    && let Some(AgentThreadEntry::UserMessage(user_message)) =
-                        active.thread.read(cx).entries().get(event.entry_index)
-                    && user_message.id.is_some()
-                {
-                    if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) {
-                        if let Some(active) = self.as_active_thread_mut() {
-                            active.editing_message = None;
-                        }
-                        cx.notify();
-                    }
-                }
-            }
-            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::SendImmediately) => {}
-            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => {
-                self.regenerate(event.entry_index, editor.clone(), window, cx);
-            }
-            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => {
-                self.cancel_editing(&Default::default(), window, cx);
-            }
-        }
-    }
-
-    pub fn is_loading(&self) -> bool {
-        matches!(self.server_state, ServerState::Loading { .. })
-    }
-
-    fn retry_generation(&mut self, cx: &mut Context<Self>) {
-        if let Some(active) = self.as_active_thread_mut() {
-            active.retry_generation(cx);
-        };
-    }
-
-    fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        let message_editor = self.message_editor.clone();
-        let login = self.login.clone();
-        let agent_name = self.agent.name();
-
-        if let Some(active) = self.as_active_thread_mut() {
-            active.send(message_editor, agent_name, login, window, cx);
-        }
-    }
-
-    fn interrupt_and_send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        let message_editor = self.message_editor.clone();
-        if let Some(active) = self.as_active_thread_mut() {
-            active.interrupt_and_send(message_editor, window, cx);
-        };
-    }
-
-    fn start_turn(&mut self, cx: &mut Context<Self>) -> usize {
-        self.as_active_thread_mut()
-            .map(|active| active.start_turn(cx))
-            .unwrap_or(0)
-    }
-
-    fn stop_turn(&mut self, generation: usize) {
-        if let Some(active) = self.as_active_thread_mut() {
-            active.stop_turn(generation);
-        }
-    }
-
-    fn update_turn_tokens(&mut self, cx: &App) {
-        if let Some(active) = self.as_active_thread_mut() {
-            active.update_turn_tokens(cx);
-        }
-    }
-
-    fn send_impl(
-        &mut self,
-        message_editor: Entity<MessageEditor>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        let full_mention_content = self.as_native_thread(cx).is_some_and(|thread| {
-            // Include full contents when using minimal profile
-            let thread = thread.read(cx);
-            AgentSettings::get_global(cx)
-                .profiles
-                .get(thread.profile())
-                .is_some_and(|profile| profile.tools.is_empty())
-        });
-
-        let cached_commands = self.cached_slash_commands(cx);
-        let cached_errors = self.cached_slash_command_errors(cx);
-        let contents = message_editor.update(cx, |message_editor, cx| {
-            message_editor.contents_with_cache(
-                full_mention_content,
-                Some(cached_commands),
-                Some(cached_errors),
-                cx,
-            )
-        });
-
-        if let Some(thread) = self.as_active_thread_mut() {
-            thread.thread_error.take();
-            thread.thread_feedback.clear();
-            thread.editing_message.take();
-
-            if thread.should_be_following {
-                let _ = self.workspace.update(cx, |workspace, cx| {
-                    workspace.follow(CollaboratorId::Agent, window, cx);
-                });
-            }
-        }
-
-        let contents_task = cx.spawn_in(window, async move |this, cx| {
-            let (contents, tracked_buffers) = contents.await?;
-
-            if contents.is_empty() {
-                return Ok(None);
-            }
-
-            this.update_in(cx, |this, window, cx| {
-                this.message_editor.update(cx, |message_editor, cx| {
-                    message_editor.clear(window, cx);
-                });
-            })?;
-
-            Ok(Some((contents, tracked_buffers)))
-        });
-
-        self.send_content(contents_task, window, cx);
-    }
-
-    fn send_content(
-        &mut self,
-        contents_task: Task<anyhow::Result<Option<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some(active) = self.as_active_thread_mut() {
-            active.send_content(contents_task, window, cx);
-        };
-    }
-
-    fn send_queued_message_at_index(
-        &mut self,
-        index: usize,
-        is_send_now: bool,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some(active) = self.as_active_thread_mut() {
-            active.send_queued_message_at_index(index, is_send_now, window, cx);
-        }
-    }
-
-    fn cancel_editing(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
-        let focus_handle = self.focus_handle(cx);
-        if let Some(active) = self.as_active_thread_mut() {
-            active.cancel_editing(focus_handle, window, cx);
-        }
-    }
-
-    fn regenerate(
-        &mut self,
-        entry_ix: usize,
-        message_editor: Entity<MessageEditor>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some(active) = self.as_active_thread_mut() {
-            active.regenerate(entry_ix, message_editor, window, cx);
-        }
-    }
-
-    fn open_edited_buffer(
-        &mut self,
-        buffer: &Entity<Buffer>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if let Some(active) = self.as_active_thread_mut() {
-            active.open_edited_buffer(buffer, window, cx);
-        };
-    }
-
-    fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
-        if let Some(active) = self.as_active_thread_mut() {
-            active.handle_open_rules(window, cx);
-        }
-    }
-
-    fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context<Self>) {
-        let error = ThreadError::from_err(error, &self.agent);
-        self.emit_thread_error_telemetry(&error, cx);
-        if let Some(thread) = self.as_active_thread_mut() {
-            thread.thread_error = Some(error);
-        }
-        cx.notify();
-    }
-
-    fn emit_thread_error_telemetry(&self, error: &ThreadError, cx: &mut Context<Self>) {
-        let (error_kind, acp_error_code, message): (&str, Option<SharedString>, SharedString) =
-            match error {
-                ThreadError::PaymentRequired => (
-                    "payment_required",
-                    None,
-                    "You reached your free usage limit. Upgrade to Zed Pro for more prompts."
-                        .into(),
-                ),
-                ThreadError::Refusal => {
-                    let model_or_agent_name = self.current_model_name(cx);
-                    let message = format!(
-                        "{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.",
-                        model_or_agent_name
-                    );
-                    ("refusal", None, message.into())
-                }
-                ThreadError::AuthenticationRequired(message) => {
-                    ("authentication_required", None, message.clone())
-                }
-                ThreadError::Other {
-                    acp_error_code,
-                    message,
-                } => ("other", acp_error_code.clone(), message.clone()),
-            };
-
-        let (agent_telemetry_id, session_id) = self
-            .as_active_thread()
-            .map(|r| {
-                let thread = r.thread.read(cx);
-                (
-                    thread.connection().telemetry_id(),
-                    thread.session_id().clone(),
-                )
-            })
-            .unzip();
-
-        telemetry::event!(
-            "Agent Panel Error Shown",
-            agent = agent_telemetry_id,
-            session_id = session_id,
-            kind = error_kind,
-            acp_error_code = acp_error_code,
-            message = message,
-        );
-    }
-
-    fn clear_thread_error(&mut self, cx: &mut Context<Self>) {
-        if let Some(active) = self.as_active_thread_mut() {
-            active.clear_thread_error(cx);
-        }
-    }
-
-    fn handle_thread_event(
-        &mut self,
-        thread: &Entity<AcpThread>,
-        event: &AcpThreadEvent,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        match event {
-            AcpThreadEvent::NewEntry => {
-                let len = thread.read(cx).entries().len();
-                let index = len - 1;
-                if let Some(active) = self.as_active_thread_mut() {
-                    active.entry_view_state.update(cx, |view_state, cx| {
-                        view_state.sync_entry(index, thread, window, cx);
-                        active.list_state.splice_focusable(
-                            index..index,
-                            [view_state
-                                .entry(index)
-                                .and_then(|entry| entry.focus_handle(cx))],
-                        );
-                    });
-                }
-            }
-            AcpThreadEvent::EntryUpdated(index) => {
-                if let Some(entry_view_state) = self
-                    .as_active_thread()
-                    .map(|active| &active.entry_view_state)
-                    .cloned()
-                {
-                    entry_view_state.update(cx, |view_state, cx| {
-                        view_state.sync_entry(*index, thread, window, cx)
-                    });
-                }
-            }
-            AcpThreadEvent::EntriesRemoved(range) => {
-                if let Some(active) = self.as_active_thread_mut() {
-                    active
-                        .entry_view_state
-                        .update(cx, |view_state, _cx| view_state.remove(range.clone()));
-                    active.list_state.splice(range.clone(), 0);
-                }
-            }
-            AcpThreadEvent::ToolAuthorizationRequired => {
-                self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
+            AcpThreadEvent::ToolAuthorizationRequired => {
+                self.notify_with_sound("Waiting for tool confirmation", IconName::Info, window, cx);
             }
             AcpThreadEvent::Retry(retry) => {
-                if let Some(active) = self.as_active_thread_mut() {
-                    active.thread_retry_status = Some(retry.clone());
+                if let Some(active) = self.as_active_thread() {
+                    active.update(cx, |active, _cx| {
+                        active.thread_retry_status = Some(retry.clone());
+                    });
                 }
             }
             AcpThreadEvent::Stopped => {
-                if let Some(active) = self.as_active_thread_mut() {
-                    active.thread_retry_status.take();
+                if let Some(active) = self.as_active_thread() {
+                    active.update(cx, |active, _cx| {
+                        active.thread_retry_status.take();
+                    });
                 }
                 let used_tools = thread.read(cx).used_tools_since_last_user_message();
                 self.notify_with_sound(

crates/agent_ui/src/acp/thread_view/active_thread.rs πŸ”—

@@ -1,8 +1,173 @@
+use gpui::List;
+
 use super::*;
 
+#[derive(Default)]
+struct ThreadFeedbackState {
+    feedback: Option<ThreadFeedback>,
+    comments_editor: Option<Entity<Editor>>,
+}
+
+impl ThreadFeedbackState {
+    pub fn submit(
+        &mut self,
+        thread: Entity<AcpThread>,
+        feedback: ThreadFeedback,
+        window: &mut Window,
+        cx: &mut App,
+    ) {
+        let Some(telemetry) = thread.read(cx).connection().telemetry() else {
+            return;
+        };
+
+        if self.feedback == Some(feedback) {
+            return;
+        }
+
+        self.feedback = Some(feedback);
+        match feedback {
+            ThreadFeedback::Positive => {
+                self.comments_editor = None;
+            }
+            ThreadFeedback::Negative => {
+                self.comments_editor = Some(Self::build_feedback_comments_editor(window, cx));
+            }
+        }
+        let session_id = thread.read(cx).session_id().clone();
+        let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
+        let task = telemetry.thread_data(&session_id, cx);
+        let rating = match feedback {
+            ThreadFeedback::Positive => "positive",
+            ThreadFeedback::Negative => "negative",
+        };
+        cx.background_spawn(async move {
+            let thread = task.await?;
+            telemetry::event!(
+                "Agent Thread Rated",
+                agent = agent_telemetry_id,
+                session_id = session_id,
+                rating = rating,
+                thread = thread
+            );
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    }
+
+    pub fn submit_comments(&mut self, thread: Entity<AcpThread>, cx: &mut App) {
+        let Some(telemetry) = thread.read(cx).connection().telemetry() else {
+            return;
+        };
+
+        let Some(comments) = self
+            .comments_editor
+            .as_ref()
+            .map(|editor| editor.read(cx).text(cx))
+            .filter(|text| !text.trim().is_empty())
+        else {
+            return;
+        };
+
+        self.comments_editor.take();
+
+        let session_id = thread.read(cx).session_id().clone();
+        let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
+        let task = telemetry.thread_data(&session_id, cx);
+        cx.background_spawn(async move {
+            let thread = task.await?;
+            telemetry::event!(
+                "Agent Thread Feedback Comments",
+                agent = agent_telemetry_id,
+                session_id = session_id,
+                comments = comments,
+                thread = thread
+            );
+            anyhow::Ok(())
+        })
+        .detach_and_log_err(cx);
+    }
+
+    pub fn clear(&mut self) {
+        *self = Self::default()
+    }
+
+    pub fn dismiss_comments(&mut self) {
+        self.comments_editor.take();
+    }
+
+    fn build_feedback_comments_editor(window: &mut Window, cx: &mut App) -> Entity<Editor> {
+        let buffer = cx.new(|cx| {
+            let empty_string = String::new();
+            MultiBuffer::singleton(cx.new(|cx| Buffer::local(empty_string, cx)), cx)
+        });
+
+        let editor = cx.new(|cx| {
+            let mut editor = Editor::new(
+                editor::EditorMode::AutoHeight {
+                    min_lines: 1,
+                    max_lines: Some(4),
+                },
+                buffer,
+                None,
+                window,
+                cx,
+            );
+            editor.set_placeholder_text(
+                "What went wrong? Share your feedback so we can improve.",
+                window,
+                cx,
+            );
+            editor
+        });
+
+        editor.read(cx).focus_handle(cx).focus(window, cx);
+        editor
+    }
+}
+
+#[derive(Default, Clone, Copy)]
+struct DiffStats {
+    lines_added: u32,
+    lines_removed: u32,
+}
+
+impl DiffStats {
+    fn single_file(buffer: &Buffer, diff: &BufferDiff, cx: &App) -> Self {
+        let mut stats = DiffStats::default();
+        let diff_snapshot = diff.snapshot(cx);
+        let buffer_snapshot = buffer.snapshot();
+        let base_text = diff_snapshot.base_text();
+
+        for hunk in diff_snapshot.hunks(&buffer_snapshot) {
+            let added_rows = hunk.range.end.row.saturating_sub(hunk.range.start.row);
+            stats.lines_added += added_rows;
+
+            let base_start = hunk.diff_base_byte_range.start.to_point(base_text).row;
+            let base_end = hunk.diff_base_byte_range.end.to_point(base_text).row;
+            let removed_rows = base_end.saturating_sub(base_start);
+            stats.lines_removed += removed_rows;
+        }
+
+        stats
+    }
+
+    fn all_files(changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>, cx: &App) -> Self {
+        let mut total = DiffStats::default();
+        for (buffer, diff) in changed_buffers {
+            let stats = DiffStats::single_file(buffer.read(cx), diff.read(cx), cx);
+            total.lines_added += stats.lines_added;
+            total.lines_removed += stats.lines_removed;
+        }
+        total
+    }
+}
+
 pub struct AcpThreadView {
     pub id: acp::SessionId,
+    pub login: Option<task::SpawnInTerminal>, // is some <=> Active | Unauthenticated
     pub thread: Entity<AcpThread>,
+    pub server_view: WeakEntity<AcpServerView>,
+    pub agent_name: SharedString,
     pub workspace: WeakEntity<Workspace>,
     pub entry_view_state: Entity<EntryViewState>,
     pub title_editor: Option<Entity<Editor>>,
@@ -15,12 +180,10 @@ pub struct AcpThreadView {
     pub(super) thread_error: Option<ThreadError>,
     pub thread_error_markdown: Option<Entity<Markdown>>,
     pub token_limit_callout_dismissed: bool,
-    pub(super) thread_feedback: ThreadFeedbackState,
+    thread_feedback: ThreadFeedbackState,
     pub list_state: ListState,
     pub prompt_capabilities: Rc<RefCell<PromptCapabilities>>,
     pub available_commands: Rc<RefCell<Vec<agent_client_protocol::AvailableCommand>>>,
-    pub cached_user_commands: Rc<RefCell<HashMap<String, UserSlashCommand>>>,
-    pub cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
     /// 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>,
@@ -39,7 +202,6 @@ pub struct AcpThreadView {
     pub queued_message_editor_subscriptions: Vec<Subscription>,
     pub last_synced_queue_length: usize,
     pub turn_fields: TurnFields,
-    pub command_load_errors_dismissed: bool,
     pub discarded_partial_edits: HashSet<agent_client_protocol::ToolCallId>,
     pub is_loading_contents: bool,
     pub new_server_version_available: Option<SharedString>,
@@ -54,7 +216,21 @@ pub struct AcpThreadView {
     pub user_interrupted_generation: bool,
     pub can_fast_track_queue: bool,
     pub hovered_edited_file_buttons: Option<usize>,
+    pub in_flight_prompt: Option<Vec<acp::ContentBlock>>,
     pub _subscriptions: Vec<Subscription>,
+    pub message_editor: Entity<MessageEditor>,
+    pub add_context_menu_handle: PopoverMenuHandle<ContextMenu>,
+    pub project: WeakEntity<Project>,
+    pub recent_history_entries: Vec<AgentSessionInfo>,
+    pub hovered_recent_history_item: Option<usize>,
+    pub show_codex_windows_warning: bool,
+    pub history: Entity<AcpThreadHistory>,
+    pub _history_subscription: Subscription,
+}
+impl Focusable for AcpThreadView {
+    fn focus_handle(&self, cx: &App) -> FocusHandle {
+        self.active_editor(cx).focus_handle(cx)
+    }
 }
 
 #[derive(Default)]
@@ -70,6 +246,10 @@ pub struct TurnFields {
 impl AcpThreadView {
     pub fn new(
         thread: Entity<AcpThread>,
+        login: Option<task::SpawnInTerminal>,
+        server_view: WeakEntity<AcpServerView>,
+        agent_name: SharedString,
+        agent_display_name: SharedString,
         workspace: WeakEntity<Workspace>,
         entry_view_state: Entity<EntryViewState>,
         title_editor: Option<Entity<Editor>>,
@@ -80,17 +260,84 @@ impl AcpThreadView {
         list_state: ListState,
         prompt_capabilities: Rc<RefCell<PromptCapabilities>>,
         available_commands: Rc<RefCell<Vec<agent_client_protocol::AvailableCommand>>>,
-        cached_user_commands: Rc<RefCell<HashMap<String, UserSlashCommand>>>,
-        cached_user_command_errors: Rc<RefCell<Vec<CommandLoadError>>>,
         resumed_without_history: bool,
         resume_thread_metadata: Option<AgentSessionInfo>,
-        subscriptions: Vec<Subscription>,
-        cx: &App,
+        project: WeakEntity<Project>,
+        thread_store: Option<Entity<ThreadStore>>,
+        history: Entity<AcpThreadHistory>,
+        prompt_store: Option<Entity<PromptStore>>,
+        initial_content: Option<ExternalAgentInitialContent>,
+        mut subscriptions: Vec<Subscription>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
     ) -> Self {
         let id = thread.read(cx).session_id().clone();
+
+        let placeholder = placeholder_text(agent_display_name.as_ref(), false);
+
+        let history_subscription = cx.observe(&history, |this, history, cx| {
+            this.update_recent_history_from_cache(&history, cx);
+        });
+
+        let message_editor = cx.new(|cx| {
+            let mut editor = MessageEditor::new(
+                workspace.clone(),
+                project.clone(),
+                thread_store,
+                history.downgrade(),
+                prompt_store,
+                prompt_capabilities.clone(),
+                available_commands.clone(),
+                agent_name.clone(),
+                &placeholder,
+                editor::EditorMode::AutoHeight {
+                    min_lines: AgentSettings::get_global(cx).message_editor_min_lines,
+                    max_lines: Some(AgentSettings::get_global(cx).set_message_editor_max_lines()),
+                },
+                window,
+                cx,
+            );
+            if let Some(content) = initial_content {
+                match content {
+                    ExternalAgentInitialContent::ThreadSummary(entry) => {
+                        editor.insert_thread_summary(entry, window, cx);
+                    }
+                    ExternalAgentInitialContent::Text(prompt) => {
+                        editor.set_message(
+                            vec![acp::ContentBlock::Text(acp::TextContent::new(prompt))],
+                            window,
+                            cx,
+                        );
+                    }
+                }
+            }
+            editor
+        });
+
+        let show_codex_windows_warning = cfg!(windows)
+            && project.upgrade().is_some_and(|p| p.read(cx).is_local())
+            && agent_name == "Codex";
+
+        subscriptions.push(cx.subscribe_in(
+            &entry_view_state,
+            window,
+            Self::handle_entry_view_event,
+        ));
+
+        subscriptions.push(cx.subscribe_in(
+            &message_editor,
+            window,
+            Self::handle_message_editor_event,
+        ));
+
+        let recent_history_entries = history.read(cx).get_recent_sessions(3);
+
         Self {
             id,
             thread,
+            login,
+            server_view,
+            agent_name,
             workspace,
             entry_view_state,
             title_editor,
@@ -101,11 +348,8 @@ impl AcpThreadView {
             list_state,
             prompt_capabilities,
             available_commands,
-            cached_user_commands,
-            cached_user_command_errors,
             resumed_without_history,
             resume_thread_metadata,
-            command_load_errors_dismissed: false,
             _subscriptions: subscriptions,
             permission_dropdown_handle: PopoverMenuHandle::default(),
             thread_retry_status: None,
@@ -138,6 +382,33 @@ impl AcpThreadView {
             user_interrupted_generation: false,
             can_fast_track_queue: false,
             hovered_edited_file_buttons: None,
+            in_flight_prompt: None,
+            message_editor,
+            add_context_menu_handle: PopoverMenuHandle::default(),
+            project,
+            recent_history_entries,
+            hovered_recent_history_item: None,
+            history,
+            _history_subscription: history_subscription,
+            show_codex_windows_warning,
+        }
+    }
+
+    pub fn handle_message_editor_event(
+        &mut self,
+        _editor: &Entity<MessageEditor>,
+        event: &MessageEditorEvent,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        match event {
+            MessageEditorEvent::Send => self.send(window, cx),
+            MessageEditorEvent::SendImmediately => self.interrupt_and_send(window, cx),
+            MessageEditorEvent::Cancel => self.cancel_generation(cx),
+            MessageEditorEvent::Focus => {
+                self.cancel_editing(&Default::default(), window, cx);
+            }
+            MessageEditorEvent::LostFocus => {}
         }
     }
 
@@ -170,6 +441,23 @@ impl AcpThreadView {
         }
     }
 
+    /// Returns the currently active editor, either for a message that is being
+    /// edited or the editor for a new message.
+    pub(crate) fn active_editor(&self, cx: &App) -> Entity<MessageEditor> {
+        if let Some(index) = self.editing_message
+            && let Some(editor) = self
+                .entry_view_state
+                .read(cx)
+                .entry(index)
+                .and_then(|entry| entry.message_editor())
+                .cloned()
+        {
+            editor
+        } else {
+            self.message_editor.clone()
+        }
+    }
+
     pub fn has_queued_messages(&self) -> bool {
         !self.local_queued_messages.is_empty()
     }
@@ -181,9 +469,62 @@ impl AcpThreadView {
         thread.read(cx).is_imported()
     }
 
+    // events
+
+    pub fn handle_entry_view_event(
+        &mut self,
+        _: &Entity<EntryViewState>,
+        event: &EntryViewEvent,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        match &event.view_event {
+            ViewEvent::NewDiff(tool_call_id) => {
+                if AgentSettings::get_global(cx).expand_edit_card {
+                    self.expanded_tool_calls.insert(tool_call_id.clone());
+                }
+            }
+            ViewEvent::NewTerminal(tool_call_id) => {
+                if AgentSettings::get_global(cx).expand_terminal_card {
+                    self.expanded_tool_calls.insert(tool_call_id.clone());
+                }
+            }
+            ViewEvent::TerminalMovedToBackground(tool_call_id) => {
+                self.expanded_tool_calls.remove(tool_call_id);
+            }
+            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Focus) => {
+                if let Some(AgentThreadEntry::UserMessage(user_message)) =
+                    self.thread.read(cx).entries().get(event.entry_index)
+                    && user_message.id.is_some()
+                {
+                    self.editing_message = Some(event.entry_index);
+                    cx.notify();
+                }
+            }
+            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::LostFocus) => {
+                if let Some(AgentThreadEntry::UserMessage(user_message)) =
+                    self.thread.read(cx).entries().get(event.entry_index)
+                    && user_message.id.is_some()
+                {
+                    if editor.read(cx).text(cx).as_str() == user_message.content.to_markdown(cx) {
+                        self.editing_message = None;
+                        cx.notify();
+                    }
+                }
+            }
+            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::SendImmediately) => {}
+            ViewEvent::MessageEditorEvent(editor, MessageEditorEvent::Send) => {
+                self.regenerate(event.entry_index, editor.clone(), window, cx);
+            }
+            ViewEvent::MessageEditorEvent(_editor, MessageEditorEvent::Cancel) => {
+                self.cancel_editing(&Default::default(), window, cx);
+            }
+        }
+    }
+
     // turns
 
-    pub fn start_turn(&mut self, cx: &mut Context<AcpServerView>) -> usize {
+    pub fn start_turn(&mut self, cx: &mut Context<Self>) -> usize {
         self.turn_fields.turn_generation += 1;
         let generation = self.turn_fields.turn_generation;
         self.turn_fields.turn_started_at = Some(Instant::now());
@@ -224,20 +565,14 @@ impl AcpThreadView {
 
     // sending
 
-    pub fn send(
-        &mut self,
-        message_editor: Entity<MessageEditor>,
-        agent_name: SharedString,
-        login: Option<task::SpawnInTerminal>,
-        window: &mut Window,
-        cx: &mut Context<AcpServerView>,
-    ) {
+    pub fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let thread = &self.thread;
 
         if self.is_loading_contents {
             return;
         }
 
+        let message_editor = self.message_editor.clone();
         let is_editor_empty = message_editor.read(cx).is_empty(cx);
         let is_generating = thread.read(cx).status() != ThreadStatus::Idle;
 
@@ -261,7 +596,7 @@ impl AcpThreadView {
         let text = text.trim();
         if text == "/login" || text == "/logout" {
             let connection = thread.read(cx).connection().clone();
-            let can_login = !connection.auth_methods().is_empty() || login.is_some();
+            let can_login = !connection.auth_methods().is_empty() || self.login.is_some();
             // Does the agent have a specific logout command? Prefer that in case they need to reset internal state.
             let logout_supported = text == "/logout"
                 && self
@@ -272,15 +607,18 @@ impl AcpThreadView {
             if can_login && !logout_supported {
                 message_editor.update(cx, |editor, cx| editor.clear(window, cx));
 
-                let this = cx.weak_entity();
-                window.defer(cx, |window, cx| {
-                    AcpServerView::handle_auth_required(
-                        this,
-                        AuthRequired::new(),
-                        agent_name,
-                        window,
-                        cx,
-                    );
+                window.defer(cx, {
+                    let agent_name = self.agent_name.clone();
+                    let server_view = self.server_view.clone();
+                    move |window, cx| {
+                        AcpServerView::handle_auth_required(
+                            server_view.clone(),
+                            AuthRequired::new(),
+                            agent_name,
+                            window,
+                            cx,
+                        );
+                    }
                 });
                 cx.notify();
                 return;
@@ -294,7 +632,7 @@ impl AcpThreadView {
         &mut self,
         message_editor: Entity<MessageEditor>,
         window: &mut Window,
-        cx: &mut Context<AcpServerView>,
+        cx: &mut Context<Self>,
     ) {
         let full_mention_content = self.as_native_thread(cx).is_some_and(|thread| {
             // Include full contents when using minimal profile
@@ -305,15 +643,8 @@ impl AcpThreadView {
                 .is_some_and(|profile| profile.tools.is_empty())
         });
 
-        let cached_commands = &self.cached_user_commands;
-        let cached_errors = &self.cached_user_command_errors;
         let contents = message_editor.update(cx, |message_editor, cx| {
-            message_editor.contents_with_cache(
-                full_mention_content,
-                Some(cached_commands.borrow().clone()),
-                Some(cached_errors.borrow().clone()),
-                cx,
-            )
+            message_editor.contents(full_mention_content, cx)
         });
 
         self.thread_error.take();
@@ -351,7 +682,7 @@ impl AcpThreadView {
         &mut self,
         contents_task: Task<anyhow::Result<Option<(Vec<acp::ContentBlock>, Vec<Entity<Buffer>>)>>>,
         window: &mut Window,
-        cx: &mut Context<AcpServerView>,
+        cx: &mut Context<Self>,
     ) {
         let session_id = self.thread.read(cx).session_id().clone();
         let agent_telemetry_id = self.thread.read(cx).connection().telemetry_id();
@@ -363,9 +694,7 @@ impl AcpThreadView {
         let mode_id = self.current_mode_id(cx);
         let guard = cx.new(|_| ());
         cx.observe_release(&guard, |this, _guard, cx| {
-            if let Some(thread) = this.as_active_thread_mut() {
-                thread.is_loading_contents = false;
-            }
+            this.is_loading_contents = false;
             cx.notify();
         })
         .detach();
@@ -375,14 +704,17 @@ impl AcpThreadView {
                 return Ok(());
             };
 
-            let generation = this.update_in(cx, |this, _window, cx| {
-                this.in_flight_prompt = Some(contents.clone());
+            let generation = this.update(cx, |this, cx| {
                 let generation = this.start_turn(cx);
-                this.set_editor_is_expanded(false, cx);
-                this.scroll_to_bottom(cx);
+                this.in_flight_prompt = Some(contents.clone());
                 generation
             })?;
 
+            this.update_in(cx, |this, _window, cx| {
+                this.set_editor_is_expanded(false, cx);
+            })?;
+            let _ = this.update(cx, |this, cx| this.scroll_to_bottom(cx));
+
             let _stop_turn = defer({
                 let this = this.clone();
                 let mut cx = cx.clone();
@@ -417,7 +749,7 @@ impl AcpThreadView {
             let turn_time_ms = turn_start_time.elapsed().as_millis();
             drop(_stop_turn);
             let status = if res.is_ok() {
-                this.update(cx, |this, _| this.in_flight_prompt.take()).ok();
+                let _ = this.update(cx, |this, _| this.in_flight_prompt.take());
                 "success"
             } else {
                 "failure"
@@ -437,7 +769,7 @@ impl AcpThreadView {
         cx.spawn(async move |this, cx| {
             if let Err(err) = task.await {
                 this.update(cx, |this, cx| {
-                    this.handle_thread_error(err, cx);
+                    this.handle_any_thread_error(err, cx);
                 })
                 .ok();
             } else {
@@ -448,9 +780,7 @@ impl AcpThreadView {
                             workspace.is_being_followed(CollaboratorId::Agent)
                         })
                         .unwrap_or_default();
-                    if let Some(thread) = this.as_active_thread_mut() {
-                        thread.should_be_following = should_be_following;
-                    }
+                    this.should_be_following = should_be_following;
                 })
                 .ok();
             }
@@ -458,30 +788,27 @@ impl AcpThreadView {
         .detach();
     }
 
-    pub fn interrupt_and_send(
-        &mut self,
-        message_editor: Entity<MessageEditor>,
-        window: &mut Window,
-        cx: &mut Context<AcpServerView>,
-    ) {
+    pub fn interrupt_and_send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
         let thread = &self.thread;
 
         if self.is_loading_contents {
             return;
         }
 
+        let message_editor = self.message_editor.clone();
         if thread.read(cx).status() == ThreadStatus::Idle {
             self.send_impl(message_editor, window, cx);
             return;
         }
 
-        self.stop_current_and_send_new_message(window, cx);
+        self.stop_current_and_send_new_message(message_editor, window, cx);
     }
 
-    pub fn stop_current_and_send_new_message(
+    fn stop_current_and_send_new_message(
         &mut self,
+        message_editor: Entity<MessageEditor>,
         window: &mut Window,
-        cx: &mut Context<AcpServerView>,
+        cx: &mut Context<Self>,
     ) {
         let thread = self.thread.clone();
         self.skip_queue_processing_count = 0;
@@ -493,23 +820,73 @@ impl AcpThreadView {
             cancelled.await;
 
             this.update_in(cx, |this, window, cx| {
-                this.send_impl(this.message_editor.clone(), window, cx);
+                this.send_impl(message_editor, window, cx);
             })
             .ok();
         })
         .detach();
     }
 
+    pub(crate) fn handle_any_thread_error(&mut self, error: anyhow::Error, cx: &mut Context<Self>) {
+        let error = ThreadError::from_err(error, &self.agent_name);
+        self.handle_thread_error(error, cx);
+    }
+
+    pub(crate) fn handle_thread_error(&mut self, error: ThreadError, cx: &mut Context<Self>) {
+        self.emit_thread_error_telemetry(&error, cx);
+        self.thread_error = Some(error);
+        cx.notify();
+    }
+
+    fn emit_thread_error_telemetry(&self, error: &ThreadError, cx: &mut Context<Self>) {
+        let (error_kind, acp_error_code, message): (&str, Option<SharedString>, SharedString) =
+            match error {
+                ThreadError::PaymentRequired => (
+                    "payment_required",
+                    None,
+                    "You reached your free usage limit. Upgrade to Zed Pro for more prompts."
+                        .into(),
+                ),
+                ThreadError::Refusal => {
+                    let model_or_agent_name = self.current_model_name(cx);
+                    let message = format!(
+                        "{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.",
+                        model_or_agent_name
+                    );
+                    ("refusal", None, message.into())
+                }
+                ThreadError::AuthenticationRequired(message) => {
+                    ("authentication_required", None, message.clone())
+                }
+                ThreadError::Other {
+                    acp_error_code,
+                    message,
+                } => ("other", acp_error_code.clone(), message.clone()),
+            };
+
+        let agent_telemetry_id = self.thread.read(cx).connection().telemetry_id();
+        let session_id = self.thread.read(cx).session_id().clone();
+
+        telemetry::event!(
+            "Agent Panel Error Shown",
+            agent = agent_telemetry_id,
+            session_id = session_id,
+            kind = error_kind,
+            acp_error_code = acp_error_code,
+            message = message,
+        );
+    }
+
     // generation
 
-    pub fn cancel_generation(&mut self, cx: &mut Context<AcpServerView>) {
+    pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
         self.thread_retry_status.take();
         self.thread_error.take();
         self.user_interrupted_generation = true;
         self._cancel_task = Some(self.thread.update(cx, |thread, cx| thread.cancel(cx)));
     }
 
-    pub fn retry_generation(&mut self, cx: &mut Context<AcpServerView>) {
+    pub fn retry_generation(&mut self, cx: &mut Context<Self>) {
         self.thread_error.take();
 
         let thread = &self.thread;
@@ -523,7 +900,7 @@ impl AcpThreadView {
 
             this.update(cx, |this, cx| {
                 if let Err(err) = result {
-                    this.handle_thread_error(err, cx);
+                    this.handle_any_thread_error(err, cx);
                 }
             })
         })
@@ -535,7 +912,7 @@ impl AcpThreadView {
         entry_ix: usize,
         message_editor: Entity<MessageEditor>,
         window: &mut Window,
-        cx: &mut Context<AcpServerView>,
+        cx: &mut Context<Self>,
     ) {
         if self.is_loading_contents {
             return;
@@ -574,9 +951,9 @@ impl AcpThreadView {
             thread
                 .update(cx, |thread, cx| thread.rewind(user_message_id, cx))
                 .await?;
-            this.update_in(cx, |this, window, cx| {
-                this.send_impl(message_editor, window, cx);
-                this.focus_handle(cx).focus(window, cx);
+            this.update_in(cx, |thread, window, cx| {
+                thread.send_impl(message_editor, window, cx);
+                thread.focus_handle(cx).focus(window, cx);
             })?;
             anyhow::Ok(())
         })
@@ -585,11 +962,11 @@ impl AcpThreadView {
 
     // message queueing
 
-    pub fn queue_message(
+    fn queue_message(
         &mut self,
         message_editor: Entity<MessageEditor>,
         window: &mut Window,
-        cx: &mut Context<AcpServerView>,
+        cx: &mut Context<Self>,
     ) {
         let is_idle = self.thread.read(cx).status() == acp_thread::ThreadStatus::Idle;
 
@@ -606,15 +983,8 @@ impl AcpThreadView {
                 .is_some_and(|profile| profile.tools.is_empty())
         });
 
-        let cached_commands = self.cached_user_commands.borrow().clone();
-        let cached_errors = self.cached_user_command_errors.borrow().clone();
         let contents = message_editor.update(cx, |message_editor, cx| {
-            message_editor.contents_with_cache(
-                full_mention_content,
-                Some(cached_commands),
-                Some(cached_errors),
-                cx,
-            )
+            message_editor.contents(full_mention_content, cx)
         });
 
         cx.spawn_in(window, async move |this, cx| {
@@ -626,8 +996,7 @@ impl AcpThreadView {
 
             this.update_in(cx, |this, window, cx| {
                 this.add_to_queue(content, tracked_buffers, cx);
-                // Enable fast-track: user can press Enter again to send this queued message immediately
-                this.set_can_fast_track_queue(true);
+                this.can_fast_track_queue = true;
                 message_editor.update(cx, |message_editor, cx| {
                     message_editor.clear(window, cx);
                 });
@@ -638,10 +1007,23 @@ impl AcpThreadView {
         .detach_and_log_err(cx);
     }
 
+    pub fn add_to_queue(
+        &mut self,
+        content: Vec<acp::ContentBlock>,
+        tracked_buffers: Vec<Entity<Buffer>>,
+        cx: &mut Context<Self>,
+    ) {
+        self.local_queued_messages.push(QueuedMessage {
+            content,
+            tracked_buffers,
+        });
+        self.sync_queue_flag_to_native_thread(cx);
+    }
+
     pub fn remove_from_queue(
         &mut self,
         index: usize,
-        cx: &mut Context<AcpServerView>,
+        cx: &mut Context<Self>,
     ) -> Option<QueuedMessage> {
         if index < self.local_queued_messages.len() {
             let removed = self.local_queued_messages.remove(index);
@@ -652,7 +1034,7 @@ impl AcpThreadView {
         }
     }
 
-    pub fn sync_queue_flag_to_native_thread(&self, cx: &mut Context<AcpServerView>) {
+    pub fn sync_queue_flag_to_native_thread(&self, cx: &mut Context<Self>) {
         if let Some(native_thread) = self.as_native_thread(cx) {
             let has_queued = self.has_queued_messages();
             native_thread.update(cx, |thread, _| {
@@ -666,7 +1048,7 @@ impl AcpThreadView {
         index: usize,
         is_send_now: bool,
         window: &mut Window,
-        cx: &mut Context<AcpServerView>,
+        cx: &mut Context<Self>,
     ) {
         let Some(queued) = self.remove_from_queue(index, cx) else {
             return;
@@ -709,22 +1091,18 @@ impl AcpThreadView {
 
     pub fn expand_message_editor(
         &mut self,
-        message_editor: Entity<MessageEditor>,
-        cx: &mut Context<AcpServerView>,
+        _: &ExpandMessageEditor,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
     ) {
-        self.set_editor_is_expanded(!self.editor_expanded, message_editor, cx);
+        self.set_editor_is_expanded(!self.editor_expanded, cx);
         cx.stop_propagation();
         cx.notify();
     }
 
-    pub fn set_editor_is_expanded(
-        &mut self,
-        is_expanded: bool,
-        message_editor: Entity<MessageEditor>,
-        cx: &mut Context<AcpServerView>,
-    ) {
+    pub fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
         self.editor_expanded = is_expanded;
-        message_editor.update(cx, |editor, cx| {
+        self.message_editor.update(cx, |editor, cx| {
             if is_expanded {
                 editor.set_mode(
                     EditorMode::Full {
@@ -753,7 +1131,7 @@ impl AcpThreadView {
         title_editor: &Entity<Editor>,
         event: &EditorEvent,
         window: &mut Window,
-        cx: &mut Context<AcpServerView>,
+        cx: &mut Context<Self>,
     ) {
         let thread = &self.thread;
 
@@ -777,12 +1155,7 @@ impl AcpThreadView {
         }
     }
 
-    pub fn cancel_editing(
-        &mut self,
-        focus_handle: FocusHandle,
-        window: &mut Window,
-        cx: &mut Context<AcpServerView>,
-    ) {
+    pub fn cancel_editing(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
         if let Some(index) = self.editing_message.take()
             && let Some(editor) = &self
                 .entry_view_state
@@ -803,7 +1176,6 @@ impl AcpThreadView {
                 }
             })
         };
-        focus_handle.focus(window, cx);
         cx.notify();
     }
 
@@ -815,7 +1187,7 @@ impl AcpThreadView {
         option_id: acp::PermissionOptionId,
         option_kind: acp::PermissionOptionKind,
         window: &mut Window,
-        cx: &mut Context<AcpServerView>,
+        cx: &mut Context<Self>,
     ) {
         let thread = &self.thread;
         let agent_telemetry_id = thread.read(cx).connection().telemetry_id();
@@ -840,11 +1212,23 @@ impl AcpThreadView {
         cx.notify();
     }
 
+    pub fn allow_always(&mut self, _: &AllowAlways, window: &mut Window, cx: &mut Context<Self>) {
+        self.authorize_pending_tool_call(acp::PermissionOptionKind::AllowAlways, window, cx);
+    }
+
+    pub fn allow_once(&mut self, _: &AllowOnce, window: &mut Window, cx: &mut Context<Self>) {
+        self.authorize_pending_with_granularity(true, window, cx);
+    }
+
+    pub fn reject_once(&mut self, _: &RejectOnce, window: &mut Window, cx: &mut Context<Self>) {
+        self.authorize_pending_with_granularity(false, window, cx);
+    }
+
     pub fn authorize_pending_tool_call(
         &mut self,
         kind: acp::PermissionOptionKind,
         window: &mut Window,
-        cx: &mut Context<AcpServerView>,
+        cx: &mut Context<Self>,
     ) -> Option<()> {
         let thread = self.thread.read(cx);
         let tool_call = thread.first_tool_awaiting_confirmation()?;
@@ -864,10 +1248,30 @@ impl AcpThreadView {
         Some(())
     }
 
+    fn handle_authorize_tool_call(
+        &mut self,
+        action: &AuthorizeToolCall,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let tool_call_id = acp::ToolCallId::new(action.tool_call_id.clone());
+        let option_id = acp::PermissionOptionId::new(action.option_id.clone());
+        let option_kind = match action.option_kind.as_str() {
+            "AllowOnce" => acp::PermissionOptionKind::AllowOnce,
+            "AllowAlways" => acp::PermissionOptionKind::AllowAlways,
+            "RejectOnce" => acp::PermissionOptionKind::RejectOnce,
+            "RejectAlways" => acp::PermissionOptionKind::RejectAlways,
+            _ => acp::PermissionOptionKind::AllowOnce,
+        };
+
+        self.authorize_tool_call(tool_call_id, option_id, option_kind, window, cx);
+    }
+
     pub fn handle_select_permission_granularity(
         &mut self,
         action: &SelectPermissionGranularity,
-        cx: &mut Context<AcpServerView>,
+        _window: &mut Window,
+        cx: &mut Context<Self>,
     ) {
         let tool_call_id = acp::ToolCallId::new(action.tool_call_id.clone());
         self.selected_permission_granularity
@@ -876,9 +1280,57 @@ impl AcpThreadView {
         cx.notify();
     }
 
+    fn authorize_pending_with_granularity(
+        &mut self,
+        is_allow: bool,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Option<()> {
+        let thread = self.thread.read(cx);
+        let tool_call = thread.first_tool_awaiting_confirmation()?;
+        let ToolCallStatus::WaitingForConfirmation { options, .. } = &tool_call.status else {
+            return None;
+        };
+        let tool_call_id = tool_call.id.clone();
+
+        let PermissionOptions::Dropdown(choices) = options else {
+            let kind = if is_allow {
+                acp::PermissionOptionKind::AllowOnce
+            } else {
+                acp::PermissionOptionKind::RejectOnce
+            };
+            return self.authorize_pending_tool_call(kind, window, cx);
+        };
+
+        // Get selected index, defaulting to last option ("Only this time")
+        let selected_index = self
+            .selected_permission_granularity
+            .get(&tool_call_id)
+            .copied()
+            .unwrap_or_else(|| choices.len().saturating_sub(1));
+
+        let selected_choice = choices.get(selected_index).or(choices.last())?;
+
+        let selected_option = if is_allow {
+            &selected_choice.allow
+        } else {
+            &selected_choice.deny
+        };
+
+        self.authorize_tool_call(
+            tool_call_id,
+            selected_option.option_id.clone(),
+            selected_option.kind,
+            window,
+            cx,
+        );
+
+        Some(())
+    }
+
     // edits
 
-    pub fn keep_all(&mut self, cx: &mut Context<AcpServerView>) {
+    pub fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
         let thread = &self.thread;
         let telemetry = ActionLogTelemetry::from(thread.read(cx));
         let action_log = thread.read(cx).action_log().clone();

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

@@ -158,7 +158,7 @@ pub fn init(cx: &mut App) {
                             thread_view
                                 .read(cx)
                                 .as_active_thread()
-                                .map(|r| r.thread.clone())
+                                .map(|r| r.read(cx).thread.clone())
                         });
 
                     if let Some(thread) = thread {
@@ -916,12 +916,18 @@ impl AgentPanel {
     }
 
     fn expand_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        if let Some(thread_view) = self.active_thread_view() {
-            thread_view.update(cx, |view, cx| {
-                view.expand_message_editor(&ExpandMessageEditor, window, cx);
-                view.focus_handle(cx).focus(window, cx);
-            });
-        }
+        let Some(thread_view) = self.active_thread_view() else {
+            return;
+        };
+
+        let Some(active_thread) = thread_view.read(cx).as_active_thread() else {
+            return;
+        };
+
+        active_thread.update(cx, |active_thread, cx| {
+            active_thread.expand_message_editor(&ExpandMessageEditor, window, cx);
+            active_thread.focus_handle(cx).focus(window, cx);
+        })
     }
 
     fn history_kind_for_selected_agent(&self, cx: &App) -> Option<HistoryKind> {
@@ -1185,22 +1191,15 @@ impl AgentPanel {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let Some(workspace) = self.workspace.upgrade() else {
-            return;
-        };
-
-        match &self.active_view {
-            ActiveView::AgentThread { thread_view } => {
-                thread_view
-                    .update(cx, |thread_view, cx| {
-                        thread_view.open_thread_as_markdown(workspace, window, cx)
-                    })
+        if let Some(workspace) = self.workspace.upgrade()
+            && let Some(thread_view) = self.active_thread_view()
+            && let Some(active_thread) = thread_view.read(cx).as_active_thread()
+        {
+            active_thread.update(cx, |thread, cx| {
+                thread
+                    .open_thread_as_markdown(workspace, window, cx)
                     .detach_and_log_err(cx);
-            }
-            ActiveView::Uninitialized
-            | ActiveView::TextThread { .. }
-            | ActiveView::History { .. }
-            | ActiveView::Configuration => {}
+            });
         }
     }
 
@@ -1421,7 +1420,7 @@ impl AgentPanel {
             ActiveView::AgentThread { thread_view, .. } => thread_view
                 .read(cx)
                 .as_active_thread()
-                .map(|r| r.thread.clone()),
+                .map(|r| r.read(cx).thread.clone()),
             _ => None,
         }
     }
@@ -1849,7 +1848,7 @@ impl AgentPanel {
                 if let Some(title_editor) = thread_view
                     .read(cx)
                     .as_active_thread()
-                    .and_then(|ready| ready.title_editor.clone())
+                    .and_then(|r| r.read(cx).title_editor.clone())
                 {
                     let container = div()
                         .w_full()

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

@@ -21,7 +21,6 @@ mod terminal_inline_assistant;
 mod text_thread_editor;
 mod text_thread_history;
 mod ui;
-mod user_slash_command;
 
 use std::rc::Rc;
 use std::sync::Arc;

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

@@ -5,14 +5,11 @@ use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
 
 use crate::acp::AcpThreadHistory;
-use crate::user_slash_command::{self, CommandLoadError, UserSlashCommand};
 use acp_thread::{AgentSessionInfo, MentionUri};
 use anyhow::Result;
-use collections::{HashMap, HashSet};
 use editor::{
     CompletionProvider, Editor, ExcerptId, code_context_menus::COMPLETION_MENU_MAX_WIDTH,
 };
-use feature_flags::{FeatureFlagAppExt as _, UserSlashCommandsFeatureFlag};
 use futures::FutureExt as _;
 use fuzzy::{PathMatch, StringMatch, StringMatchCandidate};
 use gpui::{App, BackgroundExecutor, Entity, SharedString, Task, WeakEntity};
@@ -192,18 +189,6 @@ pub struct AvailableCommand {
     pub name: Arc<str>,
     pub description: Arc<str>,
     pub requires_argument: bool,
-    pub source: CommandSource,
-}
-
-/// The source of a slash command, used to differentiate UI behavior.
-#[derive(Debug, Clone, PartialEq)]
-pub enum CommandSource {
-    /// Command provided by the ACP server
-    Server,
-    /// User-defined command from a markdown file
-    UserDefined { template: Arc<str> },
-    /// User-defined command that failed to load
-    UserDefinedError { error_message: Arc<str> },
 }
 
 pub trait PromptCompletionProviderDelegate: Send + Sync + 'static {
@@ -215,18 +200,6 @@ pub trait PromptCompletionProviderDelegate: Send + Sync + 'static {
 
     fn available_commands(&self, cx: &App) -> Vec<AvailableCommand>;
     fn confirm_command(&self, cx: &mut App);
-
-    /// Returns cached user-defined slash commands, if available.
-    /// Default implementation returns None, meaning commands will be loaded from disk.
-    fn cached_user_commands(&self, _cx: &App) -> Option<HashMap<String, UserSlashCommand>> {
-        None
-    }
-
-    /// Returns cached errors from loading user-defined slash commands, if available.
-    /// Default implementation returns None.
-    fn cached_user_command_errors(&self, _cx: &App) -> Option<Vec<CommandLoadError>> {
-        None
-    }
 }
 
 pub struct PromptCompletionProvider<T: PromptCompletionProviderDelegate> {
@@ -803,111 +776,11 @@ impl<T: PromptCompletionProviderDelegate> PromptCompletionProvider<T> {
 
     fn search_slash_commands(&self, query: String, cx: &mut App) -> Task<Vec<AvailableCommand>> {
         let commands = self.source.available_commands(cx);
-        let server_command_names = commands
-            .iter()
-            .map(|command| command.name.as_ref().to_string())
-            .collect::<HashSet<_>>();
-
-        // Try to use cached user commands and errors first
-        let cached_user_commands = if cx.has_flag::<UserSlashCommandsFeatureFlag>() {
-            self.source.cached_user_commands(cx)
-        } else {
-            None
-        };
-
-        let cached_user_command_errors = if cx.has_flag::<UserSlashCommandsFeatureFlag>() {
-            self.source.cached_user_command_errors(cx)
-        } else {
-            None
-        };
-
-        // Get fs and worktree roots for async command loading (only if not cached)
-        let (fs, worktree_roots) =
-            if cached_user_commands.is_none() && cx.has_flag::<UserSlashCommandsFeatureFlag>() {
-                let workspace = self.workspace.upgrade();
-                let fs = workspace
-                    .as_ref()
-                    .map(|w| w.read(cx).project().read(cx).fs().clone());
-                let roots: Vec<std::path::PathBuf> = workspace
-                    .map(|workspace| {
-                        workspace
-                            .read(cx)
-                            .visible_worktrees(cx)
-                            .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
-                            .collect()
-                    })
-                    .unwrap_or_default();
-                (fs, roots)
-            } else {
-                (None, Vec::new())
-            };
+        if commands.is_empty() {
+            return Task::ready(Vec::new());
+        }
 
         cx.spawn(async move |cx| {
-            let mut commands = commands;
-
-            // Use cached commands/errors if available, otherwise load from disk
-            let (mut user_commands, mut user_command_errors): (
-                Vec<UserSlashCommand>,
-                Vec<CommandLoadError>,
-            ) = if let Some(cached) = cached_user_commands {
-                let errors = cached_user_command_errors.unwrap_or_default();
-                (cached.into_values().collect(), errors)
-            } else if let Some(fs) = fs {
-                let load_result =
-                    crate::user_slash_command::load_all_commands_async(&fs, &worktree_roots).await;
-
-                (load_result.commands, load_result.errors)
-            } else {
-                (Vec::new(), Vec::new())
-            };
-
-            user_slash_command::apply_server_command_conflicts(
-                &mut user_commands,
-                &mut user_command_errors,
-                &server_command_names,
-            );
-
-            let conflicting_names: HashSet<String> = user_command_errors
-                .iter()
-                .filter_map(|error| error.command_name())
-                .filter(|name| server_command_names.contains(name))
-                .collect();
-
-            if !conflicting_names.is_empty() {
-                commands.retain(|command| !conflicting_names.contains(command.name.as_ref()));
-            }
-
-            for cmd in user_commands {
-                commands.push(AvailableCommand {
-                    name: cmd.name.clone(),
-                    description: cmd.description().into(),
-                    requires_argument: cmd.requires_arguments(),
-                    source: CommandSource::UserDefined {
-                        template: cmd.template.clone(),
-                    },
-                });
-            }
-
-            // Add errored commands so they show up in autocomplete with error indication.
-            // Errors for commands that don't match the query will be silently ignored here
-            // since the user will see them via the error callout in the thread view.
-            for error in user_command_errors {
-                if let Some(name) = error.command_name() {
-                    commands.push(AvailableCommand {
-                        name: name.into(),
-                        description: "".into(),
-                        requires_argument: false,
-                        source: CommandSource::UserDefinedError {
-                            error_message: error.message.into(),
-                        },
-                    });
-                }
-            }
-
-            if commands.is_empty() {
-                return Vec::new();
-            }
-
             let candidates = commands
                 .iter()
                 .enumerate()
@@ -1264,20 +1137,7 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
                         .await
                         .into_iter()
                         .map(|command| {
-                            let is_error =
-                                matches!(command.source, CommandSource::UserDefinedError { .. });
-
-                            // For errored commands, show the name with "(load error)" suffix
-                            let label_text = if is_error {
-                                format!("{} (load error)", command.name)
-                            } else {
-                                command.name.to_string()
-                            };
-
-                            // For errored commands, we don't want to insert anything useful
-                            let new_text = if is_error {
-                                format!("/{}", command.name)
-                            } else if let Some(argument) = argument.as_ref() {
+                            let new_text = if let Some(argument) = argument.as_ref() {
                                 format!("/{} {}", command.name, argument)
                             } else {
                                 format!("/{} ", command.name)
@@ -1286,72 +1146,21 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
                             let is_missing_argument =
                                 command.requires_argument && argument.is_none();
 
-                            // For errored commands, use a deprecated-style label to indicate the error
-                            let label = if is_error {
-                                // Create a label where the command name portion has a highlight
-                                // that will be rendered with strikethrough by the completion menu
-                                // (similar to deprecated LSP completions)
-                                CodeLabel::plain(label_text, None)
-                            } else {
-                                CodeLabel::plain(label_text, None)
-                            };
-
-                            // For errored commands, show the error message in documentation
-                            let documentation =
-                                if let CommandSource::UserDefinedError { error_message } =
-                                    &command.source
-                                {
-                                    Some(CompletionDocumentation::MultiLinePlainText(
-                                        error_message.to_string().into(),
-                                    ))
-                                } else if !command.description.is_empty() {
-                                    Some(CompletionDocumentation::MultiLinePlainText(
-                                        command.description.to_string().into(),
-                                    ))
-                                } else {
-                                    None
-                                };
-
-                            // For errored commands, use a red X icon
-                            let icon_path = if is_error {
-                                Some(IconName::XCircle.path().into())
-                            } else {
-                                None
-                            };
-
                             Completion {
                                 replace_range: source_range.clone(),
                                 new_text,
-                                label,
-                                documentation,
-                                source: if is_error {
-                                    // Use a custom source that marks this as deprecated/errored
-                                    // so the completion menu renders it with strikethrough
-                                    project::CompletionSource::Lsp {
-                                        insert_range: None,
-                                        server_id: language::LanguageServerId(0),
-                                        lsp_completion: Box::new(lsp::CompletionItem {
-                                            label: command.name.to_string(),
-                                            deprecated: Some(true),
-                                            ..Default::default()
-                                        }),
-                                        lsp_defaults: None,
-                                        resolved: true,
-                                    }
-                                } else {
-                                    project::CompletionSource::Custom
-                                },
-                                icon_path,
+                                label: CodeLabel::plain(command.name.to_string(), None),
+                                documentation: Some(CompletionDocumentation::MultiLinePlainText(
+                                    command.description.into(),
+                                )),
+                                source: project::CompletionSource::Custom,
+                                icon_path: None,
                                 match_start: None,
                                 snippet_deduplication_key: None,
                                 insert_text_mode: None,
                                 confirm: Some(Arc::new({
                                     let source = source.clone();
                                     move |intent, _window, cx| {
-                                        // Don't confirm errored commands
-                                        if is_error {
-                                            return false;
-                                        }
                                         if !is_missing_argument {
                                             cx.defer({
                                                 let source = source.clone();

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

@@ -1,5 +1,4 @@
 use crate::{
-    agent_panel::AgentType,
     language_model_selector::{LanguageModelSelector, language_model_selector},
     ui::ModelSelectorTooltip,
 };
@@ -11,7 +10,7 @@ use collections::{BTreeSet, HashMap, HashSet, hash_map};
 use editor::{
     Anchor, Editor, EditorEvent, MenuEditPredictionsPolicy, MultiBuffer, MultiBufferOffset,
     MultiBufferSnapshot, RowExt, ToOffset as _, ToPoint as _,
-    actions::{MoveToEndOfLine, Newline, SendReviewToAgent, ShowCompletions},
+    actions::{MoveToEndOfLine, Newline, ShowCompletions},
     display_map::{
         BlockPlacement, BlockProperties, BlockStyle, Crease, CreaseMetadata, CustomBlockId, FoldId,
         RenderBlock, ToDisplayPoint,
@@ -228,8 +227,7 @@ impl TextThreadEditor {
                     .register_action(TextThreadEditor::quote_selection)
                     .register_action(TextThreadEditor::insert_selection)
                     .register_action(TextThreadEditor::copy_code)
-                    .register_action(TextThreadEditor::handle_insert_dragged_files)
-                    .register_action(TextThreadEditor::handle_send_review_to_agent);
+                    .register_action(TextThreadEditor::handle_insert_dragged_files);
             },
         )
         .detach();
@@ -1521,159 +1519,6 @@ impl TextThreadEditor {
         }
     }
 
-    /// Handles the SendReviewToAgent action from the ProjectDiff toolbar.
-    /// Collects ALL stored review comments from ALL hunks and sends them
-    /// to the Agent panel as creases.
-    pub fn handle_send_review_to_agent(
-        workspace: &mut Workspace,
-        _: &SendReviewToAgent,
-        window: &mut Window,
-        cx: &mut Context<Workspace>,
-    ) {
-        use editor::{DiffHunkKey, StoredReviewComment};
-        use git_ui::project_diff::ProjectDiff;
-
-        // Find the ProjectDiff item
-        let Some(project_diff) = workspace.items_of_type::<ProjectDiff>(cx).next() else {
-            workspace.show_toast(
-                Toast::new(
-                    NotificationId::unique::<SendReviewToAgent>(),
-                    "No Project Diff panel found. Open it first to add review comments.",
-                ),
-                cx,
-            );
-            return;
-        };
-
-        // Get the buffer reference first (before taking comments)
-        let buffer = project_diff.update(cx, |project_diff, cx| {
-            project_diff
-                .editor()
-                .read(cx)
-                .rhs_editor()
-                .read(cx)
-                .buffer()
-                .clone()
-        });
-
-        // Extract all stored comments from all hunks
-        let all_comments: Vec<(DiffHunkKey, Vec<StoredReviewComment>)> =
-            project_diff.update(cx, |project_diff, cx| {
-                let editor = project_diff.editor().read(cx).rhs_editor().clone();
-                editor.update(cx, |editor, cx| editor.take_all_review_comments(cx))
-            });
-
-        // Flatten: we have Vec<(DiffHunkKey, Vec<StoredReviewComment>)>
-        // Convert to Vec<StoredReviewComment> for processing
-        let comments: Vec<StoredReviewComment> = all_comments
-            .into_iter()
-            .flat_map(|(_, comments)| comments)
-            .collect();
-
-        if comments.is_empty() {
-            workspace.show_toast(
-                Toast::new(
-                    NotificationId::unique::<SendReviewToAgent>(),
-                    "No review comments to send. Add comments using the + button in the diff view.",
-                ),
-                cx,
-            );
-            return;
-        }
-
-        // Get or create the agent panel
-        let Some(panel) = workspace.panel::<crate::AgentPanel>(cx) else {
-            workspace.show_toast(
-                Toast::new(
-                    NotificationId::unique::<SendReviewToAgent>(),
-                    "Agent panel is not available.",
-                ),
-                cx,
-            );
-            return;
-        };
-
-        // Create a new thread if there isn't an active one (synchronous call)
-        let has_active_thread = panel.read(cx).active_thread_view().is_some();
-        if !has_active_thread {
-            panel.update(cx, |panel, cx| {
-                panel.new_agent_thread(AgentType::NativeAgent, window, cx);
-            });
-        }
-
-        // Focus the agent panel
-        workspace.focus_panel::<crate::AgentPanel>(window, cx);
-
-        // Defer inserting creases until after the current update cycle completes,
-        // allowing the newly created thread (if any) to fully initialize.
-        cx.defer_in(window, move |workspace, window, cx| {
-            let Some(panel) = workspace.panel::<crate::AgentPanel>(cx) else {
-                workspace.show_toast(
-                    Toast::new(
-                        NotificationId::unique::<SendReviewToAgent>(),
-                        "Agent panel closed unexpectedly.",
-                    ),
-                    cx,
-                );
-                return;
-            };
-
-            let thread_view = panel.read(cx).active_thread_view().cloned();
-            let Some(thread_view) = thread_view else {
-                workspace.show_toast(
-                    Toast::new(
-                        NotificationId::unique::<SendReviewToAgent>(),
-                        "No active thread view available after creating thread.",
-                    ),
-                    cx,
-                );
-                return;
-            };
-
-            // Build creases for all comments, grouping by code snippet
-            // so each snippet appears once with all its comments
-            let snapshot = buffer.read(cx).snapshot(cx);
-
-            // Group comments by their point range (code snippet)
-            let mut comments_by_range: std::collections::BTreeMap<
-                (rope::Point, rope::Point),
-                Vec<String>,
-            > = std::collections::BTreeMap::new();
-
-            for comment in comments {
-                let start = comment.range.start.to_point(&snapshot);
-                let end = comment.range.end.to_point(&snapshot);
-                comments_by_range
-                    .entry((start, end))
-                    .or_default()
-                    .push(comment.comment);
-            }
-
-            // Build one crease per unique code snippet with all its comments
-            let mut all_creases = Vec::new();
-            for ((start, end), comment_texts) in comments_by_range {
-                let point_range = start..end;
-
-                let mut creases =
-                    selections_creases(vec![point_range.clone()], snapshot.clone(), cx);
-
-                // Append all comments after the code snippet
-                for (code_text, crease_title) in &mut creases {
-                    let comments_section = comment_texts.join("\n\n");
-                    *code_text = format!("{}\n\n{}", code_text, comments_section);
-                    *crease_title = format!("Review: {}", crease_title);
-                }
-
-                all_creases.extend(creases);
-            }
-
-            // Insert all creases into the message editor
-            thread_view.update(cx, |thread_view, cx| {
-                thread_view.insert_code_crease(all_creases, window, cx);
-            });
-        });
-    }
-
     pub fn quote_ranges(
         &mut self,
         ranges: Vec<Range<Point>>,

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

@@ -1,2118 +0,0 @@
-use anyhow::{Result, anyhow};
-use collections::{HashMap, HashSet};
-use fs::Fs;
-use futures::StreamExt;
-use gpui::{Context, EventEmitter, Task};
-use std::borrow::Cow;
-use std::path::{Path, PathBuf};
-use std::sync::Arc;
-use std::time::Duration;
-
-/// An error that occurred while loading a command file.
-#[derive(Debug, Clone)]
-pub struct CommandLoadError {
-    /// The path to the file that failed to load
-    pub path: PathBuf,
-    /// The base path of the commands directory (used to derive command name)
-    pub base_path: PathBuf,
-    /// A description of the error
-    pub message: String,
-}
-
-impl CommandLoadError {
-    /// Derives the command name from the file path, similar to how successful commands are named.
-    /// Returns None if the command name cannot be determined (e.g., for directory errors).
-    pub fn command_name(&self) -> Option<String> {
-        let base_name = self.path.file_stem()?.to_string_lossy().into_owned();
-
-        // Only derive command name for .md files
-        if self.path.extension().is_none_or(|ext| ext != "md") {
-            return None;
-        }
-
-        let namespace = self
-            .path
-            .parent()
-            .and_then(|parent| parent.strip_prefix(&self.base_path).ok())
-            .filter(|rel| !rel.as_os_str().is_empty())
-            .map(|rel| {
-                rel.to_string_lossy()
-                    .replace(std::path::MAIN_SEPARATOR, "/")
-            });
-
-        let name = match &namespace {
-            Some(namespace) => format!("{}:{}", namespace.replace('/', ":"), base_name),
-            None => base_name,
-        };
-
-        Some(name)
-    }
-}
-
-impl std::fmt::Display for CommandLoadError {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(
-            f,
-            "Failed to load {}: {}",
-            self.path.display(),
-            self.message
-        )
-    }
-}
-
-/// Result of loading commands, including any errors encountered.
-#[derive(Debug, Default, Clone)]
-pub struct CommandLoadResult {
-    /// Successfully loaded commands
-    pub commands: Vec<UserSlashCommand>,
-    /// Errors encountered while loading commands
-    pub errors: Vec<CommandLoadError>,
-}
-
-/// The scope of a user-defined slash command.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum CommandScope {
-    /// Project-specific command from .zed/commands/
-    Project,
-    /// User-wide command from config_dir()/commands/
-    User,
-}
-
-/// A user-defined slash command loaded from a markdown file.
-#[derive(Debug, Clone, PartialEq)]
-pub struct UserSlashCommand {
-    /// The command name for invocation.
-    /// For commands in subdirectories, this is prefixed: "namespace:name" (e.g., "frontend:component")
-    /// For commands in the root, this is just the filename without .md extension.
-    pub name: Arc<str>,
-    /// The template content from the file
-    pub template: Arc<str>,
-    /// The namespace (subdirectory path, if any), used for description display
-    pub namespace: Option<Arc<str>>,
-    /// The full path to the command file
-    pub path: PathBuf,
-    /// Whether this is a project or user command
-    pub scope: CommandScope,
-}
-
-impl UserSlashCommand {
-    /// Returns a description string for display in completions.
-    pub fn description(&self) -> String {
-        String::new()
-    }
-
-    /// Returns true if this command has any placeholders ($1, $2, etc. or $ARGUMENTS)
-    pub fn requires_arguments(&self) -> bool {
-        has_placeholders(&self.template)
-    }
-}
-
-fn command_base_path(command: &UserSlashCommand) -> PathBuf {
-    let mut base_path = command.path.clone();
-    base_path.pop();
-    if let Some(namespace) = &command.namespace {
-        for segment in namespace.split('/') {
-            if segment.is_empty() {
-                continue;
-            }
-            if !base_path.pop() {
-                break;
-            }
-        }
-    }
-    base_path
-}
-
-impl CommandLoadError {
-    pub fn from_command(command: &UserSlashCommand, message: String) -> Self {
-        Self {
-            path: command.path.clone(),
-            base_path: command_base_path(command),
-            message,
-        }
-    }
-}
-
-/// Parsed user command from input text
-#[derive(Debug, Clone, PartialEq)]
-pub struct ParsedUserCommand<'a> {
-    pub name: &'a str,
-    pub raw_arguments: &'a str,
-}
-
-/// Returns the path to the user commands directory.
-pub fn user_commands_dir() -> PathBuf {
-    paths::config_dir().join("commands")
-}
-
-/// Returns the path to the project commands directory for a given worktree root.
-pub fn project_commands_dir(worktree_root: &Path) -> PathBuf {
-    worktree_root.join(".zed").join("commands")
-}
-
-/// Events emitted by SlashCommandRegistry
-#[derive(Debug, Clone)]
-#[allow(dead_code)] // Infrastructure for future caching implementation
-pub enum SlashCommandRegistryEvent {
-    /// Commands have been reloaded
-    CommandsChanged,
-}
-
-/// A registry that caches user-defined slash commands and watches for changes.
-/// Currently used in tests; will be integrated into the UI layer for caching.
-#[allow(dead_code)]
-pub struct SlashCommandRegistry {
-    fs: Arc<dyn Fs>,
-    commands: HashMap<String, UserSlashCommand>,
-    errors: Vec<CommandLoadError>,
-    worktree_roots: Vec<PathBuf>,
-    _watch_task: Option<Task<()>>,
-}
-
-impl EventEmitter<SlashCommandRegistryEvent> for SlashCommandRegistry {}
-
-#[allow(dead_code)]
-impl SlashCommandRegistry {
-    /// Creates a new registry and starts loading commands.
-    pub fn new(fs: Arc<dyn Fs>, worktree_roots: Vec<PathBuf>, cx: &mut Context<Self>) -> Self {
-        let mut this = Self {
-            fs,
-            commands: HashMap::default(),
-            errors: Vec::new(),
-            worktree_roots,
-            _watch_task: None,
-        };
-
-        this.start_watching(cx);
-        this.reload(cx);
-
-        this
-    }
-
-    /// Returns all loaded commands.
-    pub fn commands(&self) -> &HashMap<String, UserSlashCommand> {
-        &self.commands
-    }
-
-    /// Returns any errors from the last load.
-    pub fn errors(&self) -> &[CommandLoadError] {
-        &self.errors
-    }
-
-    /// Updates the worktree roots and reloads commands.
-    pub fn set_worktree_roots(&mut self, roots: Vec<PathBuf>, cx: &mut Context<Self>) {
-        if self.worktree_roots != roots {
-            self.worktree_roots = roots;
-            self.start_watching(cx);
-            self.reload(cx);
-        }
-    }
-
-    /// Manually triggers a reload of all commands.
-    pub fn reload(&mut self, cx: &mut Context<Self>) {
-        let fs = self.fs.clone();
-        let worktree_roots = self.worktree_roots.clone();
-
-        cx.spawn(async move |this, cx| {
-            let result = load_all_commands_async(&fs, &worktree_roots).await;
-            this.update(cx, |this, cx| {
-                this.commands = commands_to_map(&result.commands);
-                this.errors = result.errors;
-                cx.emit(SlashCommandRegistryEvent::CommandsChanged);
-            })
-        })
-        .detach_and_log_err(cx);
-    }
-
-    fn start_watching(&mut self, cx: &mut Context<Self>) {
-        let fs = self.fs.clone();
-        let worktree_roots = self.worktree_roots.clone();
-
-        let task = cx.spawn(async move |this, cx| {
-            let user_dir = user_commands_dir();
-            let mut dirs_to_watch = vec![user_dir];
-            for root in &worktree_roots {
-                dirs_to_watch.push(project_commands_dir(root));
-            }
-
-            let mut watch_streams = Vec::new();
-            for dir in &dirs_to_watch {
-                let (stream, _watcher) = fs.watch(dir, Duration::from_millis(100)).await;
-                watch_streams.push(stream);
-            }
-
-            let mut combined = futures::stream::select_all(watch_streams);
-
-            while let Some(events) = combined.next().await {
-                let should_reload = events.iter().any(|event| {
-                    event.path.extension().is_some_and(|ext| ext == "md")
-                        || event.kind == Some(fs::PathEventKind::Created)
-                        || event.kind == Some(fs::PathEventKind::Removed)
-                });
-
-                if should_reload {
-                    let result = load_all_commands_async(&fs, &worktree_roots).await;
-                    let _ = this.update(cx, |this, cx| {
-                        this.commands = commands_to_map(&result.commands);
-                        this.errors = result.errors;
-                        cx.emit(SlashCommandRegistryEvent::CommandsChanged);
-                    });
-                }
-            }
-        });
-
-        self._watch_task = Some(task);
-    }
-}
-
-/// Loads all commands (both project and user) for given worktree roots asynchronously.
-pub async fn load_all_commands_async(
-    fs: &Arc<dyn Fs>,
-    worktree_roots: &[PathBuf],
-) -> CommandLoadResult {
-    let mut result = CommandLoadResult::default();
-    let mut seen_commands: HashMap<String, PathBuf> = HashMap::default();
-
-    // Load project commands first
-    for root in worktree_roots {
-        let commands_path = project_commands_dir(root);
-        let project_result =
-            load_commands_from_path_async(fs, &commands_path, CommandScope::Project).await;
-        result.errors.extend(project_result.errors);
-        for cmd in project_result.commands {
-            if let Some(existing_path) = seen_commands.get(&*cmd.name) {
-                result.errors.push(CommandLoadError {
-                    path: cmd.path.clone(),
-                    base_path: commands_path.clone(),
-                    message: format!(
-                        "Command '{}' is ambiguous: also defined at {}",
-                        cmd.name,
-                        existing_path.display()
-                    ),
-                });
-            } else {
-                seen_commands.insert(cmd.name.to_string(), cmd.path.clone());
-                result.commands.push(cmd);
-            }
-        }
-    }
-
-    // Load user commands
-    let user_commands_path = user_commands_dir();
-    let user_result =
-        load_commands_from_path_async(fs, &user_commands_path, CommandScope::User).await;
-    result.errors.extend(user_result.errors);
-    for cmd in user_result.commands {
-        if let Some(existing_path) = seen_commands.get(&*cmd.name) {
-            result.errors.push(CommandLoadError {
-                path: cmd.path.clone(),
-                base_path: user_commands_path.clone(),
-                message: format!(
-                    "Command '{}' is ambiguous: also defined at {}",
-                    cmd.name,
-                    existing_path.display()
-                ),
-            });
-        } else {
-            seen_commands.insert(cmd.name.to_string(), cmd.path.clone());
-            result.commands.push(cmd);
-        }
-    }
-
-    result
-}
-
-async fn load_commands_from_path_async(
-    fs: &Arc<dyn Fs>,
-    commands_path: &Path,
-    scope: CommandScope,
-) -> CommandLoadResult {
-    let mut result = CommandLoadResult::default();
-
-    if !fs.is_dir(commands_path).await {
-        return result;
-    }
-
-    load_commands_from_dir_async(fs, commands_path, commands_path, scope, &mut result).await;
-    result
-}
-
-fn load_commands_from_dir_async<'a>(
-    fs: &'a Arc<dyn Fs>,
-    base_path: &'a Path,
-    current_path: &'a Path,
-    scope: CommandScope,
-    result: &'a mut CommandLoadResult,
-) -> futures::future::BoxFuture<'a, ()> {
-    Box::pin(async move {
-        let entries = match fs.read_dir(current_path).await {
-            Ok(entries) => entries,
-            Err(e) => {
-                result.errors.push(CommandLoadError {
-                    path: current_path.to_path_buf(),
-                    base_path: base_path.to_path_buf(),
-                    message: format!("Failed to read directory: {}", e),
-                });
-                return;
-            }
-        };
-
-        let entries: Vec<_> = entries.collect().await;
-
-        for entry in entries {
-            let path = match entry {
-                Ok(path) => path,
-                Err(e) => {
-                    result.errors.push(CommandLoadError {
-                        path: current_path.to_path_buf(),
-                        base_path: base_path.to_path_buf(),
-                        message: format!("Failed to read directory entry: {}", e),
-                    });
-                    continue;
-                }
-            };
-
-            if fs.is_dir(&path).await {
-                load_commands_from_dir_async(fs, base_path, &path, scope, result).await;
-            } else if path.extension().is_some_and(|ext| ext == "md") {
-                match load_command_file_async(fs, base_path, &path, scope).await {
-                    Ok(Some(command)) => result.commands.push(command),
-                    Ok(None) => {} // Empty file, skip silently
-                    Err(e) => {
-                        result.errors.push(CommandLoadError {
-                            path: path.clone(),
-                            base_path: base_path.to_path_buf(),
-                            message: e.to_string(),
-                        });
-                    }
-                }
-            }
-        }
-    })
-}
-
-async fn load_command_file_async(
-    fs: &Arc<dyn Fs>,
-    base_path: &Path,
-    file_path: &Path,
-    scope: CommandScope,
-) -> Result<Option<UserSlashCommand>> {
-    let base_name = match file_path.file_stem() {
-        Some(stem) => stem.to_string_lossy().into_owned(),
-        None => return Ok(None),
-    };
-
-    let template = fs.load(file_path).await?;
-    if template.is_empty() {
-        return Ok(None);
-    }
-    if template.trim().is_empty() {
-        return Err(anyhow!("Command file contains only whitespace"));
-    }
-
-    let namespace = file_path
-        .parent()
-        .and_then(|parent| parent.strip_prefix(base_path).ok())
-        .filter(|rel| !rel.as_os_str().is_empty())
-        .map(|rel| {
-            rel.to_string_lossy()
-                .replace(std::path::MAIN_SEPARATOR, "/")
-        });
-
-    // Build the full command name: "namespace:basename" or just "basename"
-    let name = match &namespace {
-        Some(namespace) => format!("{}:{}", namespace.replace('/', ":"), base_name),
-        None => base_name,
-    };
-
-    Ok(Some(UserSlashCommand {
-        name: name.into(),
-        template: template.into(),
-        namespace: namespace.map(|s| s.into()),
-        path: file_path.to_path_buf(),
-        scope,
-    }))
-}
-
-/// Converts a list of UserSlashCommand to a HashMap for quick lookup.
-/// The key is the command name.
-pub fn commands_to_map(commands: &[UserSlashCommand]) -> HashMap<String, UserSlashCommand> {
-    let mut map = HashMap::default();
-    for cmd in commands {
-        map.insert(cmd.name.to_string(), cmd.clone());
-    }
-    map
-}
-
-fn has_error_for_command(errors: &[CommandLoadError], name: &str) -> bool {
-    errors
-        .iter()
-        .any(|error| error.command_name().as_deref() == Some(name))
-}
-
-fn server_conflict_message(name: &str) -> String {
-    format!(
-        "Command '{}' conflicts with server-provided /{}",
-        name, name
-    )
-}
-
-pub fn apply_server_command_conflicts(
-    commands: &mut Vec<UserSlashCommand>,
-    errors: &mut Vec<CommandLoadError>,
-    server_command_names: &HashSet<String>,
-) {
-    commands.retain(|command| {
-        if server_command_names.contains(command.name.as_ref()) {
-            if !has_error_for_command(errors, command.name.as_ref()) {
-                errors.push(CommandLoadError::from_command(
-                    command,
-                    server_conflict_message(command.name.as_ref()),
-                ));
-            }
-            false
-        } else {
-            true
-        }
-    });
-}
-
-pub fn apply_server_command_conflicts_to_map(
-    commands: &mut HashMap<String, UserSlashCommand>,
-    errors: &mut Vec<CommandLoadError>,
-    server_command_names: &HashSet<String>,
-) {
-    commands.retain(|name, command| {
-        if server_command_names.contains(name) {
-            if !has_error_for_command(errors, name) {
-                errors.push(CommandLoadError::from_command(
-                    command,
-                    server_conflict_message(name),
-                ));
-            }
-            false
-        } else {
-            true
-        }
-    });
-}
-
-/// Parses a line of input to extract a user command invocation.
-/// Returns None if the line doesn't start with a slash command.
-pub fn try_parse_user_command(line: &str) -> Option<ParsedUserCommand<'_>> {
-    let line = line.trim_start();
-    if !line.starts_with('/') {
-        return None;
-    }
-
-    let after_slash = &line[1..];
-    let (name, raw_arguments) = if let Some(space_idx) = after_slash.find(char::is_whitespace) {
-        let name = &after_slash[..space_idx];
-        let rest = &after_slash[space_idx..].trim_start();
-        (name, *rest)
-    } else {
-        (after_slash, "")
-    };
-
-    if name.is_empty() {
-        return None;
-    }
-
-    Some(ParsedUserCommand {
-        name,
-        raw_arguments,
-    })
-}
-
-/// Parses command arguments, supporting quoted strings.
-/// - Unquoted arguments are space-separated
-/// - Quoted arguments can contain spaces: "multi word arg"
-/// - Escape sequences: \" for literal quote, \\ for backslash, \n for newline
-pub fn parse_arguments(input: &str) -> Result<Vec<Cow<'_, str>>> {
-    let mut arguments = Vec::new();
-    let mut chars = input.char_indices().peekable();
-
-    while let Some((start_idx, c)) = chars.next() {
-        if c.is_whitespace() {
-            continue;
-        }
-
-        if c == '"' {
-            let mut result = String::new();
-            let mut closed = false;
-
-            while let Some((_, ch)) = chars.next() {
-                if ch == '\\' {
-                    if let Some((_, next_ch)) = chars.next() {
-                        match next_ch {
-                            '"' => result.push('"'),
-                            '\\' => result.push('\\'),
-                            'n' => result.push('\n'),
-                            other => {
-                                return Err(anyhow!("Unknown escape sequence: \\{}", other));
-                            }
-                        }
-                    } else {
-                        return Err(anyhow!("Unexpected end of input after backslash"));
-                    }
-                } else if ch == '"' {
-                    closed = true;
-                    break;
-                } else {
-                    result.push(ch);
-                }
-            }
-
-            if !closed {
-                return Err(anyhow!("Unclosed quote in command arguments"));
-            }
-
-            arguments.push(Cow::Owned(result));
-        } else {
-            let mut end_idx = start_idx + c.len_utf8();
-            while let Some(&(idx, ch)) = chars.peek() {
-                if ch.is_whitespace() {
-                    break;
-                }
-                if ch == '"' {
-                    return Err(anyhow!("Quote in middle of unquoted argument"));
-                }
-                end_idx = idx + ch.len_utf8();
-                chars.next();
-            }
-
-            arguments.push(Cow::Borrowed(&input[start_idx..end_idx]));
-        }
-    }
-
-    Ok(arguments)
-}
-
-/// Checks if a template has any placeholders ($1, $2, etc. or $ARGUMENTS)
-pub fn has_placeholders(template: &str) -> bool {
-    count_positional_placeholders(template) > 0 || template.contains("$ARGUMENTS")
-}
-
-/// Counts the highest positional placeholder number in the template.
-/// For example, "$1 and $3" returns 3.
-pub fn count_positional_placeholders(template: &str) -> usize {
-    let mut max_placeholder = 0;
-    let mut chars = template.chars().peekable();
-
-    while let Some(c) = chars.next() {
-        if c == '\\' {
-            chars.next();
-            continue;
-        }
-        if c == '$' {
-            let mut num_str = String::new();
-            while let Some(&next_c) = chars.peek() {
-                if next_c.is_ascii_digit() {
-                    num_str.push(next_c);
-                    chars.next();
-                } else {
-                    break;
-                }
-            }
-            if !num_str.is_empty() {
-                if let Ok(n) = num_str.parse::<usize>() {
-                    max_placeholder = max_placeholder.max(n);
-                }
-            }
-        }
-    }
-
-    max_placeholder
-}
-
-/// Validates that arguments match the template's placeholders.
-/// Templates can use $ARGUMENTS (all args as one string) or $1, $2, etc. (positional).
-pub fn validate_arguments(
-    command_name: &str,
-    template: &str,
-    arguments: &[Cow<'_, str>],
-) -> Result<()> {
-    if template.is_empty() {
-        return Err(anyhow!("Template cannot be empty"));
-    }
-
-    let has_arguments_placeholder = template.contains("$ARGUMENTS");
-    let positional_count = count_positional_placeholders(template);
-
-    if has_arguments_placeholder {
-        // $ARGUMENTS accepts any number of arguments (including zero)
-        // But if there are also positional placeholders, validate those
-        if positional_count > 0 && arguments.len() < positional_count {
-            return Err(anyhow!(
-                "The /{} command requires {} positional {}, but only {} {} provided",
-                command_name,
-                positional_count,
-                if positional_count == 1 {
-                    "argument"
-                } else {
-                    "arguments"
-                },
-                arguments.len(),
-                if arguments.len() == 1 { "was" } else { "were" }
-            ));
-        }
-        return Ok(());
-    }
-
-    if positional_count == 0 && !arguments.is_empty() {
-        return Err(anyhow!(
-            "The /{} command accepts no arguments, but {} {} provided",
-            command_name,
-            arguments.len(),
-            if arguments.len() == 1 { "was" } else { "were" }
-        ));
-    }
-
-    if arguments.len() < positional_count {
-        return Err(anyhow!(
-            "The /{} command requires {} {}, but only {} {} provided",
-            command_name,
-            positional_count,
-            if positional_count == 1 {
-                "argument"
-            } else {
-                "arguments"
-            },
-            arguments.len(),
-            if arguments.len() == 1 { "was" } else { "were" }
-        ));
-    }
-
-    if arguments.len() > positional_count {
-        return Err(anyhow!(
-            "The /{} command accepts {} {}, but {} {} provided",
-            command_name,
-            positional_count,
-            if positional_count == 1 {
-                "argument"
-            } else {
-                "arguments"
-            },
-            arguments.len(),
-            if arguments.len() == 1 { "was" } else { "were" }
-        ));
-    }
-
-    Ok(())
-}
-
-/// Expands a template by substituting placeholders with arguments.
-/// - $ARGUMENTS is replaced with all arguments as a single string
-/// - $1, $2, etc. are replaced with positional arguments
-/// - \$ produces literal $, \" produces literal ", \n produces newline
-pub fn expand_template(
-    template: &str,
-    arguments: &[Cow<'_, str>],
-    raw_arguments: &str,
-) -> Result<String> {
-    let mut result = String::with_capacity(template.len());
-    let mut chars = template.char_indices().peekable();
-
-    while let Some((_, c)) = chars.next() {
-        if c == '\\' {
-            if let Some((_, next_c)) = chars.next() {
-                match next_c {
-                    '$' => result.push('$'),
-                    '"' => result.push('"'),
-                    '\\' => result.push('\\'),
-                    'n' => result.push('\n'),
-                    other => {
-                        return Err(anyhow!("Unknown escape sequence: \\{}", other));
-                    }
-                }
-            }
-        } else if c == '$' {
-            // Check for $ARGUMENTS first
-            let remaining: String = chars.clone().map(|(_, c)| c).collect();
-            if remaining.starts_with("ARGUMENTS") {
-                result.push_str(raw_arguments);
-                // Skip "ARGUMENTS"
-                for _ in 0..9 {
-                    chars.next();
-                }
-            } else {
-                // Check for positional placeholder $N
-                let mut num_str = String::new();
-                while let Some(&(_, next_c)) = chars.peek() {
-                    if next_c.is_ascii_digit() {
-                        num_str.push(next_c);
-                        chars.next();
-                    } else {
-                        break;
-                    }
-                }
-                if !num_str.is_empty() {
-                    let n: usize = num_str.parse()?;
-                    if n == 0 {
-                        return Err(anyhow!(
-                            "Placeholder $0 is invalid; placeholders start at $1"
-                        ));
-                    }
-                    if let Some(arg) = arguments.get(n - 1) {
-                        result.push_str(arg);
-                    } else {
-                        return Err(anyhow!("Missing argument for placeholder ${}", n));
-                    }
-                } else {
-                    result.push('$');
-                }
-            }
-        } else {
-            result.push(c);
-        }
-    }
-
-    Ok(result)
-}
-
-/// Expands a user slash command, validating arguments and performing substitution.
-pub fn expand_user_slash_command(
-    command_name: &str,
-    template: &str,
-    arguments: &[Cow<'_, str>],
-    raw_arguments: &str,
-) -> Result<String> {
-    validate_arguments(command_name, template, arguments)?;
-    expand_template(template, arguments, raw_arguments)
-}
-
-/// Attempts to expand a user slash command from input text.
-/// Returns Ok(None) if the input is not a user command or the command doesn't exist.
-/// Returns Err if the command exists but expansion fails (e.g., missing arguments).
-pub fn try_expand_from_commands(
-    line: &str,
-    commands: &HashMap<String, UserSlashCommand>,
-) -> Result<Option<String>> {
-    let Some(parsed) = try_parse_user_command(line) else {
-        return Ok(None);
-    };
-
-    let Some(command) = commands.get(parsed.name) else {
-        return Ok(None);
-    };
-
-    let arguments = parse_arguments(parsed.raw_arguments)?;
-    let expanded = expand_user_slash_command(
-        parsed.name,
-        &command.template,
-        &arguments,
-        parsed.raw_arguments,
-    )?;
-    Ok(Some(expanded))
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-    use fs::{FakeFs, Fs, RemoveOptions};
-    use gpui::{AppContext as _, TestAppContext};
-    use serde_json::json;
-    use std::sync::Arc;
-    use text::Rope;
-    use util::path;
-
-    // ==================== Parsing Tests ====================
-
-    #[test]
-    fn test_try_parse_user_command() {
-        assert_eq!(
-            try_parse_user_command("/review"),
-            Some(ParsedUserCommand {
-                name: "review",
-                raw_arguments: ""
-            })
-        );
-
-        assert_eq!(
-            try_parse_user_command("/review arg1 arg2"),
-            Some(ParsedUserCommand {
-                name: "review",
-                raw_arguments: "arg1 arg2"
-            })
-        );
-
-        assert_eq!(
-            try_parse_user_command("/cmd \"multi word\" simple"),
-            Some(ParsedUserCommand {
-                name: "cmd",
-                raw_arguments: "\"multi word\" simple"
-            })
-        );
-
-        assert_eq!(try_parse_user_command("not a command"), None);
-        assert_eq!(try_parse_user_command(""), None);
-        assert_eq!(try_parse_user_command("/"), None);
-    }
-
-    #[test]
-    fn test_parse_arguments_simple_unquoted() {
-        let args = parse_arguments("foo bar").unwrap();
-        assert_eq!(args, vec!["foo", "bar"]);
-    }
-
-    #[test]
-    fn test_parse_arguments_quoted() {
-        let args = parse_arguments("\"foo bar\"").unwrap();
-        assert_eq!(args, vec!["foo bar"]);
-    }
-
-    #[test]
-    fn test_parse_arguments_mixed() {
-        let args = parse_arguments("\"foo bar\" baz \"qux\"").unwrap();
-        assert_eq!(args, vec!["foo bar", "baz", "qux"]);
-    }
-
-    #[test]
-    fn test_parse_arguments_escaped_quotes() {
-        let args = parse_arguments("\"foo \\\"bar\\\" baz\"").unwrap();
-        assert_eq!(args, vec!["foo \"bar\" baz"]);
-    }
-
-    #[test]
-    fn test_parse_arguments_escaped_backslash() {
-        let args = parse_arguments("\"foo\\\\bar\"").unwrap();
-        assert_eq!(args, vec!["foo\\bar"]);
-    }
-
-    #[test]
-    fn test_parse_arguments_unclosed_quote_error() {
-        let result = parse_arguments("\"foo");
-        assert!(result.is_err());
-        assert!(result.unwrap_err().to_string().contains("Unclosed quote"));
-    }
-
-    #[test]
-    fn test_parse_arguments_quote_in_middle_error() {
-        let result = parse_arguments("foo\"bar");
-        assert!(result.is_err());
-        assert!(result.unwrap_err().to_string().contains("Quote in middle"));
-    }
-
-    #[test]
-    fn test_parse_arguments_unknown_escape_error() {
-        let result = parse_arguments("\"\\x\"");
-        assert!(result.is_err());
-        assert!(result.unwrap_err().to_string().contains("Unknown escape"));
-    }
-
-    #[test]
-    fn test_parse_arguments_newline_escape() {
-        let args = parse_arguments("\"line1\\nline2\"").unwrap();
-        assert_eq!(args, vec!["line1\nline2"]);
-    }
-
-    // ==================== Placeholder Tests ====================
-
-    #[test]
-    fn test_count_positional_placeholders() {
-        assert_eq!(count_positional_placeholders("Hello $1"), 1);
-        assert_eq!(count_positional_placeholders("$1 and $2"), 2);
-        assert_eq!(count_positional_placeholders("$1 $1"), 1);
-        assert_eq!(count_positional_placeholders("$2 then $1"), 2);
-        assert_eq!(count_positional_placeholders("no placeholders"), 0);
-        assert_eq!(count_positional_placeholders("\\$1 escaped"), 0);
-        assert_eq!(count_positional_placeholders("$10 big number"), 10);
-    }
-
-    #[test]
-    fn test_has_placeholders() {
-        assert!(has_placeholders("Hello $1"));
-        assert!(has_placeholders("$ARGUMENTS"));
-        assert!(has_placeholders("prefix $ARGUMENTS suffix"));
-        assert!(!has_placeholders("no placeholders"));
-        assert!(!has_placeholders("\\$1 escaped"));
-    }
-
-    // ==================== Template Expansion Tests ====================
-
-    #[test]
-    fn test_expand_template_basic() {
-        let args = vec![Cow::Borrowed("world")];
-        let result = expand_template("Hello $1", &args, "world").unwrap();
-        assert_eq!(result, "Hello world");
-    }
-
-    #[test]
-    fn test_expand_template_multiple_placeholders() {
-        let args = vec![Cow::Borrowed("a"), Cow::Borrowed("b")];
-        let result = expand_template("$1 and $2", &args, "a b").unwrap();
-        assert_eq!(result, "a and b");
-    }
-
-    #[test]
-    fn test_expand_template_repeated_placeholder() {
-        let args = vec![Cow::Borrowed("x")];
-        let result = expand_template("$1 $1", &args, "x").unwrap();
-        assert_eq!(result, "x x");
-    }
-
-    #[test]
-    fn test_expand_template_out_of_order() {
-        let args = vec![Cow::Borrowed("a"), Cow::Borrowed("b")];
-        let result = expand_template("$2 then $1", &args, "a b").unwrap();
-        assert_eq!(result, "b then a");
-    }
-
-    #[test]
-    fn test_expand_template_escape_sequences() {
-        let args: Vec<Cow<'_, str>> = vec![];
-        assert_eq!(
-            expand_template("line1\\nline2", &args, "").unwrap(),
-            "line1\nline2"
-        );
-        assert_eq!(
-            expand_template("cost is \\$1", &args, "").unwrap(),
-            "cost is $1"
-        );
-        assert_eq!(
-            expand_template("say \\\"hi\\\"", &args, "").unwrap(),
-            "say \"hi\""
-        );
-        assert_eq!(
-            expand_template("path\\\\file", &args, "").unwrap(),
-            "path\\file"
-        );
-    }
-
-    #[test]
-    fn test_expand_template_arguments_placeholder() {
-        let args = vec![Cow::Borrowed("foo"), Cow::Borrowed("bar")];
-        let result = expand_template("All args: $ARGUMENTS", &args, "foo bar").unwrap();
-        assert_eq!(result, "All args: foo bar");
-    }
-
-    #[test]
-    fn test_expand_template_arguments_with_positional() {
-        let args = vec![Cow::Borrowed("first"), Cow::Borrowed("second")];
-        let result = expand_template("First: $1, All: $ARGUMENTS", &args, "first second").unwrap();
-        assert_eq!(result, "First: first, All: first second");
-    }
-
-    #[test]
-    fn test_expand_template_arguments_empty() {
-        let args: Vec<Cow<'_, str>> = vec![];
-        let result = expand_template("Args: $ARGUMENTS", &args, "").unwrap();
-        assert_eq!(result, "Args: ");
-    }
-
-    // ==================== Validation Tests ====================
-
-    #[test]
-    fn test_validate_arguments_exact_match() {
-        let args = vec![Cow::Borrowed("a"), Cow::Borrowed("b")];
-        let result = validate_arguments("test", "$1 $2", &args);
-        assert!(result.is_ok());
-    }
-
-    #[test]
-    fn test_validate_arguments_missing_args() {
-        let args = vec![Cow::Borrowed("a")];
-        let result = validate_arguments("foo", "$1 $2", &args);
-        assert!(result.is_err());
-        let err = result.unwrap_err().to_string();
-        assert!(err.contains("/foo"));
-        assert!(err.contains("requires 2 arguments"));
-    }
-
-    #[test]
-    fn test_validate_arguments_extra_args() {
-        let args = vec![Cow::Borrowed("a"), Cow::Borrowed("b")];
-        let result = validate_arguments("foo", "$1", &args);
-        assert!(result.is_err());
-        let err = result.unwrap_err().to_string();
-        assert!(err.contains("accepts 1 argument"));
-    }
-
-    #[test]
-    fn test_validate_arguments_no_placeholders() {
-        // No args expected, none provided - OK
-        let args: Vec<Cow<'_, str>> = vec![];
-        assert!(validate_arguments("test", "no placeholders", &args).is_ok());
-
-        // No args expected but some provided - Error
-        let args = vec![Cow::Borrowed("unexpected")];
-        let result = validate_arguments("test", "no placeholders", &args);
-        assert!(result.is_err());
-        assert!(
-            result
-                .unwrap_err()
-                .to_string()
-                .contains("accepts no arguments")
-        );
-    }
-
-    #[test]
-    fn test_validate_arguments_empty_template() {
-        let args: Vec<Cow<'_, str>> = vec![];
-        let result = validate_arguments("test", "", &args);
-        assert!(result.is_err());
-        assert!(result.unwrap_err().to_string().contains("cannot be empty"));
-    }
-
-    #[test]
-    fn test_validate_arguments_with_arguments_placeholder() {
-        // $ARGUMENTS accepts any number of arguments including zero
-        let args: Vec<Cow<'_, str>> = vec![];
-        assert!(validate_arguments("test", "Do: $ARGUMENTS", &args).is_ok());
-
-        let args = vec![Cow::Borrowed("a"), Cow::Borrowed("b"), Cow::Borrowed("c")];
-        assert!(validate_arguments("test", "Do: $ARGUMENTS", &args).is_ok());
-    }
-
-    #[test]
-    fn test_validate_arguments_mixed_placeholders() {
-        // Both $ARGUMENTS and positional - need at least the positional ones
-        let args = vec![Cow::Borrowed("first")];
-        assert!(validate_arguments("test", "$1 then $ARGUMENTS", &args).is_ok());
-
-        let args: Vec<Cow<'_, str>> = vec![];
-        assert!(validate_arguments("test", "$1 then $ARGUMENTS", &args).is_err());
-    }
-
-    // ==================== Integration Tests ====================
-
-    #[test]
-    fn test_expand_user_slash_command() {
-        let result = expand_user_slash_command(
-            "review",
-            "Please review: $1",
-            &[Cow::Borrowed("security")],
-            "security",
-        )
-        .unwrap();
-        assert_eq!(result, "Please review: security");
-    }
-
-    #[test]
-    fn test_try_expand_from_commands() {
-        let commands = vec![
-            UserSlashCommand {
-                name: "greet".into(),
-                template: "Hello, world!".into(),
-                namespace: None,
-                path: PathBuf::from("/greet.md"),
-                scope: CommandScope::User,
-            },
-            UserSlashCommand {
-                name: "review".into(),
-                template: "Review this for: $1".into(),
-                namespace: None,
-                path: PathBuf::from("/review.md"),
-                scope: CommandScope::User,
-            },
-            UserSlashCommand {
-                name: "search".into(),
-                template: "Search: $ARGUMENTS".into(),
-                namespace: None,
-                path: PathBuf::from("/search.md"),
-                scope: CommandScope::User,
-            },
-        ];
-        let map = commands_to_map(&commands);
-
-        // Command without arguments
-        assert_eq!(
-            try_expand_from_commands("/greet", &map).unwrap(),
-            Some("Hello, world!".to_string())
-        );
-
-        // Command with positional argument
-        assert_eq!(
-            try_expand_from_commands("/review security", &map).unwrap(),
-            Some("Review this for: security".to_string())
-        );
-
-        // Command with $ARGUMENTS
-        assert_eq!(
-            try_expand_from_commands("/search foo bar baz", &map).unwrap(),
-            Some("Search: foo bar baz".to_string())
-        );
-
-        // Unknown command returns None
-        assert_eq!(try_expand_from_commands("/unknown", &map).unwrap(), None);
-
-        // Not a command returns None
-        assert_eq!(try_expand_from_commands("just text", &map).unwrap(), None);
-    }
-
-    #[test]
-    fn test_try_expand_from_commands_missing_args() {
-        let commands = vec![UserSlashCommand {
-            name: "review".into(),
-            template: "Review: $1".into(),
-            namespace: None,
-            path: PathBuf::from("/review.md"),
-            scope: CommandScope::User,
-        }];
-        let map = commands_to_map(&commands);
-
-        let result = try_expand_from_commands("/review", &map);
-        assert!(result.is_err());
-        assert!(
-            result
-                .unwrap_err()
-                .to_string()
-                .contains("requires 1 argument")
-        );
-    }
-
-    // ==================== Edge Case Tests ====================
-
-    #[test]
-    fn test_unicode_command_names() {
-        // Test that unicode in command names works
-        let result = try_parse_user_command("/ζ—₯本θͺž arg1");
-        assert!(result.is_some());
-        let parsed = result.unwrap();
-        assert_eq!(parsed.name, "ζ—₯本θͺž");
-        assert_eq!(parsed.raw_arguments, "arg1");
-    }
-
-    #[test]
-    fn test_unicode_in_arguments() {
-        let args = parse_arguments("\"こんにけは\" δΈ–η•Œ").unwrap();
-        assert_eq!(args, vec!["こんにけは", "δΈ–η•Œ"]);
-    }
-
-    #[test]
-    fn test_unicode_in_template() {
-        let args = vec![Cow::Borrowed("名前")];
-        let result = expand_template("こんにけは、$1さん!", &args, "名前").unwrap();
-        assert_eq!(result, "こんにけは、名前さん!");
-    }
-
-    #[test]
-    fn test_command_name_with_emoji() {
-        // Emoji can be multi-codepoint, test they're handled correctly
-        let result = try_parse_user_command("/πŸš€deploy fast");
-        assert!(result.is_some());
-        let parsed = result.unwrap();
-        assert_eq!(parsed.name, "πŸš€deploy");
-        assert_eq!(parsed.raw_arguments, "fast");
-
-        // Emoji in arguments
-        let args = parse_arguments("πŸŽ‰ \"🎊 party\"").unwrap();
-        assert_eq!(args, vec!["πŸŽ‰", "🎊 party"]);
-    }
-
-    #[test]
-    fn test_many_placeholders() {
-        // Test template with many placeholders
-        let template = "$1 $2 $3 $4 $5 $6 $7 $8 $9 $10";
-        assert_eq!(count_positional_placeholders(template), 10);
-
-        let args: Vec<Cow<'_, str>> = (1..=10).map(|i| Cow::Owned(i.to_string())).collect();
-        let result = expand_template(template, &args, "1 2 3 4 5 6 7 8 9 10").unwrap();
-        assert_eq!(result, "1 2 3 4 5 6 7 8 9 10");
-    }
-
-    #[test]
-    fn test_placeholder_zero_is_invalid() {
-        let args = vec![Cow::Borrowed("a")];
-        let result = expand_template("$0", &args, "a");
-        assert!(result.is_err());
-        assert!(result.unwrap_err().to_string().contains("$0 is invalid"));
-    }
-
-    #[test]
-    fn test_dollar_sign_without_number() {
-        // Bare $ should be preserved
-        let args: Vec<Cow<'_, str>> = vec![];
-        let result = expand_template("cost is $", &args, "").unwrap();
-        assert_eq!(result, "cost is $");
-    }
-
-    #[test]
-    fn test_consecutive_whitespace_in_arguments() {
-        let args = parse_arguments("  a    b   c  ").unwrap();
-        assert_eq!(args, vec!["a", "b", "c"]);
-    }
-
-    #[test]
-    fn test_empty_input() {
-        let args = parse_arguments("").unwrap();
-        assert!(args.is_empty());
-
-        let args = parse_arguments("   ").unwrap();
-        assert!(args.is_empty());
-    }
-
-    #[test]
-    fn test_command_load_error_command_name() {
-        let error = CommandLoadError {
-            path: PathBuf::from(path!("/commands/tools/git/commit.md")),
-            base_path: PathBuf::from(path!("/commands")),
-            message: "Failed".into(),
-        };
-        assert_eq!(error.command_name().as_deref(), Some("tools:git:commit"));
-
-        let non_md_error = CommandLoadError {
-            path: PathBuf::from(path!("/commands/readme.txt")),
-            base_path: PathBuf::from(path!("/commands")),
-            message: "Failed".into(),
-        };
-        assert_eq!(non_md_error.command_name(), None);
-    }
-
-    #[test]
-    fn test_apply_server_command_conflicts() {
-        let mut commands = vec![
-            UserSlashCommand {
-                name: "help".into(),
-                template: "Help text".into(),
-                namespace: None,
-                path: PathBuf::from(path!("/commands/help.md")),
-                scope: CommandScope::User,
-            },
-            UserSlashCommand {
-                name: "review".into(),
-                template: "Review $1".into(),
-                namespace: None,
-                path: PathBuf::from(path!("/commands/review.md")),
-                scope: CommandScope::User,
-            },
-        ];
-        let mut errors = Vec::new();
-        let server_command_names = HashSet::from_iter(["help".to_string()]);
-
-        apply_server_command_conflicts(&mut commands, &mut errors, &server_command_names);
-
-        assert_eq!(commands.len(), 1);
-        assert_eq!(commands[0].name.as_ref(), "review");
-        assert_eq!(errors.len(), 1);
-        assert_eq!(errors[0].command_name().as_deref(), Some("help"));
-        assert!(errors[0].message.contains("conflicts"));
-    }
-
-    #[test]
-    fn test_apply_server_command_conflicts_to_map() {
-        let command = UserSlashCommand {
-            name: "tools:git:commit".into(),
-            template: "Commit".into(),
-            namespace: Some("tools/git".into()),
-            path: PathBuf::from(path!("/commands/tools/git/commit.md")),
-            scope: CommandScope::User,
-        };
-        let mut commands = HashMap::default();
-        commands.insert(command.name.to_string(), command.clone());
-        let mut errors = Vec::new();
-        let server_command_names = HashSet::from_iter([command.name.to_string()]);
-
-        apply_server_command_conflicts_to_map(&mut commands, &mut errors, &server_command_names);
-
-        assert!(commands.is_empty());
-        assert_eq!(errors.len(), 1);
-        assert_eq!(
-            errors[0].command_name().as_deref(),
-            Some("tools:git:commit")
-        );
-    }
-
-    // ==================== Async File Loading Tests with FakeFs ====================
-
-    #[gpui::test]
-    async fn test_load_commands_from_empty_dir(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(path!("/commands"), json!({})).await;
-        let fs: Arc<dyn Fs> = fs;
-
-        let result =
-            load_commands_from_path_async(&fs, Path::new(path!("/commands")), CommandScope::User)
-                .await;
-
-        assert!(result.commands.is_empty());
-        assert!(result.errors.is_empty());
-    }
-
-    #[gpui::test]
-    async fn test_load_commands_from_nonexistent_dir(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(path!("/"), json!({})).await;
-        let fs: Arc<dyn Fs> = fs;
-
-        let result = load_commands_from_path_async(
-            &fs,
-            Path::new(path!("/nonexistent")),
-            CommandScope::User,
-        )
-        .await;
-
-        assert!(result.commands.is_empty());
-        assert!(result.errors.is_empty());
-    }
-
-    #[gpui::test]
-    async fn test_load_single_command(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/commands"),
-            json!({
-                "review.md": "Please review: $1"
-            }),
-        )
-        .await;
-        let fs: Arc<dyn Fs> = fs;
-
-        let result =
-            load_commands_from_path_async(&fs, Path::new(path!("/commands")), CommandScope::User)
-                .await;
-
-        assert!(result.errors.is_empty());
-        assert_eq!(result.commands.len(), 1);
-        let cmd = &result.commands[0];
-        assert_eq!(cmd.name.as_ref(), "review");
-        assert_eq!(cmd.template.as_ref(), "Please review: $1");
-        assert!(cmd.namespace.is_none());
-        assert_eq!(cmd.scope, CommandScope::User);
-    }
-
-    #[gpui::test]
-    async fn test_load_commands_with_namespace(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/commands"),
-            json!({
-                "frontend": {
-                    "component.md": "Create component: $1"
-                }
-            }),
-        )
-        .await;
-        let fs: Arc<dyn Fs> = fs;
-
-        let result =
-            load_commands_from_path_async(&fs, Path::new(path!("/commands")), CommandScope::User)
-                .await;
-
-        assert!(result.errors.is_empty());
-        assert_eq!(result.commands.len(), 1);
-        let cmd = &result.commands[0];
-        assert_eq!(cmd.name.as_ref(), "frontend:component");
-        assert_eq!(cmd.namespace.as_ref().map(|s| s.as_ref()), Some("frontend"));
-    }
-
-    #[gpui::test]
-    async fn test_load_commands_nested_namespace(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/commands"),
-            json!({
-                "tools": {
-                    "git": {
-                        "commit.md": "Git commit: $ARGUMENTS"
-                    }
-                }
-            }),
-        )
-        .await;
-        let fs: Arc<dyn Fs> = fs;
-
-        let result =
-            load_commands_from_path_async(&fs, Path::new(path!("/commands")), CommandScope::User)
-                .await;
-
-        assert!(result.errors.is_empty());
-        assert_eq!(result.commands.len(), 1);
-        let cmd = &result.commands[0];
-        assert_eq!(cmd.name.as_ref(), "tools:git:commit");
-        assert_eq!(
-            cmd.namespace.as_ref().map(|s| s.as_ref()),
-            Some("tools/git")
-        );
-    }
-
-    #[gpui::test]
-    async fn test_deeply_nested_namespace(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/commands"),
-            json!({
-                "a": {
-                    "b": {
-                        "c": {
-                            "d": {
-                                "e": {
-                                    "deep.md": "Very deep command"
-                                }
-                            }
-                        }
-                    }
-                }
-            }),
-        )
-        .await;
-        let fs: Arc<dyn Fs> = fs;
-
-        let result =
-            load_commands_from_path_async(&fs, Path::new(path!("/commands")), CommandScope::User)
-                .await;
-
-        assert!(result.errors.is_empty());
-        assert_eq!(result.commands.len(), 1);
-        let cmd = &result.commands[0];
-        assert_eq!(cmd.name.as_ref(), "a:b:c:d:e:deep");
-        assert_eq!(
-            cmd.namespace.as_ref().map(|s| s.as_ref()),
-            Some("a/b/c/d/e")
-        );
-    }
-
-    #[gpui::test]
-    async fn test_load_commands_empty_file_ignored(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/commands"),
-            json!({
-                "empty.md": "",
-                "valid.md": "Hello!"
-            }),
-        )
-        .await;
-        let fs: Arc<dyn Fs> = fs;
-
-        let result =
-            load_commands_from_path_async(&fs, Path::new(path!("/commands")), CommandScope::User)
-                .await;
-
-        assert!(result.errors.is_empty());
-        assert_eq!(result.commands.len(), 1);
-        assert_eq!(result.commands[0].name.as_ref(), "valid");
-    }
-
-    #[gpui::test]
-    async fn test_load_commands_non_md_files_ignored(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/commands"),
-            json!({
-                "command.md": "Valid command",
-                "readme.txt": "Not a command",
-                "script.sh": "Also not a command"
-            }),
-        )
-        .await;
-        let fs: Arc<dyn Fs> = fs;
-
-        let result =
-            load_commands_from_path_async(&fs, Path::new(path!("/commands")), CommandScope::User)
-                .await;
-
-        assert!(result.errors.is_empty());
-        assert_eq!(result.commands.len(), 1);
-        assert_eq!(result.commands[0].name.as_ref(), "command");
-    }
-
-    #[gpui::test]
-    async fn test_load_project_commands(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({
-                ".zed": {
-                    "commands": {
-                        "build.md": "Build the project"
-                    }
-                }
-            }),
-        )
-        .await;
-        let fs: Arc<dyn Fs> = fs;
-
-        let commands_path = project_commands_dir(Path::new(path!("/project")));
-        let result =
-            load_commands_from_path_async(&fs, &commands_path, CommandScope::Project).await;
-
-        assert!(result.errors.is_empty());
-        assert_eq!(result.commands.len(), 1);
-        assert_eq!(result.commands[0].name.as_ref(), "build");
-        assert_eq!(result.commands[0].scope, CommandScope::Project);
-    }
-
-    #[gpui::test]
-    async fn test_load_all_commands_no_duplicates(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project1"),
-            json!({
-                ".zed": {
-                    "commands": {
-                        "review.md": "Project 1 review"
-                    }
-                }
-            }),
-        )
-        .await;
-        fs.insert_tree(
-            path!("/project2"),
-            json!({
-                ".zed": {
-                    "commands": {
-                        "build.md": "Project 2 build"
-                    }
-                }
-            }),
-        )
-        .await;
-        let fs: Arc<dyn Fs> = fs;
-
-        let result = load_all_commands_async(
-            &fs,
-            &[
-                PathBuf::from(path!("/project1")),
-                PathBuf::from(path!("/project2")),
-            ],
-        )
-        .await;
-
-        assert!(result.errors.is_empty());
-        assert_eq!(result.commands.len(), 2);
-        let names: Vec<&str> = result.commands.iter().map(|c| c.name.as_ref()).collect();
-        assert!(names.contains(&"review"));
-        assert!(names.contains(&"build"));
-    }
-
-    #[gpui::test]
-    async fn test_load_all_commands_duplicate_error(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project1"),
-            json!({
-                ".zed": {
-                    "commands": {
-                        "deploy.md": "Deploy from project 1"
-                    }
-                }
-            }),
-        )
-        .await;
-        fs.insert_tree(
-            path!("/project2"),
-            json!({
-                ".zed": {
-                    "commands": {
-                        "deploy.md": "Deploy from project 2"
-                    }
-                }
-            }),
-        )
-        .await;
-        let fs: Arc<dyn Fs> = fs;
-
-        let result = load_all_commands_async(
-            &fs,
-            &[
-                PathBuf::from(path!("/project1")),
-                PathBuf::from(path!("/project2")),
-            ],
-        )
-        .await;
-
-        // Should have one command and one error
-        assert_eq!(result.commands.len(), 1);
-        assert_eq!(result.errors.len(), 1);
-        assert!(result.errors[0].message.contains("ambiguous"));
-        assert!(result.errors[0].message.contains("deploy"));
-    }
-
-    #[gpui::test]
-    async fn test_registry_loads_commands(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({
-                ".zed": {
-                    "commands": {
-                        "test.md": "Test command"
-                    }
-                }
-            }),
-        )
-        .await;
-        let fs: Arc<dyn Fs> = fs;
-
-        let registry = cx.new(|cx| {
-            SlashCommandRegistry::new(fs.clone(), vec![PathBuf::from(path!("/project"))], cx)
-        });
-
-        // Wait for async load
-        cx.run_until_parked();
-
-        registry.read_with(cx, |registry: &SlashCommandRegistry, _cx| {
-            assert!(registry.errors().is_empty());
-            assert!(registry.commands().contains_key("test"));
-        });
-    }
-
-    #[gpui::test]
-    async fn test_registry_updates_worktree_roots(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project1"),
-            json!({
-                ".zed": {
-                    "commands": {
-                        "cmd1.md": "Command 1"
-                    }
-                }
-            }),
-        )
-        .await;
-        fs.insert_tree(
-            path!("/project2"),
-            json!({
-                ".zed": {
-                    "commands": {
-                        "cmd2.md": "Command 2"
-                    }
-                }
-            }),
-        )
-        .await;
-        let fs: Arc<dyn Fs> = fs;
-
-        let registry = cx.new(|cx| {
-            SlashCommandRegistry::new(fs.clone(), vec![PathBuf::from(path!("/project1"))], cx)
-        });
-
-        cx.run_until_parked();
-
-        registry.read_with(cx, |registry: &SlashCommandRegistry, _cx| {
-            assert!(registry.commands().contains_key("cmd1"));
-            assert!(!registry.commands().contains_key("cmd2"));
-        });
-
-        // Update worktree roots
-        registry.update(cx, |registry: &mut SlashCommandRegistry, cx| {
-            registry.set_worktree_roots(
-                vec![
-                    PathBuf::from(path!("/project1")),
-                    PathBuf::from(path!("/project2")),
-                ],
-                cx,
-            );
-        });
-
-        cx.run_until_parked();
-
-        registry.read_with(cx, |registry: &SlashCommandRegistry, _cx| {
-            assert!(registry.commands().contains_key("cmd1"));
-            assert!(registry.commands().contains_key("cmd2"));
-        });
-    }
-
-    #[gpui::test]
-    async fn test_registry_reloads_on_file_change(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({
-                ".zed": {
-                    "commands": {
-                        "original.md": "Original command"
-                    }
-                }
-            }),
-        )
-        .await;
-        let fs: Arc<dyn Fs> = fs.clone();
-
-        let registry = cx.new(|cx| {
-            SlashCommandRegistry::new(fs.clone(), vec![PathBuf::from(path!("/project"))], cx)
-        });
-
-        // Wait for initial load
-        cx.run_until_parked();
-
-        registry.read_with(cx, |registry, _cx| {
-            assert_eq!(registry.commands().len(), 1);
-            assert!(registry.commands().contains_key("original"));
-        });
-
-        // Add a new command file
-        fs.save(
-            Path::new(path!("/project/.zed/commands/new.md")),
-            &Rope::from("New command"),
-            text::LineEnding::Unix,
-        )
-        .await
-        .unwrap();
-
-        // Wait for watcher to process the change
-        cx.run_until_parked();
-
-        registry.read_with(cx, |registry, _cx| {
-            assert_eq!(registry.commands().len(), 2);
-            assert!(registry.commands().contains_key("original"));
-            assert!(registry.commands().contains_key("new"));
-        });
-
-        // Remove a command file
-        fs.remove_file(
-            Path::new(path!("/project/.zed/commands/original.md")),
-            RemoveOptions::default(),
-        )
-        .await
-        .unwrap();
-
-        // Wait for watcher to process the change
-        cx.run_until_parked();
-
-        registry.read_with(cx, |registry, _cx| {
-            assert_eq!(registry.commands().len(), 1);
-            assert!(!registry.commands().contains_key("original"));
-            assert!(registry.commands().contains_key("new"));
-        });
-
-        // Modify an existing command
-        fs.save(
-            Path::new(path!("/project/.zed/commands/new.md")),
-            &Rope::from("Updated content"),
-            text::LineEnding::Unix,
-        )
-        .await
-        .unwrap();
-
-        cx.run_until_parked();
-
-        registry.read_with(cx, |registry, _cx| {
-            let cmd = registry.commands().get("new").unwrap();
-            assert_eq!(cmd.template.as_ref(), "Updated content");
-        });
-    }
-
-    #[gpui::test]
-    async fn test_concurrent_command_loading(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-        fs.insert_tree(
-            path!("/project"),
-            json!({
-                ".zed": {
-                    "commands": {
-                        "cmd1.md": "Command 1",
-                        "cmd2.md": "Command 2",
-                        "cmd3.md": "Command 3"
-                    }
-                }
-            }),
-        )
-        .await;
-        let fs: Arc<dyn Fs> = fs;
-        let worktree_roots = vec![PathBuf::from(path!("/project"))];
-
-        // Spawn multiple load tasks concurrently
-        let fs1 = fs.clone();
-        let roots1 = worktree_roots.clone();
-        let task1 = cx
-            .executor()
-            .spawn(async move { load_all_commands_async(&fs1, &roots1).await });
-
-        let fs2 = fs.clone();
-        let roots2 = worktree_roots.clone();
-        let task2 = cx
-            .executor()
-            .spawn(async move { load_all_commands_async(&fs2, &roots2).await });
-
-        let fs3 = fs.clone();
-        let roots3 = worktree_roots.clone();
-        let task3 = cx
-            .executor()
-            .spawn(async move { load_all_commands_async(&fs3, &roots3).await });
-
-        // Wait for all tasks to complete
-        let (result1, result2, result3) = futures::join!(task1, task2, task3);
-
-        // All should succeed with the same results
-        assert!(result1.errors.is_empty());
-        assert!(result2.errors.is_empty());
-        assert!(result3.errors.is_empty());
-
-        assert_eq!(result1.commands.len(), 3);
-        assert_eq!(result2.commands.len(), 3);
-        assert_eq!(result3.commands.len(), 3);
-    }
-
-    // ==================== Symlink Handling Tests ====================
-
-    #[gpui::test]
-    async fn test_load_commands_from_symlinked_directory(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-
-        // Create the actual commands directory with a command
-        fs.insert_tree(
-            path!("/actual_commands"),
-            json!({
-                "review.md": "Please review: $1"
-            }),
-        )
-        .await;
-
-        // Create a symlink from /commands to /actual_commands
-        fs.insert_tree(path!("/"), json!({})).await;
-        fs.create_symlink(
-            Path::new(path!("/commands")),
-            PathBuf::from(path!("/actual_commands")),
-        )
-        .await
-        .unwrap();
-
-        let fs: Arc<dyn Fs> = fs;
-
-        let result =
-            load_commands_from_path_async(&fs, Path::new(path!("/commands")), CommandScope::User)
-                .await;
-
-        assert!(result.errors.is_empty());
-        assert_eq!(result.commands.len(), 1);
-        assert_eq!(result.commands[0].name.as_ref(), "review");
-    }
-
-    #[gpui::test]
-    async fn test_load_commands_from_symlinked_file(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-
-        // Create the actual command file
-        fs.insert_tree(
-            path!("/actual"),
-            json!({
-                "real_review.md": "Review command content: $1"
-            }),
-        )
-        .await;
-
-        // Create commands directory with a symlink to the file
-        fs.insert_tree(path!("/commands"), json!({})).await;
-        fs.create_symlink(
-            Path::new(path!("/commands/review.md")),
-            PathBuf::from(path!("/actual/real_review.md")),
-        )
-        .await
-        .unwrap();
-
-        let fs: Arc<dyn Fs> = fs;
-
-        let result =
-            load_commands_from_path_async(&fs, Path::new(path!("/commands")), CommandScope::User)
-                .await;
-
-        assert!(result.errors.is_empty());
-        assert_eq!(result.commands.len(), 1);
-        assert_eq!(result.commands[0].name.as_ref(), "review");
-        assert_eq!(
-            result.commands[0].template.as_ref(),
-            "Review command content: $1"
-        );
-    }
-
-    #[gpui::test]
-    async fn test_load_commands_claude_symlink_pattern(cx: &mut TestAppContext) {
-        // Simulates the common pattern of symlinking ~/.claude/commands/ to zed's commands dir
-        let fs = FakeFs::new(cx.executor());
-
-        // Create Claude's commands directory structure
-        fs.insert_tree(
-            path!("/home/user/.claude/commands"),
-            json!({
-                "explain.md": "Explain this code: $ARGUMENTS",
-                "refactor": {
-                    "extract.md": "Extract method: $1"
-                }
-            }),
-        )
-        .await;
-
-        // Create Zed config dir with symlink to Claude's commands
-        fs.insert_tree(path!("/home/user/.config/zed"), json!({}))
-            .await;
-        fs.create_symlink(
-            Path::new(path!("/home/user/.config/zed/commands")),
-            PathBuf::from(path!("/home/user/.claude/commands")),
-        )
-        .await
-        .unwrap();
-
-        let fs: Arc<dyn Fs> = fs;
-
-        let result = load_commands_from_path_async(
-            &fs,
-            Path::new(path!("/home/user/.config/zed/commands")),
-            CommandScope::User,
-        )
-        .await;
-
-        assert!(result.errors.is_empty());
-        assert_eq!(result.commands.len(), 2);
-
-        let names: Vec<&str> = result.commands.iter().map(|c| c.name.as_ref()).collect();
-        assert!(names.contains(&"explain"));
-        assert!(names.contains(&"refactor:extract"));
-    }
-
-    #[gpui::test]
-    async fn test_symlink_to_parent_directory_skipped(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-
-        // Create a directory structure with a symlink pointing outside the commands dir
-        // This tests that symlinks to directories outside the command tree are handled
-        fs.insert_tree(
-            path!("/commands"),
-            json!({
-                "valid.md": "Valid command"
-            }),
-        )
-        .await;
-
-        // Create a separate directory
-        fs.insert_tree(
-            path!("/other"),
-            json!({
-                "external.md": "External command"
-            }),
-        )
-        .await;
-
-        // Create a symlink from /commands/external -> /other
-        fs.create_symlink(
-            Path::new(path!("/commands/external")),
-            PathBuf::from(path!("/other")),
-        )
-        .await
-        .unwrap();
-
-        let fs: Arc<dyn Fs> = fs;
-
-        let result =
-            load_commands_from_path_async(&fs, Path::new(path!("/commands")), CommandScope::User)
-                .await;
-
-        // Should have loaded both the valid command and the external one via symlink
-        assert!(result.commands.iter().any(|c| c.name.as_ref() == "valid"));
-        assert!(
-            result
-                .commands
-                .iter()
-                .any(|c| c.name.as_ref() == "external:external")
-        );
-    }
-
-    // ==================== Permission/Error Handling Tests ====================
-
-    #[gpui::test]
-    async fn test_load_commands_reports_directory_read_errors(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-
-        // Create base directory but no commands subdirectory
-        fs.insert_tree(path!("/"), json!({})).await;
-
-        let fs: Arc<dyn Fs> = fs;
-
-        // Try to load from a path that exists but isn't a directory
-        // First create a file where we expect a directory
-        fs.create_file(Path::new(path!("/commands")), fs::CreateOptions::default())
-            .await
-            .unwrap();
-
-        let result =
-            load_commands_from_path_async(&fs, Path::new(path!("/commands")), CommandScope::User)
-                .await;
-
-        // Should return empty since /commands is a file, not a directory
-        assert!(result.commands.is_empty());
-    }
-
-    #[gpui::test]
-    async fn test_load_all_commands_aggregates_errors(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-
-        // Create two projects with duplicate command names
-        fs.insert_tree(
-            path!("/project1"),
-            json!({
-                ".zed": {
-                    "commands": {
-                        "build.md": "Build 1"
-                    }
-                }
-            }),
-        )
-        .await;
-        fs.insert_tree(
-            path!("/project2"),
-            json!({
-                ".zed": {
-                    "commands": {
-                        "build.md": "Build 2"
-                    }
-                }
-            }),
-        )
-        .await;
-        fs.insert_tree(
-            path!("/project3"),
-            json!({
-                ".zed": {
-                    "commands": {
-                        "build.md": "Build 3"
-                    }
-                }
-            }),
-        )
-        .await;
-
-        let fs: Arc<dyn Fs> = fs;
-
-        let result = load_all_commands_async(
-            &fs,
-            &[
-                PathBuf::from(path!("/project1")),
-                PathBuf::from(path!("/project2")),
-                PathBuf::from(path!("/project3")),
-            ],
-        )
-        .await;
-
-        // Should have 1 command (first one) and 2 errors (for duplicates)
-        assert_eq!(result.commands.len(), 1);
-        assert_eq!(result.errors.len(), 2);
-
-        // All errors should mention "ambiguous"
-        for error in &result.errors {
-            assert!(error.message.contains("ambiguous"));
-        }
-    }
-
-    #[gpui::test]
-    async fn test_mixed_valid_and_empty_files(cx: &mut TestAppContext) {
-        let fs = FakeFs::new(cx.executor());
-
-        fs.insert_tree(
-            path!("/commands"),
-            json!({
-                "valid.md": "Valid command",
-                "empty.md": "",
-                "whitespace_only.md": "   ",
-                "another_valid.md": "Another valid"
-            }),
-        )
-        .await;
-
-        let fs: Arc<dyn Fs> = fs;
-
-        let result =
-            load_commands_from_path_async(&fs, Path::new(path!("/commands")), CommandScope::User)
-                .await;
-
-        // Empty file is ignored, whitespace-only is an error
-        assert_eq!(result.commands.len(), 2);
-        assert_eq!(result.errors.len(), 1);
-        assert!(result.errors[0].message.contains("whitespace"));
-        assert_eq!(
-            result.errors[0].command_name().as_deref(),
-            Some("whitespace_only")
-        );
-    }
-}

crates/agent_ui_v2/src/agent_thread_pane.rs πŸ”—

@@ -143,7 +143,7 @@ impl AgentThreadPane {
         if let Some(active_thread_view) = &self.thread_view {
             let thread_view = active_thread_view.view.read(cx);
             if let Some(ready) = thread_view.as_active_thread() {
-                let title = ready.thread.read(cx).title();
+                let title = ready.read(cx).thread.read(cx).title();
                 if !title.is_empty() {
                     return title;
                 }

crates/feature_flags/src/flags.rs πŸ”—

@@ -24,12 +24,6 @@ impl FeatureFlag for AcpBetaFeatureFlag {
     const NAME: &'static str = "acp-beta";
 }
 
-pub struct UserSlashCommandsFeatureFlag;
-
-impl FeatureFlag for UserSlashCommandsFeatureFlag {
-    const NAME: &'static str = "slash-commands";
-}
-
 pub struct ToolPermissionsFeatureFlag;
 
 impl FeatureFlag for ToolPermissionsFeatureFlag {

crates/zed/src/visual_test_runner.rs πŸ”—

@@ -2030,7 +2030,7 @@ fn run_subagent_visual_tests(
             thread_view
                 .read(cx)
                 .as_active_thread()
-                .map(|active| active.thread.clone())
+                .map(|active| active.read(cx).thread.clone())
         })
         .ok_or_else(|| anyhow::anyhow!("Thread not available"))?;
 
@@ -2440,7 +2440,7 @@ fn run_agent_thread_view_test(
             thread_view
                 .read(cx)
                 .as_active_thread()
-                .map(|active| active.thread.clone())
+                .map(|active| active.read(cx).thread.clone())
         })
         .ok_or_else(|| anyhow::anyhow!("Thread not available"))?;