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