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