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