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