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