Allow editing Agent2 messages (#36155)

Conrad Irwin , Antonio Scandurra , and Agus Zubiaga created

Release Notes:

- N/A

---------

Co-authored-by: Antonio Scandurra <me@as-cii.com>
Co-authored-by: Agus Zubiaga <agus@zed.dev>

Change summary

Cargo.lock                                     |   1 
assets/keymaps/default-linux.json              |   2 
assets/keymaps/default-macos.json              |   2 
crates/acp_thread/Cargo.toml                   |   1 
crates/acp_thread/src/acp_thread.rs            |  15 
crates/acp_thread/src/mention.rs               |  94 ++
crates/agent2/src/thread.rs                    |   4 
crates/agent_ui/src/acp.rs                     |   3 
crates/agent_ui/src/acp/completion_provider.rs | 113 +-
crates/agent_ui/src/acp/message_editor.rs      | 469 ++++++++++++
crates/agent_ui/src/acp/message_history.rs     |  88 --
crates/agent_ui/src/acp/thread_view.rs         | 760 ++++++++-----------
crates/agent_ui/src/agent_panel.rs             |  15 
crates/zed_actions/src/lib.rs                  |   4 
14 files changed, 953 insertions(+), 618 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -14,6 +14,7 @@ dependencies = [
  "collections",
  "editor",
  "env_logger 0.11.8",
+ "file_icons",
  "futures 0.3.31",
  "gpui",
  "indoc",

assets/keymaps/default-linux.json 🔗

@@ -331,8 +331,6 @@
     "use_key_equivalents": true,
     "bindings": {
       "enter": "agent::Chat",
-      "up": "agent::PreviousHistoryMessage",
-      "down": "agent::NextHistoryMessage",
       "shift-ctrl-r": "agent::OpenAgentDiff",
       "ctrl-shift-y": "agent::KeepAll",
       "ctrl-shift-n": "agent::RejectAll"

assets/keymaps/default-macos.json 🔗

@@ -383,8 +383,6 @@
     "use_key_equivalents": true,
     "bindings": {
       "enter": "agent::Chat",
-      "up": "agent::PreviousHistoryMessage",
-      "down": "agent::NextHistoryMessage",
       "shift-ctrl-r": "agent::OpenAgentDiff",
       "cmd-shift-y": "agent::KeepAll",
       "cmd-shift-n": "agent::RejectAll"

crates/acp_thread/Cargo.toml 🔗

@@ -23,6 +23,7 @@ anyhow.workspace = true
 buffer_diff.workspace = true
 collections.workspace = true
 editor.workspace = true
+file_icons.workspace = true
 futures.workspace = true
 gpui.workspace = true
 itertools.workspace = true

crates/acp_thread/src/acp_thread.rs 🔗

@@ -32,6 +32,7 @@ use util::ResultExt;
 pub struct UserMessage {
     pub id: Option<UserMessageId>,
     pub content: ContentBlock,
+    pub chunks: Vec<acp::ContentBlock>,
     pub checkpoint: Option<GitStoreCheckpoint>,
 }
 
@@ -804,18 +805,25 @@ impl AcpThread {
         let entries_len = self.entries.len();
 
         if let Some(last_entry) = self.entries.last_mut()
-            && let AgentThreadEntry::UserMessage(UserMessage { id, content, .. }) = last_entry
+            && let AgentThreadEntry::UserMessage(UserMessage {
+                id,
+                content,
+                chunks,
+                ..
+            }) = last_entry
         {
             *id = message_id.or(id.take());
-            content.append(chunk, &language_registry, cx);
+            content.append(chunk.clone(), &language_registry, cx);
+            chunks.push(chunk);
             let idx = entries_len - 1;
             cx.emit(AcpThreadEvent::EntryUpdated(idx));
         } else {
-            let content = ContentBlock::new(chunk, &language_registry, cx);
+            let content = ContentBlock::new(chunk.clone(), &language_registry, cx);
             self.push_entry(
                 AgentThreadEntry::UserMessage(UserMessage {
                     id: message_id,
                     content,
+                    chunks: vec![chunk],
                     checkpoint: None,
                 }),
                 cx,
@@ -1150,6 +1158,7 @@ impl AcpThread {
             AgentThreadEntry::UserMessage(UserMessage {
                 id: message_id.clone(),
                 content: block,
+                chunks: message.clone(),
                 checkpoint: None,
             }),
             cx,

crates/acp_thread/src/mention.rs 🔗

@@ -1,16 +1,21 @@
 use agent::ThreadId;
 use anyhow::{Context as _, Result, bail};
+use file_icons::FileIcons;
 use prompt_store::{PromptId, UserPromptId};
 use std::{
     fmt,
     ops::Range,
     path::{Path, PathBuf},
 };
+use ui::{App, IconName, SharedString};
 use url::Url;
 
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub enum MentionUri {
-    File(PathBuf),
+    File {
+        abs_path: PathBuf,
+        is_directory: bool,
+    },
     Symbol {
         path: PathBuf,
         name: String,
@@ -75,8 +80,12 @@ impl MentionUri {
                 } else {
                     let file_path =
                         PathBuf::from(format!("{}{}", url.host_str().unwrap_or(""), path));
+                    let is_directory = input.ends_with("/");
 
-                    Ok(Self::File(file_path))
+                    Ok(Self::File {
+                        abs_path: file_path,
+                        is_directory,
+                    })
                 }
             }
             "zed" => {
@@ -108,9 +117,9 @@ impl MentionUri {
         }
     }
 
-    fn name(&self) -> String {
+    pub fn name(&self) -> String {
         match self {
-            MentionUri::File(path) => path
+            MentionUri::File { abs_path, .. } => abs_path
                 .file_name()
                 .unwrap_or_default()
                 .to_string_lossy()
@@ -126,15 +135,45 @@ impl MentionUri {
         }
     }
 
+    pub fn icon_path(&self, cx: &mut App) -> SharedString {
+        match self {
+            MentionUri::File {
+                abs_path,
+                is_directory,
+            } => {
+                if *is_directory {
+                    FileIcons::get_folder_icon(false, cx)
+                        .unwrap_or_else(|| IconName::Folder.path().into())
+                } else {
+                    FileIcons::get_icon(&abs_path, cx)
+                        .unwrap_or_else(|| IconName::File.path().into())
+                }
+            }
+            MentionUri::Symbol { .. } => IconName::Code.path().into(),
+            MentionUri::Thread { .. } => IconName::Thread.path().into(),
+            MentionUri::TextThread { .. } => IconName::Thread.path().into(),
+            MentionUri::Rule { .. } => IconName::Reader.path().into(),
+            MentionUri::Selection { .. } => IconName::Reader.path().into(),
+            MentionUri::Fetch { .. } => IconName::ToolWeb.path().into(),
+        }
+    }
+
     pub fn as_link<'a>(&'a self) -> MentionLink<'a> {
         MentionLink(self)
     }
 
     pub fn to_uri(&self) -> Url {
         match self {
-            MentionUri::File(path) => {
+            MentionUri::File {
+                abs_path,
+                is_directory,
+            } => {
                 let mut url = Url::parse("file:///").unwrap();
-                url.set_path(&path.to_string_lossy());
+                let mut path = abs_path.to_string_lossy().to_string();
+                if *is_directory && !path.ends_with("/") {
+                    path.push_str("/");
+                }
+                url.set_path(&path);
                 url
             }
             MentionUri::Symbol {
@@ -226,12 +265,53 @@ mod tests {
         let file_uri = "file:///path/to/file.rs";
         let parsed = MentionUri::parse(file_uri).unwrap();
         match &parsed {
-            MentionUri::File(path) => assert_eq!(path.to_str().unwrap(), "/path/to/file.rs"),
+            MentionUri::File {
+                abs_path,
+                is_directory,
+            } => {
+                assert_eq!(abs_path.to_str().unwrap(), "/path/to/file.rs");
+                assert!(!is_directory);
+            }
             _ => panic!("Expected File variant"),
         }
         assert_eq!(parsed.to_uri().to_string(), file_uri);
     }
 
+    #[test]
+    fn test_parse_directory_uri() {
+        let file_uri = "file:///path/to/dir/";
+        let parsed = MentionUri::parse(file_uri).unwrap();
+        match &parsed {
+            MentionUri::File {
+                abs_path,
+                is_directory,
+            } => {
+                assert_eq!(abs_path.to_str().unwrap(), "/path/to/dir/");
+                assert!(is_directory);
+            }
+            _ => panic!("Expected File variant"),
+        }
+        assert_eq!(parsed.to_uri().to_string(), file_uri);
+    }
+
+    #[test]
+    fn test_to_directory_uri_with_slash() {
+        let uri = MentionUri::File {
+            abs_path: PathBuf::from("/path/to/dir/"),
+            is_directory: true,
+        };
+        assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/");
+    }
+
+    #[test]
+    fn test_to_directory_uri_without_slash() {
+        let uri = MentionUri::File {
+            abs_path: PathBuf::from("/path/to/dir"),
+            is_directory: true,
+        };
+        assert_eq!(uri.to_uri().to_string(), "file:///path/to/dir/");
+    }
+
     #[test]
     fn test_parse_symbol_uri() {
         let symbol_uri = "file:///path/to/file.rs?symbol=MySymbol#L10:20";

crates/agent2/src/thread.rs 🔗

@@ -124,12 +124,12 @@ impl UserMessage {
                 }
                 UserMessageContent::Mention { uri, content } => {
                     match uri {
-                        MentionUri::File(path) => {
+                        MentionUri::File { abs_path, .. } => {
                             write!(
                                 &mut symbol_context,
                                 "\n{}",
                                 MarkdownCodeBlock {
-                                    tag: &codeblock_tag(&path, None),
+                                    tag: &codeblock_tag(&abs_path, None),
                                     text: &content.to_string(),
                                 }
                             )

crates/agent_ui/src/acp.rs 🔗

@@ -1,10 +1,9 @@
 mod completion_provider;
-mod message_history;
+mod message_editor;
 mod model_selector;
 mod model_selector_popover;
 mod thread_view;
 
-pub use message_history::MessageHistory;
 pub use model_selector::AcpModelSelector;
 pub use model_selector_popover::AcpModelSelectorPopover;
 pub use thread_view::AcpThreadView;

crates/agent_ui/src/acp/completion_provider.rs 🔗

@@ -1,5 +1,5 @@
 use std::ops::Range;
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
 use std::sync::Arc;
 use std::sync::atomic::AtomicBool;
 
@@ -8,7 +8,7 @@ use anyhow::{Context as _, Result, anyhow};
 use collections::{HashMap, HashSet};
 use editor::display_map::CreaseId;
 use editor::{CompletionProvider, Editor, ExcerptId, ToOffset as _};
-use file_icons::FileIcons;
+
 use futures::future::try_join_all;
 use fuzzy::{StringMatch, StringMatchCandidate};
 use gpui::{App, Entity, Task, WeakEntity};
@@ -28,10 +28,7 @@ use url::Url;
 use workspace::Workspace;
 use workspace::notifications::NotifyResultExt;
 
-use agent::{
-    context::RULES_ICON,
-    thread_store::{TextThreadStore, ThreadStore},
-};
+use agent::thread_store::{TextThreadStore, ThreadStore};
 
 use crate::context_picker::fetch_context_picker::fetch_url_content;
 use crate::context_picker::file_context_picker::{FileMatch, search_files};
@@ -66,6 +63,11 @@ impl MentionSet {
         self.uri_by_crease_id.drain().map(|(id, _)| id)
     }
 
+    pub fn clear(&mut self) {
+        self.fetch_results.clear();
+        self.uri_by_crease_id.clear();
+    }
+
     pub fn contents(
         &self,
         project: Entity<Project>,
@@ -79,12 +81,13 @@ impl MentionSet {
             .iter()
             .map(|(&crease_id, uri)| {
                 match uri {
-                    MentionUri::File(path) => {
+                    MentionUri::File { abs_path, .. } => {
+                        // TODO directories
                         let uri = uri.clone();
-                        let path = path.to_path_buf();
+                        let abs_path = abs_path.to_path_buf();
                         let buffer_task = project.update(cx, |project, cx| {
                             let path = project
-                                .find_project_path(path, cx)
+                                .find_project_path(abs_path, cx)
                                 .context("Failed to find project path")?;
                             anyhow::Ok(project.open_buffer(path, cx))
                         });
@@ -508,9 +511,14 @@ impl ContextPickerCompletionProvider {
                                             })
                                             .unwrap_or_default();
                                         let line_range = point_range.start.row..point_range.end.row;
+
+                                        let uri = MentionUri::Selection {
+                                            path: path.clone(),
+                                            line_range: line_range.clone(),
+                                        };
                                         let crease = crate::context_picker::crease_for_mention(
                                             selection_name(&path, &line_range).into(),
-                                            IconName::Reader.path().into(),
+                                            uri.icon_path(cx),
                                             range,
                                             editor.downgrade(),
                                         );
@@ -528,10 +536,7 @@ impl ContextPickerCompletionProvider {
                                                 crease_ids.try_into().unwrap()
                                             });
 
-                                        mention_set.lock().insert(
-                                            crease_id,
-                                            MentionUri::Selection { path, line_range },
-                                        );
+                                        mention_set.lock().insert(crease_id, uri);
 
                                         current_offset += text_len + 1;
                                     }
@@ -569,13 +574,8 @@ impl ContextPickerCompletionProvider {
         recent: bool,
         editor: Entity<Editor>,
         mention_set: Arc<Mutex<MentionSet>>,
+        cx: &mut App,
     ) -> Completion {
-        let icon_for_completion = if recent {
-            IconName::HistoryRerun
-        } else {
-            IconName::Thread
-        };
-
         let uri = match &thread_entry {
             ThreadContextEntry::Thread { id, title } => MentionUri::Thread {
                 id: id.clone(),
@@ -586,6 +586,13 @@ impl ContextPickerCompletionProvider {
                 name: title.to_string(),
             },
         };
+
+        let icon_for_completion = if recent {
+            IconName::HistoryRerun.path().into()
+        } else {
+            uri.icon_path(cx)
+        };
+
         let new_text = format!("{} ", uri.as_link());
 
         let new_text_len = new_text.len();
@@ -596,9 +603,9 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             insert_text_mode: None,
             source: project::CompletionSource::Custom,
-            icon_path: Some(icon_for_completion.path().into()),
+            icon_path: Some(icon_for_completion.clone()),
             confirm: Some(confirm_completion_callback(
-                IconName::Thread.path().into(),
+                uri.icon_path(cx),
                 thread_entry.title().clone(),
                 excerpt_id,
                 source_range.start,
@@ -616,6 +623,7 @@ impl ContextPickerCompletionProvider {
         source_range: Range<Anchor>,
         editor: Entity<Editor>,
         mention_set: Arc<Mutex<MentionSet>>,
+        cx: &mut App,
     ) -> Completion {
         let uri = MentionUri::Rule {
             id: rule.prompt_id.into(),
@@ -623,6 +631,7 @@ impl ContextPickerCompletionProvider {
         };
         let new_text = format!("{} ", uri.as_link());
         let new_text_len = new_text.len();
+        let icon_path = uri.icon_path(cx);
         Completion {
             replace_range: source_range.clone(),
             new_text,
@@ -630,9 +639,9 @@ impl ContextPickerCompletionProvider {
             documentation: None,
             insert_text_mode: None,
             source: project::CompletionSource::Custom,
-            icon_path: Some(RULES_ICON.path().into()),
+            icon_path: Some(icon_path.clone()),
             confirm: Some(confirm_completion_callback(
-                RULES_ICON.path().into(),
+                icon_path,
                 rule.title.clone(),
                 excerpt_id,
                 source_range.start,
@@ -654,7 +663,7 @@ impl ContextPickerCompletionProvider {
         editor: Entity<Editor>,
         mention_set: Arc<Mutex<MentionSet>>,
         project: Entity<Project>,
-        cx: &App,
+        cx: &mut App,
     ) -> Option<Completion> {
         let (file_name, directory) =
             crate::context_picker::file_context_picker::extract_file_name_and_directory(
@@ -664,27 +673,21 @@ impl ContextPickerCompletionProvider {
 
         let label =
             build_code_label_for_full_path(&file_name, directory.as_ref().map(|s| s.as_ref()), cx);
-        let full_path = if let Some(directory) = directory {
-            format!("{}{}", directory, file_name)
-        } else {
-            file_name.to_string()
-        };
 
-        let crease_icon_path = if is_directory {
-            FileIcons::get_folder_icon(false, cx).unwrap_or_else(|| IconName::Folder.path().into())
-        } else {
-            FileIcons::get_icon(Path::new(&full_path), cx)
-                .unwrap_or_else(|| IconName::File.path().into())
+        let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
+
+        let file_uri = MentionUri::File {
+            abs_path,
+            is_directory,
         };
+
+        let crease_icon_path = file_uri.icon_path(cx);
         let completion_icon_path = if is_recent {
             IconName::HistoryRerun.path().into()
         } else {
             crease_icon_path.clone()
         };
 
-        let abs_path = project.read(cx).absolute_path(&project_path, cx)?;
-
-        let file_uri = MentionUri::File(abs_path);
         let new_text = format!("{} ", file_uri.as_link());
         let new_text_len = new_text.len();
         Some(Completion {
@@ -729,16 +732,17 @@ impl ContextPickerCompletionProvider {
         };
         let new_text = format!("{} ", uri.as_link());
         let new_text_len = new_text.len();
+        let icon_path = uri.icon_path(cx);
         Some(Completion {
             replace_range: source_range.clone(),
             new_text,
             label,
             documentation: None,
             source: project::CompletionSource::Custom,
-            icon_path: Some(IconName::Code.path().into()),
+            icon_path: Some(icon_path.clone()),
             insert_text_mode: None,
             confirm: Some(confirm_completion_callback(
-                IconName::Code.path().into(),
+                icon_path,
                 symbol.name.clone().into(),
                 excerpt_id,
                 source_range.start,
@@ -757,16 +761,23 @@ impl ContextPickerCompletionProvider {
         editor: Entity<Editor>,
         mention_set: Arc<Mutex<MentionSet>>,
         http_client: Arc<HttpClientWithUrl>,
+        cx: &mut App,
     ) -> Option<Completion> {
         let new_text = format!("@fetch {} ", url_to_fetch.clone());
         let new_text_len = new_text.len();
+        let mention_uri = MentionUri::Fetch {
+            url: url::Url::parse(url_to_fetch.as_ref())
+                .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}")))
+                .ok()?,
+        };
+        let icon_path = mention_uri.icon_path(cx);
         Some(Completion {
             replace_range: source_range.clone(),
             new_text,
             label: CodeLabel::plain(url_to_fetch.to_string(), None),
             documentation: None,
             source: project::CompletionSource::Custom,
-            icon_path: Some(IconName::ToolWeb.path().into()),
+            icon_path: Some(icon_path.clone()),
             insert_text_mode: None,
             confirm: Some({
                 let start = source_range.start;
@@ -774,6 +785,7 @@ impl ContextPickerCompletionProvider {
                 let editor = editor.clone();
                 let url_to_fetch = url_to_fetch.clone();
                 let source_range = source_range.clone();
+                let icon_path = icon_path.clone();
                 Arc::new(move |_, window, cx| {
                     let Some(url) = url::Url::parse(url_to_fetch.as_ref())
                         .or_else(|_| url::Url::parse(&format!("https://{url_to_fetch}")))
@@ -781,12 +793,12 @@ impl ContextPickerCompletionProvider {
                     else {
                         return false;
                     };
-                    let mention_uri = MentionUri::Fetch { url: url.clone() };
 
                     let editor = editor.clone();
                     let mention_set = mention_set.clone();
                     let http_client = http_client.clone();
                     let source_range = source_range.clone();
+                    let icon_path = icon_path.clone();
                     window.defer(cx, move |window, cx| {
                         let url = url.clone();
 
@@ -795,7 +807,7 @@ impl ContextPickerCompletionProvider {
                             start,
                             content_len,
                             url.to_string().into(),
-                            IconName::ToolWeb.path().into(),
+                            icon_path,
                             editor.clone(),
                             window,
                             cx,
@@ -814,8 +826,10 @@ impl ContextPickerCompletionProvider {
                                         .await
                                         .notify_async_err(cx)
                                 {
-                                    mention_set.lock().add_fetch_result(url, content);
-                                    mention_set.lock().insert(crease_id, mention_uri.clone());
+                                    mention_set.lock().add_fetch_result(url.clone(), content);
+                                    mention_set
+                                        .lock()
+                                        .insert(crease_id, MentionUri::Fetch { url });
                                 } else {
                                     // Remove crease if we failed to fetch
                                     editor
@@ -911,8 +925,8 @@ impl CompletionProvider for ContextPickerCompletionProvider {
 
             for uri in mention_set.uri_by_crease_id.values() {
                 match uri {
-                    MentionUri::File(path) => {
-                        excluded_paths.insert(path.clone());
+                    MentionUri::File { abs_path, .. } => {
+                        excluded_paths.insert(abs_path.clone());
                     }
                     MentionUri::Thread { id, .. } => {
                         excluded_threads.insert(id.clone());
@@ -1001,6 +1015,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                             is_recent,
                             editor.clone(),
                             mention_set.clone(),
+                            cx,
                         )),
 
                         Match::Rules(user_rules) => Some(Self::completion_for_rules(
@@ -1009,6 +1024,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                             source_range.clone(),
                             editor.clone(),
                             mention_set.clone(),
+                            cx,
                         )),
 
                         Match::Fetch(url) => Self::completion_for_fetch(
@@ -1018,6 +1034,7 @@ impl CompletionProvider for ContextPickerCompletionProvider {
                             editor.clone(),
                             mention_set.clone(),
                             http_client.clone(),
+                            cx,
                         ),
 
                         Match::Entry(EntryMatch { entry, .. }) => Self::completion_for_entry(
@@ -1179,7 +1196,7 @@ mod tests {
     use serde_json::json;
     use settings::SettingsStore;
     use smol::stream::StreamExt as _;
-    use std::{ops::Deref, rc::Rc};
+    use std::{ops::Deref, path::Path, rc::Rc};
     use util::path;
     use workspace::{AppState, Item};
 

crates/agent_ui/src/acp/message_editor.rs 🔗

@@ -0,0 +1,469 @@
+use crate::acp::completion_provider::ContextPickerCompletionProvider;
+use crate::acp::completion_provider::MentionSet;
+use acp_thread::MentionUri;
+use agent::TextThreadStore;
+use agent::ThreadStore;
+use agent_client_protocol as acp;
+use anyhow::Result;
+use collections::HashSet;
+use editor::{
+    AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
+    EditorStyle, MultiBuffer,
+};
+use gpui::{
+    AppContext, Context, Entity, EventEmitter, FocusHandle, Focusable, Task, TextStyle, WeakEntity,
+};
+use language::Buffer;
+use language::Language;
+use parking_lot::Mutex;
+use project::{CompletionIntent, Project};
+use settings::Settings;
+use std::fmt::Write;
+use std::rc::Rc;
+use std::sync::Arc;
+use theme::ThemeSettings;
+use ui::{
+    ActiveTheme, App, InteractiveElement, IntoElement, ParentElement, Render, Styled, TextSize,
+    Window, div,
+};
+use util::ResultExt;
+use workspace::Workspace;
+use zed_actions::agent::Chat;
+
+pub struct MessageEditor {
+    editor: Entity<Editor>,
+    project: Entity<Project>,
+    thread_store: Entity<ThreadStore>,
+    text_thread_store: Entity<TextThreadStore>,
+    mention_set: Arc<Mutex<MentionSet>>,
+}
+
+pub enum MessageEditorEvent {
+    Send,
+    Cancel,
+}
+
+impl EventEmitter<MessageEditorEvent> for MessageEditor {}
+
+impl MessageEditor {
+    pub fn new(
+        workspace: WeakEntity<Workspace>,
+        project: Entity<Project>,
+        thread_store: Entity<ThreadStore>,
+        text_thread_store: Entity<TextThreadStore>,
+        mode: EditorMode,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let language = Language::new(
+            language::LanguageConfig {
+                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
+                ..Default::default()
+            },
+            None,
+        );
+
+        let mention_set = Arc::new(Mutex::new(MentionSet::default()));
+        let editor = cx.new(|cx| {
+            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
+            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+
+            let mut editor = Editor::new(mode, buffer, None, window, cx);
+            editor.set_placeholder_text("Message the agent - @ to include files", cx);
+            editor.set_show_indent_guides(false, cx);
+            editor.set_soft_wrap();
+            editor.set_use_modal_editing(true);
+            editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
+                mention_set.clone(),
+                workspace,
+                thread_store.downgrade(),
+                text_thread_store.downgrade(),
+                cx.weak_entity(),
+            ))));
+            editor.set_context_menu_options(ContextMenuOptions {
+                min_entries_visible: 12,
+                max_entries_visible: 12,
+                placement: Some(ContextMenuPlacement::Above),
+            });
+            editor
+        });
+
+        Self {
+            editor,
+            project,
+            mention_set,
+            thread_store,
+            text_thread_store,
+        }
+    }
+
+    pub fn is_empty(&self, cx: &App) -> bool {
+        self.editor.read(cx).is_empty(cx)
+    }
+
+    pub fn contents(
+        &self,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Task<Result<Vec<acp::ContentBlock>>> {
+        let contents = self.mention_set.lock().contents(
+            self.project.clone(),
+            self.thread_store.clone(),
+            self.text_thread_store.clone(),
+            window,
+            cx,
+        );
+        let editor = self.editor.clone();
+
+        cx.spawn(async move |_, cx| {
+            let contents = contents.await?;
+
+            editor.update(cx, |editor, cx| {
+                let mut ix = 0;
+                let mut chunks: Vec<acp::ContentBlock> = Vec::new();
+                let text = editor.text(cx);
+                editor.display_map.update(cx, |map, cx| {
+                    let snapshot = map.snapshot(cx);
+                    for (crease_id, crease) in snapshot.crease_snapshot.creases() {
+                        // Skip creases that have been edited out of the message buffer.
+                        if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
+                            continue;
+                        }
+
+                        if let Some(mention) = contents.get(&crease_id) {
+                            let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
+                            if crease_range.start > ix {
+                                chunks.push(text[ix..crease_range.start].into());
+                            }
+                            chunks.push(acp::ContentBlock::Resource(acp::EmbeddedResource {
+                                annotations: None,
+                                resource: acp::EmbeddedResourceResource::TextResourceContents(
+                                    acp::TextResourceContents {
+                                        mime_type: None,
+                                        text: mention.content.clone(),
+                                        uri: mention.uri.to_uri().to_string(),
+                                    },
+                                ),
+                            }));
+                            ix = crease_range.end;
+                        }
+                    }
+
+                    if ix < text.len() {
+                        let last_chunk = text[ix..].trim_end();
+                        if !last_chunk.is_empty() {
+                            chunks.push(last_chunk.into());
+                        }
+                    }
+                });
+
+                chunks
+            })
+        })
+    }
+
+    pub fn clear(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        self.editor.update(cx, |editor, cx| {
+            editor.clear(window, cx);
+            editor.remove_creases(self.mention_set.lock().drain(), cx)
+        });
+    }
+
+    fn chat(&mut self, _: &Chat, _: &mut Window, cx: &mut Context<Self>) {
+        cx.emit(MessageEditorEvent::Send)
+    }
+
+    fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
+        cx.emit(MessageEditorEvent::Cancel)
+    }
+
+    pub fn insert_dragged_files(
+        &self,
+        paths: Vec<project::ProjectPath>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let buffer = self.editor.read(cx).buffer().clone();
+        let Some((&excerpt_id, _, _)) = buffer.read(cx).snapshot(cx).as_singleton() else {
+            return;
+        };
+        let Some(buffer) = buffer.read(cx).as_singleton() else {
+            return;
+        };
+        for path in paths {
+            let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
+                continue;
+            };
+            let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
+                continue;
+            };
+
+            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
+            let path_prefix = abs_path
+                .file_name()
+                .unwrap_or(path.path.as_os_str())
+                .display()
+                .to_string();
+            let Some(completion) = ContextPickerCompletionProvider::completion_for_path(
+                path,
+                &path_prefix,
+                false,
+                entry.is_dir(),
+                excerpt_id,
+                anchor..anchor,
+                self.editor.clone(),
+                self.mention_set.clone(),
+                self.project.clone(),
+                cx,
+            ) else {
+                continue;
+            };
+
+            self.editor.update(cx, |message_editor, cx| {
+                message_editor.edit(
+                    [(
+                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
+                        completion.new_text,
+                    )],
+                    cx,
+                );
+            });
+            if let Some(confirm) = completion.confirm.clone() {
+                confirm(CompletionIntent::Complete, window, cx);
+            }
+        }
+    }
+
+    pub fn set_mode(&mut self, mode: EditorMode, cx: &mut Context<Self>) {
+        self.editor.update(cx, |editor, cx| {
+            editor.set_mode(mode);
+            cx.notify()
+        });
+    }
+
+    pub fn set_message(
+        &mut self,
+        message: &[acp::ContentBlock],
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let mut text = String::new();
+        let mut mentions = Vec::new();
+
+        for chunk in message {
+            match chunk {
+                acp::ContentBlock::Text(text_content) => {
+                    text.push_str(&text_content.text);
+                }
+                acp::ContentBlock::Resource(acp::EmbeddedResource {
+                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
+                    ..
+                }) => {
+                    if let Some(mention_uri) = MentionUri::parse(&resource.uri).log_err() {
+                        let start = text.len();
+                        write!(&mut text, "{}", mention_uri.as_link()).ok();
+                        let end = text.len();
+                        mentions.push((start..end, mention_uri));
+                    }
+                }
+                acp::ContentBlock::Image(_)
+                | acp::ContentBlock::Audio(_)
+                | acp::ContentBlock::Resource(_)
+                | acp::ContentBlock::ResourceLink(_) => {}
+            }
+        }
+
+        let snapshot = self.editor.update(cx, |editor, cx| {
+            editor.set_text(text, window, cx);
+            editor.buffer().read(cx).snapshot(cx)
+        });
+
+        self.mention_set.lock().clear();
+        for (range, mention_uri) in mentions {
+            let anchor = snapshot.anchor_before(range.start);
+            let crease_id = crate::context_picker::insert_crease_for_mention(
+                anchor.excerpt_id,
+                anchor.text_anchor,
+                range.end - range.start,
+                mention_uri.name().into(),
+                mention_uri.icon_path(cx),
+                self.editor.clone(),
+                window,
+                cx,
+            );
+
+            if let Some(crease_id) = crease_id {
+                self.mention_set.lock().insert(crease_id, mention_uri);
+            }
+        }
+        cx.notify();
+    }
+
+    #[cfg(test)]
+    pub fn set_text(&mut self, text: &str, window: &mut Window, cx: &mut Context<Self>) {
+        self.editor.update(cx, |editor, cx| {
+            editor.set_text(text, window, cx);
+        });
+    }
+}
+
+impl Focusable for MessageEditor {
+    fn focus_handle(&self, cx: &App) -> FocusHandle {
+        self.editor.focus_handle(cx)
+    }
+}
+
+impl Render for MessageEditor {
+    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        div()
+            .key_context("MessageEditor")
+            .on_action(cx.listener(Self::chat))
+            .on_action(cx.listener(Self::cancel))
+            .flex_1()
+            .child({
+                let settings = ThemeSettings::get_global(cx);
+                let font_size = TextSize::Small
+                    .rems(cx)
+                    .to_pixels(settings.agent_font_size(cx));
+                let line_height = settings.buffer_line_height.value() * font_size;
+
+                let text_style = TextStyle {
+                    color: cx.theme().colors().text,
+                    font_family: settings.buffer_font.family.clone(),
+                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
+                    font_features: settings.buffer_font.features.clone(),
+                    font_size: font_size.into(),
+                    line_height: line_height.into(),
+                    ..Default::default()
+                };
+
+                EditorElement::new(
+                    &self.editor,
+                    EditorStyle {
+                        background: cx.theme().colors().editor_background,
+                        local_player: cx.theme().players().local(),
+                        text: text_style,
+                        syntax: cx.theme().syntax().clone(),
+                        ..Default::default()
+                    },
+                )
+            })
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use std::path::Path;
+
+    use agent::{TextThreadStore, ThreadStore};
+    use agent_client_protocol as acp;
+    use editor::EditorMode;
+    use fs::FakeFs;
+    use gpui::{AppContext, TestAppContext};
+    use lsp::{CompletionContext, CompletionTriggerKind};
+    use project::{CompletionIntent, Project};
+    use serde_json::json;
+    use util::path;
+    use workspace::Workspace;
+
+    use crate::acp::{message_editor::MessageEditor, thread_view::tests::init_test};
+
+    #[gpui::test]
+    async fn test_at_mention_removal(cx: &mut TestAppContext) {
+        init_test(cx);
+
+        let fs = FakeFs::new(cx.executor());
+        fs.insert_tree("/project", json!({"file": ""})).await;
+        let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
+
+        let (workspace, cx) =
+            cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
+
+        let thread_store = cx.new(|cx| ThreadStore::fake(project.clone(), cx));
+        let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
+
+        let message_editor = cx.update(|window, cx| {
+            cx.new(|cx| {
+                MessageEditor::new(
+                    workspace.downgrade(),
+                    project.clone(),
+                    thread_store.clone(),
+                    text_thread_store.clone(),
+                    EditorMode::AutoHeight {
+                        min_lines: 1,
+                        max_lines: None,
+                    },
+                    window,
+                    cx,
+                )
+            })
+        });
+        let editor = message_editor.update(cx, |message_editor, _| message_editor.editor.clone());
+
+        cx.run_until_parked();
+
+        let excerpt_id = editor.update(cx, |editor, cx| {
+            editor
+                .buffer()
+                .read(cx)
+                .excerpt_ids()
+                .into_iter()
+                .next()
+                .unwrap()
+        });
+        let completions = editor.update_in(cx, |editor, window, cx| {
+            editor.set_text("Hello @file ", window, cx);
+            let buffer = editor.buffer().read(cx).as_singleton().unwrap();
+            let completion_provider = editor.completion_provider().unwrap();
+            completion_provider.completions(
+                excerpt_id,
+                &buffer,
+                text::Anchor::MAX,
+                CompletionContext {
+                    trigger_kind: CompletionTriggerKind::TRIGGER_CHARACTER,
+                    trigger_character: Some("@".into()),
+                },
+                window,
+                cx,
+            )
+        });
+        let [_, completion]: [_; 2] = completions
+            .await
+            .unwrap()
+            .into_iter()
+            .flat_map(|response| response.completions)
+            .collect::<Vec<_>>()
+            .try_into()
+            .unwrap();
+
+        editor.update_in(cx, |editor, window, cx| {
+            let snapshot = editor.buffer().read(cx).snapshot(cx);
+            let start = snapshot
+                .anchor_in_excerpt(excerpt_id, completion.replace_range.start)
+                .unwrap();
+            let end = snapshot
+                .anchor_in_excerpt(excerpt_id, completion.replace_range.end)
+                .unwrap();
+            editor.edit([(start..end, completion.new_text)], cx);
+            (completion.confirm.unwrap())(CompletionIntent::Complete, window, cx);
+        });
+
+        cx.run_until_parked();
+
+        // Backspace over the inserted crease (and the following space).
+        editor.update_in(cx, |editor, window, cx| {
+            editor.backspace(&Default::default(), window, cx);
+            editor.backspace(&Default::default(), window, cx);
+        });
+
+        let content = message_editor
+            .update_in(cx, |message_editor, window, cx| {
+                message_editor.contents(window, cx)
+            })
+            .await
+            .unwrap();
+
+        // We don't send a resource link for the deleted crease.
+        pretty_assertions::assert_matches!(content.as_slice(), [acp::ContentBlock::Text { .. }]);
+    }
+}

crates/agent_ui/src/acp/message_history.rs 🔗

@@ -1,88 +0,0 @@
-pub struct MessageHistory<T> {
-    items: Vec<T>,
-    current: Option<usize>,
-}
-
-impl<T> Default for MessageHistory<T> {
-    fn default() -> Self {
-        MessageHistory {
-            items: Vec::new(),
-            current: None,
-        }
-    }
-}
-
-impl<T> MessageHistory<T> {
-    pub fn push(&mut self, message: T) {
-        self.current.take();
-        self.items.push(message);
-    }
-
-    pub fn reset_position(&mut self) {
-        self.current.take();
-    }
-
-    pub fn prev(&mut self) -> Option<&T> {
-        if self.items.is_empty() {
-            return None;
-        }
-
-        let new_ix = self
-            .current
-            .get_or_insert(self.items.len())
-            .saturating_sub(1);
-
-        self.current = Some(new_ix);
-        self.items.get(new_ix)
-    }
-
-    pub fn next(&mut self) -> Option<&T> {
-        let current = self.current.as_mut()?;
-        *current += 1;
-
-        self.items.get(*current).or_else(|| {
-            self.current.take();
-            None
-        })
-    }
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn test_prev_next() {
-        let mut history = MessageHistory::default();
-
-        // Test empty history
-        assert_eq!(history.prev(), None);
-        assert_eq!(history.next(), None);
-
-        // Add some messages
-        history.push("first");
-        history.push("second");
-        history.push("third");
-
-        // Test prev navigation
-        assert_eq!(history.prev(), Some(&"third"));
-        assert_eq!(history.prev(), Some(&"second"));
-        assert_eq!(history.prev(), Some(&"first"));
-        assert_eq!(history.prev(), Some(&"first"));
-
-        assert_eq!(history.next(), Some(&"second"));
-
-        // Test mixed navigation
-        history.push("fourth");
-        assert_eq!(history.prev(), Some(&"fourth"));
-        assert_eq!(history.prev(), Some(&"third"));
-        assert_eq!(history.next(), Some(&"fourth"));
-        assert_eq!(history.next(), None);
-
-        // Test that push resets navigation
-        history.prev();
-        history.prev();
-        history.push("fifth");
-        assert_eq!(history.prev(), Some(&"fifth"));
-    }
-}

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

@@ -12,34 +12,25 @@ use audio::{Audio, Sound};
 use buffer_diff::BufferDiff;
 use collections::{HashMap, HashSet};
 use editor::scroll::Autoscroll;
-use editor::{
-    AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
-    EditorStyle, MinimapVisibility, MultiBuffer, PathKey, SelectionEffects,
-};
+use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey, SelectionEffects};
 use file_icons::FileIcons;
 use gpui::{
-    Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
-    FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton, PlatformDisplay,
-    SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle, TextStyleRefinement,
-    Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div, linear_color_stop,
-    linear_gradient, list, percentage, point, prelude::*, pulsating_between,
+    Action, Animation, AnimationExt, App, BorderStyle, ClickEvent, EdgesRefinement, Empty, Entity,
+    EntityId, FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, MouseButton,
+    PlatformDisplay, SharedString, Stateful, StyleRefinement, Subscription, Task, TextStyle,
+    TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, Window, WindowHandle, div,
+    linear_color_stop, linear_gradient, list, percentage, point, prelude::*, pulsating_between,
 };
+use language::Buffer;
 use language::language_settings::SoftWrap;
-use language::{Buffer, Language};
 use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
-use parking_lot::Mutex;
-use project::{CompletionIntent, Project};
+use project::Project;
 use prompt_store::PromptId;
 use rope::Point;
 use settings::{Settings as _, SettingsStore};
-use std::fmt::Write as _;
-use std::path::PathBuf;
-use std::{
-    cell::RefCell, collections::BTreeMap, path::Path, process::ExitStatus, rc::Rc, sync::Arc,
-    time::Duration,
-};
+use std::{collections::BTreeMap, process::ExitStatus, rc::Rc, time::Duration};
 use terminal_view::TerminalView;
-use text::{Anchor, BufferSnapshot};
+use text::Anchor;
 use theme::ThemeSettings;
 use ui::{
     Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState,
@@ -47,14 +38,12 @@ use ui::{
 };
 use util::{ResultExt, size::format_file_size, time::duration_alt_display};
 use workspace::{CollaboratorId, Workspace};
-use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage, ToggleModelSelector};
+use zed_actions::agent::{Chat, ToggleModelSelector};
 use zed_actions::assistant::OpenRulesLibrary;
 
 use crate::acp::AcpModelSelectorPopover;
-use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
-use crate::acp::message_history::MessageHistory;
+use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
 use crate::agent_diff::AgentDiff;
-use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
 use crate::ui::{AgentNotification, AgentNotificationEvent};
 use crate::{
     AgentDiffPane, AgentPanel, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll,
@@ -62,6 +51,9 @@ use crate::{
 
 const RESPONSE_PADDING_X: Pixels = px(19.);
 
+pub const MIN_EDITOR_LINES: usize = 4;
+pub const MAX_EDITOR_LINES: usize = 8;
+
 pub struct AcpThreadView {
     agent: Rc<dyn AgentServer>,
     workspace: WeakEntity<Workspace>,
@@ -71,11 +63,8 @@ pub struct AcpThreadView {
     thread_state: ThreadState,
     diff_editors: HashMap<EntityId, Entity<Editor>>,
     terminal_views: HashMap<EntityId, Entity<TerminalView>>,
-    message_editor: Entity<Editor>,
+    message_editor: Entity<MessageEditor>,
     model_selector: Option<Entity<AcpModelSelectorPopover>>,
-    message_set_from_history: Option<BufferSnapshot>,
-    _message_editor_subscription: Subscription,
-    mention_set: Arc<Mutex<MentionSet>>,
     notifications: Vec<WindowHandle<AgentNotification>>,
     notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
     last_error: Option<Entity<Markdown>>,
@@ -88,9 +77,16 @@ pub struct AcpThreadView {
     plan_expanded: bool,
     editor_expanded: bool,
     terminal_expanded: bool,
-    message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
+    editing_message: Option<EditingMessage>,
     _cancel_task: Option<Task<()>>,
-    _subscriptions: [Subscription; 1],
+    _subscriptions: [Subscription; 2],
+}
+
+struct EditingMessage {
+    index: usize,
+    message_id: UserMessageId,
+    editor: Entity<MessageEditor>,
+    _subscription: Subscription,
 }
 
 enum ThreadState {
@@ -117,83 +113,30 @@ impl AcpThreadView {
         project: Entity<Project>,
         thread_store: Entity<ThreadStore>,
         text_thread_store: Entity<TextThreadStore>,
-        message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
-        min_lines: usize,
-        max_lines: Option<usize>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Self {
-        let language = Language::new(
-            language::LanguageConfig {
-                completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
-                ..Default::default()
-            },
-            None,
-        );
-
-        let mention_set = Arc::new(Mutex::new(MentionSet::default()));
-
         let message_editor = cx.new(|cx| {
-            let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
-            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
-
-            let mut editor = Editor::new(
+            MessageEditor::new(
+                workspace.clone(),
+                project.clone(),
+                thread_store.clone(),
+                text_thread_store.clone(),
                 editor::EditorMode::AutoHeight {
-                    min_lines,
-                    max_lines: max_lines,
+                    min_lines: MIN_EDITOR_LINES,
+                    max_lines: Some(MAX_EDITOR_LINES),
                 },
-                buffer,
-                None,
                 window,
                 cx,
-            );
-            editor.set_placeholder_text("Message the agent - @ to include files", cx);
-            editor.set_show_indent_guides(false, cx);
-            editor.set_soft_wrap();
-            editor.set_use_modal_editing(true);
-            editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
-                mention_set.clone(),
-                workspace.clone(),
-                thread_store.downgrade(),
-                text_thread_store.downgrade(),
-                cx.weak_entity(),
-            ))));
-            editor.set_context_menu_options(ContextMenuOptions {
-                min_entries_visible: 12,
-                max_entries_visible: 12,
-                placement: Some(ContextMenuPlacement::Above),
-            });
-            editor
+            )
         });
 
-        let message_editor_subscription =
-            cx.subscribe(&message_editor, |this, editor, event, cx| {
-                if let editor::EditorEvent::BufferEdited = &event {
-                    let buffer = editor
-                        .read(cx)
-                        .buffer()
-                        .read(cx)
-                        .as_singleton()
-                        .unwrap()
-                        .read(cx)
-                        .snapshot();
-                    if let Some(message) = this.message_set_from_history.clone()
-                        && message.version() != buffer.version()
-                    {
-                        this.message_set_from_history = None;
-                    }
-
-                    if this.message_set_from_history.is_none() {
-                        this.message_history.borrow_mut().reset_position();
-                    }
-                }
-            });
-
-        let mention_set = mention_set.clone();
-
         let list_state = ListState::new(0, gpui::ListAlignment::Bottom, px(2048.0));
 
-        let subscription = cx.observe_global_in::<SettingsStore>(window, Self::settings_changed);
+        let subscriptions = [
+            cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
+            cx.subscribe_in(&message_editor, window, Self::on_message_editor_event),
+        ];
 
         Self {
             agent: agent.clone(),
@@ -204,9 +147,6 @@ impl AcpThreadView {
             thread_state: Self::initial_state(agent, workspace, project, window, cx),
             message_editor,
             model_selector: None,
-            message_set_from_history: None,
-            _message_editor_subscription: message_editor_subscription,
-            mention_set,
             notifications: Vec::new(),
             notification_subscriptions: HashMap::default(),
             diff_editors: Default::default(),
@@ -217,12 +157,12 @@ impl AcpThreadView {
             auth_task: None,
             expanded_tool_calls: HashSet::default(),
             expanded_thinking_blocks: HashSet::default(),
+            editing_message: None,
             edits_expanded: false,
             plan_expanded: false,
             editor_expanded: false,
             terminal_expanded: true,
-            message_history,
-            _subscriptions: [subscription],
+            _subscriptions: subscriptions,
             _cancel_task: None,
         }
     }
@@ -370,7 +310,7 @@ impl AcpThreadView {
         }
     }
 
-    pub fn cancel(&mut self, cx: &mut Context<Self>) {
+    pub fn cancel_generation(&mut self, cx: &mut Context<Self>) {
         self.last_error.take();
 
         if let Some(thread) = self.thread() {
@@ -390,193 +330,118 @@ impl AcpThreadView {
 
     fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
         self.editor_expanded = is_expanded;
-        self.message_editor.update(cx, |editor, _| {
-            if self.editor_expanded {
-                editor.set_mode(EditorMode::Full {
-                    scale_ui_elements_with_buffer_font_size: false,
-                    show_active_line_background: false,
-                    sized_by_content: false,
-                })
+        self.message_editor.update(cx, |editor, cx| {
+            if is_expanded {
+                editor.set_mode(
+                    EditorMode::Full {
+                        scale_ui_elements_with_buffer_font_size: false,
+                        show_active_line_background: false,
+                        sized_by_content: false,
+                    },
+                    cx,
+                )
             } else {
-                editor.set_mode(EditorMode::AutoHeight {
-                    min_lines: MIN_EDITOR_LINES,
-                    max_lines: Some(MAX_EDITOR_LINES),
-                })
+                editor.set_mode(
+                    EditorMode::AutoHeight {
+                        min_lines: MIN_EDITOR_LINES,
+                        max_lines: Some(MAX_EDITOR_LINES),
+                    },
+                    cx,
+                )
             }
         });
         cx.notify();
     }
 
-    fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
-        self.last_error.take();
+    pub fn on_message_editor_event(
+        &mut self,
+        _: &Entity<MessageEditor>,
+        event: &MessageEditorEvent,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        match event {
+            MessageEditorEvent::Send => self.send(window, cx),
+            MessageEditorEvent::Cancel => self.cancel_generation(cx),
+        }
+    }
 
-        let mut ix = 0;
-        let mut chunks: Vec<acp::ContentBlock> = Vec::new();
-        let project = self.project.clone();
+    fn send(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+        let contents = self
+            .message_editor
+            .update(cx, |message_editor, cx| message_editor.contents(window, cx));
+        self.send_impl(contents, window, cx)
+    }
 
-        let thread_store = self.thread_store.clone();
-        let text_thread_store = self.text_thread_store.clone();
+    fn send_impl(
+        &mut self,
+        contents: Task<anyhow::Result<Vec<acp::ContentBlock>>>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        self.last_error.take();
+        self.editing_message.take();
 
-        let contents =
-            self.mention_set
-                .lock()
-                .contents(project, thread_store, text_thread_store, window, cx);
+        let Some(thread) = self.thread().cloned() else {
+            return;
+        };
+        let task = cx.spawn_in(window, async move |this, cx| {
+            let contents = contents.await?;
 
-        cx.spawn_in(window, async move |this, cx| {
-            let contents = match contents.await {
-                Ok(contents) => contents,
-                Err(e) => {
-                    this.update(cx, |this, cx| {
-                        this.last_error =
-                            Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx)));
-                    })
-                    .ok();
-                    return;
-                }
-            };
+            if contents.is_empty() {
+                return Ok(());
+            }
 
             this.update_in(cx, |this, window, cx| {
-                this.message_editor.update(cx, |editor, cx| {
-                    let text = editor.text(cx);
-                    editor.display_map.update(cx, |map, cx| {
-                        let snapshot = map.snapshot(cx);
-                        for (crease_id, crease) in snapshot.crease_snapshot.creases() {
-                            // Skip creases that have been edited out of the message buffer.
-                            if !crease.range().start.is_valid(&snapshot.buffer_snapshot) {
-                                continue;
-                            }
-
-                            if let Some(mention) = contents.get(&crease_id) {
-                                let crease_range =
-                                    crease.range().to_offset(&snapshot.buffer_snapshot);
-                                if crease_range.start > ix {
-                                    chunks.push(text[ix..crease_range.start].into());
-                                }
-                                chunks.push(acp::ContentBlock::Resource(acp::EmbeddedResource {
-                                    annotations: None,
-                                    resource: acp::EmbeddedResourceResource::TextResourceContents(
-                                        acp::TextResourceContents {
-                                            mime_type: None,
-                                            text: mention.content.clone(),
-                                            uri: mention.uri.to_uri().to_string(),
-                                        },
-                                    ),
-                                }));
-                                ix = crease_range.end;
-                            }
-                        }
-
-                        if ix < text.len() {
-                            let last_chunk = text[ix..].trim_end();
-                            if !last_chunk.is_empty() {
-                                chunks.push(last_chunk.into());
-                            }
-                        }
-                    })
-                });
-
-                if chunks.is_empty() {
-                    return;
-                }
-
-                let Some(thread) = this.thread() else {
-                    return;
-                };
-                let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
-
-                cx.spawn(async move |this, cx| {
-                    let result = task.await;
-
-                    this.update(cx, |this, cx| {
-                        if let Err(err) = result {
-                            this.last_error =
-                                Some(cx.new(|cx| {
-                                    Markdown::new(err.to_string().into(), None, None, cx)
-                                }))
-                        }
-                    })
-                })
-                .detach();
-
-                let mention_set = this.mention_set.clone();
-
                 this.set_editor_is_expanded(false, cx);
-
-                this.message_editor.update(cx, |editor, cx| {
-                    editor.clear(window, cx);
-                    editor.remove_creases(mention_set.lock().drain(), cx)
-                });
-
                 this.scroll_to_bottom(cx);
+                this.message_editor.update(cx, |message_editor, cx| {
+                    message_editor.clear(window, cx);
+                });
+            })?;
+            let send = thread.update(cx, |thread, cx| thread.send(contents, cx))?;
+            send.await
+        });
 
-                this.message_history.borrow_mut().push(chunks);
-            })
-            .ok();
+        cx.spawn(async move |this, cx| {
+            if let Err(e) = task.await {
+                this.update(cx, |this, cx| {
+                    this.last_error =
+                        Some(cx.new(|cx| Markdown::new(e.to_string().into(), None, None, cx)));
+                    cx.notify()
+                })
+                .ok();
+            }
         })
         .detach();
     }
 
-    fn previous_history_message(
-        &mut self,
-        _: &PreviousHistoryMessage,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if self.message_set_from_history.is_none() && !self.message_editor.read(cx).is_empty(cx) {
-            self.message_editor.update(cx, |editor, cx| {
-                editor.move_up(&Default::default(), window, cx);
-            });
-            return;
-        }
-
-        self.message_set_from_history = Self::set_draft_message(
-            self.message_editor.clone(),
-            self.mention_set.clone(),
-            self.project.clone(),
-            self.message_history
-                .borrow_mut()
-                .prev()
-                .map(|blocks| blocks.as_slice()),
-            window,
-            cx,
-        );
+    fn cancel_editing(&mut self, _: &ClickEvent, _window: &mut Window, cx: &mut Context<Self>) {
+        self.editing_message.take();
+        cx.notify();
     }
 
-    fn next_history_message(
-        &mut self,
-        _: &NextHistoryMessage,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) {
-        if self.message_set_from_history.is_none() {
-            self.message_editor.update(cx, |editor, cx| {
-                editor.move_down(&Default::default(), window, cx);
-            });
+    fn regenerate(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
+        let Some(editing_message) = self.editing_message.take() else {
             return;
-        }
+        };
 
-        let mut message_history = self.message_history.borrow_mut();
-        let next_history = message_history.next();
-
-        let set_draft_message = Self::set_draft_message(
-            self.message_editor.clone(),
-            self.mention_set.clone(),
-            self.project.clone(),
-            Some(
-                next_history
-                    .map(|blocks| blocks.as_slice())
-                    .unwrap_or_else(|| &[]),
-            ),
-            window,
-            cx,
-        );
-        // If we reset the text to an empty string because we ran out of history,
-        // we don't want to mark it as coming from the history
-        self.message_set_from_history = if next_history.is_some() {
-            set_draft_message
-        } else {
-            None
+        let Some(thread) = self.thread().cloned() else {
+            return;
         };
+
+        let rewind = thread.update(cx, |thread, cx| {
+            thread.rewind(editing_message.message_id, cx)
+        });
+
+        let contents = editing_message
+            .editor
+            .update(cx, |message_editor, cx| message_editor.contents(window, cx));
+        let task = cx.foreground_executor().spawn(async move {
+            rewind.await?;
+            contents.await
+        });
+        self.send_impl(task, window, cx);
     }
 
     fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
@@ -606,92 +471,6 @@ impl AcpThreadView {
         })
     }
 
-    fn set_draft_message(
-        message_editor: Entity<Editor>,
-        mention_set: Arc<Mutex<MentionSet>>,
-        project: Entity<Project>,
-        message: Option<&[acp::ContentBlock]>,
-        window: &mut Window,
-        cx: &mut Context<Self>,
-    ) -> Option<BufferSnapshot> {
-        cx.notify();
-
-        let message = message?;
-
-        let mut text = String::new();
-        let mut mentions = Vec::new();
-
-        for chunk in message {
-            match chunk {
-                acp::ContentBlock::Text(text_content) => {
-                    text.push_str(&text_content.text);
-                }
-                acp::ContentBlock::Resource(acp::EmbeddedResource {
-                    resource: acp::EmbeddedResourceResource::TextResourceContents(resource),
-                    ..
-                }) => {
-                    let path = PathBuf::from(&resource.uri);
-                    let project_path = project.read(cx).project_path_for_absolute_path(&path, cx);
-                    let start = text.len();
-                    let _ = write!(&mut text, "{}", MentionUri::File(path).to_uri());
-                    let end = text.len();
-                    if let Some(project_path) = project_path {
-                        let filename: SharedString = project_path
-                            .path
-                            .file_name()
-                            .unwrap_or_default()
-                            .to_string_lossy()
-                            .to_string()
-                            .into();
-                        mentions.push((start..end, project_path, filename));
-                    }
-                }
-                acp::ContentBlock::Image(_)
-                | acp::ContentBlock::Audio(_)
-                | acp::ContentBlock::Resource(_)
-                | acp::ContentBlock::ResourceLink(_) => {}
-            }
-        }
-
-        let snapshot = message_editor.update(cx, |editor, cx| {
-            editor.set_text(text, window, cx);
-            editor.buffer().read(cx).snapshot(cx)
-        });
-
-        for (range, project_path, filename) in mentions {
-            let crease_icon_path = if project_path.path.is_dir() {
-                FileIcons::get_folder_icon(false, cx)
-                    .unwrap_or_else(|| IconName::Folder.path().into())
-            } else {
-                FileIcons::get_icon(Path::new(project_path.path.as_ref()), cx)
-                    .unwrap_or_else(|| IconName::File.path().into())
-            };
-
-            let anchor = snapshot.anchor_before(range.start);
-            if let Some(project_path) = project.read(cx).absolute_path(&project_path, cx) {
-                let crease_id = crate::context_picker::insert_crease_for_mention(
-                    anchor.excerpt_id,
-                    anchor.text_anchor,
-                    range.end - range.start,
-                    filename,
-                    crease_icon_path,
-                    message_editor.clone(),
-                    window,
-                    cx,
-                );
-
-                if let Some(crease_id) = crease_id {
-                    mention_set
-                        .lock()
-                        .insert(crease_id, MentionUri::File(project_path));
-                }
-            }
-        }
-
-        let snapshot = snapshot.as_singleton().unwrap().2.clone();
-        Some(snapshot.text)
-    }
-
     fn handle_thread_event(
         &mut self,
         thread: &Entity<AcpThread>,
@@ -968,12 +747,28 @@ impl AcpThreadView {
                         .border_1()
                         .border_color(cx.theme().colors().border)
                         .text_xs()
-                        .children(message.content.markdown().map(|md| {
-                            self.render_markdown(
-                                md.clone(),
-                                user_message_markdown_style(window, cx),
-                            )
-                        })),
+                        .id("message")
+                        .on_click(cx.listener({
+                            move |this, _, window, cx| this.start_editing_message(index, window, cx)
+                        }))
+                        .children(
+                            if let Some(editing) = self.editing_message.as_ref()
+                                && Some(&editing.message_id) == message.id.as_ref()
+                            {
+                                Some(
+                                    self.render_edit_message_editor(editing, cx)
+                                        .into_any_element(),
+                                )
+                            } else {
+                                message.content.markdown().map(|md| {
+                                    self.render_markdown(
+                                        md.clone(),
+                                        user_message_markdown_style(window, cx),
+                                    )
+                                    .into_any_element()
+                                })
+                            },
+                        ),
                 )
                 .into_any(),
             AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
@@ -1035,7 +830,7 @@ impl AcpThreadView {
         };
 
         let is_generating = matches!(thread.read(cx).status(), ThreadStatus::Generating);
-        if index == total_entries - 1 && !is_generating {
+        let primary = if index == total_entries - 1 && !is_generating {
             v_flex()
                 .w_full()
                 .child(primary)
@@ -1043,6 +838,28 @@ impl AcpThreadView {
                 .into_any_element()
         } else {
             primary
+        };
+
+        if let Some(editing) = self.editing_message.as_ref()
+            && editing.index < index
+        {
+            let backdrop = div()
+                .id(("backdrop", index))
+                .size_full()
+                .absolute()
+                .inset_0()
+                .bg(cx.theme().colors().panel_background)
+                .opacity(0.8)
+                .block_mouse_except_scroll()
+                .on_click(cx.listener(Self::cancel_editing));
+
+            div()
+                .relative()
+                .child(backdrop)
+                .child(primary)
+                .into_any_element()
+        } else {
+            primary
         }
     }
 
@@ -2561,34 +2378,7 @@ impl AcpThreadView {
                     .size_full()
                     .pt_1()
                     .pr_2p5()
-                    .child(div().flex_1().child({
-                        let settings = ThemeSettings::get_global(cx);
-                        let font_size = TextSize::Small
-                            .rems(cx)
-                            .to_pixels(settings.agent_font_size(cx));
-                        let line_height = settings.buffer_line_height.value() * font_size;
-
-                        let text_style = TextStyle {
-                            color: cx.theme().colors().text,
-                            font_family: settings.buffer_font.family.clone(),
-                            font_fallbacks: settings.buffer_font.fallbacks.clone(),
-                            font_features: settings.buffer_font.features.clone(),
-                            font_size: font_size.into(),
-                            line_height: line_height.into(),
-                            ..Default::default()
-                        };
-
-                        EditorElement::new(
-                            &self.message_editor,
-                            EditorStyle {
-                                background: editor_bg_color,
-                                local_player: cx.theme().players().local(),
-                                text: text_style,
-                                syntax: cx.theme().syntax().clone(),
-                                ..Default::default()
-                            },
-                        )
-                    }))
+                    .child(self.message_editor.clone())
                     .child(
                         h_flex()
                             .absolute()
@@ -2633,6 +2423,129 @@ impl AcpThreadView {
             .into_any()
     }
 
+    fn start_editing_message(&mut self, index: usize, window: &mut Window, cx: &mut Context<Self>) {
+        let Some(thread) = self.thread() else {
+            return;
+        };
+        let Some(AgentThreadEntry::UserMessage(message)) = thread.read(cx).entries().get(index)
+        else {
+            return;
+        };
+        let Some(message_id) = message.id.clone() else {
+            return;
+        };
+
+        self.list_state.scroll_to_reveal_item(index);
+
+        let chunks = message.chunks.clone();
+        let editor = cx.new(|cx| {
+            let mut editor = MessageEditor::new(
+                self.workspace.clone(),
+                self.project.clone(),
+                self.thread_store.clone(),
+                self.text_thread_store.clone(),
+                editor::EditorMode::AutoHeight {
+                    min_lines: 1,
+                    max_lines: None,
+                },
+                window,
+                cx,
+            );
+            editor.set_message(&chunks, window, cx);
+            editor
+        });
+        let subscription =
+            cx.subscribe_in(&editor, window, |this, _, event, window, cx| match event {
+                MessageEditorEvent::Send => {
+                    this.regenerate(&Default::default(), window, cx);
+                }
+                MessageEditorEvent::Cancel => {
+                    this.cancel_editing(&Default::default(), window, cx);
+                }
+            });
+        editor.focus_handle(cx).focus(window);
+
+        self.editing_message.replace(EditingMessage {
+            index: index,
+            message_id: message_id.clone(),
+            editor,
+            _subscription: subscription,
+        });
+        cx.notify();
+    }
+
+    fn render_edit_message_editor(&self, editing: &EditingMessage, cx: &Context<Self>) -> Div {
+        v_flex()
+            .w_full()
+            .gap_2()
+            .child(editing.editor.clone())
+            .child(
+                h_flex()
+                    .gap_1()
+                    .child(
+                        Icon::new(IconName::Warning)
+                            .color(Color::Warning)
+                            .size(IconSize::XSmall),
+                    )
+                    .child(
+                        Label::new("Editing will restart the thread from this point.")
+                            .color(Color::Muted)
+                            .size(LabelSize::XSmall),
+                    )
+                    .child(self.render_editing_message_editor_buttons(editing, cx)),
+            )
+    }
+
+    fn render_editing_message_editor_buttons(
+        &self,
+        editing: &EditingMessage,
+        cx: &Context<Self>,
+    ) -> Div {
+        h_flex()
+            .gap_0p5()
+            .flex_1()
+            .justify_end()
+            .child(
+                IconButton::new("cancel-edit-message", IconName::Close)
+                    .shape(ui::IconButtonShape::Square)
+                    .icon_color(Color::Error)
+                    .icon_size(IconSize::Small)
+                    .tooltip({
+                        let focus_handle = editing.editor.focus_handle(cx);
+                        move |window, cx| {
+                            Tooltip::for_action_in(
+                                "Cancel Edit",
+                                &menu::Cancel,
+                                &focus_handle,
+                                window,
+                                cx,
+                            )
+                        }
+                    })
+                    .on_click(cx.listener(Self::cancel_editing)),
+            )
+            .child(
+                IconButton::new("confirm-edit-message", IconName::Return)
+                    .disabled(editing.editor.read(cx).is_empty(cx))
+                    .shape(ui::IconButtonShape::Square)
+                    .icon_color(Color::Muted)
+                    .icon_size(IconSize::Small)
+                    .tooltip({
+                        let focus_handle = editing.editor.focus_handle(cx);
+                        move |window, cx| {
+                            Tooltip::for_action_in(
+                                "Regenerate",
+                                &menu::Confirm,
+                                &focus_handle,
+                                window,
+                                cx,
+                            )
+                        }
+                    })
+                    .on_click(cx.listener(Self::regenerate)),
+            )
+    }
+
     fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
         if self.thread().map_or(true, |thread| {
             thread.read(cx).status() == ThreadStatus::Idle
@@ -2649,7 +2562,7 @@ impl AcpThreadView {
                     button.tooltip(Tooltip::text("Type a message to submit"))
                 })
                 .on_click(cx.listener(|this, _, window, cx| {
-                    this.chat(&Chat, window, cx);
+                    this.send(window, cx);
                 }))
                 .into_any_element()
         } else {
@@ -2659,7 +2572,7 @@ impl AcpThreadView {
                 .tooltip(move |window, cx| {
                     Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
                 })
-                .on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
+                .on_click(cx.listener(|this, _event, _, cx| this.cancel_generation(cx)))
                 .into_any_element()
         }
     }
@@ -2723,10 +2636,10 @@ impl AcpThreadView {
 
         if let Some(mention) = MentionUri::parse(&url).log_err() {
             workspace.update(cx, |workspace, cx| match mention {
-                MentionUri::File(path) => {
+                MentionUri::File { abs_path, .. } => {
                     let project = workspace.project();
                     let Some((path, entry)) = project.update(cx, |project, cx| {
-                        let path = project.find_project_path(path, cx)?;
+                        let path = project.find_project_path(abs_path, cx)?;
                         let entry = project.entry_for_path(&path, cx)?;
                         Some((path, entry))
                     }) else {
@@ -3175,57 +3088,11 @@ impl AcpThreadView {
         paths: Vec<project::ProjectPath>,
         _added_worktrees: Vec<Entity<project::Worktree>>,
         window: &mut Window,
-        cx: &mut Context<'_, Self>,
+        cx: &mut Context<Self>,
     ) {
-        let buffer = self.message_editor.read(cx).buffer().clone();
-        let Some((&excerpt_id, _, _)) = buffer.read(cx).snapshot(cx).as_singleton() else {
-            return;
-        };
-        let Some(buffer) = buffer.read(cx).as_singleton() else {
-            return;
-        };
-        for path in paths {
-            let Some(entry) = self.project.read(cx).entry_for_path(&path, cx) else {
-                continue;
-            };
-            let Some(abs_path) = self.project.read(cx).absolute_path(&path, cx) else {
-                continue;
-            };
-
-            let anchor = buffer.update(cx, |buffer, _cx| buffer.anchor_before(buffer.len()));
-            let path_prefix = abs_path
-                .file_name()
-                .unwrap_or(path.path.as_os_str())
-                .display()
-                .to_string();
-            let Some(completion) = ContextPickerCompletionProvider::completion_for_path(
-                path,
-                &path_prefix,
-                false,
-                entry.is_dir(),
-                excerpt_id,
-                anchor..anchor,
-                self.message_editor.clone(),
-                self.mention_set.clone(),
-                self.project.clone(),
-                cx,
-            ) else {
-                continue;
-            };
-
-            self.message_editor.update(cx, |message_editor, cx| {
-                message_editor.edit(
-                    [(
-                        multi_buffer::Anchor::max()..multi_buffer::Anchor::max(),
-                        completion.new_text,
-                    )],
-                    cx,
-                );
-            });
-            if let Some(confirm) = completion.confirm.clone() {
-                confirm(CompletionIntent::Complete, window, cx);
-            }
-        }
+        self.message_editor.update(cx, |message_editor, cx| {
+            message_editor.insert_dragged_files(paths, window, cx);
+        })
     }
 }
 
@@ -3242,9 +3109,6 @@ impl Render for AcpThreadView {
         v_flex()
             .size_full()
             .key_context("AcpThread")
-            .on_action(cx.listener(Self::chat))
-            .on_action(cx.listener(Self::previous_history_message))
-            .on_action(cx.listener(Self::next_history_message))
             .on_action(cx.listener(Self::open_agent_diff))
             .bg(cx.theme().colors().panel_background)
             .child(match &self.thread_state {
@@ -3540,13 +3404,16 @@ fn terminal_command_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 }
 
 #[cfg(test)]
-mod tests {
+pub(crate) mod tests {
+    use std::{path::Path, sync::Arc};
+
     use agent::{TextThreadStore, ThreadStore};
     use agent_client_protocol::SessionId;
     use editor::EditorSettings;
     use fs::FakeFs;
     use futures::future::try_join_all;
     use gpui::{SemanticVersion, TestAppContext, VisualTestContext};
+    use parking_lot::Mutex;
     use rand::Rng;
     use settings::SettingsStore;
 
@@ -3576,7 +3443,7 @@ mod tests {
         cx.deactivate_window();
 
         thread_view.update_in(cx, |thread_view, window, cx| {
-            thread_view.chat(&Chat, window, cx);
+            thread_view.send(window, cx);
         });
 
         cx.run_until_parked();
@@ -3603,7 +3470,7 @@ mod tests {
         cx.deactivate_window();
 
         thread_view.update_in(cx, |thread_view, window, cx| {
-            thread_view.chat(&Chat, window, cx);
+            thread_view.send(window, cx);
         });
 
         cx.run_until_parked();
@@ -3649,7 +3516,7 @@ mod tests {
         cx.deactivate_window();
 
         thread_view.update_in(cx, |thread_view, window, cx| {
-            thread_view.chat(&Chat, window, cx);
+            thread_view.send(window, cx);
         });
 
         cx.run_until_parked();
@@ -3683,9 +3550,6 @@ mod tests {
                     project,
                     thread_store.clone(),
                     text_thread_store.clone(),
-                    Rc::new(RefCell::new(MessageHistory::default())),
-                    1,
-                    None,
                     window,
                     cx,
                 )

crates/agent_ui/src/agent_panel.rs 🔗

@@ -1,4 +1,3 @@
-use std::cell::RefCell;
 use std::ops::{Not, Range};
 use std::path::Path;
 use std::rc::Rc;
@@ -11,7 +10,6 @@ use serde::{Deserialize, Serialize};
 
 use crate::NewExternalAgentThread;
 use crate::agent_diff::AgentDiffThread;
-use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
 use crate::{
     AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
     DeleteRecentlyOpenThread, ExpandMessageEditor, Follow, InlineAssistant, NewTextThread,
@@ -477,8 +475,6 @@ pub struct AgentPanel {
     configuration_subscription: Option<Subscription>,
     local_timezone: UtcOffset,
     active_view: ActiveView,
-    acp_message_history:
-        Rc<RefCell<crate::acp::MessageHistory<Vec<agent_client_protocol::ContentBlock>>>>,
     previous_view: Option<ActiveView>,
     history_store: Entity<HistoryStore>,
     history: Entity<ThreadHistory>,
@@ -766,7 +762,6 @@ impl AgentPanel {
             .unwrap(),
             inline_assist_context_store,
             previous_view: None,
-            acp_message_history: Default::default(),
             history_store: history_store.clone(),
             history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
             hovered_recent_history_item: None,
@@ -824,7 +819,9 @@ impl AgentPanel {
                 thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
             }
             ActiveView::ExternalAgentThread { thread_view, .. } => {
-                thread_view.update(cx, |thread_element, cx| thread_element.cancel(cx));
+                thread_view.update(cx, |thread_element, cx| {
+                    thread_element.cancel_generation(cx)
+                });
             }
             ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
         }
@@ -963,7 +960,6 @@ impl AgentPanel {
     ) {
         let workspace = self.workspace.clone();
         let project = self.project.clone();
-        let message_history = self.acp_message_history.clone();
         let fs = self.fs.clone();
 
         const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
@@ -1016,9 +1012,6 @@ impl AgentPanel {
                         project,
                         thread_store.clone(),
                         text_thread_store.clone(),
-                        message_history,
-                        MIN_EDITOR_LINES,
-                        Some(MAX_EDITOR_LINES),
                         window,
                         cx,
                     )
@@ -1575,8 +1568,6 @@ impl AgentPanel {
             self.active_view = new_view;
         }
 
-        self.acp_message_history.borrow_mut().reset_position();
-
         self.focus_handle(cx).focus(window);
     }
 

crates/zed_actions/src/lib.rs 🔗

@@ -285,10 +285,6 @@ pub mod agent {
             ResetOnboarding,
             /// Starts a chat conversation with the agent.
             Chat,
-            /// Displays the previous message in the history.
-            PreviousHistoryMessage,
-            /// Displays the next message in the history.
-            NextHistoryMessage,
             /// Toggles the language model selector dropdown.
             #[action(deprecated_aliases = ["assistant::ToggleModelSelector", "assistant2::ToggleModelSelector"])]
             ToggleModelSelector