1use std::path::PathBuf;
2use std::sync::Arc;
3
4use anyhow::{anyhow, Result};
5use assistant_context_editor::{
6 make_lsp_adapter_delegate, render_remaining_tokens, AssistantPanelDelegate, ConfigurationError,
7 ContextEditor, SlashCommandCompletionProvider,
8};
9use assistant_settings::{AssistantDockPosition, AssistantSettings};
10use assistant_slash_command::SlashCommandWorkingSet;
11use assistant_tool::ToolWorkingSet;
12
13use client::zed_urls;
14use editor::{Editor, MultiBuffer};
15use fs::Fs;
16use gpui::{
17 prelude::*, Action, AnyElement, App, AsyncWindowContext, Corner, Entity, EventEmitter,
18 FocusHandle, Focusable, FontWeight, KeyContext, Pixels, Subscription, Task, UpdateGlobal,
19 WeakEntity,
20};
21use language::LanguageRegistry;
22use language_model::{LanguageModelProviderTosView, LanguageModelRegistry};
23use project::Project;
24use prompt_library::{open_prompt_library, PromptLibrary};
25use prompt_store::PromptBuilder;
26use settings::{update_settings_file, Settings};
27use time::UtcOffset;
28use ui::{prelude::*, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab, Tooltip};
29use util::ResultExt as _;
30use workspace::dock::{DockPosition, Panel, PanelEvent};
31use workspace::Workspace;
32use zed_actions::assistant::{DeployPromptLibrary, ToggleFocus};
33
34use crate::active_thread::ActiveThread;
35use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
36use crate::history_store::{HistoryEntry, HistoryStore};
37use crate::message_editor::MessageEditor;
38use crate::thread::{Thread, ThreadError, ThreadId};
39use crate::thread_history::{PastContext, PastThread, ThreadHistory};
40use crate::thread_store::ThreadStore;
41use crate::{
42 InlineAssistant, NewPromptEditor, NewThread, OpenActiveThreadAsMarkdown, OpenConfiguration,
43 OpenHistory,
44};
45
46pub fn init(cx: &mut App) {
47 cx.observe_new(
48 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
49 workspace
50 .register_action(|workspace, _: &NewThread, window, cx| {
51 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
52 panel.update(cx, |panel, cx| panel.new_thread(window, cx));
53 workspace.focus_panel::<AssistantPanel>(window, cx);
54 }
55 })
56 .register_action(|workspace, _: &OpenHistory, window, cx| {
57 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
58 workspace.focus_panel::<AssistantPanel>(window, cx);
59 panel.update(cx, |panel, cx| panel.open_history(window, cx));
60 }
61 })
62 .register_action(|workspace, _: &NewPromptEditor, window, cx| {
63 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
64 workspace.focus_panel::<AssistantPanel>(window, cx);
65 panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
66 }
67 })
68 .register_action(|workspace, _: &OpenConfiguration, window, cx| {
69 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
70 workspace.focus_panel::<AssistantPanel>(window, cx);
71 panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
72 }
73 });
74 },
75 )
76 .detach();
77}
78
79enum ActiveView {
80 Thread,
81 PromptEditor,
82 History,
83 Configuration,
84}
85
86pub struct AssistantPanel {
87 workspace: WeakEntity<Workspace>,
88 project: Entity<Project>,
89 fs: Arc<dyn Fs>,
90 language_registry: Arc<LanguageRegistry>,
91 thread_store: Entity<ThreadStore>,
92 thread: Entity<ActiveThread>,
93 message_editor: Entity<MessageEditor>,
94 context_store: Entity<assistant_context_editor::ContextStore>,
95 context_editor: Option<Entity<ContextEditor>>,
96 configuration: Option<Entity<AssistantConfiguration>>,
97 configuration_subscription: Option<Subscription>,
98 local_timezone: UtcOffset,
99 active_view: ActiveView,
100 history_store: Entity<HistoryStore>,
101 history: Entity<ThreadHistory>,
102 new_item_context_menu_handle: PopoverMenuHandle<ContextMenu>,
103 width: Option<Pixels>,
104 height: Option<Pixels>,
105}
106
107impl AssistantPanel {
108 pub fn load(
109 workspace: WeakEntity<Workspace>,
110 prompt_builder: Arc<PromptBuilder>,
111 cx: AsyncWindowContext,
112 ) -> Task<Result<Entity<Self>>> {
113 cx.spawn(|mut cx| async move {
114 let tools = Arc::new(ToolWorkingSet::default());
115 log::info!("[assistant2-debug] initializing ThreadStore");
116 let thread_store = workspace.update(&mut cx, |workspace, cx| {
117 let project = workspace.project().clone();
118 ThreadStore::new(project, tools.clone(), prompt_builder.clone(), cx)
119 })??;
120 log::info!("[assistant2-debug] finished initializing ThreadStore");
121
122 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
123 log::info!("[assistant2-debug] initializing ContextStore");
124 let context_store = workspace
125 .update(&mut cx, |workspace, cx| {
126 let project = workspace.project().clone();
127 assistant_context_editor::ContextStore::new(
128 project,
129 prompt_builder.clone(),
130 slash_commands,
131 cx,
132 )
133 })?
134 .await?;
135 log::info!("[assistant2-debug] finished initializing ContextStore");
136
137 workspace.update_in(&mut cx, |workspace, window, cx| {
138 cx.new(|cx| Self::new(workspace, thread_store, context_store, window, cx))
139 })
140 })
141 }
142
143 fn new(
144 workspace: &Workspace,
145 thread_store: Entity<ThreadStore>,
146 context_store: Entity<assistant_context_editor::ContextStore>,
147 window: &mut Window,
148 cx: &mut Context<Self>,
149 ) -> Self {
150 log::info!("[assistant2-debug] AssistantPanel::new");
151 let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
152 let fs = workspace.app_state().fs.clone();
153 let project = workspace.project().clone();
154 let language_registry = project.read(cx).languages().clone();
155 let workspace = workspace.weak_handle();
156 let weak_self = cx.entity().downgrade();
157
158 let message_editor = cx.new(|cx| {
159 MessageEditor::new(
160 fs.clone(),
161 workspace.clone(),
162 thread_store.downgrade(),
163 thread.clone(),
164 window,
165 cx,
166 )
167 });
168
169 let history_store =
170 cx.new(|cx| HistoryStore::new(thread_store.clone(), context_store.clone(), cx));
171
172 let thread = cx.new(|cx| {
173 ActiveThread::new(
174 thread.clone(),
175 thread_store.clone(),
176 language_registry.clone(),
177 window,
178 cx,
179 )
180 });
181
182 Self {
183 active_view: ActiveView::Thread,
184 workspace,
185 project: project.clone(),
186 fs: fs.clone(),
187 language_registry,
188 thread_store: thread_store.clone(),
189 thread,
190 message_editor,
191 context_store,
192 context_editor: None,
193 configuration: None,
194 configuration_subscription: None,
195 local_timezone: UtcOffset::from_whole_seconds(
196 chrono::Local::now().offset().local_minus_utc(),
197 )
198 .unwrap(),
199 history_store: history_store.clone(),
200 history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, cx)),
201 new_item_context_menu_handle: PopoverMenuHandle::default(),
202 width: None,
203 height: None,
204 }
205 }
206
207 pub fn toggle_focus(
208 workspace: &mut Workspace,
209 _: &ToggleFocus,
210 window: &mut Window,
211 cx: &mut Context<Workspace>,
212 ) {
213 let settings = AssistantSettings::get_global(cx);
214 if !settings.enabled {
215 return;
216 }
217
218 workspace.toggle_panel_focus::<Self>(window, cx);
219 }
220
221 pub(crate) fn local_timezone(&self) -> UtcOffset {
222 self.local_timezone
223 }
224
225 pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
226 &self.thread_store
227 }
228
229 fn cancel(
230 &mut self,
231 _: &editor::actions::Cancel,
232 _window: &mut Window,
233 cx: &mut Context<Self>,
234 ) {
235 self.thread
236 .update(cx, |thread, cx| thread.cancel_last_completion(cx));
237 }
238
239 fn new_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
240 let thread = self
241 .thread_store
242 .update(cx, |this, cx| this.create_thread(cx));
243
244 self.active_view = ActiveView::Thread;
245 self.thread = cx.new(|cx| {
246 ActiveThread::new(
247 thread.clone(),
248 self.thread_store.clone(),
249 self.language_registry.clone(),
250 window,
251 cx,
252 )
253 });
254 self.message_editor = cx.new(|cx| {
255 MessageEditor::new(
256 self.fs.clone(),
257 self.workspace.clone(),
258 self.thread_store.downgrade(),
259 thread,
260 window,
261 cx,
262 )
263 });
264 self.message_editor.focus_handle(cx).focus(window);
265 }
266
267 fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
268 self.active_view = ActiveView::PromptEditor;
269
270 let context = self
271 .context_store
272 .update(cx, |context_store, cx| context_store.create(cx));
273 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
274 .log_err()
275 .flatten();
276
277 self.context_editor = Some(cx.new(|cx| {
278 let mut editor = ContextEditor::for_context(
279 context,
280 self.fs.clone(),
281 self.workspace.clone(),
282 self.project.clone(),
283 lsp_adapter_delegate,
284 window,
285 cx,
286 );
287 editor.insert_default_prompt(window, cx);
288 editor
289 }));
290
291 if let Some(context_editor) = self.context_editor.as_ref() {
292 context_editor.focus_handle(cx).focus(window);
293 }
294 }
295
296 fn deploy_prompt_library(
297 &mut self,
298 _: &DeployPromptLibrary,
299 _window: &mut Window,
300 cx: &mut Context<Self>,
301 ) {
302 open_prompt_library(
303 self.language_registry.clone(),
304 Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
305 Arc::new(|| {
306 Box::new(SlashCommandCompletionProvider::new(
307 Arc::new(SlashCommandWorkingSet::default()),
308 None,
309 None,
310 ))
311 }),
312 cx,
313 )
314 .detach_and_log_err(cx);
315 }
316
317 fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
318 self.thread_store
319 .update(cx, |thread_store, cx| thread_store.reload(cx))
320 .detach_and_log_err(cx);
321 self.active_view = ActiveView::History;
322 self.history.focus_handle(cx).focus(window);
323 cx.notify();
324 }
325
326 pub(crate) fn open_saved_prompt_editor(
327 &mut self,
328 path: PathBuf,
329 window: &mut Window,
330 cx: &mut Context<Self>,
331 ) -> Task<Result<()>> {
332 let context = self
333 .context_store
334 .update(cx, |store, cx| store.open_local_context(path.clone(), cx));
335 let fs = self.fs.clone();
336 let project = self.project.clone();
337 let workspace = self.workspace.clone();
338
339 let lsp_adapter_delegate = make_lsp_adapter_delegate(&project, cx).log_err().flatten();
340
341 cx.spawn_in(window, |this, mut cx| async move {
342 let context = context.await?;
343 this.update_in(&mut cx, |this, window, cx| {
344 let editor = cx.new(|cx| {
345 ContextEditor::for_context(
346 context,
347 fs,
348 workspace,
349 project,
350 lsp_adapter_delegate,
351 window,
352 cx,
353 )
354 });
355 this.active_view = ActiveView::PromptEditor;
356 this.context_editor = Some(editor);
357
358 anyhow::Ok(())
359 })??;
360 Ok(())
361 })
362 }
363
364 pub(crate) fn open_thread(
365 &mut self,
366 thread_id: &ThreadId,
367 window: &mut Window,
368 cx: &mut Context<Self>,
369 ) -> Task<Result<()>> {
370 let open_thread_task = self
371 .thread_store
372 .update(cx, |this, cx| this.open_thread(thread_id, cx));
373
374 cx.spawn_in(window, |this, mut cx| async move {
375 let thread = open_thread_task.await?;
376 this.update_in(&mut cx, |this, window, cx| {
377 this.active_view = ActiveView::Thread;
378 this.thread = cx.new(|cx| {
379 ActiveThread::new(
380 thread.clone(),
381 this.thread_store.clone(),
382 this.language_registry.clone(),
383 window,
384 cx,
385 )
386 });
387 this.message_editor = cx.new(|cx| {
388 MessageEditor::new(
389 this.fs.clone(),
390 this.workspace.clone(),
391 this.thread_store.downgrade(),
392 thread,
393 window,
394 cx,
395 )
396 });
397 this.message_editor.focus_handle(cx).focus(window);
398 })
399 })
400 }
401
402 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
403 self.active_view = ActiveView::Configuration;
404 self.configuration = Some(cx.new(|cx| AssistantConfiguration::new(window, cx)));
405
406 if let Some(configuration) = self.configuration.as_ref() {
407 self.configuration_subscription = Some(cx.subscribe_in(
408 configuration,
409 window,
410 Self::handle_assistant_configuration_event,
411 ));
412
413 configuration.focus_handle(cx).focus(window);
414 }
415 }
416
417 pub(crate) fn open_active_thread_as_markdown(
418 &mut self,
419 _: &OpenActiveThreadAsMarkdown,
420 window: &mut Window,
421 cx: &mut Context<Self>,
422 ) {
423 let Some(workspace) = self
424 .workspace
425 .upgrade()
426 .ok_or_else(|| anyhow!("workspace dropped"))
427 .log_err()
428 else {
429 return;
430 };
431
432 let markdown_language_task = workspace
433 .read(cx)
434 .app_state()
435 .languages
436 .language_for_name("Markdown");
437 let thread = self.active_thread(cx);
438 cx.spawn_in(window, |_this, mut cx| async move {
439 let markdown_language = markdown_language_task.await?;
440
441 workspace.update_in(&mut cx, |workspace, window, cx| {
442 let thread = thread.read(cx);
443 let markdown = thread.to_markdown()?;
444 let thread_summary = thread
445 .summary()
446 .map(|summary| summary.to_string())
447 .unwrap_or_else(|| "Thread".to_string());
448
449 let project = workspace.project().clone();
450 let buffer = project.update(cx, |project, cx| {
451 project.create_local_buffer(&markdown, Some(markdown_language), cx)
452 });
453 let buffer = cx.new(|cx| {
454 MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
455 });
456
457 workspace.add_item_to_active_pane(
458 Box::new(cx.new(|cx| {
459 let mut editor = Editor::for_multibuffer(
460 buffer,
461 Some(project.clone()),
462 true,
463 window,
464 cx,
465 );
466 editor.set_breadcrumb_header(thread_summary);
467 editor
468 })),
469 None,
470 true,
471 window,
472 cx,
473 );
474
475 anyhow::Ok(())
476 })
477 })
478 .detach_and_log_err(cx);
479 }
480
481 fn handle_assistant_configuration_event(
482 &mut self,
483 _entity: &Entity<AssistantConfiguration>,
484 event: &AssistantConfigurationEvent,
485 window: &mut Window,
486 cx: &mut Context<Self>,
487 ) {
488 match event {
489 AssistantConfigurationEvent::NewThread(provider) => {
490 if LanguageModelRegistry::read_global(cx)
491 .active_provider()
492 .map_or(true, |active_provider| {
493 active_provider.id() != provider.id()
494 })
495 {
496 if let Some(model) = provider.default_model(cx) {
497 update_settings_file::<AssistantSettings>(
498 self.fs.clone(),
499 cx,
500 move |settings, _| settings.set_model(model),
501 );
502 }
503 }
504
505 self.new_thread(window, cx);
506 }
507 }
508 }
509
510 pub(crate) fn active_thread(&self, cx: &App) -> Entity<Thread> {
511 self.thread.read(cx).thread().clone()
512 }
513
514 pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut Context<Self>) {
515 self.thread_store
516 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
517 .detach_and_log_err(cx);
518 }
519
520 pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
521 self.context_editor.clone()
522 }
523
524 pub(crate) fn delete_context(&mut self, path: PathBuf, cx: &mut Context<Self>) {
525 self.context_store
526 .update(cx, |this, cx| this.delete_local_context(path, cx))
527 .detach_and_log_err(cx);
528 }
529}
530
531impl Focusable for AssistantPanel {
532 fn focus_handle(&self, cx: &App) -> FocusHandle {
533 match self.active_view {
534 ActiveView::Thread => self.message_editor.focus_handle(cx),
535 ActiveView::History => self.history.focus_handle(cx),
536 ActiveView::PromptEditor => {
537 if let Some(context_editor) = self.context_editor.as_ref() {
538 context_editor.focus_handle(cx)
539 } else {
540 cx.focus_handle()
541 }
542 }
543 ActiveView::Configuration => {
544 if let Some(configuration) = self.configuration.as_ref() {
545 configuration.focus_handle(cx)
546 } else {
547 cx.focus_handle()
548 }
549 }
550 }
551 }
552}
553
554impl EventEmitter<PanelEvent> for AssistantPanel {}
555
556impl Panel for AssistantPanel {
557 fn persistent_name() -> &'static str {
558 "AssistantPanel2"
559 }
560
561 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
562 match AssistantSettings::get_global(cx).dock {
563 AssistantDockPosition::Left => DockPosition::Left,
564 AssistantDockPosition::Bottom => DockPosition::Bottom,
565 AssistantDockPosition::Right => DockPosition::Right,
566 }
567 }
568
569 fn position_is_valid(&self, _: DockPosition) -> bool {
570 true
571 }
572
573 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
574 settings::update_settings_file::<AssistantSettings>(
575 self.fs.clone(),
576 cx,
577 move |settings, _| {
578 let dock = match position {
579 DockPosition::Left => AssistantDockPosition::Left,
580 DockPosition::Bottom => AssistantDockPosition::Bottom,
581 DockPosition::Right => AssistantDockPosition::Right,
582 };
583 settings.set_dock(dock);
584 },
585 );
586 }
587
588 fn size(&self, window: &Window, cx: &App) -> Pixels {
589 let settings = AssistantSettings::get_global(cx);
590 match self.position(window, cx) {
591 DockPosition::Left | DockPosition::Right => {
592 self.width.unwrap_or(settings.default_width)
593 }
594 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
595 }
596 }
597
598 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
599 match self.position(window, cx) {
600 DockPosition::Left | DockPosition::Right => self.width = size,
601 DockPosition::Bottom => self.height = size,
602 }
603 cx.notify();
604 }
605
606 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
607
608 fn remote_id() -> Option<proto::PanelId> {
609 Some(proto::PanelId::AssistantPanel)
610 }
611
612 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
613 let settings = AssistantSettings::get_global(cx);
614 if !settings.enabled || !settings.button {
615 return None;
616 }
617
618 Some(IconName::ZedAssistant)
619 }
620
621 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
622 Some("Assistant Panel")
623 }
624
625 fn toggle_action(&self) -> Box<dyn Action> {
626 Box::new(ToggleFocus)
627 }
628
629 fn activation_priority(&self) -> u32 {
630 3
631 }
632}
633
634impl AssistantPanel {
635 fn render_toolbar(&self, cx: &mut Context<Self>) -> impl IntoElement {
636 let thread = self.thread.read(cx);
637
638 let title = match self.active_view {
639 ActiveView::Thread => {
640 if thread.is_empty() {
641 thread.summary_or_default(cx)
642 } else {
643 thread
644 .summary(cx)
645 .unwrap_or_else(|| SharedString::from("Loading Summary…"))
646 }
647 }
648 ActiveView::PromptEditor => self
649 .context_editor
650 .as_ref()
651 .map(|context_editor| {
652 SharedString::from(context_editor.read(cx).title(cx).to_string())
653 })
654 .unwrap_or_else(|| SharedString::from("Loading Summary…")),
655 ActiveView::History => "History".into(),
656 ActiveView::Configuration => "Assistant Settings".into(),
657 };
658
659 h_flex()
660 .id("assistant-toolbar")
661 .h(Tab::container_height(cx))
662 .flex_none()
663 .justify_between()
664 .gap(DynamicSpacing::Base08.rems(cx))
665 .bg(cx.theme().colors().tab_bar_background)
666 .border_b_1()
667 .border_color(cx.theme().colors().border)
668 .child(
669 div()
670 .id("title")
671 .overflow_x_scroll()
672 .px(DynamicSpacing::Base08.rems(cx))
673 .child(Label::new(title).truncate()),
674 )
675 .child(
676 h_flex()
677 .h_full()
678 .pl_2()
679 .gap_2()
680 .bg(cx.theme().colors().tab_bar_background)
681 .children(if matches!(self.active_view, ActiveView::PromptEditor) {
682 self.context_editor
683 .as_ref()
684 .and_then(|editor| render_remaining_tokens(editor, cx))
685 } else {
686 None
687 })
688 .child(
689 h_flex()
690 .h_full()
691 .px(DynamicSpacing::Base08.rems(cx))
692 .border_l_1()
693 .border_color(cx.theme().colors().border)
694 .gap(DynamicSpacing::Base02.rems(cx))
695 .child(
696 PopoverMenu::new("assistant-toolbar-new-popover-menu")
697 .trigger_with_tooltip(
698 IconButton::new("new", IconName::Plus)
699 .icon_size(IconSize::Small)
700 .style(ButtonStyle::Subtle),
701 Tooltip::text("New…"),
702 )
703 .anchor(Corner::TopRight)
704 .with_handle(self.new_item_context_menu_handle.clone())
705 .menu(move |window, cx| {
706 Some(ContextMenu::build(
707 window,
708 cx,
709 |menu, _window, _cx| {
710 menu.action("New Thread", NewThread.boxed_clone())
711 .action(
712 "New Prompt Editor",
713 NewPromptEditor.boxed_clone(),
714 )
715 },
716 ))
717 }),
718 )
719 .child(
720 IconButton::new("open-history", IconName::HistoryRerun)
721 .icon_size(IconSize::Small)
722 .style(ButtonStyle::Subtle)
723 .tooltip({
724 let focus_handle = self.focus_handle(cx);
725 move |window, cx| {
726 Tooltip::for_action_in(
727 "History",
728 &OpenHistory,
729 &focus_handle,
730 window,
731 cx,
732 )
733 }
734 })
735 .on_click(move |_event, window, cx| {
736 window.dispatch_action(OpenHistory.boxed_clone(), cx);
737 }),
738 )
739 .child(
740 IconButton::new("configure-assistant", IconName::Settings)
741 .icon_size(IconSize::Small)
742 .style(ButtonStyle::Subtle)
743 .tooltip(Tooltip::text("Assistant Settings"))
744 .on_click(move |_event, window, cx| {
745 window.dispatch_action(OpenConfiguration.boxed_clone(), cx);
746 }),
747 ),
748 ),
749 )
750 }
751
752 fn render_active_thread_or_empty_state(
753 &self,
754 window: &mut Window,
755 cx: &mut Context<Self>,
756 ) -> AnyElement {
757 if self.thread.read(cx).is_empty() {
758 return self
759 .render_thread_empty_state(window, cx)
760 .into_any_element();
761 }
762
763 self.thread.clone().into_any_element()
764 }
765
766 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
767 let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
768 return Some(ConfigurationError::NoProvider);
769 };
770
771 if !provider.is_authenticated(cx) {
772 return Some(ConfigurationError::ProviderNotAuthenticated);
773 }
774
775 if provider.must_accept_terms(cx) {
776 return Some(ConfigurationError::ProviderPendingTermsAcceptance(provider));
777 }
778
779 None
780 }
781
782 fn render_thread_empty_state(
783 &self,
784 window: &mut Window,
785 cx: &mut Context<Self>,
786 ) -> impl IntoElement {
787 let recent_history = self
788 .history_store
789 .update(cx, |this, cx| this.recent_entries(6, cx));
790
791 let create_welcome_heading = || {
792 h_flex()
793 .w_full()
794 .child(Headline::new("Welcome to the Assistant Panel").size(HeadlineSize::Small))
795 };
796
797 let configuration_error = self.configuration_error(cx);
798 let no_error = configuration_error.is_none();
799
800 v_flex()
801 .p_1p5()
802 .size_full()
803 .justify_end()
804 .gap_1()
805 .map(|parent| {
806 match configuration_error {
807 Some(ConfigurationError::ProviderNotAuthenticated)
808 | Some(ConfigurationError::NoProvider) => {
809 parent.child(
810 v_flex()
811 .px_1p5()
812 .gap_0p5()
813 .child(create_welcome_heading())
814 .child(
815 Label::new(
816 "To start using the assistant, configure at least one LLM provider.",
817 )
818 .color(Color::Muted),
819 )
820 .child(
821 h_flex().mt_1().w_full().child(
822 Button::new("open-configuration", "Configure a Provider")
823 .size(ButtonSize::Compact)
824 .icon(Some(IconName::Sliders))
825 .icon_size(IconSize::Small)
826 .icon_position(IconPosition::Start)
827 .on_click(cx.listener(|this, _, window, cx| {
828 this.open_configuration(window, cx);
829 })),
830 ),
831 ),
832 )
833 }
834 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => parent
835 .child(v_flex().px_1p5().gap_0p5().child(create_welcome_heading()).children(
836 provider.render_accept_terms(
837 LanguageModelProviderTosView::ThreadEmptyState,
838 cx,
839 ),
840 )),
841 None => parent,
842 }
843 })
844 .when(recent_history.is_empty() && no_error, |parent| {
845 parent.child(v_flex().gap_0p5().child(create_welcome_heading()).child(
846 Label::new("Start typing to chat with your codebase").color(Color::Muted),
847 ))
848 })
849 .when(!recent_history.is_empty(), |parent| {
850 parent
851 .child(
852 h_flex()
853 .pl_1p5()
854 .pb_1()
855 .w_full()
856 .justify_between()
857 .border_b_1()
858 .border_color(cx.theme().colors().border_variant)
859 .child(
860 Label::new("Past Interactions")
861 .size(LabelSize::Small)
862 .color(Color::Muted),
863 )
864 .child(
865 Button::new("view-history", "View All")
866 .style(ButtonStyle::Subtle)
867 .label_size(LabelSize::Small)
868 .key_binding(KeyBinding::for_action_in(
869 &OpenHistory,
870 &self.focus_handle(cx),
871 window,
872 cx,
873 ))
874 .on_click(move |_event, window, cx| {
875 window.dispatch_action(OpenHistory.boxed_clone(), cx);
876 }),
877 ),
878 )
879 .child(v_flex().gap_1().children(
880 recent_history.into_iter().map(|entry| {
881 // TODO: Add keyboard navigation.
882 match entry {
883 HistoryEntry::Thread(thread) => {
884 PastThread::new(thread, cx.entity().downgrade(), false)
885 .into_any_element()
886 }
887 HistoryEntry::Context(context) => {
888 PastContext::new(context, cx.entity().downgrade(), false)
889 .into_any_element()
890 }
891 }
892 }),
893 ))
894 })
895 }
896
897 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
898 let last_error = self.thread.read(cx).last_error()?;
899
900 Some(
901 div()
902 .absolute()
903 .right_3()
904 .bottom_12()
905 .max_w_96()
906 .py_2()
907 .px_3()
908 .elevation_2(cx)
909 .occlude()
910 .child(match last_error {
911 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
912 ThreadError::MaxMonthlySpendReached => {
913 self.render_max_monthly_spend_reached_error(cx)
914 }
915 ThreadError::Message(error_message) => {
916 self.render_error_message(&error_message, cx)
917 }
918 })
919 .into_any(),
920 )
921 }
922
923 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
924 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.";
925
926 v_flex()
927 .gap_0p5()
928 .child(
929 h_flex()
930 .gap_1p5()
931 .items_center()
932 .child(Icon::new(IconName::XCircle).color(Color::Error))
933 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
934 )
935 .child(
936 div()
937 .id("error-message")
938 .max_h_24()
939 .overflow_y_scroll()
940 .child(Label::new(ERROR_MESSAGE)),
941 )
942 .child(
943 h_flex()
944 .justify_end()
945 .mt_1()
946 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
947 |this, _, _, cx| {
948 this.thread.update(cx, |this, _cx| {
949 this.clear_last_error();
950 });
951
952 cx.open_url(&zed_urls::account_url(cx));
953 cx.notify();
954 },
955 )))
956 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
957 |this, _, _, cx| {
958 this.thread.update(cx, |this, _cx| {
959 this.clear_last_error();
960 });
961
962 cx.notify();
963 },
964 ))),
965 )
966 .into_any()
967 }
968
969 fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
970 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
971
972 v_flex()
973 .gap_0p5()
974 .child(
975 h_flex()
976 .gap_1p5()
977 .items_center()
978 .child(Icon::new(IconName::XCircle).color(Color::Error))
979 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
980 )
981 .child(
982 div()
983 .id("error-message")
984 .max_h_24()
985 .overflow_y_scroll()
986 .child(Label::new(ERROR_MESSAGE)),
987 )
988 .child(
989 h_flex()
990 .justify_end()
991 .mt_1()
992 .child(
993 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
994 cx.listener(|this, _, _, cx| {
995 this.thread.update(cx, |this, _cx| {
996 this.clear_last_error();
997 });
998
999 cx.open_url(&zed_urls::account_url(cx));
1000 cx.notify();
1001 }),
1002 ),
1003 )
1004 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1005 |this, _, _, cx| {
1006 this.thread.update(cx, |this, _cx| {
1007 this.clear_last_error();
1008 });
1009
1010 cx.notify();
1011 },
1012 ))),
1013 )
1014 .into_any()
1015 }
1016
1017 fn render_error_message(
1018 &self,
1019 error_message: &SharedString,
1020 cx: &mut Context<Self>,
1021 ) -> AnyElement {
1022 v_flex()
1023 .gap_0p5()
1024 .child(
1025 h_flex()
1026 .gap_1p5()
1027 .items_center()
1028 .child(Icon::new(IconName::XCircle).color(Color::Error))
1029 .child(
1030 Label::new("Error interacting with language model")
1031 .weight(FontWeight::MEDIUM),
1032 ),
1033 )
1034 .child(
1035 div()
1036 .id("error-message")
1037 .max_h_32()
1038 .overflow_y_scroll()
1039 .child(Label::new(error_message.clone())),
1040 )
1041 .child(
1042 h_flex()
1043 .justify_end()
1044 .mt_1()
1045 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1046 |this, _, _, cx| {
1047 this.thread.update(cx, |this, _cx| {
1048 this.clear_last_error();
1049 });
1050
1051 cx.notify();
1052 },
1053 ))),
1054 )
1055 .into_any()
1056 }
1057
1058 fn key_context(&self) -> KeyContext {
1059 let mut key_context = KeyContext::new_with_defaults();
1060 key_context.add("AssistantPanel2");
1061 if matches!(self.active_view, ActiveView::PromptEditor) {
1062 key_context.add("prompt_editor");
1063 }
1064 key_context
1065 }
1066}
1067
1068impl Render for AssistantPanel {
1069 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1070 v_flex()
1071 .key_context(self.key_context())
1072 .justify_between()
1073 .size_full()
1074 .on_action(cx.listener(Self::cancel))
1075 .on_action(cx.listener(|this, _: &NewThread, window, cx| {
1076 this.new_thread(window, cx);
1077 }))
1078 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
1079 this.open_history(window, cx);
1080 }))
1081 .on_action(cx.listener(Self::open_active_thread_as_markdown))
1082 .on_action(cx.listener(Self::deploy_prompt_library))
1083 .child(self.render_toolbar(cx))
1084 .map(|parent| match self.active_view {
1085 ActiveView::Thread => parent
1086 .child(self.render_active_thread_or_empty_state(window, cx))
1087 .child(h_flex().child(self.message_editor.clone()))
1088 .children(self.render_last_error(cx)),
1089 ActiveView::History => parent.child(self.history.clone()),
1090 ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
1091 ActiveView::Configuration => parent.children(self.configuration.clone()),
1092 })
1093 }
1094}
1095
1096struct PromptLibraryInlineAssist {
1097 workspace: WeakEntity<Workspace>,
1098}
1099
1100impl PromptLibraryInlineAssist {
1101 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
1102 Self { workspace }
1103 }
1104}
1105
1106impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1107 fn assist(
1108 &self,
1109 prompt_editor: &Entity<Editor>,
1110 _initial_prompt: Option<String>,
1111 window: &mut Window,
1112 cx: &mut Context<PromptLibrary>,
1113 ) {
1114 InlineAssistant::update_global(cx, |assistant, cx| {
1115 assistant.assist(&prompt_editor, self.workspace.clone(), None, window, cx)
1116 })
1117 }
1118
1119 fn focus_assistant_panel(
1120 &self,
1121 workspace: &mut Workspace,
1122 window: &mut Window,
1123 cx: &mut Context<Workspace>,
1124 ) -> bool {
1125 workspace
1126 .focus_panel::<AssistantPanel>(window, cx)
1127 .is_some()
1128 }
1129}
1130
1131pub struct ConcreteAssistantPanelDelegate;
1132
1133impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1134 fn active_context_editor(
1135 &self,
1136 workspace: &mut Workspace,
1137 _window: &mut Window,
1138 cx: &mut Context<Workspace>,
1139 ) -> Option<Entity<ContextEditor>> {
1140 let panel = workspace.panel::<AssistantPanel>(cx)?;
1141 panel.update(cx, |panel, _cx| panel.context_editor.clone())
1142 }
1143
1144 fn open_saved_context(
1145 &self,
1146 workspace: &mut Workspace,
1147 path: std::path::PathBuf,
1148 window: &mut Window,
1149 cx: &mut Context<Workspace>,
1150 ) -> Task<Result<()>> {
1151 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1152 return Task::ready(Err(anyhow!("Assistant panel not found")));
1153 };
1154
1155 panel.update(cx, |panel, cx| {
1156 panel.open_saved_prompt_editor(path, window, cx)
1157 })
1158 }
1159
1160 fn open_remote_context(
1161 &self,
1162 _workspace: &mut Workspace,
1163 _context_id: assistant_context_editor::ContextId,
1164 _window: &mut Window,
1165 _cx: &mut Context<Workspace>,
1166 ) -> Task<Result<Entity<ContextEditor>>> {
1167 Task::ready(Err(anyhow!("opening remote context not implemented")))
1168 }
1169
1170 fn quote_selection(
1171 &self,
1172 _workspace: &mut Workspace,
1173 _creases: Vec<(String, String)>,
1174 _window: &mut Window,
1175 _cx: &mut Context<Workspace>,
1176 ) {
1177 }
1178}