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