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