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