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