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