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