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 = 10;
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(div().when(is_editor_expanded, |this| this.h_full()).child({
428 let settings = ThemeSettings::get_global(cx);
429
430 let text_style = TextStyle {
431 color: cx.theme().colors().text,
432 font_family: settings.buffer_font.family.clone(),
433 font_fallbacks: settings.buffer_font.fallbacks.clone(),
434 font_features: settings.buffer_font.features.clone(),
435 font_size: font_size.into(),
436 line_height: line_height.into(),
437 ..Default::default()
438 };
439
440 EditorElement::new(
441 &self.editor,
442 EditorStyle {
443 background: editor_bg_color,
444 local_player: cx.theme().players().local(),
445 text: text_style,
446 syntax: cx.theme().syntax().clone(),
447 ..Default::default()
448 },
449 )
450 .into_any()
451 }))
452 .child(
453 h_flex()
454 .flex_none()
455 .justify_between()
456 .child(h_flex().gap_2().child(self.profile_selector.clone()))
457 .child(h_flex().gap_1().child(self.model_selector.clone()).map({
458 let focus_handle = focus_handle.clone();
459 move |parent| {
460 if is_generating {
461 parent.child(
462 IconButton::new(
463 "stop-generation",
464 IconName::StopFilled,
465 )
466 .icon_color(Color::Error)
467 .style(ButtonStyle::Tinted(ui::TintColor::Error))
468 .tooltip(move |window, cx| {
469 Tooltip::for_action(
470 "Stop Generation",
471 &editor::actions::Cancel,
472 window,
473 cx,
474 )
475 })
476 .on_click({
477 let focus_handle = focus_handle.clone();
478 move |_event, window, cx| {
479 focus_handle.dispatch_action(
480 &editor::actions::Cancel,
481 window,
482 cx,
483 );
484 }
485 })
486 .with_animation(
487 "pulsating-label",
488 Animation::new(Duration::from_secs(2))
489 .repeat()
490 .with_easing(pulsating_between(0.4, 1.0)),
491 |icon_button, delta| icon_button.alpha(delta),
492 ),
493 )
494 } else {
495 parent.child(
496 IconButton::new("send-message", IconName::Send)
497 .icon_color(Color::Accent)
498 .style(ButtonStyle::Filled)
499 .disabled(
500 is_editor_empty
501 || !is_model_selected
502 || self.waiting_for_summaries_to_send,
503 )
504 .on_click({
505 let focus_handle = focus_handle.clone();
506 move |_event, window, cx| {
507 focus_handle
508 .dispatch_action(&Chat, window, cx);
509 }
510 })
511 .when(
512 !is_editor_empty && is_model_selected,
513 |button| {
514 button.tooltip(move |window, cx| {
515 Tooltip::for_action(
516 "Send", &Chat, window, cx,
517 )
518 })
519 },
520 )
521 .when(is_editor_empty, |button| {
522 button.tooltip(Tooltip::text(
523 "Type a message to submit",
524 ))
525 })
526 .when(!is_model_selected, |button| {
527 button.tooltip(Tooltip::text(
528 "Select a model to continue",
529 ))
530 }),
531 )
532 }
533 }
534 })),
535 ),
536 )
537 }
538
539 fn render_changed_buffers(
540 &self,
541 changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
542 window: &mut Window,
543 cx: &mut Context<Self>,
544 ) -> Div {
545 let focus_handle = self.editor.focus_handle(cx);
546
547 let editor_bg_color = cx.theme().colors().editor_background;
548 let border_color = cx.theme().colors().border;
549 let active_color = cx.theme().colors().element_selected;
550 let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
551 let is_edit_changes_expanded = self.edits_expanded;
552
553 v_flex()
554 .mx_2()
555 .bg(bg_edit_files_disclosure)
556 .border_1()
557 .border_b_0()
558 .border_color(border_color)
559 .rounded_t_md()
560 .shadow(smallvec::smallvec![gpui::BoxShadow {
561 color: gpui::black().opacity(0.15),
562 offset: point(px(1.), px(-1.)),
563 blur_radius: px(3.),
564 spread_radius: px(0.),
565 }])
566 .child(
567 h_flex()
568 .id("edits-container")
569 .cursor_pointer()
570 .p_1p5()
571 .justify_between()
572 .when(is_edit_changes_expanded, |this| {
573 this.border_b_1().border_color(border_color)
574 })
575 .on_click(
576 cx.listener(|this, _, window, cx| this.handle_review_click(window, cx)),
577 )
578 .child(
579 h_flex()
580 .gap_1()
581 .child(
582 Disclosure::new("edits-disclosure", is_edit_changes_expanded)
583 .on_click(cx.listener(|this, _ev, _window, cx| {
584 this.edits_expanded = !this.edits_expanded;
585 cx.notify();
586 })),
587 )
588 .child(
589 Label::new("Edits")
590 .size(LabelSize::Small)
591 .color(Color::Muted),
592 )
593 .child(Label::new("•").size(LabelSize::XSmall).color(Color::Muted))
594 .child(
595 Label::new(format!(
596 "{} {}",
597 changed_buffers.len(),
598 if changed_buffers.len() == 1 {
599 "file"
600 } else {
601 "files"
602 }
603 ))
604 .size(LabelSize::Small)
605 .color(Color::Muted),
606 ),
607 )
608 .child(
609 Button::new("review", "Review Changes")
610 .label_size(LabelSize::Small)
611 .key_binding(
612 KeyBinding::for_action_in(
613 &OpenAgentDiff,
614 &focus_handle,
615 window,
616 cx,
617 )
618 .map(|kb| kb.size(rems_from_px(12.))),
619 )
620 .on_click(cx.listener(|this, _, window, cx| {
621 this.handle_review_click(window, cx)
622 })),
623 ),
624 )
625 .when(is_edit_changes_expanded, |parent| {
626 parent.child(
627 v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
628 |(index, (buffer, _diff))| {
629 let file = buffer.read(cx).file()?;
630 let path = file.path();
631
632 let parent_label = path.parent().and_then(|parent| {
633 let parent_str = parent.to_string_lossy();
634
635 if parent_str.is_empty() {
636 None
637 } else {
638 Some(
639 Label::new(format!(
640 "/{}{}",
641 parent_str,
642 std::path::MAIN_SEPARATOR_STR
643 ))
644 .color(Color::Muted)
645 .size(LabelSize::XSmall)
646 .buffer_font(cx),
647 )
648 }
649 });
650
651 let name_label = path.file_name().map(|name| {
652 Label::new(name.to_string_lossy().to_string())
653 .size(LabelSize::XSmall)
654 .buffer_font(cx)
655 });
656
657 let file_icon = FileIcons::get_icon(&path, cx)
658 .map(Icon::from_path)
659 .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
660 .unwrap_or_else(|| {
661 Icon::new(IconName::File)
662 .color(Color::Muted)
663 .size(IconSize::Small)
664 });
665
666 let hover_color = cx
667 .theme()
668 .colors()
669 .element_background
670 .blend(cx.theme().colors().editor_foreground.opacity(0.025));
671
672 let overlay_gradient = linear_gradient(
673 90.,
674 linear_color_stop(editor_bg_color, 1.),
675 linear_color_stop(editor_bg_color.opacity(0.2), 0.),
676 );
677
678 let overlay_gradient_hover = linear_gradient(
679 90.,
680 linear_color_stop(hover_color, 1.),
681 linear_color_stop(hover_color.opacity(0.2), 0.),
682 );
683
684 let element = h_flex()
685 .group("edited-code")
686 .id(("file-container", index))
687 .cursor_pointer()
688 .relative()
689 .py_1()
690 .pl_2()
691 .pr_1()
692 .gap_2()
693 .justify_between()
694 .bg(cx.theme().colors().editor_background)
695 .hover(|style| style.bg(hover_color))
696 .when(index + 1 < changed_buffers.len(), |parent| {
697 parent.border_color(border_color).border_b_1()
698 })
699 .child(
700 h_flex()
701 .id("file-name")
702 .pr_8()
703 .gap_1p5()
704 .max_w_full()
705 .overflow_x_scroll()
706 .child(file_icon)
707 .child(
708 h_flex()
709 .gap_0p5()
710 .children(name_label)
711 .children(parent_label),
712 ) // TODO: show lines changed
713 .child(Label::new("+").color(Color::Created))
714 .child(Label::new("-").color(Color::Deleted)),
715 )
716 .child(
717 div().visible_on_hover("edited-code").child(
718 Button::new("review", "Review")
719 .label_size(LabelSize::Small)
720 .on_click({
721 let buffer = buffer.clone();
722 cx.listener(move |this, _, window, cx| {
723 this.handle_file_click(
724 buffer.clone(),
725 window,
726 cx,
727 );
728 })
729 }),
730 ),
731 )
732 .child(
733 div()
734 .id("gradient-overlay")
735 .absolute()
736 .h_5_6()
737 .w_12()
738 .bottom_0()
739 .right(px(52.))
740 .bg(overlay_gradient)
741 .group_hover("edited-code", |style| {
742 style.bg(overlay_gradient_hover)
743 }),
744 )
745 .on_click({
746 let buffer = buffer.clone();
747 cx.listener(move |this, _, window, cx| {
748 this.handle_file_click(buffer.clone(), window, cx);
749 })
750 });
751
752 Some(element)
753 },
754 )),
755 )
756 })
757 }
758
759 fn render_reaching_token_limit(&self, line_height: Pixels, cx: &mut Context<Self>) -> Div {
760 h_flex()
761 .p_2()
762 .gap_2()
763 .flex_wrap()
764 .justify_between()
765 .bg(cx.theme().status().warning_background.opacity(0.1))
766 .border_t_1()
767 .border_color(cx.theme().colors().border)
768 .child(
769 h_flex()
770 .gap_2()
771 .items_start()
772 .child(
773 h_flex()
774 .h(line_height)
775 .justify_center()
776 .child(
777 Icon::new(IconName::Warning)
778 .color(Color::Warning)
779 .size(IconSize::XSmall),
780 ),
781 )
782 .child(
783 v_flex()
784 .mr_auto()
785 .child(Label::new("Thread reaching the token limit soon").size(LabelSize::Small))
786 .child(
787 Label::new(
788 "Start a new thread from a summary to continue the conversation.",
789 )
790 .size(LabelSize::Small)
791 .color(Color::Muted),
792 ),
793 ),
794 )
795 .child(
796 Button::new("new-thread", "Start New Thread")
797 .on_click(cx.listener(|this, _, window, cx| {
798 let from_thread_id = Some(this.thread.read(cx).id().clone());
799
800 window.dispatch_action(Box::new(NewThread {
801 from_thread_id
802 }), cx);
803 }))
804 .icon(IconName::Plus)
805 .icon_position(IconPosition::Start)
806 .icon_size(IconSize::Small)
807 .style(ButtonStyle::Tinted(ui::TintColor::Accent))
808 .label_size(LabelSize::Small),
809 )
810 }
811}
812
813impl Focusable for MessageEditor {
814 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
815 self.editor.focus_handle(cx)
816 }
817}
818
819impl Render for MessageEditor {
820 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
821 let thread = self.thread.read(cx);
822 let total_token_usage = thread.total_token_usage(cx);
823
824 let action_log = self.thread.read(cx).action_log();
825 let changed_buffers = action_log.read(cx).changed_buffers(cx);
826
827 let font_size = TextSize::Small.rems(cx);
828 let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
829
830 v_flex()
831 .size_full()
832 .when(self.waiting_for_summaries_to_send, |parent| {
833 parent.child(
834 h_flex().py_3().w_full().justify_center().child(
835 h_flex()
836 .flex_none()
837 .px_2()
838 .py_2()
839 .bg(cx.theme().colors().editor_background)
840 .border_1()
841 .border_color(cx.theme().colors().border_variant)
842 .rounded_lg()
843 .shadow_md()
844 .gap_1()
845 .child(
846 Icon::new(IconName::ArrowCircle)
847 .size(IconSize::XSmall)
848 .color(Color::Muted)
849 .with_animation(
850 "arrow-circle",
851 Animation::new(Duration::from_secs(2)).repeat(),
852 |icon, delta| {
853 icon.transform(gpui::Transformation::rotate(
854 gpui::percentage(delta),
855 ))
856 },
857 ),
858 )
859 .child(
860 Label::new("Summarizing context…")
861 .size(LabelSize::XSmall)
862 .color(Color::Muted),
863 ),
864 ),
865 )
866 })
867 .when(changed_buffers.len() > 0, |parent| {
868 parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
869 })
870 .child(self.render_editor(font_size, line_height, window, cx))
871 .when(
872 total_token_usage.ratio != TokenUsageRatio::Normal,
873 |parent| parent.child(self.render_reaching_token_limit(line_height, cx)),
874 )
875 }
876}