thread_view.rs

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