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