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