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