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