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