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