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", index))
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 )
1176 .on_click({
1177 let buffer = buffer.clone();
1178 cx.listener(move |this, _, window, cx| {
1179 this.handle_file_click(buffer.clone(), window, cx);
1180 })
1181 }), // TODO: Implement line diff
1182 // .child(Label::new("+").color(Color::Created))
1183 // .child(Label::new("-").color(Color::Deleted)),
1184 //
1185 )
1186 .child(
1187 h_flex()
1188 .gap_1()
1189 .visible_on_hover("edited-code")
1190 .child(
1191 Button::new("review", "Review")
1192 .label_size(LabelSize::Small)
1193 .on_click({
1194 let buffer = buffer.clone();
1195 cx.listener(move |this, _, window, cx| {
1196 this.handle_file_click(
1197 buffer.clone(),
1198 window,
1199 cx,
1200 );
1201 })
1202 }),
1203 )
1204 .child(
1205 Divider::vertical().color(DividerColor::BorderVariant),
1206 )
1207 .child(
1208 Button::new("reject-file", "Reject")
1209 .label_size(LabelSize::Small)
1210 .disabled(pending_edits)
1211 .on_click({
1212 let buffer = buffer.clone();
1213 cx.listener(move |this, _, window, cx| {
1214 this.handle_reject_file_changes(
1215 buffer.clone(),
1216 window,
1217 cx,
1218 );
1219 })
1220 }),
1221 )
1222 .child(
1223 Button::new("accept-file", "Accept")
1224 .label_size(LabelSize::Small)
1225 .disabled(pending_edits)
1226 .on_click({
1227 let buffer = buffer.clone();
1228 cx.listener(move |this, _, window, cx| {
1229 this.handle_accept_file_changes(
1230 buffer.clone(),
1231 window,
1232 cx,
1233 );
1234 })
1235 }),
1236 ),
1237 )
1238 .child(
1239 div()
1240 .id("gradient-overlay")
1241 .absolute()
1242 .h_full()
1243 .w_12()
1244 .top_0()
1245 .bottom_0()
1246 .right(px(152.))
1247 .bg(overlay_gradient),
1248 );
1249
1250 Some(element)
1251 },
1252 )),
1253 )
1254 })
1255 }
1256
1257 fn is_using_zed_provider(&self, cx: &App) -> bool {
1258 self.thread
1259 .read(cx)
1260 .configured_model()
1261 .map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID)
1262 }
1263
1264 fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
1265 if !self.is_using_zed_provider(cx) {
1266 return None;
1267 }
1268
1269 let user_store = self.user_store.read(cx);
1270
1271 let ubb_enable = user_store
1272 .usage_based_billing_enabled()
1273 .map_or(false, |enabled| enabled);
1274
1275 if ubb_enable {
1276 return None;
1277 }
1278
1279 let plan = user_store
1280 .current_plan()
1281 .map(|plan| match plan {
1282 Plan::Free => zed_llm_client::Plan::ZedFree,
1283 Plan::ZedPro => zed_llm_client::Plan::ZedPro,
1284 Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
1285 })
1286 .unwrap_or(zed_llm_client::Plan::ZedFree);
1287
1288 let usage = user_store.model_request_usage()?;
1289
1290 Some(
1291 div()
1292 .child(UsageCallout::new(plan, usage))
1293 .line_height(line_height),
1294 )
1295 }
1296
1297 fn render_token_limit_callout(
1298 &self,
1299 line_height: Pixels,
1300 token_usage_ratio: TokenUsageRatio,
1301 cx: &mut Context<Self>,
1302 ) -> Option<Div> {
1303 let icon = if token_usage_ratio == TokenUsageRatio::Exceeded {
1304 Icon::new(IconName::X)
1305 .color(Color::Error)
1306 .size(IconSize::XSmall)
1307 } else {
1308 Icon::new(IconName::Warning)
1309 .color(Color::Warning)
1310 .size(IconSize::XSmall)
1311 };
1312
1313 let title = if token_usage_ratio == TokenUsageRatio::Exceeded {
1314 "Thread reached the token limit"
1315 } else {
1316 "Thread reaching the token limit soon"
1317 };
1318
1319 let description = if self.is_using_zed_provider(cx) {
1320 "To continue, start a new thread from a summary or turn burn mode on."
1321 } else {
1322 "To continue, start a new thread from a summary."
1323 };
1324
1325 let mut callout = Callout::new()
1326 .line_height(line_height)
1327 .icon(icon)
1328 .title(title)
1329 .description(description)
1330 .primary_action(
1331 Button::new("start-new-thread", "Start New Thread")
1332 .label_size(LabelSize::Small)
1333 .on_click(cx.listener(|this, _, window, cx| {
1334 let from_thread_id = Some(this.thread.read(cx).id().clone());
1335 window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
1336 })),
1337 );
1338
1339 if self.is_using_zed_provider(cx) {
1340 callout = callout.secondary_action(
1341 IconButton::new("burn-mode-callout", IconName::ZedBurnMode)
1342 .icon_size(IconSize::XSmall)
1343 .on_click(cx.listener(|this, _event, window, cx| {
1344 this.toggle_burn_mode(&ToggleBurnMode, window, cx);
1345 })),
1346 );
1347 }
1348
1349 Some(
1350 div()
1351 .border_t_1()
1352 .border_color(cx.theme().colors().border)
1353 .child(callout),
1354 )
1355 }
1356
1357 pub fn last_estimated_token_count(&self) -> Option<u64> {
1358 self.last_estimated_token_count
1359 }
1360
1361 pub fn is_waiting_to_update_token_count(&self) -> bool {
1362 self.update_token_count_task.is_some()
1363 }
1364
1365 fn reload_context(&mut self, cx: &mut Context<Self>) -> Task<Option<ContextLoadResult>> {
1366 let load_task = cx.spawn(async move |this, cx| {
1367 let Ok(load_task) = this.update(cx, |this, cx| {
1368 let new_context = this
1369 .context_store
1370 .read(cx)
1371 .new_context_for_thread(this.thread.read(cx), None);
1372 load_context(new_context, &this.project, &this.prompt_store, cx)
1373 }) else {
1374 return;
1375 };
1376 let result = load_task.await;
1377 this.update(cx, |this, cx| {
1378 this.last_loaded_context = Some(result);
1379 this.load_context_task = None;
1380 this.message_or_context_changed(false, cx);
1381 })
1382 .ok();
1383 });
1384 // Replace existing load task, if any, causing it to be cancelled.
1385 let load_task = load_task.shared();
1386 self.load_context_task = Some(load_task.clone());
1387 cx.spawn(async move |this, cx| {
1388 load_task.await;
1389 this.read_with(cx, |this, _cx| this.last_loaded_context.clone())
1390 .ok()
1391 .flatten()
1392 })
1393 }
1394
1395 fn handle_message_changed(&mut self, cx: &mut Context<Self>) {
1396 self.message_or_context_changed(true, cx);
1397 }
1398
1399 fn message_or_context_changed(&mut self, debounce: bool, cx: &mut Context<Self>) {
1400 cx.emit(MessageEditorEvent::Changed);
1401 self.update_token_count_task.take();
1402
1403 let Some(model) = self.thread.read(cx).configured_model() else {
1404 self.last_estimated_token_count.take();
1405 return;
1406 };
1407
1408 let editor = self.editor.clone();
1409
1410 self.update_token_count_task = Some(cx.spawn(async move |this, cx| {
1411 if debounce {
1412 cx.background_executor()
1413 .timer(Duration::from_millis(200))
1414 .await;
1415 }
1416
1417 let token_count = if let Some(task) = this
1418 .update(cx, |this, cx| {
1419 let loaded_context = this
1420 .last_loaded_context
1421 .as_ref()
1422 .map(|context_load_result| &context_load_result.loaded_context);
1423 let message_text = editor.read(cx).text(cx);
1424
1425 if message_text.is_empty()
1426 && loaded_context.map_or(true, |loaded_context| loaded_context.is_empty())
1427 {
1428 return None;
1429 }
1430
1431 let mut request_message = LanguageModelRequestMessage {
1432 role: language_model::Role::User,
1433 content: Vec::new(),
1434 cache: false,
1435 };
1436
1437 if let Some(loaded_context) = loaded_context {
1438 loaded_context.add_to_request_message(&mut request_message);
1439 }
1440
1441 if !message_text.is_empty() {
1442 request_message
1443 .content
1444 .push(MessageContent::Text(message_text));
1445 }
1446
1447 let request = language_model::LanguageModelRequest {
1448 thread_id: None,
1449 prompt_id: None,
1450 intent: None,
1451 mode: None,
1452 messages: vec![request_message],
1453 tools: vec![],
1454 tool_choice: None,
1455 stop: vec![],
1456 temperature: AgentSettings::temperature_for_model(&model.model, cx),
1457 thinking_allowed: true,
1458 };
1459
1460 Some(model.model.count_tokens(request, cx))
1461 })
1462 .ok()
1463 .flatten()
1464 {
1465 task.await.log_err()
1466 } else {
1467 Some(0)
1468 };
1469
1470 this.update(cx, |this, cx| {
1471 if let Some(token_count) = token_count {
1472 this.last_estimated_token_count = Some(token_count);
1473 cx.emit(MessageEditorEvent::EstimatedTokenCount);
1474 }
1475 this.update_token_count_task.take();
1476 })
1477 .ok();
1478 }));
1479 }
1480}
1481
1482#[derive(Default)]
1483pub struct ContextCreasesAddon {
1484 creases: HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>>,
1485 _subscription: Option<Subscription>,
1486}
1487
1488impl Addon for ContextCreasesAddon {
1489 fn to_any(&self) -> &dyn std::any::Any {
1490 self
1491 }
1492
1493 fn to_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1494 Some(self)
1495 }
1496}
1497
1498impl ContextCreasesAddon {
1499 pub fn new() -> Self {
1500 Self {
1501 creases: HashMap::default(),
1502 _subscription: None,
1503 }
1504 }
1505
1506 pub fn add_creases(
1507 &mut self,
1508 context_store: &Entity<ContextStore>,
1509 key: AgentContextKey,
1510 creases: impl IntoIterator<Item = (CreaseId, SharedString)>,
1511 cx: &mut Context<Editor>,
1512 ) {
1513 self.creases.entry(key).or_default().extend(creases);
1514 self._subscription = Some(cx.subscribe(
1515 &context_store,
1516 |editor, _, event, cx| match event {
1517 ContextStoreEvent::ContextRemoved(key) => {
1518 let Some(this) = editor.addon_mut::<Self>() else {
1519 return;
1520 };
1521 let (crease_ids, replacement_texts): (Vec<_>, Vec<_>) = this
1522 .creases
1523 .remove(key)
1524 .unwrap_or_default()
1525 .into_iter()
1526 .unzip();
1527 let ranges = editor
1528 .remove_creases(crease_ids, cx)
1529 .into_iter()
1530 .map(|(_, range)| range)
1531 .collect::<Vec<_>>();
1532 editor.unfold_ranges(&ranges, false, false, cx);
1533 editor.edit(ranges.into_iter().zip(replacement_texts), cx);
1534 cx.notify();
1535 }
1536 },
1537 ))
1538 }
1539
1540 pub fn into_inner(self) -> HashMap<AgentContextKey, Vec<(CreaseId, SharedString)>> {
1541 self.creases
1542 }
1543}
1544
1545pub fn extract_message_creases(
1546 editor: &mut Editor,
1547 cx: &mut Context<'_, Editor>,
1548) -> Vec<MessageCrease> {
1549 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1550 let mut contexts_by_crease_id = editor
1551 .addon_mut::<ContextCreasesAddon>()
1552 .map(std::mem::take)
1553 .unwrap_or_default()
1554 .into_inner()
1555 .into_iter()
1556 .flat_map(|(key, creases)| {
1557 let context = key.0;
1558 creases
1559 .into_iter()
1560 .map(move |(id, _)| (id, context.clone()))
1561 })
1562 .collect::<HashMap<_, _>>();
1563 // Filter the addon's list of creases based on what the editor reports,
1564 // since the addon might have removed creases in it.
1565 let creases = editor.display_map.update(cx, |display_map, cx| {
1566 display_map
1567 .snapshot(cx)
1568 .crease_snapshot
1569 .creases()
1570 .filter_map(|(id, crease)| {
1571 Some((
1572 id,
1573 (
1574 crease.range().to_offset(&buffer_snapshot),
1575 crease.metadata()?.clone(),
1576 ),
1577 ))
1578 })
1579 .map(|(id, (range, metadata))| {
1580 let context = contexts_by_crease_id.remove(&id);
1581 MessageCrease {
1582 range,
1583 context,
1584 label: metadata.label,
1585 icon_path: metadata.icon_path,
1586 }
1587 })
1588 .collect()
1589 });
1590 creases
1591}
1592
1593impl EventEmitter<MessageEditorEvent> for MessageEditor {}
1594
1595pub enum MessageEditorEvent {
1596 EstimatedTokenCount,
1597 Changed,
1598 ScrollThreadToBottom,
1599}
1600
1601impl Focusable for MessageEditor {
1602 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
1603 self.editor.focus_handle(cx)
1604 }
1605}
1606
1607impl Render for MessageEditor {
1608 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1609 let thread = self.thread.read(cx);
1610 let token_usage_ratio = thread
1611 .total_token_usage()
1612 .map_or(TokenUsageRatio::Normal, |total_token_usage| {
1613 total_token_usage.ratio()
1614 });
1615
1616 let burn_mode_enabled = thread.completion_mode() == CompletionMode::Burn;
1617
1618 let action_log = self.thread.read(cx).action_log();
1619 let changed_buffers = action_log.read(cx).changed_buffers(cx);
1620
1621 let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
1622
1623 v_flex()
1624 .size_full()
1625 .bg(cx.theme().colors().panel_background)
1626 .when(changed_buffers.len() > 0, |parent| {
1627 parent.child(self.render_edits_bar(&changed_buffers, window, cx))
1628 })
1629 .child(self.render_editor(window, cx))
1630 .children({
1631 let usage_callout = self.render_usage_callout(line_height, cx);
1632
1633 if usage_callout.is_some() {
1634 usage_callout
1635 } else if token_usage_ratio != TokenUsageRatio::Normal && !burn_mode_enabled {
1636 self.render_token_limit_callout(line_height, token_usage_ratio, cx)
1637 } else {
1638 None
1639 }
1640 })
1641 }
1642}
1643
1644pub fn insert_message_creases(
1645 editor: &mut Editor,
1646 message_creases: &[MessageCrease],
1647 context_store: &Entity<ContextStore>,
1648 window: &mut Window,
1649 cx: &mut Context<'_, Editor>,
1650) {
1651 let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1652 let creases = message_creases
1653 .iter()
1654 .map(|crease| {
1655 let start = buffer_snapshot.anchor_after(crease.range.start);
1656 let end = buffer_snapshot.anchor_before(crease.range.end);
1657 crease_for_mention(
1658 crease.label.clone(),
1659 crease.icon_path.clone(),
1660 start..end,
1661 cx.weak_entity(),
1662 )
1663 })
1664 .collect::<Vec<_>>();
1665 let ids = editor.insert_creases(creases.clone(), cx);
1666 editor.fold_creases(creases, false, window, cx);
1667 if let Some(addon) = editor.addon_mut::<ContextCreasesAddon>() {
1668 for (crease, id) in message_creases.iter().zip(ids) {
1669 if let Some(context) = crease.context.as_ref() {
1670 let key = AgentContextKey(context.clone());
1671 addon.add_creases(context_store, key, vec![(id, crease.label.clone())], cx);
1672 }
1673 }
1674 }
1675}
1676impl Component for MessageEditor {
1677 fn scope() -> ComponentScope {
1678 ComponentScope::Agent
1679 }
1680
1681 fn description() -> Option<&'static str> {
1682 Some(
1683 "The composer experience of the Agent Panel. This interface handles context, composing messages, switching profiles, models and more.",
1684 )
1685 }
1686}
1687
1688impl AgentPreview for MessageEditor {
1689 fn agent_preview(
1690 workspace: WeakEntity<Workspace>,
1691 active_thread: Entity<ActiveThread>,
1692 window: &mut Window,
1693 cx: &mut App,
1694 ) -> Option<AnyElement> {
1695 if let Some(workspace) = workspace.upgrade() {
1696 let fs = workspace.read(cx).app_state().fs.clone();
1697 let user_store = workspace.read(cx).app_state().user_store.clone();
1698 let project = workspace.read(cx).project().clone();
1699 let weak_project = project.downgrade();
1700 let context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
1701 let active_thread = active_thread.read(cx);
1702 let thread = active_thread.thread().clone();
1703 let thread_store = active_thread.thread_store().clone();
1704 let text_thread_store = active_thread.text_thread_store().clone();
1705
1706 let default_message_editor = cx.new(|cx| {
1707 MessageEditor::new(
1708 fs,
1709 workspace.downgrade(),
1710 user_store,
1711 context_store,
1712 None,
1713 thread_store.downgrade(),
1714 text_thread_store.downgrade(),
1715 thread,
1716 window,
1717 cx,
1718 )
1719 });
1720
1721 Some(
1722 v_flex()
1723 .gap_4()
1724 .children(vec![single_example(
1725 "Default Message Editor",
1726 div()
1727 .w(px(540.))
1728 .pt_12()
1729 .bg(cx.theme().colors().panel_background)
1730 .border_1()
1731 .border_color(cx.theme().colors().border)
1732 .child(default_message_editor.clone())
1733 .into_any_element(),
1734 )])
1735 .into_any_element(),
1736 )
1737 } else {
1738 None
1739 }
1740 }
1741}
1742
1743register_agent_preview!(MessageEditor);