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