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