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