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