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