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