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