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::agent::OpenConfiguration;
37use zed_actions::assistant::ToggleFocus;
38
39use crate::active_thread::ActiveThread;
40use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
41use crate::history_store::{HistoryEntry, HistoryStore};
42use crate::message_editor::MessageEditor;
43use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
44use crate::thread_history::{PastContext, PastThread, ThreadHistory};
45use crate::thread_store::ThreadStore;
46use crate::{
47 AgentDiff, InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown,
48 OpenAgentDiff, OpenHistory, ThreadEvent, ToggleContextPicker,
49};
50
51action_with_deprecated_aliases!(
52 assistant,
53 OpenPromptLibrary,
54 ["assistant::DeployPromptLibrary"]
55);
56
57pub fn init(cx: &mut App) {
58 cx.observe_new(
59 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
60 workspace
61 .register_action(|workspace, action: &NewThread, window, cx| {
62 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
63 panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
64 workspace.focus_panel::<AssistantPanel>(window, cx);
65 }
66 })
67 .register_action(|workspace, _: &OpenHistory, window, cx| {
68 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
69 workspace.focus_panel::<AssistantPanel>(window, cx);
70 panel.update(cx, |panel, cx| panel.open_history(window, cx));
71 }
72 })
73 .register_action(|workspace, _: &OpenConfiguration, window, cx| {
74 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
75 workspace.focus_panel::<AssistantPanel>(window, cx);
76 panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
77 }
78 })
79 .register_action(|workspace, _: &NewPromptEditor, window, cx| {
80 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
81 workspace.focus_panel::<AssistantPanel>(window, cx);
82 panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
83 }
84 })
85 .register_action(|workspace, _: &OpenPromptLibrary, window, cx| {
86 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
87 workspace.focus_panel::<AssistantPanel>(window, cx);
88 panel.update(cx, |panel, cx| {
89 panel.deploy_prompt_library(&OpenPromptLibrary, window, cx)
90 });
91 }
92 })
93 .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
94 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
95 workspace.focus_panel::<AssistantPanel>(window, cx);
96 let thread = panel.read(cx).thread.read(cx).thread().clone();
97 AgentDiff::deploy_in_workspace(thread, workspace, window, cx);
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 self.workspace
542 .update(cx, |workspace, cx| {
543 AgentDiff::deploy_in_workspace(thread, workspace, window, cx)
544 })
545 .log_err();
546 }
547
548 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
549 let context_server_manager = self.thread_store.read(cx).context_server_manager();
550 let tools = self.thread_store.read(cx).tools();
551 let fs = self.fs.clone();
552
553 self.active_view = ActiveView::Configuration;
554 self.configuration =
555 Some(cx.new(|cx| {
556 AssistantConfiguration::new(fs, context_server_manager, tools, window, cx)
557 }));
558
559 if let Some(configuration) = self.configuration.as_ref() {
560 self.configuration_subscription = Some(cx.subscribe_in(
561 configuration,
562 window,
563 Self::handle_assistant_configuration_event,
564 ));
565
566 configuration.focus_handle(cx).focus(window);
567 }
568 }
569
570 pub(crate) fn open_active_thread_as_markdown(
571 &mut self,
572 _: &OpenActiveThreadAsMarkdown,
573 window: &mut Window,
574 cx: &mut Context<Self>,
575 ) {
576 let Some(workspace) = self
577 .workspace
578 .upgrade()
579 .ok_or_else(|| anyhow!("workspace dropped"))
580 .log_err()
581 else {
582 return;
583 };
584
585 let markdown_language_task = workspace
586 .read(cx)
587 .app_state()
588 .languages
589 .language_for_name("Markdown");
590 let thread = self.active_thread(cx);
591 cx.spawn_in(window, async move |_this, cx| {
592 let markdown_language = markdown_language_task.await?;
593
594 workspace.update_in(cx, |workspace, window, cx| {
595 let thread = thread.read(cx);
596 let markdown = thread.to_markdown(cx)?;
597 let thread_summary = thread
598 .summary()
599 .map(|summary| summary.to_string())
600 .unwrap_or_else(|| "Thread".to_string());
601
602 let project = workspace.project().clone();
603 let buffer = project.update(cx, |project, cx| {
604 project.create_local_buffer(&markdown, Some(markdown_language), cx)
605 });
606 let buffer = cx.new(|cx| {
607 MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
608 });
609
610 workspace.add_item_to_active_pane(
611 Box::new(cx.new(|cx| {
612 let mut editor =
613 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
614 editor.set_breadcrumb_header(thread_summary);
615 editor
616 })),
617 None,
618 true,
619 window,
620 cx,
621 );
622
623 anyhow::Ok(())
624 })
625 })
626 .detach_and_log_err(cx);
627 }
628
629 fn handle_assistant_configuration_event(
630 &mut self,
631 _entity: &Entity<AssistantConfiguration>,
632 event: &AssistantConfigurationEvent,
633 window: &mut Window,
634 cx: &mut Context<Self>,
635 ) {
636 match event {
637 AssistantConfigurationEvent::NewThread(provider) => {
638 if LanguageModelRegistry::read_global(cx)
639 .default_model()
640 .map_or(true, |model| model.provider.id() != provider.id())
641 {
642 if let Some(model) = provider.default_model(cx) {
643 update_settings_file::<AssistantSettings>(
644 self.fs.clone(),
645 cx,
646 move |settings, _| settings.set_model(model),
647 );
648 }
649 }
650
651 self.new_thread(&NewThread::default(), window, cx);
652 }
653 }
654 }
655
656 pub(crate) fn active_thread(&self, cx: &App) -> Entity<Thread> {
657 self.thread.read(cx).thread().clone()
658 }
659
660 pub(crate) fn delete_thread(
661 &mut self,
662 thread_id: &ThreadId,
663 cx: &mut Context<Self>,
664 ) -> Task<Result<()>> {
665 self.thread_store
666 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
667 }
668
669 pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
670 self.context_editor.clone()
671 }
672
673 pub(crate) fn delete_context(
674 &mut self,
675 path: PathBuf,
676 cx: &mut Context<Self>,
677 ) -> Task<Result<()>> {
678 self.context_store
679 .update(cx, |this, cx| this.delete_local_context(path, cx))
680 }
681}
682
683impl Focusable for AssistantPanel {
684 fn focus_handle(&self, cx: &App) -> FocusHandle {
685 match self.active_view {
686 ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
687 ActiveView::History => self.history.focus_handle(cx),
688 ActiveView::PromptEditor => {
689 if let Some(context_editor) = self.context_editor.as_ref() {
690 context_editor.focus_handle(cx)
691 } else {
692 cx.focus_handle()
693 }
694 }
695 ActiveView::Configuration => {
696 if let Some(configuration) = self.configuration.as_ref() {
697 configuration.focus_handle(cx)
698 } else {
699 cx.focus_handle()
700 }
701 }
702 }
703 }
704}
705
706impl EventEmitter<PanelEvent> for AssistantPanel {}
707
708impl Panel for AssistantPanel {
709 fn persistent_name() -> &'static str {
710 "AgentPanel"
711 }
712
713 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
714 match AssistantSettings::get_global(cx).dock {
715 AssistantDockPosition::Left => DockPosition::Left,
716 AssistantDockPosition::Bottom => DockPosition::Bottom,
717 AssistantDockPosition::Right => DockPosition::Right,
718 }
719 }
720
721 fn position_is_valid(&self, _: DockPosition) -> bool {
722 true
723 }
724
725 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
726 settings::update_settings_file::<AssistantSettings>(
727 self.fs.clone(),
728 cx,
729 move |settings, _| {
730 let dock = match position {
731 DockPosition::Left => AssistantDockPosition::Left,
732 DockPosition::Bottom => AssistantDockPosition::Bottom,
733 DockPosition::Right => AssistantDockPosition::Right,
734 };
735 settings.set_dock(dock);
736 },
737 );
738 }
739
740 fn size(&self, window: &Window, cx: &App) -> Pixels {
741 let settings = AssistantSettings::get_global(cx);
742 match self.position(window, cx) {
743 DockPosition::Left | DockPosition::Right => {
744 self.width.unwrap_or(settings.default_width)
745 }
746 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
747 }
748 }
749
750 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
751 match self.position(window, cx) {
752 DockPosition::Left | DockPosition::Right => self.width = size,
753 DockPosition::Bottom => self.height = size,
754 }
755 cx.notify();
756 }
757
758 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
759
760 fn remote_id() -> Option<proto::PanelId> {
761 Some(proto::PanelId::AssistantPanel)
762 }
763
764 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
765 (self.enabled(cx) && AssistantSettings::get_global(cx).button)
766 .then_some(IconName::ZedAssistant)
767 }
768
769 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
770 Some("Agent Panel")
771 }
772
773 fn toggle_action(&self) -> Box<dyn Action> {
774 Box::new(ToggleFocus)
775 }
776
777 fn activation_priority(&self) -> u32 {
778 3
779 }
780
781 fn enabled(&self, cx: &App) -> bool {
782 AssistantSettings::get_global(cx).enabled
783 }
784}
785
786impl AssistantPanel {
787 fn render_title_view(&self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
788 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
789
790 let content = match &self.active_view {
791 ActiveView::Thread {
792 change_title_editor,
793 ..
794 } => {
795 let active_thread = self.thread.read(cx);
796 let is_empty = active_thread.is_empty();
797
798 let summary = active_thread.summary(cx);
799
800 if is_empty {
801 Label::new(Thread::DEFAULT_SUMMARY.clone())
802 .truncate()
803 .into_any_element()
804 } else if summary.is_none() {
805 Label::new(LOADING_SUMMARY_PLACEHOLDER)
806 .truncate()
807 .into_any_element()
808 } else {
809 change_title_editor.clone().into_any_element()
810 }
811 }
812 ActiveView::PromptEditor => {
813 let title = self
814 .context_editor
815 .as_ref()
816 .map(|context_editor| {
817 SharedString::from(context_editor.read(cx).title(cx).to_string())
818 })
819 .unwrap_or_else(|| SharedString::from(LOADING_SUMMARY_PLACEHOLDER));
820
821 Label::new(title).truncate().into_any_element()
822 }
823 ActiveView::History => Label::new("History").truncate().into_any_element(),
824 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
825 };
826
827 h_flex()
828 .key_context("TitleEditor")
829 .id("TitleEditor")
830 .pl_2()
831 .flex_grow()
832 .w_full()
833 .max_w_full()
834 .overflow_x_scroll()
835 .child(content)
836 .into_any()
837 }
838
839 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
840 let active_thread = self.thread.read(cx);
841 let thread = active_thread.thread().read(cx);
842 let token_usage = thread.total_token_usage(cx);
843 let thread_id = thread.id().clone();
844
845 let is_generating = thread.is_generating();
846 let is_empty = active_thread.is_empty();
847 let focus_handle = self.focus_handle(cx);
848
849 let show_token_count = match &self.active_view {
850 ActiveView::Thread { .. } => !is_empty,
851 ActiveView::PromptEditor => self.context_editor.is_some(),
852 _ => false,
853 };
854
855 h_flex()
856 .id("assistant-toolbar")
857 .h(Tab::container_height(cx))
858 .max_w_full()
859 .flex_none()
860 .justify_between()
861 .gap_2()
862 .bg(cx.theme().colors().tab_bar_background)
863 .border_b_1()
864 .border_color(cx.theme().colors().border)
865 .child(self.render_title_view(window, cx))
866 .child(
867 h_flex()
868 .h_full()
869 .gap_2()
870 .when(show_token_count, |parent| match self.active_view {
871 ActiveView::Thread { .. } => {
872 if token_usage.total == 0 {
873 return parent;
874 }
875
876 let token_color = match token_usage.ratio {
877 TokenUsageRatio::Normal => Color::Muted,
878 TokenUsageRatio::Warning => Color::Warning,
879 TokenUsageRatio::Exceeded => Color::Error,
880 };
881
882 parent.child(
883 h_flex()
884 .flex_shrink_0()
885 .gap_0p5()
886 .child(
887 Label::new(assistant_context_editor::humanize_token_count(
888 token_usage.total,
889 ))
890 .size(LabelSize::Small)
891 .color(token_color)
892 .map(|label| {
893 if is_generating {
894 label
895 .with_animation(
896 "used-tokens-label",
897 Animation::new(Duration::from_secs(2))
898 .repeat()
899 .with_easing(pulsating_between(
900 0.6, 1.,
901 )),
902 |label, delta| label.alpha(delta),
903 )
904 .into_any()
905 } else {
906 label.into_any_element()
907 }
908 }),
909 )
910 .child(
911 Label::new("/").size(LabelSize::Small).color(Color::Muted),
912 )
913 .child(
914 Label::new(assistant_context_editor::humanize_token_count(
915 token_usage.max,
916 ))
917 .size(LabelSize::Small)
918 .color(Color::Muted),
919 ),
920 )
921 }
922 ActiveView::PromptEditor => {
923 let Some(editor) = self.context_editor.as_ref() else {
924 return parent;
925 };
926 let Some(element) = render_remaining_tokens(editor, cx) else {
927 return parent;
928 };
929 parent.child(element)
930 }
931 _ => parent,
932 })
933 .child(
934 h_flex()
935 .h_full()
936 .gap(DynamicSpacing::Base02.rems(cx))
937 .px(DynamicSpacing::Base08.rems(cx))
938 .border_l_1()
939 .border_color(cx.theme().colors().border)
940 .child(
941 IconButton::new("new", IconName::Plus)
942 .icon_size(IconSize::Small)
943 .style(ButtonStyle::Subtle)
944 .tooltip(move |window, cx| {
945 Tooltip::for_action_in(
946 "New Thread",
947 &NewThread::default(),
948 &focus_handle,
949 window,
950 cx,
951 )
952 })
953 .on_click(move |_event, window, cx| {
954 window.dispatch_action(
955 NewThread::default().boxed_clone(),
956 cx,
957 );
958 }),
959 )
960 .child(
961 PopoverMenu::new("assistant-menu")
962 .trigger_with_tooltip(
963 IconButton::new("new", IconName::Ellipsis)
964 .icon_size(IconSize::Small)
965 .style(ButtonStyle::Subtle),
966 Tooltip::text("Toggle Agent Menu"),
967 )
968 .anchor(Corner::TopRight)
969 .with_handle(self.assistant_dropdown_menu_handle.clone())
970 .menu(move |window, cx| {
971 Some(ContextMenu::build(
972 window,
973 cx,
974 |menu, _window, _cx| {
975 menu.action(
976 "New Thread",
977 Box::new(NewThread {
978 from_thread_id: None,
979 }),
980 )
981 .action(
982 "New Prompt Editor",
983 NewPromptEditor.boxed_clone(),
984 )
985 .when(!is_empty, |menu| {
986 menu.action(
987 "Continue in New Thread",
988 Box::new(NewThread {
989 from_thread_id: Some(thread_id.clone()),
990 }),
991 )
992 })
993 .separator()
994 .action("History", OpenHistory.boxed_clone())
995 .action("Settings", OpenConfiguration.boxed_clone())
996 },
997 ))
998 }),
999 ),
1000 ),
1001 )
1002 }
1003
1004 fn render_active_thread_or_empty_state(
1005 &self,
1006 window: &mut Window,
1007 cx: &mut Context<Self>,
1008 ) -> AnyElement {
1009 if self.thread.read(cx).is_empty() {
1010 return self
1011 .render_thread_empty_state(window, cx)
1012 .into_any_element();
1013 }
1014
1015 self.thread.clone().into_any_element()
1016 }
1017
1018 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
1019 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
1020 return Some(ConfigurationError::NoProvider);
1021 };
1022
1023 if !model.provider.is_authenticated(cx) {
1024 return Some(ConfigurationError::ProviderNotAuthenticated);
1025 }
1026
1027 if model.provider.must_accept_terms(cx) {
1028 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
1029 model.provider,
1030 ));
1031 }
1032
1033 None
1034 }
1035
1036 fn render_thread_empty_state(
1037 &self,
1038 window: &mut Window,
1039 cx: &mut Context<Self>,
1040 ) -> impl IntoElement {
1041 let recent_history = self
1042 .history_store
1043 .update(cx, |this, cx| this.recent_entries(6, cx));
1044
1045 let configuration_error = self.configuration_error(cx);
1046 let no_error = configuration_error.is_none();
1047 let focus_handle = self.focus_handle(cx);
1048
1049 v_flex()
1050 .size_full()
1051 .when(recent_history.is_empty(), |this| {
1052 let configuration_error_ref = &configuration_error;
1053 this.child(
1054 v_flex()
1055 .size_full()
1056 .max_w_80()
1057 .mx_auto()
1058 .justify_center()
1059 .items_center()
1060 .gap_1()
1061 .child(
1062 h_flex().child(
1063 Headline::new("Welcome to the Agent Panel")
1064 ),
1065 )
1066 .when(no_error, |parent| {
1067 parent
1068 .child(
1069 h_flex().child(
1070 Label::new("Ask and build anything.")
1071 .color(Color::Muted)
1072 .mb_2p5(),
1073 ),
1074 )
1075 .child(
1076 Button::new("new-thread", "Start New Thread")
1077 .icon(IconName::Plus)
1078 .icon_position(IconPosition::Start)
1079 .icon_size(IconSize::Small)
1080 .icon_color(Color::Muted)
1081 .full_width()
1082 .key_binding(KeyBinding::for_action_in(
1083 &NewThread::default(),
1084 &focus_handle,
1085 window,
1086 cx,
1087 ))
1088 .on_click(|_event, window, cx| {
1089 window.dispatch_action(NewThread::default().boxed_clone(), cx)
1090 }),
1091 )
1092 .child(
1093 Button::new("context", "Add Context")
1094 .icon(IconName::FileCode)
1095 .icon_position(IconPosition::Start)
1096 .icon_size(IconSize::Small)
1097 .icon_color(Color::Muted)
1098 .full_width()
1099 .key_binding(KeyBinding::for_action_in(
1100 &ToggleContextPicker,
1101 &focus_handle,
1102 window,
1103 cx,
1104 ))
1105 .on_click(|_event, window, cx| {
1106 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
1107 }),
1108 )
1109 .child(
1110 Button::new("mode", "Switch Model")
1111 .icon(IconName::DatabaseZap)
1112 .icon_position(IconPosition::Start)
1113 .icon_size(IconSize::Small)
1114 .icon_color(Color::Muted)
1115 .full_width()
1116 .key_binding(KeyBinding::for_action_in(
1117 &ToggleModelSelector,
1118 &focus_handle,
1119 window,
1120 cx,
1121 ))
1122 .on_click(|_event, window, cx| {
1123 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
1124 }),
1125 )
1126 .child(
1127 Button::new("settings", "View Settings")
1128 .icon(IconName::Settings)
1129 .icon_position(IconPosition::Start)
1130 .icon_size(IconSize::Small)
1131 .icon_color(Color::Muted)
1132 .full_width()
1133 .key_binding(KeyBinding::for_action_in(
1134 &OpenConfiguration,
1135 &focus_handle,
1136 window,
1137 cx,
1138 ))
1139 .on_click(|_event, window, cx| {
1140 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1141 }),
1142 )
1143 })
1144 .map(|parent| {
1145 match configuration_error_ref {
1146 Some(ConfigurationError::ProviderNotAuthenticated)
1147 | Some(ConfigurationError::NoProvider) => {
1148 parent
1149 .child(
1150 h_flex().child(
1151 Label::new("To start using the agent, configure at least one LLM provider.")
1152 .color(Color::Muted)
1153 .mb_2p5()
1154 )
1155 )
1156 .child(
1157 Button::new("settings", "Configure a Provider")
1158 .icon(IconName::Settings)
1159 .icon_position(IconPosition::Start)
1160 .icon_size(IconSize::Small)
1161 .icon_color(Color::Muted)
1162 .full_width()
1163 .key_binding(KeyBinding::for_action_in(
1164 &OpenConfiguration,
1165 &focus_handle,
1166 window,
1167 cx,
1168 ))
1169 .on_click(|_event, window, cx| {
1170 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1171 }),
1172 )
1173 }
1174 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1175 parent.children(
1176 provider.render_accept_terms(
1177 LanguageModelProviderTosView::ThreadFreshStart,
1178 cx,
1179 ),
1180 )
1181 }
1182 None => parent,
1183 }
1184 })
1185 )
1186 })
1187 .when(!recent_history.is_empty(), |parent| {
1188 let focus_handle = focus_handle.clone();
1189 let configuration_error_ref = &configuration_error;
1190
1191 parent
1192 .p_1p5()
1193 .justify_end()
1194 .gap_1()
1195 .child(
1196 h_flex()
1197 .pl_1p5()
1198 .pb_1()
1199 .w_full()
1200 .justify_between()
1201 .border_b_1()
1202 .border_color(cx.theme().colors().border_variant)
1203 .child(
1204 Label::new("Past Interactions")
1205 .size(LabelSize::Small)
1206 .color(Color::Muted),
1207 )
1208 .child(
1209 Button::new("view-history", "View All")
1210 .style(ButtonStyle::Subtle)
1211 .label_size(LabelSize::Small)
1212 .key_binding(
1213 KeyBinding::for_action_in(
1214 &OpenHistory,
1215 &self.focus_handle(cx),
1216 window,
1217 cx,
1218 ).map(|kb| kb.size(rems_from_px(12.))),
1219 )
1220 .on_click(move |_event, window, cx| {
1221 window.dispatch_action(OpenHistory.boxed_clone(), cx);
1222 }),
1223 ),
1224 )
1225 .child(
1226 v_flex()
1227 .gap_1()
1228 .children(
1229 recent_history.into_iter().map(|entry| {
1230 // TODO: Add keyboard navigation.
1231 match entry {
1232 HistoryEntry::Thread(thread) => {
1233 PastThread::new(thread, cx.entity().downgrade(), false, vec![])
1234 .into_any_element()
1235 }
1236 HistoryEntry::Context(context) => {
1237 PastContext::new(context, cx.entity().downgrade(), false, vec![])
1238 .into_any_element()
1239 }
1240 }
1241 }),
1242 )
1243 )
1244 .map(|parent| {
1245 match configuration_error_ref {
1246 Some(ConfigurationError::ProviderNotAuthenticated)
1247 | Some(ConfigurationError::NoProvider) => {
1248 parent
1249 .child(
1250 Banner::new()
1251 .severity(ui::Severity::Warning)
1252 .children(
1253 Label::new(
1254 "Configure at least one LLM provider to start using the panel.",
1255 )
1256 .size(LabelSize::Small),
1257 )
1258 .action_slot(
1259 Button::new("settings", "Configure Provider")
1260 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
1261 .label_size(LabelSize::Small)
1262 .key_binding(
1263 KeyBinding::for_action_in(
1264 &OpenConfiguration,
1265 &focus_handle,
1266 window,
1267 cx,
1268 )
1269 .map(|kb| kb.size(rems_from_px(12.))),
1270 )
1271 .on_click(|_event, window, cx| {
1272 window.dispatch_action(
1273 OpenConfiguration.boxed_clone(),
1274 cx,
1275 )
1276 }),
1277 ),
1278 )
1279 }
1280 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1281 parent
1282 .child(
1283 Banner::new()
1284 .severity(ui::Severity::Warning)
1285 .children(
1286 h_flex()
1287 .w_full()
1288 .children(
1289 provider.render_accept_terms(
1290 LanguageModelProviderTosView::ThreadtEmptyState,
1291 cx,
1292 ),
1293 ),
1294 ),
1295 )
1296 }
1297 None => parent,
1298 }
1299 })
1300 })
1301 }
1302
1303 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1304 let last_error = self.thread.read(cx).last_error()?;
1305
1306 Some(
1307 div()
1308 .absolute()
1309 .right_3()
1310 .bottom_12()
1311 .max_w_96()
1312 .py_2()
1313 .px_3()
1314 .elevation_2(cx)
1315 .occlude()
1316 .child(match last_error {
1317 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
1318 ThreadError::MaxMonthlySpendReached => {
1319 self.render_max_monthly_spend_reached_error(cx)
1320 }
1321 ThreadError::Message { header, message } => {
1322 self.render_error_message(header, message, cx)
1323 }
1324 })
1325 .into_any(),
1326 )
1327 }
1328
1329 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
1330 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.";
1331
1332 v_flex()
1333 .gap_0p5()
1334 .child(
1335 h_flex()
1336 .gap_1p5()
1337 .items_center()
1338 .child(Icon::new(IconName::XCircle).color(Color::Error))
1339 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
1340 )
1341 .child(
1342 div()
1343 .id("error-message")
1344 .max_h_24()
1345 .overflow_y_scroll()
1346 .child(Label::new(ERROR_MESSAGE)),
1347 )
1348 .child(
1349 h_flex()
1350 .justify_end()
1351 .mt_1()
1352 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
1353 |this, _, _, cx| {
1354 this.thread.update(cx, |this, _cx| {
1355 this.clear_last_error();
1356 });
1357
1358 cx.open_url(&zed_urls::account_url(cx));
1359 cx.notify();
1360 },
1361 )))
1362 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1363 |this, _, _, cx| {
1364 this.thread.update(cx, |this, _cx| {
1365 this.clear_last_error();
1366 });
1367
1368 cx.notify();
1369 },
1370 ))),
1371 )
1372 .into_any()
1373 }
1374
1375 fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
1376 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
1377
1378 v_flex()
1379 .gap_0p5()
1380 .child(
1381 h_flex()
1382 .gap_1p5()
1383 .items_center()
1384 .child(Icon::new(IconName::XCircle).color(Color::Error))
1385 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
1386 )
1387 .child(
1388 div()
1389 .id("error-message")
1390 .max_h_24()
1391 .overflow_y_scroll()
1392 .child(Label::new(ERROR_MESSAGE)),
1393 )
1394 .child(
1395 h_flex()
1396 .justify_end()
1397 .mt_1()
1398 .child(
1399 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
1400 cx.listener(|this, _, _, cx| {
1401 this.thread.update(cx, |this, _cx| {
1402 this.clear_last_error();
1403 });
1404
1405 cx.open_url(&zed_urls::account_url(cx));
1406 cx.notify();
1407 }),
1408 ),
1409 )
1410 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1411 |this, _, _, cx| {
1412 this.thread.update(cx, |this, _cx| {
1413 this.clear_last_error();
1414 });
1415
1416 cx.notify();
1417 },
1418 ))),
1419 )
1420 .into_any()
1421 }
1422
1423 fn render_error_message(
1424 &self,
1425 header: SharedString,
1426 message: SharedString,
1427 cx: &mut Context<Self>,
1428 ) -> AnyElement {
1429 v_flex()
1430 .gap_0p5()
1431 .child(
1432 h_flex()
1433 .gap_1p5()
1434 .items_center()
1435 .child(Icon::new(IconName::XCircle).color(Color::Error))
1436 .child(Label::new(header).weight(FontWeight::MEDIUM)),
1437 )
1438 .child(
1439 div()
1440 .id("error-message")
1441 .max_h_32()
1442 .overflow_y_scroll()
1443 .child(Label::new(message)),
1444 )
1445 .child(
1446 h_flex()
1447 .justify_end()
1448 .mt_1()
1449 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1450 |this, _, _, cx| {
1451 this.thread.update(cx, |this, _cx| {
1452 this.clear_last_error();
1453 });
1454
1455 cx.notify();
1456 },
1457 ))),
1458 )
1459 .into_any()
1460 }
1461
1462 fn key_context(&self) -> KeyContext {
1463 let mut key_context = KeyContext::new_with_defaults();
1464 key_context.add("AgentPanel");
1465 if matches!(self.active_view, ActiveView::PromptEditor) {
1466 key_context.add("prompt_editor");
1467 }
1468 key_context
1469 }
1470}
1471
1472impl Render for AssistantPanel {
1473 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1474 v_flex()
1475 .key_context(self.key_context())
1476 .justify_between()
1477 .size_full()
1478 .on_action(cx.listener(Self::cancel))
1479 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
1480 this.new_thread(action, window, cx);
1481 }))
1482 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
1483 this.open_history(window, cx);
1484 }))
1485 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
1486 this.open_configuration(window, cx);
1487 }))
1488 .on_action(cx.listener(Self::open_active_thread_as_markdown))
1489 .on_action(cx.listener(Self::deploy_prompt_library))
1490 .on_action(cx.listener(Self::open_agent_diff))
1491 .child(self.render_toolbar(window, cx))
1492 .map(|parent| match self.active_view {
1493 ActiveView::Thread { .. } => parent
1494 .child(self.render_active_thread_or_empty_state(window, cx))
1495 .child(h_flex().child(self.message_editor.clone()))
1496 .children(self.render_last_error(cx)),
1497 ActiveView::History => parent.child(self.history.clone()),
1498 ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
1499 ActiveView::Configuration => parent.children(self.configuration.clone()),
1500 })
1501 }
1502}
1503
1504struct PromptLibraryInlineAssist {
1505 workspace: WeakEntity<Workspace>,
1506}
1507
1508impl PromptLibraryInlineAssist {
1509 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
1510 Self { workspace }
1511 }
1512}
1513
1514impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1515 fn assist(
1516 &self,
1517 prompt_editor: &Entity<Editor>,
1518 _initial_prompt: Option<String>,
1519 window: &mut Window,
1520 cx: &mut Context<PromptLibrary>,
1521 ) {
1522 InlineAssistant::update_global(cx, |assistant, cx| {
1523 assistant.assist(&prompt_editor, self.workspace.clone(), None, window, cx)
1524 })
1525 }
1526
1527 fn focus_assistant_panel(
1528 &self,
1529 workspace: &mut Workspace,
1530 window: &mut Window,
1531 cx: &mut Context<Workspace>,
1532 ) -> bool {
1533 workspace
1534 .focus_panel::<AssistantPanel>(window, cx)
1535 .is_some()
1536 }
1537}
1538
1539pub struct ConcreteAssistantPanelDelegate;
1540
1541impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1542 fn active_context_editor(
1543 &self,
1544 workspace: &mut Workspace,
1545 _window: &mut Window,
1546 cx: &mut Context<Workspace>,
1547 ) -> Option<Entity<ContextEditor>> {
1548 let panel = workspace.panel::<AssistantPanel>(cx)?;
1549 panel.update(cx, |panel, _cx| panel.context_editor.clone())
1550 }
1551
1552 fn open_saved_context(
1553 &self,
1554 workspace: &mut Workspace,
1555 path: std::path::PathBuf,
1556 window: &mut Window,
1557 cx: &mut Context<Workspace>,
1558 ) -> Task<Result<()>> {
1559 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1560 return Task::ready(Err(anyhow!("Agent panel not found")));
1561 };
1562
1563 panel.update(cx, |panel, cx| {
1564 panel.open_saved_prompt_editor(path, window, cx)
1565 })
1566 }
1567
1568 fn open_remote_context(
1569 &self,
1570 _workspace: &mut Workspace,
1571 _context_id: assistant_context_editor::ContextId,
1572 _window: &mut Window,
1573 _cx: &mut Context<Workspace>,
1574 ) -> Task<Result<Entity<ContextEditor>>> {
1575 Task::ready(Err(anyhow!("opening remote context not implemented")))
1576 }
1577
1578 fn quote_selection(
1579 &self,
1580 _workspace: &mut Workspace,
1581 _creases: Vec<(String, String)>,
1582 _window: &mut Window,
1583 _cx: &mut Context<Workspace>,
1584 ) {
1585 }
1586}