message_editor.rs

  1use std::sync::Arc;
  2
  3use crate::assistant_model_selector::ModelType;
  4use collections::HashSet;
  5use editor::actions::MoveUp;
  6use editor::{ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorStyle};
  7use file_icons::FileIcons;
  8use fs::Fs;
  9use gpui::{
 10    Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription, TextStyle,
 11    WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
 12};
 13use language::Buffer;
 14use language_model::{ConfiguredModel, LanguageModelRegistry};
 15use language_model_selector::ToggleModelSelector;
 16use multi_buffer;
 17use project::Project;
 18use settings::Settings;
 19use std::time::Duration;
 20use theme::ThemeSettings;
 21use ui::{Disclosure, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
 22use util::ResultExt as _;
 23use workspace::Workspace;
 24
 25use crate::assistant_model_selector::AssistantModelSelector;
 26use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider};
 27use crate::context_store::{ContextStore, refresh_context_store_text};
 28use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
 29use crate::profile_selector::ProfileSelector;
 30use crate::thread::{RequestKind, Thread, TokenUsageRatio};
 31use crate::thread_store::ThreadStore;
 32use crate::{
 33    AgentDiff, Chat, ChatMode, NewThread, OpenAgentDiff, RemoveAllContext, ThreadEvent,
 34    ToggleContextPicker, ToggleProfileSelector,
 35};
 36
 37pub struct MessageEditor {
 38    thread: Entity<Thread>,
 39    editor: Entity<Editor>,
 40    #[allow(dead_code)]
 41    workspace: WeakEntity<Workspace>,
 42    project: Entity<Project>,
 43    context_store: Entity<ContextStore>,
 44    context_strip: Entity<ContextStrip>,
 45    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
 46    inline_context_picker: Entity<ContextPicker>,
 47    inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
 48    model_selector: Entity<AssistantModelSelector>,
 49    profile_selector: Entity<ProfileSelector>,
 50    edits_expanded: bool,
 51    waiting_for_summaries_to_send: bool,
 52    _subscriptions: Vec<Subscription>,
 53}
 54
 55impl MessageEditor {
 56    pub fn new(
 57        fs: Arc<dyn Fs>,
 58        workspace: WeakEntity<Workspace>,
 59        context_store: Entity<ContextStore>,
 60        thread_store: WeakEntity<ThreadStore>,
 61        thread: Entity<Thread>,
 62        window: &mut Window,
 63        cx: &mut Context<Self>,
 64    ) -> Self {
 65        let context_picker_menu_handle = PopoverMenuHandle::default();
 66        let inline_context_picker_menu_handle = PopoverMenuHandle::default();
 67        let model_selector_menu_handle = PopoverMenuHandle::default();
 68
 69        let editor = cx.new(|cx| {
 70            let mut editor = Editor::auto_height(10, window, cx);
 71            editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
 72            editor.set_show_indent_guides(false, cx);
 73            editor.set_context_menu_options(ContextMenuOptions {
 74                min_entries_visible: 12,
 75                max_entries_visible: 12,
 76                placement: Some(ContextMenuPlacement::Above),
 77            });
 78
 79            editor
 80        });
 81
 82        let editor_entity = editor.downgrade();
 83        editor.update(cx, |editor, _| {
 84            editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
 85                workspace.clone(),
 86                context_store.downgrade(),
 87                Some(thread_store.clone()),
 88                editor_entity,
 89            ))));
 90        });
 91
 92        let inline_context_picker = cx.new(|cx| {
 93            ContextPicker::new(
 94                workspace.clone(),
 95                Some(thread_store.clone()),
 96                context_store.downgrade(),
 97                ConfirmBehavior::Close,
 98                window,
 99                cx,
100            )
101        });
102
103        let context_strip = cx.new(|cx| {
104            ContextStrip::new(
105                context_store.clone(),
106                workspace.clone(),
107                Some(thread_store.clone()),
108                context_picker_menu_handle.clone(),
109                SuggestContextKind::File,
110                window,
111                cx,
112            )
113        });
114
115        let subscriptions = vec![
116            cx.subscribe_in(
117                &inline_context_picker,
118                window,
119                Self::handle_inline_context_picker_event,
120            ),
121            cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
122        ];
123
124        Self {
125            editor: editor.clone(),
126            project: thread.read(cx).project().clone(),
127            thread,
128            workspace,
129            context_store,
130            context_strip,
131            context_picker_menu_handle,
132            inline_context_picker,
133            inline_context_picker_menu_handle,
134            model_selector: cx.new(|cx| {
135                AssistantModelSelector::new(
136                    fs.clone(),
137                    model_selector_menu_handle,
138                    editor.focus_handle(cx),
139                    ModelType::Default,
140                    window,
141                    cx,
142                )
143            }),
144            edits_expanded: false,
145            waiting_for_summaries_to_send: false,
146            profile_selector: cx
147                .new(|cx| ProfileSelector::new(fs, thread_store, editor.focus_handle(cx), cx)),
148            _subscriptions: subscriptions,
149        }
150    }
151
152    fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {
153        cx.notify();
154    }
155
156    fn toggle_context_picker(
157        &mut self,
158        _: &ToggleContextPicker,
159        window: &mut Window,
160        cx: &mut Context<Self>,
161    ) {
162        self.context_picker_menu_handle.toggle(window, cx);
163    }
164    pub fn remove_all_context(
165        &mut self,
166        _: &RemoveAllContext,
167        _window: &mut Window,
168        cx: &mut Context<Self>,
169    ) {
170        self.context_store.update(cx, |store, _cx| store.clear());
171        cx.notify();
172    }
173
174    fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
175        if self.is_editor_empty(cx) {
176            return;
177        }
178
179        if self.thread.read(cx).is_generating() {
180            return;
181        }
182
183        self.send_to_model(RequestKind::Chat, window, cx);
184    }
185
186    fn is_editor_empty(&self, cx: &App) -> bool {
187        self.editor.read(cx).text(cx).is_empty()
188    }
189
190    fn is_model_selected(&self, cx: &App) -> bool {
191        LanguageModelRegistry::read_global(cx)
192            .default_model()
193            .is_some()
194    }
195
196    fn send_to_model(
197        &mut self,
198        request_kind: RequestKind,
199        window: &mut Window,
200        cx: &mut Context<Self>,
201    ) {
202        let model_registry = LanguageModelRegistry::read_global(cx);
203        let Some(ConfiguredModel { model, provider }) = model_registry.default_model() else {
204            return;
205        };
206
207        if provider.must_accept_terms(cx) {
208            cx.notify();
209            return;
210        }
211
212        let user_message = self.editor.update(cx, |editor, cx| {
213            let text = editor.text(cx);
214            editor.clear(window, cx);
215            text
216        });
217
218        let refresh_task =
219            refresh_context_store_text(self.context_store.clone(), &HashSet::default(), cx);
220
221        let system_prompt_context_task = self.thread.read(cx).load_system_prompt_context(cx);
222
223        let thread = self.thread.clone();
224        let context_store = self.context_store.clone();
225        let git_store = self.project.read(cx).git_store().clone();
226        let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
227
228        cx.spawn(async move |this, cx| {
229            let checkpoint = checkpoint.await.ok();
230            refresh_task.await;
231            let (system_prompt_context, load_error) = system_prompt_context_task.await;
232
233            thread
234                .update(cx, |thread, cx| {
235                    thread.set_system_prompt_context(system_prompt_context);
236                    if let Some(load_error) = load_error {
237                        cx.emit(ThreadEvent::ShowError(load_error));
238                    }
239                })
240                .log_err();
241
242            thread
243                .update(cx, |thread, cx| {
244                    let context = context_store.read(cx).context().clone();
245                    thread.insert_user_message(user_message, context, checkpoint, cx);
246                })
247                .log_err();
248
249            if let Some(wait_for_summaries) = context_store
250                .update(cx, |context_store, cx| context_store.wait_for_summaries(cx))
251                .log_err()
252            {
253                this.update(cx, |this, cx| {
254                    this.waiting_for_summaries_to_send = true;
255                    cx.notify();
256                })
257                .log_err();
258
259                wait_for_summaries.await;
260
261                this.update(cx, |this, cx| {
262                    this.waiting_for_summaries_to_send = false;
263                    cx.notify();
264                })
265                .log_err();
266            }
267
268            // Send to model after summaries are done
269            thread
270                .update(cx, |thread, cx| {
271                    thread.send_to_model(model, request_kind, cx);
272                })
273                .log_err();
274        })
275        .detach();
276    }
277
278    fn handle_inline_context_picker_event(
279        &mut self,
280        _inline_context_picker: &Entity<ContextPicker>,
281        _event: &DismissEvent,
282        window: &mut Window,
283        cx: &mut Context<Self>,
284    ) {
285        let editor_focus_handle = self.editor.focus_handle(cx);
286        window.focus(&editor_focus_handle);
287    }
288
289    fn handle_context_strip_event(
290        &mut self,
291        _context_strip: &Entity<ContextStrip>,
292        event: &ContextStripEvent,
293        window: &mut Window,
294        cx: &mut Context<Self>,
295    ) {
296        match event {
297            ContextStripEvent::PickerDismissed
298            | ContextStripEvent::BlurredEmpty
299            | ContextStripEvent::BlurredDown => {
300                let editor_focus_handle = self.editor.focus_handle(cx);
301                window.focus(&editor_focus_handle);
302            }
303            ContextStripEvent::BlurredUp => {}
304        }
305    }
306
307    fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
308        if self.context_picker_menu_handle.is_deployed()
309            || self.inline_context_picker_menu_handle.is_deployed()
310        {
311            cx.propagate();
312        } else {
313            self.context_strip.focus_handle(cx).focus(window);
314        }
315    }
316
317    fn handle_review_click(&self, window: &mut Window, cx: &mut Context<Self>) {
318        AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
319    }
320
321    fn handle_file_click(
322        &self,
323        buffer: Entity<Buffer>,
324        window: &mut Window,
325        cx: &mut Context<Self>,
326    ) {
327        if let Ok(diff) = AgentDiff::deploy(self.thread.clone(), self.workspace.clone(), window, cx)
328        {
329            let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);
330            diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));
331        }
332    }
333}
334
335impl Focusable for MessageEditor {
336    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
337        self.editor.focus_handle(cx)
338    }
339}
340
341impl Render for MessageEditor {
342    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
343        let font_size = TextSize::Default.rems(cx);
344        let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
345
346        let focus_handle = self.editor.focus_handle(cx);
347        let inline_context_picker = self.inline_context_picker.clone();
348
349        let thread = self.thread.read(cx);
350        let is_generating = thread.is_generating();
351        let total_token_usage = thread.total_token_usage(cx);
352        let is_model_selected = self.is_model_selected(cx);
353        let is_editor_empty = self.is_editor_empty(cx);
354        let is_edit_changes_expanded = self.edits_expanded;
355
356        let action_log = self.thread.read(cx).action_log();
357        let changed_buffers = action_log.read(cx).changed_buffers(cx);
358        let changed_buffers_count = changed_buffers.len();
359
360        let editor_bg_color = cx.theme().colors().editor_background;
361        let border_color = cx.theme().colors().border;
362        let active_color = cx.theme().colors().element_selected;
363        let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
364
365        v_flex()
366            .size_full()
367            .when(self.waiting_for_summaries_to_send, |parent| {
368                parent.child(
369                    h_flex().py_3().w_full().justify_center().child(
370                        h_flex()
371                            .flex_none()
372                            .px_2()
373                            .py_2()
374                            .bg(editor_bg_color)
375                            .border_1()
376                            .border_color(cx.theme().colors().border_variant)
377                            .rounded_lg()
378                            .shadow_md()
379                            .gap_1()
380                            .child(
381                                Icon::new(IconName::ArrowCircle)
382                                    .size(IconSize::XSmall)
383                                    .color(Color::Muted)
384                                    .with_animation(
385                                        "arrow-circle",
386                                        Animation::new(Duration::from_secs(2)).repeat(),
387                                        |icon, delta| {
388                                            icon.transform(gpui::Transformation::rotate(
389                                                gpui::percentage(delta),
390                                            ))
391                                        },
392                                    ),
393                            )
394                            .child(
395                                Label::new("Summarizing context…")
396                                    .size(LabelSize::XSmall)
397                                    .color(Color::Muted),
398                            ),
399                    ),
400                )
401            })
402            .when(changed_buffers_count > 0, |parent| {
403                parent.child(
404                    v_flex()
405                        .mx_2()
406                        .bg(bg_edit_files_disclosure)
407                        .border_1()
408                        .border_b_0()
409                        .border_color(border_color)
410                        .rounded_t_md()
411                        .shadow(smallvec::smallvec![gpui::BoxShadow {
412                            color: gpui::black().opacity(0.15),
413                            offset: point(px(1.), px(-1.)),
414                            blur_radius: px(3.),
415                            spread_radius: px(0.),
416                        }])
417                        .child(
418                            h_flex()
419                                .id("edits-container")
420                                .cursor_pointer()
421                                .p_1p5()
422                                .justify_between()
423                                .when(is_edit_changes_expanded, |this| {
424                                    this.border_b_1().border_color(border_color)
425                                })
426                                .on_click(cx.listener(|this, _, window, cx| {
427                                    this.handle_review_click(window, cx)
428                                }))
429                                .child(
430                                    h_flex()
431                                        .gap_1()
432                                        .child(
433                                            Disclosure::new(
434                                                "edits-disclosure",
435                                                is_edit_changes_expanded,
436                                            )
437                                            .on_click(
438                                                cx.listener(|this, _ev, _window, cx| {
439                                                    this.edits_expanded = !this.edits_expanded;
440                                                    cx.notify();
441                                                }),
442                                            ),
443                                        )
444                                        .child(
445                                            Label::new("Edits")
446                                                .size(LabelSize::Small)
447                                                .color(Color::Muted),
448                                        )
449                                        .child(
450                                            Label::new("")
451                                                .size(LabelSize::XSmall)
452                                                .color(Color::Muted),
453                                        )
454                                        .child(
455                                            Label::new(format!(
456                                                "{} {}",
457                                                changed_buffers_count,
458                                                if changed_buffers_count == 1 {
459                                                    "file"
460                                                } else {
461                                                    "files"
462                                                }
463                                            ))
464                                            .size(LabelSize::Small)
465                                            .color(Color::Muted),
466                                        ),
467                                )
468                                .child(
469                                    Button::new("review", "Review Changes")
470                                        .label_size(LabelSize::Small)
471                                        .key_binding(
472                                            KeyBinding::for_action_in(
473                                                &OpenAgentDiff,
474                                                &focus_handle,
475                                                window,
476                                                cx,
477                                            )
478                                            .map(|kb| kb.size(rems_from_px(12.))),
479                                        )
480                                        .on_click(cx.listener(|this, _, window, cx| {
481                                            this.handle_review_click(window, cx)
482                                        })),
483                                ),
484                        )
485                        .when(is_edit_changes_expanded, |parent| {
486                            parent.child(
487                                v_flex().children(
488                                    changed_buffers.into_iter().enumerate().flat_map(
489                                        |(index, (buffer, _diff))| {
490                                            let file = buffer.read(cx).file()?;
491                                            let path = file.path();
492
493                                            let parent_label = path.parent().and_then(|parent| {
494                                                let parent_str = parent.to_string_lossy();
495
496                                                if parent_str.is_empty() {
497                                                    None
498                                                } else {
499                                                    Some(
500                                                        Label::new(format!(
501                                                            "/{}{}",
502                                                            parent_str,
503                                                            std::path::MAIN_SEPARATOR_STR
504                                                        ))
505                                                        .color(Color::Muted)
506                                                        .size(LabelSize::XSmall)
507                                                        .buffer_font(cx),
508                                                    )
509                                                }
510                                            });
511
512                                            let name_label = path.file_name().map(|name| {
513                                                Label::new(name.to_string_lossy().to_string())
514                                                    .size(LabelSize::XSmall)
515                                                    .buffer_font(cx)
516                                            });
517
518                                            let file_icon = FileIcons::get_icon(&path, cx)
519                                                .map(Icon::from_path)
520                                                .map(|icon| {
521                                                    icon.color(Color::Muted).size(IconSize::Small)
522                                                })
523                                                .unwrap_or_else(|| {
524                                                    Icon::new(IconName::File)
525                                                        .color(Color::Muted)
526                                                        .size(IconSize::Small)
527                                                });
528
529                                            let hover_color = cx.theme()
530                                                .colors()
531                                                .element_background
532                                                .blend(cx.theme().colors().editor_foreground.opacity(0.025));
533
534                                            let overlay_gradient = linear_gradient(
535                                                90.,
536                                                linear_color_stop(
537                                                    editor_bg_color,
538                                                    1.,
539                                                ),
540                                                linear_color_stop(
541                                                    editor_bg_color
542                                                        .opacity(0.2),
543                                                    0.,
544                                                ),
545                                            );
546
547                                            let overlay_gradient_hover = linear_gradient(
548                                                90.,
549                                                linear_color_stop(
550                                                    hover_color,
551                                                    1.,
552                                                ),
553                                                linear_color_stop(
554                                                    hover_color
555                                                        .opacity(0.2),
556                                                    0.,
557                                                ),
558                                            );
559
560                                            let element = h_flex()
561                                                .group("edited-code")
562                                                .id(("file-container", index))
563                                                .cursor_pointer()
564                                                .relative()
565                                                .py_1()
566                                                .pl_2()
567                                                .pr_1()
568                                                .gap_2()
569                                                .justify_between()
570                                                .bg(cx.theme().colors().editor_background)
571                                                .hover(|style| style.bg(hover_color))
572                                                .when(index + 1 < changed_buffers_count, |parent| {
573                                                    parent.border_color(border_color).border_b_1()
574                                                })
575                                                .child(
576                                                    h_flex()
577                                                        .id("file-name")
578                                                        .pr_8()
579                                                        .gap_1p5()
580                                                        .max_w_full()
581                                                        .overflow_x_scroll()
582                                                        .child(file_icon)
583                                                        .child(
584                                                            h_flex()
585                                                                .gap_0p5()
586                                                                .children(name_label)
587                                                                .children(parent_label)
588                                                        ) // TODO: show lines changed
589                                                        .child(
590                                                            Label::new("+")
591                                                                .color(Color::Created),
592                                                        )
593                                                        .child(
594                                                            Label::new("-")
595                                                                .color(Color::Deleted),
596                                                        ),
597                                                )
598                                                .child(
599                                                    div().visible_on_hover("edited-code").child(
600                                                        Button::new("review", "Review")
601                                                            .label_size(LabelSize::Small)
602                                                            .on_click({
603                                                                let buffer = buffer.clone();
604                                                                cx.listener(move |this, _, window, cx| {
605                                                                    this.handle_file_click(buffer.clone(), window, cx);
606                                                                })
607                                                            })
608                                                    )
609                                                )
610                                                .child(
611                                                    div()
612                                                        .id("gradient-overlay")
613                                                        .absolute()
614                                                        .h_5_6()
615                                                        .w_12()
616                                                        .bottom_0()
617                                                        .right(px(52.))
618                                                        .bg(overlay_gradient)
619                                                        .group_hover("edited-code", |style| style.bg(overlay_gradient_hover))
620                                                    ,
621                                                )
622                                                .on_click({
623                                                    let buffer = buffer.clone();
624                                                    cx.listener(move |this, _, window, cx| {
625                                                        this.handle_file_click(buffer.clone(), window, cx);
626                                                    })
627                                                });
628
629                                            Some(element)
630                                        },
631                                    ),
632                                ),
633                            )
634                        }),
635                )
636            })
637            .child(
638                v_flex()
639                    .key_context("MessageEditor")
640                    .on_action(cx.listener(Self::chat))
641                    .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
642                        this.profile_selector
643                            .read(cx)
644                            .menu_handle()
645                            .toggle(window, cx);
646                    }))
647                    .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
648                        this.model_selector
649                            .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
650                    }))
651                    .on_action(cx.listener(Self::toggle_context_picker))
652                    .on_action(cx.listener(Self::remove_all_context))
653                    .on_action(cx.listener(Self::move_up))
654                    .on_action(cx.listener(Self::toggle_chat_mode))
655                    .gap_2()
656                    .p_2()
657                    .bg(editor_bg_color)
658                    .border_t_1()
659                    .border_color(cx.theme().colors().border)
660                    .child(h_flex().justify_between().child(self.context_strip.clone()))
661                    .child(
662                        v_flex()
663                            .gap_5()
664                            .child({
665                                    let settings = ThemeSettings::get_global(cx);
666                                    let text_style = TextStyle {
667                                        color: cx.theme().colors().text,
668                                        font_family: settings.ui_font.family.clone(),
669                                        font_fallbacks: settings.ui_font.fallbacks.clone(),
670                                        font_features: settings.ui_font.features.clone(),
671                                        font_size: font_size.into(),
672                                        font_weight: settings.ui_font.weight,
673                                        line_height: line_height.into(),
674                                        ..Default::default()
675                                    };
676
677                                    EditorElement::new(
678                                        &self.editor,
679                                        EditorStyle {
680                                            background: editor_bg_color,
681                                            local_player: cx.theme().players().local(),
682                                            text: text_style,
683                                            syntax: cx.theme().syntax().clone(),
684                                            ..Default::default()
685                                        },
686                                    ).into_any()
687                            })
688                            .child(
689                                PopoverMenu::new("inline-context-picker")
690                                    .menu(move |window, cx| {
691                                        inline_context_picker.update(cx, |this, cx| {
692                                            this.init(window, cx);
693                                        });
694                                        Some(inline_context_picker.clone())
695                                    })
696                                    .attach(gpui::Corner::TopLeft)
697                                    .anchor(gpui::Corner::BottomLeft)
698                                    .offset(gpui::Point {
699                                        x: px(0.0),
700                                        y: (-ThemeSettings::get_global(cx).ui_font_size(cx) * 2)
701                                            - px(4.0),
702                                    })
703                                    .with_handle(self.inline_context_picker_menu_handle.clone()),
704                            )
705                            .child(
706                                h_flex()
707                                    .justify_between()
708                                    .child(h_flex().gap_2().child(self.profile_selector.clone()))
709                                    .child(
710                                        h_flex().gap_1().child(self.model_selector.clone())
711                                            .map(|parent| {
712                                                if is_generating {
713                                                    parent.child(
714                                                        IconButton::new("stop-generation", IconName::StopFilled)
715                                                            .icon_color(Color::Error)
716                                                            .style(ButtonStyle::Tinted(ui::TintColor::Error))
717                                                            .tooltip(move |window, cx| {
718                                                                Tooltip::for_action(
719                                                                    "Stop Generation",
720                                                                    &editor::actions::Cancel,
721                                                                    window,
722                                                                    cx,
723                                                                )
724                                                            })
725                                                            .on_click(move |_event, window, cx| {
726                                                                focus_handle.dispatch_action(
727                                                                    &editor::actions::Cancel,
728                                                                    window,
729                                                                    cx,
730                                                                );
731                                                            })
732                                                            .with_animation(
733                                                                "pulsating-label",
734                                                                Animation::new(Duration::from_secs(2))
735                                                                    .repeat()
736                                                                    .with_easing(pulsating_between(0.4, 1.0)),
737                                                                |icon_button, delta| icon_button.alpha(delta),
738                                                            ),
739                                                    )
740                                                } else {
741                                                    parent.child(
742                                                        IconButton::new("send-message", IconName::Send)
743                                                            .icon_color(Color::Accent)
744                                                            .style(ButtonStyle::Filled)
745                                                            .disabled(
746                                                                is_editor_empty
747                                                                    || !is_model_selected
748                                                                    || self.waiting_for_summaries_to_send
749                                                            )
750                                                            .on_click(move |_event, window, cx| {
751                                                                focus_handle.dispatch_action(&Chat, window, cx);
752                                                            })
753                                                            .when(!is_editor_empty && is_model_selected, |button| {
754                                                                button.tooltip(move |window, cx| {
755                                                                    Tooltip::for_action(
756                                                                        "Send",
757                                                                        &Chat,
758                                                                        window,
759                                                                        cx,
760                                                                    )
761                                                                })
762                                                            })
763                                                            .when(is_editor_empty, |button| {
764                                                                button.tooltip(Tooltip::text(
765                                                                    "Type a message to submit",
766                                                                ))
767                                                            })
768                                                            .when(!is_model_selected, |button| {
769                                                                button.tooltip(Tooltip::text(
770                                                                    "Select a model to continue",
771                                                                ))
772                                                            })
773                                                    )
774                                                }
775                                            })
776                                    ),
777                            ),
778                    )
779            )
780            .when(total_token_usage.ratio != TokenUsageRatio::Normal, |parent| {
781                parent.child(
782                    h_flex()
783                        .p_2()
784                        .gap_2()
785                        .flex_wrap()
786                        .justify_between()
787                        .bg(cx.theme().status().warning_background.opacity(0.1))
788                        .border_t_1()
789                        .border_color(cx.theme().colors().border)
790                        .child(
791                            h_flex()
792                                .gap_2()
793                                .items_start()
794                                .child(
795                                    h_flex()
796                                        .h(line_height)
797                                        .justify_center()
798                                        .child(
799                                            Icon::new(IconName::Warning)
800                                                .color(Color::Warning)
801                                                .size(IconSize::XSmall),
802                                        ),
803                                )
804                                .child(
805                                    v_flex()
806                                        .mr_auto()
807                                        .child(Label::new("Thread reaching the token limit soon").size(LabelSize::Small))
808                                        .child(
809                                            Label::new(
810                                                "Start a new thread from a summary to continue the conversation.",
811                                            )
812                                            .size(LabelSize::Small)
813                                            .color(Color::Muted),
814                                        ),
815                                ),
816                        )
817                        .child(
818                            Button::new("new-thread", "Start New Thread")
819                                .on_click(cx.listener(|this, _, window, cx| {
820                                    let from_thread_id = Some(this.thread.read(cx).id().clone());
821
822                                    window.dispatch_action(Box::new(NewThread {
823                                        from_thread_id
824                                    }), cx);
825                                }))
826                                .icon(IconName::Plus)
827                                .icon_position(IconPosition::Start)
828                                .icon_size(IconSize::Small)
829                                .style(ButtonStyle::Tinted(ui::TintColor::Accent))
830                                .label_size(LabelSize::Small),
831                        ),
832                )
833            })
834    }
835}