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