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