1use std::ops::Range;
2use std::path::Path;
3use std::sync::Arc;
4use std::time::Duration;
5
6use anyhow::{Result, anyhow};
7use assistant_context_editor::{
8 AssistantContext, AssistantPanelDelegate, ConfigurationError, ContextEditor, ContextEvent,
9 SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate,
10 render_remaining_tokens,
11};
12use assistant_settings::{AssistantDockPosition, AssistantSettings};
13use assistant_slash_command::SlashCommandWorkingSet;
14use assistant_tool::ToolWorkingSet;
15
16use client::{UserStore, zed_urls};
17use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
18use fs::Fs;
19use gpui::{
20 Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
21 Corner, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext,
22 Pixels, Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
23};
24use language::LanguageRegistry;
25use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
26use language_model_selector::ToggleModelSelector;
27use project::Project;
28use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
29use proto::Plan;
30use rules_library::{RulesLibrary, open_rules_library};
31use settings::{Settings, update_settings_file};
32use time::UtcOffset;
33use ui::{
34 Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*,
35};
36use util::ResultExt as _;
37use workspace::Workspace;
38use workspace::dock::{DockPosition, Panel, PanelEvent};
39use zed_actions::agent::OpenConfiguration;
40use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
41
42use crate::active_thread::{ActiveThread, ActiveThreadEvent};
43use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
44use crate::history_store::{HistoryEntry, HistoryStore, RecentEntry};
45use crate::message_editor::{MessageEditor, MessageEditorEvent};
46use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
47use crate::thread_history::{PastContext, PastThread, ThreadHistory};
48use crate::thread_store::ThreadStore;
49use crate::ui::UsageBanner;
50use crate::{
51 AddContextServer, AgentDiff, DeleteRecentlyOpenThread, ExpandMessageEditor, InlineAssistant,
52 NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ThreadEvent,
53 ToggleContextPicker, ToggleNavigationMenu,
54};
55
56pub fn init(cx: &mut App) {
57 cx.observe_new(
58 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
59 workspace
60 .register_action(|workspace, action: &NewThread, window, cx| {
61 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
62 panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
63 workspace.focus_panel::<AssistantPanel>(window, cx);
64 }
65 })
66 .register_action(|workspace, _: &OpenHistory, window, cx| {
67 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
68 workspace.focus_panel::<AssistantPanel>(window, cx);
69 panel.update(cx, |panel, cx| panel.open_history(window, cx));
70 }
71 })
72 .register_action(|workspace, _: &OpenConfiguration, window, cx| {
73 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
74 workspace.focus_panel::<AssistantPanel>(window, cx);
75 panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
76 }
77 })
78 .register_action(|workspace, _: &NewTextThread, window, cx| {
79 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
80 workspace.focus_panel::<AssistantPanel>(window, cx);
81 panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
82 }
83 })
84 .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
85 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
86 workspace.focus_panel::<AssistantPanel>(window, cx);
87 panel.update(cx, |panel, cx| {
88 panel.deploy_rules_library(action, window, cx)
89 });
90 }
91 })
92 .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
93 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
94 workspace.focus_panel::<AssistantPanel>(window, cx);
95 let thread = panel.read(cx).thread.read(cx).thread().clone();
96 AgentDiff::deploy_in_workspace(thread, workspace, window, cx);
97 }
98 })
99 .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
100 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
101 workspace.focus_panel::<AssistantPanel>(window, cx);
102 panel.update(cx, |panel, cx| {
103 panel.message_editor.update(cx, |editor, cx| {
104 editor.expand_message_editor(&ExpandMessageEditor, window, cx);
105 });
106 });
107 }
108 })
109 .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
110 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
111 workspace.focus_panel::<AssistantPanel>(window, cx);
112 panel.update(cx, |panel, cx| {
113 panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
114 });
115 }
116 });
117 },
118 )
119 .detach();
120}
121
122enum ActiveView {
123 Thread {
124 change_title_editor: Entity<Editor>,
125 thread: WeakEntity<Thread>,
126 _subscriptions: Vec<gpui::Subscription>,
127 },
128 PromptEditor {
129 context_editor: Entity<ContextEditor>,
130 title_editor: Entity<Editor>,
131 _subscriptions: Vec<gpui::Subscription>,
132 },
133 History,
134 Configuration,
135}
136
137impl ActiveView {
138 pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
139 let summary = thread.read(cx).summary_or_default();
140
141 let editor = cx.new(|cx| {
142 let mut editor = Editor::single_line(window, cx);
143 editor.set_text(summary.clone(), window, cx);
144 editor
145 });
146
147 let subscriptions = vec![
148 window.subscribe(&editor, cx, {
149 {
150 let thread = thread.clone();
151 move |editor, event, window, cx| match event {
152 EditorEvent::BufferEdited => {
153 let new_summary = editor.read(cx).text(cx);
154
155 thread.update(cx, |thread, cx| {
156 thread.set_summary(new_summary, cx);
157 })
158 }
159 EditorEvent::Blurred => {
160 if editor.read(cx).text(cx).is_empty() {
161 let summary = thread.read(cx).summary_or_default();
162
163 editor.update(cx, |editor, cx| {
164 editor.set_text(summary, window, cx);
165 });
166 }
167 }
168 _ => {}
169 }
170 }
171 }),
172 window.subscribe(&thread, cx, {
173 let editor = editor.clone();
174 move |thread, event, window, cx| match event {
175 ThreadEvent::SummaryGenerated => {
176 let summary = thread.read(cx).summary_or_default();
177
178 editor.update(cx, |editor, cx| {
179 editor.set_text(summary, window, cx);
180 })
181 }
182 _ => {}
183 }
184 }),
185 ];
186
187 Self::Thread {
188 change_title_editor: editor,
189 thread: thread.downgrade(),
190 _subscriptions: subscriptions,
191 }
192 }
193
194 pub fn prompt_editor(
195 context_editor: Entity<ContextEditor>,
196 window: &mut Window,
197 cx: &mut App,
198 ) -> Self {
199 let title = context_editor.read(cx).title(cx).to_string();
200
201 let editor = cx.new(|cx| {
202 let mut editor = Editor::single_line(window, cx);
203 editor.set_text(title, window, cx);
204 editor
205 });
206
207 // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would
208 // cause a custom summary to be set. The presence of this custom summary would cause
209 // summarization to not happen.
210 let mut suppress_first_edit = true;
211
212 let subscriptions = vec![
213 window.subscribe(&editor, cx, {
214 {
215 let context_editor = context_editor.clone();
216 move |editor, event, window, cx| match event {
217 EditorEvent::BufferEdited => {
218 if suppress_first_edit {
219 suppress_first_edit = false;
220 return;
221 }
222 let new_summary = editor.read(cx).text(cx);
223
224 context_editor.update(cx, |context_editor, cx| {
225 context_editor
226 .context()
227 .update(cx, |assistant_context, cx| {
228 assistant_context.set_custom_summary(new_summary, cx);
229 })
230 })
231 }
232 EditorEvent::Blurred => {
233 if editor.read(cx).text(cx).is_empty() {
234 let summary = context_editor
235 .read(cx)
236 .context()
237 .read(cx)
238 .summary_or_default();
239
240 editor.update(cx, |editor, cx| {
241 editor.set_text(summary, window, cx);
242 });
243 }
244 }
245 _ => {}
246 }
247 }
248 }),
249 window.subscribe(&context_editor.read(cx).context().clone(), cx, {
250 let editor = editor.clone();
251 move |assistant_context, event, window, cx| match event {
252 ContextEvent::SummaryGenerated => {
253 let summary = assistant_context.read(cx).summary_or_default();
254
255 editor.update(cx, |editor, cx| {
256 editor.set_text(summary, window, cx);
257 })
258 }
259 _ => {}
260 }
261 }),
262 ];
263
264 Self::PromptEditor {
265 context_editor,
266 title_editor: editor,
267 _subscriptions: subscriptions,
268 }
269 }
270}
271
272pub struct AssistantPanel {
273 workspace: WeakEntity<Workspace>,
274 user_store: Entity<UserStore>,
275 project: Entity<Project>,
276 fs: Arc<dyn Fs>,
277 language_registry: Arc<LanguageRegistry>,
278 thread_store: Entity<ThreadStore>,
279 thread: Entity<ActiveThread>,
280 message_editor: Entity<MessageEditor>,
281 _active_thread_subscriptions: Vec<Subscription>,
282 _default_model_subscription: Subscription,
283 context_store: Entity<assistant_context_editor::ContextStore>,
284 prompt_store: Option<Entity<PromptStore>>,
285 configuration: Option<Entity<AssistantConfiguration>>,
286 configuration_subscription: Option<Subscription>,
287 local_timezone: UtcOffset,
288 active_view: ActiveView,
289 previous_view: Option<ActiveView>,
290 history_store: Entity<HistoryStore>,
291 history: Entity<ThreadHistory>,
292 assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
293 assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
294 assistant_navigation_menu: Option<Entity<ContextMenu>>,
295 width: Option<Pixels>,
296 height: Option<Pixels>,
297}
298
299impl AssistantPanel {
300 pub fn load(
301 workspace: WeakEntity<Workspace>,
302 prompt_builder: Arc<PromptBuilder>,
303 mut cx: AsyncWindowContext,
304 ) -> Task<Result<Entity<Self>>> {
305 let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
306 cx.spawn(async move |cx| {
307 let prompt_store = match prompt_store {
308 Ok(prompt_store) => prompt_store.await.ok(),
309 Err(_) => None,
310 };
311 let tools = cx.new(|_| ToolWorkingSet::default())?;
312 let thread_store = workspace
313 .update(cx, |workspace, cx| {
314 let project = workspace.project().clone();
315 ThreadStore::load(
316 project,
317 tools.clone(),
318 prompt_store.clone(),
319 prompt_builder.clone(),
320 cx,
321 )
322 })?
323 .await?;
324
325 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
326 let context_store = workspace
327 .update(cx, |workspace, cx| {
328 let project = workspace.project().clone();
329 assistant_context_editor::ContextStore::new(
330 project,
331 prompt_builder.clone(),
332 slash_commands,
333 cx,
334 )
335 })?
336 .await?;
337
338 workspace.update_in(cx, |workspace, window, cx| {
339 cx.new(|cx| {
340 Self::new(
341 workspace,
342 thread_store,
343 context_store,
344 prompt_store,
345 window,
346 cx,
347 )
348 })
349 })
350 })
351 }
352
353 fn new(
354 workspace: &Workspace,
355 thread_store: Entity<ThreadStore>,
356 context_store: Entity<assistant_context_editor::ContextStore>,
357 prompt_store: Option<Entity<PromptStore>>,
358 window: &mut Window,
359 cx: &mut Context<Self>,
360 ) -> Self {
361 let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
362 let fs = workspace.app_state().fs.clone();
363 let user_store = workspace.app_state().user_store.clone();
364 let project = workspace.project();
365 let language_registry = project.read(cx).languages().clone();
366 let workspace = workspace.weak_handle();
367 let weak_self = cx.entity().downgrade();
368
369 let message_editor_context_store = cx.new(|_cx| {
370 crate::context_store::ContextStore::new(
371 project.downgrade(),
372 Some(thread_store.downgrade()),
373 )
374 });
375
376 let message_editor = cx.new(|cx| {
377 MessageEditor::new(
378 fs.clone(),
379 workspace.clone(),
380 message_editor_context_store.clone(),
381 prompt_store.clone(),
382 thread_store.downgrade(),
383 thread.clone(),
384 window,
385 cx,
386 )
387 });
388
389 let message_editor_subscription =
390 cx.subscribe(&message_editor, |_, _, event, cx| match event {
391 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
392 cx.notify();
393 }
394 });
395
396 let history_store = cx.new(|cx| {
397 HistoryStore::new(
398 thread_store.clone(),
399 context_store.clone(),
400 [RecentEntry::Thread(thread.clone())],
401 cx,
402 )
403 });
404
405 cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
406
407 let active_view = ActiveView::thread(thread.clone(), window, cx);
408 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
409 if let ThreadEvent::MessageAdded(_) = &event {
410 // needed to leave empty state
411 cx.notify();
412 }
413 });
414 let active_thread = cx.new(|cx| {
415 ActiveThread::new(
416 thread.clone(),
417 thread_store.clone(),
418 language_registry.clone(),
419 workspace.clone(),
420 window,
421 cx,
422 )
423 });
424
425 let active_thread_subscription =
426 cx.subscribe(&active_thread, |_, _, event, cx| match &event {
427 ActiveThreadEvent::EditingMessageTokenCountChanged => {
428 cx.notify();
429 }
430 });
431
432 let weak_panel = weak_self.clone();
433
434 window.defer(cx, move |window, cx| {
435 let panel = weak_panel.clone();
436 let assistant_navigation_menu =
437 ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
438 let recently_opened = panel
439 .update(cx, |this, cx| {
440 this.history_store.update(cx, |history_store, cx| {
441 history_store.recently_opened_entries(cx)
442 })
443 })
444 .unwrap_or_default();
445
446 if !recently_opened.is_empty() {
447 menu = menu.header("Recently Opened");
448
449 for entry in recently_opened.iter() {
450 let summary = entry.summary(cx);
451 menu = menu.entry_with_end_slot(
452 summary,
453 None,
454 {
455 let panel = panel.clone();
456 let entry = entry.clone();
457 move |window, cx| {
458 panel
459 .update(cx, {
460 let entry = entry.clone();
461 move |this, cx| match entry {
462 RecentEntry::Thread(thread) => {
463 this.open_thread(thread, window, cx)
464 }
465 RecentEntry::Context(context) => {
466 let Some(path) = context.read(cx).path()
467 else {
468 return;
469 };
470 this.open_saved_prompt_editor(
471 path.clone(),
472 window,
473 cx,
474 )
475 .detach_and_log_err(cx)
476 }
477 }
478 })
479 .ok();
480 }
481 },
482 IconName::Close,
483 "Close Entry".into(),
484 {
485 let panel = panel.clone();
486 let entry = entry.clone();
487 move |_window, cx| {
488 panel
489 .update(cx, |this, cx| {
490 this.history_store.update(
491 cx,
492 |history_store, cx| {
493 history_store.remove_recently_opened_entry(
494 &entry, cx,
495 );
496 },
497 );
498 })
499 .ok();
500 }
501 },
502 );
503 }
504
505 menu = menu.separator();
506 }
507
508 menu.action("View All", Box::new(OpenHistory))
509 .end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
510 .fixed_width(px(320.).into())
511 .keep_open_on_confirm(false)
512 .key_context("NavigationMenu")
513 });
514 weak_panel
515 .update(cx, |panel, cx| {
516 cx.subscribe_in(
517 &assistant_navigation_menu,
518 window,
519 |_, menu, _: &DismissEvent, window, cx| {
520 menu.update(cx, |menu, _| {
521 menu.clear_selected();
522 });
523 cx.focus_self(window);
524 },
525 )
526 .detach();
527 panel.assistant_navigation_menu = Some(assistant_navigation_menu);
528 })
529 .ok();
530 });
531
532 let _default_model_subscription = cx.subscribe(
533 &LanguageModelRegistry::global(cx),
534 |this, _, event: &language_model::Event, cx| match event {
535 language_model::Event::DefaultModelChanged => {
536 this.thread
537 .read(cx)
538 .thread()
539 .clone()
540 .update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
541 }
542 _ => {}
543 },
544 );
545
546 Self {
547 active_view,
548 workspace,
549 user_store,
550 project: project.clone(),
551 fs: fs.clone(),
552 language_registry,
553 thread_store: thread_store.clone(),
554 thread: active_thread,
555 message_editor,
556 _active_thread_subscriptions: vec![
557 thread_subscription,
558 active_thread_subscription,
559 message_editor_subscription,
560 ],
561 _default_model_subscription,
562 context_store,
563 prompt_store,
564 configuration: None,
565 configuration_subscription: None,
566 local_timezone: UtcOffset::from_whole_seconds(
567 chrono::Local::now().offset().local_minus_utc(),
568 )
569 .unwrap(),
570 previous_view: None,
571 history_store: history_store.clone(),
572 history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
573 assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
574 assistant_navigation_menu_handle: PopoverMenuHandle::default(),
575 assistant_navigation_menu: None,
576 width: None,
577 height: None,
578 }
579 }
580
581 pub fn toggle_focus(
582 workspace: &mut Workspace,
583 _: &ToggleFocus,
584 window: &mut Window,
585 cx: &mut Context<Workspace>,
586 ) {
587 if workspace
588 .panel::<Self>(cx)
589 .is_some_and(|panel| panel.read(cx).enabled(cx))
590 {
591 workspace.toggle_panel_focus::<Self>(window, cx);
592 }
593 }
594
595 pub(crate) fn local_timezone(&self) -> UtcOffset {
596 self.local_timezone
597 }
598
599 pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
600 &self.prompt_store
601 }
602
603 pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
604 &self.thread_store
605 }
606
607 fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
608 self.thread
609 .update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
610 }
611
612 fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
613 let thread = self
614 .thread_store
615 .update(cx, |this, cx| this.create_thread(cx));
616
617 let thread_view = ActiveView::thread(thread.clone(), window, cx);
618 self.set_active_view(thread_view, window, cx);
619
620 let message_editor_context_store = cx.new(|_cx| {
621 crate::context_store::ContextStore::new(
622 self.project.downgrade(),
623 Some(self.thread_store.downgrade()),
624 )
625 });
626
627 if let Some(other_thread_id) = action.from_thread_id.clone() {
628 let other_thread_task = self
629 .thread_store
630 .update(cx, |this, cx| this.open_thread(&other_thread_id, cx));
631
632 cx.spawn({
633 let context_store = message_editor_context_store.clone();
634
635 async move |_panel, cx| {
636 let other_thread = other_thread_task.await?;
637
638 context_store.update(cx, |this, cx| {
639 this.add_thread(other_thread, false, cx);
640 })?;
641 anyhow::Ok(())
642 }
643 })
644 .detach_and_log_err(cx);
645 }
646
647 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
648 if let ThreadEvent::MessageAdded(_) = &event {
649 // needed to leave empty state
650 cx.notify();
651 }
652 });
653
654 self.thread = cx.new(|cx| {
655 ActiveThread::new(
656 thread.clone(),
657 self.thread_store.clone(),
658 self.language_registry.clone(),
659 self.workspace.clone(),
660 window,
661 cx,
662 )
663 });
664
665 let active_thread_subscription =
666 cx.subscribe(&self.thread, |_, _, event, cx| match &event {
667 ActiveThreadEvent::EditingMessageTokenCountChanged => {
668 cx.notify();
669 }
670 });
671
672 self.message_editor = cx.new(|cx| {
673 MessageEditor::new(
674 self.fs.clone(),
675 self.workspace.clone(),
676 message_editor_context_store,
677 self.prompt_store.clone(),
678 self.thread_store.downgrade(),
679 thread,
680 window,
681 cx,
682 )
683 });
684 self.message_editor.focus_handle(cx).focus(window);
685
686 let message_editor_subscription =
687 cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
688 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
689 cx.notify();
690 }
691 });
692
693 self._active_thread_subscriptions = vec![
694 thread_subscription,
695 active_thread_subscription,
696 message_editor_subscription,
697 ];
698 }
699
700 fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
701 let context = self
702 .context_store
703 .update(cx, |context_store, cx| context_store.create(cx));
704 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
705 .log_err()
706 .flatten();
707
708 let context_editor = cx.new(|cx| {
709 let mut editor = ContextEditor::for_context(
710 context,
711 self.fs.clone(),
712 self.workspace.clone(),
713 self.project.clone(),
714 lsp_adapter_delegate,
715 window,
716 cx,
717 );
718 editor.insert_default_prompt(window, cx);
719 editor
720 });
721
722 self.set_active_view(
723 ActiveView::prompt_editor(context_editor.clone(), window, cx),
724 window,
725 cx,
726 );
727 context_editor.focus_handle(cx).focus(window);
728 }
729
730 fn deploy_rules_library(
731 &mut self,
732 action: &OpenRulesLibrary,
733 _window: &mut Window,
734 cx: &mut Context<Self>,
735 ) {
736 open_rules_library(
737 self.language_registry.clone(),
738 Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
739 Arc::new(|| {
740 Box::new(SlashCommandCompletionProvider::new(
741 Arc::new(SlashCommandWorkingSet::default()),
742 None,
743 None,
744 ))
745 }),
746 action
747 .prompt_to_select
748 .map(|uuid| UserPromptId(uuid).into()),
749 cx,
750 )
751 .detach_and_log_err(cx);
752 }
753
754 fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
755 if matches!(self.active_view, ActiveView::History) {
756 if let Some(previous_view) = self.previous_view.take() {
757 self.set_active_view(previous_view, window, cx);
758 }
759 } else {
760 self.thread_store
761 .update(cx, |thread_store, cx| thread_store.reload(cx))
762 .detach_and_log_err(cx);
763 self.set_active_view(ActiveView::History, window, cx);
764 }
765 cx.notify();
766 }
767
768 pub(crate) fn open_saved_prompt_editor(
769 &mut self,
770 path: Arc<Path>,
771 window: &mut Window,
772 cx: &mut Context<Self>,
773 ) -> Task<Result<()>> {
774 let context = self
775 .context_store
776 .update(cx, |store, cx| store.open_local_context(path, cx));
777 let fs = self.fs.clone();
778 let project = self.project.clone();
779 let workspace = self.workspace.clone();
780
781 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
782
783 cx.spawn_in(window, async move |this, cx| {
784 let context = context.await?;
785 this.update_in(cx, |this, window, cx| {
786 let editor = cx.new(|cx| {
787 ContextEditor::for_context(
788 context,
789 fs,
790 workspace,
791 project,
792 lsp_adapter_delegate,
793 window,
794 cx,
795 )
796 });
797
798 this.set_active_view(
799 ActiveView::prompt_editor(editor.clone(), window, cx),
800 window,
801 cx,
802 );
803
804 anyhow::Ok(())
805 })??;
806 Ok(())
807 })
808 }
809
810 pub(crate) fn open_thread_by_id(
811 &mut self,
812 thread_id: &ThreadId,
813 window: &mut Window,
814 cx: &mut Context<Self>,
815 ) -> Task<Result<()>> {
816 let open_thread_task = self
817 .thread_store
818 .update(cx, |this, cx| this.open_thread(thread_id, cx));
819 cx.spawn_in(window, async move |this, cx| {
820 let thread = open_thread_task.await?;
821 this.update_in(cx, |this, window, cx| {
822 this.open_thread(thread, window, cx);
823 anyhow::Ok(())
824 })??;
825 Ok(())
826 })
827 }
828
829 pub(crate) fn open_thread(
830 &mut self,
831 thread: Entity<Thread>,
832 window: &mut Window,
833 cx: &mut Context<Self>,
834 ) {
835 let thread_view = ActiveView::thread(thread.clone(), window, cx);
836 self.set_active_view(thread_view, window, cx);
837 let message_editor_context_store = cx.new(|_cx| {
838 crate::context_store::ContextStore::new(
839 self.project.downgrade(),
840 Some(self.thread_store.downgrade()),
841 )
842 });
843 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
844 if let ThreadEvent::MessageAdded(_) = &event {
845 // needed to leave empty state
846 cx.notify();
847 }
848 });
849
850 self.thread = cx.new(|cx| {
851 ActiveThread::new(
852 thread.clone(),
853 self.thread_store.clone(),
854 self.language_registry.clone(),
855 self.workspace.clone(),
856 window,
857 cx,
858 )
859 });
860
861 let active_thread_subscription =
862 cx.subscribe(&self.thread, |_, _, event, cx| match &event {
863 ActiveThreadEvent::EditingMessageTokenCountChanged => {
864 cx.notify();
865 }
866 });
867
868 self.message_editor = cx.new(|cx| {
869 MessageEditor::new(
870 self.fs.clone(),
871 self.workspace.clone(),
872 message_editor_context_store,
873 self.prompt_store.clone(),
874 self.thread_store.downgrade(),
875 thread,
876 window,
877 cx,
878 )
879 });
880 self.message_editor.focus_handle(cx).focus(window);
881
882 let message_editor_subscription =
883 cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
884 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
885 cx.notify();
886 }
887 });
888
889 self._active_thread_subscriptions = vec![
890 thread_subscription,
891 active_thread_subscription,
892 message_editor_subscription,
893 ];
894 }
895
896 pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
897 match self.active_view {
898 ActiveView::Configuration | ActiveView::History => {
899 self.active_view =
900 ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
901 self.message_editor.focus_handle(cx).focus(window);
902 cx.notify();
903 }
904 _ => {}
905 }
906 }
907
908 pub fn toggle_navigation_menu(
909 &mut self,
910 _: &ToggleNavigationMenu,
911 window: &mut Window,
912 cx: &mut Context<Self>,
913 ) {
914 self.assistant_navigation_menu_handle.toggle(window, cx);
915 }
916
917 pub fn open_agent_diff(
918 &mut self,
919 _: &OpenAgentDiff,
920 window: &mut Window,
921 cx: &mut Context<Self>,
922 ) {
923 let thread = self.thread.read(cx).thread().clone();
924 self.workspace
925 .update(cx, |workspace, cx| {
926 AgentDiff::deploy_in_workspace(thread, workspace, window, cx)
927 })
928 .log_err();
929 }
930
931 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
932 let context_server_manager = self.thread_store.read(cx).context_server_manager();
933 let tools = self.thread_store.read(cx).tools();
934 let fs = self.fs.clone();
935
936 self.set_active_view(ActiveView::Configuration, window, cx);
937 self.configuration =
938 Some(cx.new(|cx| {
939 AssistantConfiguration::new(fs, context_server_manager, tools, window, cx)
940 }));
941
942 if let Some(configuration) = self.configuration.as_ref() {
943 self.configuration_subscription = Some(cx.subscribe_in(
944 configuration,
945 window,
946 Self::handle_assistant_configuration_event,
947 ));
948
949 configuration.focus_handle(cx).focus(window);
950 }
951 }
952
953 pub(crate) fn open_active_thread_as_markdown(
954 &mut self,
955 _: &OpenActiveThreadAsMarkdown,
956 window: &mut Window,
957 cx: &mut Context<Self>,
958 ) {
959 let Some(workspace) = self
960 .workspace
961 .upgrade()
962 .ok_or_else(|| anyhow!("workspace dropped"))
963 .log_err()
964 else {
965 return;
966 };
967
968 let markdown_language_task = workspace
969 .read(cx)
970 .app_state()
971 .languages
972 .language_for_name("Markdown");
973 let thread = self.active_thread(cx);
974 cx.spawn_in(window, async move |_this, cx| {
975 let markdown_language = markdown_language_task.await?;
976
977 workspace.update_in(cx, |workspace, window, cx| {
978 let thread = thread.read(cx);
979 let markdown = thread.to_markdown(cx)?;
980 let thread_summary = thread
981 .summary()
982 .map(|summary| summary.to_string())
983 .unwrap_or_else(|| "Thread".to_string());
984
985 let project = workspace.project().clone();
986 let buffer = project.update(cx, |project, cx| {
987 project.create_local_buffer(&markdown, Some(markdown_language), cx)
988 });
989 let buffer = cx.new(|cx| {
990 MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
991 });
992
993 workspace.add_item_to_active_pane(
994 Box::new(cx.new(|cx| {
995 let mut editor =
996 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
997 editor.set_breadcrumb_header(thread_summary);
998 editor
999 })),
1000 None,
1001 true,
1002 window,
1003 cx,
1004 );
1005
1006 anyhow::Ok(())
1007 })
1008 })
1009 .detach_and_log_err(cx);
1010 }
1011
1012 fn handle_assistant_configuration_event(
1013 &mut self,
1014 _entity: &Entity<AssistantConfiguration>,
1015 event: &AssistantConfigurationEvent,
1016 window: &mut Window,
1017 cx: &mut Context<Self>,
1018 ) {
1019 match event {
1020 AssistantConfigurationEvent::NewThread(provider) => {
1021 if LanguageModelRegistry::read_global(cx)
1022 .default_model()
1023 .map_or(true, |model| model.provider.id() != provider.id())
1024 {
1025 if let Some(model) = provider.default_model(cx) {
1026 update_settings_file::<AssistantSettings>(
1027 self.fs.clone(),
1028 cx,
1029 move |settings, _| settings.set_model(model),
1030 );
1031 }
1032 }
1033
1034 self.new_thread(&NewThread::default(), window, cx);
1035 }
1036 }
1037 }
1038
1039 pub(crate) fn active_thread(&self, cx: &App) -> Entity<Thread> {
1040 self.thread.read(cx).thread().clone()
1041 }
1042
1043 pub(crate) fn delete_thread(
1044 &mut self,
1045 thread_id: &ThreadId,
1046 cx: &mut Context<Self>,
1047 ) -> Task<Result<()>> {
1048 self.thread_store
1049 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
1050 }
1051
1052 pub(crate) fn has_active_thread(&self) -> bool {
1053 matches!(self.active_view, ActiveView::Thread { .. })
1054 }
1055
1056 pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
1057 match &self.active_view {
1058 ActiveView::PromptEditor { context_editor, .. } => Some(context_editor.clone()),
1059 _ => None,
1060 }
1061 }
1062
1063 pub(crate) fn delete_context(
1064 &mut self,
1065 path: Arc<Path>,
1066 cx: &mut Context<Self>,
1067 ) -> Task<Result<()>> {
1068 self.context_store
1069 .update(cx, |this, cx| this.delete_local_context(path, cx))
1070 }
1071
1072 fn set_active_view(
1073 &mut self,
1074 new_view: ActiveView,
1075 window: &mut Window,
1076 cx: &mut Context<Self>,
1077 ) {
1078 let current_is_history = matches!(self.active_view, ActiveView::History);
1079 let new_is_history = matches!(new_view, ActiveView::History);
1080
1081 match &self.active_view {
1082 ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1083 if let Some(thread) = thread.upgrade() {
1084 if thread.read(cx).is_empty() {
1085 store.remove_recently_opened_entry(&RecentEntry::Thread(thread), cx);
1086 }
1087 }
1088 }),
1089 _ => {}
1090 }
1091
1092 match &new_view {
1093 ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1094 if let Some(thread) = thread.upgrade() {
1095 store.push_recently_opened_entry(RecentEntry::Thread(thread), cx);
1096 }
1097 }),
1098 ActiveView::PromptEditor { context_editor, .. } => {
1099 self.history_store.update(cx, |store, cx| {
1100 let context = context_editor.read(cx).context().clone();
1101 store.push_recently_opened_entry(RecentEntry::Context(context), cx)
1102 })
1103 }
1104 _ => {}
1105 }
1106
1107 if current_is_history && !new_is_history {
1108 self.active_view = new_view;
1109 } else if !current_is_history && new_is_history {
1110 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1111 } else {
1112 if !new_is_history {
1113 self.previous_view = None;
1114 }
1115 self.active_view = new_view;
1116 }
1117
1118 self.focus_handle(cx).focus(window);
1119 }
1120}
1121
1122impl Focusable for AssistantPanel {
1123 fn focus_handle(&self, cx: &App) -> FocusHandle {
1124 match &self.active_view {
1125 ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
1126 ActiveView::History => self.history.focus_handle(cx),
1127 ActiveView::PromptEditor { context_editor, .. } => context_editor.focus_handle(cx),
1128 ActiveView::Configuration => {
1129 if let Some(configuration) = self.configuration.as_ref() {
1130 configuration.focus_handle(cx)
1131 } else {
1132 cx.focus_handle()
1133 }
1134 }
1135 }
1136 }
1137}
1138
1139impl EventEmitter<PanelEvent> for AssistantPanel {}
1140
1141impl Panel for AssistantPanel {
1142 fn persistent_name() -> &'static str {
1143 "AgentPanel"
1144 }
1145
1146 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1147 match AssistantSettings::get_global(cx).dock {
1148 AssistantDockPosition::Left => DockPosition::Left,
1149 AssistantDockPosition::Bottom => DockPosition::Bottom,
1150 AssistantDockPosition::Right => DockPosition::Right,
1151 }
1152 }
1153
1154 fn position_is_valid(&self, _: DockPosition) -> bool {
1155 true
1156 }
1157
1158 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1159 settings::update_settings_file::<AssistantSettings>(
1160 self.fs.clone(),
1161 cx,
1162 move |settings, _| {
1163 let dock = match position {
1164 DockPosition::Left => AssistantDockPosition::Left,
1165 DockPosition::Bottom => AssistantDockPosition::Bottom,
1166 DockPosition::Right => AssistantDockPosition::Right,
1167 };
1168 settings.set_dock(dock);
1169 },
1170 );
1171 }
1172
1173 fn size(&self, window: &Window, cx: &App) -> Pixels {
1174 let settings = AssistantSettings::get_global(cx);
1175 match self.position(window, cx) {
1176 DockPosition::Left | DockPosition::Right => {
1177 self.width.unwrap_or(settings.default_width)
1178 }
1179 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1180 }
1181 }
1182
1183 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1184 match self.position(window, cx) {
1185 DockPosition::Left | DockPosition::Right => self.width = size,
1186 DockPosition::Bottom => self.height = size,
1187 }
1188 cx.notify();
1189 }
1190
1191 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
1192
1193 fn remote_id() -> Option<proto::PanelId> {
1194 Some(proto::PanelId::AssistantPanel)
1195 }
1196
1197 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1198 (self.enabled(cx) && AssistantSettings::get_global(cx).button)
1199 .then_some(IconName::ZedAssistant)
1200 }
1201
1202 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1203 Some("Agent Panel")
1204 }
1205
1206 fn toggle_action(&self) -> Box<dyn Action> {
1207 Box::new(ToggleFocus)
1208 }
1209
1210 fn activation_priority(&self) -> u32 {
1211 3
1212 }
1213
1214 fn enabled(&self, cx: &App) -> bool {
1215 AssistantSettings::get_global(cx).enabled
1216 }
1217}
1218
1219impl AssistantPanel {
1220 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1221 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1222
1223 let content = match &self.active_view {
1224 ActiveView::Thread {
1225 change_title_editor,
1226 ..
1227 } => {
1228 let active_thread = self.thread.read(cx);
1229 let is_empty = active_thread.is_empty();
1230
1231 let summary = active_thread.summary(cx);
1232
1233 if is_empty {
1234 Label::new(Thread::DEFAULT_SUMMARY.clone())
1235 .truncate()
1236 .into_any_element()
1237 } else if summary.is_none() {
1238 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1239 .truncate()
1240 .into_any_element()
1241 } else {
1242 div()
1243 .w_full()
1244 .child(change_title_editor.clone())
1245 .into_any_element()
1246 }
1247 }
1248 ActiveView::PromptEditor {
1249 title_editor,
1250 context_editor,
1251 ..
1252 } => {
1253 let context_editor = context_editor.read(cx);
1254 let summary = context_editor.context().read(cx).summary();
1255
1256 match summary {
1257 None => Label::new(AssistantContext::DEFAULT_SUMMARY.clone())
1258 .truncate()
1259 .into_any_element(),
1260 Some(summary) => {
1261 if summary.done {
1262 div()
1263 .w_full()
1264 .child(title_editor.clone())
1265 .into_any_element()
1266 } else {
1267 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1268 .truncate()
1269 .into_any_element()
1270 }
1271 }
1272 }
1273 }
1274 ActiveView::History => Label::new("History").truncate().into_any_element(),
1275 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1276 };
1277
1278 h_flex()
1279 .key_context("TitleEditor")
1280 .id("TitleEditor")
1281 .flex_grow()
1282 .w_full()
1283 .max_w_full()
1284 .overflow_x_scroll()
1285 .child(content)
1286 .into_any()
1287 }
1288
1289 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1290 let active_thread = self.thread.read(cx);
1291 let thread = active_thread.thread().read(cx);
1292 let thread_id = thread.id().clone();
1293 let is_empty = active_thread.is_empty();
1294
1295 let show_token_count = match &self.active_view {
1296 ActiveView::Thread { .. } => !is_empty,
1297 ActiveView::PromptEditor { .. } => true,
1298 _ => false,
1299 };
1300
1301 let focus_handle = self.focus_handle(cx);
1302
1303 let go_back_button = div().child(
1304 IconButton::new("go-back", IconName::ArrowLeft)
1305 .icon_size(IconSize::Small)
1306 .on_click(cx.listener(|this, _, window, cx| {
1307 this.go_back(&workspace::GoBack, window, cx);
1308 }))
1309 .tooltip({
1310 let focus_handle = focus_handle.clone();
1311 move |window, cx| {
1312 Tooltip::for_action_in(
1313 "Go Back",
1314 &workspace::GoBack,
1315 &focus_handle,
1316 window,
1317 cx,
1318 )
1319 }
1320 }),
1321 );
1322
1323 let recent_entries_menu = div().child(
1324 PopoverMenu::new("agent-nav-menu")
1325 .trigger_with_tooltip(
1326 IconButton::new("agent-nav-menu", IconName::MenuAlt)
1327 .icon_size(IconSize::Small)
1328 .style(ui::ButtonStyle::Subtle),
1329 {
1330 let focus_handle = focus_handle.clone();
1331 move |window, cx| {
1332 Tooltip::for_action_in(
1333 "Toggle Panel Menu",
1334 &ToggleNavigationMenu,
1335 &focus_handle,
1336 window,
1337 cx,
1338 )
1339 }
1340 },
1341 )
1342 .anchor(Corner::TopLeft)
1343 .with_handle(self.assistant_navigation_menu_handle.clone())
1344 .menu({
1345 let menu = self.assistant_navigation_menu.clone();
1346 move |window, cx| {
1347 if let Some(menu) = menu.as_ref() {
1348 menu.update(cx, |_, cx| {
1349 cx.defer_in(window, |menu, window, cx| {
1350 menu.rebuild(window, cx);
1351 });
1352 })
1353 }
1354 menu.clone()
1355 }
1356 }),
1357 );
1358
1359 let agent_extra_menu = PopoverMenu::new("assistant-menu")
1360 .trigger_with_tooltip(
1361 IconButton::new("new", IconName::Ellipsis)
1362 .icon_size(IconSize::Small)
1363 .style(ButtonStyle::Subtle),
1364 Tooltip::text("Toggle Agent Menu"),
1365 )
1366 .anchor(Corner::TopRight)
1367 .with_handle(self.assistant_dropdown_menu_handle.clone())
1368 .menu(move |window, cx| {
1369 Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
1370 menu.when(!is_empty, |menu| {
1371 menu.action(
1372 "Start New From Summary",
1373 Box::new(NewThread {
1374 from_thread_id: Some(thread_id.clone()),
1375 }),
1376 )
1377 .separator()
1378 })
1379 .action("New Text Thread", NewTextThread.boxed_clone())
1380 .action("Rules Library", Box::new(OpenRulesLibrary::default()))
1381 .action("Settings", Box::new(OpenConfiguration))
1382 .separator()
1383 .header("MCPs")
1384 .action(
1385 "View Server Extensions",
1386 Box::new(zed_actions::Extensions {
1387 category_filter: Some(
1388 zed_actions::ExtensionCategoryFilter::ContextServers,
1389 ),
1390 }),
1391 )
1392 .action("Add Custom Server", Box::new(AddContextServer))
1393 }))
1394 });
1395
1396 h_flex()
1397 .id("assistant-toolbar")
1398 .h(Tab::container_height(cx))
1399 .max_w_full()
1400 .flex_none()
1401 .justify_between()
1402 .gap_2()
1403 .bg(cx.theme().colors().tab_bar_background)
1404 .border_b_1()
1405 .border_color(cx.theme().colors().border)
1406 .child(
1407 h_flex()
1408 .size_full()
1409 .pl_1()
1410 .gap_1()
1411 .child(match &self.active_view {
1412 ActiveView::History | ActiveView::Configuration => go_back_button,
1413 _ => recent_entries_menu,
1414 })
1415 .child(self.render_title_view(window, cx)),
1416 )
1417 .child(
1418 h_flex()
1419 .h_full()
1420 .gap_2()
1421 .when(show_token_count, |parent| {
1422 parent.children(self.render_token_count(&thread, cx))
1423 })
1424 .child(
1425 h_flex()
1426 .h_full()
1427 .gap(DynamicSpacing::Base02.rems(cx))
1428 .px(DynamicSpacing::Base08.rems(cx))
1429 .border_l_1()
1430 .border_color(cx.theme().colors().border)
1431 .child(
1432 IconButton::new("new", IconName::Plus)
1433 .icon_size(IconSize::Small)
1434 .style(ButtonStyle::Subtle)
1435 .tooltip(move |window, cx| {
1436 Tooltip::for_action_in(
1437 "New Thread",
1438 &NewThread::default(),
1439 &focus_handle,
1440 window,
1441 cx,
1442 )
1443 })
1444 .on_click(move |_event, window, cx| {
1445 window.dispatch_action(
1446 NewThread::default().boxed_clone(),
1447 cx,
1448 );
1449 }),
1450 )
1451 .child(agent_extra_menu),
1452 ),
1453 )
1454 }
1455
1456 fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
1457 let is_generating = thread.is_generating();
1458 let message_editor = self.message_editor.read(cx);
1459
1460 let conversation_token_usage = thread.total_token_usage()?;
1461
1462 let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
1463 self.thread.read(cx).editing_message_id()
1464 {
1465 let combined = thread
1466 .token_usage_up_to_message(editing_message_id)
1467 .add(unsent_tokens);
1468
1469 (combined, unsent_tokens > 0)
1470 } else {
1471 let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
1472 let combined = conversation_token_usage.add(unsent_tokens);
1473
1474 (combined, unsent_tokens > 0)
1475 };
1476
1477 let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
1478
1479 match &self.active_view {
1480 ActiveView::Thread { .. } => {
1481 if total_token_usage.total == 0 {
1482 return None;
1483 }
1484
1485 let token_color = match total_token_usage.ratio() {
1486 TokenUsageRatio::Normal if is_estimating => Color::Default,
1487 TokenUsageRatio::Normal => Color::Muted,
1488 TokenUsageRatio::Warning => Color::Warning,
1489 TokenUsageRatio::Exceeded => Color::Error,
1490 };
1491
1492 let token_count = h_flex()
1493 .id("token-count")
1494 .flex_shrink_0()
1495 .gap_0p5()
1496 .when(!is_generating && is_estimating, |parent| {
1497 parent
1498 .child(
1499 h_flex()
1500 .mr_1()
1501 .size_2p5()
1502 .justify_center()
1503 .rounded_full()
1504 .bg(cx.theme().colors().text.opacity(0.1))
1505 .child(
1506 div().size_1().rounded_full().bg(cx.theme().colors().text),
1507 ),
1508 )
1509 .tooltip(move |window, cx| {
1510 Tooltip::with_meta(
1511 "Estimated New Token Count",
1512 None,
1513 format!(
1514 "Current Conversation Tokens: {}",
1515 humanize_token_count(conversation_token_usage.total)
1516 ),
1517 window,
1518 cx,
1519 )
1520 })
1521 })
1522 .child(
1523 Label::new(humanize_token_count(total_token_usage.total))
1524 .size(LabelSize::Small)
1525 .color(token_color)
1526 .map(|label| {
1527 if is_generating || is_waiting_to_update_token_count {
1528 label
1529 .with_animation(
1530 "used-tokens-label",
1531 Animation::new(Duration::from_secs(2))
1532 .repeat()
1533 .with_easing(pulsating_between(0.6, 1.)),
1534 |label, delta| label.alpha(delta),
1535 )
1536 .into_any()
1537 } else {
1538 label.into_any_element()
1539 }
1540 }),
1541 )
1542 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
1543 .child(
1544 Label::new(humanize_token_count(total_token_usage.max))
1545 .size(LabelSize::Small)
1546 .color(Color::Muted),
1547 )
1548 .into_any();
1549
1550 Some(token_count)
1551 }
1552 ActiveView::PromptEditor { context_editor, .. } => {
1553 let element = render_remaining_tokens(context_editor, cx)?;
1554
1555 Some(element.into_any_element())
1556 }
1557 _ => None,
1558 }
1559 }
1560
1561 fn render_active_thread_or_empty_state(
1562 &self,
1563 window: &mut Window,
1564 cx: &mut Context<Self>,
1565 ) -> AnyElement {
1566 if self.thread.read(cx).is_empty() {
1567 return self
1568 .render_thread_empty_state(window, cx)
1569 .into_any_element();
1570 }
1571
1572 self.thread.clone().into_any_element()
1573 }
1574
1575 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
1576 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
1577 return Some(ConfigurationError::NoProvider);
1578 };
1579
1580 if !model.provider.is_authenticated(cx) {
1581 return Some(ConfigurationError::ProviderNotAuthenticated);
1582 }
1583
1584 if model.provider.must_accept_terms(cx) {
1585 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
1586 model.provider,
1587 ));
1588 }
1589
1590 None
1591 }
1592
1593 fn render_thread_empty_state(
1594 &self,
1595 window: &mut Window,
1596 cx: &mut Context<Self>,
1597 ) -> impl IntoElement {
1598 let recent_history = self
1599 .history_store
1600 .update(cx, |this, cx| this.recent_entries(6, cx));
1601
1602 let configuration_error = self.configuration_error(cx);
1603 let no_error = configuration_error.is_none();
1604 let focus_handle = self.focus_handle(cx);
1605
1606 v_flex()
1607 .size_full()
1608 .when(recent_history.is_empty(), |this| {
1609 let configuration_error_ref = &configuration_error;
1610 this.child(
1611 v_flex()
1612 .size_full()
1613 .max_w_80()
1614 .mx_auto()
1615 .justify_center()
1616 .items_center()
1617 .gap_1()
1618 .child(
1619 h_flex().child(
1620 Headline::new("Welcome to the Agent Panel")
1621 ),
1622 )
1623 .when(no_error, |parent| {
1624 parent
1625 .child(
1626 h_flex().child(
1627 Label::new("Ask and build anything.")
1628 .color(Color::Muted)
1629 .mb_2p5(),
1630 ),
1631 )
1632 .child(
1633 Button::new("new-thread", "Start New Thread")
1634 .icon(IconName::Plus)
1635 .icon_position(IconPosition::Start)
1636 .icon_size(IconSize::Small)
1637 .icon_color(Color::Muted)
1638 .full_width()
1639 .key_binding(KeyBinding::for_action_in(
1640 &NewThread::default(),
1641 &focus_handle,
1642 window,
1643 cx,
1644 ))
1645 .on_click(|_event, window, cx| {
1646 window.dispatch_action(NewThread::default().boxed_clone(), cx)
1647 }),
1648 )
1649 .child(
1650 Button::new("context", "Add Context")
1651 .icon(IconName::FileCode)
1652 .icon_position(IconPosition::Start)
1653 .icon_size(IconSize::Small)
1654 .icon_color(Color::Muted)
1655 .full_width()
1656 .key_binding(KeyBinding::for_action_in(
1657 &ToggleContextPicker,
1658 &focus_handle,
1659 window,
1660 cx,
1661 ))
1662 .on_click(|_event, window, cx| {
1663 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
1664 }),
1665 )
1666 .child(
1667 Button::new("mode", "Switch Model")
1668 .icon(IconName::DatabaseZap)
1669 .icon_position(IconPosition::Start)
1670 .icon_size(IconSize::Small)
1671 .icon_color(Color::Muted)
1672 .full_width()
1673 .key_binding(KeyBinding::for_action_in(
1674 &ToggleModelSelector,
1675 &focus_handle,
1676 window,
1677 cx,
1678 ))
1679 .on_click(|_event, window, cx| {
1680 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
1681 }),
1682 )
1683 .child(
1684 Button::new("settings", "View Settings")
1685 .icon(IconName::Settings)
1686 .icon_position(IconPosition::Start)
1687 .icon_size(IconSize::Small)
1688 .icon_color(Color::Muted)
1689 .full_width()
1690 .key_binding(KeyBinding::for_action_in(
1691 &OpenConfiguration,
1692 &focus_handle,
1693 window,
1694 cx,
1695 ))
1696 .on_click(|_event, window, cx| {
1697 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1698 }),
1699 )
1700 })
1701 .map(|parent| {
1702 match configuration_error_ref {
1703 Some(ConfigurationError::ProviderNotAuthenticated)
1704 | Some(ConfigurationError::NoProvider) => {
1705 parent
1706 .child(
1707 h_flex().child(
1708 Label::new("To start using the agent, configure at least one LLM provider.")
1709 .color(Color::Muted)
1710 .mb_2p5()
1711 )
1712 )
1713 .child(
1714 Button::new("settings", "Configure a Provider")
1715 .icon(IconName::Settings)
1716 .icon_position(IconPosition::Start)
1717 .icon_size(IconSize::Small)
1718 .icon_color(Color::Muted)
1719 .full_width()
1720 .key_binding(KeyBinding::for_action_in(
1721 &OpenConfiguration,
1722 &focus_handle,
1723 window,
1724 cx,
1725 ))
1726 .on_click(|_event, window, cx| {
1727 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1728 }),
1729 )
1730 }
1731 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1732 parent.children(
1733 provider.render_accept_terms(
1734 LanguageModelProviderTosView::ThreadFreshStart,
1735 cx,
1736 ),
1737 )
1738 }
1739 None => parent,
1740 }
1741 })
1742 )
1743 })
1744 .when(!recent_history.is_empty(), |parent| {
1745 let focus_handle = focus_handle.clone();
1746 let configuration_error_ref = &configuration_error;
1747
1748 parent
1749 .overflow_hidden()
1750 .p_1p5()
1751 .justify_end()
1752 .gap_1()
1753 .child(
1754 h_flex()
1755 .pl_1p5()
1756 .pb_1()
1757 .w_full()
1758 .justify_between()
1759 .border_b_1()
1760 .border_color(cx.theme().colors().border_variant)
1761 .child(
1762 Label::new("Past Interactions")
1763 .size(LabelSize::Small)
1764 .color(Color::Muted),
1765 )
1766 .child(
1767 Button::new("view-history", "View All")
1768 .style(ButtonStyle::Subtle)
1769 .label_size(LabelSize::Small)
1770 .key_binding(
1771 KeyBinding::for_action_in(
1772 &OpenHistory,
1773 &self.focus_handle(cx),
1774 window,
1775 cx,
1776 ).map(|kb| kb.size(rems_from_px(12.))),
1777 )
1778 .on_click(move |_event, window, cx| {
1779 window.dispatch_action(OpenHistory.boxed_clone(), cx);
1780 }),
1781 ),
1782 )
1783 .child(
1784 v_flex()
1785 .gap_1()
1786 .children(
1787 recent_history.into_iter().map(|entry| {
1788 // TODO: Add keyboard navigation.
1789 match entry {
1790 HistoryEntry::Thread(thread) => {
1791 PastThread::new(thread, cx.entity().downgrade(), false, vec![])
1792 .into_any_element()
1793 }
1794 HistoryEntry::Context(context) => {
1795 PastContext::new(context, cx.entity().downgrade(), false, vec![])
1796 .into_any_element()
1797 }
1798 }
1799 }),
1800 )
1801 )
1802 .map(|parent| {
1803 match configuration_error_ref {
1804 Some(ConfigurationError::ProviderNotAuthenticated)
1805 | Some(ConfigurationError::NoProvider) => {
1806 parent
1807 .child(
1808 Banner::new()
1809 .severity(ui::Severity::Warning)
1810 .children(
1811 Label::new(
1812 "Configure at least one LLM provider to start using the panel.",
1813 )
1814 .size(LabelSize::Small),
1815 )
1816 .action_slot(
1817 Button::new("settings", "Configure Provider")
1818 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
1819 .label_size(LabelSize::Small)
1820 .key_binding(
1821 KeyBinding::for_action_in(
1822 &OpenConfiguration,
1823 &focus_handle,
1824 window,
1825 cx,
1826 )
1827 .map(|kb| kb.size(rems_from_px(12.))),
1828 )
1829 .on_click(|_event, window, cx| {
1830 window.dispatch_action(
1831 OpenConfiguration.boxed_clone(),
1832 cx,
1833 )
1834 }),
1835 ),
1836 )
1837 }
1838 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1839 parent
1840 .child(
1841 Banner::new()
1842 .severity(ui::Severity::Warning)
1843 .children(
1844 h_flex()
1845 .w_full()
1846 .children(
1847 provider.render_accept_terms(
1848 LanguageModelProviderTosView::ThreadtEmptyState,
1849 cx,
1850 ),
1851 ),
1852 ),
1853 )
1854 }
1855 None => parent,
1856 }
1857 })
1858 })
1859 }
1860
1861 fn render_usage_banner(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1862 let plan = self
1863 .user_store
1864 .read(cx)
1865 .current_plan()
1866 .map(|plan| match plan {
1867 Plan::Free => zed_llm_client::Plan::Free,
1868 Plan::ZedPro => zed_llm_client::Plan::ZedPro,
1869 Plan::ZedProTrial => zed_llm_client::Plan::ZedProTrial,
1870 })
1871 .unwrap_or(zed_llm_client::Plan::Free);
1872 let usage = self.thread.read(cx).last_usage()?;
1873
1874 Some(UsageBanner::new(plan, usage).into_any_element())
1875 }
1876
1877 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1878 let last_error = self.thread.read(cx).last_error()?;
1879
1880 Some(
1881 div()
1882 .absolute()
1883 .right_3()
1884 .bottom_12()
1885 .max_w_96()
1886 .py_2()
1887 .px_3()
1888 .elevation_2(cx)
1889 .occlude()
1890 .child(match last_error {
1891 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
1892 ThreadError::MaxMonthlySpendReached => {
1893 self.render_max_monthly_spend_reached_error(cx)
1894 }
1895 ThreadError::ModelRequestLimitReached { plan } => {
1896 self.render_model_request_limit_reached_error(plan, cx)
1897 }
1898 ThreadError::Message { header, message } => {
1899 self.render_error_message(header, message, cx)
1900 }
1901 })
1902 .into_any(),
1903 )
1904 }
1905
1906 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
1907 const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
1908
1909 v_flex()
1910 .gap_0p5()
1911 .child(
1912 h_flex()
1913 .gap_1p5()
1914 .items_center()
1915 .child(Icon::new(IconName::XCircle).color(Color::Error))
1916 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
1917 )
1918 .child(
1919 div()
1920 .id("error-message")
1921 .max_h_24()
1922 .overflow_y_scroll()
1923 .child(Label::new(ERROR_MESSAGE)),
1924 )
1925 .child(
1926 h_flex()
1927 .justify_end()
1928 .mt_1()
1929 .gap_1()
1930 .child(self.create_copy_button(ERROR_MESSAGE))
1931 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
1932 |this, _, _, cx| {
1933 this.thread.update(cx, |this, _cx| {
1934 this.clear_last_error();
1935 });
1936
1937 cx.open_url(&zed_urls::account_url(cx));
1938 cx.notify();
1939 },
1940 )))
1941 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1942 |this, _, _, cx| {
1943 this.thread.update(cx, |this, _cx| {
1944 this.clear_last_error();
1945 });
1946
1947 cx.notify();
1948 },
1949 ))),
1950 )
1951 .into_any()
1952 }
1953
1954 fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
1955 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
1956
1957 v_flex()
1958 .gap_0p5()
1959 .child(
1960 h_flex()
1961 .gap_1p5()
1962 .items_center()
1963 .child(Icon::new(IconName::XCircle).color(Color::Error))
1964 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
1965 )
1966 .child(
1967 div()
1968 .id("error-message")
1969 .max_h_24()
1970 .overflow_y_scroll()
1971 .child(Label::new(ERROR_MESSAGE)),
1972 )
1973 .child(
1974 h_flex()
1975 .justify_end()
1976 .mt_1()
1977 .gap_1()
1978 .child(self.create_copy_button(ERROR_MESSAGE))
1979 .child(
1980 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
1981 cx.listener(|this, _, _, cx| {
1982 this.thread.update(cx, |this, _cx| {
1983 this.clear_last_error();
1984 });
1985
1986 cx.open_url(&zed_urls::account_url(cx));
1987 cx.notify();
1988 }),
1989 ),
1990 )
1991 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1992 |this, _, _, cx| {
1993 this.thread.update(cx, |this, _cx| {
1994 this.clear_last_error();
1995 });
1996
1997 cx.notify();
1998 },
1999 ))),
2000 )
2001 .into_any()
2002 }
2003
2004 fn render_model_request_limit_reached_error(
2005 &self,
2006 plan: Plan,
2007 cx: &mut Context<Self>,
2008 ) -> AnyElement {
2009 let error_message = match plan {
2010 Plan::ZedPro => {
2011 "Model request limit reached. Upgrade to usage-based billing for more requests."
2012 }
2013 Plan::ZedProTrial => {
2014 "Model request limit reached. Upgrade to Zed Pro for more requests."
2015 }
2016 Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
2017 };
2018 let call_to_action = match plan {
2019 Plan::ZedPro => "Upgrade to usage-based billing",
2020 Plan::ZedProTrial => "Upgrade to Zed Pro",
2021 Plan::Free => "Upgrade to Zed Pro",
2022 };
2023
2024 v_flex()
2025 .gap_0p5()
2026 .child(
2027 h_flex()
2028 .gap_1p5()
2029 .items_center()
2030 .child(Icon::new(IconName::XCircle).color(Color::Error))
2031 .child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
2032 )
2033 .child(
2034 div()
2035 .id("error-message")
2036 .max_h_24()
2037 .overflow_y_scroll()
2038 .child(Label::new(error_message)),
2039 )
2040 .child(
2041 h_flex()
2042 .justify_end()
2043 .mt_1()
2044 .gap_1()
2045 .child(self.create_copy_button(error_message))
2046 .child(
2047 Button::new("subscribe", call_to_action).on_click(cx.listener(
2048 |this, _, _, cx| {
2049 this.thread.update(cx, |this, _cx| {
2050 this.clear_last_error();
2051 });
2052
2053 cx.open_url(&zed_urls::account_url(cx));
2054 cx.notify();
2055 },
2056 )),
2057 )
2058 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2059 |this, _, _, cx| {
2060 this.thread.update(cx, |this, _cx| {
2061 this.clear_last_error();
2062 });
2063
2064 cx.notify();
2065 },
2066 ))),
2067 )
2068 .into_any()
2069 }
2070
2071 fn render_error_message(
2072 &self,
2073 header: SharedString,
2074 message: SharedString,
2075 cx: &mut Context<Self>,
2076 ) -> AnyElement {
2077 let message_with_header = format!("{}\n{}", header, message);
2078 v_flex()
2079 .gap_0p5()
2080 .child(
2081 h_flex()
2082 .gap_1p5()
2083 .items_center()
2084 .child(Icon::new(IconName::XCircle).color(Color::Error))
2085 .child(Label::new(header).weight(FontWeight::MEDIUM)),
2086 )
2087 .child(
2088 div()
2089 .id("error-message")
2090 .max_h_32()
2091 .overflow_y_scroll()
2092 .child(Label::new(message.clone())),
2093 )
2094 .child(
2095 h_flex()
2096 .justify_end()
2097 .mt_1()
2098 .gap_1()
2099 .child(self.create_copy_button(message_with_header))
2100 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2101 |this, _, _, cx| {
2102 this.thread.update(cx, |this, _cx| {
2103 this.clear_last_error();
2104 });
2105
2106 cx.notify();
2107 },
2108 ))),
2109 )
2110 .into_any()
2111 }
2112
2113 fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
2114 let message = message.into();
2115 IconButton::new("copy", IconName::Copy)
2116 .on_click(move |_, _, cx| {
2117 cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
2118 })
2119 .tooltip(Tooltip::text("Copy Error Message"))
2120 }
2121
2122 fn key_context(&self) -> KeyContext {
2123 let mut key_context = KeyContext::new_with_defaults();
2124 key_context.add("AgentPanel");
2125 if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
2126 key_context.add("prompt_editor");
2127 }
2128 key_context
2129 }
2130}
2131
2132impl Render for AssistantPanel {
2133 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2134 v_flex()
2135 .key_context(self.key_context())
2136 .justify_between()
2137 .size_full()
2138 .on_action(cx.listener(Self::cancel))
2139 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
2140 this.new_thread(action, window, cx);
2141 }))
2142 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
2143 this.open_history(window, cx);
2144 }))
2145 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
2146 this.open_configuration(window, cx);
2147 }))
2148 .on_action(cx.listener(Self::open_active_thread_as_markdown))
2149 .on_action(cx.listener(Self::deploy_rules_library))
2150 .on_action(cx.listener(Self::open_agent_diff))
2151 .on_action(cx.listener(Self::go_back))
2152 .on_action(cx.listener(Self::toggle_navigation_menu))
2153 .child(self.render_toolbar(window, cx))
2154 .map(|parent| match &self.active_view {
2155 ActiveView::Thread { .. } => parent
2156 .child(self.render_active_thread_or_empty_state(window, cx))
2157 .children(self.render_usage_banner(cx))
2158 .child(h_flex().child(self.message_editor.clone()))
2159 .children(self.render_last_error(cx)),
2160 ActiveView::History => parent.child(self.history.clone()),
2161 ActiveView::PromptEditor { context_editor, .. } => {
2162 parent.child(context_editor.clone())
2163 }
2164 ActiveView::Configuration => parent.children(self.configuration.clone()),
2165 })
2166 }
2167}
2168
2169struct PromptLibraryInlineAssist {
2170 workspace: WeakEntity<Workspace>,
2171}
2172
2173impl PromptLibraryInlineAssist {
2174 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
2175 Self { workspace }
2176 }
2177}
2178
2179impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
2180 fn assist(
2181 &self,
2182 prompt_editor: &Entity<Editor>,
2183 _initial_prompt: Option<String>,
2184 window: &mut Window,
2185 cx: &mut Context<RulesLibrary>,
2186 ) {
2187 InlineAssistant::update_global(cx, |assistant, cx| {
2188 let Some(project) = self
2189 .workspace
2190 .upgrade()
2191 .map(|workspace| workspace.read(cx).project().downgrade())
2192 else {
2193 return;
2194 };
2195 let prompt_store = None;
2196 let thread_store = None;
2197 assistant.assist(
2198 &prompt_editor,
2199 self.workspace.clone(),
2200 project,
2201 prompt_store,
2202 thread_store,
2203 window,
2204 cx,
2205 )
2206 })
2207 }
2208
2209 fn focus_assistant_panel(
2210 &self,
2211 workspace: &mut Workspace,
2212 window: &mut Window,
2213 cx: &mut Context<Workspace>,
2214 ) -> bool {
2215 workspace
2216 .focus_panel::<AssistantPanel>(window, cx)
2217 .is_some()
2218 }
2219}
2220
2221pub struct ConcreteAssistantPanelDelegate;
2222
2223impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
2224 fn active_context_editor(
2225 &self,
2226 workspace: &mut Workspace,
2227 _window: &mut Window,
2228 cx: &mut Context<Workspace>,
2229 ) -> Option<Entity<ContextEditor>> {
2230 let panel = workspace.panel::<AssistantPanel>(cx)?;
2231 panel.read(cx).active_context_editor()
2232 }
2233
2234 fn open_saved_context(
2235 &self,
2236 workspace: &mut Workspace,
2237 path: Arc<Path>,
2238 window: &mut Window,
2239 cx: &mut Context<Workspace>,
2240 ) -> Task<Result<()>> {
2241 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2242 return Task::ready(Err(anyhow!("Agent panel not found")));
2243 };
2244
2245 panel.update(cx, |panel, cx| {
2246 panel.open_saved_prompt_editor(path, window, cx)
2247 })
2248 }
2249
2250 fn open_remote_context(
2251 &self,
2252 _workspace: &mut Workspace,
2253 _context_id: assistant_context_editor::ContextId,
2254 _window: &mut Window,
2255 _cx: &mut Context<Workspace>,
2256 ) -> Task<Result<Entity<ContextEditor>>> {
2257 Task::ready(Err(anyhow!("opening remote context not implemented")))
2258 }
2259
2260 fn quote_selection(
2261 &self,
2262 workspace: &mut Workspace,
2263 selection_ranges: Vec<Range<Anchor>>,
2264 buffer: Entity<MultiBuffer>,
2265 window: &mut Window,
2266 cx: &mut Context<Workspace>,
2267 ) {
2268 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2269 return;
2270 };
2271
2272 if !panel.focus_handle(cx).contains_focused(window, cx) {
2273 workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
2274 }
2275
2276 panel.update(cx, |_, cx| {
2277 // Wait to create a new context until the workspace is no longer
2278 // being updated.
2279 cx.defer_in(window, move |panel, window, cx| {
2280 if panel.has_active_thread() {
2281 panel.message_editor.update(cx, |message_editor, cx| {
2282 message_editor.context_store().update(cx, |store, cx| {
2283 let buffer = buffer.read(cx);
2284 let selection_ranges = selection_ranges
2285 .into_iter()
2286 .flat_map(|range| {
2287 let (start_buffer, start) =
2288 buffer.text_anchor_for_position(range.start, cx)?;
2289 let (end_buffer, end) =
2290 buffer.text_anchor_for_position(range.end, cx)?;
2291 if start_buffer != end_buffer {
2292 return None;
2293 }
2294 Some((start_buffer, start..end))
2295 })
2296 .collect::<Vec<_>>();
2297
2298 for (buffer, range) in selection_ranges {
2299 store.add_selection(buffer, range, cx);
2300 }
2301 })
2302 })
2303 } else if let Some(context_editor) = panel.active_context_editor() {
2304 let snapshot = buffer.read(cx).snapshot(cx);
2305 let selection_ranges = selection_ranges
2306 .into_iter()
2307 .map(|range| range.to_point(&snapshot))
2308 .collect::<Vec<_>>();
2309
2310 context_editor.update(cx, |context_editor, cx| {
2311 context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
2312 });
2313 }
2314 });
2315 });
2316 }
2317}