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