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