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