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