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