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