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