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