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