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