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