message_editor.rs

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