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