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