1use std::rc::Rc;
2
3use anyhow::Result;
4use editor::{Editor, MultiBuffer};
5use gpui::{
6 App, EdgesRefinement, Empty, Entity, Focusable, ListState, SharedString, StyleRefinement,
7 Subscription, TextStyleRefinement, UnderlineStyle, Window, div, list, prelude::*,
8};
9use gpui::{FocusHandle, Task};
10use language::Buffer;
11use markdown::{HeadingLevelStyles, MarkdownElement, MarkdownStyle};
12use settings::Settings as _;
13use theme::ThemeSettings;
14use ui::Tooltip;
15use ui::prelude::*;
16use zed_actions::agent::Chat;
17
18use crate::{AcpThread, AcpThreadEvent, AgentThreadEntryContent, MessageChunk, Role, ThreadEntry};
19
20pub struct AcpThreadView {
21 thread: Entity<AcpThread>,
22 // todo! use full message editor from agent2
23 message_editor: Entity<Editor>,
24 list_state: ListState,
25 send_task: Option<Task<Result<()>>>,
26 _subscription: Subscription,
27}
28
29impl AcpThreadView {
30 pub fn new(thread: Entity<AcpThread>, window: &mut Window, cx: &mut Context<Self>) -> Self {
31 let message_editor = cx.new(|cx| {
32 let buffer = cx.new(|cx| Buffer::local("", cx));
33 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
34
35 let mut editor = Editor::new(
36 editor::EditorMode::AutoHeight {
37 min_lines: 5,
38 max_lines: None,
39 },
40 buffer,
41 None,
42 window,
43 cx,
44 );
45 editor.set_placeholder_text("Send a message", cx);
46 editor.set_soft_wrap();
47 editor
48 });
49
50 let subscription = cx.subscribe(&thread, |this, _, event, cx| {
51 let count = this.list_state.item_count();
52 match event {
53 AcpThreadEvent::NewEntry => {
54 this.list_state.splice(count..count, 1);
55 }
56 AcpThreadEvent::LastEntryUpdated => {
57 this.list_state.splice(count - 1..count, 1);
58 }
59 }
60 cx.notify();
61 });
62
63 let list_state = ListState::new(
64 thread.read(cx).entries.len(),
65 gpui::ListAlignment::Top,
66 px(1000.0),
67 cx.processor({
68 move |this: &mut Self, item: usize, window, cx| {
69 let Some(entry) = this.thread.read(cx).entries.get(item) else {
70 return Empty.into_any();
71 };
72 this.render_entry(entry, window, cx)
73 }
74 }),
75 );
76 Self {
77 thread,
78 message_editor,
79 send_task: None,
80 list_state: list_state,
81 _subscription: subscription,
82 }
83 }
84
85 pub fn title(&self, cx: &App) -> SharedString {
86 self.thread.read(cx).title()
87 }
88
89 pub fn cancel(&mut self) {
90 self.send_task.take();
91 }
92
93 fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
94 let text = self.message_editor.read(cx).text(cx);
95 if text.is_empty() {
96 return;
97 }
98
99 let task = self.thread.update(cx, |thread, cx| thread.send(&text, cx));
100
101 self.send_task = Some(cx.spawn(async move |this, cx| {
102 task.await?;
103
104 this.update(cx, |this, _cx| {
105 this.send_task.take();
106 })
107 }));
108
109 self.message_editor.update(cx, |editor, cx| {
110 editor.clear(window, cx);
111 });
112 }
113
114 fn render_entry(
115 &self,
116 entry: &ThreadEntry,
117 window: &mut Window,
118 cx: &Context<Self>,
119 ) -> AnyElement {
120 match &entry.content {
121 AgentThreadEntryContent::Message(message) => {
122 let style = if message.role == Role::User {
123 user_message_markdown_style(window, cx)
124 } else {
125 default_markdown_style(window, cx)
126 };
127 let message_body = div()
128 .children(message.chunks.iter().map(|chunk| match chunk {
129 MessageChunk::Text { chunk } => {
130 // todo!() open link
131 MarkdownElement::new(chunk.clone(), style.clone())
132 }
133 _ => todo!(),
134 }))
135 .into_any();
136
137 match message.role {
138 Role::User => div()
139 .text_xs()
140 .m_1()
141 .p_2()
142 .bg(cx.theme().colors().editor_background)
143 .rounded_lg()
144 .shadow_md()
145 .border_1()
146 .border_color(cx.theme().colors().border)
147 .child(message_body)
148 .into_any(),
149 Role::Assistant => div()
150 .text_ui(cx)
151 .px_2()
152 .py_4()
153 .child(message_body)
154 .into_any(),
155 }
156 }
157 AgentThreadEntryContent::ReadFile { path, content: _ } => {
158 // todo!
159 div()
160 .child(format!("<Reading file {}>", path.display()))
161 .into_any()
162 }
163 }
164 }
165}
166
167impl Focusable for AcpThreadView {
168 fn focus_handle(&self, cx: &App) -> FocusHandle {
169 self.message_editor.focus_handle(cx)
170 }
171}
172
173impl Render for AcpThreadView {
174 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
175 let text = self.message_editor.read(cx).text(cx);
176 let is_editor_empty = text.is_empty();
177 let focus_handle = self.message_editor.focus_handle(cx);
178
179 v_flex()
180 .key_context("MessageEditor")
181 .on_action(cx.listener(Self::chat))
182 .child(
183 div()
184 .child(
185 list(self.list_state.clone())
186 .with_sizing_behavior(gpui::ListSizingBehavior::Infer),
187 )
188 .p_2(),
189 )
190 .when(self.send_task.is_some(), |this| {
191 this.child(
192 div().p_2().child(
193 Label::new("Generating...")
194 .color(Color::Muted)
195 .size(LabelSize::Small),
196 ),
197 )
198 })
199 .child(
200 div()
201 .bg(cx.theme().colors().editor_background)
202 .border_t_1()
203 .border_color(cx.theme().colors().border)
204 .p_2()
205 .child(self.message_editor.clone()),
206 )
207 .child(
208 h_flex()
209 .p_2()
210 .justify_end()
211 .child(if self.send_task.is_some() {
212 IconButton::new("stop-generation", IconName::StopFilled)
213 .icon_color(Color::Error)
214 .style(ButtonStyle::Tinted(ui::TintColor::Error))
215 .tooltip(move |window, cx| {
216 Tooltip::for_action(
217 "Stop Generation",
218 &editor::actions::Cancel,
219 window,
220 cx,
221 )
222 })
223 .disabled(is_editor_empty)
224 .on_click(cx.listener(|this, _event, _, _| this.cancel()))
225 } else {
226 IconButton::new("send-message", IconName::Send)
227 .icon_color(Color::Accent)
228 .style(ButtonStyle::Filled)
229 .disabled(is_editor_empty)
230 .on_click({
231 let focus_handle = focus_handle.clone();
232 move |_event, window, cx| {
233 focus_handle.dispatch_action(&Chat, window, cx);
234 }
235 })
236 .when(!is_editor_empty, |button| {
237 button.tooltip(move |window, cx| {
238 Tooltip::for_action("Send", &Chat, window, cx)
239 })
240 })
241 .when(is_editor_empty, |button| {
242 button.tooltip(Tooltip::text("Type a message to submit"))
243 })
244 }),
245 )
246 }
247}
248
249fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
250 let mut style = default_markdown_style(window, cx);
251 let mut text_style = window.text_style();
252 let theme_settings = ThemeSettings::get_global(cx);
253
254 let buffer_font = theme_settings.buffer_font.family.clone();
255 let buffer_font_size = TextSize::Small.rems(cx);
256
257 text_style.refine(&TextStyleRefinement {
258 font_family: Some(buffer_font),
259 font_size: Some(buffer_font_size.into()),
260 ..Default::default()
261 });
262
263 style.base_text_style = text_style;
264 style
265}
266
267fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
268 let theme_settings = ThemeSettings::get_global(cx);
269 let colors = cx.theme().colors();
270 let ui_font_size = TextSize::Default.rems(cx);
271 let buffer_font_size = TextSize::Small.rems(cx);
272 let mut text_style = window.text_style();
273 let line_height = buffer_font_size * 1.75;
274
275 text_style.refine(&TextStyleRefinement {
276 font_family: Some(theme_settings.ui_font.family.clone()),
277 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
278 font_features: Some(theme_settings.ui_font.features.clone()),
279 font_size: Some(ui_font_size.into()),
280 line_height: Some(line_height.into()),
281 color: Some(cx.theme().colors().text),
282 ..Default::default()
283 });
284
285 MarkdownStyle {
286 base_text_style: text_style.clone(),
287 syntax: cx.theme().syntax().clone(),
288 selection_background_color: cx.theme().colors().element_selection_background,
289 code_block_overflow_x_scroll: true,
290 table_overflow_x_scroll: true,
291 heading_level_styles: Some(HeadingLevelStyles {
292 h1: Some(TextStyleRefinement {
293 font_size: Some(rems(1.15).into()),
294 ..Default::default()
295 }),
296 h2: Some(TextStyleRefinement {
297 font_size: Some(rems(1.1).into()),
298 ..Default::default()
299 }),
300 h3: Some(TextStyleRefinement {
301 font_size: Some(rems(1.05).into()),
302 ..Default::default()
303 }),
304 h4: Some(TextStyleRefinement {
305 font_size: Some(rems(1.).into()),
306 ..Default::default()
307 }),
308 h5: Some(TextStyleRefinement {
309 font_size: Some(rems(0.95).into()),
310 ..Default::default()
311 }),
312 h6: Some(TextStyleRefinement {
313 font_size: Some(rems(0.875).into()),
314 ..Default::default()
315 }),
316 }),
317 code_block: StyleRefinement {
318 padding: EdgesRefinement {
319 top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
320 left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
321 right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
322 bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
323 },
324 background: Some(colors.editor_background.into()),
325 text: Some(TextStyleRefinement {
326 font_family: Some(theme_settings.buffer_font.family.clone()),
327 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
328 font_features: Some(theme_settings.buffer_font.features.clone()),
329 font_size: Some(buffer_font_size.into()),
330 ..Default::default()
331 }),
332 ..Default::default()
333 },
334 inline_code: TextStyleRefinement {
335 font_family: Some(theme_settings.buffer_font.family.clone()),
336 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
337 font_features: Some(theme_settings.buffer_font.features.clone()),
338 font_size: Some(buffer_font_size.into()),
339 background_color: Some(colors.editor_foreground.opacity(0.08)),
340 ..Default::default()
341 },
342 link: TextStyleRefinement {
343 background_color: Some(colors.editor_foreground.opacity(0.025)),
344 underline: Some(UnderlineStyle {
345 color: Some(colors.text_accent.opacity(0.5)),
346 thickness: px(1.),
347 ..Default::default()
348 }),
349 ..Default::default()
350 },
351 link_callback: Some(Rc::new(move |_url, _cx| {
352 // todo!()
353 // if MentionLink::is_valid(url) {
354 // let colors = cx.theme().colors();
355 // Some(TextStyleRefinement {
356 // background_color: Some(colors.element_background),
357 // ..Default::default()
358 // })
359 // } else {
360 None
361 // }
362 })),
363 ..Default::default()
364 }
365}