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 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}