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