diff --git a/crates/acp/src/acp.rs b/crates/acp/src/acp.rs index bf1bb0bf58391b9a49121a90e98a45b8c63304db..50ea79d14a3fd6f4162dad557e22d67be5a7bfdb 100644 --- a/crates/acp/src/acp.rs +++ b/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, +pub struct UserMessage { + pub chunks: Vec, } -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, }, @@ -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, 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, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum AssistantMessageChunk { + Text { chunk: Entity }, + Thought { chunk: Entity }, +} + +impl AssistantMessageChunk { pub fn from_acp( - chunk: acp::MessageChunk, + chunk: acp::AssistantMessageChunk, language_registry: Arc, 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, 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) { + pub fn push_assistant_chunk( + &mut self, + chunk: acp::AssistantMessageChunk, + cx: &mut Context, + ) { 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> { 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!(); }; diff --git a/crates/acp/src/server.rs b/crates/acp/src/server.rs index 79bea3b5ba535a27db096b4dc83788c97d778f76..29025e9f3cf13a6362ec54405f81ac69dcd1097a 100644 --- a/crates/acp/src/server.rs +++ b/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 { + params: acp::StreamAssistantMessageChunkParams, + ) -> Result { 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, }) diff --git a/crates/acp/src/thread_view.rs b/crates/acp/src/thread_view.rs index 86e6bd93a060ce3b24d481e7152cca93e11d0655..b8b7b081aea7c6c0fec0d4ce6e64e1b68dc21a80 100644 --- a/crates/acp/src/thread_view.rs +++ b/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, ) -> 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, + window: &Window, + cx: &Context, + ) -> 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,