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