thread_view.rs

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