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

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
This commit is contained in:
Agus Zubiaga
2025-07-03 13:01:24 -03:00
3 changed files with 186 additions and 121 deletions

View File

@@ -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)),
},
acp::AssistantMessageChunk::Thought { chunk } => Self::Thought {
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(),
},
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!();
};

View File

@@ -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,
})

View File

@@ -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,