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