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