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