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