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