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