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