Merge branch 'acp' of github.com:zed-industries/zed into acp

Agus Zubiaga and Mikayla Maki created

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>

Change summary

crates/acp/src/acp.rs         | 179 ++++++++++++++++++++----------------
crates/acp/src/server.rs      |  12 +-
crates/acp/src/thread_view.rs | 114 ++++++++++++++++-------
3 files changed, 185 insertions(+), 120 deletions(-)

Detailed changes

crates/acp/src/acp.rs 🔗

@@ -1,7 +1,7 @@
 mod server;
 mod thread_view;
 
-use agentic_coding_protocol::{self as acp, Role};
+use agentic_coding_protocol::{self as acp};
 use anyhow::{Context as _, Result};
 use buffer_diff::BufferDiff;
 use chrono::{DateTime, Utc};
@@ -39,15 +39,13 @@ pub struct FileContent {
 }
 
 #[derive(Clone, Debug, Eq, PartialEq)]
-pub struct Message {
-    pub role: acp::Role,
-    pub chunks: Vec<MessageChunk>,
+pub struct UserMessage {
+    pub chunks: Vec<UserMessageChunk>,
 }
 
-impl Message {
-    fn into_acp(self, cx: &App) -> acp::Message {
-        acp::Message {
-            role: self.role,
+impl UserMessage {
+    fn into_acp(self, cx: &App) -> acp::UserMessage {
+        acp::UserMessage {
             chunks: self
                 .chunks
                 .into_iter()
@@ -58,7 +56,7 @@ impl Message {
 }
 
 #[derive(Clone, Debug, Eq, PartialEq)]
-pub enum MessageChunk {
+pub enum UserMessageChunk {
     Text {
         chunk: Entity<Markdown>,
     },
@@ -82,33 +80,57 @@ pub enum MessageChunk {
     },
 }
 
-impl MessageChunk {
+impl UserMessageChunk {
+    pub fn into_acp(self, cx: &App) -> acp::UserMessageChunk {
+        match self {
+            Self::Text { chunk } => acp::UserMessageChunk::Text {
+                chunk: chunk.read(cx).source().to_string(),
+            },
+            Self::File { .. } => todo!(),
+            Self::Directory { .. } => todo!(),
+            Self::Symbol { .. } => todo!(),
+            Self::Fetch { .. } => todo!(),
+        }
+    }
+
+    pub fn from_str(chunk: &str, language_registry: Arc<LanguageRegistry>, cx: &mut App) -> Self {
+        Self::Text {
+            chunk: cx.new(|cx| {
+                Markdown::new(chunk.to_owned().into(), Some(language_registry), None, cx)
+            }),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct AssistantMessage {
+    pub chunks: Vec<AssistantMessageChunk>,
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum AssistantMessageChunk {
+    Text { chunk: Entity<Markdown> },
+    Thought { chunk: Entity<Markdown> },
+}
+
+impl AssistantMessageChunk {
     pub fn from_acp(
-        chunk: acp::MessageChunk,
+        chunk: acp::AssistantMessageChunk,
         language_registry: Arc<LanguageRegistry>,
         cx: &mut App,
     ) -> Self {
         match chunk {
-            acp::MessageChunk::Text { chunk } => MessageChunk::Text {
+            acp::AssistantMessageChunk::Text { chunk } => Self::Text {
                 chunk: cx.new(|cx| Markdown::new(chunk.into(), Some(language_registry), None, cx)),
             },
-        }
-    }
-
-    pub fn into_acp(self, cx: &App) -> acp::MessageChunk {
-        match self {
-            MessageChunk::Text { chunk } => acp::MessageChunk::Text {
-                chunk: chunk.read(cx).source().to_string(),
+            acp::AssistantMessageChunk::Thought { chunk } => Self::Thought {
+                chunk: cx.new(|cx| Markdown::new(chunk.into(), Some(language_registry), None, cx)),
             },
-            MessageChunk::File { .. } => todo!(),
-            MessageChunk::Directory { .. } => todo!(),
-            MessageChunk::Symbol { .. } => todo!(),
-            MessageChunk::Fetch { .. } => todo!(),
         }
     }
 
     pub fn from_str(chunk: &str, language_registry: Arc<LanguageRegistry>, cx: &mut App) -> Self {
-        MessageChunk::Text {
+        Self::Text {
             chunk: cx.new(|cx| {
                 Markdown::new(chunk.to_owned().into(), Some(language_registry), None, cx)
             }),
@@ -118,7 +140,8 @@ impl MessageChunk {
 
 #[derive(Debug)]
 pub enum AgentThreadEntryContent {
-    Message(Message),
+    UserMessage(UserMessage),
+    AssistantMessage(AssistantMessage),
     ToolCall(ToolCall),
 }
 
@@ -434,44 +457,53 @@ impl AcpThread {
         id
     }
 
-    pub fn push_assistant_chunk(&mut self, chunk: acp::MessageChunk, cx: &mut Context<Self>) {
+    pub fn push_assistant_chunk(
+        &mut self,
+        chunk: acp::AssistantMessageChunk,
+        cx: &mut Context<Self>,
+    ) {
         let entries_len = self.entries.len();
         if let Some(last_entry) = self.entries.last_mut()
-            && let AgentThreadEntryContent::Message(Message {
-                ref mut chunks,
-                role: Role::Assistant,
-            }) = last_entry.content
+            && let AgentThreadEntryContent::AssistantMessage(AssistantMessage { ref mut chunks }) =
+                last_entry.content
         {
             cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1));
 
-            if let (
-                Some(MessageChunk::Text { chunk: old_chunk }),
-                acp::MessageChunk::Text { chunk: new_chunk },
-            ) = (chunks.last_mut(), &chunk)
-            {
-                old_chunk.update(cx, |old_chunk, cx| {
-                    old_chunk.append(&new_chunk, cx);
-                });
-            } else {
-                chunks.push(MessageChunk::from_acp(
-                    chunk,
-                    self.project.read(cx).languages().clone(),
-                    cx,
-                ));
+            match (chunks.last_mut(), &chunk) {
+                (
+                    Some(AssistantMessageChunk::Text { chunk: old_chunk }),
+                    acp::AssistantMessageChunk::Text { chunk: new_chunk },
+                )
+                | (
+                    Some(AssistantMessageChunk::Thought { chunk: old_chunk }),
+                    acp::AssistantMessageChunk::Thought { chunk: new_chunk },
+                ) => {
+                    old_chunk.update(cx, |old_chunk, cx| {
+                        old_chunk.append(&new_chunk, cx);
+                    });
+                }
+                _ => {
+                    chunks.push(AssistantMessageChunk::from_acp(
+                        chunk,
+                        self.project.read(cx).languages().clone(),
+                        cx,
+                    ));
+                }
             }
+        } else {
+            let chunk = AssistantMessageChunk::from_acp(
+                chunk,
+                self.project.read(cx).languages().clone(),
+                cx,
+            );
 
-            return;
+            self.push_entry(
+                AgentThreadEntryContent::AssistantMessage(AssistantMessage {
+                    chunks: vec![chunk],
+                }),
+                cx,
+            );
         }
-
-        let chunk = MessageChunk::from_acp(chunk, self.project.read(cx).languages().clone(), cx);
-
-        self.push_entry(
-            AgentThreadEntryContent::Message(Message {
-                role: Role::Assistant,
-                chunks: vec![chunk],
-            }),
-            cx,
-        );
     }
 
     pub fn request_tool_call(
@@ -632,7 +664,8 @@ impl AcpThread {
                     | ToolCallStatus::Rejected
                     | ToolCallStatus::Canceled => continue,
                 },
-                AgentThreadEntryContent::Message(_) => {
+                AgentThreadEntryContent::UserMessage(_)
+                | AgentThreadEntryContent::AssistantMessage(_) => {
                     // Reached the beginning of the turn
                     return false;
                 }
@@ -648,13 +681,12 @@ impl AcpThread {
     ) -> impl use<> + Future<Output = Result<()>> {
         let agent = self.server.clone();
         let id = self.id.clone();
-
-        let chunk = MessageChunk::from_str(message, self.project.read(cx).languages().clone(), cx);
-        let message = Message {
-            role: Role::User,
+        let chunk =
+            UserMessageChunk::from_str(message, self.project.read(cx).languages().clone(), cx);
+        let message = UserMessage {
             chunks: vec![chunk],
         };
-        self.push_entry(AgentThreadEntryContent::Message(message.clone()), cx);
+        self.push_entry(AgentThreadEntryContent::UserMessage(message.clone()), cx);
         let acp_message = message.into_acp(cx);
 
         let (tx, rx) = oneshot::channel();
@@ -777,17 +809,11 @@ mod tests {
             assert_eq!(thread.entries.len(), 2);
             assert!(matches!(
                 thread.entries[0].content,
-                AgentThreadEntryContent::Message(Message {
-                    role: Role::User,
-                    ..
-                })
+                AgentThreadEntryContent::UserMessage(_)
             ));
             assert!(matches!(
                 thread.entries[1].content,
-                AgentThreadEntryContent::Message(Message {
-                    role: Role::Assistant,
-                    ..
-                })
+                AgentThreadEntryContent::AssistantMessage(_)
             ));
         });
     }
@@ -818,7 +844,7 @@ mod tests {
             .unwrap();
         thread.read_with(cx, |thread, _cx| {
             assert!(matches!(
-                &thread.entries()[1].content,
+                &thread.entries()[2].content,
                 AgentThreadEntryContent::ToolCall(ToolCall {
                     status: ToolCallStatus::Allowed { .. },
                     ..
@@ -826,11 +852,8 @@ mod tests {
             ));
 
             assert!(matches!(
-                thread.entries[2].content,
-                AgentThreadEntryContent::Message(Message {
-                    role: Role::Assistant,
-                    ..
-                })
+                thread.entries[3].content,
+                AgentThreadEntryContent::AssistantMessage(_)
             ));
         });
     }
@@ -860,7 +883,7 @@ mod tests {
                         ..
                     },
                 ..
-            }) = &thread.entries()[1].content
+            }) = &thread.entries()[2].content
             else {
                 panic!();
             };
@@ -874,7 +897,7 @@ mod tests {
             thread.authorize_tool_call(tool_call_id, acp::ToolCallConfirmationOutcome::Allow, cx);
 
             assert!(matches!(
-                &thread.entries()[1].content,
+                &thread.entries()[2].content,
                 AgentThreadEntryContent::ToolCall(ToolCall {
                     status: ToolCallStatus::Allowed { .. },
                     ..
@@ -889,7 +912,7 @@ mod tests {
                 content: Some(ToolCallContent::Markdown { markdown }),
                 status: ToolCallStatus::Allowed { .. },
                 ..
-            }) = &thread.entries()[1].content
+            }) = &thread.entries()[2].content
             else {
                 panic!();
             };

crates/acp/src/server.rs 🔗

@@ -47,10 +47,10 @@ impl AcpClientDelegate {
 
 #[async_trait(?Send)]
 impl acp::Client for AcpClientDelegate {
-    async fn stream_message_chunk(
+    async fn stream_assistant_message_chunk(
         &self,
-        params: acp::StreamMessageChunkParams,
-    ) -> Result<acp::StreamMessageChunkResponse> {
+        params: acp::StreamAssistantMessageChunkParams,
+    ) -> Result<acp::StreamAssistantMessageChunkResponse> {
         let cx = &mut self.cx.clone();
 
         cx.update(|cx| {
@@ -59,7 +59,7 @@ impl acp::Client for AcpClientDelegate {
             });
         })?;
 
-        Ok(acp::StreamMessageChunkResponse)
+        Ok(acp::StreamAssistantMessageChunkResponse)
     }
 
     async fn request_tool_call_confirmation(
@@ -200,11 +200,11 @@ impl AcpServer {
     pub async fn send_message(
         &self,
         thread_id: ThreadId,
-        message: acp::Message,
+        message: acp::UserMessage,
         _cx: &mut AsyncApp,
     ) -> Result<()> {
         self.connection
-            .request(acp::SendMessageParams {
+            .request(acp::SendUserMessageParams {
                 thread_id: thread_id.clone().into(),
                 message,
             })

crates/acp/src/thread_view.rs 🔗

@@ -23,9 +23,9 @@ use util::{ResultExt, paths};
 use zed_actions::agent::Chat;
 
 use crate::{
-    AcpServer, AcpThread, AcpThreadEvent, AgentThreadEntryContent, Diff, MessageChunk, Role,
-    ThreadEntry, ThreadStatus, ToolCall, ToolCallConfirmation, ToolCallContent, ToolCallId,
-    ToolCallStatus,
+    AcpServer, AcpThread, AcpThreadEvent, AgentThreadEntryContent, AssistantMessage,
+    AssistantMessageChunk, Diff, ThreadEntry, ThreadStatus, ToolCall, ToolCallConfirmation,
+    ToolCallContent, ToolCallId, ToolCallStatus, UserMessageChunk,
 };
 
 pub struct AcpThreadView {
@@ -392,45 +392,51 @@ impl AcpThreadView {
         cx: &Context<Self>,
     ) -> AnyElement {
         match &entry.content {
-            AgentThreadEntryContent::Message(message) => {
-                let style = if message.role == Role::User {
-                    user_message_markdown_style(window, cx)
-                } else {
-                    default_markdown_style(window, cx)
-                };
+            AgentThreadEntryContent::UserMessage(message) => {
+                let style = user_message_markdown_style(window, cx);
+                let message_body = div().children(message.chunks.iter().map(|chunk| match chunk {
+                    UserMessageChunk::Text { chunk } => {
+                        // todo!() open link
+                        MarkdownElement::new(chunk.clone(), style.clone())
+                    }
+                    _ => todo!(),
+                }));
+                div()
+                    .p_2()
+                    .pt_5()
+                    .child(
+                        div()
+                            .text_xs()
+                            .p_3()
+                            .bg(cx.theme().colors().editor_background)
+                            .rounded_lg()
+                            .shadow_md()
+                            .border_1()
+                            .border_color(cx.theme().colors().border)
+                            .child(message_body),
+                    )
+                    .into_any()
+            }
+            AgentThreadEntryContent::AssistantMessage(AssistantMessage { chunks }) => {
+                let style = default_markdown_style(window, cx);
                 let message_body = div()
-                    .children(message.chunks.iter().map(|chunk| match chunk {
-                        MessageChunk::Text { chunk } => {
+                    .children(chunks.iter().map(|chunk| match chunk {
+                        AssistantMessageChunk::Text { chunk } => {
                             // todo!() open link
-                            MarkdownElement::new(chunk.clone(), style.clone())
+                            MarkdownElement::new(chunk.clone(), style.clone()).into_any_element()
+                        }
+                        AssistantMessageChunk::Thought { chunk } => {
+                            self.render_thinking_block(chunk.clone(), window, cx)
                         }
-                        _ => todo!(),
                     }))
                     .into_any();
 
-                match message.role {
-                    Role::User => div()
-                        .p_2()
-                        .pt_5()
-                        .child(
-                            div()
-                                .text_xs()
-                                .p_3()
-                                .bg(cx.theme().colors().editor_background)
-                                .rounded_lg()
-                                .shadow_md()
-                                .border_1()
-                                .border_color(cx.theme().colors().border)
-                                .child(message_body),
-                        )
-                        .into_any(),
-                    Role::Assistant => div()
-                        .text_ui(cx)
-                        .p_5()
-                        .pt_2()
-                        .child(message_body)
-                        .into_any(),
-                }
+                div()
+                    .text_ui(cx)
+                    .p_5()
+                    .pt_2()
+                    .child(message_body)
+                    .into_any()
             }
             AgentThreadEntryContent::ToolCall(tool_call) => div()
                 .px_2()
@@ -440,6 +446,42 @@ impl AcpThreadView {
         }
     }
 
+    fn render_thinking_block(
+        &self,
+        chunk: Entity<Markdown>,
+        window: &Window,
+        cx: &Context<Self>,
+    ) -> AnyElement {
+        v_flex()
+            .mt_neg_2()
+            .mb_1p5()
+            .child(
+                h_flex().group("disclosure-header").justify_between().child(
+                    h_flex()
+                        .gap_1p5()
+                        .child(
+                            Icon::new(IconName::LightBulb)
+                                .size(IconSize::XSmall)
+                                .color(Color::Muted),
+                        )
+                        .child(Label::new("Thinking").size(LabelSize::Small)),
+                ),
+            )
+            .child(div().relative().rounded_b_lg().mt_2().pl_4().child(
+                div().max_h_20().text_ui_sm(cx).overflow_hidden().child(
+                    // todo! url click
+                    MarkdownElement::new(chunk, default_markdown_style(window, cx)),
+                    // .on_url_click({
+                    //     let workspace = self.workspace.clone();
+                    //     move |text, window, cx| {
+                    //         open_markdown_link(text, workspace.clone(), window, cx);
+                    //     }
+                    // }),
+                ),
+            ))
+            .into_any_element()
+    }
+
     fn render_tool_call(
         &self,
         entry_ix: usize,