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