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}