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