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