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