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