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