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