1use std::ops::Range;
2use std::path::Path;
3use std::sync::Arc;
4use std::time::Duration;
5
6use db::kvp::KEY_VALUE_STORE;
7use markdown::Markdown;
8use serde::{Deserialize, Serialize};
9
10use anyhow::{Result, anyhow};
11use assistant_context_editor::{
12 AssistantContext, AssistantPanelDelegate, ConfigurationError, ContextEditor, ContextEvent,
13 SlashCommandCompletionProvider, humanize_token_count, make_lsp_adapter_delegate,
14 render_remaining_tokens,
15};
16use assistant_settings::{AssistantDockPosition, AssistantSettings};
17use assistant_slash_command::SlashCommandWorkingSet;
18use assistant_tool::ToolWorkingSet;
19
20use client::{UserStore, zed_urls};
21use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
22use fs::Fs;
23use gpui::{
24 Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
25 Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight,
26 KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
27 linear_gradient, prelude::*, pulsating_between,
28};
29use language::LanguageRegistry;
30use language_model::{
31 LanguageModelProviderTosView, LanguageModelRegistry, RequestUsage, ZED_CLOUD_PROVIDER_ID,
32};
33use language_model_selector::ToggleModelSelector;
34use project::{Project, ProjectPath, Worktree};
35use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
36use proto::Plan;
37use rules_library::{RulesLibrary, open_rules_library};
38use search::{BufferSearchBar, buffer_search};
39use settings::{Settings, update_settings_file};
40use theme::ThemeSettings;
41use time::UtcOffset;
42use ui::utils::WithRemSize;
43use ui::{
44 Banner, CheckboxWithLabel, ContextMenu, KeyBinding, PopoverMenu, PopoverMenuHandle,
45 ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
46};
47use util::{ResultExt as _, maybe};
48use workspace::dock::{DockPosition, Panel, PanelEvent};
49use workspace::{CollaboratorId, DraggedSelection, DraggedTab, ToolbarItemView, Workspace};
50use zed_actions::agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding};
51use zed_actions::assistant::{OpenRulesLibrary, ToggleFocus};
52use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
53use zed_llm_client::UsageLimit;
54
55use crate::active_thread::{self, ActiveThread, ActiveThreadEvent};
56use crate::agent_diff::AgentDiff;
57use crate::assistant_configuration::{AssistantConfiguration, AssistantConfigurationEvent};
58use crate::history_store::{HistoryEntry, HistoryStore, RecentEntry};
59use crate::message_editor::{MessageEditor, MessageEditorEvent};
60use crate::thread::{Thread, ThreadError, ThreadId, TokenUsageRatio};
61use crate::thread_history::{EntryTimeFormat, PastContext, PastThread, ThreadHistory};
62use crate::thread_store::ThreadStore;
63use crate::ui::AgentOnboardingModal;
64use crate::{
65 AddContextServer, AgentDiffPane, ContextStore, DeleteRecentlyOpenThread, ExpandMessageEditor,
66 Follow, InlineAssistant, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
67 OpenHistory, ResetTrialUpsell, TextThreadStore, ThreadEvent, ToggleContextPicker,
68 ToggleNavigationMenu, ToggleOptionsMenu,
69};
70
71const AGENT_PANEL_KEY: &str = "agent_panel";
72
73#[derive(Serialize, Deserialize)]
74struct SerializedAssistantPanel {
75 width: Option<Pixels>,
76}
77
78pub fn init(cx: &mut App) {
79 cx.observe_new(
80 |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
81 workspace
82 .register_action(|workspace, action: &NewThread, window, cx| {
83 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
84 panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
85 workspace.focus_panel::<AssistantPanel>(window, cx);
86 }
87 })
88 .register_action(|workspace, _: &OpenHistory, window, cx| {
89 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
90 workspace.focus_panel::<AssistantPanel>(window, cx);
91 panel.update(cx, |panel, cx| panel.open_history(window, cx));
92 }
93 })
94 .register_action(|workspace, _: &OpenConfiguration, window, cx| {
95 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
96 workspace.focus_panel::<AssistantPanel>(window, cx);
97 panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
98 }
99 })
100 .register_action(|workspace, _: &NewTextThread, window, cx| {
101 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
102 workspace.focus_panel::<AssistantPanel>(window, cx);
103 panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
104 }
105 })
106 .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
107 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
108 workspace.focus_panel::<AssistantPanel>(window, cx);
109 panel.update(cx, |panel, cx| {
110 panel.deploy_rules_library(action, window, cx)
111 });
112 }
113 })
114 .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
115 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
116 workspace.focus_panel::<AssistantPanel>(window, cx);
117 let thread = panel.read(cx).thread.read(cx).thread().clone();
118 AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
119 }
120 })
121 .register_action(|workspace, _: &Follow, window, cx| {
122 workspace.follow(CollaboratorId::Agent, window, cx);
123 })
124 .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
125 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
126 workspace.focus_panel::<AssistantPanel>(window, cx);
127 panel.update(cx, |panel, cx| {
128 panel.message_editor.update(cx, |editor, cx| {
129 editor.expand_message_editor(&ExpandMessageEditor, window, cx);
130 });
131 });
132 }
133 })
134 .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
135 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
136 workspace.focus_panel::<AssistantPanel>(window, cx);
137 panel.update(cx, |panel, cx| {
138 panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
139 });
140 }
141 })
142 .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
143 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
144 workspace.focus_panel::<AssistantPanel>(window, cx);
145 panel.update(cx, |panel, cx| {
146 panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
147 });
148 }
149 })
150 .register_action(|workspace, _: &OpenOnboardingModal, window, cx| {
151 AgentOnboardingModal::toggle(workspace, window, cx)
152 })
153 .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
154 window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
155 window.refresh();
156 })
157 .register_action(|_workspace, _: &ResetTrialUpsell, _window, cx| {
158 set_trial_upsell_dismissed(false, cx);
159 });
160 },
161 )
162 .detach();
163}
164
165enum ActiveView {
166 Thread {
167 change_title_editor: Entity<Editor>,
168 thread: WeakEntity<Thread>,
169 _subscriptions: Vec<gpui::Subscription>,
170 },
171 PromptEditor {
172 context_editor: Entity<ContextEditor>,
173 title_editor: Entity<Editor>,
174 buffer_search_bar: Entity<BufferSearchBar>,
175 _subscriptions: Vec<gpui::Subscription>,
176 },
177 History,
178 Configuration,
179}
180
181enum WhichFontSize {
182 AgentFont,
183 BufferFont,
184 None,
185}
186
187impl ActiveView {
188 pub fn which_font_size_used(&self) -> WhichFontSize {
189 match self {
190 ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont,
191 ActiveView::PromptEditor { .. } => WhichFontSize::BufferFont,
192 ActiveView::Configuration => WhichFontSize::None,
193 }
194 }
195
196 pub fn thread(thread: Entity<Thread>, window: &mut Window, cx: &mut App) -> Self {
197 let summary = thread.read(cx).summary_or_default();
198
199 let editor = cx.new(|cx| {
200 let mut editor = Editor::single_line(window, cx);
201 editor.set_text(summary.clone(), window, cx);
202 editor
203 });
204
205 let subscriptions = vec![
206 window.subscribe(&editor, cx, {
207 {
208 let thread = thread.clone();
209 move |editor, event, window, cx| match event {
210 EditorEvent::BufferEdited => {
211 let new_summary = editor.read(cx).text(cx);
212
213 thread.update(cx, |thread, cx| {
214 thread.set_summary(new_summary, cx);
215 })
216 }
217 EditorEvent::Blurred => {
218 if editor.read(cx).text(cx).is_empty() {
219 let summary = thread.read(cx).summary_or_default();
220
221 editor.update(cx, |editor, cx| {
222 editor.set_text(summary, window, cx);
223 });
224 }
225 }
226 _ => {}
227 }
228 }
229 }),
230 window.subscribe(&thread, cx, {
231 let editor = editor.clone();
232 move |thread, event, window, cx| match event {
233 ThreadEvent::SummaryGenerated => {
234 let summary = thread.read(cx).summary_or_default();
235
236 editor.update(cx, |editor, cx| {
237 editor.set_text(summary, window, cx);
238 })
239 }
240 _ => {}
241 }
242 }),
243 ];
244
245 Self::Thread {
246 change_title_editor: editor,
247 thread: thread.downgrade(),
248 _subscriptions: subscriptions,
249 }
250 }
251
252 pub fn prompt_editor(
253 context_editor: Entity<ContextEditor>,
254 language_registry: Arc<LanguageRegistry>,
255 window: &mut Window,
256 cx: &mut App,
257 ) -> Self {
258 let title = context_editor.read(cx).title(cx).to_string();
259
260 let editor = cx.new(|cx| {
261 let mut editor = Editor::single_line(window, cx);
262 editor.set_text(title, window, cx);
263 editor
264 });
265
266 // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would
267 // cause a custom summary to be set. The presence of this custom summary would cause
268 // summarization to not happen.
269 let mut suppress_first_edit = true;
270
271 let subscriptions = vec![
272 window.subscribe(&editor, cx, {
273 {
274 let context_editor = context_editor.clone();
275 move |editor, event, window, cx| match event {
276 EditorEvent::BufferEdited => {
277 if suppress_first_edit {
278 suppress_first_edit = false;
279 return;
280 }
281 let new_summary = editor.read(cx).text(cx);
282
283 context_editor.update(cx, |context_editor, cx| {
284 context_editor
285 .context()
286 .update(cx, |assistant_context, cx| {
287 assistant_context.set_custom_summary(new_summary, cx);
288 })
289 })
290 }
291 EditorEvent::Blurred => {
292 if editor.read(cx).text(cx).is_empty() {
293 let summary = context_editor
294 .read(cx)
295 .context()
296 .read(cx)
297 .summary_or_default();
298
299 editor.update(cx, |editor, cx| {
300 editor.set_text(summary, window, cx);
301 });
302 }
303 }
304 _ => {}
305 }
306 }
307 }),
308 window.subscribe(&context_editor.read(cx).context().clone(), cx, {
309 let editor = editor.clone();
310 move |assistant_context, event, window, cx| match event {
311 ContextEvent::SummaryGenerated => {
312 let summary = assistant_context.read(cx).summary_or_default();
313
314 editor.update(cx, |editor, cx| {
315 editor.set_text(summary, window, cx);
316 })
317 }
318 _ => {}
319 }
320 }),
321 ];
322
323 let buffer_search_bar =
324 cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx));
325 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
326 buffer_search_bar.set_active_pane_item(Some(&context_editor), window, cx)
327 });
328
329 Self::PromptEditor {
330 context_editor,
331 title_editor: editor,
332 buffer_search_bar,
333 _subscriptions: subscriptions,
334 }
335 }
336}
337
338pub struct AssistantPanel {
339 workspace: WeakEntity<Workspace>,
340 user_store: Entity<UserStore>,
341 project: Entity<Project>,
342 fs: Arc<dyn Fs>,
343 language_registry: Arc<LanguageRegistry>,
344 thread_store: Entity<ThreadStore>,
345 thread: Entity<ActiveThread>,
346 message_editor: Entity<MessageEditor>,
347 _active_thread_subscriptions: Vec<Subscription>,
348 _default_model_subscription: Subscription,
349 context_store: Entity<TextThreadStore>,
350 prompt_store: Option<Entity<PromptStore>>,
351 inline_assist_context_store: Entity<crate::context_store::ContextStore>,
352 configuration: Option<Entity<AssistantConfiguration>>,
353 configuration_subscription: Option<Subscription>,
354 local_timezone: UtcOffset,
355 active_view: ActiveView,
356 previous_view: Option<ActiveView>,
357 history_store: Entity<HistoryStore>,
358 history: Entity<ThreadHistory>,
359 assistant_dropdown_menu_handle: PopoverMenuHandle<ContextMenu>,
360 assistant_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
361 assistant_navigation_menu: Option<Entity<ContextMenu>>,
362 width: Option<Pixels>,
363 height: Option<Pixels>,
364 pending_serialization: Option<Task<Result<()>>>,
365 hide_trial_upsell: bool,
366 _trial_markdown: Entity<Markdown>,
367}
368
369impl AssistantPanel {
370 fn serialize(&mut self, cx: &mut Context<Self>) {
371 let width = self.width;
372 self.pending_serialization = Some(cx.background_spawn(async move {
373 KEY_VALUE_STORE
374 .write_kvp(
375 AGENT_PANEL_KEY.into(),
376 serde_json::to_string(&SerializedAssistantPanel { width })?,
377 )
378 .await?;
379 anyhow::Ok(())
380 }));
381 }
382 pub fn load(
383 workspace: WeakEntity<Workspace>,
384 prompt_builder: Arc<PromptBuilder>,
385 mut cx: AsyncWindowContext,
386 ) -> Task<Result<Entity<Self>>> {
387 let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
388 cx.spawn(async move |cx| {
389 let prompt_store = match prompt_store {
390 Ok(prompt_store) => prompt_store.await.ok(),
391 Err(_) => None,
392 };
393 let tools = cx.new(|_| ToolWorkingSet::default())?;
394 let thread_store = workspace
395 .update(cx, |workspace, cx| {
396 let project = workspace.project().clone();
397 ThreadStore::load(
398 project,
399 tools.clone(),
400 prompt_store.clone(),
401 prompt_builder.clone(),
402 cx,
403 )
404 })?
405 .await?;
406
407 let slash_commands = Arc::new(SlashCommandWorkingSet::default());
408 let context_store = workspace
409 .update(cx, |workspace, cx| {
410 let project = workspace.project().clone();
411 assistant_context_editor::ContextStore::new(
412 project,
413 prompt_builder.clone(),
414 slash_commands,
415 cx,
416 )
417 })?
418 .await?;
419
420 let serialized_panel = if let Some(panel) = cx
421 .background_spawn(async move { KEY_VALUE_STORE.read_kvp(AGENT_PANEL_KEY) })
422 .await
423 .log_err()
424 .flatten()
425 {
426 Some(serde_json::from_str::<SerializedAssistantPanel>(&panel)?)
427 } else {
428 None
429 };
430
431 let panel = workspace.update_in(cx, |workspace, window, cx| {
432 let panel = cx.new(|cx| {
433 Self::new(
434 workspace,
435 thread_store,
436 context_store,
437 prompt_store,
438 window,
439 cx,
440 )
441 });
442 if let Some(serialized_panel) = serialized_panel {
443 panel.update(cx, |panel, cx| {
444 panel.width = serialized_panel.width.map(|w| w.round());
445 cx.notify();
446 });
447 }
448 panel
449 })?;
450
451 Ok(panel)
452 })
453 }
454
455 fn new(
456 workspace: &Workspace,
457 thread_store: Entity<ThreadStore>,
458 context_store: Entity<TextThreadStore>,
459 prompt_store: Option<Entity<PromptStore>>,
460 window: &mut Window,
461 cx: &mut Context<Self>,
462 ) -> Self {
463 let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
464 let fs = workspace.app_state().fs.clone();
465 let user_store = workspace.app_state().user_store.clone();
466 let project = workspace.project();
467 let language_registry = project.read(cx).languages().clone();
468 let workspace = workspace.weak_handle();
469 let weak_self = cx.entity().downgrade();
470
471 let message_editor_context_store = cx.new(|_cx| {
472 crate::context_store::ContextStore::new(
473 project.downgrade(),
474 Some(thread_store.downgrade()),
475 )
476 });
477 let inline_assist_context_store = cx.new(|_cx| {
478 crate::context_store::ContextStore::new(
479 project.downgrade(),
480 Some(thread_store.downgrade()),
481 )
482 });
483
484 let message_editor = cx.new(|cx| {
485 MessageEditor::new(
486 fs.clone(),
487 workspace.clone(),
488 user_store.clone(),
489 message_editor_context_store.clone(),
490 prompt_store.clone(),
491 thread_store.downgrade(),
492 context_store.downgrade(),
493 thread.clone(),
494 agent_panel_dock_position(cx),
495 window,
496 cx,
497 )
498 });
499
500 let message_editor_subscription =
501 cx.subscribe(&message_editor, |_, _, event, cx| match event {
502 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
503 cx.notify();
504 }
505 });
506
507 let thread_id = thread.read(cx).id().clone();
508 let history_store = cx.new(|cx| {
509 HistoryStore::new(
510 thread_store.clone(),
511 context_store.clone(),
512 [RecentEntry::Thread(thread_id, thread.clone())],
513 window,
514 cx,
515 )
516 });
517
518 cx.observe(&history_store, |_, _, cx| cx.notify()).detach();
519
520 let active_view = ActiveView::thread(thread.clone(), window, cx);
521 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
522 if let ThreadEvent::MessageAdded(_) = &event {
523 // needed to leave empty state
524 cx.notify();
525 }
526 });
527 let active_thread = cx.new(|cx| {
528 ActiveThread::new(
529 thread.clone(),
530 thread_store.clone(),
531 context_store.clone(),
532 message_editor_context_store.clone(),
533 language_registry.clone(),
534 workspace.clone(),
535 window,
536 cx,
537 )
538 });
539 AgentDiff::set_active_thread(&workspace, &thread, window, cx);
540
541 let active_thread_subscription =
542 cx.subscribe(&active_thread, |_, _, event, cx| match &event {
543 ActiveThreadEvent::EditingMessageTokenCountChanged => {
544 cx.notify();
545 }
546 });
547
548 let weak_panel = weak_self.clone();
549
550 window.defer(cx, move |window, cx| {
551 let panel = weak_panel.clone();
552 let assistant_navigation_menu =
553 ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
554 let recently_opened = panel
555 .update(cx, |this, cx| {
556 this.history_store.update(cx, |history_store, cx| {
557 history_store.recently_opened_entries(cx)
558 })
559 })
560 .unwrap_or_default();
561
562 if !recently_opened.is_empty() {
563 menu = menu.header("Recently Opened");
564
565 for entry in recently_opened.iter() {
566 let summary = entry.summary(cx);
567
568 menu = menu.entry_with_end_slot_on_hover(
569 summary,
570 None,
571 {
572 let panel = panel.clone();
573 let entry = entry.clone();
574 move |window, cx| {
575 panel
576 .update(cx, {
577 let entry = entry.clone();
578 move |this, cx| match entry {
579 RecentEntry::Thread(_, thread) => {
580 this.open_thread(thread, window, cx)
581 }
582 RecentEntry::Context(context) => {
583 let Some(path) = context.read(cx).path()
584 else {
585 return;
586 };
587 this.open_saved_prompt_editor(
588 path.clone(),
589 window,
590 cx,
591 )
592 .detach_and_log_err(cx)
593 }
594 }
595 })
596 .ok();
597 }
598 },
599 IconName::Close,
600 "Close Entry".into(),
601 {
602 let panel = panel.clone();
603 let entry = entry.clone();
604 move |_window, cx| {
605 panel
606 .update(cx, |this, cx| {
607 this.history_store.update(
608 cx,
609 |history_store, cx| {
610 history_store.remove_recently_opened_entry(
611 &entry, cx,
612 );
613 },
614 );
615 })
616 .ok();
617 }
618 },
619 );
620 }
621
622 menu = menu.separator();
623 }
624
625 menu.action("View All", Box::new(OpenHistory))
626 .end_slot_action(DeleteRecentlyOpenThread.boxed_clone())
627 .fixed_width(px(320.).into())
628 .keep_open_on_confirm(false)
629 .key_context("NavigationMenu")
630 });
631 weak_panel
632 .update(cx, |panel, cx| {
633 cx.subscribe_in(
634 &assistant_navigation_menu,
635 window,
636 |_, menu, _: &DismissEvent, window, cx| {
637 menu.update(cx, |menu, _| {
638 menu.clear_selected();
639 });
640 cx.focus_self(window);
641 },
642 )
643 .detach();
644 panel.assistant_navigation_menu = Some(assistant_navigation_menu);
645 })
646 .ok();
647 });
648
649 let _default_model_subscription = cx.subscribe(
650 &LanguageModelRegistry::global(cx),
651 |this, _, event: &language_model::Event, cx| match event {
652 language_model::Event::DefaultModelChanged => {
653 this.thread
654 .read(cx)
655 .thread()
656 .clone()
657 .update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
658 }
659 _ => {}
660 },
661 );
662
663 let trial_markdown = cx.new(|cx| {
664 Markdown::new(
665 include_str!("trial_markdown.md").into(),
666 Some(language_registry.clone()),
667 None,
668 cx,
669 )
670 });
671
672 Self {
673 active_view,
674 workspace,
675 user_store,
676 project: project.clone(),
677 fs: fs.clone(),
678 language_registry,
679 thread_store: thread_store.clone(),
680 thread: active_thread,
681 message_editor,
682 _active_thread_subscriptions: vec![
683 thread_subscription,
684 active_thread_subscription,
685 message_editor_subscription,
686 ],
687 _default_model_subscription,
688 context_store,
689 prompt_store,
690 configuration: None,
691 configuration_subscription: None,
692 local_timezone: UtcOffset::from_whole_seconds(
693 chrono::Local::now().offset().local_minus_utc(),
694 )
695 .unwrap(),
696 inline_assist_context_store,
697 previous_view: None,
698 history_store: history_store.clone(),
699 history: cx.new(|cx| ThreadHistory::new(weak_self, history_store, window, cx)),
700 assistant_dropdown_menu_handle: PopoverMenuHandle::default(),
701 assistant_navigation_menu_handle: PopoverMenuHandle::default(),
702 assistant_navigation_menu: None,
703 width: None,
704 height: None,
705 pending_serialization: None,
706 hide_trial_upsell: false,
707 _trial_markdown: trial_markdown,
708 }
709 }
710
711 pub fn toggle_focus(
712 workspace: &mut Workspace,
713 _: &ToggleFocus,
714 window: &mut Window,
715 cx: &mut Context<Workspace>,
716 ) {
717 if workspace
718 .panel::<Self>(cx)
719 .is_some_and(|panel| panel.read(cx).enabled(cx))
720 {
721 workspace.toggle_panel_focus::<Self>(window, cx);
722 }
723 }
724
725 pub(crate) fn local_timezone(&self) -> UtcOffset {
726 self.local_timezone
727 }
728
729 pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
730 &self.prompt_store
731 }
732
733 pub(crate) fn inline_assist_context_store(
734 &self,
735 ) -> &Entity<crate::context_store::ContextStore> {
736 &self.inline_assist_context_store
737 }
738
739 pub(crate) fn thread_store(&self) -> &Entity<ThreadStore> {
740 &self.thread_store
741 }
742
743 pub(crate) fn text_thread_store(&self) -> &Entity<TextThreadStore> {
744 &self.context_store
745 }
746
747 fn cancel(&mut self, _: &editor::actions::Cancel, window: &mut Window, cx: &mut Context<Self>) {
748 self.thread
749 .update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
750 }
751
752 fn new_thread(&mut self, action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
753 let thread = self
754 .thread_store
755 .update(cx, |this, cx| this.create_thread(cx));
756
757 let thread_view = ActiveView::thread(thread.clone(), window, cx);
758 self.set_active_view(thread_view, window, cx);
759
760 let context_store = cx.new(|_cx| {
761 crate::context_store::ContextStore::new(
762 self.project.downgrade(),
763 Some(self.thread_store.downgrade()),
764 )
765 });
766
767 if let Some(other_thread_id) = action.from_thread_id.clone() {
768 let other_thread_task = self.thread_store.update(cx, |this, cx| {
769 this.open_thread(&other_thread_id, window, cx)
770 });
771
772 cx.spawn({
773 let context_store = context_store.clone();
774
775 async move |_panel, cx| {
776 let other_thread = other_thread_task.await?;
777
778 context_store.update(cx, |this, cx| {
779 this.add_thread(other_thread, false, cx);
780 })?;
781 anyhow::Ok(())
782 }
783 })
784 .detach_and_log_err(cx);
785 }
786
787 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
788 if let ThreadEvent::MessageAdded(_) = &event {
789 // needed to leave empty state
790 cx.notify();
791 }
792 });
793
794 self.thread = cx.new(|cx| {
795 ActiveThread::new(
796 thread.clone(),
797 self.thread_store.clone(),
798 self.context_store.clone(),
799 context_store.clone(),
800 self.language_registry.clone(),
801 self.workspace.clone(),
802 window,
803 cx,
804 )
805 });
806 AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
807
808 let active_thread_subscription =
809 cx.subscribe(&self.thread, |_, _, event, cx| match &event {
810 ActiveThreadEvent::EditingMessageTokenCountChanged => {
811 cx.notify();
812 }
813 });
814
815 self.message_editor = cx.new(|cx| {
816 MessageEditor::new(
817 self.fs.clone(),
818 self.workspace.clone(),
819 self.user_store.clone(),
820 context_store,
821 self.prompt_store.clone(),
822 self.thread_store.downgrade(),
823 self.context_store.downgrade(),
824 thread,
825 agent_panel_dock_position(cx),
826 window,
827 cx,
828 )
829 });
830 self.message_editor.focus_handle(cx).focus(window);
831
832 let message_editor_subscription =
833 cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
834 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
835 cx.notify();
836 }
837 });
838
839 self._active_thread_subscriptions = vec![
840 thread_subscription,
841 active_thread_subscription,
842 message_editor_subscription,
843 ];
844 }
845
846 fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
847 let context = self
848 .context_store
849 .update(cx, |context_store, cx| context_store.create(cx));
850 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
851 .log_err()
852 .flatten();
853
854 let context_editor = cx.new(|cx| {
855 let mut editor = ContextEditor::for_context(
856 context,
857 self.fs.clone(),
858 self.workspace.clone(),
859 self.project.clone(),
860 lsp_adapter_delegate,
861 window,
862 cx,
863 );
864 editor.insert_default_prompt(window, cx);
865 editor
866 });
867
868 self.set_active_view(
869 ActiveView::prompt_editor(
870 context_editor.clone(),
871 self.language_registry.clone(),
872 window,
873 cx,
874 ),
875 window,
876 cx,
877 );
878 context_editor.focus_handle(cx).focus(window);
879 }
880
881 fn deploy_rules_library(
882 &mut self,
883 action: &OpenRulesLibrary,
884 _window: &mut Window,
885 cx: &mut Context<Self>,
886 ) {
887 open_rules_library(
888 self.language_registry.clone(),
889 Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
890 Arc::new(|| {
891 Box::new(SlashCommandCompletionProvider::new(
892 Arc::new(SlashCommandWorkingSet::default()),
893 None,
894 None,
895 ))
896 }),
897 action
898 .prompt_to_select
899 .map(|uuid| UserPromptId(uuid).into()),
900 cx,
901 )
902 .detach_and_log_err(cx);
903 }
904
905 fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
906 if matches!(self.active_view, ActiveView::History) {
907 if let Some(previous_view) = self.previous_view.take() {
908 self.set_active_view(previous_view, window, cx);
909 }
910 } else {
911 self.thread_store
912 .update(cx, |thread_store, cx| thread_store.reload(cx))
913 .detach_and_log_err(cx);
914 self.set_active_view(ActiveView::History, window, cx);
915 }
916 cx.notify();
917 }
918
919 pub(crate) fn open_saved_prompt_editor(
920 &mut self,
921 path: Arc<Path>,
922 window: &mut Window,
923 cx: &mut Context<Self>,
924 ) -> Task<Result<()>> {
925 let context = self
926 .context_store
927 .update(cx, |store, cx| store.open_local_context(path, cx));
928 cx.spawn_in(window, async move |this, cx| {
929 let context = context.await?;
930 this.update_in(cx, |this, window, cx| {
931 this.open_prompt_editor(context, window, cx);
932 })
933 })
934 }
935
936 pub(crate) fn open_prompt_editor(
937 &mut self,
938 context: Entity<AssistantContext>,
939 window: &mut Window,
940 cx: &mut Context<Self>,
941 ) {
942 let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx)
943 .log_err()
944 .flatten();
945 let editor = cx.new(|cx| {
946 ContextEditor::for_context(
947 context,
948 self.fs.clone(),
949 self.workspace.clone(),
950 self.project.clone(),
951 lsp_adapter_delegate,
952 window,
953 cx,
954 )
955 });
956 self.set_active_view(
957 ActiveView::prompt_editor(editor.clone(), self.language_registry.clone(), window, cx),
958 window,
959 cx,
960 );
961 }
962
963 pub(crate) fn open_thread_by_id(
964 &mut self,
965 thread_id: &ThreadId,
966 window: &mut Window,
967 cx: &mut Context<Self>,
968 ) -> Task<Result<()>> {
969 let open_thread_task = self
970 .thread_store
971 .update(cx, |this, cx| this.open_thread(thread_id, window, cx));
972 cx.spawn_in(window, async move |this, cx| {
973 let thread = open_thread_task.await?;
974 this.update_in(cx, |this, window, cx| {
975 this.open_thread(thread, window, cx);
976 anyhow::Ok(())
977 })??;
978 Ok(())
979 })
980 }
981
982 pub(crate) fn open_thread(
983 &mut self,
984 thread: Entity<Thread>,
985 window: &mut Window,
986 cx: &mut Context<Self>,
987 ) {
988 let thread_view = ActiveView::thread(thread.clone(), window, cx);
989 self.set_active_view(thread_view, window, cx);
990 let context_store = cx.new(|_cx| {
991 crate::context_store::ContextStore::new(
992 self.project.downgrade(),
993 Some(self.thread_store.downgrade()),
994 )
995 });
996 let thread_subscription = cx.subscribe(&thread, |_, _, event, cx| {
997 if let ThreadEvent::MessageAdded(_) = &event {
998 // needed to leave empty state
999 cx.notify();
1000 }
1001 });
1002
1003 self.thread = cx.new(|cx| {
1004 ActiveThread::new(
1005 thread.clone(),
1006 self.thread_store.clone(),
1007 self.context_store.clone(),
1008 context_store.clone(),
1009 self.language_registry.clone(),
1010 self.workspace.clone(),
1011 window,
1012 cx,
1013 )
1014 });
1015 AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
1016
1017 let active_thread_subscription =
1018 cx.subscribe(&self.thread, |_, _, event, cx| match &event {
1019 ActiveThreadEvent::EditingMessageTokenCountChanged => {
1020 cx.notify();
1021 }
1022 });
1023
1024 self.message_editor = cx.new(|cx| {
1025 MessageEditor::new(
1026 self.fs.clone(),
1027 self.workspace.clone(),
1028 self.user_store.clone(),
1029 context_store,
1030 self.prompt_store.clone(),
1031 self.thread_store.downgrade(),
1032 self.context_store.downgrade(),
1033 thread,
1034 agent_panel_dock_position(cx),
1035 window,
1036 cx,
1037 )
1038 });
1039 self.message_editor.focus_handle(cx).focus(window);
1040
1041 let message_editor_subscription =
1042 cx.subscribe(&self.message_editor, |_, _, event, cx| match event {
1043 MessageEditorEvent::Changed | MessageEditorEvent::EstimatedTokenCount => {
1044 cx.notify();
1045 }
1046 });
1047
1048 self._active_thread_subscriptions = vec![
1049 thread_subscription,
1050 active_thread_subscription,
1051 message_editor_subscription,
1052 ];
1053 }
1054
1055 pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
1056 match self.active_view {
1057 ActiveView::Configuration | ActiveView::History => {
1058 self.active_view =
1059 ActiveView::thread(self.thread.read(cx).thread().clone(), window, cx);
1060 self.message_editor.focus_handle(cx).focus(window);
1061 cx.notify();
1062 }
1063 _ => {}
1064 }
1065 }
1066
1067 pub fn toggle_navigation_menu(
1068 &mut self,
1069 _: &ToggleNavigationMenu,
1070 window: &mut Window,
1071 cx: &mut Context<Self>,
1072 ) {
1073 self.assistant_navigation_menu_handle.toggle(window, cx);
1074 }
1075
1076 pub fn toggle_options_menu(
1077 &mut self,
1078 _: &ToggleOptionsMenu,
1079 window: &mut Window,
1080 cx: &mut Context<Self>,
1081 ) {
1082 self.assistant_dropdown_menu_handle.toggle(window, cx);
1083 }
1084
1085 pub fn increase_font_size(
1086 &mut self,
1087 action: &IncreaseBufferFontSize,
1088 _: &mut Window,
1089 cx: &mut Context<Self>,
1090 ) {
1091 self.handle_font_size_action(action.persist, px(1.0), cx);
1092 }
1093
1094 pub fn decrease_font_size(
1095 &mut self,
1096 action: &DecreaseBufferFontSize,
1097 _: &mut Window,
1098 cx: &mut Context<Self>,
1099 ) {
1100 self.handle_font_size_action(action.persist, px(-1.0), cx);
1101 }
1102
1103 fn handle_font_size_action(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
1104 match self.active_view.which_font_size_used() {
1105 WhichFontSize::AgentFont => {
1106 if persist {
1107 update_settings_file::<ThemeSettings>(
1108 self.fs.clone(),
1109 cx,
1110 move |settings, cx| {
1111 let agent_font_size =
1112 ThemeSettings::get_global(cx).agent_font_size(cx) + delta;
1113 let _ = settings
1114 .agent_font_size
1115 .insert(theme::clamp_font_size(agent_font_size).0);
1116 },
1117 );
1118 } else {
1119 theme::adjust_agent_font_size(cx, |size| {
1120 *size += delta;
1121 });
1122 }
1123 }
1124 WhichFontSize::BufferFont => {
1125 // Prompt editor uses the buffer font size, so allow the action to propagate to the
1126 // default handler that changes that font size.
1127 cx.propagate();
1128 }
1129 WhichFontSize::None => {}
1130 }
1131 }
1132
1133 pub fn reset_font_size(
1134 &mut self,
1135 action: &ResetBufferFontSize,
1136 _: &mut Window,
1137 cx: &mut Context<Self>,
1138 ) {
1139 if action.persist {
1140 update_settings_file::<ThemeSettings>(self.fs.clone(), cx, move |settings, _| {
1141 settings.agent_font_size = None;
1142 });
1143 } else {
1144 theme::reset_agent_font_size(cx);
1145 }
1146 }
1147
1148 pub fn open_agent_diff(
1149 &mut self,
1150 _: &OpenAgentDiff,
1151 window: &mut Window,
1152 cx: &mut Context<Self>,
1153 ) {
1154 let thread = self.thread.read(cx).thread().clone();
1155 self.workspace
1156 .update(cx, |workspace, cx| {
1157 AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
1158 })
1159 .log_err();
1160 }
1161
1162 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1163 let context_server_store = self.project.read(cx).context_server_store();
1164 let tools = self.thread_store.read(cx).tools();
1165 let fs = self.fs.clone();
1166
1167 self.set_active_view(ActiveView::Configuration, window, cx);
1168 self.configuration =
1169 Some(cx.new(|cx| {
1170 AssistantConfiguration::new(fs, context_server_store, tools, window, cx)
1171 }));
1172
1173 if let Some(configuration) = self.configuration.as_ref() {
1174 self.configuration_subscription = Some(cx.subscribe_in(
1175 configuration,
1176 window,
1177 Self::handle_assistant_configuration_event,
1178 ));
1179
1180 configuration.focus_handle(cx).focus(window);
1181 }
1182 }
1183
1184 pub(crate) fn open_active_thread_as_markdown(
1185 &mut self,
1186 _: &OpenActiveThreadAsMarkdown,
1187 window: &mut Window,
1188 cx: &mut Context<Self>,
1189 ) {
1190 let Some(workspace) = self
1191 .workspace
1192 .upgrade()
1193 .ok_or_else(|| anyhow!("workspace dropped"))
1194 .log_err()
1195 else {
1196 return;
1197 };
1198
1199 let Some(thread) = self.active_thread() else {
1200 return;
1201 };
1202
1203 active_thread::open_active_thread_as_markdown(thread, workspace, window, cx)
1204 .detach_and_log_err(cx);
1205 }
1206
1207 fn handle_assistant_configuration_event(
1208 &mut self,
1209 _entity: &Entity<AssistantConfiguration>,
1210 event: &AssistantConfigurationEvent,
1211 window: &mut Window,
1212 cx: &mut Context<Self>,
1213 ) {
1214 match event {
1215 AssistantConfigurationEvent::NewThread(provider) => {
1216 if LanguageModelRegistry::read_global(cx)
1217 .default_model()
1218 .map_or(true, |model| model.provider.id() != provider.id())
1219 {
1220 if let Some(model) = provider.default_model(cx) {
1221 update_settings_file::<AssistantSettings>(
1222 self.fs.clone(),
1223 cx,
1224 move |settings, _| settings.set_model(model),
1225 );
1226 }
1227 }
1228
1229 self.new_thread(&NewThread::default(), window, cx);
1230 }
1231 }
1232 }
1233
1234 pub(crate) fn active_thread(&self) -> Option<Entity<Thread>> {
1235 match &self.active_view {
1236 ActiveView::Thread { thread, .. } => thread.upgrade(),
1237 _ => None,
1238 }
1239 }
1240
1241 pub(crate) fn delete_thread(
1242 &mut self,
1243 thread_id: &ThreadId,
1244 cx: &mut Context<Self>,
1245 ) -> Task<Result<()>> {
1246 self.thread_store
1247 .update(cx, |this, cx| this.delete_thread(thread_id, cx))
1248 }
1249
1250 pub(crate) fn has_active_thread(&self) -> bool {
1251 matches!(self.active_view, ActiveView::Thread { .. })
1252 }
1253
1254 pub(crate) fn active_context_editor(&self) -> Option<Entity<ContextEditor>> {
1255 match &self.active_view {
1256 ActiveView::PromptEditor { context_editor, .. } => Some(context_editor.clone()),
1257 _ => None,
1258 }
1259 }
1260
1261 pub(crate) fn delete_context(
1262 &mut self,
1263 path: Arc<Path>,
1264 cx: &mut Context<Self>,
1265 ) -> Task<Result<()>> {
1266 self.context_store
1267 .update(cx, |this, cx| this.delete_local_context(path, cx))
1268 }
1269
1270 fn set_active_view(
1271 &mut self,
1272 new_view: ActiveView,
1273 window: &mut Window,
1274 cx: &mut Context<Self>,
1275 ) {
1276 let current_is_history = matches!(self.active_view, ActiveView::History);
1277 let new_is_history = matches!(new_view, ActiveView::History);
1278
1279 match &self.active_view {
1280 ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1281 if let Some(thread) = thread.upgrade() {
1282 if thread.read(cx).is_empty() {
1283 let id = thread.read(cx).id().clone();
1284 store.remove_recently_opened_thread(id, cx);
1285 }
1286 }
1287 }),
1288 _ => {}
1289 }
1290
1291 match &new_view {
1292 ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
1293 if let Some(thread) = thread.upgrade() {
1294 let id = thread.read(cx).id().clone();
1295 store.push_recently_opened_entry(RecentEntry::Thread(id, thread), cx);
1296 }
1297 }),
1298 ActiveView::PromptEditor { context_editor, .. } => {
1299 self.history_store.update(cx, |store, cx| {
1300 let context = context_editor.read(cx).context().clone();
1301 store.push_recently_opened_entry(RecentEntry::Context(context), cx)
1302 })
1303 }
1304 _ => {}
1305 }
1306
1307 if current_is_history && !new_is_history {
1308 self.active_view = new_view;
1309 } else if !current_is_history && new_is_history {
1310 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1311 } else {
1312 if !new_is_history {
1313 self.previous_view = None;
1314 }
1315 self.active_view = new_view;
1316 }
1317
1318 self.focus_handle(cx).focus(window);
1319 }
1320}
1321
1322impl Focusable for AssistantPanel {
1323 fn focus_handle(&self, cx: &App) -> FocusHandle {
1324 match &self.active_view {
1325 ActiveView::Thread { .. } => self.message_editor.focus_handle(cx),
1326 ActiveView::History => self.history.focus_handle(cx),
1327 ActiveView::PromptEditor { context_editor, .. } => context_editor.focus_handle(cx),
1328 ActiveView::Configuration => {
1329 if let Some(configuration) = self.configuration.as_ref() {
1330 configuration.focus_handle(cx)
1331 } else {
1332 cx.focus_handle()
1333 }
1334 }
1335 }
1336 }
1337}
1338
1339fn agent_panel_dock_position(cx: &App) -> DockPosition {
1340 match AssistantSettings::get_global(cx).dock {
1341 AssistantDockPosition::Left => DockPosition::Left,
1342 AssistantDockPosition::Bottom => DockPosition::Bottom,
1343 AssistantDockPosition::Right => DockPosition::Right,
1344 }
1345}
1346
1347impl EventEmitter<PanelEvent> for AssistantPanel {}
1348
1349impl Panel for AssistantPanel {
1350 fn persistent_name() -> &'static str {
1351 "AgentPanel"
1352 }
1353
1354 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1355 agent_panel_dock_position(cx)
1356 }
1357
1358 fn position_is_valid(&self, position: DockPosition) -> bool {
1359 position != DockPosition::Bottom
1360 }
1361
1362 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1363 self.message_editor.update(cx, |message_editor, cx| {
1364 message_editor.set_dock_position(position, cx);
1365 });
1366
1367 settings::update_settings_file::<AssistantSettings>(
1368 self.fs.clone(),
1369 cx,
1370 move |settings, _| {
1371 let dock = match position {
1372 DockPosition::Left => AssistantDockPosition::Left,
1373 DockPosition::Bottom => AssistantDockPosition::Bottom,
1374 DockPosition::Right => AssistantDockPosition::Right,
1375 };
1376 settings.set_dock(dock);
1377 },
1378 );
1379 }
1380
1381 fn size(&self, window: &Window, cx: &App) -> Pixels {
1382 let settings = AssistantSettings::get_global(cx);
1383 match self.position(window, cx) {
1384 DockPosition::Left | DockPosition::Right => {
1385 self.width.unwrap_or(settings.default_width)
1386 }
1387 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1388 }
1389 }
1390
1391 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1392 match self.position(window, cx) {
1393 DockPosition::Left | DockPosition::Right => self.width = size,
1394 DockPosition::Bottom => self.height = size,
1395 }
1396 self.serialize(cx);
1397 cx.notify();
1398 }
1399
1400 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
1401
1402 fn remote_id() -> Option<proto::PanelId> {
1403 Some(proto::PanelId::AssistantPanel)
1404 }
1405
1406 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1407 (self.enabled(cx) && AssistantSettings::get_global(cx).button)
1408 .then_some(IconName::ZedAssistant)
1409 }
1410
1411 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1412 Some("Agent Panel")
1413 }
1414
1415 fn toggle_action(&self) -> Box<dyn Action> {
1416 Box::new(ToggleFocus)
1417 }
1418
1419 fn activation_priority(&self) -> u32 {
1420 3
1421 }
1422
1423 fn enabled(&self, cx: &App) -> bool {
1424 AssistantSettings::get_global(cx).enabled
1425 }
1426}
1427
1428impl AssistantPanel {
1429 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1430 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1431
1432 let content = match &self.active_view {
1433 ActiveView::Thread {
1434 change_title_editor,
1435 ..
1436 } => {
1437 let active_thread = self.thread.read(cx);
1438 let is_empty = active_thread.is_empty();
1439
1440 let summary = active_thread.summary(cx);
1441
1442 if is_empty {
1443 Label::new(Thread::DEFAULT_SUMMARY.clone())
1444 .truncate()
1445 .into_any_element()
1446 } else if summary.is_none() {
1447 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1448 .truncate()
1449 .into_any_element()
1450 } else {
1451 div()
1452 .w_full()
1453 .child(change_title_editor.clone())
1454 .into_any_element()
1455 }
1456 }
1457 ActiveView::PromptEditor {
1458 title_editor,
1459 context_editor,
1460 ..
1461 } => {
1462 let context_editor = context_editor.read(cx);
1463 let summary = context_editor.context().read(cx).summary();
1464
1465 match summary {
1466 None => Label::new(AssistantContext::DEFAULT_SUMMARY.clone())
1467 .truncate()
1468 .into_any_element(),
1469 Some(summary) => {
1470 if summary.done {
1471 div()
1472 .w_full()
1473 .child(title_editor.clone())
1474 .into_any_element()
1475 } else {
1476 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1477 .truncate()
1478 .into_any_element()
1479 }
1480 }
1481 }
1482 }
1483 ActiveView::History => Label::new("History").truncate().into_any_element(),
1484 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1485 };
1486
1487 h_flex()
1488 .key_context("TitleEditor")
1489 .id("TitleEditor")
1490 .flex_grow()
1491 .w_full()
1492 .max_w_full()
1493 .overflow_x_scroll()
1494 .child(content)
1495 .into_any()
1496 }
1497
1498 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1499 let active_thread = self.thread.read(cx);
1500 let user_store = self.user_store.read(cx);
1501 let thread = active_thread.thread().read(cx);
1502 let thread_id = thread.id().clone();
1503 let is_empty = active_thread.is_empty();
1504 let editor_empty = self.message_editor.read(cx).is_editor_fully_empty(cx);
1505 let last_usage = active_thread.thread().read(cx).last_usage().or_else(|| {
1506 maybe!({
1507 let amount = user_store.model_request_usage_amount()?;
1508 let limit = user_store.model_request_usage_limit()?.variant?;
1509
1510 Some(RequestUsage {
1511 amount: amount as i32,
1512 limit: match limit {
1513 proto::usage_limit::Variant::Limited(limited) => {
1514 zed_llm_client::UsageLimit::Limited(limited.limit as i32)
1515 }
1516 proto::usage_limit::Variant::Unlimited(_) => {
1517 zed_llm_client::UsageLimit::Unlimited
1518 }
1519 },
1520 })
1521 })
1522 });
1523
1524 let account_url = zed_urls::account_url(cx);
1525
1526 let show_token_count = match &self.active_view {
1527 ActiveView::Thread { .. } => !is_empty || !editor_empty,
1528 ActiveView::PromptEditor { .. } => true,
1529 _ => false,
1530 };
1531
1532 let focus_handle = self.focus_handle(cx);
1533
1534 let go_back_button = div().child(
1535 IconButton::new("go-back", IconName::ArrowLeft)
1536 .icon_size(IconSize::Small)
1537 .on_click(cx.listener(|this, _, window, cx| {
1538 this.go_back(&workspace::GoBack, window, cx);
1539 }))
1540 .tooltip({
1541 let focus_handle = focus_handle.clone();
1542 move |window, cx| {
1543 Tooltip::for_action_in(
1544 "Go Back",
1545 &workspace::GoBack,
1546 &focus_handle,
1547 window,
1548 cx,
1549 )
1550 }
1551 }),
1552 );
1553
1554 let recent_entries_menu = div().child(
1555 PopoverMenu::new("agent-nav-menu")
1556 .trigger_with_tooltip(
1557 IconButton::new("agent-nav-menu", IconName::MenuAlt)
1558 .icon_size(IconSize::Small)
1559 .style(ui::ButtonStyle::Subtle),
1560 {
1561 let focus_handle = focus_handle.clone();
1562 move |window, cx| {
1563 Tooltip::for_action_in(
1564 "Toggle Panel Menu",
1565 &ToggleNavigationMenu,
1566 &focus_handle,
1567 window,
1568 cx,
1569 )
1570 }
1571 },
1572 )
1573 .anchor(Corner::TopLeft)
1574 .with_handle(self.assistant_navigation_menu_handle.clone())
1575 .menu({
1576 let menu = self.assistant_navigation_menu.clone();
1577 move |window, cx| {
1578 if let Some(menu) = menu.as_ref() {
1579 menu.update(cx, |_, cx| {
1580 cx.defer_in(window, |menu, window, cx| {
1581 menu.rebuild(window, cx);
1582 });
1583 })
1584 }
1585 menu.clone()
1586 }
1587 }),
1588 );
1589
1590 let agent_extra_menu = PopoverMenu::new("agent-options-menu")
1591 .trigger_with_tooltip(
1592 IconButton::new("agent-options-menu", IconName::Ellipsis)
1593 .icon_size(IconSize::Small),
1594 {
1595 let focus_handle = focus_handle.clone();
1596 move |window, cx| {
1597 Tooltip::for_action_in(
1598 "Toggle Agent Menu",
1599 &ToggleOptionsMenu,
1600 &focus_handle,
1601 window,
1602 cx,
1603 )
1604 }
1605 },
1606 )
1607 .anchor(Corner::TopRight)
1608 .with_handle(self.assistant_dropdown_menu_handle.clone())
1609 .menu(move |window, cx| {
1610 Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| {
1611 menu = menu
1612 .action("New Thread", NewThread::default().boxed_clone())
1613 .action("New Text Thread", NewTextThread.boxed_clone())
1614 .when(!is_empty, |menu| {
1615 menu.action(
1616 "New From Summary",
1617 Box::new(NewThread {
1618 from_thread_id: Some(thread_id.clone()),
1619 }),
1620 )
1621 })
1622 .separator();
1623
1624 menu = menu
1625 .header("MCP Servers")
1626 .action(
1627 "View Server Extensions",
1628 Box::new(zed_actions::Extensions {
1629 category_filter: Some(
1630 zed_actions::ExtensionCategoryFilter::ContextServers,
1631 ),
1632 }),
1633 )
1634 .action("Add Custom Server…", Box::new(AddContextServer))
1635 .separator();
1636
1637 if let Some(usage) = last_usage {
1638 menu = menu
1639 .header_with_link("Prompt Usage", "Manage", account_url.clone())
1640 .custom_entry(
1641 move |_window, cx| {
1642 let used_percentage = match usage.limit {
1643 UsageLimit::Limited(limit) => {
1644 Some((usage.amount as f32 / limit as f32) * 100.)
1645 }
1646 UsageLimit::Unlimited => None,
1647 };
1648
1649 h_flex()
1650 .flex_1()
1651 .gap_1p5()
1652 .children(used_percentage.map(|percent| {
1653 ProgressBar::new("usage", percent, 100., cx)
1654 }))
1655 .child(
1656 Label::new(match usage.limit {
1657 UsageLimit::Limited(limit) => {
1658 format!("{} / {limit}", usage.amount)
1659 }
1660 UsageLimit::Unlimited => {
1661 format!("{} / ∞", usage.amount)
1662 }
1663 })
1664 .size(LabelSize::Small)
1665 .color(Color::Muted),
1666 )
1667 .into_any_element()
1668 },
1669 move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
1670 )
1671 .separator()
1672 }
1673
1674 menu = menu
1675 .action("Rules…", Box::new(OpenRulesLibrary::default()))
1676 .action("Settings", Box::new(OpenConfiguration));
1677 menu
1678 }))
1679 });
1680
1681 h_flex()
1682 .id("assistant-toolbar")
1683 .h(Tab::container_height(cx))
1684 .max_w_full()
1685 .flex_none()
1686 .justify_between()
1687 .gap_2()
1688 .bg(cx.theme().colors().tab_bar_background)
1689 .border_b_1()
1690 .border_color(cx.theme().colors().border)
1691 .child(
1692 h_flex()
1693 .size_full()
1694 .pl_1()
1695 .gap_1()
1696 .child(match &self.active_view {
1697 ActiveView::History | ActiveView::Configuration => go_back_button,
1698 _ => recent_entries_menu,
1699 })
1700 .child(self.render_title_view(window, cx)),
1701 )
1702 .child(
1703 h_flex()
1704 .h_full()
1705 .gap_2()
1706 .when(show_token_count, |parent| {
1707 parent.children(self.render_token_count(&thread, cx))
1708 })
1709 .child(
1710 h_flex()
1711 .h_full()
1712 .gap(DynamicSpacing::Base02.rems(cx))
1713 .px(DynamicSpacing::Base08.rems(cx))
1714 .border_l_1()
1715 .border_color(cx.theme().colors().border)
1716 .child(
1717 IconButton::new("new", IconName::Plus)
1718 .icon_size(IconSize::Small)
1719 .style(ButtonStyle::Subtle)
1720 .tooltip(move |window, cx| {
1721 Tooltip::for_action_in(
1722 "New Thread",
1723 &NewThread::default(),
1724 &focus_handle,
1725 window,
1726 cx,
1727 )
1728 })
1729 .on_click(move |_event, window, cx| {
1730 window.dispatch_action(
1731 NewThread::default().boxed_clone(),
1732 cx,
1733 );
1734 }),
1735 )
1736 .child(agent_extra_menu),
1737 ),
1738 )
1739 }
1740
1741 fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
1742 let is_generating = thread.is_generating();
1743 let message_editor = self.message_editor.read(cx);
1744
1745 let conversation_token_usage = thread.total_token_usage()?;
1746
1747 let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
1748 self.thread.read(cx).editing_message_id()
1749 {
1750 let combined = thread
1751 .token_usage_up_to_message(editing_message_id)
1752 .add(unsent_tokens);
1753
1754 (combined, unsent_tokens > 0)
1755 } else {
1756 let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
1757 let combined = conversation_token_usage.add(unsent_tokens);
1758
1759 (combined, unsent_tokens > 0)
1760 };
1761
1762 let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
1763
1764 match &self.active_view {
1765 ActiveView::Thread { .. } => {
1766 if total_token_usage.total == 0 {
1767 return None;
1768 }
1769
1770 let token_color = match total_token_usage.ratio() {
1771 TokenUsageRatio::Normal if is_estimating => Color::Default,
1772 TokenUsageRatio::Normal => Color::Muted,
1773 TokenUsageRatio::Warning => Color::Warning,
1774 TokenUsageRatio::Exceeded => Color::Error,
1775 };
1776
1777 let token_count = h_flex()
1778 .id("token-count")
1779 .flex_shrink_0()
1780 .gap_0p5()
1781 .when(!is_generating && is_estimating, |parent| {
1782 parent
1783 .child(
1784 h_flex()
1785 .mr_1()
1786 .size_2p5()
1787 .justify_center()
1788 .rounded_full()
1789 .bg(cx.theme().colors().text.opacity(0.1))
1790 .child(
1791 div().size_1().rounded_full().bg(cx.theme().colors().text),
1792 ),
1793 )
1794 .tooltip(move |window, cx| {
1795 Tooltip::with_meta(
1796 "Estimated New Token Count",
1797 None,
1798 format!(
1799 "Current Conversation Tokens: {}",
1800 humanize_token_count(conversation_token_usage.total)
1801 ),
1802 window,
1803 cx,
1804 )
1805 })
1806 })
1807 .child(
1808 Label::new(humanize_token_count(total_token_usage.total))
1809 .size(LabelSize::Small)
1810 .color(token_color)
1811 .map(|label| {
1812 if is_generating || is_waiting_to_update_token_count {
1813 label
1814 .with_animation(
1815 "used-tokens-label",
1816 Animation::new(Duration::from_secs(2))
1817 .repeat()
1818 .with_easing(pulsating_between(0.6, 1.)),
1819 |label, delta| label.alpha(delta),
1820 )
1821 .into_any()
1822 } else {
1823 label.into_any_element()
1824 }
1825 }),
1826 )
1827 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
1828 .child(
1829 Label::new(humanize_token_count(total_token_usage.max))
1830 .size(LabelSize::Small)
1831 .color(Color::Muted),
1832 )
1833 .into_any();
1834
1835 Some(token_count)
1836 }
1837 ActiveView::PromptEditor { context_editor, .. } => {
1838 let element = render_remaining_tokens(context_editor, cx)?;
1839
1840 Some(element.into_any_element())
1841 }
1842 _ => None,
1843 }
1844 }
1845
1846 fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
1847 if !matches!(self.active_view, ActiveView::Thread { .. }) {
1848 return false;
1849 }
1850
1851 if self.hide_trial_upsell || dismissed_trial_upsell() {
1852 return false;
1853 }
1854
1855 let is_using_zed_provider = self
1856 .thread
1857 .read(cx)
1858 .thread()
1859 .read(cx)
1860 .configured_model()
1861 .map_or(false, |model| {
1862 model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
1863 });
1864 if !is_using_zed_provider {
1865 return false;
1866 }
1867
1868 let plan = self.user_store.read(cx).current_plan();
1869 if matches!(plan, Some(Plan::ZedPro | Plan::ZedProTrial)) {
1870 return false;
1871 }
1872
1873 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
1874 if has_previous_trial {
1875 return false;
1876 }
1877
1878 true
1879 }
1880
1881 fn render_trial_upsell(
1882 &self,
1883 _window: &mut Window,
1884 cx: &mut Context<Self>,
1885 ) -> Option<impl IntoElement> {
1886 if !self.should_render_upsell(cx) {
1887 return None;
1888 }
1889
1890 let checkbox = CheckboxWithLabel::new(
1891 "dont-show-again",
1892 Label::new("Don't show again").color(Color::Muted),
1893 ToggleState::Unselected,
1894 move |toggle_state, _window, cx| {
1895 let toggle_state_bool = toggle_state.selected();
1896
1897 set_trial_upsell_dismissed(toggle_state_bool, cx);
1898 },
1899 );
1900
1901 Some(
1902 div().p_2().child(
1903 v_flex()
1904 .w_full()
1905 .elevation_2(cx)
1906 .rounded(px(8.))
1907 .bg(cx.theme().colors().background.alpha(0.5))
1908 .p(px(3.))
1909
1910 .child(
1911 div()
1912 .gap_2()
1913 .flex()
1914 .flex_col()
1915 .size_full()
1916 .border_1()
1917 .rounded(px(5.))
1918 .border_color(cx.theme().colors().text.alpha(0.1))
1919 .overflow_hidden()
1920 .relative()
1921 .bg(cx.theme().colors().panel_background)
1922 .px_4()
1923 .py_3()
1924 .child(
1925 div()
1926 .absolute()
1927 .top_0()
1928 .right(px(-1.0))
1929 .w(px(441.))
1930 .h(px(167.))
1931 .child(
1932 Vector::new(VectorName::Grid, rems_from_px(441.), rems_from_px(167.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1)))
1933 )
1934 )
1935 .child(
1936 div()
1937 .absolute()
1938 .top(px(-8.0))
1939 .right_0()
1940 .w(px(400.))
1941 .h(px(92.))
1942 .child(
1943 Vector::new(VectorName::AiGrid, rems_from_px(400.), rems_from_px(92.)).color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32)))
1944 )
1945 )
1946 // .child(
1947 // div()
1948 // .absolute()
1949 // .top_0()
1950 // .right(px(360.))
1951 // .size(px(401.))
1952 // .overflow_hidden()
1953 // .bg(cx.theme().colors().panel_background)
1954 // )
1955 .child(
1956 div()
1957 .absolute()
1958 .top_0()
1959 .right_0()
1960 .w(px(660.))
1961 .h(px(401.))
1962 .overflow_hidden()
1963 .bg(linear_gradient(
1964 75.,
1965 linear_color_stop(cx.theme().colors().panel_background.alpha(0.01), 1.0),
1966 linear_color_stop(cx.theme().colors().panel_background, 0.45),
1967 ))
1968 )
1969 .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
1970 .child(Label::new("Try Zed Pro for free for 14 days - no credit card required.").size(LabelSize::Small))
1971 .child(Label::new("Use your own API keys or enable usage-based billing once you hit the cap.").color(Color::Muted))
1972 .child(
1973 h_flex()
1974 .w_full()
1975 .px_neg_1()
1976 .justify_between()
1977 .items_center()
1978 .child(h_flex().items_center().gap_1().child(checkbox))
1979 .child(
1980 h_flex()
1981 .gap_2()
1982 .child(
1983 Button::new("dismiss-button", "Not Now")
1984 .style(ButtonStyle::Transparent)
1985 .color(Color::Muted)
1986 .on_click({
1987 let assistant_panel = cx.entity();
1988 move |_, _, cx| {
1989 assistant_panel.update(
1990 cx,
1991 |this, cx| {
1992 let hidden =
1993 this.hide_trial_upsell;
1994 println!("hidden: {}", hidden);
1995 this.hide_trial_upsell = true;
1996 let new_hidden =
1997 this.hide_trial_upsell;
1998 println!(
1999 "new_hidden: {}",
2000 new_hidden
2001 );
2002
2003 cx.notify();
2004 },
2005 );
2006 }
2007 }),
2008 )
2009 .child(
2010 Button::new("cta-button", "Start Trial")
2011 .style(ButtonStyle::Transparent)
2012 .on_click(|_, _, cx| {
2013 cx.open_url(&zed_urls::account_url(cx))
2014 }),
2015 ),
2016 ),
2017 ),
2018 ),
2019 ),
2020 )
2021 }
2022
2023 fn render_active_thread_or_empty_state(
2024 &self,
2025 window: &mut Window,
2026 cx: &mut Context<Self>,
2027 ) -> AnyElement {
2028 if self.thread.read(cx).is_empty() {
2029 return self
2030 .render_thread_empty_state(window, cx)
2031 .into_any_element();
2032 }
2033
2034 self.thread.clone().into_any_element()
2035 }
2036
2037 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
2038 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
2039 return Some(ConfigurationError::NoProvider);
2040 };
2041
2042 if !model.provider.is_authenticated(cx) {
2043 return Some(ConfigurationError::ProviderNotAuthenticated);
2044 }
2045
2046 if model.provider.must_accept_terms(cx) {
2047 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
2048 model.provider,
2049 ));
2050 }
2051
2052 None
2053 }
2054
2055 fn render_thread_empty_state(
2056 &self,
2057 window: &mut Window,
2058 cx: &mut Context<Self>,
2059 ) -> impl IntoElement {
2060 let recent_history = self
2061 .history_store
2062 .update(cx, |this, cx| this.recent_entries(6, cx));
2063
2064 let configuration_error = self.configuration_error(cx);
2065 let no_error = configuration_error.is_none();
2066 let focus_handle = self.focus_handle(cx);
2067
2068 v_flex()
2069 .size_full()
2070 .when(recent_history.is_empty(), |this| {
2071 let configuration_error_ref = &configuration_error;
2072 this.child(
2073 v_flex()
2074 .size_full()
2075 .max_w_80()
2076 .mx_auto()
2077 .justify_center()
2078 .items_center()
2079 .gap_1()
2080 .child(
2081 h_flex().child(
2082 Headline::new("Welcome to the Agent Panel")
2083 ),
2084 )
2085 .when(no_error, |parent| {
2086 parent
2087 .child(
2088 h_flex().child(
2089 Label::new("Ask and build anything.")
2090 .color(Color::Muted)
2091 .mb_2p5(),
2092 ),
2093 )
2094 .child(
2095 Button::new("new-thread", "Start New Thread")
2096 .icon(IconName::Plus)
2097 .icon_position(IconPosition::Start)
2098 .icon_size(IconSize::Small)
2099 .icon_color(Color::Muted)
2100 .full_width()
2101 .key_binding(KeyBinding::for_action_in(
2102 &NewThread::default(),
2103 &focus_handle,
2104 window,
2105 cx,
2106 ))
2107 .on_click(|_event, window, cx| {
2108 window.dispatch_action(NewThread::default().boxed_clone(), cx)
2109 }),
2110 )
2111 .child(
2112 Button::new("context", "Add Context")
2113 .icon(IconName::FileCode)
2114 .icon_position(IconPosition::Start)
2115 .icon_size(IconSize::Small)
2116 .icon_color(Color::Muted)
2117 .full_width()
2118 .key_binding(KeyBinding::for_action_in(
2119 &ToggleContextPicker,
2120 &focus_handle,
2121 window,
2122 cx,
2123 ))
2124 .on_click(|_event, window, cx| {
2125 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
2126 }),
2127 )
2128 .child(
2129 Button::new("mode", "Switch Model")
2130 .icon(IconName::DatabaseZap)
2131 .icon_position(IconPosition::Start)
2132 .icon_size(IconSize::Small)
2133 .icon_color(Color::Muted)
2134 .full_width()
2135 .key_binding(KeyBinding::for_action_in(
2136 &ToggleModelSelector,
2137 &focus_handle,
2138 window,
2139 cx,
2140 ))
2141 .on_click(|_event, window, cx| {
2142 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
2143 }),
2144 )
2145 .child(
2146 Button::new("settings", "View Settings")
2147 .icon(IconName::Settings)
2148 .icon_position(IconPosition::Start)
2149 .icon_size(IconSize::Small)
2150 .icon_color(Color::Muted)
2151 .full_width()
2152 .key_binding(KeyBinding::for_action_in(
2153 &OpenConfiguration,
2154 &focus_handle,
2155 window,
2156 cx,
2157 ))
2158 .on_click(|_event, window, cx| {
2159 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2160 }),
2161 )
2162 })
2163 .map(|parent| {
2164 match configuration_error_ref {
2165 Some(ConfigurationError::ProviderNotAuthenticated)
2166 | Some(ConfigurationError::NoProvider) => {
2167 parent
2168 .child(
2169 h_flex().child(
2170 Label::new("To start using the agent, configure at least one LLM provider.")
2171 .color(Color::Muted)
2172 .mb_2p5()
2173 )
2174 )
2175 .child(
2176 Button::new("settings", "Configure a Provider")
2177 .icon(IconName::Settings)
2178 .icon_position(IconPosition::Start)
2179 .icon_size(IconSize::Small)
2180 .icon_color(Color::Muted)
2181 .full_width()
2182 .key_binding(KeyBinding::for_action_in(
2183 &OpenConfiguration,
2184 &focus_handle,
2185 window,
2186 cx,
2187 ))
2188 .on_click(|_event, window, cx| {
2189 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2190 }),
2191 )
2192 }
2193 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2194 parent.children(
2195 provider.render_accept_terms(
2196 LanguageModelProviderTosView::ThreadFreshStart,
2197 cx,
2198 ),
2199 )
2200 }
2201 None => parent,
2202 }
2203 })
2204 )
2205 })
2206 .when(!recent_history.is_empty(), |parent| {
2207 let focus_handle = focus_handle.clone();
2208 let configuration_error_ref = &configuration_error;
2209
2210 parent
2211 .overflow_hidden()
2212 .p_1p5()
2213 .justify_end()
2214 .gap_1()
2215 .child(
2216 h_flex()
2217 .pl_1p5()
2218 .pb_1()
2219 .w_full()
2220 .justify_between()
2221 .border_b_1()
2222 .border_color(cx.theme().colors().border_variant)
2223 .child(
2224 Label::new("Past Interactions")
2225 .size(LabelSize::Small)
2226 .color(Color::Muted),
2227 )
2228 .child(
2229 Button::new("view-history", "View All")
2230 .style(ButtonStyle::Subtle)
2231 .label_size(LabelSize::Small)
2232 .key_binding(
2233 KeyBinding::for_action_in(
2234 &OpenHistory,
2235 &self.focus_handle(cx),
2236 window,
2237 cx,
2238 ).map(|kb| kb.size(rems_from_px(12.))),
2239 )
2240 .on_click(move |_event, window, cx| {
2241 window.dispatch_action(OpenHistory.boxed_clone(), cx);
2242 }),
2243 ),
2244 )
2245 .child(
2246 v_flex()
2247 .gap_1()
2248 .children(
2249 recent_history.into_iter().map(|entry| {
2250 // TODO: Add keyboard navigation.
2251 match entry {
2252 HistoryEntry::Thread(thread) => {
2253 PastThread::new(thread, cx.entity().downgrade(), false, vec![], EntryTimeFormat::DateAndTime)
2254 .into_any_element()
2255 }
2256 HistoryEntry::Context(context) => {
2257 PastContext::new(context, cx.entity().downgrade(), false, vec![], EntryTimeFormat::DateAndTime)
2258 .into_any_element()
2259 }
2260 }
2261 }),
2262 )
2263 )
2264 .map(|parent| {
2265 match configuration_error_ref {
2266 Some(ConfigurationError::ProviderNotAuthenticated)
2267 | Some(ConfigurationError::NoProvider) => {
2268 parent
2269 .child(
2270 Banner::new()
2271 .severity(ui::Severity::Warning)
2272 .child(
2273 Label::new(
2274 "Configure at least one LLM provider to start using the panel.",
2275 )
2276 .size(LabelSize::Small),
2277 )
2278 .action_slot(
2279 Button::new("settings", "Configure Provider")
2280 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2281 .label_size(LabelSize::Small)
2282 .key_binding(
2283 KeyBinding::for_action_in(
2284 &OpenConfiguration,
2285 &focus_handle,
2286 window,
2287 cx,
2288 )
2289 .map(|kb| kb.size(rems_from_px(12.))),
2290 )
2291 .on_click(|_event, window, cx| {
2292 window.dispatch_action(
2293 OpenConfiguration.boxed_clone(),
2294 cx,
2295 )
2296 }),
2297 ),
2298 )
2299 }
2300 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2301 parent
2302 .child(
2303 Banner::new()
2304 .severity(ui::Severity::Warning)
2305 .child(
2306 h_flex()
2307 .w_full()
2308 .children(
2309 provider.render_accept_terms(
2310 LanguageModelProviderTosView::ThreadtEmptyState,
2311 cx,
2312 ),
2313 ),
2314 ),
2315 )
2316 }
2317 None => parent,
2318 }
2319 })
2320 })
2321 }
2322
2323 fn render_tool_use_limit_reached(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2324 let tool_use_limit_reached = self
2325 .thread
2326 .read(cx)
2327 .thread()
2328 .read(cx)
2329 .tool_use_limit_reached();
2330 if !tool_use_limit_reached {
2331 return None;
2332 }
2333
2334 let model = self
2335 .thread
2336 .read(cx)
2337 .thread()
2338 .read(cx)
2339 .configured_model()?
2340 .model;
2341
2342 let max_mode_upsell = if model.supports_max_mode() {
2343 " Enable max mode for unlimited tool use."
2344 } else {
2345 ""
2346 };
2347
2348 Some(
2349 Banner::new()
2350 .severity(ui::Severity::Info)
2351 .child(h_flex().child(Label::new(format!(
2352 "Consecutive tool use limit reached.{max_mode_upsell}"
2353 ))))
2354 .into_any_element(),
2355 )
2356 }
2357
2358 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2359 let last_error = self.thread.read(cx).last_error()?;
2360
2361 Some(
2362 div()
2363 .absolute()
2364 .right_3()
2365 .bottom_12()
2366 .max_w_96()
2367 .py_2()
2368 .px_3()
2369 .elevation_2(cx)
2370 .occlude()
2371 .child(match last_error {
2372 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
2373 ThreadError::MaxMonthlySpendReached => {
2374 self.render_max_monthly_spend_reached_error(cx)
2375 }
2376 ThreadError::ModelRequestLimitReached { plan } => {
2377 self.render_model_request_limit_reached_error(plan, cx)
2378 }
2379 ThreadError::Message { header, message } => {
2380 self.render_error_message(header, message, cx)
2381 }
2382 })
2383 .into_any(),
2384 )
2385 }
2386
2387 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
2388 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.";
2389
2390 v_flex()
2391 .gap_0p5()
2392 .child(
2393 h_flex()
2394 .gap_1p5()
2395 .items_center()
2396 .child(Icon::new(IconName::XCircle).color(Color::Error))
2397 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
2398 )
2399 .child(
2400 div()
2401 .id("error-message")
2402 .max_h_24()
2403 .overflow_y_scroll()
2404 .child(Label::new(ERROR_MESSAGE)),
2405 )
2406 .child(
2407 h_flex()
2408 .justify_end()
2409 .mt_1()
2410 .gap_1()
2411 .child(self.create_copy_button(ERROR_MESSAGE))
2412 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
2413 |this, _, _, cx| {
2414 this.thread.update(cx, |this, _cx| {
2415 this.clear_last_error();
2416 });
2417
2418 cx.open_url(&zed_urls::account_url(cx));
2419 cx.notify();
2420 },
2421 )))
2422 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2423 |this, _, _, cx| {
2424 this.thread.update(cx, |this, _cx| {
2425 this.clear_last_error();
2426 });
2427
2428 cx.notify();
2429 },
2430 ))),
2431 )
2432 .into_any()
2433 }
2434
2435 fn render_max_monthly_spend_reached_error(&self, cx: &mut Context<Self>) -> AnyElement {
2436 const ERROR_MESSAGE: &str = "You have reached your maximum monthly spend. Increase your spend limit to continue using Zed LLMs.";
2437
2438 v_flex()
2439 .gap_0p5()
2440 .child(
2441 h_flex()
2442 .gap_1p5()
2443 .items_center()
2444 .child(Icon::new(IconName::XCircle).color(Color::Error))
2445 .child(Label::new("Max Monthly Spend Reached").weight(FontWeight::MEDIUM)),
2446 )
2447 .child(
2448 div()
2449 .id("error-message")
2450 .max_h_24()
2451 .overflow_y_scroll()
2452 .child(Label::new(ERROR_MESSAGE)),
2453 )
2454 .child(
2455 h_flex()
2456 .justify_end()
2457 .mt_1()
2458 .gap_1()
2459 .child(self.create_copy_button(ERROR_MESSAGE))
2460 .child(
2461 Button::new("subscribe", "Update Monthly Spend Limit").on_click(
2462 cx.listener(|this, _, _, cx| {
2463 this.thread.update(cx, |this, _cx| {
2464 this.clear_last_error();
2465 });
2466
2467 cx.open_url(&zed_urls::account_url(cx));
2468 cx.notify();
2469 }),
2470 ),
2471 )
2472 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2473 |this, _, _, cx| {
2474 this.thread.update(cx, |this, _cx| {
2475 this.clear_last_error();
2476 });
2477
2478 cx.notify();
2479 },
2480 ))),
2481 )
2482 .into_any()
2483 }
2484
2485 fn render_model_request_limit_reached_error(
2486 &self,
2487 plan: Plan,
2488 cx: &mut Context<Self>,
2489 ) -> AnyElement {
2490 let error_message = match plan {
2491 Plan::ZedPro => {
2492 "Model request limit reached. Upgrade to usage-based billing for more requests."
2493 }
2494 Plan::ZedProTrial => {
2495 "Model request limit reached. Upgrade to Zed Pro for more requests."
2496 }
2497 Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
2498 };
2499 let call_to_action = match plan {
2500 Plan::ZedPro => "Upgrade to usage-based billing",
2501 Plan::ZedProTrial => "Upgrade to Zed Pro",
2502 Plan::Free => "Upgrade to Zed Pro",
2503 };
2504
2505 v_flex()
2506 .gap_0p5()
2507 .child(
2508 h_flex()
2509 .gap_1p5()
2510 .items_center()
2511 .child(Icon::new(IconName::XCircle).color(Color::Error))
2512 .child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
2513 )
2514 .child(
2515 div()
2516 .id("error-message")
2517 .max_h_24()
2518 .overflow_y_scroll()
2519 .child(Label::new(error_message)),
2520 )
2521 .child(
2522 h_flex()
2523 .justify_end()
2524 .mt_1()
2525 .gap_1()
2526 .child(self.create_copy_button(error_message))
2527 .child(
2528 Button::new("subscribe", call_to_action).on_click(cx.listener(
2529 |this, _, _, cx| {
2530 this.thread.update(cx, |this, _cx| {
2531 this.clear_last_error();
2532 });
2533
2534 cx.open_url(&zed_urls::account_url(cx));
2535 cx.notify();
2536 },
2537 )),
2538 )
2539 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2540 |this, _, _, cx| {
2541 this.thread.update(cx, |this, _cx| {
2542 this.clear_last_error();
2543 });
2544
2545 cx.notify();
2546 },
2547 ))),
2548 )
2549 .into_any()
2550 }
2551
2552 fn render_error_message(
2553 &self,
2554 header: SharedString,
2555 message: SharedString,
2556 cx: &mut Context<Self>,
2557 ) -> AnyElement {
2558 let message_with_header = format!("{}\n{}", header, message);
2559 v_flex()
2560 .gap_0p5()
2561 .child(
2562 h_flex()
2563 .gap_1p5()
2564 .items_center()
2565 .child(Icon::new(IconName::XCircle).color(Color::Error))
2566 .child(Label::new(header).weight(FontWeight::MEDIUM)),
2567 )
2568 .child(
2569 div()
2570 .id("error-message")
2571 .max_h_32()
2572 .overflow_y_scroll()
2573 .child(Label::new(message.clone())),
2574 )
2575 .child(
2576 h_flex()
2577 .justify_end()
2578 .mt_1()
2579 .gap_1()
2580 .child(self.create_copy_button(message_with_header))
2581 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2582 |this, _, _, cx| {
2583 this.thread.update(cx, |this, _cx| {
2584 this.clear_last_error();
2585 });
2586
2587 cx.notify();
2588 },
2589 ))),
2590 )
2591 .into_any()
2592 }
2593
2594 fn render_prompt_editor(
2595 &self,
2596 context_editor: &Entity<ContextEditor>,
2597 buffer_search_bar: &Entity<BufferSearchBar>,
2598 window: &mut Window,
2599 cx: &mut Context<Self>,
2600 ) -> Div {
2601 let mut registrar = buffer_search::DivRegistrar::new(
2602 |this, _, _cx| match &this.active_view {
2603 ActiveView::PromptEditor {
2604 buffer_search_bar, ..
2605 } => Some(buffer_search_bar.clone()),
2606 _ => None,
2607 },
2608 cx,
2609 );
2610 BufferSearchBar::register(&mut registrar);
2611 registrar
2612 .into_div()
2613 .size_full()
2614 .relative()
2615 .map(|parent| {
2616 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2617 if buffer_search_bar.is_dismissed() {
2618 return parent;
2619 }
2620 parent.child(
2621 div()
2622 .p(DynamicSpacing::Base08.rems(cx))
2623 .border_b_1()
2624 .border_color(cx.theme().colors().border_variant)
2625 .bg(cx.theme().colors().editor_background)
2626 .child(buffer_search_bar.render(window, cx)),
2627 )
2628 })
2629 })
2630 .child(context_editor.clone())
2631 .child(self.render_drag_target(cx))
2632 }
2633
2634 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
2635 let is_local = self.project.read(cx).is_local();
2636 div()
2637 .invisible()
2638 .absolute()
2639 .top_0()
2640 .right_0()
2641 .bottom_0()
2642 .left_0()
2643 .bg(cx.theme().colors().drop_target_background)
2644 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
2645 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
2646 .when(is_local, |this| {
2647 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
2648 })
2649 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
2650 let item = tab.pane.read(cx).item_for_index(tab.ix);
2651 let project_paths = item
2652 .and_then(|item| item.project_path(cx))
2653 .into_iter()
2654 .collect::<Vec<_>>();
2655 this.handle_drop(project_paths, vec![], window, cx);
2656 }))
2657 .on_drop(
2658 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2659 let project_paths = selection
2660 .items()
2661 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
2662 .collect::<Vec<_>>();
2663 this.handle_drop(project_paths, vec![], window, cx);
2664 }),
2665 )
2666 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
2667 let tasks = paths
2668 .paths()
2669 .into_iter()
2670 .map(|path| {
2671 Workspace::project_path_for_path(this.project.clone(), &path, false, cx)
2672 })
2673 .collect::<Vec<_>>();
2674 cx.spawn_in(window, async move |this, cx| {
2675 let mut paths = vec![];
2676 let mut added_worktrees = vec![];
2677 let opened_paths = futures::future::join_all(tasks).await;
2678 for entry in opened_paths {
2679 if let Some((worktree, project_path)) = entry.log_err() {
2680 added_worktrees.push(worktree);
2681 paths.push(project_path);
2682 }
2683 }
2684 this.update_in(cx, |this, window, cx| {
2685 this.handle_drop(paths, added_worktrees, window, cx);
2686 })
2687 .ok();
2688 })
2689 .detach();
2690 }))
2691 }
2692
2693 fn handle_drop(
2694 &mut self,
2695 paths: Vec<ProjectPath>,
2696 added_worktrees: Vec<Entity<Worktree>>,
2697 window: &mut Window,
2698 cx: &mut Context<Self>,
2699 ) {
2700 match &self.active_view {
2701 ActiveView::Thread { .. } => {
2702 let context_store = self.thread.read(cx).context_store().clone();
2703 context_store.update(cx, move |context_store, cx| {
2704 let mut tasks = Vec::new();
2705 for project_path in &paths {
2706 tasks.push(context_store.add_file_from_path(
2707 project_path.clone(),
2708 false,
2709 cx,
2710 ));
2711 }
2712 cx.background_spawn(async move {
2713 futures::future::join_all(tasks).await;
2714 // Need to hold onto the worktrees until they have already been used when
2715 // opening the buffers.
2716 drop(added_worktrees);
2717 })
2718 .detach();
2719 });
2720 }
2721 ActiveView::PromptEditor { context_editor, .. } => {
2722 context_editor.update(cx, |context_editor, cx| {
2723 ContextEditor::insert_dragged_files(
2724 context_editor,
2725 paths,
2726 added_worktrees,
2727 window,
2728 cx,
2729 );
2730 });
2731 }
2732 ActiveView::History | ActiveView::Configuration => {}
2733 }
2734 }
2735
2736 fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
2737 let message = message.into();
2738 IconButton::new("copy", IconName::Copy)
2739 .on_click(move |_, _, cx| {
2740 cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
2741 })
2742 .tooltip(Tooltip::text("Copy Error Message"))
2743 }
2744
2745 fn key_context(&self) -> KeyContext {
2746 let mut key_context = KeyContext::new_with_defaults();
2747 key_context.add("AgentPanel");
2748 if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
2749 key_context.add("prompt_editor");
2750 }
2751 key_context
2752 }
2753}
2754
2755impl Render for AssistantPanel {
2756 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2757 let content = match &self.active_view {
2758 ActiveView::Thread { .. } => v_flex()
2759 .relative()
2760 .justify_between()
2761 .size_full()
2762 .child(self.render_active_thread_or_empty_state(window, cx))
2763 .children(self.render_tool_use_limit_reached(cx))
2764 .child(h_flex().child(self.message_editor.clone()))
2765 .children(self.render_last_error(cx))
2766 .child(self.render_drag_target(cx))
2767 .into_any(),
2768 ActiveView::History => self.history.clone().into_any_element(),
2769 ActiveView::PromptEditor {
2770 context_editor,
2771 buffer_search_bar,
2772 ..
2773 } => self
2774 .render_prompt_editor(context_editor, buffer_search_bar, window, cx)
2775 .into_any(),
2776 ActiveView::Configuration => v_flex()
2777 .size_full()
2778 .children(self.configuration.clone())
2779 .into_any(),
2780 };
2781
2782 let content = match self.active_view.which_font_size_used() {
2783 WhichFontSize::AgentFont => {
2784 WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
2785 .size_full()
2786 .child(content)
2787 .into_any()
2788 }
2789 _ => content,
2790 };
2791
2792 v_flex()
2793 .key_context(self.key_context())
2794 .justify_between()
2795 .size_full()
2796 .on_action(cx.listener(Self::cancel))
2797 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
2798 this.new_thread(action, window, cx);
2799 }))
2800 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
2801 this.open_history(window, cx);
2802 }))
2803 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
2804 this.open_configuration(window, cx);
2805 }))
2806 .on_action(cx.listener(Self::open_active_thread_as_markdown))
2807 .on_action(cx.listener(Self::deploy_rules_library))
2808 .on_action(cx.listener(Self::open_agent_diff))
2809 .on_action(cx.listener(Self::go_back))
2810 .on_action(cx.listener(Self::toggle_navigation_menu))
2811 .on_action(cx.listener(Self::toggle_options_menu))
2812 .on_action(cx.listener(Self::increase_font_size))
2813 .on_action(cx.listener(Self::decrease_font_size))
2814 .on_action(cx.listener(Self::reset_font_size))
2815 .child(self.render_toolbar(window, cx))
2816 .children(self.render_trial_upsell(window, cx))
2817 .child(content)
2818 }
2819}
2820
2821struct PromptLibraryInlineAssist {
2822 workspace: WeakEntity<Workspace>,
2823}
2824
2825impl PromptLibraryInlineAssist {
2826 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
2827 Self { workspace }
2828 }
2829}
2830
2831impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
2832 fn assist(
2833 &self,
2834 prompt_editor: &Entity<Editor>,
2835 initial_prompt: Option<String>,
2836 window: &mut Window,
2837 cx: &mut Context<RulesLibrary>,
2838 ) {
2839 InlineAssistant::update_global(cx, |assistant, cx| {
2840 let Some(project) = self
2841 .workspace
2842 .upgrade()
2843 .map(|workspace| workspace.read(cx).project().downgrade())
2844 else {
2845 return;
2846 };
2847 let prompt_store = None;
2848 let thread_store = None;
2849 let text_thread_store = None;
2850 let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
2851 assistant.assist(
2852 &prompt_editor,
2853 self.workspace.clone(),
2854 context_store,
2855 project,
2856 prompt_store,
2857 thread_store,
2858 text_thread_store,
2859 initial_prompt,
2860 window,
2861 cx,
2862 )
2863 })
2864 }
2865
2866 fn focus_assistant_panel(
2867 &self,
2868 workspace: &mut Workspace,
2869 window: &mut Window,
2870 cx: &mut Context<Workspace>,
2871 ) -> bool {
2872 workspace
2873 .focus_panel::<AssistantPanel>(window, cx)
2874 .is_some()
2875 }
2876}
2877
2878pub struct ConcreteAssistantPanelDelegate;
2879
2880impl AssistantPanelDelegate for ConcreteAssistantPanelDelegate {
2881 fn active_context_editor(
2882 &self,
2883 workspace: &mut Workspace,
2884 _window: &mut Window,
2885 cx: &mut Context<Workspace>,
2886 ) -> Option<Entity<ContextEditor>> {
2887 let panel = workspace.panel::<AssistantPanel>(cx)?;
2888 panel.read(cx).active_context_editor()
2889 }
2890
2891 fn open_saved_context(
2892 &self,
2893 workspace: &mut Workspace,
2894 path: Arc<Path>,
2895 window: &mut Window,
2896 cx: &mut Context<Workspace>,
2897 ) -> Task<Result<()>> {
2898 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2899 return Task::ready(Err(anyhow!("Agent panel not found")));
2900 };
2901
2902 panel.update(cx, |panel, cx| {
2903 panel.open_saved_prompt_editor(path, window, cx)
2904 })
2905 }
2906
2907 fn open_remote_context(
2908 &self,
2909 _workspace: &mut Workspace,
2910 _context_id: assistant_context_editor::ContextId,
2911 _window: &mut Window,
2912 _cx: &mut Context<Workspace>,
2913 ) -> Task<Result<Entity<ContextEditor>>> {
2914 Task::ready(Err(anyhow!("opening remote context not implemented")))
2915 }
2916
2917 fn quote_selection(
2918 &self,
2919 workspace: &mut Workspace,
2920 selection_ranges: Vec<Range<Anchor>>,
2921 buffer: Entity<MultiBuffer>,
2922 window: &mut Window,
2923 cx: &mut Context<Workspace>,
2924 ) {
2925 let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2926 return;
2927 };
2928
2929 if !panel.focus_handle(cx).contains_focused(window, cx) {
2930 workspace.toggle_panel_focus::<AssistantPanel>(window, cx);
2931 }
2932
2933 panel.update(cx, |_, cx| {
2934 // Wait to create a new context until the workspace is no longer
2935 // being updated.
2936 cx.defer_in(window, move |panel, window, cx| {
2937 if panel.has_active_thread() {
2938 panel.message_editor.update(cx, |message_editor, cx| {
2939 message_editor.context_store().update(cx, |store, cx| {
2940 let buffer = buffer.read(cx);
2941 let selection_ranges = selection_ranges
2942 .into_iter()
2943 .flat_map(|range| {
2944 let (start_buffer, start) =
2945 buffer.text_anchor_for_position(range.start, cx)?;
2946 let (end_buffer, end) =
2947 buffer.text_anchor_for_position(range.end, cx)?;
2948 if start_buffer != end_buffer {
2949 return None;
2950 }
2951 Some((start_buffer, start..end))
2952 })
2953 .collect::<Vec<_>>();
2954
2955 for (buffer, range) in selection_ranges {
2956 store.add_selection(buffer, range, cx);
2957 }
2958 })
2959 })
2960 } else if let Some(context_editor) = panel.active_context_editor() {
2961 let snapshot = buffer.read(cx).snapshot(cx);
2962 let selection_ranges = selection_ranges
2963 .into_iter()
2964 .map(|range| range.to_point(&snapshot))
2965 .collect::<Vec<_>>();
2966
2967 context_editor.update(cx, |context_editor, cx| {
2968 context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
2969 });
2970 }
2971 });
2972 });
2973 }
2974}
2975
2976const DISMISSED_TRIAL_UPSELL_KEY: &str = "dismissed-trial-upsell";
2977
2978fn dismissed_trial_upsell() -> bool {
2979 db::kvp::KEY_VALUE_STORE
2980 .read_kvp(DISMISSED_TRIAL_UPSELL_KEY)
2981 .log_err()
2982 .map_or(false, |s| s.is_some())
2983}
2984
2985fn set_trial_upsell_dismissed(is_dismissed: bool, cx: &mut App) {
2986 db::write_and_log(cx, move || async move {
2987 if is_dismissed {
2988 db::kvp::KEY_VALUE_STORE
2989 .write_kvp(DISMISSED_TRIAL_UPSELL_KEY.into(), "1".into())
2990 .await
2991 } else {
2992 db::kvp::KEY_VALUE_STORE
2993 .delete_kvp(DISMISSED_TRIAL_UPSELL_KEY.into())
2994 .await
2995 }
2996 })
2997}