thread_view.rs

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