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