1use std::path::Path;
2use std::rc::Rc;
3use std::time::Duration;
4
5use agentic_coding_protocol::{self as acp, ToolCallConfirmation};
6use anyhow::Result;
7use editor::{Editor, MultiBuffer};
8use gpui::{
9 Animation, AnimationExt, App, EdgesRefinement, Empty, Entity, Focusable, ListState,
10 SharedString, StyleRefinement, Subscription, TextStyleRefinement, Transformation,
11 UnderlineStyle, Window, div, list, percentage, prelude::*,
12};
13use gpui::{FocusHandle, Task};
14use language::Buffer;
15use markdown::{HeadingLevelStyles, MarkdownElement, MarkdownStyle};
16use project::Project;
17use settings::Settings as _;
18use theme::ThemeSettings;
19use ui::prelude::*;
20use ui::{Button, Tooltip};
21use util::ResultExt;
22use zed_actions::agent::Chat;
23
24use crate::{
25 AcpServer, AcpThread, AcpThreadEvent, AgentThreadEntryContent, MessageChunk, Role, ThreadEntry,
26 ToolCall, ToolCallId, ToolCallStatus,
27};
28
29pub struct AcpThreadView {
30 thread_state: ThreadState,
31 // todo! use full message editor from agent2
32 message_editor: Entity<Editor>,
33 list_state: ListState,
34 send_task: Option<Task<Result<()>>>,
35}
36
37enum ThreadState {
38 Loading {
39 _task: Task<()>,
40 },
41 Ready {
42 thread: Entity<AcpThread>,
43 _subscription: Subscription,
44 },
45 LoadError(SharedString),
46}
47
48impl AcpThreadView {
49 pub fn new(project: Entity<Project>, window: &mut Window, cx: &mut Context<Self>) -> Self {
50 let message_editor = cx.new(|cx| {
51 let buffer = cx.new(|cx| Buffer::local("", cx));
52 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
53
54 let mut editor = Editor::new(
55 editor::EditorMode::AutoHeight {
56 min_lines: 4,
57 max_lines: None,
58 },
59 buffer,
60 None,
61 window,
62 cx,
63 );
64 editor.set_placeholder_text("Send a message", cx);
65 editor.set_soft_wrap();
66 editor
67 });
68
69 let list_state = ListState::new(
70 0,
71 gpui::ListAlignment::Bottom,
72 px(2048.0),
73 cx.processor({
74 move |this: &mut Self, item: usize, window, cx| {
75 let Some(entry) = this
76 .thread()
77 .and_then(|thread| thread.read(cx).entries.get(item))
78 else {
79 return Empty.into_any();
80 };
81 this.render_entry(entry, window, cx)
82 }
83 }),
84 );
85
86 Self {
87 thread_state: Self::initial_state(project, window, cx),
88 message_editor,
89 send_task: None,
90 list_state: list_state,
91 }
92 }
93
94 fn initial_state(
95 project: Entity<Project>,
96 window: &mut Window,
97 cx: &mut Context<Self>,
98 ) -> ThreadState {
99 let Some(root_dir) = project
100 .read(cx)
101 .visible_worktrees(cx)
102 .next()
103 .map(|worktree| worktree.read(cx).abs_path())
104 else {
105 return ThreadState::LoadError(
106 "Gemini threads must be created within a project".into(),
107 );
108 };
109
110 let cli_path =
111 Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../gemini-cli/packages/cli");
112
113 let child = util::command::new_smol_command("node")
114 .arg(cli_path)
115 .arg("--acp")
116 .current_dir(root_dir)
117 .stdin(std::process::Stdio::piped())
118 .stdout(std::process::Stdio::piped())
119 .stderr(std::process::Stdio::inherit())
120 .kill_on_drop(true)
121 .spawn()
122 .unwrap();
123
124 let project = project.clone();
125 let load_task = cx.spawn_in(window, async move |this, cx| {
126 let agent = AcpServer::stdio(child, project, cx);
127 let result = agent.clone().create_thread(cx).await;
128
129 this.update(cx, |this, cx| {
130 match result {
131 Ok(thread) => {
132 let subscription = cx.subscribe(&thread, |this, _, event, cx| {
133 let count = this.list_state.item_count();
134 match event {
135 AcpThreadEvent::NewEntry => {
136 this.list_state.splice(count..count, 1);
137 }
138 AcpThreadEvent::EntryUpdated(index) => {
139 this.list_state.splice(*index..*index + 1, 1);
140 }
141 }
142 cx.notify();
143 });
144 this.list_state
145 .splice(0..0, thread.read(cx).entries().len());
146
147 this.thread_state = ThreadState::Ready {
148 thread,
149 _subscription: subscription,
150 };
151 }
152 Err(e) => {
153 if let Some(exit_status) = agent.exit_status() {
154 this.thread_state = ThreadState::LoadError(
155 format!(
156 "Gemini exited with status {}",
157 exit_status.code().unwrap_or(-127)
158 )
159 .into(),
160 )
161 } else {
162 this.thread_state = ThreadState::LoadError(e.to_string().into())
163 }
164 }
165 };
166 cx.notify();
167 })
168 .log_err();
169 });
170
171 ThreadState::Loading { _task: load_task }
172 }
173
174 fn thread(&self) -> Option<&Entity<AcpThread>> {
175 match &self.thread_state {
176 ThreadState::Ready { thread, .. } => Some(thread),
177 ThreadState::Loading { .. } | ThreadState::LoadError(..) => None,
178 }
179 }
180
181 pub fn title(&self, cx: &App) -> SharedString {
182 match &self.thread_state {
183 ThreadState::Ready { thread, .. } => thread.read(cx).title(),
184 ThreadState::Loading { .. } => "Loading...".into(),
185 ThreadState::LoadError(_) => "Failed to load".into(),
186 }
187 }
188
189 pub fn cancel(&mut self) {
190 self.send_task.take();
191 }
192
193 fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
194 let text = self.message_editor.read(cx).text(cx);
195 if text.is_empty() {
196 return;
197 }
198 let Some(thread) = self.thread() else { return };
199
200 let task = thread.update(cx, |thread, cx| thread.send(&text, cx));
201
202 self.send_task = Some(cx.spawn(async move |this, cx| {
203 task.await?;
204
205 this.update(cx, |this, _cx| {
206 this.send_task.take();
207 })
208 }));
209
210 self.message_editor.update(cx, |editor, cx| {
211 editor.clear(window, cx);
212 });
213 }
214
215 fn authorize_tool_call(
216 &mut self,
217 id: ToolCallId,
218 outcome: acp::ToolCallConfirmationOutcome,
219 cx: &mut Context<Self>,
220 ) {
221 let Some(thread) = self.thread() else {
222 return;
223 };
224 thread.update(cx, |thread, cx| {
225 thread.authorize_tool_call(id, outcome, cx);
226 });
227 cx.notify();
228 }
229
230 fn render_entry(
231 &self,
232 entry: &ThreadEntry,
233 window: &mut Window,
234 cx: &Context<Self>,
235 ) -> AnyElement {
236 match &entry.content {
237 AgentThreadEntryContent::Message(message) => {
238 let style = if message.role == Role::User {
239 user_message_markdown_style(window, cx)
240 } else {
241 default_markdown_style(window, cx)
242 };
243 let message_body = div()
244 .children(message.chunks.iter().map(|chunk| match chunk {
245 MessageChunk::Text { chunk } => {
246 // todo!() open link
247 MarkdownElement::new(chunk.clone(), style.clone())
248 }
249 _ => todo!(),
250 }))
251 .into_any();
252
253 match message.role {
254 Role::User => div()
255 .p_2()
256 .pt_5()
257 .child(
258 div()
259 .text_xs()
260 .p_3()
261 .bg(cx.theme().colors().editor_background)
262 .rounded_lg()
263 .shadow_md()
264 .border_1()
265 .border_color(cx.theme().colors().border)
266 .child(message_body),
267 )
268 .into_any(),
269 Role::Assistant => div()
270 .text_ui(cx)
271 .p_5()
272 .pt_2()
273 .child(message_body)
274 .into_any(),
275 }
276 }
277 AgentThreadEntryContent::ToolCall(tool_call) => div()
278 .px_2()
279 .py_4()
280 .child(self.render_tool_call(tool_call, window, cx))
281 .into_any(),
282 }
283 }
284
285 fn render_tool_call(&self, tool_call: &ToolCall, window: &Window, cx: &Context<Self>) -> Div {
286 let status_icon = match &tool_call.status {
287 ToolCallStatus::WaitingForConfirmation { .. } => Empty.into_element().into_any(),
288 ToolCallStatus::Allowed {
289 status: acp::ToolCallStatus::Running,
290 ..
291 } => Icon::new(IconName::ArrowCircle)
292 .color(Color::Success)
293 .size(IconSize::Small)
294 .with_animation(
295 "running",
296 Animation::new(Duration::from_secs(2)).repeat(),
297 |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
298 )
299 .into_any_element(),
300 ToolCallStatus::Allowed {
301 status: acp::ToolCallStatus::Finished,
302 ..
303 } => Icon::new(IconName::Check)
304 .color(Color::Success)
305 .size(IconSize::Small)
306 .into_any_element(),
307 ToolCallStatus::Rejected
308 | ToolCallStatus::Allowed {
309 status: acp::ToolCallStatus::Error,
310 ..
311 } => Icon::new(IconName::X)
312 .color(Color::Error)
313 .size(IconSize::Small)
314 .into_any_element(),
315 };
316
317 let content = match &tool_call.status {
318 ToolCallStatus::WaitingForConfirmation { confirmation, .. } => {
319 Some(self.render_tool_call_confirmation(tool_call.id, confirmation, cx))
320 }
321 ToolCallStatus::Allowed { content, .. } => content.clone().map(|content| {
322 div()
323 .border_color(cx.theme().colors().border)
324 .border_t_1()
325 .px_2()
326 .py_1p5()
327 .child(MarkdownElement::new(
328 content,
329 default_markdown_style(window, cx),
330 ))
331 .into_any_element()
332 }),
333 ToolCallStatus::Rejected => None,
334 };
335
336 v_flex()
337 .text_xs()
338 .rounded_md()
339 .border_1()
340 .border_color(cx.theme().colors().border)
341 .bg(cx.theme().colors().editor_background)
342 .child(
343 h_flex()
344 .px_2()
345 .py_1p5()
346 .w_full()
347 .gap_1p5()
348 .child(
349 Icon::new(tool_call.icon.into())
350 .size(IconSize::Small)
351 .color(Color::Muted),
352 )
353 // todo! danilo please help
354 .child(MarkdownElement::new(
355 tool_call.label.clone(),
356 default_markdown_style(window, cx),
357 ))
358 .child(div().w_full())
359 .child(status_icon),
360 )
361 .children(content)
362 }
363
364 fn render_tool_call_confirmation(
365 &self,
366 tool_call_id: ToolCallId,
367 confirmation: &ToolCallConfirmation,
368 cx: &Context<Self>,
369 ) -> AnyElement {
370 match confirmation {
371 ToolCallConfirmation::Edit {
372 file_name,
373 file_diff,
374 description,
375 } => v_flex()
376 .border_color(cx.theme().colors().border)
377 .border_t_1()
378 .px_2()
379 .py_1p5()
380 // todo! nicer rendering
381 .child(file_name.clone())
382 .child(file_diff.clone())
383 .children(description.clone())
384 .child(
385 h_flex()
386 .justify_end()
387 .gap_1()
388 .child(
389 Button::new(
390 ("always_allow", tool_call_id.as_u64()),
391 "Always Allow Edits",
392 )
393 .icon(IconName::CheckDouble)
394 .icon_position(IconPosition::Start)
395 .icon_size(IconSize::Small)
396 .icon_color(Color::Success)
397 .on_click(cx.listener({
398 let id = tool_call_id;
399 move |this, _, _, cx| {
400 this.authorize_tool_call(
401 id,
402 acp::ToolCallConfirmationOutcome::AlwaysAllow,
403 cx,
404 );
405 }
406 })),
407 )
408 .child(
409 Button::new(("allow", tool_call_id.as_u64()), "Allow")
410 .icon(IconName::Check)
411 .icon_position(IconPosition::Start)
412 .icon_size(IconSize::Small)
413 .icon_color(Color::Success)
414 .on_click(cx.listener({
415 let id = tool_call_id;
416 move |this, _, _, cx| {
417 this.authorize_tool_call(
418 id,
419 acp::ToolCallConfirmationOutcome::Allow,
420 cx,
421 );
422 }
423 })),
424 )
425 .child(
426 Button::new(("reject", tool_call_id.as_u64()), "Reject")
427 .icon(IconName::X)
428 .icon_position(IconPosition::Start)
429 .icon_size(IconSize::Small)
430 .icon_color(Color::Error)
431 .on_click(cx.listener({
432 let id = tool_call_id;
433 move |this, _, _, cx| {
434 this.authorize_tool_call(
435 id,
436 acp::ToolCallConfirmationOutcome::Reject,
437 cx,
438 );
439 }
440 })),
441 ),
442 )
443 .into_any(),
444 ToolCallConfirmation::Execute {
445 command,
446 root_command,
447 description,
448 } => v_flex()
449 .border_color(cx.theme().colors().border)
450 .border_t_1()
451 .px_2()
452 .py_1p5()
453 // todo! nicer rendering
454 .child(command.clone())
455 .children(description.clone())
456 .child(
457 h_flex()
458 .justify_end()
459 .gap_1()
460 .child(
461 Button::new(
462 ("always_allow", tool_call_id.as_u64()),
463 format!("Always Allow {root_command}"),
464 )
465 .icon(IconName::CheckDouble)
466 .icon_position(IconPosition::Start)
467 .icon_size(IconSize::Small)
468 .icon_color(Color::Success)
469 .on_click(cx.listener({
470 let id = tool_call_id;
471 move |this, _, _, cx| {
472 this.authorize_tool_call(
473 id,
474 acp::ToolCallConfirmationOutcome::AlwaysAllow,
475 cx,
476 );
477 }
478 })),
479 )
480 .child(
481 Button::new(("allow", tool_call_id.as_u64()), "Allow")
482 .icon(IconName::Check)
483 .icon_position(IconPosition::Start)
484 .icon_size(IconSize::Small)
485 .icon_color(Color::Success)
486 .on_click(cx.listener({
487 let id = tool_call_id;
488 move |this, _, _, cx| {
489 this.authorize_tool_call(
490 id,
491 acp::ToolCallConfirmationOutcome::Allow,
492 cx,
493 );
494 }
495 })),
496 )
497 .child(
498 Button::new(("reject", tool_call_id.as_u64()), "Reject")
499 .icon(IconName::X)
500 .icon_position(IconPosition::Start)
501 .icon_size(IconSize::Small)
502 .icon_color(Color::Error)
503 .on_click(cx.listener({
504 let id = tool_call_id;
505 move |this, _, _, cx| {
506 this.authorize_tool_call(
507 id,
508 acp::ToolCallConfirmationOutcome::Reject,
509 cx,
510 );
511 }
512 })),
513 ),
514 )
515 .into_any(),
516 ToolCallConfirmation::Mcp {
517 server_name,
518 tool_name: _,
519 tool_display_name,
520 description,
521 } => v_flex()
522 .border_color(cx.theme().colors().border)
523 .border_t_1()
524 .px_2()
525 .py_1p5()
526 // todo! nicer rendering
527 .child(format!("{server_name} - {tool_display_name}"))
528 .children(description.clone())
529 .child(
530 h_flex()
531 .justify_end()
532 .gap_1()
533 .child(
534 Button::new(
535 ("always_allow_server", tool_call_id.as_u64()),
536 format!("Always Allow {server_name}"),
537 )
538 .icon(IconName::CheckDouble)
539 .icon_position(IconPosition::Start)
540 .icon_size(IconSize::Small)
541 .icon_color(Color::Success)
542 .on_click(cx.listener({
543 let id = tool_call_id;
544 move |this, _, _, cx| {
545 this.authorize_tool_call(
546 id,
547 acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
548 cx,
549 );
550 }
551 })),
552 )
553 .child(
554 Button::new(
555 ("always_allow_tool", tool_call_id.as_u64()),
556 format!("Always Allow {tool_display_name}"),
557 )
558 .icon(IconName::CheckDouble)
559 .icon_position(IconPosition::Start)
560 .icon_size(IconSize::Small)
561 .icon_color(Color::Success)
562 .on_click(cx.listener({
563 let id = tool_call_id;
564 move |this, _, _, cx| {
565 this.authorize_tool_call(
566 id,
567 acp::ToolCallConfirmationOutcome::AlwaysAllowTool,
568 cx,
569 );
570 }
571 })),
572 )
573 .child(
574 Button::new(("allow", tool_call_id.as_u64()), "Allow")
575 .icon(IconName::Check)
576 .icon_position(IconPosition::Start)
577 .icon_size(IconSize::Small)
578 .icon_color(Color::Success)
579 .on_click(cx.listener({
580 let id = tool_call_id;
581 move |this, _, _, cx| {
582 this.authorize_tool_call(
583 id,
584 acp::ToolCallConfirmationOutcome::Allow,
585 cx,
586 );
587 }
588 })),
589 )
590 .child(
591 Button::new(("reject", tool_call_id.as_u64()), "Reject")
592 .icon(IconName::X)
593 .icon_position(IconPosition::Start)
594 .icon_size(IconSize::Small)
595 .icon_color(Color::Error)
596 .on_click(cx.listener({
597 let id = tool_call_id;
598 move |this, _, _, cx| {
599 this.authorize_tool_call(
600 id,
601 acp::ToolCallConfirmationOutcome::Reject,
602 cx,
603 );
604 }
605 })),
606 ),
607 )
608 .into_any(),
609 ToolCallConfirmation::Fetch { description, urls } => v_flex()
610 .border_color(cx.theme().colors().border)
611 .border_t_1()
612 .px_2()
613 .py_1p5()
614 // todo! nicer rendering
615 .children(urls.clone())
616 .children(description.clone())
617 .child(
618 h_flex()
619 .justify_end()
620 .gap_1()
621 .child(
622 Button::new(("always_allow", tool_call_id.as_u64()), "Always Allow")
623 .icon(IconName::CheckDouble)
624 .icon_position(IconPosition::Start)
625 .icon_size(IconSize::Small)
626 .icon_color(Color::Success)
627 .on_click(cx.listener({
628 let id = tool_call_id;
629 move |this, _, _, cx| {
630 this.authorize_tool_call(
631 id,
632 acp::ToolCallConfirmationOutcome::AlwaysAllow,
633 cx,
634 );
635 }
636 })),
637 )
638 .child(
639 Button::new(("allow", tool_call_id.as_u64()), "Allow")
640 .icon(IconName::Check)
641 .icon_position(IconPosition::Start)
642 .icon_size(IconSize::Small)
643 .icon_color(Color::Success)
644 .on_click(cx.listener({
645 let id = tool_call_id;
646 move |this, _, _, cx| {
647 this.authorize_tool_call(
648 id,
649 acp::ToolCallConfirmationOutcome::Allow,
650 cx,
651 );
652 }
653 })),
654 )
655 .child(
656 Button::new(("reject", tool_call_id.as_u64()), "Reject")
657 .icon(IconName::X)
658 .icon_position(IconPosition::Start)
659 .icon_size(IconSize::Small)
660 .icon_color(Color::Error)
661 .on_click(cx.listener({
662 let id = tool_call_id;
663 move |this, _, _, cx| {
664 this.authorize_tool_call(
665 id,
666 acp::ToolCallConfirmationOutcome::Reject,
667 cx,
668 );
669 }
670 })),
671 ),
672 )
673 .into_any(),
674 ToolCallConfirmation::Other { description } => v_flex()
675 .border_color(cx.theme().colors().border)
676 .border_t_1()
677 .px_2()
678 .py_1p5()
679 // todo! nicer rendering
680 .child(description.clone())
681 .child(
682 h_flex()
683 .justify_end()
684 .gap_1()
685 .child(
686 Button::new(("always_allow", tool_call_id.as_u64()), "Always Allow")
687 .icon(IconName::CheckDouble)
688 .icon_position(IconPosition::Start)
689 .icon_size(IconSize::Small)
690 .icon_color(Color::Success)
691 .on_click(cx.listener({
692 let id = tool_call_id;
693 move |this, _, _, cx| {
694 this.authorize_tool_call(
695 id,
696 acp::ToolCallConfirmationOutcome::AlwaysAllow,
697 cx,
698 );
699 }
700 })),
701 )
702 .child(
703 Button::new(("allow", tool_call_id.as_u64()), "Allow")
704 .icon(IconName::Check)
705 .icon_position(IconPosition::Start)
706 .icon_size(IconSize::Small)
707 .icon_color(Color::Success)
708 .on_click(cx.listener({
709 let id = tool_call_id;
710 move |this, _, _, cx| {
711 this.authorize_tool_call(
712 id,
713 acp::ToolCallConfirmationOutcome::Allow,
714 cx,
715 );
716 }
717 })),
718 )
719 .child(
720 Button::new(("reject", tool_call_id.as_u64()), "Reject")
721 .icon(IconName::X)
722 .icon_position(IconPosition::Start)
723 .icon_size(IconSize::Small)
724 .icon_color(Color::Error)
725 .on_click(cx.listener({
726 let id = tool_call_id;
727 move |this, _, _, cx| {
728 this.authorize_tool_call(
729 id,
730 acp::ToolCallConfirmationOutcome::Reject,
731 cx,
732 );
733 }
734 })),
735 ),
736 )
737 .into_any(),
738 }
739 }
740}
741
742impl Focusable for AcpThreadView {
743 fn focus_handle(&self, cx: &App) -> FocusHandle {
744 self.message_editor.focus_handle(cx)
745 }
746}
747
748impl Render for AcpThreadView {
749 fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
750 let text = self.message_editor.read(cx).text(cx);
751 let is_editor_empty = text.is_empty();
752 let focus_handle = self.message_editor.focus_handle(cx);
753
754 v_flex()
755 .key_context("MessageEditor")
756 .on_action(cx.listener(Self::chat))
757 .h_full()
758 .child(match &self.thread_state {
759 ThreadState::Loading { .. } => v_flex()
760 .p_2()
761 .flex_1()
762 .justify_end()
763 .child(Label::new("Connecting to Gemini...")),
764 ThreadState::LoadError(e) => div()
765 .p_2()
766 .flex_1()
767 .justify_end()
768 .child(Label::new(format!("Failed to load: {e}")).into_any_element()),
769 ThreadState::Ready { thread, .. } => v_flex()
770 .flex_1()
771 .gap_2()
772 .pb_2()
773 .child(
774 list(self.list_state.clone())
775 .with_sizing_behavior(gpui::ListSizingBehavior::Auto)
776 .flex_grow(),
777 )
778 .child(div().px_3().children(if self.send_task.is_none() {
779 None
780 } else {
781 Label::new(if thread.read(cx).waiting_for_tool_confirmation() {
782 "Waiting for tool confirmation"
783 } else {
784 "Generating..."
785 })
786 .color(Color::Muted)
787 .size(LabelSize::Small)
788 .into()
789 })),
790 })
791 .child(
792 v_flex()
793 .bg(cx.theme().colors().editor_background)
794 .border_t_1()
795 .border_color(cx.theme().colors().border)
796 .p_2()
797 .gap_2()
798 .child(self.message_editor.clone())
799 .child(h_flex().justify_end().child(if self.send_task.is_some() {
800 IconButton::new("stop-generation", IconName::StopFilled)
801 .icon_color(Color::Error)
802 .style(ButtonStyle::Tinted(ui::TintColor::Error))
803 .tooltip(move |window, cx| {
804 Tooltip::for_action(
805 "Stop Generation",
806 &editor::actions::Cancel,
807 window,
808 cx,
809 )
810 })
811 .disabled(is_editor_empty)
812 .on_click(cx.listener(|this, _event, _, _| this.cancel()))
813 } else {
814 IconButton::new("send-message", IconName::Send)
815 .icon_color(Color::Accent)
816 .style(ButtonStyle::Filled)
817 .disabled(is_editor_empty)
818 .on_click({
819 let focus_handle = focus_handle.clone();
820 move |_event, window, cx| {
821 focus_handle.dispatch_action(&Chat, window, cx);
822 }
823 })
824 .when(!is_editor_empty, |button| {
825 button.tooltip(move |window, cx| {
826 Tooltip::for_action("Send", &Chat, window, cx)
827 })
828 })
829 .when(is_editor_empty, |button| {
830 button.tooltip(Tooltip::text("Type a message to submit"))
831 })
832 })),
833 )
834 }
835}
836
837fn user_message_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
838 let mut style = default_markdown_style(window, cx);
839 let mut text_style = window.text_style();
840 let theme_settings = ThemeSettings::get_global(cx);
841
842 let buffer_font = theme_settings.buffer_font.family.clone();
843 let buffer_font_size = TextSize::Small.rems(cx);
844
845 text_style.refine(&TextStyleRefinement {
846 font_family: Some(buffer_font),
847 font_size: Some(buffer_font_size.into()),
848 ..Default::default()
849 });
850
851 style.base_text_style = text_style;
852 style
853}
854
855fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
856 let theme_settings = ThemeSettings::get_global(cx);
857 let colors = cx.theme().colors();
858 let ui_font_size = TextSize::Default.rems(cx);
859 let buffer_font_size = TextSize::Small.rems(cx);
860 let mut text_style = window.text_style();
861 let line_height = buffer_font_size * 1.75;
862
863 text_style.refine(&TextStyleRefinement {
864 font_family: Some(theme_settings.ui_font.family.clone()),
865 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
866 font_features: Some(theme_settings.ui_font.features.clone()),
867 font_size: Some(ui_font_size.into()),
868 line_height: Some(line_height.into()),
869 color: Some(cx.theme().colors().text),
870 ..Default::default()
871 });
872
873 MarkdownStyle {
874 base_text_style: text_style.clone(),
875 syntax: cx.theme().syntax().clone(),
876 selection_background_color: cx.theme().colors().element_selection_background,
877 code_block_overflow_x_scroll: true,
878 table_overflow_x_scroll: true,
879 heading_level_styles: Some(HeadingLevelStyles {
880 h1: Some(TextStyleRefinement {
881 font_size: Some(rems(1.15).into()),
882 ..Default::default()
883 }),
884 h2: Some(TextStyleRefinement {
885 font_size: Some(rems(1.1).into()),
886 ..Default::default()
887 }),
888 h3: Some(TextStyleRefinement {
889 font_size: Some(rems(1.05).into()),
890 ..Default::default()
891 }),
892 h4: Some(TextStyleRefinement {
893 font_size: Some(rems(1.).into()),
894 ..Default::default()
895 }),
896 h5: Some(TextStyleRefinement {
897 font_size: Some(rems(0.95).into()),
898 ..Default::default()
899 }),
900 h6: Some(TextStyleRefinement {
901 font_size: Some(rems(0.875).into()),
902 ..Default::default()
903 }),
904 }),
905 code_block: StyleRefinement {
906 padding: EdgesRefinement {
907 top: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
908 left: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
909 right: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
910 bottom: Some(DefiniteLength::Absolute(AbsoluteLength::Pixels(Pixels(8.)))),
911 },
912 background: Some(colors.editor_background.into()),
913 text: Some(TextStyleRefinement {
914 font_family: Some(theme_settings.buffer_font.family.clone()),
915 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
916 font_features: Some(theme_settings.buffer_font.features.clone()),
917 font_size: Some(buffer_font_size.into()),
918 ..Default::default()
919 }),
920 ..Default::default()
921 },
922 inline_code: TextStyleRefinement {
923 font_family: Some(theme_settings.buffer_font.family.clone()),
924 font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
925 font_features: Some(theme_settings.buffer_font.features.clone()),
926 font_size: Some(buffer_font_size.into()),
927 background_color: Some(colors.editor_foreground.opacity(0.08)),
928 ..Default::default()
929 },
930 link: TextStyleRefinement {
931 background_color: Some(colors.editor_foreground.opacity(0.025)),
932 underline: Some(UnderlineStyle {
933 color: Some(colors.text_accent.opacity(0.5)),
934 thickness: px(1.),
935 ..Default::default()
936 }),
937 ..Default::default()
938 },
939 link_callback: Some(Rc::new(move |_url, _cx| {
940 // todo!()
941 // if MentionLink::is_valid(url) {
942 // let colors = cx.theme().colors();
943 // Some(TextStyleRefinement {
944 // background_color: Some(colors.element_background),
945 // ..Default::default()
946 // })
947 // } else {
948 None
949 // }
950 })),
951 ..Default::default()
952 }
953}