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