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 if let Some(workspace) = self.workspace.upgrade() {
1213 workspace.update(cx, |workspace, cx| {
1214 struct NoThreadToast;
1215 workspace.show_toast(
1216 workspace::Toast::new(
1217 workspace::notifications::NotificationId::unique::<NoThreadToast>(),
1218 "No active native thread to copy",
1219 )
1220 .autohide(),
1221 cx,
1222 );
1223 });
1224 }
1225 return;
1226 };
1227
1228 let workspace = self.workspace.clone();
1229 let load_task = thread.read(cx).to_db(cx);
1230
1231 cx.spawn_in(window, async move |_this, cx| {
1232 let db_thread = load_task.await;
1233 let shared_thread = SharedThread::from_db_thread(&db_thread);
1234 let thread_data = shared_thread.to_bytes()?;
1235 let encoded = base64::Engine::encode(&base64::prelude::BASE64_STANDARD, &thread_data);
1236
1237 cx.update(|_window, cx| {
1238 cx.write_to_clipboard(ClipboardItem::new_string(encoded));
1239 if let Some(workspace) = workspace.upgrade() {
1240 workspace.update(cx, |workspace, cx| {
1241 struct ThreadCopiedToast;
1242 workspace.show_toast(
1243 workspace::Toast::new(
1244 workspace::notifications::NotificationId::unique::<ThreadCopiedToast>(),
1245 "Thread copied to clipboard (base64 encoded)",
1246 )
1247 .autohide(),
1248 cx,
1249 );
1250 });
1251 }
1252 })?;
1253
1254 anyhow::Ok(())
1255 })
1256 .detach_and_log_err(cx);
1257 }
1258
1259 fn load_thread_from_clipboard(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1260 let Some(clipboard) = cx.read_from_clipboard() else {
1261 if let Some(workspace) = self.workspace.upgrade() {
1262 workspace.update(cx, |workspace, cx| {
1263 struct NoClipboardToast;
1264 workspace.show_toast(
1265 workspace::Toast::new(
1266 workspace::notifications::NotificationId::unique::<NoClipboardToast>(),
1267 "No clipboard content available",
1268 )
1269 .autohide(),
1270 cx,
1271 );
1272 });
1273 }
1274 return;
1275 };
1276
1277 let Some(encoded) = clipboard.text() else {
1278 if let Some(workspace) = self.workspace.upgrade() {
1279 workspace.update(cx, |workspace, cx| {
1280 struct InvalidClipboardToast;
1281 workspace.show_toast(
1282 workspace::Toast::new(
1283 workspace::notifications::NotificationId::unique::<InvalidClipboardToast>(),
1284 "Clipboard does not contain text",
1285 )
1286 .autohide(),
1287 cx,
1288 );
1289 });
1290 }
1291 return;
1292 };
1293
1294 let thread_data = match base64::Engine::decode(&base64::prelude::BASE64_STANDARD, &encoded)
1295 {
1296 Ok(data) => data,
1297 Err(_) => {
1298 if let Some(workspace) = self.workspace.upgrade() {
1299 workspace.update(cx, |workspace, cx| {
1300 struct DecodeErrorToast;
1301 workspace.show_toast(
1302 workspace::Toast::new(
1303 workspace::notifications::NotificationId::unique::<DecodeErrorToast>(),
1304 "Failed to decode clipboard content (expected base64)",
1305 )
1306 .autohide(),
1307 cx,
1308 );
1309 });
1310 }
1311 return;
1312 }
1313 };
1314
1315 let shared_thread = match SharedThread::from_bytes(&thread_data) {
1316 Ok(thread) => thread,
1317 Err(_) => {
1318 if let Some(workspace) = self.workspace.upgrade() {
1319 workspace.update(cx, |workspace, cx| {
1320 struct ParseErrorToast;
1321 workspace.show_toast(
1322 workspace::Toast::new(
1323 workspace::notifications::NotificationId::unique::<ParseErrorToast>(
1324 ),
1325 "Failed to parse thread data from clipboard",
1326 )
1327 .autohide(),
1328 cx,
1329 );
1330 });
1331 }
1332 return;
1333 }
1334 };
1335
1336 let db_thread = shared_thread.to_db_thread();
1337 let session_id = acp::SessionId::new(uuid::Uuid::new_v4().to_string());
1338 let thread_store = self.thread_store.clone();
1339 let title = db_thread.title.clone();
1340 let workspace = self.workspace.clone();
1341
1342 cx.spawn_in(window, async move |this, cx| {
1343 thread_store
1344 .update(&mut cx.clone(), |store, cx| {
1345 store.save_thread(session_id.clone(), db_thread, cx)
1346 })
1347 .await?;
1348
1349 let thread_metadata = acp_thread::AgentSessionInfo {
1350 session_id,
1351 cwd: None,
1352 title: Some(title),
1353 updated_at: Some(chrono::Utc::now()),
1354 meta: None,
1355 };
1356
1357 this.update_in(cx, |this, window, cx| {
1358 this.open_thread(thread_metadata, window, cx);
1359 })?;
1360
1361 this.update_in(cx, |_, _window, cx| {
1362 if let Some(workspace) = workspace.upgrade() {
1363 workspace.update(cx, |workspace, cx| {
1364 struct ThreadLoadedToast;
1365 workspace.show_toast(
1366 workspace::Toast::new(
1367 workspace::notifications::NotificationId::unique::<ThreadLoadedToast>(),
1368 "Thread loaded from clipboard",
1369 )
1370 .autohide(),
1371 cx,
1372 );
1373 });
1374 }
1375 })?;
1376
1377 anyhow::Ok(())
1378 })
1379 .detach_and_log_err(cx);
1380 }
1381
1382 fn handle_agent_configuration_event(
1383 &mut self,
1384 _entity: &Entity<AgentConfiguration>,
1385 event: &AssistantConfigurationEvent,
1386 window: &mut Window,
1387 cx: &mut Context<Self>,
1388 ) {
1389 match event {
1390 AssistantConfigurationEvent::NewThread(provider) => {
1391 if LanguageModelRegistry::read_global(cx)
1392 .default_model()
1393 .is_none_or(|model| model.provider.id() != provider.id())
1394 && let Some(model) = provider.default_model(cx)
1395 {
1396 update_settings_file(self.fs.clone(), cx, move |settings, _| {
1397 let provider = model.provider_id().0.to_string();
1398 let model = model.id().0.to_string();
1399 settings
1400 .agent
1401 .get_or_insert_default()
1402 .set_model(LanguageModelSelection {
1403 provider: LanguageModelProviderSetting(provider),
1404 model,
1405 enable_thinking: false,
1406 effort: None,
1407 })
1408 });
1409 }
1410
1411 self.new_thread(&NewThread, window, cx);
1412 if let Some((thread, model)) = self
1413 .active_native_agent_thread(cx)
1414 .zip(provider.default_model(cx))
1415 {
1416 thread.update(cx, |thread, cx| {
1417 thread.set_model(model, cx);
1418 });
1419 }
1420 }
1421 }
1422 }
1423
1424 pub(crate) fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
1425 match &self.active_view {
1426 ActiveView::AgentThread { thread_view, .. } => thread_view
1427 .read(cx)
1428 .active_thread()
1429 .map(|r| r.read(cx).thread.clone()),
1430 _ => None,
1431 }
1432 }
1433
1434 pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
1435 match &self.active_view {
1436 ActiveView::AgentThread { thread_view, .. } => {
1437 thread_view.read(cx).as_native_thread(cx)
1438 }
1439 _ => None,
1440 }
1441 }
1442
1443 pub(crate) fn active_text_thread_editor(&self) -> Option<Entity<TextThreadEditor>> {
1444 match &self.active_view {
1445 ActiveView::TextThread {
1446 text_thread_editor, ..
1447 } => Some(text_thread_editor.clone()),
1448 _ => None,
1449 }
1450 }
1451
1452 fn set_active_view(
1453 &mut self,
1454 new_view: ActiveView,
1455 focus: bool,
1456 window: &mut Window,
1457 cx: &mut Context<Self>,
1458 ) {
1459 let current_is_uninitialized = matches!(self.active_view, ActiveView::Uninitialized);
1460 let current_is_history = matches!(self.active_view, ActiveView::History { .. });
1461 let new_is_history = matches!(new_view, ActiveView::History { .. });
1462
1463 let current_is_config = matches!(self.active_view, ActiveView::Configuration);
1464 let new_is_config = matches!(new_view, ActiveView::Configuration);
1465
1466 let current_is_special = current_is_history || current_is_config;
1467 let new_is_special = new_is_history || new_is_config;
1468
1469 if current_is_uninitialized || (current_is_special && !new_is_special) {
1470 self.active_view = new_view;
1471 } else if !current_is_special && new_is_special {
1472 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1473 } else {
1474 if !new_is_special {
1475 self.previous_view = None;
1476 }
1477 self.active_view = new_view;
1478 }
1479
1480 if focus {
1481 self.focus_handle(cx).focus(window, cx);
1482 }
1483 }
1484
1485 fn populate_recently_updated_menu_section(
1486 mut menu: ContextMenu,
1487 panel: Entity<Self>,
1488 kind: HistoryKind,
1489 cx: &mut Context<ContextMenu>,
1490 ) -> ContextMenu {
1491 match kind {
1492 HistoryKind::AgentThreads => {
1493 let entries = panel
1494 .read(cx)
1495 .acp_history
1496 .read(cx)
1497 .sessions()
1498 .iter()
1499 .take(RECENTLY_UPDATED_MENU_LIMIT)
1500 .cloned()
1501 .collect::<Vec<_>>();
1502
1503 if entries.is_empty() {
1504 return menu;
1505 }
1506
1507 menu = menu.header("Recently Updated");
1508
1509 for entry in entries {
1510 let title = entry
1511 .title
1512 .as_ref()
1513 .filter(|title| !title.is_empty())
1514 .cloned()
1515 .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE));
1516
1517 menu = menu.entry(title, None, {
1518 let panel = panel.downgrade();
1519 let entry = entry.clone();
1520 move |window, cx| {
1521 let entry = entry.clone();
1522 panel
1523 .update(cx, move |this, cx| {
1524 this.load_agent_thread(entry.clone(), window, cx);
1525 })
1526 .ok();
1527 }
1528 });
1529 }
1530 }
1531 HistoryKind::TextThreads => {
1532 let entries = panel
1533 .read(cx)
1534 .text_thread_store
1535 .read(cx)
1536 .ordered_text_threads()
1537 .take(RECENTLY_UPDATED_MENU_LIMIT)
1538 .cloned()
1539 .collect::<Vec<_>>();
1540
1541 if entries.is_empty() {
1542 return menu;
1543 }
1544
1545 menu = menu.header("Recent Text Threads");
1546
1547 for entry in entries {
1548 let title = if entry.title.is_empty() {
1549 SharedString::new_static(DEFAULT_THREAD_TITLE)
1550 } else {
1551 entry.title.clone()
1552 };
1553
1554 menu = menu.entry(title, None, {
1555 let panel = panel.downgrade();
1556 let entry = entry.clone();
1557 move |window, cx| {
1558 let path = entry.path.clone();
1559 panel
1560 .update(cx, move |this, cx| {
1561 this.open_saved_text_thread(path.clone(), window, cx)
1562 .detach_and_log_err(cx);
1563 })
1564 .ok();
1565 }
1566 });
1567 }
1568 }
1569 }
1570
1571 menu.separator()
1572 }
1573
1574 pub fn selected_agent(&self) -> AgentType {
1575 self.selected_agent.clone()
1576 }
1577
1578 fn selected_external_agent(&self) -> Option<ExternalAgent> {
1579 match &self.selected_agent {
1580 AgentType::NativeAgent => Some(ExternalAgent::NativeAgent),
1581 AgentType::Gemini => Some(ExternalAgent::Gemini),
1582 AgentType::ClaudeAgent => Some(ExternalAgent::ClaudeCode),
1583 AgentType::Codex => Some(ExternalAgent::Codex),
1584 AgentType::Custom { name } => Some(ExternalAgent::Custom { name: name.clone() }),
1585 AgentType::TextThread => None,
1586 }
1587 }
1588
1589 fn sync_agent_servers_from_extensions(&mut self, cx: &mut Context<Self>) {
1590 if let Some(extension_store) = ExtensionStore::try_global(cx) {
1591 let (manifests, extensions_dir) = {
1592 let store = extension_store.read(cx);
1593 let installed = store.installed_extensions();
1594 let manifests: Vec<_> = installed
1595 .iter()
1596 .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
1597 .collect();
1598 let extensions_dir = paths::extensions_dir().join("installed");
1599 (manifests, extensions_dir)
1600 };
1601
1602 self.project.update(cx, |project, cx| {
1603 project.agent_server_store().update(cx, |store, cx| {
1604 let manifest_refs: Vec<_> = manifests
1605 .iter()
1606 .map(|(id, manifest)| (id.as_ref(), manifest.as_ref()))
1607 .collect();
1608 store.sync_extension_agents(manifest_refs, extensions_dir, cx);
1609 });
1610 });
1611 }
1612 }
1613
1614 pub fn new_external_thread_with_text(
1615 &mut self,
1616 initial_text: Option<String>,
1617 window: &mut Window,
1618 cx: &mut Context<Self>,
1619 ) {
1620 self.external_thread(
1621 None,
1622 None,
1623 initial_text.map(ExternalAgentInitialContent::Text),
1624 window,
1625 cx,
1626 );
1627 }
1628
1629 pub fn new_agent_thread(
1630 &mut self,
1631 agent: AgentType,
1632 window: &mut Window,
1633 cx: &mut Context<Self>,
1634 ) {
1635 match agent {
1636 AgentType::TextThread => {
1637 window.dispatch_action(NewTextThread.boxed_clone(), cx);
1638 }
1639 AgentType::NativeAgent => self.external_thread(
1640 Some(crate::ExternalAgent::NativeAgent),
1641 None,
1642 None,
1643 window,
1644 cx,
1645 ),
1646 AgentType::Gemini => {
1647 self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx)
1648 }
1649 AgentType::ClaudeAgent => {
1650 self.selected_agent = AgentType::ClaudeAgent;
1651 self.serialize(cx);
1652 self.external_thread(
1653 Some(crate::ExternalAgent::ClaudeCode),
1654 None,
1655 None,
1656 window,
1657 cx,
1658 )
1659 }
1660 AgentType::Codex => {
1661 self.selected_agent = AgentType::Codex;
1662 self.serialize(cx);
1663 self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx)
1664 }
1665 AgentType::Custom { name } => self.external_thread(
1666 Some(crate::ExternalAgent::Custom { name }),
1667 None,
1668 None,
1669 window,
1670 cx,
1671 ),
1672 }
1673 }
1674
1675 pub fn load_agent_thread(
1676 &mut self,
1677 thread: AgentSessionInfo,
1678 window: &mut Window,
1679 cx: &mut Context<Self>,
1680 ) {
1681 let Some(agent) = self.selected_external_agent() else {
1682 return;
1683 };
1684 self.external_thread(Some(agent), Some(thread), None, window, cx);
1685 }
1686
1687 fn _external_thread(
1688 &mut self,
1689 server: Rc<dyn AgentServer>,
1690 resume_thread: Option<AgentSessionInfo>,
1691 initial_content: Option<ExternalAgentInitialContent>,
1692 workspace: WeakEntity<Workspace>,
1693 project: Entity<Project>,
1694 ext_agent: ExternalAgent,
1695 window: &mut Window,
1696 cx: &mut Context<Self>,
1697 ) {
1698 let selected_agent = AgentType::from(ext_agent);
1699 if self.selected_agent != selected_agent {
1700 self.selected_agent = selected_agent;
1701 self.serialize(cx);
1702 }
1703 let thread_store = server
1704 .clone()
1705 .downcast::<agent::NativeAgentServer>()
1706 .is_some()
1707 .then(|| self.thread_store.clone());
1708
1709 let thread_view = cx.new(|cx| {
1710 crate::acp::AcpServerView::new(
1711 server,
1712 resume_thread,
1713 initial_content,
1714 workspace.clone(),
1715 project,
1716 thread_store,
1717 self.prompt_store.clone(),
1718 self.acp_history.clone(),
1719 window,
1720 cx,
1721 )
1722 });
1723
1724 self.set_active_view(ActiveView::AgentThread { thread_view }, true, window, cx);
1725 }
1726}
1727
1728impl Focusable for AgentPanel {
1729 fn focus_handle(&self, cx: &App) -> FocusHandle {
1730 match &self.active_view {
1731 ActiveView::Uninitialized => self.focus_handle.clone(),
1732 ActiveView::AgentThread { thread_view, .. } => thread_view.focus_handle(cx),
1733 ActiveView::History { kind } => match kind {
1734 HistoryKind::AgentThreads => self.acp_history.focus_handle(cx),
1735 HistoryKind::TextThreads => self.text_thread_history.focus_handle(cx),
1736 },
1737 ActiveView::TextThread {
1738 text_thread_editor, ..
1739 } => text_thread_editor.focus_handle(cx),
1740 ActiveView::Configuration => {
1741 if let Some(configuration) = self.configuration.as_ref() {
1742 configuration.focus_handle(cx)
1743 } else {
1744 self.focus_handle.clone()
1745 }
1746 }
1747 }
1748 }
1749}
1750
1751fn agent_panel_dock_position(cx: &App) -> DockPosition {
1752 AgentSettings::get_global(cx).dock.into()
1753}
1754
1755impl EventEmitter<PanelEvent> for AgentPanel {}
1756
1757impl Panel for AgentPanel {
1758 fn persistent_name() -> &'static str {
1759 "AgentPanel"
1760 }
1761
1762 fn panel_key() -> &'static str {
1763 AGENT_PANEL_KEY
1764 }
1765
1766 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1767 agent_panel_dock_position(cx)
1768 }
1769
1770 fn position_is_valid(&self, position: DockPosition) -> bool {
1771 position != DockPosition::Bottom
1772 }
1773
1774 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1775 settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1776 settings
1777 .agent
1778 .get_or_insert_default()
1779 .set_dock(position.into());
1780 });
1781 }
1782
1783 fn size(&self, window: &Window, cx: &App) -> Pixels {
1784 let settings = AgentSettings::get_global(cx);
1785 match self.position(window, cx) {
1786 DockPosition::Left | DockPosition::Right => {
1787 self.width.unwrap_or(settings.default_width)
1788 }
1789 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1790 }
1791 }
1792
1793 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1794 match self.position(window, cx) {
1795 DockPosition::Left | DockPosition::Right => self.width = size,
1796 DockPosition::Bottom => self.height = size,
1797 }
1798 self.serialize(cx);
1799 cx.notify();
1800 }
1801
1802 fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1803 if active && matches!(self.active_view, ActiveView::Uninitialized) {
1804 let selected_agent = self.selected_agent.clone();
1805 self.new_agent_thread(selected_agent, window, cx);
1806 }
1807 }
1808
1809 fn remote_id() -> Option<proto::PanelId> {
1810 Some(proto::PanelId::AssistantPanel)
1811 }
1812
1813 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1814 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
1815 }
1816
1817 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1818 Some("Agent Panel")
1819 }
1820
1821 fn toggle_action(&self) -> Box<dyn Action> {
1822 Box::new(ToggleFocus)
1823 }
1824
1825 fn activation_priority(&self) -> u32 {
1826 3
1827 }
1828
1829 fn enabled(&self, cx: &App) -> bool {
1830 AgentSettings::get_global(cx).enabled(cx)
1831 }
1832
1833 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1834 self.zoomed
1835 }
1836
1837 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1838 self.zoomed = zoomed;
1839 cx.notify();
1840 }
1841}
1842
1843impl AgentPanel {
1844 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1845 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1846
1847 let content = match &self.active_view {
1848 ActiveView::AgentThread { thread_view } => {
1849 let is_generating_title = thread_view
1850 .read(cx)
1851 .as_native_thread(cx)
1852 .map_or(false, |t| t.read(cx).is_generating_title());
1853
1854 if let Some(title_editor) = thread_view
1855 .read(cx)
1856 .parent_thread(cx)
1857 .and_then(|r| r.read(cx).title_editor.clone())
1858 {
1859 let container = div()
1860 .w_full()
1861 .on_action({
1862 let thread_view = thread_view.downgrade();
1863 move |_: &menu::Confirm, window, cx| {
1864 if let Some(thread_view) = thread_view.upgrade() {
1865 thread_view.focus_handle(cx).focus(window, cx);
1866 }
1867 }
1868 })
1869 .on_action({
1870 let thread_view = thread_view.downgrade();
1871 move |_: &editor::actions::Cancel, window, cx| {
1872 if let Some(thread_view) = thread_view.upgrade() {
1873 thread_view.focus_handle(cx).focus(window, cx);
1874 }
1875 }
1876 })
1877 .child(title_editor);
1878
1879 if is_generating_title {
1880 container
1881 .with_animation(
1882 "generating_title",
1883 Animation::new(Duration::from_secs(2))
1884 .repeat()
1885 .with_easing(pulsating_between(0.4, 0.8)),
1886 |div, delta| div.opacity(delta),
1887 )
1888 .into_any_element()
1889 } else {
1890 container.into_any_element()
1891 }
1892 } else {
1893 Label::new(thread_view.read(cx).title(cx))
1894 .color(Color::Muted)
1895 .truncate()
1896 .into_any_element()
1897 }
1898 }
1899 ActiveView::TextThread {
1900 title_editor,
1901 text_thread_editor,
1902 ..
1903 } => {
1904 let summary = text_thread_editor.read(cx).text_thread().read(cx).summary();
1905
1906 match summary {
1907 TextThreadSummary::Pending => Label::new(TextThreadSummary::DEFAULT)
1908 .color(Color::Muted)
1909 .truncate()
1910 .into_any_element(),
1911 TextThreadSummary::Content(summary) => {
1912 if summary.done {
1913 div()
1914 .w_full()
1915 .child(title_editor.clone())
1916 .into_any_element()
1917 } else {
1918 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1919 .truncate()
1920 .color(Color::Muted)
1921 .with_animation(
1922 "generating_title",
1923 Animation::new(Duration::from_secs(2))
1924 .repeat()
1925 .with_easing(pulsating_between(0.4, 0.8)),
1926 |label, delta| label.alpha(delta),
1927 )
1928 .into_any_element()
1929 }
1930 }
1931 TextThreadSummary::Error => h_flex()
1932 .w_full()
1933 .child(title_editor.clone())
1934 .child(
1935 IconButton::new("retry-summary-generation", IconName::RotateCcw)
1936 .icon_size(IconSize::Small)
1937 .on_click({
1938 let text_thread_editor = text_thread_editor.clone();
1939 move |_, _window, cx| {
1940 text_thread_editor.update(cx, |text_thread_editor, cx| {
1941 text_thread_editor.regenerate_summary(cx);
1942 });
1943 }
1944 })
1945 .tooltip(move |_window, cx| {
1946 cx.new(|_| {
1947 Tooltip::new("Failed to generate title")
1948 .meta("Click to try again")
1949 })
1950 .into()
1951 }),
1952 )
1953 .into_any_element(),
1954 }
1955 }
1956 ActiveView::History { kind } => {
1957 let title = match kind {
1958 HistoryKind::AgentThreads => "History",
1959 HistoryKind::TextThreads => "Text Thread History",
1960 };
1961 Label::new(title).truncate().into_any_element()
1962 }
1963 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1964 ActiveView::Uninitialized => Label::new("Agent").truncate().into_any_element(),
1965 };
1966
1967 h_flex()
1968 .key_context("TitleEditor")
1969 .id("TitleEditor")
1970 .flex_grow()
1971 .w_full()
1972 .max_w_full()
1973 .overflow_x_scroll()
1974 .child(content)
1975 .into_any()
1976 }
1977
1978 fn handle_regenerate_thread_title(thread_view: Entity<AcpServerView>, cx: &mut App) {
1979 thread_view.update(cx, |thread_view, cx| {
1980 if let Some(thread) = thread_view.as_native_thread(cx) {
1981 thread.update(cx, |thread, cx| {
1982 thread.generate_title(cx);
1983 });
1984 }
1985 });
1986 }
1987
1988 fn handle_regenerate_text_thread_title(
1989 text_thread_editor: Entity<TextThreadEditor>,
1990 cx: &mut App,
1991 ) {
1992 text_thread_editor.update(cx, |text_thread_editor, cx| {
1993 text_thread_editor.regenerate_summary(cx);
1994 });
1995 }
1996
1997 fn render_panel_options_menu(
1998 &self,
1999 window: &mut Window,
2000 cx: &mut Context<Self>,
2001 ) -> impl IntoElement {
2002 let focus_handle = self.focus_handle(cx);
2003
2004 let full_screen_label = if self.is_zoomed(window, cx) {
2005 "Disable Full Screen"
2006 } else {
2007 "Enable Full Screen"
2008 };
2009
2010 let selected_agent = self.selected_agent.clone();
2011
2012 let text_thread_view = match &self.active_view {
2013 ActiveView::TextThread {
2014 text_thread_editor, ..
2015 } => Some(text_thread_editor.clone()),
2016 _ => None,
2017 };
2018 let text_thread_with_messages = match &self.active_view {
2019 ActiveView::TextThread {
2020 text_thread_editor, ..
2021 } => text_thread_editor
2022 .read(cx)
2023 .text_thread()
2024 .read(cx)
2025 .messages(cx)
2026 .any(|message| message.role == language_model::Role::Assistant),
2027 _ => false,
2028 };
2029
2030 let thread_view = match &self.active_view {
2031 ActiveView::AgentThread { thread_view } => Some(thread_view.clone()),
2032 _ => None,
2033 };
2034 let thread_with_messages = match &self.active_view {
2035 ActiveView::AgentThread { thread_view } => {
2036 thread_view.read(cx).has_user_submitted_prompt(cx)
2037 }
2038 _ => false,
2039 };
2040
2041 PopoverMenu::new("agent-options-menu")
2042 .trigger_with_tooltip(
2043 IconButton::new("agent-options-menu", IconName::Ellipsis)
2044 .icon_size(IconSize::Small),
2045 {
2046 let focus_handle = focus_handle.clone();
2047 move |_window, cx| {
2048 Tooltip::for_action_in(
2049 "Toggle Agent Menu",
2050 &ToggleOptionsMenu,
2051 &focus_handle,
2052 cx,
2053 )
2054 }
2055 },
2056 )
2057 .anchor(Corner::TopRight)
2058 .with_handle(self.agent_panel_menu_handle.clone())
2059 .menu({
2060 move |window, cx| {
2061 Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
2062 menu = menu.context(focus_handle.clone());
2063
2064 if thread_with_messages | text_thread_with_messages {
2065 menu = menu.header("Current Thread");
2066
2067 if let Some(text_thread_view) = text_thread_view.as_ref() {
2068 menu = menu
2069 .entry("Regenerate Thread Title", None, {
2070 let text_thread_view = text_thread_view.clone();
2071 move |_, cx| {
2072 Self::handle_regenerate_text_thread_title(
2073 text_thread_view.clone(),
2074 cx,
2075 );
2076 }
2077 })
2078 .separator();
2079 }
2080
2081 if let Some(thread_view) = thread_view.as_ref() {
2082 menu = menu
2083 .entry("Regenerate Thread Title", None, {
2084 let thread_view = thread_view.clone();
2085 move |_, cx| {
2086 Self::handle_regenerate_thread_title(
2087 thread_view.clone(),
2088 cx,
2089 );
2090 }
2091 })
2092 .separator();
2093 }
2094 }
2095
2096 menu = menu
2097 .header("MCP Servers")
2098 .action(
2099 "View Server Extensions",
2100 Box::new(zed_actions::Extensions {
2101 category_filter: Some(
2102 zed_actions::ExtensionCategoryFilter::ContextServers,
2103 ),
2104 id: None,
2105 }),
2106 )
2107 .action("Add Custom Server…", Box::new(AddContextServer))
2108 .separator()
2109 .action("Rules", Box::new(OpenRulesLibrary::default()))
2110 .action("Profiles", Box::new(ManageProfiles::default()))
2111 .action("Settings", Box::new(OpenSettings))
2112 .separator()
2113 .action(full_screen_label, Box::new(ToggleZoom));
2114
2115 if selected_agent == AgentType::Gemini {
2116 menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
2117 }
2118
2119 menu
2120 }))
2121 }
2122 })
2123 }
2124
2125 fn render_recent_entries_menu(
2126 &self,
2127 icon: IconName,
2128 corner: Corner,
2129 cx: &mut Context<Self>,
2130 ) -> impl IntoElement {
2131 let focus_handle = self.focus_handle(cx);
2132
2133 PopoverMenu::new("agent-nav-menu")
2134 .trigger_with_tooltip(
2135 IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
2136 {
2137 move |_window, cx| {
2138 Tooltip::for_action_in(
2139 "Toggle Recently Updated Threads",
2140 &ToggleNavigationMenu,
2141 &focus_handle,
2142 cx,
2143 )
2144 }
2145 },
2146 )
2147 .anchor(corner)
2148 .with_handle(self.agent_navigation_menu_handle.clone())
2149 .menu({
2150 let menu = self.agent_navigation_menu.clone();
2151 move |window, cx| {
2152 telemetry::event!("View Thread History Clicked");
2153
2154 if let Some(menu) = menu.as_ref() {
2155 menu.update(cx, |_, cx| {
2156 cx.defer_in(window, |menu, window, cx| {
2157 menu.rebuild(window, cx);
2158 });
2159 })
2160 }
2161 menu.clone()
2162 }
2163 })
2164 }
2165
2166 fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
2167 let focus_handle = self.focus_handle(cx);
2168
2169 IconButton::new("go-back", IconName::ArrowLeft)
2170 .icon_size(IconSize::Small)
2171 .on_click(cx.listener(|this, _, window, cx| {
2172 this.go_back(&workspace::GoBack, window, cx);
2173 }))
2174 .tooltip({
2175 move |_window, cx| {
2176 Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
2177 }
2178 })
2179 }
2180
2181 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2182 let agent_server_store = self.project.read(cx).agent_server_store().clone();
2183 let focus_handle = self.focus_handle(cx);
2184
2185 let (selected_agent_custom_icon, selected_agent_label) =
2186 if let AgentType::Custom { name, .. } = &self.selected_agent {
2187 let store = agent_server_store.read(cx);
2188 let icon = store.agent_icon(&ExternalAgentServerName(name.clone()));
2189
2190 let label = store
2191 .agent_display_name(&ExternalAgentServerName(name.clone()))
2192 .unwrap_or_else(|| self.selected_agent.label());
2193 (icon, label)
2194 } else {
2195 (None, self.selected_agent.label())
2196 };
2197
2198 let active_thread = match &self.active_view {
2199 ActiveView::AgentThread { thread_view } => thread_view.read(cx).as_native_thread(cx),
2200 ActiveView::Uninitialized
2201 | ActiveView::TextThread { .. }
2202 | ActiveView::History { .. }
2203 | ActiveView::Configuration => None,
2204 };
2205
2206 let new_thread_menu = PopoverMenu::new("new_thread_menu")
2207 .trigger_with_tooltip(
2208 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2209 {
2210 let focus_handle = focus_handle.clone();
2211 move |_window, cx| {
2212 Tooltip::for_action_in(
2213 "New Thread…",
2214 &ToggleNewThreadMenu,
2215 &focus_handle,
2216 cx,
2217 )
2218 }
2219 },
2220 )
2221 .anchor(Corner::TopRight)
2222 .with_handle(self.new_thread_menu_handle.clone())
2223 .menu({
2224 let selected_agent = self.selected_agent.clone();
2225 let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
2226
2227 let workspace = self.workspace.clone();
2228 let is_via_collab = workspace
2229 .update(cx, |workspace, cx| {
2230 workspace.project().read(cx).is_via_collab()
2231 })
2232 .unwrap_or_default();
2233
2234 move |window, cx| {
2235 telemetry::event!("New Thread Clicked");
2236
2237 let active_thread = active_thread.clone();
2238 Some(ContextMenu::build(window, cx, |menu, _window, cx| {
2239 menu.context(focus_handle.clone())
2240 .when_some(active_thread, |this, active_thread| {
2241 let thread = active_thread.read(cx);
2242
2243 if !thread.is_empty() {
2244 let session_id = thread.id().clone();
2245 this.item(
2246 ContextMenuEntry::new("New From Summary")
2247 .icon(IconName::ThreadFromSummary)
2248 .icon_color(Color::Muted)
2249 .handler(move |window, cx| {
2250 window.dispatch_action(
2251 Box::new(NewNativeAgentThreadFromSummary {
2252 from_session_id: session_id.clone(),
2253 }),
2254 cx,
2255 );
2256 }),
2257 )
2258 } else {
2259 this
2260 }
2261 })
2262 .item(
2263 ContextMenuEntry::new("Zed Agent")
2264 .when(
2265 is_agent_selected(AgentType::NativeAgent)
2266 | is_agent_selected(AgentType::TextThread),
2267 |this| {
2268 this.action(Box::new(NewExternalAgentThread {
2269 agent: None,
2270 }))
2271 },
2272 )
2273 .icon(IconName::ZedAgent)
2274 .icon_color(Color::Muted)
2275 .handler({
2276 let workspace = workspace.clone();
2277 move |window, cx| {
2278 if let Some(workspace) = workspace.upgrade() {
2279 workspace.update(cx, |workspace, cx| {
2280 if let Some(panel) =
2281 workspace.panel::<AgentPanel>(cx)
2282 {
2283 panel.update(cx, |panel, cx| {
2284 panel.new_agent_thread(
2285 AgentType::NativeAgent,
2286 window,
2287 cx,
2288 );
2289 });
2290 }
2291 });
2292 }
2293 }
2294 }),
2295 )
2296 .item(
2297 ContextMenuEntry::new("Text Thread")
2298 .action(NewTextThread.boxed_clone())
2299 .icon(IconName::TextThread)
2300 .icon_color(Color::Muted)
2301 .handler({
2302 let workspace = workspace.clone();
2303 move |window, cx| {
2304 if let Some(workspace) = workspace.upgrade() {
2305 workspace.update(cx, |workspace, cx| {
2306 if let Some(panel) =
2307 workspace.panel::<AgentPanel>(cx)
2308 {
2309 panel.update(cx, |panel, cx| {
2310 panel.new_agent_thread(
2311 AgentType::TextThread,
2312 window,
2313 cx,
2314 );
2315 });
2316 }
2317 });
2318 }
2319 }
2320 }),
2321 )
2322 .separator()
2323 .header("External Agents")
2324 .item(
2325 ContextMenuEntry::new("Claude Agent")
2326 .when(is_agent_selected(AgentType::ClaudeAgent), |this| {
2327 this.action(Box::new(NewExternalAgentThread {
2328 agent: None,
2329 }))
2330 })
2331 .icon(IconName::AiClaude)
2332 .disabled(is_via_collab)
2333 .icon_color(Color::Muted)
2334 .handler({
2335 let workspace = workspace.clone();
2336 move |window, cx| {
2337 if let Some(workspace) = workspace.upgrade() {
2338 workspace.update(cx, |workspace, cx| {
2339 if let Some(panel) =
2340 workspace.panel::<AgentPanel>(cx)
2341 {
2342 panel.update(cx, |panel, cx| {
2343 panel.new_agent_thread(
2344 AgentType::ClaudeAgent,
2345 window,
2346 cx,
2347 );
2348 });
2349 }
2350 });
2351 }
2352 }
2353 }),
2354 )
2355 .item(
2356 ContextMenuEntry::new("Codex CLI")
2357 .when(is_agent_selected(AgentType::Codex), |this| {
2358 this.action(Box::new(NewExternalAgentThread {
2359 agent: None,
2360 }))
2361 })
2362 .icon(IconName::AiOpenAi)
2363 .disabled(is_via_collab)
2364 .icon_color(Color::Muted)
2365 .handler({
2366 let workspace = workspace.clone();
2367 move |window, cx| {
2368 if let Some(workspace) = workspace.upgrade() {
2369 workspace.update(cx, |workspace, cx| {
2370 if let Some(panel) =
2371 workspace.panel::<AgentPanel>(cx)
2372 {
2373 panel.update(cx, |panel, cx| {
2374 panel.new_agent_thread(
2375 AgentType::Codex,
2376 window,
2377 cx,
2378 );
2379 });
2380 }
2381 });
2382 }
2383 }
2384 }),
2385 )
2386 .item(
2387 ContextMenuEntry::new("Gemini CLI")
2388 .when(is_agent_selected(AgentType::Gemini), |this| {
2389 this.action(Box::new(NewExternalAgentThread {
2390 agent: None,
2391 }))
2392 })
2393 .icon(IconName::AiGemini)
2394 .icon_color(Color::Muted)
2395 .disabled(is_via_collab)
2396 .handler({
2397 let workspace = workspace.clone();
2398 move |window, cx| {
2399 if let Some(workspace) = workspace.upgrade() {
2400 workspace.update(cx, |workspace, cx| {
2401 if let Some(panel) =
2402 workspace.panel::<AgentPanel>(cx)
2403 {
2404 panel.update(cx, |panel, cx| {
2405 panel.new_agent_thread(
2406 AgentType::Gemini,
2407 window,
2408 cx,
2409 );
2410 });
2411 }
2412 });
2413 }
2414 }
2415 }),
2416 )
2417 .map(|mut menu| {
2418 let agent_server_store = agent_server_store.read(cx);
2419 let agent_names = agent_server_store
2420 .external_agents()
2421 .filter(|name| {
2422 name.0 != GEMINI_NAME
2423 && name.0 != CLAUDE_AGENT_NAME
2424 && name.0 != CODEX_NAME
2425 })
2426 .cloned()
2427 .collect::<Vec<_>>();
2428
2429 for agent_name in agent_names {
2430 let icon_path = agent_server_store.agent_icon(&agent_name);
2431 let display_name = agent_server_store
2432 .agent_display_name(&agent_name)
2433 .unwrap_or_else(|| agent_name.0.clone());
2434
2435 let mut entry = ContextMenuEntry::new(display_name);
2436
2437 if let Some(icon_path) = icon_path {
2438 entry = entry.custom_icon_svg(icon_path);
2439 } else {
2440 entry = entry.icon(IconName::Sparkle);
2441 }
2442 entry = entry
2443 .when(
2444 is_agent_selected(AgentType::Custom {
2445 name: agent_name.0.clone(),
2446 }),
2447 |this| {
2448 this.action(Box::new(NewExternalAgentThread {
2449 agent: None,
2450 }))
2451 },
2452 )
2453 .icon_color(Color::Muted)
2454 .disabled(is_via_collab)
2455 .handler({
2456 let workspace = workspace.clone();
2457 let agent_name = agent_name.clone();
2458 move |window, cx| {
2459 if let Some(workspace) = workspace.upgrade() {
2460 workspace.update(cx, |workspace, cx| {
2461 if let Some(panel) =
2462 workspace.panel::<AgentPanel>(cx)
2463 {
2464 panel.update(cx, |panel, cx| {
2465 panel.new_agent_thread(
2466 AgentType::Custom {
2467 name: agent_name
2468 .clone()
2469 .into(),
2470 },
2471 window,
2472 cx,
2473 );
2474 });
2475 }
2476 });
2477 }
2478 }
2479 });
2480
2481 menu = menu.item(entry);
2482 }
2483
2484 menu
2485 })
2486 .separator()
2487 .item(
2488 ContextMenuEntry::new("Add More Agents")
2489 .icon(IconName::Plus)
2490 .icon_color(Color::Muted)
2491 .handler({
2492 move |window, cx| {
2493 window.dispatch_action(
2494 Box::new(zed_actions::AcpRegistry),
2495 cx,
2496 )
2497 }
2498 }),
2499 )
2500 }))
2501 }
2502 });
2503
2504 let is_thread_loading = self
2505 .active_thread_view()
2506 .map(|thread| thread.read(cx).is_loading())
2507 .unwrap_or(false);
2508
2509 let has_custom_icon = selected_agent_custom_icon.is_some();
2510
2511 let selected_agent = div()
2512 .id("selected_agent_icon")
2513 .when_some(selected_agent_custom_icon, |this, icon_path| {
2514 this.px_1()
2515 .child(Icon::from_external_svg(icon_path).color(Color::Muted))
2516 })
2517 .when(!has_custom_icon, |this| {
2518 this.when_some(self.selected_agent.icon(), |this, icon| {
2519 this.px_1().child(Icon::new(icon).color(Color::Muted))
2520 })
2521 })
2522 .tooltip(move |_, cx| {
2523 Tooltip::with_meta(selected_agent_label.clone(), None, "Selected Agent", cx)
2524 });
2525
2526 let selected_agent = if is_thread_loading {
2527 selected_agent
2528 .with_animation(
2529 "pulsating-icon",
2530 Animation::new(Duration::from_secs(1))
2531 .repeat()
2532 .with_easing(pulsating_between(0.2, 0.6)),
2533 |icon, delta| icon.opacity(delta),
2534 )
2535 .into_any_element()
2536 } else {
2537 selected_agent.into_any_element()
2538 };
2539
2540 let show_history_menu = self.history_kind_for_selected_agent(cx).is_some();
2541
2542 h_flex()
2543 .id("agent-panel-toolbar")
2544 .h(Tab::container_height(cx))
2545 .max_w_full()
2546 .flex_none()
2547 .justify_between()
2548 .gap_2()
2549 .bg(cx.theme().colors().tab_bar_background)
2550 .border_b_1()
2551 .border_color(cx.theme().colors().border)
2552 .child(
2553 h_flex()
2554 .size_full()
2555 .gap(DynamicSpacing::Base04.rems(cx))
2556 .pl(DynamicSpacing::Base04.rems(cx))
2557 .child(match &self.active_view {
2558 ActiveView::History { .. } | ActiveView::Configuration => {
2559 self.render_toolbar_back_button(cx).into_any_element()
2560 }
2561 _ => selected_agent.into_any_element(),
2562 })
2563 .child(self.render_title_view(window, cx)),
2564 )
2565 .child(
2566 h_flex()
2567 .flex_none()
2568 .gap(DynamicSpacing::Base02.rems(cx))
2569 .pl(DynamicSpacing::Base04.rems(cx))
2570 .pr(DynamicSpacing::Base06.rems(cx))
2571 .child(new_thread_menu)
2572 .when(show_history_menu, |this| {
2573 this.child(self.render_recent_entries_menu(
2574 IconName::MenuAltTemp,
2575 Corner::TopRight,
2576 cx,
2577 ))
2578 })
2579 .child(self.render_panel_options_menu(window, cx)),
2580 )
2581 }
2582
2583 fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2584 if TrialEndUpsell::dismissed() {
2585 return false;
2586 }
2587
2588 match &self.active_view {
2589 ActiveView::TextThread { .. } => {
2590 if LanguageModelRegistry::global(cx)
2591 .read(cx)
2592 .default_model()
2593 .is_some_and(|model| {
2594 model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2595 })
2596 {
2597 return false;
2598 }
2599 }
2600 ActiveView::Uninitialized
2601 | ActiveView::AgentThread { .. }
2602 | ActiveView::History { .. }
2603 | ActiveView::Configuration => return false,
2604 }
2605
2606 let plan = self.user_store.read(cx).plan();
2607 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2608
2609 plan.is_some_and(|plan| plan == Plan::ZedFree) && has_previous_trial
2610 }
2611
2612 fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
2613 if OnboardingUpsell::dismissed() {
2614 return false;
2615 }
2616
2617 let user_store = self.user_store.read(cx);
2618
2619 if user_store.plan().is_some_and(|plan| plan == Plan::ZedPro)
2620 && user_store
2621 .subscription_period()
2622 .and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
2623 .is_some_and(|date| date < chrono::Utc::now())
2624 {
2625 OnboardingUpsell::set_dismissed(true, cx);
2626 return false;
2627 }
2628
2629 match &self.active_view {
2630 ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
2631 false
2632 }
2633 ActiveView::AgentThread { thread_view, .. }
2634 if thread_view.read(cx).as_native_thread(cx).is_none() =>
2635 {
2636 false
2637 }
2638 _ => {
2639 let history_is_empty = self.acp_history.read(cx).is_empty();
2640
2641 let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
2642 .visible_providers()
2643 .iter()
2644 .any(|provider| {
2645 provider.is_authenticated(cx)
2646 && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2647 });
2648
2649 history_is_empty || !has_configured_non_zed_providers
2650 }
2651 }
2652 }
2653
2654 fn render_onboarding(
2655 &self,
2656 _window: &mut Window,
2657 cx: &mut Context<Self>,
2658 ) -> Option<impl IntoElement> {
2659 if !self.should_render_onboarding(cx) {
2660 return None;
2661 }
2662
2663 let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
2664
2665 Some(
2666 div()
2667 .when(text_thread_view, |this| {
2668 this.bg(cx.theme().colors().editor_background)
2669 })
2670 .child(self.onboarding.clone()),
2671 )
2672 }
2673
2674 fn render_trial_end_upsell(
2675 &self,
2676 _window: &mut Window,
2677 cx: &mut Context<Self>,
2678 ) -> Option<impl IntoElement> {
2679 if !self.should_render_trial_end_upsell(cx) {
2680 return None;
2681 }
2682
2683 Some(
2684 v_flex()
2685 .absolute()
2686 .inset_0()
2687 .size_full()
2688 .bg(cx.theme().colors().panel_background)
2689 .opacity(0.85)
2690 .block_mouse_except_scroll()
2691 .child(EndTrialUpsell::new(Arc::new({
2692 let this = cx.entity();
2693 move |_, cx| {
2694 this.update(cx, |_this, cx| {
2695 TrialEndUpsell::set_dismissed(true, cx);
2696 cx.notify();
2697 });
2698 }
2699 }))),
2700 )
2701 }
2702
2703 fn emit_configuration_error_telemetry_if_needed(
2704 &mut self,
2705 configuration_error: Option<&ConfigurationError>,
2706 ) {
2707 let error_kind = configuration_error.map(|err| match err {
2708 ConfigurationError::NoProvider => "no_provider",
2709 ConfigurationError::ModelNotFound => "model_not_found",
2710 ConfigurationError::ProviderNotAuthenticated(_) => "provider_not_authenticated",
2711 });
2712
2713 let error_kind_string = error_kind.map(String::from);
2714
2715 if self.last_configuration_error_telemetry == error_kind_string {
2716 return;
2717 }
2718
2719 self.last_configuration_error_telemetry = error_kind_string;
2720
2721 if let Some(kind) = error_kind {
2722 let message = configuration_error
2723 .map(|err| err.to_string())
2724 .unwrap_or_default();
2725
2726 telemetry::event!("Agent Panel Error Shown", kind = kind, message = message,);
2727 }
2728 }
2729
2730 fn render_configuration_error(
2731 &self,
2732 border_bottom: bool,
2733 configuration_error: &ConfigurationError,
2734 focus_handle: &FocusHandle,
2735 cx: &mut App,
2736 ) -> impl IntoElement {
2737 let zed_provider_configured = AgentSettings::get_global(cx)
2738 .default_model
2739 .as_ref()
2740 .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev");
2741
2742 let callout = if zed_provider_configured {
2743 Callout::new()
2744 .icon(IconName::Warning)
2745 .severity(Severity::Warning)
2746 .when(border_bottom, |this| {
2747 this.border_position(ui::BorderPosition::Bottom)
2748 })
2749 .title("Sign in to continue using Zed as your LLM provider.")
2750 .actions_slot(
2751 Button::new("sign_in", "Sign In")
2752 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2753 .label_size(LabelSize::Small)
2754 .on_click({
2755 let workspace = self.workspace.clone();
2756 move |_, _, cx| {
2757 let Ok(client) =
2758 workspace.update(cx, |workspace, _| workspace.client().clone())
2759 else {
2760 return;
2761 };
2762
2763 cx.spawn(async move |cx| {
2764 client.sign_in_with_optional_connect(true, cx).await
2765 })
2766 .detach_and_log_err(cx);
2767 }
2768 }),
2769 )
2770 } else {
2771 Callout::new()
2772 .icon(IconName::Warning)
2773 .severity(Severity::Warning)
2774 .when(border_bottom, |this| {
2775 this.border_position(ui::BorderPosition::Bottom)
2776 })
2777 .title(configuration_error.to_string())
2778 .actions_slot(
2779 Button::new("settings", "Configure")
2780 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2781 .label_size(LabelSize::Small)
2782 .key_binding(
2783 KeyBinding::for_action_in(&OpenSettings, focus_handle, cx)
2784 .map(|kb| kb.size(rems_from_px(12.))),
2785 )
2786 .on_click(|_event, window, cx| {
2787 window.dispatch_action(OpenSettings.boxed_clone(), cx)
2788 }),
2789 )
2790 };
2791
2792 match configuration_error {
2793 ConfigurationError::ModelNotFound
2794 | ConfigurationError::ProviderNotAuthenticated(_)
2795 | ConfigurationError::NoProvider => callout.into_any_element(),
2796 }
2797 }
2798
2799 fn render_text_thread(
2800 &self,
2801 text_thread_editor: &Entity<TextThreadEditor>,
2802 buffer_search_bar: &Entity<BufferSearchBar>,
2803 window: &mut Window,
2804 cx: &mut Context<Self>,
2805 ) -> Div {
2806 let mut registrar = buffer_search::DivRegistrar::new(
2807 |this, _, _cx| match &this.active_view {
2808 ActiveView::TextThread {
2809 buffer_search_bar, ..
2810 } => Some(buffer_search_bar.clone()),
2811 _ => None,
2812 },
2813 cx,
2814 );
2815 BufferSearchBar::register(&mut registrar);
2816 registrar
2817 .into_div()
2818 .size_full()
2819 .relative()
2820 .map(|parent| {
2821 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2822 if buffer_search_bar.is_dismissed() {
2823 return parent;
2824 }
2825 parent.child(
2826 div()
2827 .p(DynamicSpacing::Base08.rems(cx))
2828 .border_b_1()
2829 .border_color(cx.theme().colors().border_variant)
2830 .bg(cx.theme().colors().editor_background)
2831 .child(buffer_search_bar.render(window, cx)),
2832 )
2833 })
2834 })
2835 .child(text_thread_editor.clone())
2836 .child(self.render_drag_target(cx))
2837 }
2838
2839 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
2840 let is_local = self.project.read(cx).is_local();
2841 div()
2842 .invisible()
2843 .absolute()
2844 .top_0()
2845 .right_0()
2846 .bottom_0()
2847 .left_0()
2848 .bg(cx.theme().colors().drop_target_background)
2849 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
2850 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
2851 .when(is_local, |this| {
2852 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
2853 })
2854 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
2855 let item = tab.pane.read(cx).item_for_index(tab.ix);
2856 let project_paths = item
2857 .and_then(|item| item.project_path(cx))
2858 .into_iter()
2859 .collect::<Vec<_>>();
2860 this.handle_drop(project_paths, vec![], window, cx);
2861 }))
2862 .on_drop(
2863 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2864 let project_paths = selection
2865 .items()
2866 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
2867 .collect::<Vec<_>>();
2868 this.handle_drop(project_paths, vec![], window, cx);
2869 }),
2870 )
2871 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
2872 let tasks = paths
2873 .paths()
2874 .iter()
2875 .map(|path| {
2876 Workspace::project_path_for_path(this.project.clone(), path, false, cx)
2877 })
2878 .collect::<Vec<_>>();
2879 cx.spawn_in(window, async move |this, cx| {
2880 let mut paths = vec![];
2881 let mut added_worktrees = vec![];
2882 let opened_paths = futures::future::join_all(tasks).await;
2883 for entry in opened_paths {
2884 if let Some((worktree, project_path)) = entry.log_err() {
2885 added_worktrees.push(worktree);
2886 paths.push(project_path);
2887 }
2888 }
2889 this.update_in(cx, |this, window, cx| {
2890 this.handle_drop(paths, added_worktrees, window, cx);
2891 })
2892 .ok();
2893 })
2894 .detach();
2895 }))
2896 }
2897
2898 fn handle_drop(
2899 &mut self,
2900 paths: Vec<ProjectPath>,
2901 added_worktrees: Vec<Entity<Worktree>>,
2902 window: &mut Window,
2903 cx: &mut Context<Self>,
2904 ) {
2905 match &self.active_view {
2906 ActiveView::AgentThread { thread_view } => {
2907 thread_view.update(cx, |thread_view, cx| {
2908 thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
2909 });
2910 }
2911 ActiveView::TextThread {
2912 text_thread_editor, ..
2913 } => {
2914 text_thread_editor.update(cx, |text_thread_editor, cx| {
2915 TextThreadEditor::insert_dragged_files(
2916 text_thread_editor,
2917 paths,
2918 added_worktrees,
2919 window,
2920 cx,
2921 );
2922 });
2923 }
2924 ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
2925 }
2926 }
2927
2928 fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
2929 if !self.show_trust_workspace_message {
2930 return None;
2931 }
2932
2933 let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
2934
2935 Some(
2936 Callout::new()
2937 .icon(IconName::Warning)
2938 .severity(Severity::Warning)
2939 .border_position(ui::BorderPosition::Bottom)
2940 .title("You're in Restricted Mode")
2941 .description(description)
2942 .actions_slot(
2943 Button::new("open-trust-modal", "Configure Project Trust")
2944 .label_size(LabelSize::Small)
2945 .style(ButtonStyle::Outlined)
2946 .on_click({
2947 cx.listener(move |this, _, window, cx| {
2948 this.workspace
2949 .update(cx, |workspace, cx| {
2950 workspace
2951 .show_worktree_trust_security_modal(true, window, cx)
2952 })
2953 .log_err();
2954 })
2955 }),
2956 ),
2957 )
2958 }
2959
2960 fn key_context(&self) -> KeyContext {
2961 let mut key_context = KeyContext::new_with_defaults();
2962 key_context.add("AgentPanel");
2963 match &self.active_view {
2964 ActiveView::AgentThread { .. } => key_context.add("acp_thread"),
2965 ActiveView::TextThread { .. } => key_context.add("text_thread"),
2966 ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
2967 }
2968 key_context
2969 }
2970}
2971
2972impl Render for AgentPanel {
2973 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2974 // WARNING: Changes to this element hierarchy can have
2975 // non-obvious implications to the layout of children.
2976 //
2977 // If you need to change it, please confirm:
2978 // - The message editor expands (cmd-option-esc) correctly
2979 // - When expanded, the buttons at the bottom of the panel are displayed correctly
2980 // - Font size works as expected and can be changed with cmd-+/cmd-
2981 // - Scrolling in all views works as expected
2982 // - Files can be dropped into the panel
2983 let content = v_flex()
2984 .relative()
2985 .size_full()
2986 .justify_between()
2987 .key_context(self.key_context())
2988 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
2989 this.new_thread(action, window, cx);
2990 }))
2991 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
2992 this.open_history(window, cx);
2993 }))
2994 .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
2995 this.open_configuration(window, cx);
2996 }))
2997 .on_action(cx.listener(Self::open_active_thread_as_markdown))
2998 .on_action(cx.listener(Self::deploy_rules_library))
2999 .on_action(cx.listener(Self::go_back))
3000 .on_action(cx.listener(Self::toggle_navigation_menu))
3001 .on_action(cx.listener(Self::toggle_options_menu))
3002 .on_action(cx.listener(Self::increase_font_size))
3003 .on_action(cx.listener(Self::decrease_font_size))
3004 .on_action(cx.listener(Self::reset_font_size))
3005 .on_action(cx.listener(Self::toggle_zoom))
3006 .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
3007 if let Some(thread_view) = this.active_thread_view() {
3008 thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
3009 }
3010 }))
3011 .child(self.render_toolbar(window, cx))
3012 .children(self.render_workspace_trust_message(cx))
3013 .children(self.render_onboarding(window, cx))
3014 .map(|parent| {
3015 // Emit configuration error telemetry before entering the match to avoid borrow conflicts
3016 if matches!(&self.active_view, ActiveView::TextThread { .. }) {
3017 let model_registry = LanguageModelRegistry::read_global(cx);
3018 let configuration_error =
3019 model_registry.configuration_error(model_registry.default_model(), cx);
3020 self.emit_configuration_error_telemetry_if_needed(configuration_error.as_ref());
3021 }
3022
3023 match &self.active_view {
3024 ActiveView::Uninitialized => parent,
3025 ActiveView::AgentThread { thread_view, .. } => parent
3026 .child(thread_view.clone())
3027 .child(self.render_drag_target(cx)),
3028 ActiveView::History { kind } => match kind {
3029 HistoryKind::AgentThreads => parent.child(self.acp_history.clone()),
3030 HistoryKind::TextThreads => parent.child(self.text_thread_history.clone()),
3031 },
3032 ActiveView::TextThread {
3033 text_thread_editor,
3034 buffer_search_bar,
3035 ..
3036 } => {
3037 let model_registry = LanguageModelRegistry::read_global(cx);
3038 let configuration_error =
3039 model_registry.configuration_error(model_registry.default_model(), cx);
3040
3041 parent
3042 .map(|this| {
3043 if !self.should_render_onboarding(cx)
3044 && let Some(err) = configuration_error.as_ref()
3045 {
3046 this.child(self.render_configuration_error(
3047 true,
3048 err,
3049 &self.focus_handle(cx),
3050 cx,
3051 ))
3052 } else {
3053 this
3054 }
3055 })
3056 .child(self.render_text_thread(
3057 text_thread_editor,
3058 buffer_search_bar,
3059 window,
3060 cx,
3061 ))
3062 }
3063 ActiveView::Configuration => parent.children(self.configuration.clone()),
3064 }
3065 })
3066 .children(self.render_trial_end_upsell(window, cx));
3067
3068 match self.active_view.which_font_size_used() {
3069 WhichFontSize::AgentFont => {
3070 WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
3071 .size_full()
3072 .child(content)
3073 .into_any()
3074 }
3075 _ => content.into_any(),
3076 }
3077 }
3078}
3079
3080struct PromptLibraryInlineAssist {
3081 workspace: WeakEntity<Workspace>,
3082}
3083
3084impl PromptLibraryInlineAssist {
3085 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3086 Self { workspace }
3087 }
3088}
3089
3090impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3091 fn assist(
3092 &self,
3093 prompt_editor: &Entity<Editor>,
3094 initial_prompt: Option<String>,
3095 window: &mut Window,
3096 cx: &mut Context<RulesLibrary>,
3097 ) {
3098 InlineAssistant::update_global(cx, |assistant, cx| {
3099 let Some(workspace) = self.workspace.upgrade() else {
3100 return;
3101 };
3102 let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
3103 return;
3104 };
3105 let project = workspace.read(cx).project().downgrade();
3106 let panel = panel.read(cx);
3107 let thread_store = panel.thread_store().clone();
3108 let history = panel.history().downgrade();
3109 assistant.assist(
3110 prompt_editor,
3111 self.workspace.clone(),
3112 project,
3113 thread_store,
3114 None,
3115 history,
3116 initial_prompt,
3117 window,
3118 cx,
3119 );
3120 })
3121 }
3122
3123 fn focus_agent_panel(
3124 &self,
3125 workspace: &mut Workspace,
3126 window: &mut Window,
3127 cx: &mut Context<Workspace>,
3128 ) -> bool {
3129 workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3130 }
3131}
3132
3133pub struct ConcreteAssistantPanelDelegate;
3134
3135impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3136 fn active_text_thread_editor(
3137 &self,
3138 workspace: &mut Workspace,
3139 _window: &mut Window,
3140 cx: &mut Context<Workspace>,
3141 ) -> Option<Entity<TextThreadEditor>> {
3142 let panel = workspace.panel::<AgentPanel>(cx)?;
3143 panel.read(cx).active_text_thread_editor()
3144 }
3145
3146 fn open_local_text_thread(
3147 &self,
3148 workspace: &mut Workspace,
3149 path: Arc<Path>,
3150 window: &mut Window,
3151 cx: &mut Context<Workspace>,
3152 ) -> Task<Result<()>> {
3153 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3154 return Task::ready(Err(anyhow!("Agent panel not found")));
3155 };
3156
3157 panel.update(cx, |panel, cx| {
3158 panel.open_saved_text_thread(path, window, cx)
3159 })
3160 }
3161
3162 fn open_remote_text_thread(
3163 &self,
3164 _workspace: &mut Workspace,
3165 _text_thread_id: assistant_text_thread::TextThreadId,
3166 _window: &mut Window,
3167 _cx: &mut Context<Workspace>,
3168 ) -> Task<Result<Entity<TextThreadEditor>>> {
3169 Task::ready(Err(anyhow!("opening remote context not implemented")))
3170 }
3171
3172 fn quote_selection(
3173 &self,
3174 workspace: &mut Workspace,
3175 selection_ranges: Vec<Range<Anchor>>,
3176 buffer: Entity<MultiBuffer>,
3177 window: &mut Window,
3178 cx: &mut Context<Workspace>,
3179 ) {
3180 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3181 return;
3182 };
3183
3184 if !panel.focus_handle(cx).contains_focused(window, cx) {
3185 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3186 }
3187
3188 panel.update(cx, |_, cx| {
3189 // Wait to create a new context until the workspace is no longer
3190 // being updated.
3191 cx.defer_in(window, move |panel, window, cx| {
3192 if let Some(thread_view) = panel.active_thread_view() {
3193 thread_view.update(cx, |thread_view, cx| {
3194 thread_view.insert_selections(window, cx);
3195 });
3196 } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
3197 let snapshot = buffer.read(cx).snapshot(cx);
3198 let selection_ranges = selection_ranges
3199 .into_iter()
3200 .map(|range| range.to_point(&snapshot))
3201 .collect::<Vec<_>>();
3202
3203 text_thread_editor.update(cx, |text_thread_editor, cx| {
3204 text_thread_editor.quote_ranges(selection_ranges, snapshot, window, cx)
3205 });
3206 }
3207 });
3208 });
3209 }
3210
3211 fn quote_terminal_text(
3212 &self,
3213 workspace: &mut Workspace,
3214 text: String,
3215 window: &mut Window,
3216 cx: &mut Context<Workspace>,
3217 ) {
3218 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3219 return;
3220 };
3221
3222 if !panel.focus_handle(cx).contains_focused(window, cx) {
3223 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3224 }
3225
3226 panel.update(cx, |_, cx| {
3227 // Wait to create a new context until the workspace is no longer
3228 // being updated.
3229 cx.defer_in(window, move |panel, window, cx| {
3230 if let Some(thread_view) = panel.active_thread_view() {
3231 thread_view.update(cx, |thread_view, cx| {
3232 thread_view.insert_terminal_text(text, window, cx);
3233 });
3234 } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
3235 text_thread_editor.update(cx, |text_thread_editor, cx| {
3236 text_thread_editor.quote_terminal_text(text, window, cx)
3237 });
3238 }
3239 });
3240 });
3241 }
3242}
3243
3244struct OnboardingUpsell;
3245
3246impl Dismissable for OnboardingUpsell {
3247 const KEY: &'static str = "dismissed-trial-upsell";
3248}
3249
3250struct TrialEndUpsell;
3251
3252impl Dismissable for TrialEndUpsell {
3253 const KEY: &'static str = "dismissed-trial-end-upsell";
3254}
3255
3256#[cfg(feature = "test-support")]
3257impl AgentPanel {
3258 /// Opens an external thread using an arbitrary AgentServer.
3259 ///
3260 /// This is a test-only helper that allows visual tests and integration tests
3261 /// to inject a stub server without modifying production code paths.
3262 /// Not compiled into production builds.
3263 pub fn open_external_thread_with_server(
3264 &mut self,
3265 server: Rc<dyn AgentServer>,
3266 window: &mut Window,
3267 cx: &mut Context<Self>,
3268 ) {
3269 let workspace = self.workspace.clone();
3270 let project = self.project.clone();
3271
3272 let ext_agent = ExternalAgent::Custom {
3273 name: server.name(),
3274 };
3275
3276 self._external_thread(
3277 server, None, None, workspace, project, ext_agent, window, cx,
3278 );
3279 }
3280
3281 /// Returns the currently active thread view, if any.
3282 ///
3283 /// This is a test-only accessor that exposes the private `active_thread_view()`
3284 /// method for test assertions. Not compiled into production builds.
3285 pub fn active_thread_view_for_tests(&self) -> Option<&Entity<AcpServerView>> {
3286 self.active_thread_view()
3287 }
3288}