1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use crate::agent_model_selector::{AgentModelSelector, ModelType};
5use crate::context::{AgentContextKey, ContextCreasesAddon, ContextLoadResult, load_context};
6use crate::tool_compatibility::{IncompatibleToolsState, IncompatibleToolsTooltip};
7use crate::ui::{
8 AnimatedLabel, MaxModeTooltip,
9 preview::{AgentPreview, UsageCallout},
10};
11use assistant_settings::{AssistantSettings, CompletionMode};
12use buffer_diff::BufferDiff;
13use client::UserStore;
14use collections::{HashMap, HashSet};
15use editor::actions::{MoveUp, Paste};
16use editor::{
17 AnchorRangeExt, ContextMenuOptions, ContextMenuPlacement, Editor, EditorElement, EditorEvent,
18 EditorMode, EditorStyle, MultiBuffer,
19};
20use file_icons::FileIcons;
21use fs::Fs;
22use futures::future::Shared;
23use futures::{FutureExt as _, future};
24use gpui::{
25 Animation, AnimationExt, App, ClipboardEntry, Entity, EventEmitter, Focusable, Subscription,
26 Task, TextStyle, WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
27};
28use language::{Buffer, Language};
29use language_model::{
30 ConfiguredModel, LanguageModelRequestMessage, MessageContent, RequestUsage,
31 ZED_CLOUD_PROVIDER_ID,
32};
33use language_model_selector::ToggleModelSelector;
34use multi_buffer;
35use project::Project;
36use prompt_store::PromptStore;
37use proto::Plan;
38use settings::Settings;
39use std::time::Duration;
40use theme::ThemeSettings;
41use ui::{Disclosure, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
42use util::{ResultExt as _, maybe};
43use workspace::{CollaboratorId, Workspace};
44
45use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
46use crate::context_store::ContextStore;
47use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
48use crate::profile_selector::ProfileSelector;
49use crate::thread::{MessageCrease, Thread, TokenUsageRatio};
50use crate::thread_store::{TextThreadStore, ThreadStore};
51use crate::{
52 ActiveThread, AgentDiffPane, Chat, ExpandMessageEditor, Follow, NewThread, OpenAgentDiff,
53 RemoveAllContext, ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
54};
55
56#[derive(RegisterComponent)]
57pub struct MessageEditor {
58 thread: Entity<Thread>,
59 incompatible_tools_state: Entity<IncompatibleToolsState>,
60 editor: Entity<Editor>,
61 workspace: WeakEntity<Workspace>,
62 project: Entity<Project>,
63 user_store: Entity<UserStore>,
64 context_store: Entity<ContextStore>,
65 prompt_store: Option<Entity<PromptStore>>,
66 context_strip: Entity<ContextStrip>,
67 context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
68 model_selector: Entity<AgentModelSelector>,
69 last_loaded_context: Option<ContextLoadResult>,
70 load_context_task: Option<Shared<Task<()>>>,
71 profile_selector: Entity<ProfileSelector>,
72 edits_expanded: bool,
73 editor_is_expanded: bool,
74 last_estimated_token_count: Option<usize>,
75 update_token_count_task: Option<Task<()>>,
76 _subscriptions: Vec<Subscription>,
77}
78
79const MAX_EDITOR_LINES: usize = 8;
80
81pub(crate) fn create_editor(
82 workspace: WeakEntity<Workspace>,
83 context_store: WeakEntity<ContextStore>,
84 thread_store: WeakEntity<ThreadStore>,
85 text_thread_store: WeakEntity<TextThreadStore>,
86 window: &mut Window,
87 cx: &mut App,
88) -> Entity<Editor> {
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("Message the agent – @ to include context", 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.register_addon(ContextCreasesAddon::new());
118 editor
119 });
120
121 let editor_entity = editor.downgrade();
122 editor.update(cx, |editor, _| {
123 editor.set_completion_provider(Some(Box::new(ContextPickerCompletionProvider::new(
124 workspace,
125 context_store,
126 Some(thread_store),
127 Some(text_thread_store),
128 editor_entity,
129 None,
130 ))));
131 });
132 editor
133}
134
135impl MessageEditor {
136 pub fn new(
137 fs: Arc<dyn Fs>,
138 workspace: WeakEntity<Workspace>,
139 user_store: Entity<UserStore>,
140 context_store: Entity<ContextStore>,
141 prompt_store: Option<Entity<PromptStore>>,
142 thread_store: WeakEntity<ThreadStore>,
143 text_thread_store: WeakEntity<TextThreadStore>,
144 thread: Entity<Thread>,
145 window: &mut Window,
146 cx: &mut Context<Self>,
147 ) -> Self {
148 let context_picker_menu_handle = PopoverMenuHandle::default();
149 let model_selector_menu_handle = PopoverMenuHandle::default();
150
151 let editor = create_editor(
152 workspace.clone(),
153 context_store.downgrade(),
154 thread_store.clone(),
155 text_thread_store.clone(),
156 window,
157 cx,
158 );
159
160 let context_strip = cx.new(|cx| {
161 ContextStrip::new(
162 context_store.clone(),
163 workspace.clone(),
164 Some(thread_store.clone()),
165 Some(text_thread_store.clone()),
166 context_picker_menu_handle.clone(),
167 SuggestContextKind::File,
168 window,
169 cx,
170 )
171 });
172
173 let incompatible_tools =
174 cx.new(|cx| IncompatibleToolsState::new(thread.read(cx).tools().clone(), cx));
175
176 let subscriptions = vec![
177 cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
178 cx.subscribe(&editor, |this, _, event, cx| match event {
179 EditorEvent::BufferEdited => this.handle_message_changed(cx),
180 _ => {}
181 }),
182 cx.observe(&context_store, |this, _, cx| {
183 // When context changes, reload it for token counting.
184 let _ = this.reload_context(cx);
185 }),
186 cx.observe(&thread.read(cx).action_log().clone(), |_, _, cx| {
187 cx.notify()
188 }),
189 ];
190
191 let model_selector = cx.new(|cx| {
192 AgentModelSelector::new(
193 fs.clone(),
194 model_selector_menu_handle,
195 editor.focus_handle(cx),
196 ModelType::Default(thread.clone()),
197 window,
198 cx,
199 )
200 });
201
202 let profile_selector = cx.new(|cx| {
203 ProfileSelector::new(thread.clone(), thread_store, editor.focus_handle(cx), cx)
204 });
205
206 Self {
207 editor: editor.clone(),
208 project: thread.read(cx).project().clone(),
209 user_store,
210 thread,
211 incompatible_tools_state: incompatible_tools.clone(),
212 workspace,
213 context_store,
214 prompt_store,
215 context_strip,
216 context_picker_menu_handle,
217 load_context_task: None,
218 last_loaded_context: None,
219 model_selector,
220 edits_expanded: false,
221 editor_is_expanded: false,
222 profile_selector,
223 last_estimated_token_count: None,
224 update_token_count_task: None,
225 _subscriptions: subscriptions,
226 }
227 }
228
229 pub fn context_store(&self) -> &Entity<ContextStore> {
230 &self.context_store
231 }
232
233 pub fn expand_message_editor(
234 &mut self,
235 _: &ExpandMessageEditor,
236 _window: &mut Window,
237 cx: &mut Context<Self>,
238 ) {
239 self.set_editor_is_expanded(!self.editor_is_expanded, cx);
240 }
241
242 fn set_editor_is_expanded(&mut self, is_expanded: bool, cx: &mut Context<Self>) {
243 self.editor_is_expanded = is_expanded;
244 self.editor.update(cx, |editor, _| {
245 if self.editor_is_expanded {
246 editor.set_mode(EditorMode::Full {
247 scale_ui_elements_with_buffer_font_size: false,
248 show_active_line_background: false,
249 sized_by_content: false,
250 })
251 } else {
252 editor.set_mode(EditorMode::AutoHeight {
253 max_lines: MAX_EDITOR_LINES,
254 })
255 }
256 });
257 cx.notify();
258 }
259
260 fn toggle_context_picker(
261 &mut self,
262 _: &ToggleContextPicker,
263 window: &mut Window,
264 cx: &mut Context<Self>,
265 ) {
266 self.context_picker_menu_handle.toggle(window, cx);
267 }
268
269 pub fn remove_all_context(
270 &mut self,
271 _: &RemoveAllContext,
272 _window: &mut Window,
273 cx: &mut Context<Self>,
274 ) {
275 self.context_store.update(cx, |store, _cx| store.clear());
276 cx.notify();
277 }
278
279 fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
280 if self.is_editor_empty(cx) {
281 return;
282 }
283
284 self.thread.update(cx, |thread, cx| {
285 thread.cancel_editing(cx);
286 });
287
288 if self.thread.read(cx).is_generating() {
289 self.stop_current_and_send_new_message(window, cx);
290 return;
291 }
292
293 self.set_editor_is_expanded(false, cx);
294 self.send_to_model(window, cx);
295
296 cx.notify();
297 }
298
299 fn is_editor_empty(&self, cx: &App) -> bool {
300 self.editor.read(cx).text(cx).trim().is_empty()
301 }
302
303 pub fn is_editor_fully_empty(&self, cx: &App) -> bool {
304 self.editor.read(cx).is_empty(cx)
305 }
306
307 fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
308 let Some(ConfiguredModel { model, provider }) = self
309 .thread
310 .update(cx, |thread, cx| thread.get_or_init_configured_model(cx))
311 else {
312 return;
313 };
314
315 if provider.must_accept_terms(cx) {
316 cx.notify();
317 return;
318 }
319
320 let (user_message, user_message_creases) = self.editor.update(cx, |editor, cx| {
321 let creases = extract_message_creases(editor, cx);
322 let text = editor.text(cx);
323 editor.clear(window, cx);
324 (text, creases)
325 });
326
327 self.last_estimated_token_count.take();
328 cx.emit(MessageEditorEvent::EstimatedTokenCount);
329
330 let thread = self.thread.clone();
331 let git_store = self.project.read(cx).git_store().clone();
332 let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
333 let context_task = self.reload_context(cx);
334 let window_handle = window.window_handle();
335
336 cx.spawn(async move |_this, cx| {
337 let (checkpoint, loaded_context) = future::join(checkpoint, context_task).await;
338 let loaded_context = loaded_context.unwrap_or_default();
339
340 thread
341 .update(cx, |thread, cx| {
342 thread.insert_user_message(
343 user_message,
344 loaded_context,
345 checkpoint.ok(),
346 user_message_creases,
347 cx,
348 );
349 })
350 .log_err();
351
352 thread
353 .update(cx, |thread, cx| {
354 thread.advance_prompt_id();
355 thread.send_to_model(model, Some(window_handle), cx);
356 })
357 .log_err();
358 })
359 .detach();
360 }
361
362 fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
363 self.thread.update(cx, |thread, cx| {
364 thread.cancel_editing(cx);
365 });
366
367 let cancelled = self.thread.update(cx, |thread, cx| {
368 thread.cancel_last_completion(Some(window.window_handle()), cx)
369 });
370
371 if cancelled {
372 self.set_editor_is_expanded(false, cx);
373 self.send_to_model(window, cx);
374 }
375 }
376
377 fn handle_context_strip_event(
378 &mut self,
379 _context_strip: &Entity<ContextStrip>,
380 event: &ContextStripEvent,
381 window: &mut Window,
382 cx: &mut Context<Self>,
383 ) {
384 match event {
385 ContextStripEvent::PickerDismissed
386 | ContextStripEvent::BlurredEmpty
387 | ContextStripEvent::BlurredDown => {
388 let editor_focus_handle = self.editor.focus_handle(cx);
389 window.focus(&editor_focus_handle);
390 }
391 ContextStripEvent::BlurredUp => {}
392 }
393 }
394
395 fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
396 if self.context_picker_menu_handle.is_deployed() {
397 cx.propagate();
398 } else {
399 self.context_strip.focus_handle(cx).focus(window);
400 }
401 }
402
403 fn paste(&mut self, _: &Paste, _: &mut Window, cx: &mut Context<Self>) {
404 let images = cx
405 .read_from_clipboard()
406 .map(|item| {
407 item.into_entries()
408 .filter_map(|entry| {
409 if let ClipboardEntry::Image(image) = entry {
410 Some(image)
411 } else {
412 None
413 }
414 })
415 .collect::<Vec<_>>()
416 })
417 .unwrap_or_default();
418
419 if images.is_empty() {
420 return;
421 }
422 cx.stop_propagation();
423
424 self.context_store.update(cx, |store, cx| {
425 for image in images {
426 store.add_image_instance(Arc::new(image), cx);
427 }
428 });
429 }
430
431 fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context<Self>) {
432 self.edits_expanded = true;
433 AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
434 cx.notify();
435 }
436
437 fn handle_file_click(
438 &self,
439 buffer: Entity<Buffer>,
440 window: &mut Window,
441 cx: &mut Context<Self>,
442 ) {
443 if let Ok(diff) =
444 AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx)
445 {
446 let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);
447 diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));
448 }
449 }
450
451 fn render_max_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
452 let thread = self.thread.read(cx);
453 let model = thread.configured_model();
454 if !model?.model.supports_max_mode() {
455 return None;
456 }
457
458 let active_completion_mode = thread.completion_mode();
459 let max_mode_enabled = active_completion_mode == CompletionMode::Max;
460
461 Some(
462 Button::new("max-mode", "Max Mode")
463 .label_size(LabelSize::Small)
464 .color(Color::Muted)
465 .icon(IconName::ZedMaxMode)
466 .icon_size(IconSize::Small)
467 .icon_color(Color::Muted)
468 .icon_position(IconPosition::Start)
469 .toggle_state(max_mode_enabled)
470 .on_click(cx.listener(move |this, _event, _window, cx| {
471 this.thread.update(cx, |thread, _cx| {
472 thread.set_completion_mode(match active_completion_mode {
473 CompletionMode::Max => CompletionMode::Normal,
474 CompletionMode::Normal => CompletionMode::Max,
475 });
476 });
477 }))
478 .tooltip(move |_window, cx| {
479 cx.new(|_| MaxModeTooltip::new().selected(max_mode_enabled))
480 .into()
481 })
482 .into_any_element(),
483 )
484 }
485
486 fn render_follow_toggle(&self, cx: &mut Context<Self>) -> impl IntoElement {
487 let following = self
488 .workspace
489 .read_with(cx, |workspace, _| {
490 workspace.is_being_followed(CollaboratorId::Agent)
491 })
492 .unwrap_or(false);
493
494 IconButton::new("follow-agent", IconName::Crosshair)
495 .icon_size(IconSize::Small)
496 .icon_color(Color::Muted)
497 .toggle_state(following)
498 .selected_icon_color(Some(Color::Custom(cx.theme().players().agent().cursor)))
499 .tooltip(move |window, cx| {
500 if following {
501 Tooltip::for_action("Stop Following Agent", &Follow, window, cx)
502 } else {
503 Tooltip::with_meta(
504 "Follow Agent",
505 Some(&Follow),
506 "Track the agent's location as it reads and edits files.",
507 window,
508 cx,
509 )
510 }
511 })
512 .on_click(cx.listener(move |this, _, window, cx| {
513 this.workspace
514 .update(cx, |workspace, cx| {
515 if following {
516 workspace.unfollow(CollaboratorId::Agent, window, cx);
517 } else {
518 workspace.follow(CollaboratorId::Agent, window, cx);
519 }
520 })
521 .ok();
522 }))
523 }
524
525 fn render_editor(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
526 let thread = self.thread.read(cx);
527 let model = thread.configured_model();
528
529 let editor_bg_color = cx.theme().colors().editor_background;
530 let is_generating = thread.is_generating();
531 let focus_handle = self.editor.focus_handle(cx);
532
533 let is_model_selected = model.is_some();
534 let is_editor_empty = self.is_editor_empty(cx);
535
536 let incompatible_tools = model
537 .as_ref()
538 .map(|model| {
539 self.incompatible_tools_state.update(cx, |state, cx| {
540 state
541 .incompatible_tools(&model.model, cx)
542 .iter()
543 .cloned()
544 .collect::<Vec<_>>()
545 })
546 })
547 .unwrap_or_default();
548
549 let is_editor_expanded = self.editor_is_expanded;
550 let expand_icon = if is_editor_expanded {
551 IconName::Minimize
552 } else {
553 IconName::Maximize
554 };
555
556 v_flex()
557 .key_context("MessageEditor")
558 .on_action(cx.listener(Self::chat))
559 .on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
560 this.profile_selector
561 .read(cx)
562 .menu_handle()
563 .toggle(window, cx);
564 }))
565 .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
566 this.model_selector
567 .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
568 }))
569 .on_action(cx.listener(Self::toggle_context_picker))
570 .on_action(cx.listener(Self::remove_all_context))
571 .on_action(cx.listener(Self::move_up))
572 .on_action(cx.listener(Self::expand_message_editor))
573 .capture_action(cx.listener(Self::paste))
574 .gap_2()
575 .p_2()
576 .bg(editor_bg_color)
577 .border_t_1()
578 .border_color(cx.theme().colors().border)
579 .child(
580 h_flex()
581 .items_start()
582 .justify_between()
583 .child(self.context_strip.clone())
584 .child(
585 h_flex()
586 .gap_1()
587 .when(focus_handle.is_focused(window), |this| {
588 this.child(
589 IconButton::new("toggle-height", expand_icon)
590 .icon_size(IconSize::XSmall)
591 .icon_color(Color::Muted)
592 .tooltip({
593 let focus_handle = focus_handle.clone();
594 move |window, cx| {
595 let expand_label = if is_editor_expanded {
596 "Minimize Message Editor".to_string()
597 } else {
598 "Expand Message Editor".to_string()
599 };
600
601 Tooltip::for_action_in(
602 expand_label,
603 &ExpandMessageEditor,
604 &focus_handle,
605 window,
606 cx,
607 )
608 }
609 })
610 .on_click(cx.listener(|_, _, window, cx| {
611 window
612 .dispatch_action(Box::new(ExpandMessageEditor), cx);
613 })),
614 )
615 }),
616 ),
617 )
618 .child(
619 v_flex()
620 .size_full()
621 .gap_4()
622 .when(is_editor_expanded, |this| {
623 this.h(vh(0.8, window)).justify_between()
624 })
625 .child(
626 v_flex()
627 .min_h_16()
628 .when(is_editor_expanded, |this| this.h_full())
629 .child({
630 let settings = ThemeSettings::get_global(cx);
631 let font_size = TextSize::Small
632 .rems(cx)
633 .to_pixels(settings.agent_font_size(cx));
634 let line_height = settings.buffer_line_height.value() * font_size;
635
636 let text_style = TextStyle {
637 color: cx.theme().colors().text,
638 font_family: settings.buffer_font.family.clone(),
639 font_fallbacks: settings.buffer_font.fallbacks.clone(),
640 font_features: settings.buffer_font.features.clone(),
641 font_size: font_size.into(),
642 line_height: line_height.into(),
643 ..Default::default()
644 };
645
646 EditorElement::new(
647 &self.editor,
648 EditorStyle {
649 background: editor_bg_color,
650 local_player: cx.theme().players().local(),
651 text: text_style,
652 syntax: cx.theme().syntax().clone(),
653 ..Default::default()
654 },
655 )
656 .into_any()
657 }),
658 )
659 .child(
660 h_flex()
661 .flex_none()
662 .justify_between()
663 .child(
664 h_flex()
665 .gap_1()
666 .child(self.render_follow_toggle(cx))
667 .children(self.render_max_mode_toggle(cx)),
668 )
669 .child(
670 h_flex()
671 .gap_1()
672 .when(!incompatible_tools.is_empty(), |this| {
673 this.child(
674 IconButton::new(
675 "tools-incompatible-warning",
676 IconName::Warning,
677 )
678 .icon_color(Color::Warning)
679 .icon_size(IconSize::Small)
680 .tooltip({
681 move |_, cx| {
682 cx.new(|_| IncompatibleToolsTooltip {
683 incompatible_tools: incompatible_tools
684 .clone(),
685 })
686 .into()
687 }
688 }),
689 )
690 })
691 .child(self.profile_selector.clone())
692 .child(self.model_selector.clone())
693 .map({
694 let focus_handle = focus_handle.clone();
695 move |parent| {
696 if is_generating {
697 parent
698 .when(is_editor_empty, |parent| {
699 parent.child(
700 IconButton::new(
701 "stop-generation",
702 IconName::StopFilled,
703 )
704 .icon_color(Color::Error)
705 .style(ButtonStyle::Tinted(
706 ui::TintColor::Error,
707 ))
708 .tooltip(move |window, cx| {
709 Tooltip::for_action(
710 "Stop Generation",
711 &editor::actions::Cancel,
712 window,
713 cx,
714 )
715 })
716 .on_click({
717 let focus_handle =
718 focus_handle.clone();
719 move |_event, window, cx| {
720 focus_handle.dispatch_action(
721 &editor::actions::Cancel,
722 window,
723 cx,
724 );
725 }
726 })
727 .with_animation(
728 "pulsating-label",
729 Animation::new(
730 Duration::from_secs(2),
731 )
732 .repeat()
733 .with_easing(pulsating_between(
734 0.4, 1.0,
735 )),
736 |icon_button, delta| {
737 icon_button.alpha(delta)
738 },
739 ),
740 )
741 })
742 .when(!is_editor_empty, |parent| {
743 parent.child(
744 IconButton::new(
745 "send-message",
746 IconName::Send,
747 )
748 .icon_color(Color::Accent)
749 .style(ButtonStyle::Filled)
750 .disabled(!is_model_selected)
751 .on_click({
752 let focus_handle =
753 focus_handle.clone();
754 move |_event, window, cx| {
755 focus_handle.dispatch_action(
756 &Chat, window, cx,
757 );
758 }
759 })
760 .tooltip(move |window, cx| {
761 Tooltip::for_action(
762 "Stop and Send New Message",
763 &Chat,
764 window,
765 cx,
766 )
767 }),
768 )
769 })
770 } else {
771 parent.child(
772 IconButton::new("send-message", IconName::Send)
773 .icon_color(Color::Accent)
774 .style(ButtonStyle::Filled)
775 .disabled(
776 is_editor_empty || !is_model_selected,
777 )
778 .on_click({
779 let focus_handle = focus_handle.clone();
780 move |_event, window, cx| {
781 focus_handle.dispatch_action(
782 &Chat, window, cx,
783 );
784 }
785 })
786 .when(
787 !is_editor_empty && is_model_selected,
788 |button| {
789 button.tooltip(move |window, cx| {
790 Tooltip::for_action(
791 "Send", &Chat, window, cx,
792 )
793 })
794 },
795 )
796 .when(is_editor_empty, |button| {
797 button.tooltip(Tooltip::text(
798 "Type a message to submit",
799 ))
800 })
801 .when(!is_model_selected, |button| {
802 button.tooltip(Tooltip::text(
803 "Select a model to continue",
804 ))
805 }),
806 )
807 }
808 }
809 }),
810 ),
811 ),
812 )
813 }
814
815 fn render_changed_buffers(
816 &self,
817 changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
818 window: &mut Window,
819 cx: &mut Context<Self>,
820 ) -> Div {
821 let focus_handle = self.editor.focus_handle(cx);
822
823 let editor_bg_color = cx.theme().colors().editor_background;
824 let border_color = cx.theme().colors().border;
825 let active_color = cx.theme().colors().element_selected;
826 let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
827
828 let is_edit_changes_expanded = self.edits_expanded;
829 let is_generating = self.thread.read(cx).is_generating();
830
831 v_flex()
832 .mt_1()
833 .mx_2()
834 .bg(bg_edit_files_disclosure)
835 .border_1()
836 .border_b_0()
837 .border_color(border_color)
838 .rounded_t_md()
839 .shadow(smallvec::smallvec![gpui::BoxShadow {
840 color: gpui::black().opacity(0.15),
841 offset: point(px(1.), px(-1.)),
842 blur_radius: px(3.),
843 spread_radius: px(0.),
844 }])
845 .child(
846 h_flex()
847 .id("edits-container")
848 .cursor_pointer()
849 .p_1p5()
850 .justify_between()
851 .when(is_edit_changes_expanded, |this| {
852 this.border_b_1().border_color(border_color)
853 })
854 .on_click(
855 cx.listener(|this, _, window, cx| this.handle_review_click(window, cx)),
856 )
857 .child(
858 h_flex()
859 .gap_1()
860 .child(
861 Disclosure::new("edits-disclosure", is_edit_changes_expanded)
862 .on_click(cx.listener(|this, _ev, _window, cx| {
863 this.edits_expanded = !this.edits_expanded;
864 cx.notify();
865 })),
866 )
867 .map(|this| {
868 if is_generating {
869 this.child(
870 AnimatedLabel::new(format!(
871 "Editing {} {}",
872 changed_buffers.len(),
873 if changed_buffers.len() == 1 {
874 "file"
875 } else {
876 "files"
877 }
878 ))
879 .size(LabelSize::Small),
880 )
881 } else {
882 this.child(
883 Label::new("Edits")
884 .size(LabelSize::Small)
885 .color(Color::Muted),
886 )
887 .child(
888 Label::new("•").size(LabelSize::XSmall).color(Color::Muted),
889 )
890 .child(
891 Label::new(format!(
892 "{} {}",
893 changed_buffers.len(),
894 if changed_buffers.len() == 1 {
895 "file"
896 } else {
897 "files"
898 }
899 ))
900 .size(LabelSize::Small)
901 .color(Color::Muted),
902 )
903 }
904 }),
905 )
906 .child(
907 Button::new("review", "Review Changes")
908 .label_size(LabelSize::Small)
909 .key_binding(
910 KeyBinding::for_action_in(
911 &OpenAgentDiff,
912 &focus_handle,
913 window,
914 cx,
915 )
916 .map(|kb| kb.size(rems_from_px(12.))),
917 )
918 .on_click(cx.listener(|this, _, window, cx| {
919 this.handle_review_click(window, cx)
920 })),
921 ),
922 )
923 .when(is_edit_changes_expanded, |parent| {
924 parent.child(
925 v_flex().children(changed_buffers.into_iter().enumerate().flat_map(
926 |(index, (buffer, _diff))| {
927 let file = buffer.read(cx).file()?;
928 let path = file.path();
929
930 let parent_label = path.parent().and_then(|parent| {
931 let parent_str = parent.to_string_lossy();
932
933 if parent_str.is_empty() {
934 None
935 } else {
936 Some(
937 Label::new(format!(
938 "/{}{}",
939 parent_str,
940 std::path::MAIN_SEPARATOR_STR
941 ))
942 .color(Color::Muted)
943 .size(LabelSize::XSmall)
944 .buffer_font(cx),
945 )
946 }
947 });
948
949 let name_label = path.file_name().map(|name| {
950 Label::new(name.to_string_lossy().to_string())
951 .size(LabelSize::XSmall)
952 .buffer_font(cx)
953 });
954
955 let file_icon = FileIcons::get_icon(&path, cx)
956 .map(Icon::from_path)
957 .map(|icon| icon.color(Color::Muted).size(IconSize::Small))
958 .unwrap_or_else(|| {
959 Icon::new(IconName::File)
960 .color(Color::Muted)
961 .size(IconSize::Small)
962 });
963
964 let hover_color = cx
965 .theme()
966 .colors()
967 .element_background
968 .blend(cx.theme().colors().editor_foreground.opacity(0.025));
969
970 let overlay_gradient = linear_gradient(
971 90.,
972 linear_color_stop(editor_bg_color, 1.),
973 linear_color_stop(editor_bg_color.opacity(0.2), 0.),
974 );
975
976 let overlay_gradient_hover = linear_gradient(
977 90.,
978 linear_color_stop(hover_color, 1.),
979 linear_color_stop(hover_color.opacity(0.2), 0.),
980 );
981
982 let element = h_flex()
983 .group("edited-code")
984 .id(("file-container", index))
985 .cursor_pointer()
986 .relative()
987 .py_1()
988 .pl_2()
989 .pr_1()
990 .gap_2()
991 .justify_between()
992 .bg(cx.theme().colors().editor_background)
993 .hover(|style| style.bg(hover_color))
994 .when(index < changed_buffers.len() - 1, |parent| {
995 parent.border_color(border_color).border_b_1()
996 })
997 .child(
998 h_flex()
999 .id("file-name")
1000 .pr_8()
1001 .gap_1p5()
1002 .max_w_full()
1003 .overflow_x_scroll()
1004 .child(file_icon)
1005 .child(
1006 h_flex()
1007 .gap_0p5()
1008 .children(name_label)
1009 .children(parent_label),
1010 ), // TODO: Implement line diff
1011 // .child(Label::new("+").color(Color::Created))
1012 // .child(Label::new("-").color(Color::Deleted)),
1013 )
1014 .child(
1015 div().visible_on_hover("edited-code").child(
1016 Button::new("review", "Review")
1017 .label_size(LabelSize::Small)
1018 .on_click({
1019 let buffer = buffer.clone();
1020 cx.listener(move |this, _, window, cx| {
1021 this.handle_file_click(
1022 buffer.clone(),
1023 window,
1024 cx,
1025 );
1026 })
1027 }),
1028 ),
1029 )
1030 .child(
1031 div()
1032 .id("gradient-overlay")
1033 .absolute()
1034 .h_5_6()
1035 .w_12()
1036 .bottom_0()
1037 .right(px(52.))
1038 .bg(overlay_gradient)
1039 .group_hover("edited-code", |style| {
1040 style.bg(overlay_gradient_hover)
1041 }),
1042 )
1043 .on_click({
1044 let buffer = buffer.clone();
1045 cx.listener(move |this, _, window, cx| {
1046 this.handle_file_click(buffer.clone(), window, cx);
1047 })
1048 });
1049
1050 Some(element)
1051 },
1052 )),
1053 )
1054 })
1055 }
1056
1057 fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
1058 let is_using_zed_provider = self
1059 .thread
1060 .read(cx)
1061 .configured_model()
1062 .map_or(false, |model| {
1063 model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
1064 });
1065 if !is_using_zed_provider {
1066 return None;
1067 }
1068
1069 let user_store = self.user_store.read(cx);
1070
1071 let ubb_enable = user_store
1072 .usage_based_billing_enabled()
1073 .map_or(false, |enabled| enabled);
1074
1075 if ubb_enable {
1076 return None;
1077 }
1078
1079 let plan = user_store
1080 .current_plan()
1081 .map(|plan| match plan {
1082 Plan::Free => zed_llm_client::Plan::Free,
1083 Plan::ZedPro => zed_llm_client::Plan::ZedPro,
1084 Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
1085 })
1086 .unwrap_or(zed_llm_client::Plan::Free);
1087 let usage = self.thread.read(cx).last_usage().or_else(|| {
1088 maybe!({
1089 let amount = user_store.model_request_usage_amount()?;
1090 let limit = user_store.model_request_usage_limit()?.variant?;
1091
1092 Some(RequestUsage {
1093 amount: amount as i32,
1094 limit: match limit {
1095 proto::usage_limit::Variant::Limited(limited) => {
1096 zed_llm_client::UsageLimit::Limited(limited.limit as i32)
1097 }
1098 proto::usage_limit::Variant::Unlimited(_) => {
1099 zed_llm_client::UsageLimit::Unlimited
1100 }
1101 },
1102 })
1103 })
1104 })?;
1105
1106 Some(
1107 div()
1108 .child(UsageCallout::new(plan, usage))
1109 .line_height(line_height),
1110 )
1111 }
1112
1113 fn render_token_limit_callout(
1114 &self,
1115 line_height: Pixels,
1116 token_usage_ratio: TokenUsageRatio,
1117 cx: &mut Context<Self>,
1118 ) -> Option<Div> {
1119 let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
1120 "Thread reached the token limit"
1121 } else {
1122 "Thread reaching the token limit soon"
1123 };
1124
1125 let message = "Start a new thread from a summary to continue the conversation.";
1126
1127 let icon = if token_usage_ratio == TokenUsageRatio::Exceeded {
1128 Icon::new(IconName::X)
1129 .color(Color::Error)
1130 .size(IconSize::XSmall)
1131 } else {
1132 Icon::new(IconName::Warning)
1133 .color(Color::Warning)
1134 .size(IconSize::XSmall)
1135 };
1136
1137 Some(
1138 div()
1139 .child(ui::Callout::multi_line(
1140 title,
1141 message,
1142 icon,
1143 "Start New Thread",
1144 Box::new(cx.listener(|this, _, window, cx| {
1145 let from_thread_id = Some(this.thread.read(cx).id().clone());
1146 window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
1147 })),
1148 ))
1149 .line_height(line_height),
1150 )
1151 }
1152
1153 pub fn last_estimated_token_count(&self) -> Option<usize> {
1154 self.last_estimated_token_count
1155 }
1156
1157 pub fn is_waiting_to_update_token_count(&self) -> bool {
1158 self.update_token_count_task.is_some()
1159 }
1160
1161 fn reload_context(&mut self, cx: &mut Context<Self>) -> Task<Option<ContextLoadResult>> {
1162 let load_task = cx.spawn(async move |this, cx| {
1163 let Ok(load_task) = this.update(cx, |this, cx| {
1164 let new_context = this.context_store.read_with(cx, |context_store, cx| {
1165 context_store.new_context_for_thread(this.thread.read(cx), None)
1166 });
1167 load_context(new_context, &this.project, &this.prompt_store, cx)
1168 }) else {
1169 return;
1170 };
1171 let result = load_task.await;
1172 this.update(cx, |this, cx| {
1173 this.last_loaded_context = Some(result);
1174 this.load_context_task = None;
1175 this.message_or_context_changed(false, cx);
1176 })
1177 .ok();
1178 });
1179 // Replace existing load task, if any, causing it to be cancelled.
1180 let load_task = load_task.shared();
1181 self.load_context_task = Some(load_task.clone());
1182 cx.spawn(async move |this, cx| {
1183 load_task.await;
1184 this.read_with(cx, |this, _cx| this.last_loaded_context.clone())
1185 .ok()
1186 .flatten()
1187 })
1188 }
1189
1190 fn handle_message_changed(&mut self, cx: &mut Context<Self>) {
1191 self.message_or_context_changed(true, cx);
1192 }
1193
1194 fn message_or_context_changed(&mut self, debounce: bool, cx: &mut Context<Self>) {
1195 cx.emit(MessageEditorEvent::Changed);
1196 self.update_token_count_task.take();
1197
1198 let Some(model) = self.thread.read(cx).configured_model() else {
1199 self.last_estimated_token_count.take();
1200 return;
1201 };
1202
1203 let editor = self.editor.clone();
1204
1205 self.update_token_count_task = Some(cx.spawn(async move |this, cx| {
1206 if debounce {
1207 cx.background_executor()
1208 .timer(Duration::from_millis(200))
1209 .await;
1210 }
1211
1212 let token_count = if let Some(task) = this
1213 .update(cx, |this, cx| {
1214 let loaded_context = this
1215 .last_loaded_context
1216 .as_ref()
1217 .map(|context_load_result| &context_load_result.loaded_context);
1218 let message_text = editor.read(cx).text(cx);
1219
1220 if message_text.is_empty()
1221 && loaded_context.map_or(true, |loaded_context| loaded_context.is_empty())
1222 {
1223 return None;
1224 }
1225
1226 let mut request_message = LanguageModelRequestMessage {
1227 role: language_model::Role::User,
1228 content: Vec::new(),
1229 cache: false,
1230 };
1231
1232 if let Some(loaded_context) = loaded_context {
1233 loaded_context.add_to_request_message(&mut request_message);
1234 }
1235
1236 if !message_text.is_empty() {
1237 request_message
1238 .content
1239 .push(MessageContent::Text(message_text));
1240 }
1241
1242 let request = language_model::LanguageModelRequest {
1243 thread_id: None,
1244 prompt_id: None,
1245 mode: None,
1246 messages: vec![request_message],
1247 tools: vec![],
1248 tool_choice: None,
1249 stop: vec![],
1250 temperature: AssistantSettings::temperature_for_model(&model.model, cx),
1251 };
1252
1253 Some(model.model.count_tokens(request, cx))
1254 })
1255 .ok()
1256 .flatten()
1257 {
1258 task.await.log_err()
1259 } else {
1260 Some(0)
1261 };
1262
1263 this.update(cx, |this, cx| {
1264 if let Some(token_count) = token_count {
1265 this.last_estimated_token_count = Some(token_count);
1266 cx.emit(MessageEditorEvent::EstimatedTokenCount);
1267 }
1268 this.update_token_count_task.take();
1269 })
1270 .ok();
1271 }));
1272 }
1273}
1274
1275pub fn extract_message_creases(
1276 editor: &mut Editor,
1277 cx: &mut Context<'_, Editor>,
1278) -> Vec<MessageCrease> {
1279 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1280 let mut contexts_by_crease_id = editor
1281 .addon_mut::<ContextCreasesAddon>()
1282 .map(std::mem::take)
1283 .unwrap_or_default()
1284 .into_inner()
1285 .into_iter()
1286 .flat_map(|(key, creases)| {
1287 let context = key.0;
1288 creases
1289 .into_iter()
1290 .map(move |(id, _)| (id, context.clone()))
1291 })
1292 .collect::<HashMap<_, _>>();
1293 // Filter the addon's list of creases based on what the editor reports,
1294 // since the addon might have removed creases in it.
1295 let creases = editor.display_map.update(cx, |display_map, cx| {
1296 display_map
1297 .snapshot(cx)
1298 .crease_snapshot
1299 .creases()
1300 .filter_map(|(id, crease)| {
1301 Some((
1302 id,
1303 (
1304 crease.range().to_offset(&buffer_snapshot),
1305 crease.metadata()?.clone(),
1306 ),
1307 ))
1308 })
1309 .map(|(id, (range, metadata))| {
1310 let context = contexts_by_crease_id.remove(&id);
1311 MessageCrease {
1312 range,
1313 metadata,
1314 context,
1315 }
1316 })
1317 .collect()
1318 });
1319 creases
1320}
1321
1322impl EventEmitter<MessageEditorEvent> for MessageEditor {}
1323
1324pub enum MessageEditorEvent {
1325 EstimatedTokenCount,
1326 Changed,
1327}
1328
1329impl Focusable for MessageEditor {
1330 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
1331 self.editor.focus_handle(cx)
1332 }
1333}
1334
1335impl Render for MessageEditor {
1336 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1337 let thread = self.thread.read(cx);
1338 let token_usage_ratio = thread
1339 .total_token_usage()
1340 .map_or(TokenUsageRatio::Normal, |total_token_usage| {
1341 total_token_usage.ratio()
1342 });
1343
1344 let action_log = self.thread.read(cx).action_log();
1345 let changed_buffers = action_log.read(cx).changed_buffers(cx);
1346
1347 let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
1348
1349 v_flex()
1350 .size_full()
1351 .when(changed_buffers.len() > 0, |parent| {
1352 parent.child(self.render_changed_buffers(&changed_buffers, window, cx))
1353 })
1354 .child(self.render_editor(window, cx))
1355 .children({
1356 let usage_callout = self.render_usage_callout(line_height, cx);
1357
1358 if usage_callout.is_some() {
1359 usage_callout
1360 } else if token_usage_ratio != TokenUsageRatio::Normal {
1361 self.render_token_limit_callout(line_height, token_usage_ratio, cx)
1362 } else {
1363 None
1364 }
1365 })
1366 }
1367}
1368
1369pub fn insert_message_creases(
1370 editor: &mut Editor,
1371 message_creases: &[MessageCrease],
1372 context_store: &Entity<ContextStore>,
1373 window: &mut Window,
1374 cx: &mut Context<'_, Editor>,
1375) {
1376 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1377 let creases = message_creases
1378 .iter()
1379 .map(|crease| {
1380 let start = buffer_snapshot.anchor_after(crease.range.start);
1381 let end = buffer_snapshot.anchor_before(crease.range.end);
1382 crease_for_mention(
1383 crease.metadata.label.clone(),
1384 crease.metadata.icon_path.clone(),
1385 start..end,
1386 cx.weak_entity(),
1387 )
1388 })
1389 .collect::<Vec<_>>();
1390 let ids = editor.insert_creases(creases.clone(), cx);
1391 editor.fold_creases(creases, false, window, cx);
1392 if let Some(addon) = editor.addon_mut::<ContextCreasesAddon>() {
1393 for (crease, id) in message_creases.iter().zip(ids) {
1394 if let Some(context) = crease.context.as_ref() {
1395 let key = AgentContextKey(context.clone());
1396 addon.add_creases(
1397 context_store,
1398 key,
1399 vec![(id, crease.metadata.label.clone())],
1400 cx,
1401 );
1402 }
1403 }
1404 }
1405}
1406impl Component for MessageEditor {
1407 fn scope() -> ComponentScope {
1408 ComponentScope::Agent
1409 }
1410
1411 fn description() -> Option<&'static str> {
1412 Some(
1413 "The composer experience of the Agent Panel. This interface handles context, composing messages, switching profiles, models and more.",
1414 )
1415 }
1416}
1417
1418impl AgentPreview for MessageEditor {
1419 fn agent_preview(
1420 workspace: WeakEntity<Workspace>,
1421 active_thread: Entity<ActiveThread>,
1422 window: &mut Window,
1423 cx: &mut App,
1424 ) -> Option<AnyElement> {
1425 if let Some(workspace) = workspace.upgrade() {
1426 let fs = workspace.read(cx).app_state().fs.clone();
1427 let user_store = workspace.read(cx).app_state().user_store.clone();
1428 let project = workspace.read(cx).project().clone();
1429 let weak_project = project.downgrade();
1430 let context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
1431 let active_thread = active_thread.read(cx);
1432 let thread = active_thread.thread().clone();
1433 let thread_store = active_thread.thread_store().clone();
1434 let text_thread_store = active_thread.text_thread_store().clone();
1435
1436 let default_message_editor = cx.new(|cx| {
1437 MessageEditor::new(
1438 fs,
1439 workspace.downgrade(),
1440 user_store,
1441 context_store,
1442 None,
1443 thread_store.downgrade(),
1444 text_thread_store.downgrade(),
1445 thread,
1446 window,
1447 cx,
1448 )
1449 });
1450
1451 Some(
1452 v_flex()
1453 .gap_4()
1454 .children(vec![single_example(
1455 "Default Message Editor",
1456 div()
1457 .w(px(540.))
1458 .pt_12()
1459 .bg(cx.theme().colors().panel_background)
1460 .border_1()
1461 .border_color(cx.theme().colors().border)
1462 .child(default_message_editor.clone())
1463 .into_any_element(),
1464 )])
1465 .into_any_element(),
1466 )
1467 } else {
1468 None
1469 }
1470 }
1471}
1472
1473register_agent_preview!(MessageEditor);