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