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