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