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