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