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