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