1use acp_thread::{AgentConnection, Plan};
2use agent_servers::AgentServer;
3use std::cell::RefCell;
4use std::collections::BTreeMap;
5use std::path::Path;
6use std::rc::Rc;
7use std::sync::Arc;
8use std::time::Duration;
9
10use agent_client_protocol as acp;
11use assistant_tool::ActionLog;
12use buffer_diff::BufferDiff;
13use collections::{HashMap, HashSet};
14use editor::{
15 AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorMode,
16 EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
17};
18use file_icons::FileIcons;
19use gpui::{
20 Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
21 FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
22 Subscription, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
23 Window, div, linear_color_stop, linear_gradient, list, percentage, point, prelude::*,
24 pulsating_between,
25};
26use language::language_settings::SoftWrap;
27use language::{Buffer, Language};
28use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
29use parking_lot::Mutex;
30use project::Project;
31use settings::Settings as _;
32use text::Anchor;
33use theme::ThemeSettings;
34use ui::{Disclosure, Divider, DividerColor, KeyBinding, Tooltip, prelude::*};
35use util::ResultExt;
36use workspace::{CollaboratorId, Workspace};
37use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
38
39use ::acp_thread::{
40 AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, Diff,
41 LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
42};
43
44use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
45use crate::acp::message_history::MessageHistory;
46use crate::agent_diff::AgentDiff;
47use crate::message_editor::{MAX_EDITOR_LINES, MIN_EDITOR_LINES};
48use crate::{AgentDiffPane, ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, RejectAll};
49
50const RESPONSE_PADDING_X: Pixels = px(19.);
51
52pub struct AcpThreadView {
53 agent: Rc<dyn AgentServer>,
54 workspace: WeakEntity<Workspace>,
55 project: Entity<Project>,
56 thread_state: ThreadState,
57 diff_editors: HashMap<EntityId, Entity<Editor>>,
58 message_editor: Entity<Editor>,
59 message_set_from_history: bool,
60 _message_editor_subscription: Subscription,
61 mention_set: Arc<Mutex<MentionSet>>,
62 last_error: Option<Entity<Markdown>>,
63 list_state: ListState,
64 auth_task: Option<Task<()>>,
65 expanded_tool_calls: HashSet<acp::ToolCallId>,
66 expanded_thinking_blocks: HashSet<(usize, usize)>,
67 edits_expanded: bool,
68 plan_expanded: bool,
69 editor_expanded: bool,
70 message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
71 _cancel_task: Option<Task<()>>,
72}
73
74enum ThreadState {
75 Loading {
76 _task: Task<()>,
77 },
78 Ready {
79 thread: Entity<AcpThread>,
80 _subscription: [Subscription; 2],
81 },
82 LoadError(LoadError),
83 Unauthenticated {
84 connection: Rc<dyn AgentConnection>,
85 },
86}
87
88impl AcpThreadView {
89 pub fn new(
90 agent: Rc<dyn AgentServer>,
91 workspace: WeakEntity<Workspace>,
92 project: Entity<Project>,
93 message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
94 min_lines: usize,
95 max_lines: Option<usize>,
96 window: &mut Window,
97 cx: &mut Context<Self>,
98 ) -> Self {
99 let language = Language::new(
100 language::LanguageConfig {
101 completion_query_characters: HashSet::from_iter(['.', '-', '_', '@']),
102 ..Default::default()
103 },
104 None,
105 );
106
107 let mention_set = Arc::new(Mutex::new(MentionSet::default()));
108
109 let message_editor = cx.new(|cx| {
110 let buffer = cx.new(|cx| Buffer::local("", cx).with_language(Arc::new(language), cx));
111 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
112
113 let mut editor = Editor::new(
114 editor::EditorMode::AutoHeight {
115 min_lines,
116 max_lines: max_lines,
117 },
118 buffer,
119 None,
120 window,
121 cx,
122 );
123 editor.set_placeholder_text("Message the agent - @ to include files", cx);
124 editor.set_show_indent_guides(false, cx);
125 editor.set_soft_wrap();
126 editor.set_use_modal_editing(true);
127 editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
128 mention_set.clone(),
129 workspace.clone(),
130 cx.weak_entity(),
131 ))));
132 editor.set_context_menu_options(ContextMenuOptions {
133 min_entries_visible: 12,
134 max_entries_visible: 12,
135 placement: Some(ContextMenuPlacement::Above),
136 });
137 editor
138 });
139
140 let message_editor_subscription = cx.subscribe(&message_editor, |this, _, event, _| {
141 if let editor::EditorEvent::BufferEdited = &event {
142 if !this.message_set_from_history {
143 this.message_history.borrow_mut().reset_position();
144 }
145 this.message_set_from_history = false;
146 }
147 });
148
149 let mention_set = mention_set.clone();
150
151 let list_state = ListState::new(
152 0,
153 gpui::ListAlignment::Bottom,
154 px(2048.0),
155 cx.processor({
156 move |this: &mut Self, index: usize, window, cx| {
157 let Some((entry, len)) = this.thread().and_then(|thread| {
158 let entries = &thread.read(cx).entries();
159 Some((entries.get(index)?, entries.len()))
160 }) else {
161 return Empty.into_any();
162 };
163 this.render_entry(index, len, entry, window, cx)
164 }
165 }),
166 );
167
168 Self {
169 agent: agent.clone(),
170 workspace: workspace.clone(),
171 project: project.clone(),
172 thread_state: Self::initial_state(agent, workspace, project, window, cx),
173 message_editor,
174 message_set_from_history: false,
175 _message_editor_subscription: message_editor_subscription,
176 mention_set,
177 diff_editors: Default::default(),
178 list_state: list_state,
179 last_error: None,
180 auth_task: None,
181 expanded_tool_calls: HashSet::default(),
182 expanded_thinking_blocks: HashSet::default(),
183 edits_expanded: false,
184 plan_expanded: false,
185 editor_expanded: false,
186 message_history,
187 _cancel_task: None,
188 }
189 }
190
191 fn initial_state(
192 agent: Rc<dyn AgentServer>,
193 workspace: WeakEntity<Workspace>,
194 project: Entity<Project>,
195 window: &mut Window,
196 cx: &mut Context<Self>,
197 ) -> ThreadState {
198 let root_dir = project
199 .read(cx)
200 .visible_worktrees(cx)
201 .next()
202 .map(|worktree| worktree.read(cx).abs_path())
203 .unwrap_or_else(|| paths::home_dir().as_path().into());
204
205 let connect_task = agent.connect(&root_dir, &project, cx);
206 let load_task = cx.spawn_in(window, async move |this, cx| {
207 let connection = match connect_task.await {
208 Ok(thread) => thread,
209 Err(err) => {
210 this.update(cx, |this, cx| {
211 this.handle_load_error(err, cx);
212 cx.notify();
213 })
214 .log_err();
215 return;
216 }
217 };
218
219 if connection.state().needs_authentication {
220 this.update(cx, |this, cx| {
221 this.thread_state = ThreadState::Unauthenticated { connection };
222 cx.notify();
223 })
224 .ok();
225 return;
226 }
227
228 let result = match connection
229 .clone()
230 .new_thread(project.clone(), &root_dir, cx)
231 .await
232 {
233 Err(e) => {
234 let mut cx = cx.clone();
235 // todo! remove duplication
236 if e.downcast_ref::<acp_thread::Unauthenticated>().is_some() {
237 this.update(&mut cx, |this, cx| {
238 this.thread_state = ThreadState::Unauthenticated { connection };
239 cx.notify();
240 })
241 .ok();
242 return;
243 } else {
244 Err(e)
245 }
246 }
247 Ok(session_id) => Ok(session_id),
248 };
249
250 this.update_in(cx, |this, window, cx| {
251 match result {
252 Ok(thread) => {
253 let thread_subscription =
254 cx.subscribe_in(&thread, window, Self::handle_thread_event);
255
256 let action_log = thread.read(cx).action_log().clone();
257 let action_log_subscription =
258 cx.observe(&action_log, |_, _, cx| cx.notify());
259
260 this.list_state
261 .splice(0..0, thread.read(cx).entries().len());
262
263 AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
264
265 this.thread_state = ThreadState::Ready {
266 thread,
267 _subscription: [thread_subscription, action_log_subscription],
268 };
269
270 cx.notify();
271 }
272 Err(err) => {
273 this.handle_load_error(err, cx);
274 }
275 };
276 })
277 .log_err();
278 });
279
280 ThreadState::Loading { _task: load_task }
281 }
282
283 fn handle_load_error(&mut self, err: anyhow::Error, cx: &mut Context<Self>) {
284 if let Some(load_err) = err.downcast_ref::<LoadError>() {
285 self.thread_state = ThreadState::LoadError(load_err.clone());
286 } else {
287 self.thread_state = ThreadState::LoadError(LoadError::Other(err.to_string().into()))
288 }
289 cx.notify();
290 }
291
292 pub fn thread(&self) -> Option<&Entity<AcpThread>> {
293 match &self.thread_state {
294 ThreadState::Ready { thread, .. } => Some(thread),
295 ThreadState::Unauthenticated { .. }
296 | ThreadState::Loading { .. }
297 | ThreadState::LoadError(..) => None,
298 }
299 }
300
301 pub fn title(&self, cx: &App) -> SharedString {
302 match &self.thread_state {
303 ThreadState::Ready { thread, .. } => thread.read(cx).title(),
304 ThreadState::Loading { .. } => "Loading…".into(),
305 ThreadState::LoadError(_) => "Failed to load".into(),
306 ThreadState::Unauthenticated { .. } => "Not authenticated".into(),
307 }
308 }
309
310 pub fn cancel(&mut self, cx: &mut Context<Self>) {
311 self.last_error.take();
312
313 if let Some(thread) = self.thread() {
314 self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx)));
315 }
316 }
317
318 pub fn expand_message_editor(
319 &mut self,
320 _: &ExpandMessageEditor,
321 _window: &mut Window,
322 cx: &mut Context<Self>,
323 ) {
324 self.set_editor_is_expanded(!self.editor_expanded, cx);
325 cx.notify();
326 }
327
328 fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
329 self.editor_expanded = is_expanded;
330 self.message_editor.update(cx, |editor, _| {
331 if self.editor_expanded {
332 editor.set_mode(EditorMode::Full {
333 scale_ui_elements_with_buffer_font_size: false,
334 show_active_line_background: false,
335 sized_by_content: false,
336 })
337 } else {
338 editor.set_mode(EditorMode::AutoHeight {
339 min_lines: MIN_EDITOR_LINES,
340 max_lines: Some(MAX_EDITOR_LINES),
341 })
342 }
343 });
344 cx.notify();
345 }
346
347 fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
348 self.last_error.take();
349
350 let mut ix = 0;
351 let mut chunks: Vec<acp::ContentBlock> = Vec::new();
352 let project = self.project.clone();
353 self.message_editor.update(cx, |editor, cx| {
354 let text = editor.text(cx);
355 editor.display_map.update(cx, |map, cx| {
356 let snapshot = map.snapshot(cx);
357 for (crease_id, crease) in snapshot.crease_snapshot.creases() {
358 if let Some(project_path) =
359 self.mention_set.lock().path_for_crease_id(crease_id)
360 {
361 let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
362 if crease_range.start > ix {
363 chunks.push(text[ix..crease_range.start].into());
364 }
365 if let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) {
366 let path_str = abs_path.display().to_string();
367 chunks.push(acp::ContentBlock::ResourceLink(acp::ResourceLink {
368 uri: path_str.clone(),
369 name: path_str,
370 annotations: None,
371 description: None,
372 mime_type: None,
373 size: None,
374 title: None,
375 }));
376 }
377 ix = crease_range.end;
378 }
379 }
380
381 if ix < text.len() {
382 let last_chunk = text[ix..].trim();
383 if !last_chunk.is_empty() {
384 chunks.push(last_chunk.into());
385 }
386 }
387 })
388 });
389
390 if chunks.is_empty() {
391 return;
392 }
393
394 let Some(thread) = self.thread() else { return };
395 let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
396
397 cx.spawn(async move |this, cx| {
398 let result = task.await;
399
400 this.update(cx, |this, cx| {
401 if let Err(err) = result {
402 this.last_error =
403 Some(cx.new(|cx| Markdown::new(err.to_string().into(), None, None, cx)))
404 }
405 })
406 })
407 .detach();
408
409 let mention_set = self.mention_set.clone();
410
411 self.set_editor_is_expanded(false, cx);
412 self.message_editor.update(cx, |editor, cx| {
413 editor.clear(window, cx);
414 editor.remove_creases(mention_set.lock().drain(), cx)
415 });
416
417 self.message_history.borrow_mut().push(chunks);
418 }
419
420 fn previous_history_message(
421 &mut self,
422 _: &PreviousHistoryMessage,
423 window: &mut Window,
424 cx: &mut Context<Self>,
425 ) {
426 self.message_set_from_history = Self::set_draft_message(
427 self.message_editor.clone(),
428 self.mention_set.clone(),
429 self.project.clone(),
430 self.message_history.borrow_mut().prev(),
431 window,
432 cx,
433 );
434 }
435
436 fn next_history_message(
437 &mut self,
438 _: &NextHistoryMessage,
439 window: &mut Window,
440 cx: &mut Context<Self>,
441 ) {
442 self.message_set_from_history = Self::set_draft_message(
443 self.message_editor.clone(),
444 self.mention_set.clone(),
445 self.project.clone(),
446 self.message_history.borrow_mut().next(),
447 window,
448 cx,
449 );
450 }
451
452 fn open_agent_diff(&mut self, _: &OpenAgentDiff, window: &mut Window, cx: &mut Context<Self>) {
453 if let Some(thread) = self.thread() {
454 AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err();
455 }
456 }
457
458 fn open_edited_buffer(
459 &mut self,
460 buffer: &Entity<Buffer>,
461 window: &mut Window,
462 cx: &mut Context<Self>,
463 ) {
464 let Some(thread) = self.thread() else {
465 return;
466 };
467
468 let Some(diff) =
469 AgentDiffPane::deploy(thread.clone(), self.workspace.clone(), window, cx).log_err()
470 else {
471 return;
472 };
473
474 diff.update(cx, |diff, cx| {
475 diff.move_to_path(PathKey::for_buffer(&buffer, cx), window, cx)
476 })
477 }
478
479 fn set_draft_message(
480 message_editor: Entity<Editor>,
481 mention_set: Arc<Mutex<MentionSet>>,
482 project: Entity<Project>,
483 message: Option<&Vec<acp::ContentBlock>>,
484 window: &mut Window,
485 cx: &mut Context<Self>,
486 ) -> bool {
487 cx.notify();
488
489 let Some(message) = message else {
490 return false;
491 };
492
493 let mut text = String::new();
494 let mut mentions = Vec::new();
495
496 for chunk in message {
497 match chunk {
498 acp::ContentBlock::Text(text_content) => {
499 text.push_str(&text_content.text);
500 }
501 acp::ContentBlock::ResourceLink(resource_link) => {
502 let path = Path::new(&resource_link.uri);
503 let start = text.len();
504 let content = MentionPath::new(&path).to_string();
505 text.push_str(&content);
506 let end = text.len();
507 if let Some(project_path) =
508 project.read(cx).project_path_for_absolute_path(&path, cx)
509 {
510 let filename: SharedString = path
511 .file_name()
512 .unwrap_or_default()
513 .to_string_lossy()
514 .to_string()
515 .into();
516 mentions.push((start..end, project_path, filename));
517 }
518 }
519 acp::ContentBlock::Image(_)
520 | acp::ContentBlock::Audio(_)
521 | acp::ContentBlock::Resource(_) => {}
522 }
523 }
524
525 let snapshot = message_editor.update(cx, |editor, cx| {
526 editor.set_text(text, window, cx);
527 editor.buffer().read(cx).snapshot(cx)
528 });
529
530 for (range, project_path, filename) in mentions {
531 let crease_icon_path = if project_path.path.is_dir() {
532 FileIcons::get_folder_icon(false, cx)
533 .unwrap_or_else(|| IconName::Folder.path().into())
534 } else {
535 FileIcons::get_icon(Path::new(project_path.path.as_ref()), cx)
536 .unwrap_or_else(|| IconName::File.path().into())
537 };
538
539 let anchor = snapshot.anchor_before(range.start);
540 let crease_id = crate::context_picker::insert_crease_for_mention(
541 anchor.excerpt_id,
542 anchor.text_anchor,
543 range.end - range.start,
544 filename,
545 crease_icon_path,
546 message_editor.clone(),
547 window,
548 cx,
549 );
550 if let Some(crease_id) = crease_id {
551 mention_set.lock().insert(crease_id, project_path);
552 }
553 }
554
555 true
556 }
557
558 fn handle_thread_event(
559 &mut self,
560 thread: &Entity<AcpThread>,
561 event: &AcpThreadEvent,
562 window: &mut Window,
563 cx: &mut Context<Self>,
564 ) {
565 let count = self.list_state.item_count();
566 match event {
567 AcpThreadEvent::NewEntry => {
568 let index = thread.read(cx).entries().len() - 1;
569 self.sync_thread_entry_view(index, window, cx);
570 self.list_state.splice(count..count, 1);
571 }
572 AcpThreadEvent::EntryUpdated(index) => {
573 let index = *index;
574 self.sync_thread_entry_view(index, window, cx);
575 self.list_state.splice(index..index + 1, 1);
576 }
577 }
578 cx.notify();
579 }
580
581 fn sync_thread_entry_view(
582 &mut self,
583 entry_ix: usize,
584 window: &mut Window,
585 cx: &mut Context<Self>,
586 ) {
587 let Some(multibuffers) = self.entry_diff_multibuffers(entry_ix, cx) else {
588 return;
589 };
590
591 let multibuffers = multibuffers.collect::<Vec<_>>();
592
593 for multibuffer in multibuffers {
594 if self.diff_editors.contains_key(&multibuffer.entity_id()) {
595 return;
596 }
597
598 let editor = cx.new(|cx| {
599 let mut editor = Editor::new(
600 EditorMode::Full {
601 scale_ui_elements_with_buffer_font_size: false,
602 show_active_line_background: false,
603 sized_by_content: true,
604 },
605 multibuffer.clone(),
606 None,
607 window,
608 cx,
609 );
610 editor.set_show_gutter(false, cx);
611 editor.disable_inline_diagnostics();
612 editor.disable_expand_excerpt_buttons(cx);
613 editor.set_show_vertical_scrollbar(false, cx);
614 editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
615 editor.set_soft_wrap_mode(SoftWrap::None, cx);
616 editor.scroll_manager.set_forbid_vertical_scroll(true);
617 editor.set_show_indent_guides(false, cx);
618 editor.set_read_only(true);
619 editor.set_show_breakpoints(false, cx);
620 editor.set_show_code_actions(false, cx);
621 editor.set_show_git_diff_gutter(false, cx);
622 editor.set_expand_all_diff_hunks(cx);
623 editor.set_text_style_refinement(TextStyleRefinement {
624 font_size: Some(
625 TextSize::Small
626 .rems(cx)
627 .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
628 .into(),
629 ),
630 ..Default::default()
631 });
632 editor
633 });
634 let entity_id = multibuffer.entity_id();
635 cx.observe_release(&multibuffer, move |this, _, _| {
636 this.diff_editors.remove(&entity_id);
637 })
638 .detach();
639
640 self.diff_editors.insert(entity_id, editor);
641 }
642 }
643
644 fn entry_diff_multibuffers(
645 &self,
646 entry_ix: usize,
647 cx: &App,
648 ) -> Option<impl Iterator<Item = Entity<MultiBuffer>>> {
649 let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
650 Some(entry.diffs().map(|diff| diff.multibuffer.clone()))
651 }
652
653 fn authenticate(
654 &mut self,
655 method: acp::AuthMethodId,
656 window: &mut Window,
657 cx: &mut Context<Self>,
658 ) {
659 let ThreadState::Unauthenticated { ref connection } = self.thread_state else {
660 return;
661 };
662
663 self.last_error.take();
664 let authenticate = connection.authenticate(method, cx);
665 self.auth_task = Some(cx.spawn_in(window, {
666 let project = self.project.clone();
667 let agent = self.agent.clone();
668 async move |this, cx| {
669 let result = authenticate.await;
670
671 this.update_in(cx, |this, window, cx| {
672 if let Err(err) = result {
673 this.last_error = Some(cx.new(|cx| {
674 Markdown::new(format!("Error: {err}").into(), None, None, cx)
675 }))
676 } else {
677 this.thread_state = Self::initial_state(
678 agent,
679 this.workspace.clone(),
680 project.clone(),
681 window,
682 cx,
683 )
684 }
685 this.auth_task.take()
686 })
687 .ok();
688 }
689 }));
690 }
691
692 fn authorize_tool_call(
693 &mut self,
694 tool_call_id: acp::ToolCallId,
695 option_id: acp::PermissionOptionId,
696 option_kind: acp::PermissionOptionKind,
697 cx: &mut Context<Self>,
698 ) {
699 let Some(thread) = self.thread() else {
700 return;
701 };
702 thread.update(cx, |thread, cx| {
703 thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
704 });
705 cx.notify();
706 }
707
708 fn render_entry(
709 &self,
710 index: usize,
711 total_entries: usize,
712 entry: &AgentThreadEntry,
713 window: &mut Window,
714 cx: &Context<Self>,
715 ) -> AnyElement {
716 match &entry {
717 AgentThreadEntry::UserMessage(message) => div()
718 .py_4()
719 .px_2()
720 .child(
721 v_flex()
722 .p_3()
723 .gap_1p5()
724 .rounded_lg()
725 .shadow_md()
726 .bg(cx.theme().colors().editor_background)
727 .border_1()
728 .border_color(cx.theme().colors().border)
729 .text_xs()
730 .children(message.content.markdown().map(|md| {
731 self.render_markdown(
732 md.clone(),
733 user_message_markdown_style(window, cx),
734 )
735 })),
736 )
737 .into_any(),
738 AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
739 let style = default_markdown_style(false, window, cx);
740 let message_body = v_flex()
741 .w_full()
742 .gap_2p5()
743 .children(chunks.iter().enumerate().filter_map(
744 |(chunk_ix, chunk)| match chunk {
745 AssistantMessageChunk::Message { block } => {
746 block.markdown().map(|md| {
747 self.render_markdown(md.clone(), style.clone())
748 .into_any_element()
749 })
750 }
751 AssistantMessageChunk::Thought { block } => {
752 block.markdown().map(|md| {
753 self.render_thinking_block(
754 index,
755 chunk_ix,
756 md.clone(),
757 window,
758 cx,
759 )
760 .into_any_element()
761 })
762 }
763 },
764 ))
765 .into_any();
766
767 v_flex()
768 .px_5()
769 .py_1()
770 .when(index + 1 == total_entries, |this| this.pb_4())
771 .w_full()
772 .text_ui(cx)
773 .child(message_body)
774 .into_any()
775 }
776 AgentThreadEntry::ToolCall(tool_call) => div()
777 .py_1p5()
778 .px_5()
779 .child(self.render_tool_call(index, tool_call, window, cx))
780 .into_any(),
781 }
782 }
783
784 fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
785 cx.theme()
786 .colors()
787 .element_background
788 .blend(cx.theme().colors().editor_foreground.opacity(0.025))
789 }
790
791 fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
792 cx.theme().colors().border.opacity(0.6)
793 }
794
795 fn tool_name_font_size(&self) -> Rems {
796 rems_from_px(13.)
797 }
798
799 fn render_thinking_block(
800 &self,
801 entry_ix: usize,
802 chunk_ix: usize,
803 chunk: Entity<Markdown>,
804 window: &Window,
805 cx: &Context<Self>,
806 ) -> AnyElement {
807 let header_id = SharedString::from(format!("thinking-block-header-{}", entry_ix));
808 let key = (entry_ix, chunk_ix);
809 let is_open = self.expanded_thinking_blocks.contains(&key);
810
811 v_flex()
812 .child(
813 h_flex()
814 .id(header_id)
815 .group("disclosure-header")
816 .w_full()
817 .justify_between()
818 .opacity(0.8)
819 .hover(|style| style.opacity(1.))
820 .child(
821 h_flex()
822 .gap_1p5()
823 .child(
824 Icon::new(IconName::ToolBulb)
825 .size(IconSize::Small)
826 .color(Color::Muted),
827 )
828 .child(
829 div()
830 .text_size(self.tool_name_font_size())
831 .child("Thinking"),
832 ),
833 )
834 .child(
835 div().visible_on_hover("disclosure-header").child(
836 Disclosure::new("thinking-disclosure", is_open)
837 .opened_icon(IconName::ChevronUp)
838 .closed_icon(IconName::ChevronDown)
839 .on_click(cx.listener({
840 move |this, _event, _window, cx| {
841 if is_open {
842 this.expanded_thinking_blocks.remove(&key);
843 } else {
844 this.expanded_thinking_blocks.insert(key);
845 }
846 cx.notify();
847 }
848 })),
849 ),
850 )
851 .on_click(cx.listener({
852 move |this, _event, _window, cx| {
853 if is_open {
854 this.expanded_thinking_blocks.remove(&key);
855 } else {
856 this.expanded_thinking_blocks.insert(key);
857 }
858 cx.notify();
859 }
860 })),
861 )
862 .when(is_open, |this| {
863 this.child(
864 div()
865 .relative()
866 .mt_1p5()
867 .ml(px(7.))
868 .pl_4()
869 .border_l_1()
870 .border_color(self.tool_card_border_color(cx))
871 .text_ui_sm(cx)
872 .child(
873 self.render_markdown(chunk, default_markdown_style(false, window, cx)),
874 ),
875 )
876 })
877 .into_any_element()
878 }
879
880 fn render_tool_call(
881 &self,
882 entry_ix: usize,
883 tool_call: &ToolCall,
884 window: &Window,
885 cx: &Context<Self>,
886 ) -> Div {
887 let header_id = SharedString::from(format!("tool-call-header-{}", entry_ix));
888
889 let status_icon = match &tool_call.status {
890 ToolCallStatus::Allowed {
891 status: acp::ToolCallStatus::Pending,
892 }
893 | ToolCallStatus::WaitingForConfirmation { .. } => None,
894 ToolCallStatus::Allowed {
895 status: acp::ToolCallStatus::InProgress,
896 ..
897 } => Some(
898 Icon::new(IconName::ArrowCircle)
899 .color(Color::Accent)
900 .size(IconSize::Small)
901 .with_animation(
902 "running",
903 Animation::new(Duration::from_secs(2)).repeat(),
904 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
905 )
906 .into_any(),
907 ),
908 ToolCallStatus::Allowed {
909 status: acp::ToolCallStatus::Completed,
910 ..
911 } => None,
912 ToolCallStatus::Rejected
913 | ToolCallStatus::Canceled
914 | ToolCallStatus::Allowed {
915 status: acp::ToolCallStatus::Failed,
916 ..
917 } => Some(
918 Icon::new(IconName::X)
919 .color(Color::Error)
920 .size(IconSize::Small)
921 .into_any_element(),
922 ),
923 };
924
925 let needs_confirmation = match &tool_call.status {
926 ToolCallStatus::WaitingForConfirmation { .. } => true,
927 _ => tool_call
928 .content
929 .iter()
930 .any(|content| matches!(content, ToolCallContent::Diff { .. })),
931 };
932
933 let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
934 let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id);
935
936 v_flex()
937 .when(needs_confirmation, |this| {
938 this.rounded_lg()
939 .border_1()
940 .border_color(self.tool_card_border_color(cx))
941 .bg(cx.theme().colors().editor_background)
942 .overflow_hidden()
943 })
944 .child(
945 h_flex()
946 .id(header_id)
947 .w_full()
948 .gap_1()
949 .justify_between()
950 .map(|this| {
951 if needs_confirmation {
952 this.px_2()
953 .py_1()
954 .rounded_t_md()
955 .bg(self.tool_card_header_bg(cx))
956 .border_b_1()
957 .border_color(self.tool_card_border_color(cx))
958 } else {
959 this.opacity(0.8).hover(|style| style.opacity(1.))
960 }
961 })
962 .child(
963 h_flex()
964 .id("tool-call-header")
965 .overflow_x_scroll()
966 .map(|this| {
967 if needs_confirmation {
968 this.text_xs()
969 } else {
970 this.text_size(self.tool_name_font_size())
971 }
972 })
973 .gap_1p5()
974 .child(
975 Icon::new(match tool_call.kind {
976 acp::ToolKind::Read => IconName::ToolRead,
977 acp::ToolKind::Edit => IconName::ToolPencil,
978 acp::ToolKind::Delete => IconName::ToolDeleteFile,
979 acp::ToolKind::Move => IconName::ArrowRightLeft,
980 acp::ToolKind::Search => IconName::ToolSearch,
981 acp::ToolKind::Execute => IconName::ToolTerminal,
982 acp::ToolKind::Think => IconName::ToolBulb,
983 acp::ToolKind::Fetch => IconName::ToolWeb,
984 acp::ToolKind::Other => IconName::ToolHammer,
985 })
986 .size(IconSize::Small)
987 .color(Color::Muted),
988 )
989 .child(if tool_call.locations.len() == 1 {
990 let name = tool_call.locations[0]
991 .path
992 .file_name()
993 .unwrap_or_default()
994 .display()
995 .to_string();
996
997 h_flex()
998 .id(("open-tool-call-location", entry_ix))
999 .child(name)
1000 .w_full()
1001 .max_w_full()
1002 .pr_1()
1003 .gap_0p5()
1004 .cursor_pointer()
1005 .rounded_sm()
1006 .opacity(0.8)
1007 .hover(|label| {
1008 label.opacity(1.).bg(cx
1009 .theme()
1010 .colors()
1011 .element_hover
1012 .opacity(0.5))
1013 })
1014 .tooltip(Tooltip::text("Jump to File"))
1015 .on_click(cx.listener(move |this, _, window, cx| {
1016 this.open_tool_call_location(entry_ix, 0, window, cx);
1017 }))
1018 .into_any_element()
1019 } else {
1020 self.render_markdown(
1021 tool_call.label.clone(),
1022 default_markdown_style(needs_confirmation, window, cx),
1023 )
1024 .into_any()
1025 }),
1026 )
1027 .child(
1028 h_flex()
1029 .gap_0p5()
1030 .when(is_collapsible, |this| {
1031 this.child(
1032 Disclosure::new(("expand", entry_ix), is_open)
1033 .opened_icon(IconName::ChevronUp)
1034 .closed_icon(IconName::ChevronDown)
1035 .on_click(cx.listener({
1036 let id = tool_call.id.clone();
1037 move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1038 if is_open {
1039 this.expanded_tool_calls.remove(&id);
1040 } else {
1041 this.expanded_tool_calls.insert(id.clone());
1042 }
1043 cx.notify();
1044 }
1045 })),
1046 )
1047 })
1048 .children(status_icon),
1049 )
1050 .on_click(cx.listener({
1051 let id = tool_call.id.clone();
1052 move |this: &mut Self, _, _, cx: &mut Context<Self>| {
1053 if is_open {
1054 this.expanded_tool_calls.remove(&id);
1055 } else {
1056 this.expanded_tool_calls.insert(id.clone());
1057 }
1058 cx.notify();
1059 }
1060 })),
1061 )
1062 .when(is_open, |this| {
1063 this.child(
1064 v_flex()
1065 .text_xs()
1066 .when(is_collapsible, |this| {
1067 this.mt_1()
1068 .border_1()
1069 .border_color(self.tool_card_border_color(cx))
1070 .bg(cx.theme().colors().editor_background)
1071 .rounded_lg()
1072 })
1073 .map(|this| {
1074 if is_open {
1075 match &tool_call.status {
1076 ToolCallStatus::WaitingForConfirmation { options, .. } => this
1077 .children(tool_call.content.iter().map(|content| {
1078 div()
1079 .py_1p5()
1080 .child(
1081 self.render_tool_call_content(
1082 content, window, cx,
1083 ),
1084 )
1085 .into_any_element()
1086 }))
1087 .child(self.render_permission_buttons(
1088 options,
1089 entry_ix,
1090 tool_call.id.clone(),
1091 tool_call.content.is_empty(),
1092 cx,
1093 )),
1094 ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
1095 this.children(tool_call.content.iter().map(|content| {
1096 div()
1097 .py_1p5()
1098 .child(
1099 self.render_tool_call_content(
1100 content, window, cx,
1101 ),
1102 )
1103 .into_any_element()
1104 }))
1105 }
1106 ToolCallStatus::Rejected => this,
1107 }
1108 } else {
1109 this
1110 }
1111 }),
1112 )
1113 })
1114 }
1115
1116 fn render_tool_call_content(
1117 &self,
1118 content: &ToolCallContent,
1119 window: &Window,
1120 cx: &Context<Self>,
1121 ) -> AnyElement {
1122 match content {
1123 ToolCallContent::ContentBlock { content } => {
1124 if let Some(md) = content.markdown() {
1125 div()
1126 .p_2()
1127 .child(
1128 self.render_markdown(
1129 md.clone(),
1130 default_markdown_style(false, window, cx),
1131 ),
1132 )
1133 .into_any_element()
1134 } else {
1135 Empty.into_any_element()
1136 }
1137 }
1138 ToolCallContent::Diff {
1139 diff: Diff { multibuffer, .. },
1140 ..
1141 } => self.render_diff_editor(multibuffer),
1142 }
1143 }
1144
1145 fn render_permission_buttons(
1146 &self,
1147 options: &[acp::PermissionOption],
1148 entry_ix: usize,
1149 tool_call_id: acp::ToolCallId,
1150 empty_content: bool,
1151 cx: &Context<Self>,
1152 ) -> Div {
1153 h_flex()
1154 .py_1p5()
1155 .px_1p5()
1156 .gap_1()
1157 .justify_end()
1158 .when(!empty_content, |this| {
1159 this.border_t_1()
1160 .border_color(self.tool_card_border_color(cx))
1161 })
1162 .children(options.iter().map(|option| {
1163 let option_id = SharedString::from(option.id.0.clone());
1164 Button::new((option_id, entry_ix), option.label.clone())
1165 .map(|this| match option.kind {
1166 acp::PermissionOptionKind::AllowOnce => {
1167 this.icon(IconName::Check).icon_color(Color::Success)
1168 }
1169 acp::PermissionOptionKind::AllowAlways => {
1170 this.icon(IconName::CheckDouble).icon_color(Color::Success)
1171 }
1172 acp::PermissionOptionKind::RejectOnce => {
1173 this.icon(IconName::X).icon_color(Color::Error)
1174 }
1175 acp::PermissionOptionKind::RejectAlways => {
1176 this.icon(IconName::X).icon_color(Color::Error)
1177 }
1178 })
1179 .icon_position(IconPosition::Start)
1180 .icon_size(IconSize::XSmall)
1181 .on_click(cx.listener({
1182 let tool_call_id = tool_call_id.clone();
1183 let option_id = option.id.clone();
1184 let option_kind = option.kind;
1185 move |this, _, _, cx| {
1186 this.authorize_tool_call(
1187 tool_call_id.clone(),
1188 option_id.clone(),
1189 option_kind,
1190 cx,
1191 );
1192 }
1193 }))
1194 }))
1195 }
1196
1197 fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
1198 v_flex()
1199 .h_full()
1200 .child(
1201 if let Some(editor) = self.diff_editors.get(&multibuffer.entity_id()) {
1202 editor.clone().into_any_element()
1203 } else {
1204 Empty.into_any()
1205 },
1206 )
1207 .into_any()
1208 }
1209
1210 fn render_agent_logo(&self) -> AnyElement {
1211 Icon::new(self.agent.logo())
1212 .color(Color::Muted)
1213 .size(IconSize::XLarge)
1214 .into_any_element()
1215 }
1216
1217 fn render_error_agent_logo(&self) -> AnyElement {
1218 let logo = Icon::new(self.agent.logo())
1219 .color(Color::Muted)
1220 .size(IconSize::XLarge)
1221 .into_any_element();
1222
1223 h_flex()
1224 .relative()
1225 .justify_center()
1226 .child(div().opacity(0.3).child(logo))
1227 .child(
1228 h_flex().absolute().right_1().bottom_0().child(
1229 Icon::new(IconName::XCircle)
1230 .color(Color::Error)
1231 .size(IconSize::Small),
1232 ),
1233 )
1234 .into_any_element()
1235 }
1236
1237 fn render_empty_state(&self, cx: &App) -> AnyElement {
1238 let loading = matches!(&self.thread_state, ThreadState::Loading { .. });
1239
1240 v_flex()
1241 .size_full()
1242 .items_center()
1243 .justify_center()
1244 .child(if loading {
1245 h_flex()
1246 .justify_center()
1247 .child(self.render_agent_logo())
1248 .with_animation(
1249 "pulsating_icon",
1250 Animation::new(Duration::from_secs(2))
1251 .repeat()
1252 .with_easing(pulsating_between(0.4, 1.0)),
1253 |icon, delta| icon.opacity(delta),
1254 )
1255 .into_any()
1256 } else {
1257 self.render_agent_logo().into_any_element()
1258 })
1259 .child(h_flex().mt_4().mb_1().justify_center().child(if loading {
1260 div()
1261 .child(LoadingLabel::new("").size(LabelSize::Large))
1262 .into_any_element()
1263 } else {
1264 Headline::new(self.agent.empty_state_headline())
1265 .size(HeadlineSize::Medium)
1266 .into_any_element()
1267 }))
1268 .child(
1269 div()
1270 .max_w_1_2()
1271 .text_sm()
1272 .text_center()
1273 .map(|this| {
1274 if loading {
1275 this.invisible()
1276 } else {
1277 this.text_color(cx.theme().colors().text_muted)
1278 }
1279 })
1280 .child(self.agent.empty_state_message()),
1281 )
1282 .into_any()
1283 }
1284
1285 fn render_pending_auth_state(&self) -> AnyElement {
1286 v_flex()
1287 .items_center()
1288 .justify_center()
1289 .child(self.render_error_agent_logo())
1290 .child(
1291 h_flex()
1292 .mt_4()
1293 .mb_1()
1294 .justify_center()
1295 .child(Headline::new("Not Authenticated").size(HeadlineSize::Medium)),
1296 )
1297 .into_any()
1298 }
1299
1300 fn render_error_state(&self, e: &LoadError, cx: &Context<Self>) -> AnyElement {
1301 let mut container = v_flex()
1302 .items_center()
1303 .justify_center()
1304 .child(self.render_error_agent_logo())
1305 .child(
1306 v_flex()
1307 .mt_4()
1308 .mb_2()
1309 .gap_0p5()
1310 .text_center()
1311 .items_center()
1312 .child(Headline::new("Failed to launch").size(HeadlineSize::Medium))
1313 .child(
1314 Label::new(e.to_string())
1315 .size(LabelSize::Small)
1316 .color(Color::Muted),
1317 ),
1318 );
1319
1320 if let LoadError::Unsupported {
1321 upgrade_message,
1322 upgrade_command,
1323 ..
1324 } = &e
1325 {
1326 let upgrade_message = upgrade_message.clone();
1327 let upgrade_command = upgrade_command.clone();
1328 container = container.child(Button::new("upgrade", upgrade_message).on_click(
1329 cx.listener(move |this, _, window, cx| {
1330 this.workspace
1331 .update(cx, |workspace, cx| {
1332 let project = workspace.project().read(cx);
1333 let cwd = project.first_project_directory(cx);
1334 let shell = project.terminal_settings(&cwd, cx).shell.clone();
1335 let spawn_in_terminal = task::SpawnInTerminal {
1336 id: task::TaskId("install".to_string()),
1337 full_label: upgrade_command.clone(),
1338 label: upgrade_command.clone(),
1339 command: Some(upgrade_command.clone()),
1340 args: Vec::new(),
1341 command_label: upgrade_command.clone(),
1342 cwd,
1343 env: Default::default(),
1344 use_new_terminal: true,
1345 allow_concurrent_runs: true,
1346 reveal: Default::default(),
1347 reveal_target: Default::default(),
1348 hide: Default::default(),
1349 shell,
1350 show_summary: true,
1351 show_command: true,
1352 show_rerun: false,
1353 };
1354 workspace
1355 .spawn_in_terminal(spawn_in_terminal, window, cx)
1356 .detach();
1357 })
1358 .ok();
1359 }),
1360 ));
1361 }
1362
1363 container.into_any()
1364 }
1365
1366 fn render_activity_bar(
1367 &self,
1368 thread_entity: &Entity<AcpThread>,
1369 window: &mut Window,
1370 cx: &Context<Self>,
1371 ) -> Option<AnyElement> {
1372 let thread = thread_entity.read(cx);
1373 let action_log = thread.action_log();
1374 let changed_buffers = action_log.read(cx).changed_buffers(cx);
1375 let plan = thread.plan();
1376
1377 if changed_buffers.is_empty() && plan.is_empty() {
1378 return None;
1379 }
1380
1381 let editor_bg_color = cx.theme().colors().editor_background;
1382 let active_color = cx.theme().colors().element_selected;
1383 let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
1384
1385 let pending_edits = thread.has_pending_edit_tool_calls();
1386
1387 v_flex()
1388 .mt_1()
1389 .mx_2()
1390 .bg(bg_edit_files_disclosure)
1391 .border_1()
1392 .border_b_0()
1393 .border_color(cx.theme().colors().border)
1394 .rounded_t_md()
1395 .shadow(vec![gpui::BoxShadow {
1396 color: gpui::black().opacity(0.15),
1397 offset: point(px(1.), px(-1.)),
1398 blur_radius: px(3.),
1399 spread_radius: px(0.),
1400 }])
1401 .when(!plan.is_empty(), |this| {
1402 this.child(self.render_plan_summary(plan, window, cx))
1403 .when(self.plan_expanded, |parent| {
1404 parent.child(self.render_plan_entries(plan, window, cx))
1405 })
1406 })
1407 .when(!changed_buffers.is_empty(), |this| {
1408 this.child(Divider::horizontal())
1409 .child(self.render_edits_summary(
1410 action_log,
1411 &changed_buffers,
1412 self.edits_expanded,
1413 pending_edits,
1414 window,
1415 cx,
1416 ))
1417 .when(self.edits_expanded, |parent| {
1418 parent.child(self.render_edited_files(
1419 action_log,
1420 &changed_buffers,
1421 pending_edits,
1422 cx,
1423 ))
1424 })
1425 })
1426 .into_any()
1427 .into()
1428 }
1429
1430 fn render_plan_summary(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
1431 let stats = plan.stats();
1432
1433 let title = if let Some(entry) = stats.in_progress_entry
1434 && !self.plan_expanded
1435 {
1436 h_flex()
1437 .w_full()
1438 .gap_1()
1439 .text_xs()
1440 .text_color(cx.theme().colors().text_muted)
1441 .justify_between()
1442 .child(
1443 h_flex()
1444 .gap_1()
1445 .child(
1446 Label::new("Current:")
1447 .size(LabelSize::Small)
1448 .color(Color::Muted),
1449 )
1450 .child(MarkdownElement::new(
1451 entry.content.clone(),
1452 plan_label_markdown_style(&entry.status, window, cx),
1453 )),
1454 )
1455 .when(stats.pending > 0, |this| {
1456 this.child(
1457 Label::new(format!("{} left", stats.pending))
1458 .size(LabelSize::Small)
1459 .color(Color::Muted)
1460 .mr_1(),
1461 )
1462 })
1463 } else {
1464 let status_label = if stats.pending == 0 {
1465 "All Done".to_string()
1466 } else if stats.completed == 0 {
1467 format!("{}", plan.entries.len())
1468 } else {
1469 format!("{}/{}", stats.completed, plan.entries.len())
1470 };
1471
1472 h_flex()
1473 .w_full()
1474 .gap_1()
1475 .justify_between()
1476 .child(
1477 Label::new("Plan")
1478 .size(LabelSize::Small)
1479 .color(Color::Muted),
1480 )
1481 .child(
1482 Label::new(status_label)
1483 .size(LabelSize::Small)
1484 .color(Color::Muted)
1485 .mr_1(),
1486 )
1487 };
1488
1489 h_flex()
1490 .p_1()
1491 .justify_between()
1492 .when(self.plan_expanded, |this| {
1493 this.border_b_1().border_color(cx.theme().colors().border)
1494 })
1495 .child(
1496 h_flex()
1497 .id("plan_summary")
1498 .w_full()
1499 .gap_1()
1500 .child(Disclosure::new("plan_disclosure", self.plan_expanded))
1501 .child(title)
1502 .on_click(cx.listener(|this, _, _, cx| {
1503 this.plan_expanded = !this.plan_expanded;
1504 cx.notify();
1505 })),
1506 )
1507 }
1508
1509 fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
1510 v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
1511 let element = h_flex()
1512 .py_1()
1513 .px_2()
1514 .gap_2()
1515 .justify_between()
1516 .bg(cx.theme().colors().editor_background)
1517 .when(index < plan.entries.len() - 1, |parent| {
1518 parent.border_color(cx.theme().colors().border).border_b_1()
1519 })
1520 .child(
1521 h_flex()
1522 .id(("plan_entry", index))
1523 .gap_1p5()
1524 .max_w_full()
1525 .overflow_x_scroll()
1526 .text_xs()
1527 .text_color(cx.theme().colors().text_muted)
1528 .child(match entry.status {
1529 acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
1530 .size(IconSize::Small)
1531 .color(Color::Muted)
1532 .into_any_element(),
1533 acp::PlanEntryStatus::InProgress => Icon::new(IconName::TodoProgress)
1534 .size(IconSize::Small)
1535 .color(Color::Accent)
1536 .with_animation(
1537 "running",
1538 Animation::new(Duration::from_secs(2)).repeat(),
1539 |icon, delta| {
1540 icon.transform(Transformation::rotate(percentage(delta)))
1541 },
1542 )
1543 .into_any_element(),
1544 acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
1545 .size(IconSize::Small)
1546 .color(Color::Success)
1547 .into_any_element(),
1548 })
1549 .child(MarkdownElement::new(
1550 entry.content.clone(),
1551 plan_label_markdown_style(&entry.status, window, cx),
1552 )),
1553 );
1554
1555 Some(element)
1556 }))
1557 }
1558
1559 fn render_edits_summary(
1560 &self,
1561 action_log: &Entity<ActionLog>,
1562 changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
1563 expanded: bool,
1564 pending_edits: bool,
1565 window: &mut Window,
1566 cx: &Context<Self>,
1567 ) -> Div {
1568 const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
1569
1570 let focus_handle = self.focus_handle(cx);
1571
1572 h_flex()
1573 .p_1()
1574 .justify_between()
1575 .when(expanded, |this| {
1576 this.border_b_1().border_color(cx.theme().colors().border)
1577 })
1578 .child(
1579 h_flex()
1580 .id("edits-container")
1581 .cursor_pointer()
1582 .w_full()
1583 .gap_1()
1584 .child(Disclosure::new("edits-disclosure", expanded))
1585 .map(|this| {
1586 if pending_edits {
1587 this.child(
1588 Label::new(format!(
1589 "Editing {} {}…",
1590 changed_buffers.len(),
1591 if changed_buffers.len() == 1 {
1592 "file"
1593 } else {
1594 "files"
1595 }
1596 ))
1597 .color(Color::Muted)
1598 .size(LabelSize::Small)
1599 .with_animation(
1600 "edit-label",
1601 Animation::new(Duration::from_secs(2))
1602 .repeat()
1603 .with_easing(pulsating_between(0.3, 0.7)),
1604 |label, delta| label.alpha(delta),
1605 ),
1606 )
1607 } else {
1608 this.child(
1609 Label::new("Edits")
1610 .size(LabelSize::Small)
1611 .color(Color::Muted),
1612 )
1613 .child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted))
1614 .child(
1615 Label::new(format!(
1616 "{} {}",
1617 changed_buffers.len(),
1618 if changed_buffers.len() == 1 {
1619 "file"
1620 } else {
1621 "files"
1622 }
1623 ))
1624 .size(LabelSize::Small)
1625 .color(Color::Muted),
1626 )
1627 }
1628 })
1629 .on_click(cx.listener(|this, _, _, cx| {
1630 this.edits_expanded = !this.edits_expanded;
1631 cx.notify();
1632 })),
1633 )
1634 .child(
1635 h_flex()
1636 .gap_1()
1637 .child(
1638 IconButton::new("review-changes", IconName::ListTodo)
1639 .icon_size(IconSize::Small)
1640 .tooltip({
1641 let focus_handle = focus_handle.clone();
1642 move |window, cx| {
1643 Tooltip::for_action_in(
1644 "Review Changes",
1645 &OpenAgentDiff,
1646 &focus_handle,
1647 window,
1648 cx,
1649 )
1650 }
1651 })
1652 .on_click(cx.listener(|_, _, window, cx| {
1653 window.dispatch_action(OpenAgentDiff.boxed_clone(), cx);
1654 })),
1655 )
1656 .child(Divider::vertical().color(DividerColor::Border))
1657 .child(
1658 Button::new("reject-all-changes", "Reject All")
1659 .label_size(LabelSize::Small)
1660 .disabled(pending_edits)
1661 .when(pending_edits, |this| {
1662 this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
1663 })
1664 .key_binding(
1665 KeyBinding::for_action_in(
1666 &RejectAll,
1667 &focus_handle.clone(),
1668 window,
1669 cx,
1670 )
1671 .map(|kb| kb.size(rems_from_px(10.))),
1672 )
1673 .on_click({
1674 let action_log = action_log.clone();
1675 cx.listener(move |_, _, _, cx| {
1676 action_log.update(cx, |action_log, cx| {
1677 action_log.reject_all_edits(cx).detach();
1678 })
1679 })
1680 }),
1681 )
1682 .child(
1683 Button::new("keep-all-changes", "Keep All")
1684 .label_size(LabelSize::Small)
1685 .disabled(pending_edits)
1686 .when(pending_edits, |this| {
1687 this.tooltip(Tooltip::text(EDIT_NOT_READY_TOOLTIP_LABEL))
1688 })
1689 .key_binding(
1690 KeyBinding::for_action_in(&KeepAll, &focus_handle, window, cx)
1691 .map(|kb| kb.size(rems_from_px(10.))),
1692 )
1693 .on_click({
1694 let action_log = action_log.clone();
1695 cx.listener(move |_, _, _, cx| {
1696 action_log.update(cx, |action_log, cx| {
1697 action_log.keep_all_edits(cx);
1698 })
1699 })
1700 }),
1701 ),
1702 )
1703 }
1704
1705 fn render_edited_files(
1706 &self,
1707 action_log: &Entity<ActionLog>,
1708 changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
1709 pending_edits: bool,
1710 cx: &Context<Self>,
1711 ) -> Div {
1712 let editor_bg_color = cx.theme().colors().editor_background;
1713
1714 v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
1715 |(index, (buffer, _diff))| {
1716 let file = buffer.read(cx).file()?;
1717 let path = file.path();
1718
1719 let file_path = path.parent().and_then(|parent| {
1720 let parent_str = parent.to_string_lossy();
1721
1722 if parent_str.is_empty() {
1723 None
1724 } else {
1725 Some(
1726 Label::new(format!("/{}{}", parent_str, std::path::MAIN_SEPARATOR_STR))
1727 .color(Color::Muted)
1728 .size(LabelSize::XSmall)
1729 .buffer_font(cx),
1730 )
1731 }
1732 });
1733
1734 let file_name = path.file_name().map(|name| {
1735 Label::new(name.to_string_lossy().to_string())
1736 .size(LabelSize::XSmall)
1737 .buffer_font(cx)
1738 });
1739
1740 let file_icon = FileIcons::get_icon(&path, cx)
1741 .map(Icon::from_path)
1742 .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
1743 .unwrap_or_else(|| {
1744 Icon::new(IconName::File)
1745 .color(Color::Muted)
1746 .size(IconSize::Small)
1747 });
1748
1749 let overlay_gradient = linear_gradient(
1750 90.,
1751 linear_color_stop(editor_bg_color, 1.),
1752 linear_color_stop(editor_bg_color.opacity(0.2), 0.),
1753 );
1754
1755 let element = h_flex()
1756 .group("edited-code")
1757 .id(("file-container", index))
1758 .relative()
1759 .py_1()
1760 .pl_2()
1761 .pr_1()
1762 .gap_2()
1763 .justify_between()
1764 .bg(editor_bg_color)
1765 .when(index < changed_buffers.len() - 1, |parent| {
1766 parent.border_color(cx.theme().colors().border).border_b_1()
1767 })
1768 .child(
1769 h_flex()
1770 .id(("file-name", index))
1771 .pr_8()
1772 .gap_1p5()
1773 .max_w_full()
1774 .overflow_x_scroll()
1775 .child(file_icon)
1776 .child(h_flex().gap_0p5().children(file_name).children(file_path))
1777 .on_click({
1778 let buffer = buffer.clone();
1779 cx.listener(move |this, _, window, cx| {
1780 this.open_edited_buffer(&buffer, window, cx);
1781 })
1782 }),
1783 )
1784 .child(
1785 h_flex()
1786 .gap_1()
1787 .visible_on_hover("edited-code")
1788 .child(
1789 Button::new("review", "Review")
1790 .label_size(LabelSize::Small)
1791 .on_click({
1792 let buffer = buffer.clone();
1793 cx.listener(move |this, _, window, cx| {
1794 this.open_edited_buffer(&buffer, window, cx);
1795 })
1796 }),
1797 )
1798 .child(Divider::vertical().color(DividerColor::BorderVariant))
1799 .child(
1800 Button::new("reject-file", "Reject")
1801 .label_size(LabelSize::Small)
1802 .disabled(pending_edits)
1803 .on_click({
1804 let buffer = buffer.clone();
1805 let action_log = action_log.clone();
1806 move |_, _, cx| {
1807 action_log.update(cx, |action_log, cx| {
1808 action_log
1809 .reject_edits_in_ranges(
1810 buffer.clone(),
1811 vec![Anchor::MIN..Anchor::MAX],
1812 cx,
1813 )
1814 .detach_and_log_err(cx);
1815 })
1816 }
1817 }),
1818 )
1819 .child(
1820 Button::new("keep-file", "Keep")
1821 .label_size(LabelSize::Small)
1822 .disabled(pending_edits)
1823 .on_click({
1824 let buffer = buffer.clone();
1825 let action_log = action_log.clone();
1826 move |_, _, cx| {
1827 action_log.update(cx, |action_log, cx| {
1828 action_log.keep_edits_in_range(
1829 buffer.clone(),
1830 Anchor::MIN..Anchor::MAX,
1831 cx,
1832 );
1833 })
1834 }
1835 }),
1836 ),
1837 )
1838 .child(
1839 div()
1840 .id("gradient-overlay")
1841 .absolute()
1842 .h_full()
1843 .w_12()
1844 .top_0()
1845 .bottom_0()
1846 .right(px(152.))
1847 .bg(overlay_gradient),
1848 );
1849
1850 Some(element)
1851 },
1852 ))
1853 }
1854
1855 fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
1856 let focus_handle = self.message_editor.focus_handle(cx);
1857 let editor_bg_color = cx.theme().colors().editor_background;
1858 let (expand_icon, expand_tooltip) = if self.editor_expanded {
1859 (IconName::Minimize, "Minimize Message Editor")
1860 } else {
1861 (IconName::Maximize, "Expand Message Editor")
1862 };
1863
1864 v_flex()
1865 .on_action(cx.listener(Self::expand_message_editor))
1866 .p_2()
1867 .gap_2()
1868 .border_t_1()
1869 .border_color(cx.theme().colors().border)
1870 .bg(editor_bg_color)
1871 .when(self.editor_expanded, |this| {
1872 this.h(vh(0.8, window)).size_full().justify_between()
1873 })
1874 .child(
1875 v_flex()
1876 .relative()
1877 .size_full()
1878 .pt_1()
1879 .pr_2p5()
1880 .child(div().flex_1().child({
1881 let settings = ThemeSettings::get_global(cx);
1882 let font_size = TextSize::Small
1883 .rems(cx)
1884 .to_pixels(settings.agent_font_size(cx));
1885 let line_height = settings.buffer_line_height.value() * font_size;
1886
1887 let text_style = TextStyle {
1888 color: cx.theme().colors().text,
1889 font_family: settings.buffer_font.family.clone(),
1890 font_fallbacks: settings.buffer_font.fallbacks.clone(),
1891 font_features: settings.buffer_font.features.clone(),
1892 font_size: font_size.into(),
1893 line_height: line_height.into(),
1894 ..Default::default()
1895 };
1896
1897 EditorElement::new(
1898 &self.message_editor,
1899 EditorStyle {
1900 background: editor_bg_color,
1901 local_player: cx.theme().players().local(),
1902 text: text_style,
1903 syntax: cx.theme().syntax().clone(),
1904 ..Default::default()
1905 },
1906 )
1907 }))
1908 .child(
1909 h_flex()
1910 .absolute()
1911 .top_0()
1912 .right_0()
1913 .opacity(0.5)
1914 .hover(|this| this.opacity(1.0))
1915 .child(
1916 IconButton::new("toggle-height", expand_icon)
1917 .icon_size(IconSize::XSmall)
1918 .icon_color(Color::Muted)
1919 .tooltip({
1920 let focus_handle = focus_handle.clone();
1921 move |window, cx| {
1922 Tooltip::for_action_in(
1923 expand_tooltip,
1924 &ExpandMessageEditor,
1925 &focus_handle,
1926 window,
1927 cx,
1928 )
1929 }
1930 })
1931 .on_click(cx.listener(|_, _, window, cx| {
1932 window.dispatch_action(Box::new(ExpandMessageEditor), cx);
1933 })),
1934 ),
1935 ),
1936 )
1937 .child(
1938 h_flex()
1939 .flex_none()
1940 .justify_between()
1941 .child(self.render_follow_toggle(cx))
1942 .child(self.render_send_button(cx)),
1943 )
1944 .into_any()
1945 }
1946
1947 fn render_send_button(&self, cx: &mut Context<Self>) -> AnyElement {
1948 if self.thread().map_or(true, |thread| {
1949 thread.read(cx).status() == ThreadStatus::Idle
1950 }) {
1951 let is_editor_empty = self.message_editor.read(cx).is_empty(cx);
1952 IconButton::new("send-message", IconName::Send)
1953 .icon_color(Color::Accent)
1954 .style(ButtonStyle::Filled)
1955 .disabled(self.thread().is_none() || is_editor_empty)
1956 .on_click(cx.listener(|this, _, window, cx| {
1957 this.chat(&Chat, window, cx);
1958 }))
1959 .when(!is_editor_empty, |button| {
1960 button.tooltip(move |window, cx| Tooltip::for_action("Send", &Chat, window, cx))
1961 })
1962 .when(is_editor_empty, |button| {
1963 button.tooltip(Tooltip::text("Type a message to submit"))
1964 })
1965 .into_any_element()
1966 } else {
1967 IconButton::new("stop-generation", IconName::StopFilled)
1968 .icon_color(Color::Error)
1969 .style(ButtonStyle::Tinted(ui::TintColor::Error))
1970 .tooltip(move |window, cx| {
1971 Tooltip::for_action("Stop Generation", &editor::actions::Cancel, window, cx)
1972 })
1973 .on_click(cx.listener(|this, _event, _, cx| this.cancel(cx)))
1974 .into_any_element()
1975 }
1976 }
1977
1978 fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
1979 let following = self
1980 .workspace
1981 .read_with(cx, |workspace, _| {
1982 workspace.is_being_followed(CollaboratorId::Agent)
1983 })
1984 .unwrap_or(false);
1985
1986 IconButton::new("follow-agent", IconName::Crosshair)
1987 .icon_size(IconSize::Small)
1988 .icon_color(Color::Muted)
1989 .toggle_state(following)
1990 .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
1991 .tooltip(move |window, cx| {
1992 if following {
1993 Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
1994 } else {
1995 Tooltip::with_meta(
1996 "Follow Agent",
1997 Some(&Follow),
1998 "Track the agent's location as it reads and edits files.",
1999 window,
2000 cx,
2001 )
2002 }
2003 })
2004 .on_click(cx.listener(move |this, _, window, cx| {
2005 this.workspace
2006 .update(cx, |workspace, cx| {
2007 if following {
2008 workspace.unfollow(CollaboratorId::Agent, window, cx);
2009 } else {
2010 workspace.follow(CollaboratorId::Agent, window, cx);
2011 }
2012 })
2013 .ok();
2014 }))
2015 }
2016
2017 fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
2018 let workspace = self.workspace.clone();
2019 MarkdownElement::new(markdown, style).on_url_click(move |text, window, cx| {
2020 Self::open_link(text, &workspace, window, cx);
2021 })
2022 }
2023
2024 fn open_link(
2025 url: SharedString,
2026 workspace: &WeakEntity<Workspace>,
2027 window: &mut Window,
2028 cx: &mut App,
2029 ) {
2030 let Some(workspace) = workspace.upgrade() else {
2031 cx.open_url(&url);
2032 return;
2033 };
2034
2035 if let Some(mention_path) = MentionPath::try_parse(&url) {
2036 workspace.update(cx, |workspace, cx| {
2037 let project = workspace.project();
2038 let Some((path, entry)) = project.update(cx, |project, cx| {
2039 let path = project.find_project_path(mention_path.path(), cx)?;
2040 let entry = project.entry_for_path(&path, cx)?;
2041 Some((path, entry))
2042 }) else {
2043 return;
2044 };
2045
2046 if entry.is_dir() {
2047 project.update(cx, |_, cx| {
2048 cx.emit(project::Event::RevealInProjectPanel(entry.id));
2049 });
2050 } else {
2051 workspace
2052 .open_path(path, None, true, window, cx)
2053 .detach_and_log_err(cx);
2054 }
2055 })
2056 } else {
2057 cx.open_url(&url);
2058 }
2059 }
2060
2061 fn open_tool_call_location(
2062 &self,
2063 entry_ix: usize,
2064 location_ix: usize,
2065 window: &mut Window,
2066 cx: &mut Context<Self>,
2067 ) -> Option<()> {
2068 let location = self
2069 .thread()?
2070 .read(cx)
2071 .entries()
2072 .get(entry_ix)?
2073 .locations()?
2074 .get(location_ix)?;
2075
2076 let project_path = self
2077 .project
2078 .read(cx)
2079 .find_project_path(&location.path, cx)?;
2080
2081 let open_task = self
2082 .workspace
2083 .update(cx, |worskpace, cx| {
2084 worskpace.open_path(project_path, None, true, window, cx)
2085 })
2086 .log_err()?;
2087
2088 window
2089 .spawn(cx, async move |cx| {
2090 let item = open_task.await?;
2091
2092 let Some(active_editor) = item.downcast::<Editor>() else {
2093 return anyhow::Ok(());
2094 };
2095
2096 active_editor.update_in(cx, |editor, window, cx| {
2097 let snapshot = editor.buffer().read(cx).snapshot(cx);
2098 let first_hunk = editor
2099 .diff_hunks_in_ranges(
2100 &[editor::Anchor::min()..editor::Anchor::max()],
2101 &snapshot,
2102 )
2103 .next();
2104 if let Some(first_hunk) = first_hunk {
2105 let first_hunk_start = first_hunk.multi_buffer_range().start;
2106 editor.change_selections(Default::default(), window, cx, |selections| {
2107 selections.select_anchor_ranges([first_hunk_start..first_hunk_start]);
2108 })
2109 }
2110 })?;
2111
2112 anyhow::Ok(())
2113 })
2114 .detach_and_log_err(cx);
2115
2116 None
2117 }
2118
2119 pub fn open_thread_as_markdown(
2120 &self,
2121 workspace: Entity<Workspace>,
2122 window: &mut Window,
2123 cx: &mut App,
2124 ) -> Task<anyhow::Result<()>> {
2125 let markdown_language_task = workspace
2126 .read(cx)
2127 .app_state()
2128 .languages
2129 .language_for_name("Markdown");
2130
2131 let (thread_summary, markdown) = if let Some(thread) = self.thread() {
2132 let thread = thread.read(cx);
2133 (thread.title().to_string(), thread.to_markdown(cx))
2134 } else {
2135 return Task::ready(Ok(()));
2136 };
2137
2138 window.spawn(cx, async move |cx| {
2139 let markdown_language = markdown_language_task.await?;
2140
2141 workspace.update_in(cx, |workspace, window, cx| {
2142 let project = workspace.project().clone();
2143
2144 if !project.read(cx).is_local() {
2145 anyhow::bail!("failed to open active thread as markdown in remote project");
2146 }
2147
2148 let buffer = project.update(cx, |project, cx| {
2149 project.create_local_buffer(&markdown, Some(markdown_language), cx)
2150 });
2151 let buffer = cx.new(|cx| {
2152 MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
2153 });
2154
2155 workspace.add_item_to_active_pane(
2156 Box::new(cx.new(|cx| {
2157 let mut editor =
2158 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
2159 editor.set_breadcrumb_header(thread_summary);
2160 editor
2161 })),
2162 None,
2163 true,
2164 window,
2165 cx,
2166 );
2167
2168 anyhow::Ok(())
2169 })??;
2170 anyhow::Ok(())
2171 })
2172 }
2173
2174 fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
2175 self.list_state.scroll_to(ListOffset::default());
2176 cx.notify();
2177 }
2178}
2179
2180impl Focusable for AcpThreadView {
2181 fn focus_handle(&self, cx: &App) -> FocusHandle {
2182 self.message_editor.focus_handle(cx)
2183 }
2184}
2185
2186impl Render for AcpThreadView {
2187 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2188 let open_as_markdown = IconButton::new("open-as-markdown", IconName::DocumentText)
2189 .icon_size(IconSize::XSmall)
2190 .icon_color(Color::Ignored)
2191 .tooltip(Tooltip::text("Open Thread as Markdown"))
2192 .on_click(cx.listener(move |this, _, window, cx| {
2193 if let Some(workspace) = this.workspace.upgrade() {
2194 this.open_thread_as_markdown(workspace, window, cx)
2195 .detach_and_log_err(cx);
2196 }
2197 }));
2198
2199 let scroll_to_top = IconButton::new("scroll_to_top", IconName::ArrowUpAlt)
2200 .icon_size(IconSize::XSmall)
2201 .icon_color(Color::Ignored)
2202 .tooltip(Tooltip::text("Scroll To Top"))
2203 .on_click(cx.listener(move |this, _, _, cx| {
2204 this.scroll_to_top(cx);
2205 }));
2206
2207 v_flex()
2208 .size_full()
2209 .key_context("AcpThread")
2210 .on_action(cx.listener(Self::chat))
2211 .on_action(cx.listener(Self::previous_history_message))
2212 .on_action(cx.listener(Self::next_history_message))
2213 .on_action(cx.listener(Self::open_agent_diff))
2214 .child(match &self.thread_state {
2215 ThreadState::Unauthenticated { connection } => v_flex()
2216 .p_2()
2217 .flex_1()
2218 .items_center()
2219 .justify_center()
2220 .child(self.render_pending_auth_state())
2221 .child(h_flex().mt_1p5().justify_center().children(
2222 connection.state().auth_methods.iter().map(|method| {
2223 Button::new(
2224 SharedString::from(method.id.0.clone()),
2225 method.label.clone(),
2226 )
2227 .on_click({
2228 let method_id = method.id.clone();
2229 cx.listener(move |this, _, window, cx| {
2230 this.authenticate(method_id.clone(), window, cx)
2231 })
2232 })
2233 }),
2234 )),
2235 ThreadState::Loading { .. } => v_flex().flex_1().child(self.render_empty_state(cx)),
2236 ThreadState::LoadError(e) => v_flex()
2237 .p_2()
2238 .flex_1()
2239 .items_center()
2240 .justify_center()
2241 .child(self.render_error_state(e, cx)),
2242 ThreadState::Ready { thread, .. } => v_flex().flex_1().map(|this| {
2243 if self.list_state.item_count() > 0 {
2244 this.child(
2245 list(self.list_state.clone())
2246 .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
2247 .flex_grow()
2248 .into_any(),
2249 )
2250 .child(
2251 h_flex()
2252 .group("controls")
2253 .mt_1()
2254 .mr_1()
2255 .py_2()
2256 .px(RESPONSE_PADDING_X)
2257 .opacity(0.4)
2258 .hover(|style| style.opacity(1.))
2259 .flex_wrap()
2260 .justify_end()
2261 .child(open_as_markdown)
2262 .child(scroll_to_top)
2263 .into_any_element(),
2264 )
2265 .children(match thread.read(cx).status() {
2266 ThreadStatus::Idle | ThreadStatus::WaitingForToolConfirmation => None,
2267 ThreadStatus::Generating => div()
2268 .px_5()
2269 .py_2()
2270 .child(LoadingLabel::new("").size(LabelSize::Small))
2271 .into(),
2272 })
2273 .children(self.render_activity_bar(&thread, window, cx))
2274 } else {
2275 this.child(self.render_empty_state(cx))
2276 }
2277 }),
2278 })
2279 .when_some(self.last_error.clone(), |el, error| {
2280 el.child(
2281 div()
2282 .p_2()
2283 .text_xs()
2284 .border_t_1()
2285 .border_color(cx.theme().colors().border)
2286 .bg(cx.theme().status().error_background)
2287 .child(
2288 self.render_markdown(error, default_markdown_style(false, window, cx)),
2289 ),
2290 )
2291 })
2292 .child(self.render_message_editor(window, cx))
2293 }
2294}
2295
2296fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
2297 let mut style = default_markdown_style(false, window, cx);
2298 let mut text_style = window.text_style();
2299 let theme_settings = ThemeSettings::get_global(cx);
2300
2301 let buffer_font = theme_settings.buffer_font.family.clone();
2302 let buffer_font_size = TextSize::Small.rems(cx);
2303
2304 text_style.refine(&TextStyleRefinement {
2305 font_family: Some(buffer_font),
2306 font_size: Some(buffer_font_size.into()),
2307 ..Default::default()
2308 });
2309
2310 style.base_text_style = text_style;
2311 style.link_callback = Some(Rc::new(move |url, cx| {
2312 if MentionPath::try_parse(url).is_some() {
2313 let colors = cx.theme().colors();
2314 Some(TextStyleRefinement {
2315 background_color: Some(colors.element_background),
2316 ..Default::default()
2317 })
2318 } else {
2319 None
2320 }
2321 }));
2322 style
2323}
2324
2325fn default_markdown_style(buffer_font: bool, window: &Window, cx: &App) -> MarkdownStyle {
2326 let theme_settings = ThemeSettings::get_global(cx);
2327 let colors = cx.theme().colors();
2328
2329 let buffer_font_size = TextSize::Small.rems(cx);
2330
2331 let mut text_style = window.text_style();
2332 let line_height = buffer_font_size * 1.75;
2333
2334 let font_family = if buffer_font {
2335 theme_settings.buffer_font.family.clone()
2336 } else {
2337 theme_settings.ui_font.family.clone()
2338 };
2339
2340 let font_size = if buffer_font {
2341 TextSize::Small.rems(cx)
2342 } else {
2343 TextSize::Default.rems(cx)
2344 };
2345
2346 text_style.refine(&TextStyleRefinement {
2347 font_family: Some(font_family),
2348 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
2349 font_features: Some(theme_settings.ui_font.features.clone()),
2350 font_size: Some(font_size.into()),
2351 line_height: Some(line_height.into()),
2352 color: Some(cx.theme().colors().text),
2353 ..Default::default()
2354 });
2355
2356 MarkdownStyle {
2357 base_text_style: text_style.clone(),
2358 syntax: cx.theme().syntax().clone(),
2359 selection_background_color: cx.theme().colors().element_selection_background,
2360 code_block_overflow_x_scroll: true,
2361 table_overflow_x_scroll: true,
2362 heading_level_styles: Some(HeadingLevelStyles {
2363 h1: Some(TextStyleRefinement {
2364 font_size: Some(rems(1.15).into()),
2365 ..Default::default()
2366 }),
2367 h2: Some(TextStyleRefinement {
2368 font_size: Some(rems(1.1).into()),
2369 ..Default::default()
2370 }),
2371 h3: Some(TextStyleRefinement {
2372 font_size: Some(rems(1.05).into()),
2373 ..Default::default()
2374 }),
2375 h4: Some(TextStyleRefinement {
2376 font_size: Some(rems(1.).into()),
2377 ..Default::default()
2378 }),
2379 h5: Some(TextStyleRefinement {
2380 font_size: Some(rems(0.95).into()),
2381 ..Default::default()
2382 }),
2383 h6: Some(TextStyleRefinement {
2384 font_size: Some(rems(0.875).into()),
2385 ..Default::default()
2386 }),
2387 }),
2388 code_block: StyleRefinement {
2389 padding: EdgesRefinement {
2390 top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2391 left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2392 right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2393 bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
2394 },
2395 margin: EdgesRefinement {
2396 top: Some(Length::Definite(Pixels(8.).into())),
2397 left: Some(Length::Definite(Pixels(0.).into())),
2398 right: Some(Length::Definite(Pixels(0.).into())),
2399 bottom: Some(Length::Definite(Pixels(12.).into())),
2400 },
2401 border_style: Some(BorderStyle::Solid),
2402 border_widths: EdgesRefinement {
2403 top: Some(AbsoluteLength::Pixels(Pixels(1.))),
2404 left: Some(AbsoluteLength::Pixels(Pixels(1.))),
2405 right: Some(AbsoluteLength::Pixels(Pixels(1.))),
2406 bottom: Some(AbsoluteLength::Pixels(Pixels(1.))),
2407 },
2408 border_color: Some(colors.border_variant),
2409 background: Some(colors.editor_background.into()),
2410 text: Some(TextStyleRefinement {
2411 font_family: Some(theme_settings.buffer_font.family.clone()),
2412 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
2413 font_features: Some(theme_settings.buffer_font.features.clone()),
2414 font_size: Some(buffer_font_size.into()),
2415 ..Default::default()
2416 }),
2417 ..Default::default()
2418 },
2419 inline_code: TextStyleRefinement {
2420 font_family: Some(theme_settings.buffer_font.family.clone()),
2421 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
2422 font_features: Some(theme_settings.buffer_font.features.clone()),
2423 font_size: Some(buffer_font_size.into()),
2424 background_color: Some(colors.editor_foreground.opacity(0.08)),
2425 ..Default::default()
2426 },
2427 link: TextStyleRefinement {
2428 background_color: Some(colors.editor_foreground.opacity(0.025)),
2429 underline: Some(UnderlineStyle {
2430 color: Some(colors.text_accent.opacity(0.5)),
2431 thickness: px(1.),
2432 ..Default::default()
2433 }),
2434 ..Default::default()
2435 },
2436 ..Default::default()
2437 }
2438}
2439
2440fn plan_label_markdown_style(
2441 status: &acp::PlanEntryStatus,
2442 window: &Window,
2443 cx: &App,
2444) -> MarkdownStyle {
2445 let default_md_style = default_markdown_style(false, window, cx);
2446
2447 MarkdownStyle {
2448 base_text_style: TextStyle {
2449 color: cx.theme().colors().text_muted,
2450 strikethrough: if matches!(status, acp::PlanEntryStatus::Completed) {
2451 Some(gpui::StrikethroughStyle {
2452 thickness: px(1.),
2453 color: Some(cx.theme().colors().text_muted.opacity(0.8)),
2454 })
2455 } else {
2456 None
2457 },
2458 ..default_md_style.base_text_style
2459 },
2460 ..default_md_style
2461 }
2462}