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