message_editor.rs

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