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