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