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