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