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