1use std::path::PathBuf;
2use std::sync::Arc;
3use std::time::Duration;
4
5use anyhow::{Result, anyhow};
6use assistant_context_editor::{
7 AssistantPanelDelegate, ConfigurationError, ContextEditor, SlashCommandCompletionProvider,
8 make_lsp_adapter_delegate, render_remaining_tokens,
9};
10use assistant_settings::{AssistantDockPosition, AssistantSettings};
11use assistant_slash_command::SlashCommandWorkingSet;
12use assistant_tool::ToolWorkingSet;
13
14use client::zed_urls;
15use editor::{Editor, EditorEvent, MultiBuffer};
16use fs::Fs;
17use gpui::{
18 Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, Corner, Entity,
19 EventEmitter, FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task,
20 UpdateGlobal, WeakEntity, action_with_deprecated_aliases, prelude::*, pulsating_between,
21};
22use language::LanguageRegistry;
23use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
24use language_model_selector::ToggleModelSelector;
25use project::Project;
26use prompt_library::{PromptLibrary, open_prompt_library};
27use prompt_store::PromptBuilder;
28use settings::{Settings, update_settings_file};
29use time::UtcOffset;
30use ui::{
31 Banner, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip, prelude::*,
32};
33use util::ResultExt as _;
34use workspace::Workspace;
35use workspace::dock::{DockPosition, Panel, PanelEvent};
36use zed_actions::assistant::ToggleFocus;
37
38use crate::active_thread::ActiveThread;
39use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
40use crate::history_store::{HistoryEntry, HistoryStore};
41use crate::message_editor::MessageEditor;
42use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
43use crate::thread_history::{PastContext, PastThread, ThreadHistory};
44use crate::thread_store::ThreadStore;
45use crate::{
46 AgentDiff, InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown,
47 OpenAgentDiff, OpenConfiguration, OpenHistory, ThreadEvent, ToggleContextPicker,
48};
49
50action_with_deprecated_aliases!(
51 assistant,
52 OpenPromptLibrary,
53 ["assistant::DeployPromptLibrary"]
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, _: &NewPromptEditor, 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, _: &OpenPromptLibrary, 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_prompt_library(&OpenPromptLibrary, 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 panel.update(cx, |panel, cx| {
96 panel.open_agent_diff(&OpenAgentDiff, window, cx);
97 });
98 }
99 });
100 },
101 )
102 .detach();
103}
104
105enum ActiveView {
106 Thread {
107 change_title_editor: Entity<Editor>,
108 _subscriptions: Vec<gpui::Subscription>,
109 },
110 PromptEditor,
111 History,
112 Configuration,
113}
114
115impl ActiveView {
116 pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
117 let summary = thread.read(cx).summary_or_default();
118
119 let editor = cx.new(|cx| {
120 let mut editor = Editor::single_line(window, cx);
121 editor.set_text(summary, window, cx);
122 editor
123 });
124
125 let subscriptions = vec![
126 window.subscribe(&editor, cx, {
127 {
128 let thread = thread.clone();
129 move |editor, event, window, cx| match event {
130 EditorEvent::BufferEdited => {
131 let new_summary = editor.read(cx).text(cx);
132
133 thread.update(cx, |thread, cx| {
134 thread.set_summary(new_summary, cx);
135 })
136 }
137 EditorEvent::Blurred => {
138 if editor.read(cx).text(cx).is_empty() {
139 let summary = thread.read(cx).summary_or_default();
140
141 editor.update(cx, |editor, cx| {
142 editor.set_text(summary, window, cx);
143 });
144 }
145 }
146 _ => {}
147 }
148 }
149 }),
150 window.subscribe(&thread, cx, {
151 let editor = editor.clone();
152 move |thread, event, window, cx| match event {
153 ThreadEvent::SummaryGenerated => {
154 let summary = thread.read(cx).summary_or_default();
155
156 editor.update(cx, |editor, cx| {
157 editor.set_text(summary, window, cx);
158 })
159 }
160 _ => {}
161 }
162 }),
163 ];
164
165 Self::Thread {
166 change_title_editor: editor,
167 _subscriptions: subscriptions,
168 }
169 }
170}
171
172pub struct AssistantPanel {
173 workspace: WeakEntity<Workspace>,
174 project: Entity<Project>,
175 fs: Arc<dyn Fs>,
176 language_registry: Arc<LanguageRegistry>,
177 thread_store: Entity<ThreadStore>,
178 thread: Entity<ActiveThread>,
179 message_editor: Entity<MessageEditor>,
180 context_store: Entity<assistant_context_editor::ContextStore>,
181 context_editor: Option<Entity<ContextEditor>>,
182 configuration: Option<Entity<AssistantConfiguration>>,
183 configuration_subscription: Option<Subscription>,
184 local_timezone: UtcOffset,
185 active_view: ActiveView,
186 history_store: Entity<HistoryStore>,
187 history: Entity<ThreadHistory>,
188 assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
189 width: Option<Pixels>,
190 height: Option<Pixels>,
191}
192
193impl AssistantPanel {
194 pub fn load(
195 workspace: WeakEntity<Workspace>,
196 prompt_builder: Arc<PromptBuilder>,
197 cx: AsyncWindowContext,
198 ) -> Task<Result<Entity<Self>>> {
199 cx.spawn(async move |cx| {
200 let tools = Arc::new(ToolWorkingSet::default());
201 let thread_store = workspace.update(cx, |workspace, cx| {
202 let project = workspace.project().clone();
203 ThreadStore::new(project, tools.clone(), prompt_builder.clone(), cx)
204 })??;
205
206 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
207 let context_store = workspace
208 .update(cx, |workspace, cx| {
209 let project = workspace.project().clone();
210 assistant_context_editor::ContextStore::new(
211 project,
212 prompt_builder.clone(),
213 slash_commands,
214 cx,
215 )
216 })?
217 .await?;
218
219 workspace.update_in(cx, |workspace, window, cx| {
220 cx.new(|cx| Self::new(workspace, thread_store, context_store, window, cx))
221 })
222 })
223 }
224
225 fn new(
226 workspace: &Workspace,
227 thread_store: Entity<ThreadStore>,
228 context_store: Entity<assistant_context_editor::ContextStore>,
229 window: &mut Window,
230 cx: &mut Context<Self>,
231 ) -> Self {
232 let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
233 let fs = workspace.app_state().fs.clone();
234 let project = workspace.project().clone();
235 let language_registry = project.read(cx).languages().clone();
236 let workspace = workspace.weak_handle();
237 let weak_self = cx.entity().downgrade();
238
239 let message_editor_context_store = cx.new(|_cx| {
240 crate::context_store::ContextStore::new(
241 workspace.clone(),
242 Some(thread_store.downgrade()),
243 )
244 });
245
246 let message_editor = cx.new(|cx| {
247 MessageEditor::new(
248 fs.clone(),
249 workspace.clone(),
250 message_editor_context_store.clone(),
251 thread_store.downgrade(),
252 thread.clone(),
253 window,
254 cx,
255 )
256 });
257
258 let history_store =
259 cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
260
261 let active_view = ActiveView::thread(thread.clone(), window, cx);
262 let thread = cx.new(|cx| {
263 ActiveThread::new(
264 thread.clone(),
265 thread_store.clone(),
266 language_registry.clone(),
267 message_editor_context_store.clone(),
268 workspace.clone(),
269 window,
270 cx,
271 )
272 });
273
274 Self {
275 active_view,
276 workspace,
277 project: project.clone(),
278 fs: fs.clone(),
279 language_registry,
280 thread_store: thread_store.clone(),
281 thread,
282 message_editor,
283 context_store,
284 context_editor: None,
285 configuration: None,
286 configuration_subscription: None,
287 local_timezone: UtcOffset::from_whole_seconds(
288 chrono::Local::now().offset().local_minus_utc(),
289 )
290 .unwrap(),
291 history_store: history_store.clone(),
292 history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
293 assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
294 width: None,
295 height: None,
296 }
297 }
298
299 pub fn toggle_focus(
300 workspace: &mut Workspace,
301 _: &ToggleFocus,
302 window: &mut Window,
303 cx: &mut Context<Workspace>,
304 ) {
305 if workspace
306 .panel::<Self>(cx)
307 .is_some_and(|panel| panel.read(cx).enabled(cx))
308 {
309 workspace.toggle_panel_focus::<Self>(window, cx);
310 }
311 }
312
313 pub(crate) fn local_timezone(&self) -> UtcOffset {
314 self.local_timezone
315 }
316
317 pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
318 &self.thread_store
319 }
320
321 fn cancel(
322 &mut self,
323 _: &editor::actions::Cancel,
324 _window: &mut Window,
325 cx: &mut Context<Self>,
326 ) {
327 self.thread
328 .update(cx, |thread, cx| thread.cancel_last_completion(cx));
329 }
330
331 fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
332 let thread = self
333 .thread_store
334 .update(cx, |this, cx| this.create_thread(cx));
335
336 self.active_view = ActiveView::thread(thread.clone(), window, cx);
337
338 let message_editor_context_store = cx.new(|_cx| {
339 crate::context_store::ContextStore::new(
340 self.workspace.clone(),
341 Some(self.thread_store.downgrade()),
342 )
343 });
344
345 if let Some(other_thread_id) = action.from_thread_id.clone() {
346 let other_thread_task = self
347 .thread_store
348 .update(cx, |this, cx| this.open_thread(&other_thread_id, cx));
349
350 cx.spawn({
351 let context_store = message_editor_context_store.clone();
352
353 async move |_panel, cx| {
354 let other_thread = other_thread_task.await?;
355
356 context_store.update(cx, |this, cx| {
357 this.add_thread(other_thread, false, cx);
358 })?;
359 anyhow::Ok(())
360 }
361 })
362 .detach_and_log_err(cx);
363 }
364
365 self.thread = cx.new(|cx| {
366 ActiveThread::new(
367 thread.clone(),
368 self.thread_store.clone(),
369 self.language_registry.clone(),
370 message_editor_context_store.clone(),
371 self.workspace.clone(),
372 window,
373 cx,
374 )
375 });
376 self.message_editor = cx.new(|cx| {
377 MessageEditor::new(
378 self.fs.clone(),
379 self.workspace.clone(),
380 message_editor_context_store,
381 self.thread_store.downgrade(),
382 thread,
383 window,
384 cx,
385 )
386 });
387 self.message_editor.focus_handle(cx).focus(window);
388 }
389
390 fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
391 self.active_view = ActiveView::PromptEditor;
392
393 let context = self
394 .context_store
395 .update(cx, |context_store, cx| context_store.create(cx));
396 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
397 .log_err()
398 .flatten();
399
400 self.context_editor = Some(cx.new(|cx| {
401 let mut editor = ContextEditor::for_context(
402 context,
403 self.fs.clone(),
404 self.workspace.clone(),
405 self.project.clone(),
406 lsp_adapter_delegate,
407 window,
408 cx,
409 );
410 editor.insert_default_prompt(window, cx);
411 editor
412 }));
413
414 if let Some(context_editor) = self.context_editor.as_ref() {
415 context_editor.focus_handle(cx).focus(window);
416 }
417 }
418
419 fn deploy_prompt_library(
420 &mut self,
421 _: &OpenPromptLibrary,
422 _window: &mut Window,
423 cx: &mut Context<Self>,
424 ) {
425 open_prompt_library(
426 self.language_registry.clone(),
427 Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
428 Arc::new(|| {
429 Box::new(SlashCommandCompletionProvider::new(
430 Arc::new(SlashCommandWorkingSet::default()),
431 None,
432 None,
433 ))
434 }),
435 cx,
436 )
437 .detach_and_log_err(cx);
438 }
439
440 fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
441 self.thread_store
442 .update(cx, |thread_store, cx| thread_store.reload(cx))
443 .detach_and_log_err(cx);
444 self.active_view = ActiveView::History;
445 self.history.focus_handle(cx).focus(window);
446 cx.notify();
447 }
448
449 pub(crate) fn open_saved_prompt_editor(
450 &mut self,
451 path: PathBuf,
452 window: &mut Window,
453 cx: &mut Context<Self>,
454 ) -> Task<Result<()>> {
455 let context = self
456 .context_store
457 .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
458 let fs = self.fs.clone();
459 let project = self.project.clone();
460 let workspace = self.workspace.clone();
461
462 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
463
464 cx.spawn_in(window, async move |this, cx| {
465 let context = context.await?;
466 this.update_in(cx, |this, window, cx| {
467 let editor = cx.new(|cx| {
468 ContextEditor::for_context(
469 context,
470 fs,
471 workspace,
472 project,
473 lsp_adapter_delegate,
474 window,
475 cx,
476 )
477 });
478 this.active_view = ActiveView::PromptEditor;
479 this.context_editor = Some(editor);
480
481 anyhow::Ok(())
482 })??;
483 Ok(())
484 })
485 }
486
487 pub(crate) fn open_thread(
488 &mut self,
489 thread_id: &ThreadId,
490 window: &mut Window,
491 cx: &mut Context<Self>,
492 ) -> Task<Result<()>> {
493 let open_thread_task = self
494 .thread_store
495 .update(cx, |this, cx| this.open_thread(thread_id, cx));
496
497 cx.spawn_in(window, async move |this, cx| {
498 let thread = open_thread_task.await?;
499 this.update_in(cx, |this, window, cx| {
500 this.active_view = ActiveView::thread(thread.clone(), window, cx);
501 let message_editor_context_store = cx.new(|_cx| {
502 crate::context_store::ContextStore::new(
503 this.workspace.clone(),
504 Some(this.thread_store.downgrade()),
505 )
506 });
507 this.thread = cx.new(|cx| {
508 ActiveThread::new(
509 thread.clone(),
510 this.thread_store.clone(),
511 this.language_registry.clone(),
512 message_editor_context_store.clone(),
513 this.workspace.clone(),
514 window,
515 cx,
516 )
517 });
518 this.message_editor = cx.new(|cx| {
519 MessageEditor::new(
520 this.fs.clone(),
521 this.workspace.clone(),
522 message_editor_context_store,
523 this.thread_store.downgrade(),
524 thread,
525 window,
526 cx,
527 )
528 });
529 this.message_editor.focus_handle(cx).focus(window);
530 })
531 })
532 }
533
534 pub fn open_agent_diff(
535 &mut self,
536 _: &OpenAgentDiff,
537 window: &mut Window,
538 cx: &mut Context<Self>,
539 ) {
540 let thread = self.thread.read(cx).thread().clone();
541 AgentDiff::deploy(thread, self.workspace.clone(), window, cx).log_err();
542 }
543
544 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
545 let context_server_manager = self.thread_store.read(cx).context_server_manager();
546 let tools = self.thread_store.read(cx).tools();
547 let fs = self.fs.clone();
548
549 self.active_view = ActiveView::Configuration;
550 self.configuration =
551 Some(cx.new(|cx| {
552 AssistantConfiguration::new(fs, context_server_manager, tools, window, cx)
553 }));
554
555 if let Some(configuration) = self.configuration.as_ref() {
556 self.configuration_subscription = Some(cx.subscribe_in(
557 configuration,
558 window,
559 Self::handle_assistant_configuration_event,
560 ));
561
562 configuration.focus_handle(cx).focus(window);
563 }
564 }
565
566 pub(crate) fn open_active_thread_as_markdown(
567 &mut self,
568 _: &OpenActiveThreadAsMarkdown,
569 window: &mut Window,
570 cx: &mut Context<Self>,
571 ) {
572 let Some(workspace) = self
573 .workspace
574 .upgrade()
575 .ok_or_else(|| anyhow!("workspace dropped"))
576 .log_err()
577 else {
578 return;
579 };
580
581 let markdown_language_task = workspace
582 .read(cx)
583 .app_state()
584 .languages
585 .language_for_name("Markdown");
586 let thread = self.active_thread(cx);
587 cx.spawn_in(window, async move |_this, cx| {
588 let markdown_language = markdown_language_task.await?;
589
590 workspace.update_in(cx, |workspace, window, cx| {
591 let thread = thread.read(cx);
592 let markdown = thread.to_markdown(cx)?;
593 let thread_summary = thread
594 .summary()
595 .map(|summary| summary.to_string())
596 .unwrap_or_else(|| "Thread".to_string());
597
598 let project = workspace.project().clone();
599 let buffer = project.update(cx, |project, cx| {
600 project.create_local_buffer(&markdown, Some(markdown_language), cx)
601 });
602 let buffer = cx.new(|cx| {
603 MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
604 });
605
606 workspace.add_item_to_active_pane(
607 Box::new(cx.new(|cx| {
608 let mut editor =
609 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
610 editor.set_breadcrumb_header(thread_summary);
611 editor
612 })),
613 None,
614 true,
615 window,
616 cx,
617 );
618
619 anyhow::Ok(())
620 })
621 })
622 .detach_and_log_err(cx);
623 }
624
625 fn handle_assistant_configuration_event(
626 &mut self,
627 _entity: &Entity<AssistantConfiguration>,
628 event: &AssistantConfigurationEvent,
629 window: &mut Window,
630 cx: &mut Context<Self>,
631 ) {
632 match event {
633 AssistantConfigurationEvent::NewThread(provider) => {
634 if LanguageModelRegistry::read_global(cx)
635 .default_model()
636 .map_or(true, |model| model.provider.id() != provider.id())
637 {
638 if let Some(model) = provider.default_model(cx) {
639 update_settings_file::<AssistantSettings>(
640 self.fs.clone(),
641 cx,
642 move |settings, _| settings.set_model(model),
643 );
644 }
645 }
646
647 self.new_thread(&NewThread::default(), window, cx);
648 }
649 }
650 }
651
652 pub(crate) fn active_thread(&self, cx: &App) -> Entity<Thread> {
653 self.thread.read(cx).thread().clone()
654 }
655
656 pub(crate) fn delete_thread(
657 &mut self,
658 thread_id: &ThreadId,
659 cx: &mut Context<Self>,
660 ) -> Task<Result<()>> {
661 self.thread_store
662 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
663 }
664
665 pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
666 self.context_editor.clone()
667 }
668
669 pub(crate) fn delete_context(
670 &mut self,
671 path: PathBuf,
672 cx: &mut Context<Self>,
673 ) -> Task<Result<()>> {
674 self.context_store
675 .update(cx, |this, cx| this.delete_local_context(path, cx))
676 }
677}
678
679impl Focusable for AssistantPanel {
680 fn focus_handle(&self, cx: &App) -> FocusHandle {
681 match self.active_view {
682 ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
683 ActiveView::History => self.history.focus_handle(cx),
684 ActiveView::PromptEditor => {
685 if let Some(context_editor) = self.context_editor.as_ref() {
686 context_editor.focus_handle(cx)
687 } else {
688 cx.focus_handle()
689 }
690 }
691 ActiveView::Configuration => {
692 if let Some(configuration) = self.configuration.as_ref() {
693 configuration.focus_handle(cx)
694 } else {
695 cx.focus_handle()
696 }
697 }
698 }
699 }
700}
701
702impl EventEmitter<PanelEvent> for AssistantPanel {}
703
704impl Panel for AssistantPanel {
705 fn persistent_name() -> &'static str {
706 "AgentPanel"
707 }
708
709 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
710 match AssistantSettings::get_global(cx).dock {
711 AssistantDockPosition::Left => DockPosition::Left,
712 AssistantDockPosition::Bottom => DockPosition::Bottom,
713 AssistantDockPosition::Right => DockPosition::Right,
714 }
715 }
716
717 fn position_is_valid(&self, _: DockPosition) -> bool {
718 true
719 }
720
721 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
722 settings::update_settings_file::<AssistantSettings>(
723 self.fs.clone(),
724 cx,
725 move |settings, _| {
726 let dock = match position {
727 DockPosition::Left => AssistantDockPosition::Left,
728 DockPosition::Bottom => AssistantDockPosition::Bottom,
729 DockPosition::Right => AssistantDockPosition::Right,
730 };
731 settings.set_dock(dock);
732 },
733 );
734 }
735
736 fn size(&self, window: &Window, cx: &App) -> Pixels {
737 let settings = AssistantSettings::get_global(cx);
738 match self.position(window, cx) {
739 DockPosition::Left | DockPosition::Right => {
740 self.width.unwrap_or(settings.default_width)
741 }
742 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
743 }
744 }
745
746 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
747 match self.position(window, cx) {
748 DockPosition::Left | DockPosition::Right => self.width = size,
749 DockPosition::Bottom => self.height = size,
750 }
751 cx.notify();
752 }
753
754 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
755
756 fn remote_id() -> Option<proto::PanelId> {
757 Some(proto::PanelId::AssistantPanel)
758 }
759
760 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
761 (self.enabled(cx) && AssistantSettings::get_global(cx).button)
762 .then_some(IconName::ZedAssistant)
763 }
764
765 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
766 Some("Agent Panel")
767 }
768
769 fn toggle_action(&self) -> Box<dyn Action> {
770 Box::new(ToggleFocus)
771 }
772
773 fn activation_priority(&self) -> u32 {
774 3
775 }
776
777 fn enabled(&self, cx: &App) -> bool {
778 AssistantSettings::get_global(cx).enabled
779 }
780}
781
782impl AssistantPanel {
783 fn render_title_view(&self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
784 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
785
786 let content = match &self.active_view {
787 ActiveView::Thread {
788 change_title_editor,
789 ..
790 } => {
791 let active_thread = self.thread.read(cx);
792 let is_empty = active_thread.is_empty();
793
794 let summary = active_thread.summary(cx);
795
796 if is_empty {
797 Label::new(Thread::DEFAULT_SUMMARY.clone())
798 .truncate()
799 .into_any_element()
800 } else if summary.is_none() {
801 Label::new(LOADING_SUMMARY_PLACEHOLDER)
802 .truncate()
803 .into_any_element()
804 } else {
805 change_title_editor.clone().into_any_element()
806 }
807 }
808 ActiveView::PromptEditor => {
809 let title = self
810 .context_editor
811 .as_ref()
812 .map(|context_editor| {
813 SharedString::from(context_editor.read(cx).title(cx).to_string())
814 })
815 .unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
816
817 Label::new(title).truncate().into_any_element()
818 }
819 ActiveView::History => Label::new("History").truncate().into_any_element(),
820 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
821 };
822
823 h_flex()
824 .key_context("TitleEditor")
825 .id("TitleEditor")
826 .pl_2()
827 .flex_grow()
828 .w_full()
829 .max_w_full()
830 .overflow_x_scroll()
831 .child(content)
832 .into_any()
833 }
834
835 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
836 let active_thread = self.thread.read(cx);
837 let thread = active_thread.thread().read(cx);
838 let token_usage = thread.total_token_usage(cx);
839 let thread_id = thread.id().clone();
840
841 let is_generating = thread.is_generating();
842 let is_empty = active_thread.is_empty();
843 let focus_handle = self.focus_handle(cx);
844
845 let show_token_count = match &self.active_view {
846 ActiveView::Thread { .. } => !is_empty,
847 ActiveView::PromptEditor => self.context_editor.is_some(),
848 _ => false,
849 };
850
851 h_flex()
852 .id("assistant-toolbar")
853 .h(Tab::container_height(cx))
854 .max_w_full()
855 .flex_none()
856 .justify_between()
857 .gap_2()
858 .bg(cx.theme().colors().tab_bar_background)
859 .border_b_1()
860 .border_color(cx.theme().colors().border)
861 .child(self.render_title_view(window, cx))
862 .child(
863 h_flex()
864 .h_full()
865 .gap_2()
866 .when(show_token_count, |parent| match self.active_view {
867 ActiveView::Thread { .. } => {
868 if token_usage.total == 0 {
869 return parent;
870 }
871
872 let token_color = match token_usage.ratio {
873 TokenUsageRatio::Normal => Color::Muted,
874 TokenUsageRatio::Warning => Color::Warning,
875 TokenUsageRatio::Exceeded => Color::Error,
876 };
877
878 parent.child(
879 h_flex()
880 .flex_shrink_0()
881 .gap_0p5()
882 .child(
883 Label::new(assistant_context_editor::humanize_token_count(
884 token_usage.total,
885 ))
886 .size(LabelSize::Small)
887 .color(token_color)
888 .map(|label| {
889 if is_generating {
890 label
891 .with_animation(
892 "used-tokens-label",
893 Animation::new(Duration::from_secs(2))
894 .repeat()
895 .with_easing(pulsating_between(
896 0.6, 1.,
897 )),
898 |label, delta| label.alpha(delta),
899 )
900 .into_any()
901 } else {
902 label.into_any_element()
903 }
904 }),
905 )
906 .child(
907 Label::new("/").size(LabelSize::Small).color(Color::Muted),
908 )
909 .child(
910 Label::new(assistant_context_editor::humanize_token_count(
911 token_usage.max,
912 ))
913 .size(LabelSize::Small)
914 .color(Color::Muted),
915 ),
916 )
917 }
918 ActiveView::PromptEditor => {
919 let Some(editor) = self.context_editor.as_ref() else {
920 return parent;
921 };
922 let Some(element) = render_remaining_tokens(editor, cx) else {
923 return parent;
924 };
925 parent.child(element)
926 }
927 _ => parent,
928 })
929 .child(
930 h_flex()
931 .h_full()
932 .gap(DynamicSpacing::Base02.rems(cx))
933 .px(DynamicSpacing::Base08.rems(cx))
934 .border_l_1()
935 .border_color(cx.theme().colors().border)
936 .child(
937 IconButton::new("new", IconName::Plus)
938 .icon_size(IconSize::Small)
939 .style(ButtonStyle::Subtle)
940 .tooltip(move |window, cx| {
941 Tooltip::for_action_in(
942 "New Thread",
943 &NewThread::default(),
944 &focus_handle,
945 window,
946 cx,
947 )
948 })
949 .on_click(move |_event, window, cx| {
950 window.dispatch_action(
951 NewThread::default().boxed_clone(),
952 cx,
953 );
954 }),
955 )
956 .child(
957 PopoverMenu::new("assistant-menu")
958 .trigger_with_tooltip(
959 IconButton::new("new", IconName::Ellipsis)
960 .icon_size(IconSize::Small)
961 .style(ButtonStyle::Subtle),
962 Tooltip::text("Toggle Agent Menu"),
963 )
964 .anchor(Corner::TopRight)
965 .with_handle(self.assistant_dropdown_menu_handle.clone())
966 .menu(move |window, cx| {
967 Some(ContextMenu::build(
968 window,
969 cx,
970 |menu, _window, _cx| {
971 menu.action(
972 "New Thread",
973 Box::new(NewThread {
974 from_thread_id: None,
975 }),
976 )
977 .action(
978 "New Prompt Editor",
979 NewPromptEditor.boxed_clone(),
980 )
981 .when(!is_empty, |menu| {
982 menu.action(
983 "Continue in New Thread",
984 Box::new(NewThread {
985 from_thread_id: Some(thread_id.clone()),
986 }),
987 )
988 })
989 .separator()
990 .action("History", OpenHistory.boxed_clone())
991 .action("Settings", OpenConfiguration.boxed_clone())
992 },
993 ))
994 }),
995 ),
996 ),
997 )
998 }
999
1000 fn render_active_thread_or_empty_state(
1001 &self,
1002 window: &mut Window,
1003 cx: &mut Context<Self>,
1004 ) -> AnyElement {
1005 if self.thread.read(cx).is_empty() {
1006 return self
1007 .render_thread_empty_state(window, cx)
1008 .into_any_element();
1009 }
1010
1011 self.thread.clone().into_any_element()
1012 }
1013
1014 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
1015 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
1016 return Some(ConfigurationError::NoProvider);
1017 };
1018
1019 if !model.provider.is_authenticated(cx) {
1020 return Some(ConfigurationError::ProviderNotAuthenticated);
1021 }
1022
1023 if model.provider.must_accept_terms(cx) {
1024 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
1025 model.provider,
1026 ));
1027 }
1028
1029 None
1030 }
1031
1032 fn render_thread_empty_state(
1033 &self,
1034 window: &mut Window,
1035 cx: &mut Context<Self>,
1036 ) -> impl IntoElement {
1037 let recent_history = self
1038 .history_store
1039 .update(cx, |this, cx| this.recent_entries(6, cx));
1040
1041 let configuration_error = self.configuration_error(cx);
1042 let no_error = configuration_error.is_none();
1043 let focus_handle = self.focus_handle(cx);
1044
1045 v_flex()
1046 .size_full()
1047 .when(recent_history.is_empty(), |this| {
1048 let configuration_error_ref = &configuration_error;
1049 this.child(
1050 v_flex()
1051 .size_full()
1052 .max_w_80()
1053 .mx_auto()
1054 .justify_center()
1055 .items_center()
1056 .gap_1()
1057 .child(
1058 h_flex().child(
1059 Headline::new("Welcome to the Agent Panel")
1060 ),
1061 )
1062 .when(no_error, |parent| {
1063 parent
1064 .child(
1065 h_flex().child(
1066 Label::new("Ask and build anything.")
1067 .color(Color::Muted)
1068 .mb_2p5(),
1069 ),
1070 )
1071 .child(
1072 Button::new("new-thread", "Start New Thread")
1073 .icon(IconName::Plus)
1074 .icon_position(IconPosition::Start)
1075 .icon_size(IconSize::Small)
1076 .icon_color(Color::Muted)
1077 .full_width()
1078 .key_binding(KeyBinding::for_action_in(
1079 &NewThread::default(),
1080 &focus_handle,
1081 window,
1082 cx,
1083 ))
1084 .on_click(|_event, window, cx| {
1085 window.dispatch_action(NewThread::default().boxed_clone(), cx)
1086 }),
1087 )
1088 .child(
1089 Button::new("context", "Add Context")
1090 .icon(IconName::FileCode)
1091 .icon_position(IconPosition::Start)
1092 .icon_size(IconSize::Small)
1093 .icon_color(Color::Muted)
1094 .full_width()
1095 .key_binding(KeyBinding::for_action_in(
1096 &ToggleContextPicker,
1097 &focus_handle,
1098 window,
1099 cx,
1100 ))
1101 .on_click(|_event, window, cx| {
1102 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
1103 }),
1104 )
1105 .child(
1106 Button::new("mode", "Switch Model")
1107 .icon(IconName::DatabaseZap)
1108 .icon_position(IconPosition::Start)
1109 .icon_size(IconSize::Small)
1110 .icon_color(Color::Muted)
1111 .full_width()
1112 .key_binding(KeyBinding::for_action_in(
1113 &ToggleModelSelector,
1114 &focus_handle,
1115 window,
1116 cx,
1117 ))
1118 .on_click(|_event, window, cx| {
1119 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
1120 }),
1121 )
1122 .child(
1123 Button::new("settings", "View Settings")
1124 .icon(IconName::Settings)
1125 .icon_position(IconPosition::Start)
1126 .icon_size(IconSize::Small)
1127 .icon_color(Color::Muted)
1128 .full_width()
1129 .key_binding(KeyBinding::for_action_in(
1130 &OpenConfiguration,
1131 &focus_handle,
1132 window,
1133 cx,
1134 ))
1135 .on_click(|_event, window, cx| {
1136 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1137 }),
1138 )
1139 })
1140 .map(|parent| {
1141 match configuration_error_ref {
1142 Some(ConfigurationError::ProviderNotAuthenticated)
1143 | Some(ConfigurationError::NoProvider) => {
1144 parent
1145 .child(
1146 h_flex().child(
1147 Label::new("To start using the agent, configure at least one LLM provider.")
1148 .color(Color::Muted)
1149 .mb_2p5()
1150 )
1151 )
1152 .child(
1153 Button::new("settings", "Configure a Provider")
1154 .icon(IconName::Settings)
1155 .icon_position(IconPosition::Start)
1156 .icon_size(IconSize::Small)
1157 .icon_color(Color::Muted)
1158 .full_width()
1159 .key_binding(KeyBinding::for_action_in(
1160 &OpenConfiguration,
1161 &focus_handle,
1162 window,
1163 cx,
1164 ))
1165 .on_click(|_event, window, cx| {
1166 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1167 }),
1168 )
1169 }
1170 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1171 parent.children(
1172 provider.render_accept_terms(
1173 LanguageModelProviderTosView::ThreadFreshStart,
1174 cx,
1175 ),
1176 )
1177 }
1178 None => parent,
1179 }
1180 })
1181 )
1182 })
1183 .when(!recent_history.is_empty(), |parent| {
1184 let focus_handle = focus_handle.clone();
1185 let configuration_error_ref = &configuration_error;
1186
1187 parent
1188 .p_1p5()
1189 .justify_end()
1190 .gap_1()
1191 .child(
1192 h_flex()
1193 .pl_1p5()
1194 .pb_1()
1195 .w_full()
1196 .justify_between()
1197 .border_b_1()
1198 .border_color(cx.theme().colors().border_variant)
1199 .child(
1200 Label::new("Past Interactions")
1201 .size(LabelSize::Small)
1202 .color(Color::Muted),
1203 )
1204 .child(
1205 Button::new("view-history", "View All")
1206 .style(ButtonStyle::Subtle)
1207 .label_size(LabelSize::Small)
1208 .key_binding(
1209 KeyBinding::for_action_in(
1210 &OpenHistory,
1211 &self.focus_handle(cx),
1212 window,
1213 cx,
1214 ).map(|kb| kb.size(rems_from_px(12.))),
1215 )
1216 .on_click(move |_event, window, cx| {
1217 window.dispatch_action(OpenHistory.boxed_clone(), cx);
1218 }),
1219 ),
1220 )
1221 .child(
1222 v_flex()
1223 .gap_1()
1224 .children(
1225 recent_history.into_iter().map(|entry| {
1226 // TODO: Add keyboard navigation.
1227 match entry {
1228 HistoryEntry::Thread(thread) => {
1229 PastThread::new(thread, cx.entity().downgrade(), false, vec![])
1230 .into_any_element()
1231 }
1232 HistoryEntry::Context(context) => {
1233 PastContext::new(context, cx.entity().downgrade(), false, vec![])
1234 .into_any_element()
1235 }
1236 }
1237 }),
1238 )
1239 )
1240 .map(|parent| {
1241 match configuration_error_ref {
1242 Some(ConfigurationError::ProviderNotAuthenticated)
1243 | Some(ConfigurationError::NoProvider) => {
1244 parent
1245 .child(
1246 Banner::new()
1247 .severity(ui::Severity::Warning)
1248 .children(
1249 Label::new(
1250 "Configure at least one LLM provider to start using the panel.",
1251 )
1252 .size(LabelSize::Small),
1253 )
1254 .action_slot(
1255 Button::new("settings", "Configure Provider")
1256 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
1257 .label_size(LabelSize::Small)
1258 .key_binding(
1259 KeyBinding::for_action_in(
1260 &OpenConfiguration,
1261 &focus_handle,
1262 window,
1263 cx,
1264 )
1265 .map(|kb| kb.size(rems_from_px(12.))),
1266 )
1267 .on_click(|_event, window, cx| {
1268 window.dispatch_action(
1269 OpenConfiguration.boxed_clone(),
1270 cx,
1271 )
1272 }),
1273 ),
1274 )
1275 }
1276 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1277 parent
1278 .child(
1279 Banner::new()
1280 .severity(ui::Severity::Warning)
1281 .children(
1282 h_flex()
1283 .w_full()
1284 .children(
1285 provider.render_accept_terms(
1286 LanguageModelProviderTosView::ThreadtEmptyState,
1287 cx,
1288 ),
1289 ),
1290 ),
1291 )
1292 }
1293 None => parent,
1294 }
1295 })
1296 })
1297 }
1298
1299 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1300 let last_error = self.thread.read(cx).last_error()?;
1301
1302 Some(
1303 div()
1304 .absolute()
1305 .right_3()
1306 .bottom_12()
1307 .max_w_96()
1308 .py_2()
1309 .px_3()
1310 .elevation_2(cx)
1311 .occlude()
1312 .child(match last_error {
1313 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
1314 ThreadError::MaxMonthlySpendReached => {
1315 self.render_max_monthly_spend_reached_error(cx)
1316 }
1317 ThreadError::Message { header, message } => {
1318 self.render_error_message(header, message, cx)
1319 }
1320 })
1321 .into_any(),
1322 )
1323 }
1324
1325 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
1326 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.";
1327
1328 v_flex()
1329 .gap_0p5()
1330 .child(
1331 h_flex()
1332 .gap_1p5()
1333 .items_center()
1334 .child(Icon::new(IconName::XCircle).color(Color::Error))
1335 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
1336 )
1337 .child(
1338 div()
1339 .id("error-message")
1340 .max_h_24()
1341 .overflow_y_scroll()
1342 .child(Label::new(ERROR_MESSAGE)),
1343 )
1344 .child(
1345 h_flex()
1346 .justify_end()
1347 .mt_1()
1348 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
1349 |this, _, _, cx| {
1350 this.thread.update(cx, |this, _cx| {
1351 this.clear_last_error();
1352 });
1353
1354 cx.open_url(&zed_urls::account_url(cx));
1355 cx.notify();
1356 },
1357 )))
1358 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1359 |this, _, _, cx| {
1360 this.thread.update(cx, |this, _cx| {
1361 this.clear_last_error();
1362 });
1363
1364 cx.notify();
1365 },
1366 ))),
1367 )
1368 .into_any()
1369 }
1370
1371 fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
1372 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
1373
1374 v_flex()
1375 .gap_0p5()
1376 .child(
1377 h_flex()
1378 .gap_1p5()
1379 .items_center()
1380 .child(Icon::new(IconName::XCircle).color(Color::Error))
1381 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
1382 )
1383 .child(
1384 div()
1385 .id("error-message")
1386 .max_h_24()
1387 .overflow_y_scroll()
1388 .child(Label::new(ERROR_MESSAGE)),
1389 )
1390 .child(
1391 h_flex()
1392 .justify_end()
1393 .mt_1()
1394 .child(
1395 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
1396 cx.listener(|this, _, _, cx| {
1397 this.thread.update(cx, |this, _cx| {
1398 this.clear_last_error();
1399 });
1400
1401 cx.open_url(&zed_urls::account_url(cx));
1402 cx.notify();
1403 }),
1404 ),
1405 )
1406 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1407 |this, _, _, cx| {
1408 this.thread.update(cx, |this, _cx| {
1409 this.clear_last_error();
1410 });
1411
1412 cx.notify();
1413 },
1414 ))),
1415 )
1416 .into_any()
1417 }
1418
1419 fn render_error_message(
1420 &self,
1421 header: SharedString,
1422 message: SharedString,
1423 cx: &mut Context<Self>,
1424 ) -> AnyElement {
1425 v_flex()
1426 .gap_0p5()
1427 .child(
1428 h_flex()
1429 .gap_1p5()
1430 .items_center()
1431 .child(Icon::new(IconName::XCircle).color(Color::Error))
1432 .child(Label::new(header).weight(FontWeight::MEDIUM)),
1433 )
1434 .child(
1435 div()
1436 .id("error-message")
1437 .max_h_32()
1438 .overflow_y_scroll()
1439 .child(Label::new(message)),
1440 )
1441 .child(
1442 h_flex()
1443 .justify_end()
1444 .mt_1()
1445 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1446 |this, _, _, cx| {
1447 this.thread.update(cx, |this, _cx| {
1448 this.clear_last_error();
1449 });
1450
1451 cx.notify();
1452 },
1453 ))),
1454 )
1455 .into_any()
1456 }
1457
1458 fn key_context(&self) -> KeyContext {
1459 let mut key_context = KeyContext::new_with_defaults();
1460 key_context.add("AgentPanel");
1461 if matches!(self.active_view, ActiveView::PromptEditor) {
1462 key_context.add("prompt_editor");
1463 }
1464 key_context
1465 }
1466}
1467
1468impl Render for AssistantPanel {
1469 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1470 v_flex()
1471 .key_context(self.key_context())
1472 .justify_between()
1473 .size_full()
1474 .on_action(cx.listener(Self::cancel))
1475 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
1476 this.new_thread(action, window, cx);
1477 }))
1478 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
1479 this.open_history(window, cx);
1480 }))
1481 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
1482 this.open_configuration(window, cx);
1483 }))
1484 .on_action(cx.listener(Self::open_active_thread_as_markdown))
1485 .on_action(cx.listener(Self::deploy_prompt_library))
1486 .on_action(cx.listener(Self::open_agent_diff))
1487 .child(self.render_toolbar(window, cx))
1488 .map(|parent| match self.active_view {
1489 ActiveView::Thread { .. } => parent
1490 .child(self.render_active_thread_or_empty_state(window, cx))
1491 .child(h_flex().child(self.message_editor.clone()))
1492 .children(self.render_last_error(cx)),
1493 ActiveView::History => parent.child(self.history.clone()),
1494 ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
1495 ActiveView::Configuration => parent.children(self.configuration.clone()),
1496 })
1497 }
1498}
1499
1500struct PromptLibraryInlineAssist {
1501 workspace: WeakEntity<Workspace>,
1502}
1503
1504impl PromptLibraryInlineAssist {
1505 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
1506 Self { workspace }
1507 }
1508}
1509
1510impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1511 fn assist(
1512 &self,
1513 prompt_editor: &Entity<Editor>,
1514 _initial_prompt: Option<String>,
1515 window: &mut Window,
1516 cx: &mut Context<PromptLibrary>,
1517 ) {
1518 InlineAssistant::update_global(cx, |assistant, cx| {
1519 assistant.assist(&prompt_editor, self.workspace.clone(), None, window, cx)
1520 })
1521 }
1522
1523 fn focus_assistant_panel(
1524 &self,
1525 workspace: &mut Workspace,
1526 window: &mut Window,
1527 cx: &mut Context<Workspace>,
1528 ) -> bool {
1529 workspace
1530 .focus_panel::<AssistantPanel>(window, cx)
1531 .is_some()
1532 }
1533}
1534
1535pub struct ConcreteAssistantPanelDelegate;
1536
1537impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1538 fn active_context_editor(
1539 &self,
1540 workspace: &mut Workspace,
1541 _window: &mut Window,
1542 cx: &mut Context<Workspace>,
1543 ) -> Option<Entity<ContextEditor>> {
1544 let panel = workspace.panel::<AssistantPanel>(cx)?;
1545 panel.update(cx, |panel, _cx| panel.context_editor.clone())
1546 }
1547
1548 fn open_saved_context(
1549 &self,
1550 workspace: &mut Workspace,
1551 path: std::path::PathBuf,
1552 window: &mut Window,
1553 cx: &mut Context<Workspace>,
1554 ) -> Task<Result<()>> {
1555 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1556 return Task::ready(Err(anyhow!("Agent panel not found")));
1557 };
1558
1559 panel.update(cx, |panel, cx| {
1560 panel.open_saved_prompt_editor(path, window, cx)
1561 })
1562 }
1563
1564 fn open_remote_context(
1565 &self,
1566 _workspace: &mut Workspace,
1567 _context_id: assistant_context_editor::ContextId,
1568 _window: &mut Window,
1569 _cx: &mut Context<Workspace>,
1570 ) -> Task<Result<Entity<ContextEditor>>> {
1571 Task::ready(Err(anyhow!("opening remote context not implemented")))
1572 }
1573
1574 fn quote_selection(
1575 &self,
1576 _workspace: &mut Workspace,
1577 _creases: Vec<(String, String)>,
1578 _window: &mut Window,
1579 _cx: &mut Context<Workspace>,
1580 ) {
1581 }
1582}