Cargo.lock 🔗
@@ -17,11 +17,13 @@ dependencies = [
"futures 0.3.31",
"gpui",
"language",
+ "markdown",
"parking_lot",
"project",
"serde_json",
"settings",
"smol",
+ "theme",
"ui",
"util",
"uuid",
Agus Zubiaga and Conrad Irwin created
Co-authored-by: Conrad Irwin <conrad.irwin@gmail.com>
Cargo.lock | 2
crates/acp/Cargo.toml | 3
crates/acp/src/acp.rs | 101 ++++++++++++++++------
crates/acp/src/server.rs | 42 ++-------
crates/acp/src/thread_view.rs | 160 +++++++++++++++++++++++++++++++++---
5 files changed, 230 insertions(+), 78 deletions(-)
@@ -17,11 +17,13 @@ dependencies = [
"futures 0.3.31",
"gpui",
"language",
+ "markdown",
"parking_lot",
"project",
"serde_json",
"settings",
"smol",
+ "theme",
"ui",
"util",
"uuid",
@@ -26,9 +26,12 @@ editor.workspace = true
futures.workspace = true
gpui.workspace = true
language.workspace = true
+markdown.workspace = true
parking_lot.workspace = true
project.workspace = true
+settings.workspace = true
smol.workspace = true
+theme.workspace = true
ui.workspace = true
util.workspace = true
uuid.workspace = true
@@ -1,11 +1,15 @@
mod server;
mod thread_view;
+use agentic_coding_protocol::{self as acp, Role};
use anyhow::Result;
use chrono::{DateTime, Utc};
-use gpui::{Context, Entity, SharedString, Task};
+use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task};
+use language::LanguageRegistry;
+use markdown::Markdown;
use project::Project;
use std::{ops::Range, path::PathBuf, sync::Arc};
+use ui::App;
pub use server::AcpServer;
pub use thread_view::AcpThreadView;
@@ -30,23 +34,29 @@ pub struct FileContent {
pub content: SharedString,
}
-#[derive(Copy, Clone, Debug, Eq, PartialEq)]
-pub enum Role {
- User,
- Assistant,
-}
-
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct Message {
- pub role: Role,
+ pub role: acp::Role,
pub chunks: Vec<MessageChunk>,
}
+impl Message {
+ fn into_acp(self, cx: &App) -> acp::Message {
+ acp::Message {
+ role: self.role,
+ chunks: self
+ .chunks
+ .into_iter()
+ .map(|chunk| chunk.into_acp(cx))
+ .collect(),
+ }
+ }
+}
+
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum MessageChunk {
Text {
- // todo! should it be shared string? what about streaming?
- chunk: String,
+ chunk: Entity<Markdown>,
},
File {
content: FileContent,
@@ -68,10 +78,36 @@ pub enum MessageChunk {
},
}
-impl From<&str> for MessageChunk {
- fn from(chunk: &str) -> Self {
+impl MessageChunk {
+ pub fn from_acp(
+ chunk: acp::MessageChunk,
+ language_registry: Arc<LanguageRegistry>,
+ cx: &mut App,
+ ) -> Self {
+ match chunk {
+ acp::MessageChunk::Text { chunk } => MessageChunk::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(),
+ },
+ 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 {
- chunk: chunk.to_string().into(),
+ chunk: cx.new(|cx| {
+ Markdown::new(chunk.to_owned().into(), Some(language_registry), None, cx)
+ }),
}
}
}
@@ -156,7 +192,7 @@ impl AcpThread {
cx.emit(AcpThreadEvent::NewEntry)
}
- pub fn push_assistant_chunk(&mut self, chunk: MessageChunk, cx: &mut Context<Self>) {
+ pub fn push_assistant_chunk(&mut self, chunk: acp::MessageChunk, cx: &mut Context<Self>) {
if let Some(last_entry) = self.entries.last_mut()
&& let AgentThreadEntryContent::Message(Message {
ref mut chunks,
@@ -167,33 +203,46 @@ impl AcpThread {
if let (
Some(MessageChunk::Text { chunk: old_chunk }),
- MessageChunk::Text { chunk: new_chunk },
+ acp::MessageChunk::Text { chunk: new_chunk },
) = (chunks.last_mut(), &chunk)
{
- old_chunk.push_str(&new_chunk);
+ old_chunk.update(cx, |old_chunk, cx| {
+ old_chunk.append(&new_chunk, cx);
+ });
} else {
- chunks.push(chunk);
- return cx.notify();
+ chunks.push(MessageChunk::from_acp(
+ chunk,
+ self.project.read(cx).languages().clone(),
+ cx,
+ ));
}
return;
}
+ 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.notify();
+ cx,
+ );
}
- pub fn send(&mut self, message: Message, cx: &mut Context<Self>) -> Task<Result<()>> {
+ pub fn send(&mut self, message: &str, cx: &mut Context<Self>) -> Task<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,
+ chunks: vec![chunk],
+ };
self.push_entry(AgentThreadEntryContent::Message(message.clone()), cx);
+ let acp_message = message.into_acp(cx);
cx.spawn(async move |_, cx| {
- agent.send_message(id, message, cx).await?;
+ agent.send_message(id, acp_message, cx).await?;
Ok(())
})
}
@@ -237,13 +286,7 @@ mod tests {
thread
.update(cx, |thread, cx| {
thread.send(
- Message {
- role: Role::User,
- chunks: vec![
- "Read the '/private/tmp/foo' file and output all of its contents."
- .into(),
- ],
- },
+ "Read the '/private/tmp/foo' file and output all of its contents.",
cx,
)
})
@@ -1,4 +1,4 @@
-use crate::{AcpThread, AgentThreadEntryContent, MessageChunk, Role, ThreadEntryId, ThreadId};
+use crate::{AcpThread, AgentThreadEntryContent, ThreadEntryId, ThreadId};
use agentic_coding_protocol as acp;
use anyhow::{Context as _, Result};
use async_trait::async_trait;
@@ -42,7 +42,7 @@ impl AcpClientDelegate {
&self,
thread_id: &ThreadId,
cx: &mut App,
- callback: impl FnMut(&mut AcpThread, &mut Context<AcpThread>) -> R,
+ callback: impl FnOnce(&mut AcpThread, &mut Context<AcpThread>) -> R,
) -> Option<R> {
let thread = self.threads.lock().get(&thread_id)?.clone();
let Some(thread) = thread.upgrade() else {
@@ -80,18 +80,11 @@ impl acp::Client for AcpClientDelegate {
&self,
params: acp::StreamMessageChunkParams,
) -> Result<acp::StreamMessageChunkResponse> {
- dbg!();
let cx = &mut self.cx.clone();
cx.update(|cx| {
self.update_thread(¶ms.thread_id.into(), cx, |thread, cx| {
- let acp::MessageChunk::Text { chunk } = ¶ms.chunk;
- thread.push_assistant_chunk(
- MessageChunk::Text {
- chunk: chunk.into(),
- },
- cx,
- )
+ thread.push_assistant_chunk(params.chunk, cx)
});
})?;
@@ -186,7 +179,10 @@ impl acp::Client for AcpClientDelegate {
})
}
- async fn glob_search(&self, request: acp::GlobSearchParams) -> Result<acp::GlobSearchResponse> {
+ async fn glob_search(
+ &self,
+ _request: acp::GlobSearchParams,
+ ) -> Result<acp::GlobSearchResponse> {
todo!()
}
}
@@ -238,31 +234,13 @@ impl AcpServer {
pub async fn send_message(
&self,
thread_id: ThreadId,
- message: crate::Message,
- cx: &mut AsyncApp,
+ message: acp::Message,
+ _cx: &mut AsyncApp,
) -> Result<()> {
self.connection
.request(acp::SendMessageParams {
thread_id: thread_id.clone().into(),
- message: acp::Message {
- role: match message.role {
- Role::User => acp::Role::User,
- Role::Assistant => acp::Role::Assistant,
- },
- chunks: message
- .chunks
- .into_iter()
- .map(|chunk| match chunk {
- MessageChunk::Text { chunk } => acp::MessageChunk::Text {
- chunk: chunk.into(),
- },
- MessageChunk::File { .. } => todo!(),
- MessageChunk::Directory { .. } => todo!(),
- MessageChunk::Symbol { .. } => todo!(),
- MessageChunk::Fetch { .. } => todo!(),
- })
- .collect(),
- },
+ message,
})
.await?;
Ok(())
@@ -1,18 +1,21 @@
+use std::rc::Rc;
+
use anyhow::Result;
use editor::{Editor, MultiBuffer};
use gpui::{
- App, Empty, Entity, Focusable, ListState, SharedString, Subscription, Window, div, list,
- prelude::*,
+ App, EdgesRefinement, Empty, Entity, Focusable, ListState, SharedString, StyleRefinement,
+ Subscription, TextStyleRefinement, UnderlineStyle, Window, div, list, prelude::*,
};
use gpui::{FocusHandle, Task};
use language::Buffer;
+use markdown::{HeadingLevelStyles, MarkdownElement, MarkdownStyle};
+use settings::Settings as _;
+use theme::ThemeSettings;
use ui::Tooltip;
use ui::prelude::*;
use zed_actions::agent::Chat;
-use crate::{
- AcpThread, AcpThreadEvent, AgentThreadEntryContent, Message, MessageChunk, Role, ThreadEntry,
-};
+use crate::{AcpThread, AcpThreadEvent, AgentThreadEntryContent, MessageChunk, Role, ThreadEntry};
pub struct AcpThreadView {
thread: Entity<AcpThread>,
@@ -93,13 +96,7 @@ impl AcpThreadView {
return;
}
- let task = self.thread.update(cx, |thread, cx| {
- let message = Message {
- role: Role::User,
- chunks: vec![MessageChunk::Text { chunk: text.into() }],
- };
- thread.send(message, cx)
- });
+ let task = self.thread.update(cx, |thread, cx| thread.send(&text, cx));
self.send_task = Some(cx.spawn(async move |this, cx| {
task.await?;
@@ -117,16 +114,21 @@ impl AcpThreadView {
fn render_entry(
&self,
entry: &ThreadEntry,
- _window: &mut Window,
+ window: &mut Window,
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)
+ };
let message_body = div()
.children(message.chunks.iter().map(|chunk| match chunk {
MessageChunk::Text { chunk } => {
- // todo! markdown
- Label::new(chunk.clone())
+ // todo!() open link
+ MarkdownElement::new(chunk.clone(), style.clone())
}
_ => todo!(),
}))
@@ -134,7 +136,8 @@ impl AcpThreadView {
match message.role {
Role::User => div()
- .my_1()
+ .text_xs()
+ .m_1()
.p_2()
.bg(cx.theme().colors().editor_background)
.rounded_lg()
@@ -143,7 +146,12 @@ impl AcpThreadView {
.border_color(cx.theme().colors().border)
.child(message_body)
.into_any(),
- Role::Assistant => message_body,
+ Role::Assistant => div()
+ .text_ui(cx)
+ .px_2()
+ .py_4()
+ .child(message_body)
+ .into_any(),
}
}
AgentThreadEntryContent::ReadFile { path, content: _ } => {
@@ -237,3 +245,121 @@ impl Render for AcpThreadView {
)
}
}
+
+fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
+ let mut style = default_markdown_style(window, cx);
+ let mut text_style = window.text_style();
+ let theme_settings = ThemeSettings::get_global(cx);
+
+ let buffer_font = theme_settings.buffer_font.family.clone();
+ let buffer_font_size = TextSize::Small.rems(cx);
+
+ text_style.refine(&TextStyleRefinement {
+ font_family: Some(buffer_font),
+ font_size: Some(buffer_font_size.into()),
+ ..Default::default()
+ });
+
+ style.base_text_style = text_style;
+ style
+}
+
+fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
+ let theme_settings = ThemeSettings::get_global(cx);
+ let colors = cx.theme().colors();
+ let ui_font_size = TextSize::Default.rems(cx);
+ let buffer_font_size = TextSize::Small.rems(cx);
+ let mut text_style = window.text_style();
+ let line_height = buffer_font_size * 1.75;
+
+ text_style.refine(&TextStyleRefinement {
+ font_family: Some(theme_settings.ui_font.family.clone()),
+ font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
+ font_features: Some(theme_settings.ui_font.features.clone()),
+ font_size: Some(ui_font_size.into()),
+ line_height: Some(line_height.into()),
+ color: Some(cx.theme().colors().text),
+ ..Default::default()
+ });
+
+ MarkdownStyle {
+ base_text_style: text_style.clone(),
+ syntax: cx.theme().syntax().clone(),
+ selection_background_color: cx.theme().colors().element_selection_background,
+ code_block_overflow_x_scroll: true,
+ table_overflow_x_scroll: true,
+ heading_level_styles: Some(HeadingLevelStyles {
+ h1: Some(TextStyleRefinement {
+ font_size: Some(rems(1.15).into()),
+ ..Default::default()
+ }),
+ h2: Some(TextStyleRefinement {
+ font_size: Some(rems(1.1).into()),
+ ..Default::default()
+ }),
+ h3: Some(TextStyleRefinement {
+ font_size: Some(rems(1.05).into()),
+ ..Default::default()
+ }),
+ h4: Some(TextStyleRefinement {
+ font_size: Some(rems(1.).into()),
+ ..Default::default()
+ }),
+ h5: Some(TextStyleRefinement {
+ font_size: Some(rems(0.95).into()),
+ ..Default::default()
+ }),
+ h6: Some(TextStyleRefinement {
+ font_size: Some(rems(0.875).into()),
+ ..Default::default()
+ }),
+ }),
+ code_block: StyleRefinement {
+ padding: EdgesRefinement {
+ top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
+ left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
+ right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
+ bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
+ },
+ background: Some(colors.editor_background.into()),
+ text: Some(TextStyleRefinement {
+ font_family: Some(theme_settings.buffer_font.family.clone()),
+ font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
+ font_features: Some(theme_settings.buffer_font.features.clone()),
+ font_size: Some(buffer_font_size.into()),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ inline_code: TextStyleRefinement {
+ font_family: Some(theme_settings.buffer_font.family.clone()),
+ font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
+ font_features: Some(theme_settings.buffer_font.features.clone()),
+ font_size: Some(buffer_font_size.into()),
+ background_color: Some(colors.editor_foreground.opacity(0.08)),
+ ..Default::default()
+ },
+ link: TextStyleRefinement {
+ background_color: Some(colors.editor_foreground.opacity(0.025)),
+ underline: Some(UnderlineStyle {
+ color: Some(colors.text_accent.opacity(0.5)),
+ thickness: px(1.),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ link_callback: Some(Rc::new(move |_url, _cx| {
+ // todo!()
+ // if MentionLink::is_valid(url) {
+ // let colors = cx.theme().colors();
+ // Some(TextStyleRefinement {
+ // background_color: Some(colors.element_background),
+ // ..Default::default()
+ // })
+ // } else {
+ None
+ // }
+ })),
+ ..Default::default()
+ }
+}