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