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