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 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, DefaultView};
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 = AssistantSettings::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::<AssistantSettings>(
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 AssistantSettings::get_global(cx).dock {
1385 AssistantDockPosition::Left => DockPosition::Left,
1386 AssistantDockPosition::Bottom => DockPosition::Bottom,
1387 AssistantDockPosition::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::<AssistantSettings>(
1408 self.fs.clone(),
1409 cx,
1410 move |settings, _| {
1411 let dock = match position {
1412 DockPosition::Left => AssistantDockPosition::Left,
1413 DockPosition::Bottom => AssistantDockPosition::Bottom,
1414 DockPosition::Right => AssistantDockPosition::Right,
1415 };
1416 settings.set_dock(dock);
1417 },
1418 );
1419 }
1420
1421 fn size(&self, window: &Window, cx: &App) -> Pixels {
1422 let settings = AssistantSettings::get_global(cx);
1423 match self.position(window, cx) {
1424 DockPosition::Left | DockPosition::Right => {
1425 self.width.unwrap_or(settings.default_width)
1426 }
1427 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1428 }
1429 }
1430
1431 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1432 match self.position(window, cx) {
1433 DockPosition::Left | DockPosition::Right => self.width = size,
1434 DockPosition::Bottom => self.height = size,
1435 }
1436 self.serialize(cx);
1437 cx.notify();
1438 }
1439
1440 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
1441
1442 fn remote_id() -> Option<proto::PanelId> {
1443 Some(proto::PanelId::AssistantPanel)
1444 }
1445
1446 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1447 (self.enabled(cx) && AssistantSettings::get_global(cx).button)
1448 .then_some(IconName::ZedAssistant)
1449 }
1450
1451 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1452 Some("Agent Panel")
1453 }
1454
1455 fn toggle_action(&self) -> Box<dyn Action> {
1456 Box::new(ToggleFocus)
1457 }
1458
1459 fn activation_priority(&self) -> u32 {
1460 3
1461 }
1462
1463 fn enabled(&self, cx: &App) -> bool {
1464 AssistantSettings::get_global(cx).enabled
1465 }
1466
1467 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1468 self.zoomed
1469 }
1470
1471 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1472 self.zoomed = zoomed;
1473 cx.notify();
1474 }
1475}
1476
1477impl AgentPanel {
1478 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1479 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1480
1481 let content = match &self.active_view {
1482 ActiveView::Thread {
1483 change_title_editor,
1484 ..
1485 } => {
1486 let active_thread = self.thread.read(cx);
1487 let state = if active_thread.is_empty() {
1488 &ThreadSummary::Pending
1489 } else {
1490 active_thread.summary(cx)
1491 };
1492
1493 match state {
1494 ThreadSummary::Pending => Label::new(ThreadSummary::DEFAULT.clone())
1495 .truncate()
1496 .into_any_element(),
1497 ThreadSummary::Generating => Label::new(LOADING_SUMMARY_PLACEHOLDER)
1498 .truncate()
1499 .into_any_element(),
1500 ThreadSummary::Ready(_) => div()
1501 .w_full()
1502 .child(change_title_editor.clone())
1503 .into_any_element(),
1504 ThreadSummary::Error => h_flex()
1505 .w_full()
1506 .child(change_title_editor.clone())
1507 .child(
1508 ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
1509 .on_click({
1510 let active_thread = self.thread.clone();
1511 move |_, _window, cx| {
1512 active_thread.update(cx, |thread, cx| {
1513 thread.regenerate_summary(cx);
1514 });
1515 }
1516 })
1517 .tooltip(move |_window, cx| {
1518 cx.new(|_| {
1519 Tooltip::new("Failed to generate title")
1520 .meta("Click to try again")
1521 })
1522 .into()
1523 }),
1524 )
1525 .into_any_element(),
1526 }
1527 }
1528 ActiveView::PromptEditor {
1529 title_editor,
1530 context_editor,
1531 ..
1532 } => {
1533 let summary = context_editor.read(cx).context().read(cx).summary();
1534
1535 match summary {
1536 ContextSummary::Pending => Label::new(ContextSummary::DEFAULT)
1537 .truncate()
1538 .into_any_element(),
1539 ContextSummary::Content(summary) => {
1540 if summary.done {
1541 div()
1542 .w_full()
1543 .child(title_editor.clone())
1544 .into_any_element()
1545 } else {
1546 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1547 .truncate()
1548 .into_any_element()
1549 }
1550 }
1551 ContextSummary::Error => h_flex()
1552 .w_full()
1553 .child(title_editor.clone())
1554 .child(
1555 ui::IconButton::new("retry-summary-generation", IconName::RotateCcw)
1556 .on_click({
1557 let context_editor = context_editor.clone();
1558 move |_, _window, cx| {
1559 context_editor.update(cx, |context_editor, cx| {
1560 context_editor.regenerate_summary(cx);
1561 });
1562 }
1563 })
1564 .tooltip(move |_window, cx| {
1565 cx.new(|_| {
1566 Tooltip::new("Failed to generate title")
1567 .meta("Click to try again")
1568 })
1569 .into()
1570 }),
1571 )
1572 .into_any_element(),
1573 }
1574 }
1575 ActiveView::History => Label::new("History").truncate().into_any_element(),
1576 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1577 };
1578
1579 h_flex()
1580 .key_context("TitleEditor")
1581 .id("TitleEditor")
1582 .flex_grow()
1583 .w_full()
1584 .max_w_full()
1585 .overflow_x_scroll()
1586 .child(content)
1587 .into_any()
1588 }
1589
1590 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1591 let active_thread = self.thread.read(cx);
1592 let user_store = self.user_store.read(cx);
1593 let thread = active_thread.thread().read(cx);
1594 let thread_id = thread.id().clone();
1595 let is_empty = active_thread.is_empty();
1596 let editor_empty = self.message_editor.read(cx).is_editor_fully_empty(cx);
1597 let last_usage = active_thread.thread().read(cx).last_usage().or_else(|| {
1598 maybe!({
1599 let amount = user_store.model_request_usage_amount()?;
1600 let limit = user_store.model_request_usage_limit()?.variant?;
1601
1602 Some(RequestUsage {
1603 amount: amount as i32,
1604 limit: match limit {
1605 proto::usage_limit::Variant::Limited(limited) => {
1606 zed_llm_client::UsageLimit::Limited(limited.limit as i32)
1607 }
1608 proto::usage_limit::Variant::Unlimited(_) => {
1609 zed_llm_client::UsageLimit::Unlimited
1610 }
1611 },
1612 })
1613 })
1614 });
1615
1616 let account_url = zed_urls::account_url(cx);
1617
1618 let show_token_count = match &self.active_view {
1619 ActiveView::Thread { .. } => !is_empty || !editor_empty,
1620 ActiveView::PromptEditor { .. } => true,
1621 _ => false,
1622 };
1623
1624 let focus_handle = self.focus_handle(cx);
1625
1626 let go_back_button = div().child(
1627 IconButton::new("go-back", IconName::ArrowLeft)
1628 .icon_size(IconSize::Small)
1629 .on_click(cx.listener(|this, _, window, cx| {
1630 this.go_back(&workspace::GoBack, window, cx);
1631 }))
1632 .tooltip({
1633 let focus_handle = focus_handle.clone();
1634 move |window, cx| {
1635 Tooltip::for_action_in(
1636 "Go Back",
1637 &workspace::GoBack,
1638 &focus_handle,
1639 window,
1640 cx,
1641 )
1642 }
1643 }),
1644 );
1645
1646 let recent_entries_menu = div().child(
1647 PopoverMenu::new("agent-nav-menu")
1648 .trigger_with_tooltip(
1649 IconButton::new("agent-nav-menu", IconName::MenuAlt)
1650 .icon_size(IconSize::Small)
1651 .style(ui::ButtonStyle::Subtle),
1652 {
1653 let focus_handle = focus_handle.clone();
1654 move |window, cx| {
1655 Tooltip::for_action_in(
1656 "Toggle Panel Menu",
1657 &ToggleNavigationMenu,
1658 &focus_handle,
1659 window,
1660 cx,
1661 )
1662 }
1663 },
1664 )
1665 .anchor(Corner::TopLeft)
1666 .with_handle(self.assistant_navigation_menu_handle.clone())
1667 .menu({
1668 let menu = self.assistant_navigation_menu.clone();
1669 move |window, cx| {
1670 if let Some(menu) = menu.as_ref() {
1671 menu.update(cx, |_, cx| {
1672 cx.defer_in(window, |menu, window, cx| {
1673 menu.rebuild(window, cx);
1674 });
1675 })
1676 }
1677 menu.clone()
1678 }
1679 }),
1680 );
1681
1682 let zoom_in_label = if self.is_zoomed(window, cx) {
1683 "Zoom Out"
1684 } else {
1685 "Zoom In"
1686 };
1687
1688 let agent_extra_menu = PopoverMenu::new("agent-options-menu")
1689 .trigger_with_tooltip(
1690 IconButton::new("agent-options-menu", IconName::Ellipsis)
1691 .icon_size(IconSize::Small),
1692 {
1693 let focus_handle = focus_handle.clone();
1694 move |window, cx| {
1695 Tooltip::for_action_in(
1696 "Toggle Agent Menu",
1697 &ToggleOptionsMenu,
1698 &focus_handle,
1699 window,
1700 cx,
1701 )
1702 }
1703 },
1704 )
1705 .anchor(Corner::TopRight)
1706 .with_handle(self.assistant_dropdown_menu_handle.clone())
1707 .menu(move |window, cx| {
1708 Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| {
1709 menu = menu
1710 .action("New Thread", NewThread::default().boxed_clone())
1711 .action("New Text Thread", NewTextThread.boxed_clone())
1712 .when(!is_empty, |menu| {
1713 menu.action(
1714 "New From Summary",
1715 Box::new(NewThread {
1716 from_thread_id: Some(thread_id.clone()),
1717 }),
1718 )
1719 })
1720 .separator();
1721
1722 menu = menu
1723 .header("MCP Servers")
1724 .action(
1725 "View Server Extensions",
1726 Box::new(zed_actions::Extensions {
1727 category_filter: Some(
1728 zed_actions::ExtensionCategoryFilter::ContextServers,
1729 ),
1730 }),
1731 )
1732 .action("Add Custom Server…", Box::new(AddContextServer))
1733 .separator();
1734
1735 if let Some(usage) = last_usage {
1736 menu = menu
1737 .header_with_link("Prompt Usage", "Manage", account_url.clone())
1738 .custom_entry(
1739 move |_window, cx| {
1740 let used_percentage = match usage.limit {
1741 UsageLimit::Limited(limit) => {
1742 Some((usage.amount as f32 / limit as f32) * 100.)
1743 }
1744 UsageLimit::Unlimited => None,
1745 };
1746
1747 h_flex()
1748 .flex_1()
1749 .gap_1p5()
1750 .children(used_percentage.map(|percent| {
1751 ProgressBar::new("usage", percent, 100., cx)
1752 }))
1753 .child(
1754 Label::new(match usage.limit {
1755 UsageLimit::Limited(limit) => {
1756 format!("{} / {limit}", usage.amount)
1757 }
1758 UsageLimit::Unlimited => {
1759 format!("{} / ∞", usage.amount)
1760 }
1761 })
1762 .size(LabelSize::Small)
1763 .color(Color::Muted),
1764 )
1765 .into_any_element()
1766 },
1767 move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
1768 )
1769 .separator()
1770 }
1771
1772 menu = menu
1773 .action("Rules…", Box::new(OpenRulesLibrary::default()))
1774 .action("Settings", Box::new(OpenConfiguration))
1775 .action(zoom_in_label, Box::new(ToggleZoom));
1776 menu
1777 }))
1778 });
1779
1780 h_flex()
1781 .id("assistant-toolbar")
1782 .h(Tab::container_height(cx))
1783 .max_w_full()
1784 .flex_none()
1785 .justify_between()
1786 .gap_2()
1787 .bg(cx.theme().colors().tab_bar_background)
1788 .border_b_1()
1789 .border_color(cx.theme().colors().border)
1790 .child(
1791 h_flex()
1792 .size_full()
1793 .pl_1()
1794 .gap_1()
1795 .child(match &self.active_view {
1796 ActiveView::History | ActiveView::Configuration => go_back_button,
1797 _ => recent_entries_menu,
1798 })
1799 .child(self.render_title_view(window, cx)),
1800 )
1801 .child(
1802 h_flex()
1803 .h_full()
1804 .gap_2()
1805 .when(show_token_count, |parent| {
1806 parent.children(self.render_token_count(&thread, cx))
1807 })
1808 .child(
1809 h_flex()
1810 .h_full()
1811 .gap(DynamicSpacing::Base02.rems(cx))
1812 .px(DynamicSpacing::Base08.rems(cx))
1813 .border_l_1()
1814 .border_color(cx.theme().colors().border)
1815 .child(
1816 IconButton::new("new", IconName::Plus)
1817 .icon_size(IconSize::Small)
1818 .style(ButtonStyle::Subtle)
1819 .tooltip(move |window, cx| {
1820 Tooltip::for_action_in(
1821 "New Thread",
1822 &NewThread::default(),
1823 &focus_handle,
1824 window,
1825 cx,
1826 )
1827 })
1828 .on_click(move |_event, window, cx| {
1829 window.dispatch_action(
1830 NewThread::default().boxed_clone(),
1831 cx,
1832 );
1833 }),
1834 )
1835 .child(agent_extra_menu),
1836 ),
1837 )
1838 }
1839
1840 fn render_token_count(&self, thread: &Thread, cx: &App) -> Option<AnyElement> {
1841 let is_generating = thread.is_generating();
1842 let message_editor = self.message_editor.read(cx);
1843
1844 let conversation_token_usage = thread.total_token_usage()?;
1845
1846 let (total_token_usage, is_estimating) = if let Some((editing_message_id, unsent_tokens)) =
1847 self.thread.read(cx).editing_message_id()
1848 {
1849 let combined = thread
1850 .token_usage_up_to_message(editing_message_id)
1851 .add(unsent_tokens);
1852
1853 (combined, unsent_tokens > 0)
1854 } else {
1855 let unsent_tokens = message_editor.last_estimated_token_count().unwrap_or(0);
1856 let combined = conversation_token_usage.add(unsent_tokens);
1857
1858 (combined, unsent_tokens > 0)
1859 };
1860
1861 let is_waiting_to_update_token_count = message_editor.is_waiting_to_update_token_count();
1862
1863 match &self.active_view {
1864 ActiveView::Thread { .. } => {
1865 if total_token_usage.total == 0 {
1866 return None;
1867 }
1868
1869 let token_color = match total_token_usage.ratio() {
1870 TokenUsageRatio::Normal if is_estimating => Color::Default,
1871 TokenUsageRatio::Normal => Color::Muted,
1872 TokenUsageRatio::Warning => Color::Warning,
1873 TokenUsageRatio::Exceeded => Color::Error,
1874 };
1875
1876 let token_count = h_flex()
1877 .id("token-count")
1878 .flex_shrink_0()
1879 .gap_0p5()
1880 .when(!is_generating && is_estimating, |parent| {
1881 parent
1882 .child(
1883 h_flex()
1884 .mr_1()
1885 .size_2p5()
1886 .justify_center()
1887 .rounded_full()
1888 .bg(cx.theme().colors().text.opacity(0.1))
1889 .child(
1890 div().size_1().rounded_full().bg(cx.theme().colors().text),
1891 ),
1892 )
1893 .tooltip(move |window, cx| {
1894 Tooltip::with_meta(
1895 "Estimated New Token Count",
1896 None,
1897 format!(
1898 "Current Conversation Tokens: {}",
1899 humanize_token_count(conversation_token_usage.total)
1900 ),
1901 window,
1902 cx,
1903 )
1904 })
1905 })
1906 .child(
1907 Label::new(humanize_token_count(total_token_usage.total))
1908 .size(LabelSize::Small)
1909 .color(token_color)
1910 .map(|label| {
1911 if is_generating || is_waiting_to_update_token_count {
1912 label
1913 .with_animation(
1914 "used-tokens-label",
1915 Animation::new(Duration::from_secs(2))
1916 .repeat()
1917 .with_easing(pulsating_between(0.6, 1.)),
1918 |label, delta| label.alpha(delta),
1919 )
1920 .into_any()
1921 } else {
1922 label.into_any_element()
1923 }
1924 }),
1925 )
1926 .child(Label::new("/").size(LabelSize::Small).color(Color::Muted))
1927 .child(
1928 Label::new(humanize_token_count(total_token_usage.max))
1929 .size(LabelSize::Small)
1930 .color(Color::Muted),
1931 )
1932 .into_any();
1933
1934 Some(token_count)
1935 }
1936 ActiveView::PromptEditor { context_editor, .. } => {
1937 let element = render_remaining_tokens(context_editor, cx)?;
1938
1939 Some(element.into_any_element())
1940 }
1941 _ => None,
1942 }
1943 }
1944
1945 fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
1946 if TrialEndUpsell::dismissed() {
1947 return false;
1948 }
1949
1950 let plan = self.user_store.read(cx).current_plan();
1951 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
1952
1953 matches!(plan, Some(Plan::Free)) && has_previous_trial
1954 }
1955
1956 fn should_render_upsell(&self, cx: &mut Context<Self>) -> bool {
1957 if !matches!(self.active_view, ActiveView::Thread { .. }) {
1958 return false;
1959 }
1960
1961 if self.hide_upsell || Upsell::dismissed() {
1962 return false;
1963 }
1964
1965 let is_using_zed_provider = self
1966 .thread
1967 .read(cx)
1968 .thread()
1969 .read(cx)
1970 .configured_model()
1971 .map_or(false, |model| {
1972 model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
1973 });
1974 if !is_using_zed_provider {
1975 return false;
1976 }
1977
1978 let plan = self.user_store.read(cx).current_plan();
1979 if matches!(plan, Some(Plan::ZedPro | Plan::ZedProTrial)) {
1980 return false;
1981 }
1982
1983 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
1984 if has_previous_trial {
1985 return false;
1986 }
1987
1988 true
1989 }
1990
1991 fn render_upsell(
1992 &self,
1993 _window: &mut Window,
1994 cx: &mut Context<Self>,
1995 ) -> Option<impl IntoElement> {
1996 if !self.should_render_upsell(cx) {
1997 return None;
1998 }
1999
2000 if self.user_store.read(cx).account_too_young() {
2001 Some(self.render_young_account_upsell(cx).into_any_element())
2002 } else {
2003 Some(self.render_trial_upsell(cx).into_any_element())
2004 }
2005 }
2006
2007 fn render_young_account_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
2008 let checkbox = CheckboxWithLabel::new(
2009 "dont-show-again",
2010 Label::new("Don't show again").color(Color::Muted),
2011 ToggleState::Unselected,
2012 move |toggle_state, _window, cx| {
2013 let toggle_state_bool = toggle_state.selected();
2014
2015 Upsell::set_dismissed(toggle_state_bool, cx);
2016 },
2017 );
2018
2019 let contents = div()
2020 .size_full()
2021 .gap_2()
2022 .flex()
2023 .flex_col()
2024 .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
2025 .child(
2026 Label::new("Your GitHub account was created less than 30 days ago, so we can't offer you a free trial.")
2027 .size(LabelSize::Small),
2028 )
2029 .child(
2030 Label::new(
2031 "Use your own API keys, upgrade to Zed Pro or send an email to billing-support@zed.dev.",
2032 )
2033 .color(Color::Muted),
2034 )
2035 .child(
2036 h_flex()
2037 .w_full()
2038 .px_neg_1()
2039 .justify_between()
2040 .items_center()
2041 .child(h_flex().items_center().gap_1().child(checkbox))
2042 .child(
2043 h_flex()
2044 .gap_2()
2045 .child(
2046 Button::new("dismiss-button", "Not Now")
2047 .style(ButtonStyle::Transparent)
2048 .color(Color::Muted)
2049 .on_click({
2050 let agent_panel = cx.entity();
2051 move |_, _, cx| {
2052 agent_panel.update(cx, |this, cx| {
2053 this.hide_upsell = true;
2054 cx.notify();
2055 });
2056 }
2057 }),
2058 )
2059 .child(
2060 Button::new("cta-button", "Upgrade to Zed Pro")
2061 .style(ButtonStyle::Transparent)
2062 .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
2063 ),
2064 ),
2065 );
2066
2067 self.render_upsell_container(cx, contents)
2068 }
2069
2070 fn render_trial_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
2071 let checkbox = CheckboxWithLabel::new(
2072 "dont-show-again",
2073 Label::new("Don't show again").color(Color::Muted),
2074 ToggleState::Unselected,
2075 move |toggle_state, _window, cx| {
2076 let toggle_state_bool = toggle_state.selected();
2077
2078 Upsell::set_dismissed(toggle_state_bool, cx);
2079 },
2080 );
2081
2082 let contents = div()
2083 .size_full()
2084 .gap_2()
2085 .flex()
2086 .flex_col()
2087 .child(Headline::new("Build better with Zed Pro").size(HeadlineSize::Small))
2088 .child(
2089 Label::new("Try Zed Pro for free for 14 days - no credit card required.")
2090 .size(LabelSize::Small),
2091 )
2092 .child(
2093 Label::new(
2094 "Use your own API keys or enable usage-based billing once you hit the cap.",
2095 )
2096 .color(Color::Muted),
2097 )
2098 .child(
2099 h_flex()
2100 .w_full()
2101 .px_neg_1()
2102 .justify_between()
2103 .items_center()
2104 .child(h_flex().items_center().gap_1().child(checkbox))
2105 .child(
2106 h_flex()
2107 .gap_2()
2108 .child(
2109 Button::new("dismiss-button", "Not Now")
2110 .style(ButtonStyle::Transparent)
2111 .color(Color::Muted)
2112 .on_click({
2113 let agent_panel = cx.entity();
2114 move |_, _, cx| {
2115 agent_panel.update(cx, |this, cx| {
2116 this.hide_upsell = true;
2117 cx.notify();
2118 });
2119 }
2120 }),
2121 )
2122 .child(
2123 Button::new("cta-button", "Start Trial")
2124 .style(ButtonStyle::Transparent)
2125 .on_click(|_, _, cx| cx.open_url(&zed_urls::account_url(cx))),
2126 ),
2127 ),
2128 );
2129
2130 self.render_upsell_container(cx, contents)
2131 }
2132
2133 fn render_trial_end_upsell(
2134 &self,
2135 _window: &mut Window,
2136 cx: &mut Context<Self>,
2137 ) -> Option<impl IntoElement> {
2138 if !self.should_render_trial_end_upsell(cx) {
2139 return None;
2140 }
2141
2142 Some(
2143 self.render_upsell_container(
2144 cx,
2145 div()
2146 .size_full()
2147 .gap_2()
2148 .flex()
2149 .flex_col()
2150 .child(
2151 Headline::new("Your Zed Pro trial has expired.").size(HeadlineSize::Small),
2152 )
2153 .child(
2154 Label::new("You've been automatically reset to the free plan.")
2155 .size(LabelSize::Small),
2156 )
2157 .child(
2158 h_flex()
2159 .w_full()
2160 .px_neg_1()
2161 .justify_between()
2162 .items_center()
2163 .child(div())
2164 .child(
2165 h_flex()
2166 .gap_2()
2167 .child(
2168 Button::new("dismiss-button", "Stay on Free")
2169 .style(ButtonStyle::Transparent)
2170 .color(Color::Muted)
2171 .on_click({
2172 let agent_panel = cx.entity();
2173 move |_, _, cx| {
2174 agent_panel.update(cx, |_this, cx| {
2175 TrialEndUpsell::set_dismissed(true, cx);
2176 cx.notify();
2177 });
2178 }
2179 }),
2180 )
2181 .child(
2182 Button::new("cta-button", "Upgrade to Zed Pro")
2183 .style(ButtonStyle::Transparent)
2184 .on_click(|_, _, cx| {
2185 cx.open_url(&zed_urls::account_url(cx))
2186 }),
2187 ),
2188 ),
2189 ),
2190 ),
2191 )
2192 }
2193
2194 fn render_upsell_container(&self, cx: &mut Context<Self>, content: Div) -> Div {
2195 div().p_2().child(
2196 v_flex()
2197 .w_full()
2198 .elevation_2(cx)
2199 .rounded(px(8.))
2200 .bg(cx.theme().colors().background.alpha(0.5))
2201 .p(px(3.))
2202 .child(
2203 div()
2204 .gap_2()
2205 .flex()
2206 .flex_col()
2207 .size_full()
2208 .border_1()
2209 .rounded(px(5.))
2210 .border_color(cx.theme().colors().text.alpha(0.1))
2211 .overflow_hidden()
2212 .relative()
2213 .bg(cx.theme().colors().panel_background)
2214 .px_4()
2215 .py_3()
2216 .child(
2217 div()
2218 .absolute()
2219 .top_0()
2220 .right(px(-1.0))
2221 .w(px(441.))
2222 .h(px(167.))
2223 .child(
2224 Vector::new(
2225 VectorName::Grid,
2226 rems_from_px(441.),
2227 rems_from_px(167.),
2228 )
2229 .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.1))),
2230 ),
2231 )
2232 .child(
2233 div()
2234 .absolute()
2235 .top(px(-8.0))
2236 .right_0()
2237 .w(px(400.))
2238 .h(px(92.))
2239 .child(
2240 Vector::new(
2241 VectorName::AiGrid,
2242 rems_from_px(400.),
2243 rems_from_px(92.),
2244 )
2245 .color(ui::Color::Custom(cx.theme().colors().text.alpha(0.32))),
2246 ),
2247 )
2248 // .child(
2249 // div()
2250 // .absolute()
2251 // .top_0()
2252 // .right(px(360.))
2253 // .size(px(401.))
2254 // .overflow_hidden()
2255 // .bg(cx.theme().colors().panel_background)
2256 // )
2257 .child(
2258 div()
2259 .absolute()
2260 .top_0()
2261 .right_0()
2262 .w(px(660.))
2263 .h(px(401.))
2264 .overflow_hidden()
2265 .bg(linear_gradient(
2266 75.,
2267 linear_color_stop(
2268 cx.theme().colors().panel_background.alpha(0.01),
2269 1.0,
2270 ),
2271 linear_color_stop(cx.theme().colors().panel_background, 0.45),
2272 )),
2273 )
2274 .child(content),
2275 ),
2276 )
2277 }
2278
2279 fn render_active_thread_or_empty_state(
2280 &self,
2281 window: &mut Window,
2282 cx: &mut Context<Self>,
2283 ) -> AnyElement {
2284 if self.thread.read(cx).is_empty() {
2285 return self
2286 .render_thread_empty_state(window, cx)
2287 .into_any_element();
2288 }
2289
2290 self.thread.clone().into_any_element()
2291 }
2292
2293 fn configuration_error(&self, cx: &App) -> Option<ConfigurationError> {
2294 let Some(model) = LanguageModelRegistry::read_global(cx).default_model() else {
2295 return Some(ConfigurationError::NoProvider);
2296 };
2297
2298 if !model.provider.is_authenticated(cx) {
2299 return Some(ConfigurationError::ProviderNotAuthenticated);
2300 }
2301
2302 if model.provider.must_accept_terms(cx) {
2303 return Some(ConfigurationError::ProviderPendingTermsAcceptance(
2304 model.provider,
2305 ));
2306 }
2307
2308 None
2309 }
2310
2311 fn render_thread_empty_state(
2312 &self,
2313 window: &mut Window,
2314 cx: &mut Context<Self>,
2315 ) -> impl IntoElement {
2316 let recent_history = self
2317 .history_store
2318 .update(cx, |this, cx| this.recent_entries(6, cx));
2319
2320 let configuration_error = self.configuration_error(cx);
2321 let no_error = configuration_error.is_none();
2322 let focus_handle = self.focus_handle(cx);
2323
2324 v_flex()
2325 .size_full()
2326 .bg(cx.theme().colors().panel_background)
2327 .when(recent_history.is_empty(), |this| {
2328 let configuration_error_ref = &configuration_error;
2329 this.child(
2330 v_flex()
2331 .size_full()
2332 .max_w_80()
2333 .mx_auto()
2334 .justify_center()
2335 .items_center()
2336 .gap_1()
2337 .child(
2338 h_flex().child(
2339 Headline::new("Welcome to the Agent Panel")
2340 ),
2341 )
2342 .when(no_error, |parent| {
2343 parent
2344 .child(
2345 h_flex().child(
2346 Label::new("Ask and build anything.")
2347 .color(Color::Muted)
2348 .mb_2p5(),
2349 ),
2350 )
2351 .child(
2352 Button::new("new-thread", "Start New Thread")
2353 .icon(IconName::Plus)
2354 .icon_position(IconPosition::Start)
2355 .icon_size(IconSize::Small)
2356 .icon_color(Color::Muted)
2357 .full_width()
2358 .key_binding(KeyBinding::for_action_in(
2359 &NewThread::default(),
2360 &focus_handle,
2361 window,
2362 cx,
2363 ))
2364 .on_click(|_event, window, cx| {
2365 window.dispatch_action(NewThread::default().boxed_clone(), cx)
2366 }),
2367 )
2368 .child(
2369 Button::new("context", "Add Context")
2370 .icon(IconName::FileCode)
2371 .icon_position(IconPosition::Start)
2372 .icon_size(IconSize::Small)
2373 .icon_color(Color::Muted)
2374 .full_width()
2375 .key_binding(KeyBinding::for_action_in(
2376 &ToggleContextPicker,
2377 &focus_handle,
2378 window,
2379 cx,
2380 ))
2381 .on_click(|_event, window, cx| {
2382 window.dispatch_action(ToggleContextPicker.boxed_clone(), cx)
2383 }),
2384 )
2385 .child(
2386 Button::new("mode", "Switch Model")
2387 .icon(IconName::DatabaseZap)
2388 .icon_position(IconPosition::Start)
2389 .icon_size(IconSize::Small)
2390 .icon_color(Color::Muted)
2391 .full_width()
2392 .key_binding(KeyBinding::for_action_in(
2393 &ToggleModelSelector,
2394 &focus_handle,
2395 window,
2396 cx,
2397 ))
2398 .on_click(|_event, window, cx| {
2399 window.dispatch_action(ToggleModelSelector.boxed_clone(), cx)
2400 }),
2401 )
2402 .child(
2403 Button::new("settings", "View Settings")
2404 .icon(IconName::Settings)
2405 .icon_position(IconPosition::Start)
2406 .icon_size(IconSize::Small)
2407 .icon_color(Color::Muted)
2408 .full_width()
2409 .key_binding(KeyBinding::for_action_in(
2410 &OpenConfiguration,
2411 &focus_handle,
2412 window,
2413 cx,
2414 ))
2415 .on_click(|_event, window, cx| {
2416 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2417 }),
2418 )
2419 })
2420 .map(|parent| {
2421 match configuration_error_ref {
2422 Some(ConfigurationError::ProviderNotAuthenticated)
2423 | Some(ConfigurationError::NoProvider) => {
2424 parent
2425 .child(
2426 h_flex().child(
2427 Label::new("To start using the agent, configure at least one LLM provider.")
2428 .color(Color::Muted)
2429 .mb_2p5()
2430 )
2431 )
2432 .child(
2433 Button::new("settings", "Configure a Provider")
2434 .icon(IconName::Settings)
2435 .icon_position(IconPosition::Start)
2436 .icon_size(IconSize::Small)
2437 .icon_color(Color::Muted)
2438 .full_width()
2439 .key_binding(KeyBinding::for_action_in(
2440 &OpenConfiguration,
2441 &focus_handle,
2442 window,
2443 cx,
2444 ))
2445 .on_click(|_event, window, cx| {
2446 window.dispatch_action(OpenConfiguration.boxed_clone(), cx)
2447 }),
2448 )
2449 }
2450 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2451 parent.children(
2452 provider.render_accept_terms(
2453 LanguageModelProviderTosView::ThreadFreshStart,
2454 cx,
2455 ),
2456 )
2457 }
2458 None => parent,
2459 }
2460 })
2461 )
2462 })
2463 .when(!recent_history.is_empty(), |parent| {
2464 let focus_handle = focus_handle.clone();
2465 let configuration_error_ref = &configuration_error;
2466
2467 parent
2468 .overflow_hidden()
2469 .p_1p5()
2470 .justify_end()
2471 .gap_1()
2472 .child(
2473 h_flex()
2474 .pl_1p5()
2475 .pb_1()
2476 .w_full()
2477 .justify_between()
2478 .border_b_1()
2479 .border_color(cx.theme().colors().border_variant)
2480 .child(
2481 Label::new("Recent")
2482 .size(LabelSize::Small)
2483 .color(Color::Muted),
2484 )
2485 .child(
2486 Button::new("view-history", "View All")
2487 .style(ButtonStyle::Subtle)
2488 .label_size(LabelSize::Small)
2489 .key_binding(
2490 KeyBinding::for_action_in(
2491 &OpenHistory,
2492 &self.focus_handle(cx),
2493 window,
2494 cx,
2495 ).map(|kb| kb.size(rems_from_px(12.))),
2496 )
2497 .on_click(move |_event, window, cx| {
2498 window.dispatch_action(OpenHistory.boxed_clone(), cx);
2499 }),
2500 ),
2501 )
2502 .child(
2503 v_flex()
2504 .gap_1()
2505 .children(
2506 recent_history.into_iter().enumerate().map(|(index, entry)| {
2507 // TODO: Add keyboard navigation.
2508 let is_hovered = self.hovered_recent_history_item == Some(index);
2509 HistoryEntryElement::new(entry.clone(), cx.entity().downgrade())
2510 .hovered(is_hovered)
2511 .on_hover(cx.listener(move |this, is_hovered, _window, cx| {
2512 if *is_hovered {
2513 this.hovered_recent_history_item = Some(index);
2514 } else if this.hovered_recent_history_item == Some(index) {
2515 this.hovered_recent_history_item = None;
2516 }
2517 cx.notify();
2518 }))
2519 .into_any_element()
2520 }),
2521 )
2522 )
2523 .map(|parent| {
2524 match configuration_error_ref {
2525 Some(ConfigurationError::ProviderNotAuthenticated)
2526 | Some(ConfigurationError::NoProvider) => {
2527 parent
2528 .child(
2529 Banner::new()
2530 .severity(ui::Severity::Warning)
2531 .child(
2532 Label::new(
2533 "Configure at least one LLM provider to start using the panel.",
2534 )
2535 .size(LabelSize::Small),
2536 )
2537 .action_slot(
2538 Button::new("settings", "Configure Provider")
2539 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2540 .label_size(LabelSize::Small)
2541 .key_binding(
2542 KeyBinding::for_action_in(
2543 &OpenConfiguration,
2544 &focus_handle,
2545 window,
2546 cx,
2547 )
2548 .map(|kb| kb.size(rems_from_px(12.))),
2549 )
2550 .on_click(|_event, window, cx| {
2551 window.dispatch_action(
2552 OpenConfiguration.boxed_clone(),
2553 cx,
2554 )
2555 }),
2556 ),
2557 )
2558 }
2559 Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
2560 parent
2561 .child(
2562 Banner::new()
2563 .severity(ui::Severity::Warning)
2564 .child(
2565 h_flex()
2566 .w_full()
2567 .children(
2568 provider.render_accept_terms(
2569 LanguageModelProviderTosView::ThreadtEmptyState,
2570 cx,
2571 ),
2572 ),
2573 ),
2574 )
2575 }
2576 None => parent,
2577 }
2578 })
2579 })
2580 }
2581
2582 fn render_tool_use_limit_reached(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2583 let tool_use_limit_reached = self
2584 .thread
2585 .read(cx)
2586 .thread()
2587 .read(cx)
2588 .tool_use_limit_reached();
2589 if !tool_use_limit_reached {
2590 return None;
2591 }
2592
2593 let model = self
2594 .thread
2595 .read(cx)
2596 .thread()
2597 .read(cx)
2598 .configured_model()?
2599 .model;
2600
2601 let max_mode_upsell = if model.supports_max_mode() {
2602 " Enable max mode for unlimited tool use."
2603 } else {
2604 ""
2605 };
2606
2607 let banner = Banner::new()
2608 .severity(ui::Severity::Info)
2609 .child(h_flex().child(Label::new(format!(
2610 "Consecutive tool use limit reached.{max_mode_upsell}"
2611 ))));
2612
2613 Some(div().px_2().pb_2().child(banner).into_any_element())
2614 }
2615
2616 fn render_last_error(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
2617 let last_error = self.thread.read(cx).last_error()?;
2618
2619 Some(
2620 div()
2621 .absolute()
2622 .right_3()
2623 .bottom_12()
2624 .max_w_96()
2625 .py_2()
2626 .px_3()
2627 .elevation_2(cx)
2628 .occlude()
2629 .child(match last_error {
2630 ThreadError::PaymentRequired => self.render_payment_required_error(cx),
2631 ThreadError::ModelRequestLimitReached { plan } => {
2632 self.render_model_request_limit_reached_error(plan, cx)
2633 }
2634 ThreadError::Message { header, message } => {
2635 self.render_error_message(header, message, cx)
2636 }
2637 })
2638 .into_any(),
2639 )
2640 }
2641
2642 fn render_payment_required_error(&self, cx: &mut Context<Self>) -> AnyElement {
2643 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.";
2644
2645 v_flex()
2646 .gap_0p5()
2647 .child(
2648 h_flex()
2649 .gap_1p5()
2650 .items_center()
2651 .child(Icon::new(IconName::XCircle).color(Color::Error))
2652 .child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
2653 )
2654 .child(
2655 div()
2656 .id("error-message")
2657 .max_h_24()
2658 .overflow_y_scroll()
2659 .child(Label::new(ERROR_MESSAGE)),
2660 )
2661 .child(
2662 h_flex()
2663 .justify_end()
2664 .mt_1()
2665 .gap_1()
2666 .child(self.create_copy_button(ERROR_MESSAGE))
2667 .child(Button::new("subscribe", "Subscribe").on_click(cx.listener(
2668 |this, _, _, cx| {
2669 this.thread.update(cx, |this, _cx| {
2670 this.clear_last_error();
2671 });
2672
2673 cx.open_url(&zed_urls::account_url(cx));
2674 cx.notify();
2675 },
2676 )))
2677 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2678 |this, _, _, cx| {
2679 this.thread.update(cx, |this, _cx| {
2680 this.clear_last_error();
2681 });
2682
2683 cx.notify();
2684 },
2685 ))),
2686 )
2687 .into_any()
2688 }
2689
2690 fn render_model_request_limit_reached_error(
2691 &self,
2692 plan: Plan,
2693 cx: &mut Context<Self>,
2694 ) -> AnyElement {
2695 let error_message = match plan {
2696 Plan::ZedPro => {
2697 "Model request limit reached. Upgrade to usage-based billing for more requests."
2698 }
2699 Plan::ZedProTrial => {
2700 "Model request limit reached. Upgrade to Zed Pro for more requests."
2701 }
2702 Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
2703 };
2704 let call_to_action = match plan {
2705 Plan::ZedPro => "Upgrade to usage-based billing",
2706 Plan::ZedProTrial => "Upgrade to Zed Pro",
2707 Plan::Free => "Upgrade to Zed Pro",
2708 };
2709
2710 v_flex()
2711 .gap_0p5()
2712 .child(
2713 h_flex()
2714 .gap_1p5()
2715 .items_center()
2716 .child(Icon::new(IconName::XCircle).color(Color::Error))
2717 .child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
2718 )
2719 .child(
2720 div()
2721 .id("error-message")
2722 .max_h_24()
2723 .overflow_y_scroll()
2724 .child(Label::new(error_message)),
2725 )
2726 .child(
2727 h_flex()
2728 .justify_end()
2729 .mt_1()
2730 .gap_1()
2731 .child(self.create_copy_button(error_message))
2732 .child(
2733 Button::new("subscribe", call_to_action).on_click(cx.listener(
2734 |this, _, _, cx| {
2735 this.thread.update(cx, |this, _cx| {
2736 this.clear_last_error();
2737 });
2738
2739 cx.open_url(&zed_urls::account_url(cx));
2740 cx.notify();
2741 },
2742 )),
2743 )
2744 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2745 |this, _, _, cx| {
2746 this.thread.update(cx, |this, _cx| {
2747 this.clear_last_error();
2748 });
2749
2750 cx.notify();
2751 },
2752 ))),
2753 )
2754 .into_any()
2755 }
2756
2757 fn render_error_message(
2758 &self,
2759 header: SharedString,
2760 message: SharedString,
2761 cx: &mut Context<Self>,
2762 ) -> AnyElement {
2763 let message_with_header = format!("{}\n{}", header, message);
2764 v_flex()
2765 .gap_0p5()
2766 .child(
2767 h_flex()
2768 .gap_1p5()
2769 .items_center()
2770 .child(Icon::new(IconName::XCircle).color(Color::Error))
2771 .child(Label::new(header).weight(FontWeight::MEDIUM)),
2772 )
2773 .child(
2774 div()
2775 .id("error-message")
2776 .max_h_32()
2777 .overflow_y_scroll()
2778 .child(Label::new(message.clone())),
2779 )
2780 .child(
2781 h_flex()
2782 .justify_end()
2783 .mt_1()
2784 .gap_1()
2785 .child(self.create_copy_button(message_with_header))
2786 .child(Button::new("dismiss", "Dismiss").on_click(cx.listener(
2787 |this, _, _, cx| {
2788 this.thread.update(cx, |this, _cx| {
2789 this.clear_last_error();
2790 });
2791
2792 cx.notify();
2793 },
2794 ))),
2795 )
2796 .into_any()
2797 }
2798
2799 fn render_prompt_editor(
2800 &self,
2801 context_editor: &Entity<ContextEditor>,
2802 buffer_search_bar: &Entity<BufferSearchBar>,
2803 window: &mut Window,
2804 cx: &mut Context<Self>,
2805 ) -> Div {
2806 let mut registrar = buffer_search::DivRegistrar::new(
2807 |this, _, _cx| match &this.active_view {
2808 ActiveView::PromptEditor {
2809 buffer_search_bar, ..
2810 } => Some(buffer_search_bar.clone()),
2811 _ => None,
2812 },
2813 cx,
2814 );
2815 BufferSearchBar::register(&mut registrar);
2816 registrar
2817 .into_div()
2818 .size_full()
2819 .relative()
2820 .map(|parent| {
2821 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2822 if buffer_search_bar.is_dismissed() {
2823 return parent;
2824 }
2825 parent.child(
2826 div()
2827 .p(DynamicSpacing::Base08.rems(cx))
2828 .border_b_1()
2829 .border_color(cx.theme().colors().border_variant)
2830 .bg(cx.theme().colors().editor_background)
2831 .child(buffer_search_bar.render(window, cx)),
2832 )
2833 })
2834 })
2835 .child(context_editor.clone())
2836 .child(self.render_drag_target(cx))
2837 }
2838
2839 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
2840 let is_local = self.project.read(cx).is_local();
2841 div()
2842 .invisible()
2843 .absolute()
2844 .top_0()
2845 .right_0()
2846 .bottom_0()
2847 .left_0()
2848 .bg(cx.theme().colors().drop_target_background)
2849 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
2850 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
2851 .when(is_local, |this| {
2852 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
2853 })
2854 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
2855 let item = tab.pane.read(cx).item_for_index(tab.ix);
2856 let project_paths = item
2857 .and_then(|item| item.project_path(cx))
2858 .into_iter()
2859 .collect::<Vec<_>>();
2860 this.handle_drop(project_paths, vec![], window, cx);
2861 }))
2862 .on_drop(
2863 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2864 let project_paths = selection
2865 .items()
2866 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
2867 .collect::<Vec<_>>();
2868 this.handle_drop(project_paths, vec![], window, cx);
2869 }),
2870 )
2871 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
2872 let tasks = paths
2873 .paths()
2874 .into_iter()
2875 .map(|path| {
2876 Workspace::project_path_for_path(this.project.clone(), &path, false, cx)
2877 })
2878 .collect::<Vec<_>>();
2879 cx.spawn_in(window, async move |this, cx| {
2880 let mut paths = vec![];
2881 let mut added_worktrees = vec![];
2882 let opened_paths = futures::future::join_all(tasks).await;
2883 for entry in opened_paths {
2884 if let Some((worktree, project_path)) = entry.log_err() {
2885 added_worktrees.push(worktree);
2886 paths.push(project_path);
2887 }
2888 }
2889 this.update_in(cx, |this, window, cx| {
2890 this.handle_drop(paths, added_worktrees, window, cx);
2891 })
2892 .ok();
2893 })
2894 .detach();
2895 }))
2896 }
2897
2898 fn handle_drop(
2899 &mut self,
2900 paths: Vec<ProjectPath>,
2901 added_worktrees: Vec<Entity<Worktree>>,
2902 window: &mut Window,
2903 cx: &mut Context<Self>,
2904 ) {
2905 match &self.active_view {
2906 ActiveView::Thread { .. } => {
2907 let context_store = self.thread.read(cx).context_store().clone();
2908 context_store.update(cx, move |context_store, cx| {
2909 let mut tasks = Vec::new();
2910 for project_path in &paths {
2911 tasks.push(context_store.add_file_from_path(
2912 project_path.clone(),
2913 false,
2914 cx,
2915 ));
2916 }
2917 cx.background_spawn(async move {
2918 futures::future::join_all(tasks).await;
2919 // Need to hold onto the worktrees until they have already been used when
2920 // opening the buffers.
2921 drop(added_worktrees);
2922 })
2923 .detach();
2924 });
2925 }
2926 ActiveView::PromptEditor { context_editor, .. } => {
2927 context_editor.update(cx, |context_editor, cx| {
2928 ContextEditor::insert_dragged_files(
2929 context_editor,
2930 paths,
2931 added_worktrees,
2932 window,
2933 cx,
2934 );
2935 });
2936 }
2937 ActiveView::History | ActiveView::Configuration => {}
2938 }
2939 }
2940
2941 fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
2942 let message = message.into();
2943 IconButton::new("copy", IconName::Copy)
2944 .on_click(move |_, _, cx| {
2945 cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
2946 })
2947 .tooltip(Tooltip::text("Copy Error Message"))
2948 }
2949
2950 fn key_context(&self) -> KeyContext {
2951 let mut key_context = KeyContext::new_with_defaults();
2952 key_context.add("AgentPanel");
2953 if matches!(self.active_view, ActiveView::PromptEditor { .. }) {
2954 key_context.add("prompt_editor");
2955 }
2956 key_context
2957 }
2958}
2959
2960impl Render for AgentPanel {
2961 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2962 // WARNING: Changes to this element hierarchy can have
2963 // non-obvious implications to the layout of children.
2964 //
2965 // If you need to change it, please confirm:
2966 // - The message editor expands (⌘esc) correctly
2967 // - When expanded, the buttons at the bottom of the panel are displayed correctly
2968 // - Font size works as expected and can be changed with ⌘+/⌘-
2969 // - Scrolling in all views works as expected
2970 // - Files can be dropped into the panel
2971 let content = v_flex()
2972 .key_context(self.key_context())
2973 .justify_between()
2974 .size_full()
2975 .on_action(cx.listener(Self::cancel))
2976 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
2977 this.new_thread(action, window, cx);
2978 }))
2979 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
2980 this.open_history(window, cx);
2981 }))
2982 .on_action(cx.listener(|this, _: &OpenConfiguration, window, cx| {
2983 this.open_configuration(window, cx);
2984 }))
2985 .on_action(cx.listener(Self::open_active_thread_as_markdown))
2986 .on_action(cx.listener(Self::deploy_rules_library))
2987 .on_action(cx.listener(Self::open_agent_diff))
2988 .on_action(cx.listener(Self::go_back))
2989 .on_action(cx.listener(Self::toggle_navigation_menu))
2990 .on_action(cx.listener(Self::toggle_options_menu))
2991 .on_action(cx.listener(Self::increase_font_size))
2992 .on_action(cx.listener(Self::decrease_font_size))
2993 .on_action(cx.listener(Self::reset_font_size))
2994 .on_action(cx.listener(Self::toggle_zoom))
2995 .child(self.render_toolbar(window, cx))
2996 .children(self.render_upsell(window, cx))
2997 .children(self.render_trial_end_upsell(window, cx))
2998 .map(|parent| match &self.active_view {
2999 ActiveView::Thread { .. } => parent
3000 .relative()
3001 .child(self.render_active_thread_or_empty_state(window, cx))
3002 .children(self.render_tool_use_limit_reached(cx))
3003 .child(h_flex().child(self.message_editor.clone()))
3004 .children(self.render_last_error(cx))
3005 .child(self.render_drag_target(cx)),
3006 ActiveView::History => parent.child(self.history.clone()),
3007 ActiveView::PromptEditor {
3008 context_editor,
3009 buffer_search_bar,
3010 ..
3011 } => parent.child(self.render_prompt_editor(
3012 context_editor,
3013 buffer_search_bar,
3014 window,
3015 cx,
3016 )),
3017 ActiveView::Configuration => parent.children(self.configuration.clone()),
3018 });
3019
3020 match self.active_view.which_font_size_used() {
3021 WhichFontSize::AgentFont => {
3022 WithRemSize::new(ThemeSettings::get_global(cx).agent_font_size(cx))
3023 .size_full()
3024 .child(content)
3025 .into_any()
3026 }
3027 _ => content.into_any(),
3028 }
3029 }
3030}
3031
3032struct PromptLibraryInlineAssist {
3033 workspace: WeakEntity<Workspace>,
3034}
3035
3036impl PromptLibraryInlineAssist {
3037 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3038 Self { workspace }
3039 }
3040}
3041
3042impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3043 fn assist(
3044 &self,
3045 prompt_editor: &Entity<Editor>,
3046 initial_prompt: Option<String>,
3047 window: &mut Window,
3048 cx: &mut Context<RulesLibrary>,
3049 ) {
3050 InlineAssistant::update_global(cx, |assistant, cx| {
3051 let Some(project) = self
3052 .workspace
3053 .upgrade()
3054 .map(|workspace| workspace.read(cx).project().downgrade())
3055 else {
3056 return;
3057 };
3058 let prompt_store = None;
3059 let thread_store = None;
3060 let text_thread_store = None;
3061 let context_store = cx.new(|_| ContextStore::new(project.clone(), None));
3062 assistant.assist(
3063 &prompt_editor,
3064 self.workspace.clone(),
3065 context_store,
3066 project,
3067 prompt_store,
3068 thread_store,
3069 text_thread_store,
3070 initial_prompt,
3071 window,
3072 cx,
3073 )
3074 })
3075 }
3076
3077 fn focus_agent_panel(
3078 &self,
3079 workspace: &mut Workspace,
3080 window: &mut Window,
3081 cx: &mut Context<Workspace>,
3082 ) -> bool {
3083 workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3084 }
3085}
3086
3087pub struct ConcreteAssistantPanelDelegate;
3088
3089impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3090 fn active_context_editor(
3091 &self,
3092 workspace: &mut Workspace,
3093 _window: &mut Window,
3094 cx: &mut Context<Workspace>,
3095 ) -> Option<Entity<ContextEditor>> {
3096 let panel = workspace.panel::<AgentPanel>(cx)?;
3097 panel.read(cx).active_context_editor()
3098 }
3099
3100 fn open_saved_context(
3101 &self,
3102 workspace: &mut Workspace,
3103 path: Arc<Path>,
3104 window: &mut Window,
3105 cx: &mut Context<Workspace>,
3106 ) -> Task<Result<()>> {
3107 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3108 return Task::ready(Err(anyhow!("Agent panel not found")));
3109 };
3110
3111 panel.update(cx, |panel, cx| {
3112 panel.open_saved_prompt_editor(path, window, cx)
3113 })
3114 }
3115
3116 fn open_remote_context(
3117 &self,
3118 _workspace: &mut Workspace,
3119 _context_id: assistant_context_editor::ContextId,
3120 _window: &mut Window,
3121 _cx: &mut Context<Workspace>,
3122 ) -> Task<Result<Entity<ContextEditor>>> {
3123 Task::ready(Err(anyhow!("opening remote context not implemented")))
3124 }
3125
3126 fn quote_selection(
3127 &self,
3128 workspace: &mut Workspace,
3129 selection_ranges: Vec<Range<Anchor>>,
3130 buffer: Entity<MultiBuffer>,
3131 window: &mut Window,
3132 cx: &mut Context<Workspace>,
3133 ) {
3134 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3135 return;
3136 };
3137
3138 if !panel.focus_handle(cx).contains_focused(window, cx) {
3139 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3140 }
3141
3142 panel.update(cx, |_, cx| {
3143 // Wait to create a new context until the workspace is no longer
3144 // being updated.
3145 cx.defer_in(window, move |panel, window, cx| {
3146 if panel.has_active_thread() {
3147 panel.message_editor.update(cx, |message_editor, cx| {
3148 message_editor.context_store().update(cx, |store, cx| {
3149 let buffer = buffer.read(cx);
3150 let selection_ranges = selection_ranges
3151 .into_iter()
3152 .flat_map(|range| {
3153 let (start_buffer, start) =
3154 buffer.text_anchor_for_position(range.start, cx)?;
3155 let (end_buffer, end) =
3156 buffer.text_anchor_for_position(range.end, cx)?;
3157 if start_buffer != end_buffer {
3158 return None;
3159 }
3160 Some((start_buffer, start..end))
3161 })
3162 .collect::<Vec<_>>();
3163
3164 for (buffer, range) in selection_ranges {
3165 store.add_selection(buffer, range, cx);
3166 }
3167 })
3168 })
3169 } else if let Some(context_editor) = panel.active_context_editor() {
3170 let snapshot = buffer.read(cx).snapshot(cx);
3171 let selection_ranges = selection_ranges
3172 .into_iter()
3173 .map(|range| range.to_point(&snapshot))
3174 .collect::<Vec<_>>();
3175
3176 context_editor.update(cx, |context_editor, cx| {
3177 context_editor.quote_ranges(selection_ranges, snapshot, window, cx)
3178 });
3179 }
3180 });
3181 });
3182 }
3183}
3184
3185struct Upsell;
3186
3187impl Dismissable for Upsell {
3188 const KEY: &'static str = "dismissed-trial-upsell";
3189}
3190
3191struct TrialEndUpsell;
3192
3193impl Dismissable for TrialEndUpsell {
3194 const KEY: &'static str = "dismissed-trial-end-upsell";
3195}