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 let fs = self.fs.clone();
486
487 self.active_view = ActiveView::Configuration;
488 self.configuration =
489 Some(cx.new(|cx| {
490 AssistantConfiguration::new(fs, context_server_manager, tools, window, cx)
491 }));
492
493 if let Some(configuration) = self.configuration.as_ref() {
494 self.configuration_subscription = Some(cx.subscribe_in(
495 configuration,
496 window,
497 Self::handle_assistant_configuration_event,
498 ));
499
500 configuration.focus_handle(cx).focus(window);
501 }
502 }
503
504 pub(crate) fn open_active_thread_as_markdown(
505 &mut self,
506 _: &OpenActiveThreadAsMarkdown,
507 window: &mut Window,
508 cx: &mut Context<Self>,
509 ) {
510 let Some(workspace) = self
511 .workspace
512 .upgrade()
513 .ok_or_else(|| anyhow!("workspace dropped"))
514 .log_err()
515 else {
516 return;
517 };
518
519 let markdown_language_task = workspace
520 .read(cx)
521 .app_state()
522 .languages
523 .language_for_name("Markdown");
524 let thread = self.active_thread(cx);
525 cx.spawn_in(window, async move |_this, cx| {
526 let markdown_language = markdown_language_task.await?;
527
528 workspace.update_in(cx, |workspace, window, cx| {
529 let thread = thread.read(cx);
530 let markdown = thread.to_markdown(cx)?;
531 let thread_summary = thread
532 .summary()
533 .map(|summary| summary.to_string())
534 .unwrap_or_else(|| "Thread".to_string());
535
536 let project = workspace.project().clone();
537 let buffer = project.update(cx, |project, cx| {
538 project.create_local_buffer(&markdown, Some(markdown_language), cx)
539 });
540 let buffer = cx.new(|cx| {
541 MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
542 });
543
544 workspace.add_item_to_active_pane(
545 Box::new(cx.new(|cx| {
546 let mut editor =
547 Editor::for_multibuffer(buffer, Some(project.clone()), window, cx);
548 editor.set_breadcrumb_header(thread_summary);
549 editor
550 })),
551 None,
552 true,
553 window,
554 cx,
555 );
556
557 anyhow::Ok(())
558 })
559 })
560 .detach_and_log_err(cx);
561 }
562
563 fn handle_assistant_configuration_event(
564 &mut self,
565 _entity: &Entity<AssistantConfiguration>,
566 event: &AssistantConfigurationEvent,
567 window: &mut Window,
568 cx: &mut Context<Self>,
569 ) {
570 match event {
571 AssistantConfigurationEvent::NewThread(provider) => {
572 if LanguageModelRegistry::read_global(cx)
573 .active_provider()
574 .map_or(true, |active_provider| {
575 active_provider.id() != provider.id()
576 })
577 {
578 if let Some(model) = provider.default_model(cx) {
579 update_settings_file::<AssistantSettings>(
580 self.fs.clone(),
581 cx,
582 move |settings, _| settings.set_model(model),
583 );
584 }
585 }
586
587 self.new_thread(&NewThread::default(), window, cx);
588 }
589 }
590 }
591
592 pub(crate) fn active_thread(&self, cx: &App) -> Entity<Thread> {
593 self.thread.read(cx).thread().clone()
594 }
595
596 pub(crate) fn delete_thread(&mut self, thread_id: &ThreadId, cx: &mut Context<Self>) {
597 self.thread_store
598 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
599 .detach_and_log_err(cx);
600 }
601
602 pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
603 self.context_editor.clone()
604 }
605
606 pub(crate) fn delete_context(&mut self, path: PathBuf, cx: &mut Context<Self>) {
607 self.context_store
608 .update(cx, |this, cx| this.delete_local_context(path, cx))
609 .detach_and_log_err(cx);
610 }
611}
612
613impl Focusable for AssistantPanel {
614 fn focus_handle(&self, cx: &App) -> FocusHandle {
615 match self.active_view {
616 ActiveView::Thread => self.message_editor.focus_handle(cx),
617 ActiveView::History => self.history.focus_handle(cx),
618 ActiveView::PromptEditor => {
619 if let Some(context_editor) = self.context_editor.as_ref() {
620 context_editor.focus_handle(cx)
621 } else {
622 cx.focus_handle()
623 }
624 }
625 ActiveView::Configuration => {
626 if let Some(configuration) = self.configuration.as_ref() {
627 configuration.focus_handle(cx)
628 } else {
629 cx.focus_handle()
630 }
631 }
632 }
633 }
634}
635
636impl EventEmitter<PanelEvent> for AssistantPanel {}
637
638impl Panel for AssistantPanel {
639 fn persistent_name() -> &'static str {
640 "AgentPanel"
641 }
642
643 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
644 match AssistantSettings::get_global(cx).dock {
645 AssistantDockPosition::Left => DockPosition::Left,
646 AssistantDockPosition::Bottom => DockPosition::Bottom,
647 AssistantDockPosition::Right => DockPosition::Right,
648 }
649 }
650
651 fn position_is_valid(&self, _: DockPosition) -> bool {
652 true
653 }
654
655 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
656 settings::update_settings_file::<AssistantSettings>(
657 self.fs.clone(),
658 cx,
659 move |settings, _| {
660 let dock = match position {
661 DockPosition::Left => AssistantDockPosition::Left,
662 DockPosition::Bottom => AssistantDockPosition::Bottom,
663 DockPosition::Right => AssistantDockPosition::Right,
664 };
665 settings.set_dock(dock);
666 },
667 );
668 }
669
670 fn size(&self, window: &Window, cx: &App) -> Pixels {
671 let settings = AssistantSettings::get_global(cx);
672 match self.position(window, cx) {
673 DockPosition::Left | DockPosition::Right => {
674 self.width.unwrap_or(settings.default_width)
675 }
676 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
677 }
678 }
679
680 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
681 match self.position(window, cx) {
682 DockPosition::Left | DockPosition::Right => self.width = size,
683 DockPosition::Bottom => self.height = size,
684 }
685 cx.notify();
686 }
687
688 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
689
690 fn remote_id() -> Option<proto::PanelId> {
691 Some(proto::PanelId::AssistantPanel)
692 }
693
694 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
695 (self.enabled(cx) && AssistantSettings::get_global(cx).button)
696 .then_some(IconName::ZedAssistant)
697 }
698
699 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
700 Some("Agent Panel")
701 }
702
703 fn toggle_action(&self) -> Box<dyn Action> {
704 Box::new(ToggleFocus)
705 }
706
707 fn activation_priority(&self) -> u32 {
708 3
709 }
710
711 fn enabled(&self, cx: &App) -> bool {
712 AssistantSettings::get_global(cx).enabled
713 }
714}
715
716impl AssistantPanel {
717 fn render_toolbar(&self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
718 let thread = self.thread.read(cx);
719 let is_empty = thread.is_empty();
720
721 let thread_id = thread.thread().read(cx).id().clone();
722 let focus_handle = self.focus_handle(cx);
723
724 let title = match self.active_view {
725 ActiveView::Thread => {
726 if is_empty {
727 thread.summary_or_default(cx)
728 } else {
729 thread
730 .summary(cx)
731 .unwrap_or_else(|| SharedString::from("Loading Summary…"))
732 }
733 }
734 ActiveView::PromptEditor => self
735 .context_editor
736 .as_ref()
737 .map(|context_editor| {
738 SharedString::from(context_editor.read(cx).title(cx).to_string())
739 })
740 .unwrap_or_else(|| SharedString::from("Loading Summary…")),
741 ActiveView::History => "History".into(),
742 ActiveView::Configuration => "Settings".into(),
743 };
744
745 h_flex()
746 .id("assistant-toolbar")
747 .h(Tab::container_height(cx))
748 .flex_none()
749 .justify_between()
750 .gap(DynamicSpacing::Base08.rems(cx))
751 .bg(cx.theme().colors().tab_bar_background)
752 .border_b_1()
753 .border_color(cx.theme().colors().border)
754 .child(
755 div()
756 .id("title")
757 .overflow_x_scroll()
758 .px(DynamicSpacing::Base08.rems(cx))
759 .child(Label::new(title).truncate()),
760 )
761 .child(
762 h_flex()
763 .h_full()
764 .pl_2()
765 .gap_2()
766 .bg(cx.theme().colors().tab_bar_background)
767 .children(if matches!(self.active_view, ActiveView::PromptEditor) {
768 self.context_editor
769 .as_ref()
770 .and_then(|editor| render_remaining_tokens(editor, cx))
771 } else {
772 None
773 })
774 .child(
775 h_flex()
776 .h_full()
777 .px(DynamicSpacing::Base08.rems(cx))
778 .border_l_1()
779 .border_color(cx.theme().colors().border)
780 .gap(DynamicSpacing::Base02.rems(cx))
781 .child(
782 IconButton::new("new", IconName::Plus)
783 .icon_size(IconSize::Small)
784 .style(ButtonStyle::Subtle)
785 .tooltip(move |window, cx| {
786 Tooltip::for_action_in(
787 "New Thread",
788 &NewThread::default(),
789 &focus_handle,
790 window,
791 cx,
792 )
793 })
794 .on_click(move |_event, window, cx| {
795 window.dispatch_action(
796 NewThread::default().boxed_clone(),
797 cx,
798 );
799 }),
800 )
801 .child(
802 PopoverMenu::new("assistant-menu")
803 .trigger_with_tooltip(
804 IconButton::new("new", IconName::Ellipsis)
805 .icon_size(IconSize::Small)
806 .style(ButtonStyle::Subtle),
807 Tooltip::text("Toggle Agent Menu"),
808 )
809 .anchor(Corner::TopRight)
810 .with_handle(self.assistant_dropdown_menu_handle.clone())
811 .menu(move |window, cx| {
812 Some(ContextMenu::build(
813 window,
814 cx,
815 |menu, _window, _cx| {
816 menu.action(
817 "New Thread",
818 Box::new(NewThread {
819 from_thread_id: None,
820 }),
821 )
822 .action(
823 "New Prompt Editor",
824 NewPromptEditor.boxed_clone(),
825 )
826 .when(!is_empty, |menu| {
827 menu.action(
828 "Continue in New Thread",
829 Box::new(NewThread {
830 from_thread_id: Some(thread_id.clone()),
831 }),
832 )
833 })
834 .separator()
835 .action("History", OpenHistory.boxed_clone())
836 .action("Settings", OpenConfiguration.boxed_clone())
837 },
838 ))
839 }),
840 ),
841 ),
842 )
843 }
844
845 fn render_active_thread_or_empty_state(
846 &self,
847 window: &mut Window,
848 cx: &mut Context<Self>,
849 ) -> AnyElement {
850 if self.thread.read(cx).is_empty() {
851 return self
852 .render_thread_empty_state(window, cx)
853 .into_any_element();
854 }
855
856 self.thread.clone().into_any_element()
857 }
858
859 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
860 let Some(provider) = LanguageModelRegistry::read_global(cx).active_provider() else {
861 return Some(ConfigurationError::NoProvider);
862 };
863
864 if !provider.is_authenticated(cx) {
865 return Some(ConfigurationError::ProviderNotAuthenticated);
866 }
867
868 if provider.must_accept_terms(cx) {
869 return Some(ConfigurationError::ProviderPendingTermsAcceptance(provider));
870 }
871
872 None
873 }
874
875 fn render_thread_empty_state(
876 &self,
877 window: &mut Window,
878 cx: &mut Context<Self>,
879 ) -> impl IntoElement {
880 let recent_history = self
881 .history_store
882 .update(cx, |this, cx| this.recent_entries(6, cx));
883
884 let configuration_error = self.configuration_error(cx);
885 let no_error = configuration_error.is_none();
886 let focus_handle = self.focus_handle(cx);
887
888 v_flex()
889 .size_full()
890 .when(recent_history.is_empty(), |this| {
891 let configuration_error_ref = &configuration_error;
892 this.child(
893 v_flex()
894 .size_full()
895 .max_w_80()
896 .mx_auto()
897 .justify_center()
898 .items_center()
899 .gap_1()
900 .child(
901 h_flex().child(
902 Headline::new("Welcome to the Agent Panel")
903 ),
904 )
905 .when(no_error, |parent| {
906 parent
907 .child(
908 h_flex().child(
909 Label::new("Ask and build anything.")
910 .color(Color::Muted)
911 .mb_2p5(),
912 ),
913 )
914 .child(
915 Button::new("new-thread", "Start New Thread")
916 .icon(IconName::Plus)
917 .icon_position(IconPosition::Start)
918 .icon_size(IconSize::Small)
919 .icon_color(Color::Muted)
920 .full_width()
921 .key_binding(KeyBinding::for_action_in(
922 &NewThread::default(),
923 &focus_handle,
924 window,
925 cx,
926 ))
927 .on_click(|_event, window, cx| {
928 window.dispatch_action(NewThread::default().boxed_clone(), cx)
929 }),
930 )
931 .child(
932 Button::new("context", "Add Context")
933 .icon(IconName::FileCode)
934 .icon_position(IconPosition::Start)
935 .icon_size(IconSize::Small)
936 .icon_color(Color::Muted)
937 .full_width()
938 .key_binding(KeyBinding::for_action_in(
939 &ToggleContextPicker,
940 &focus_handle,
941 window,
942 cx,
943 ))
944 .on_click(|_event, window, cx| {
945 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
946 }),
947 )
948 .child(
949 Button::new("mode", "Switch Model")
950 .icon(IconName::DatabaseZap)
951 .icon_position(IconPosition::Start)
952 .icon_size(IconSize::Small)
953 .icon_color(Color::Muted)
954 .full_width()
955 .key_binding(KeyBinding::for_action_in(
956 &ToggleModelSelector,
957 &focus_handle,
958 window,
959 cx,
960 ))
961 .on_click(|_event, window, cx| {
962 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
963 }),
964 )
965 .child(
966 Button::new("settings", "View Settings")
967 .icon(IconName::Settings)
968 .icon_position(IconPosition::Start)
969 .icon_size(IconSize::Small)
970 .icon_color(Color::Muted)
971 .full_width()
972 .key_binding(KeyBinding::for_action_in(
973 &OpenConfiguration,
974 &focus_handle,
975 window,
976 cx,
977 ))
978 .on_click(|_event, window, cx| {
979 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
980 }),
981 )
982 })
983 .map(|parent| {
984 match configuration_error_ref {
985 Some(ConfigurationError::ProviderNotAuthenticated)
986 | Some(ConfigurationError::NoProvider) => {
987 parent
988 .child(
989 h_flex().child(
990 Label::new("To start using the agent, configure at least one LLM provider.")
991 .color(Color::Muted)
992 .mb_2p5()
993 )
994 )
995 .child(
996 Button::new("settings", "Configure a Provider")
997 .icon(IconName::Settings)
998 .icon_position(IconPosition::Start)
999 .icon_size(IconSize::Small)
1000 .icon_color(Color::Muted)
1001 .full_width()
1002 .key_binding(KeyBinding::for_action_in(
1003 &OpenConfiguration,
1004 &focus_handle,
1005 window,
1006 cx,
1007 ))
1008 .on_click(|_event, window, cx| {
1009 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
1010 }),
1011 )
1012 }
1013 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1014 parent.children(
1015 provider.render_accept_terms(
1016 LanguageModelProviderTosView::ThreadFreshStart,
1017 cx,
1018 ),
1019 )
1020 }
1021 None => parent,
1022 }
1023 })
1024 )
1025 })
1026 .when(!recent_history.is_empty(), |parent| {
1027 let focus_handle = focus_handle.clone();
1028 let configuration_error_ref = &configuration_error;
1029
1030 parent
1031 .p_1p5()
1032 .justify_end()
1033 .gap_1()
1034 .child(
1035 h_flex()
1036 .pl_1p5()
1037 .pb_1()
1038 .w_full()
1039 .justify_between()
1040 .border_b_1()
1041 .border_color(cx.theme().colors().border_variant)
1042 .child(
1043 Label::new("Past Interactions")
1044 .size(LabelSize::Small)
1045 .color(Color::Muted),
1046 )
1047 .child(
1048 Button::new("view-history", "View All")
1049 .style(ButtonStyle::Subtle)
1050 .label_size(LabelSize::Small)
1051 .key_binding(
1052 KeyBinding::for_action_in(
1053 &OpenHistory,
1054 &self.focus_handle(cx),
1055 window,
1056 cx,
1057 ).map(|kb| kb.size(rems_from_px(12.))),
1058 )
1059 .on_click(move |_event, window, cx| {
1060 window.dispatch_action(OpenHistory.boxed_clone(), cx);
1061 }),
1062 ),
1063 )
1064 .child(
1065 v_flex()
1066 .gap_1()
1067 .children(
1068 recent_history.into_iter().map(|entry| {
1069 // TODO: Add keyboard navigation.
1070 match entry {
1071 HistoryEntry::Thread(thread) => {
1072 PastThread::new(thread, cx.entity().downgrade(), false)
1073 .into_any_element()
1074 }
1075 HistoryEntry::Context(context) => {
1076 PastContext::new(context, cx.entity().downgrade(), false)
1077 .into_any_element()
1078 }
1079 }
1080 }),
1081 )
1082 )
1083 .map(|parent| {
1084 match configuration_error_ref {
1085 Some(ConfigurationError::ProviderNotAuthenticated)
1086 | Some(ConfigurationError::NoProvider) => {
1087 parent
1088 .child(
1089 Banner::new()
1090 .severity(ui::Severity::Warning)
1091 .children(
1092 Label::new(
1093 "Configure at least one LLM provider to start using the panel.",
1094 )
1095 .size(LabelSize::Small),
1096 )
1097 .action_slot(
1098 Button::new("settings", "Configure Provider")
1099 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
1100 .label_size(LabelSize::Small)
1101 .key_binding(
1102 KeyBinding::for_action_in(
1103 &OpenConfiguration,
1104 &focus_handle,
1105 window,
1106 cx,
1107 )
1108 .map(|kb| kb.size(rems_from_px(12.))),
1109 )
1110 .on_click(|_event, window, cx| {
1111 window.dispatch_action(
1112 OpenConfiguration.boxed_clone(),
1113 cx,
1114 )
1115 }),
1116 ),
1117 )
1118 }
1119 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
1120 parent
1121 .child(
1122 Banner::new()
1123 .severity(ui::Severity::Warning)
1124 .children(
1125 h_flex()
1126 .w_full()
1127 .children(
1128 provider.render_accept_terms(
1129 LanguageModelProviderTosView::ThreadtEmptyState,
1130 cx,
1131 ),
1132 ),
1133 ),
1134 )
1135 }
1136 None => parent,
1137 }
1138 })
1139 })
1140 }
1141
1142 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
1143 let last_error = self.thread.read(cx).last_error()?;
1144
1145 Some(
1146 div()
1147 .absolute()
1148 .right_3()
1149 .bottom_12()
1150 .max_w_96()
1151 .py_2()
1152 .px_3()
1153 .elevation_2(cx)
1154 .occlude()
1155 .child(match last_error {
1156 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
1157 ThreadError::MaxMonthlySpendReached => {
1158 self.render_max_monthly_spend_reached_error(cx)
1159 }
1160 ThreadError::Message { header, message } => {
1161 self.render_error_message(header, message, cx)
1162 }
1163 })
1164 .into_any(),
1165 )
1166 }
1167
1168 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
1169 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.";
1170
1171 v_flex()
1172 .gap_0p5()
1173 .child(
1174 h_flex()
1175 .gap_1p5()
1176 .items_center()
1177 .child(Icon::new(IconName::XCircle).color(Color::Error))
1178 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
1179 )
1180 .child(
1181 div()
1182 .id("error-message")
1183 .max_h_24()
1184 .overflow_y_scroll()
1185 .child(Label::new(ERROR_MESSAGE)),
1186 )
1187 .child(
1188 h_flex()
1189 .justify_end()
1190 .mt_1()
1191 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
1192 |this, _, _, cx| {
1193 this.thread.update(cx, |this, _cx| {
1194 this.clear_last_error();
1195 });
1196
1197 cx.open_url(&zed_urls::account_url(cx));
1198 cx.notify();
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_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
1215 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
1216
1217 v_flex()
1218 .gap_0p5()
1219 .child(
1220 h_flex()
1221 .gap_1p5()
1222 .items_center()
1223 .child(Icon::new(IconName::XCircle).color(Color::Error))
1224 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
1225 )
1226 .child(
1227 div()
1228 .id("error-message")
1229 .max_h_24()
1230 .overflow_y_scroll()
1231 .child(Label::new(ERROR_MESSAGE)),
1232 )
1233 .child(
1234 h_flex()
1235 .justify_end()
1236 .mt_1()
1237 .child(
1238 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
1239 cx.listener(|this, _, _, cx| {
1240 this.thread.update(cx, |this, _cx| {
1241 this.clear_last_error();
1242 });
1243
1244 cx.open_url(&zed_urls::account_url(cx));
1245 cx.notify();
1246 }),
1247 ),
1248 )
1249 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1250 |this, _, _, cx| {
1251 this.thread.update(cx, |this, _cx| {
1252 this.clear_last_error();
1253 });
1254
1255 cx.notify();
1256 },
1257 ))),
1258 )
1259 .into_any()
1260 }
1261
1262 fn render_error_message(
1263 &self,
1264 header: SharedString,
1265 message: SharedString,
1266 cx: &mut Context<Self>,
1267 ) -> AnyElement {
1268 v_flex()
1269 .gap_0p5()
1270 .child(
1271 h_flex()
1272 .gap_1p5()
1273 .items_center()
1274 .child(Icon::new(IconName::XCircle).color(Color::Error))
1275 .child(Label::new(header).weight(FontWeight::MEDIUM)),
1276 )
1277 .child(
1278 div()
1279 .id("error-message")
1280 .max_h_32()
1281 .overflow_y_scroll()
1282 .child(Label::new(message)),
1283 )
1284 .child(
1285 h_flex()
1286 .justify_end()
1287 .mt_1()
1288 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
1289 |this, _, _, cx| {
1290 this.thread.update(cx, |this, _cx| {
1291 this.clear_last_error();
1292 });
1293
1294 cx.notify();
1295 },
1296 ))),
1297 )
1298 .into_any()
1299 }
1300
1301 fn key_context(&self) -> KeyContext {
1302 let mut key_context = KeyContext::new_with_defaults();
1303 key_context.add("AgentPanel");
1304 if matches!(self.active_view, ActiveView::PromptEditor) {
1305 key_context.add("prompt_editor");
1306 }
1307 key_context
1308 }
1309}
1310
1311impl Render for AssistantPanel {
1312 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1313 v_flex()
1314 .key_context(self.key_context())
1315 .justify_between()
1316 .size_full()
1317 .on_action(cx.listener(Self::cancel))
1318 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
1319 this.new_thread(action, window, cx);
1320 }))
1321 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
1322 this.open_history(window, cx);
1323 }))
1324 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
1325 this.open_configuration(window, cx);
1326 }))
1327 .on_action(cx.listener(Self::open_active_thread_as_markdown))
1328 .on_action(cx.listener(Self::deploy_prompt_library))
1329 .on_action(cx.listener(Self::open_agent_diff))
1330 .child(self.render_toolbar(window, cx))
1331 .map(|parent| match self.active_view {
1332 ActiveView::Thread => parent
1333 .child(self.render_active_thread_or_empty_state(window, cx))
1334 .child(h_flex().child(self.message_editor.clone()))
1335 .children(self.render_last_error(cx)),
1336 ActiveView::History => parent.child(self.history.clone()),
1337 ActiveView::PromptEditor => parent.children(self.context_editor.clone()),
1338 ActiveView::Configuration => parent.children(self.configuration.clone()),
1339 })
1340 }
1341}
1342
1343struct PromptLibraryInlineAssist {
1344 workspace: WeakEntity<Workspace>,
1345}
1346
1347impl PromptLibraryInlineAssist {
1348 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
1349 Self { workspace }
1350 }
1351}
1352
1353impl prompt_library::InlineAssistDelegate for PromptLibraryInlineAssist {
1354 fn assist(
1355 &self,
1356 prompt_editor: &Entity<Editor>,
1357 _initial_prompt: Option<String>,
1358 window: &mut Window,
1359 cx: &mut Context<PromptLibrary>,
1360 ) {
1361 InlineAssistant::update_global(cx, |assistant, cx| {
1362 assistant.assist(&prompt_editor, self.workspace.clone(), None, window, cx)
1363 })
1364 }
1365
1366 fn focus_assistant_panel(
1367 &self,
1368 workspace: &mut Workspace,
1369 window: &mut Window,
1370 cx: &mut Context<Workspace>,
1371 ) -> bool {
1372 workspace
1373 .focus_panel::<AssistantPanel>(window, cx)
1374 .is_some()
1375 }
1376}
1377
1378pub struct ConcreteAssistantPanelDelegate;
1379
1380impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
1381 fn active_context_editor(
1382 &self,
1383 workspace: &mut Workspace,
1384 _window: &mut Window,
1385 cx: &mut Context<Workspace>,
1386 ) -> Option<Entity<ContextEditor>> {
1387 let panel = workspace.panel::<AssistantPanel>(cx)?;
1388 panel.update(cx, |panel, _cx| panel.context_editor.clone())
1389 }
1390
1391 fn open_saved_context(
1392 &self,
1393 workspace: &mut Workspace,
1394 path: std::path::PathBuf,
1395 window: &mut Window,
1396 cx: &mut Context<Workspace>,
1397 ) -> Task<Result<()>> {
1398 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1399 return Task::ready(Err(anyhow!("Agent panel not found")));
1400 };
1401
1402 panel.update(cx, |panel, cx| {
1403 panel.open_saved_prompt_editor(path, window, cx)
1404 })
1405 }
1406
1407 fn open_remote_context(
1408 &self,
1409 _workspace: &mut Workspace,
1410 _context_id: assistant_context_editor::ContextId,
1411 _window: &mut Window,
1412 _cx: &mut Context<Workspace>,
1413 ) -> Task<Result<Entity<ContextEditor>>> {
1414 Task::ready(Err(anyhow!("opening remote context not implemented")))
1415 }
1416
1417 fn quote_selection(
1418 &self,
1419 _workspace: &mut Workspace,
1420 _creases: Vec<(String, String)>,
1421 _window: &mut Window,
1422 _cx: &mut Context<Workspace>,
1423 ) {
1424 }
1425}