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 _ = settings
1063 .theme
1064 .agent_ui_font_size
1065 .insert(theme::clamp_font_size(agent_ui_font_size).into());
1066 });
1067 } else {
1068 theme::adjust_agent_ui_font_size(cx, |size| size + delta);
1069 }
1070 }
1071 WhichFontSize::BufferFont => {
1072 // Prompt editor uses the buffer font size, so allow the action to propagate to the
1073 // default handler that changes that font size.
1074 cx.propagate();
1075 }
1076 WhichFontSize::None => {}
1077 }
1078 }
1079
1080 pub fn reset_font_size(
1081 &mut self,
1082 action: &ResetBufferFontSize,
1083 _: &mut Window,
1084 cx: &mut Context<Self>,
1085 ) {
1086 if action.persist {
1087 update_settings_file(self.fs.clone(), cx, move |settings, _| {
1088 settings.theme.agent_ui_font_size = None;
1089 });
1090 } else {
1091 theme::reset_agent_ui_font_size(cx);
1092 }
1093 }
1094
1095 pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1096 if self.zoomed {
1097 cx.emit(PanelEvent::ZoomOut);
1098 } else {
1099 if !self.focus_handle(cx).contains_focused(window, cx) {
1100 cx.focus_self(window);
1101 }
1102 cx.emit(PanelEvent::ZoomIn);
1103 }
1104 }
1105
1106 pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1107 let agent_server_store = self.project.read(cx).agent_server_store().clone();
1108 let context_server_store = self.project.read(cx).context_server_store();
1109 let fs = self.fs.clone();
1110
1111 self.set_active_view(ActiveView::Configuration, window, cx);
1112 self.configuration = Some(cx.new(|cx| {
1113 AgentConfiguration::new(
1114 fs,
1115 agent_server_store,
1116 context_server_store,
1117 self.context_server_registry.clone(),
1118 self.language_registry.clone(),
1119 self.workspace.clone(),
1120 window,
1121 cx,
1122 )
1123 }));
1124
1125 if let Some(configuration) = self.configuration.as_ref() {
1126 self.configuration_subscription = Some(cx.subscribe_in(
1127 configuration,
1128 window,
1129 Self::handle_agent_configuration_event,
1130 ));
1131
1132 configuration.focus_handle(cx).focus(window);
1133 }
1134 }
1135
1136 pub(crate) fn open_active_thread_as_markdown(
1137 &mut self,
1138 _: &OpenActiveThreadAsMarkdown,
1139 window: &mut Window,
1140 cx: &mut Context<Self>,
1141 ) {
1142 let Some(workspace) = self.workspace.upgrade() else {
1143 return;
1144 };
1145
1146 match &self.active_view {
1147 ActiveView::ExternalAgentThread { thread_view } => {
1148 thread_view
1149 .update(cx, |thread_view, cx| {
1150 thread_view.open_thread_as_markdown(workspace, window, cx)
1151 })
1152 .detach_and_log_err(cx);
1153 }
1154 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
1155 }
1156 }
1157
1158 fn handle_agent_configuration_event(
1159 &mut self,
1160 _entity: &Entity<AgentConfiguration>,
1161 event: &AssistantConfigurationEvent,
1162 window: &mut Window,
1163 cx: &mut Context<Self>,
1164 ) {
1165 match event {
1166 AssistantConfigurationEvent::NewThread(provider) => {
1167 if LanguageModelRegistry::read_global(cx)
1168 .default_model()
1169 .is_none_or(|model| model.provider.id() != provider.id())
1170 && let Some(model) = provider.default_model(cx)
1171 {
1172 update_settings_file(self.fs.clone(), cx, move |settings, _| {
1173 let provider = model.provider_id().0.to_string();
1174 let model = model.id().0.to_string();
1175 settings
1176 .agent
1177 .get_or_insert_default()
1178 .set_model(LanguageModelSelection {
1179 provider: LanguageModelProviderSetting(provider),
1180 model,
1181 })
1182 });
1183 }
1184
1185 self.new_thread(&NewThread, window, cx);
1186 if let Some((thread, model)) = self
1187 .active_native_agent_thread(cx)
1188 .zip(provider.default_model(cx))
1189 {
1190 thread.update(cx, |thread, cx| {
1191 thread.set_model(model, cx);
1192 });
1193 }
1194 }
1195 }
1196 }
1197
1198 pub(crate) fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
1199 match &self.active_view {
1200 ActiveView::ExternalAgentThread { thread_view, .. } => {
1201 thread_view.read(cx).thread().cloned()
1202 }
1203 _ => None,
1204 }
1205 }
1206
1207 pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
1208 match &self.active_view {
1209 ActiveView::ExternalAgentThread { thread_view, .. } => {
1210 thread_view.read(cx).as_native_thread(cx)
1211 }
1212 _ => None,
1213 }
1214 }
1215
1216 pub(crate) fn active_text_thread_editor(&self) -> Option<Entity<TextThreadEditor>> {
1217 match &self.active_view {
1218 ActiveView::TextThread {
1219 text_thread_editor, ..
1220 } => Some(text_thread_editor.clone()),
1221 _ => None,
1222 }
1223 }
1224
1225 fn set_active_view(
1226 &mut self,
1227 new_view: ActiveView,
1228 window: &mut Window,
1229 cx: &mut Context<Self>,
1230 ) {
1231 let current_is_history = matches!(self.active_view, ActiveView::History);
1232 let new_is_history = matches!(new_view, ActiveView::History);
1233
1234 let current_is_config = matches!(self.active_view, ActiveView::Configuration);
1235 let new_is_config = matches!(new_view, ActiveView::Configuration);
1236
1237 let current_is_special = current_is_history || current_is_config;
1238 let new_is_special = new_is_history || new_is_config;
1239
1240 match &new_view {
1241 ActiveView::TextThread {
1242 text_thread_editor, ..
1243 } => self.history_store.update(cx, |store, cx| {
1244 if let Some(path) = text_thread_editor.read(cx).text_thread().read(cx).path() {
1245 store.push_recently_opened_entry(
1246 agent::HistoryEntryId::TextThread(path.clone()),
1247 cx,
1248 )
1249 }
1250 }),
1251 ActiveView::ExternalAgentThread { .. } => {}
1252 ActiveView::History | ActiveView::Configuration => {}
1253 }
1254
1255 if current_is_special && !new_is_special {
1256 self.active_view = new_view;
1257 } else if !current_is_special && new_is_special {
1258 self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1259 } else {
1260 if !new_is_special {
1261 self.previous_view = None;
1262 }
1263 self.active_view = new_view;
1264 }
1265
1266 self.focus_handle(cx).focus(window);
1267 }
1268
1269 fn populate_recently_opened_menu_section(
1270 mut menu: ContextMenu,
1271 panel: Entity<Self>,
1272 cx: &mut Context<ContextMenu>,
1273 ) -> ContextMenu {
1274 let entries = panel
1275 .read(cx)
1276 .history_store
1277 .read(cx)
1278 .recently_opened_entries(cx);
1279
1280 if entries.is_empty() {
1281 return menu;
1282 }
1283
1284 menu = menu.header("Recently Opened");
1285
1286 for entry in entries {
1287 let title = entry.title().clone();
1288
1289 menu = menu.entry_with_end_slot_on_hover(
1290 title,
1291 None,
1292 {
1293 let panel = panel.downgrade();
1294 let entry = entry.clone();
1295 move |window, cx| {
1296 let entry = entry.clone();
1297 panel
1298 .update(cx, move |this, cx| match &entry {
1299 agent::HistoryEntry::AcpThread(entry) => this.external_thread(
1300 Some(ExternalAgent::NativeAgent),
1301 Some(entry.clone()),
1302 None,
1303 window,
1304 cx,
1305 ),
1306 agent::HistoryEntry::TextThread(entry) => this
1307 .open_saved_text_thread(entry.path.clone(), window, cx)
1308 .detach_and_log_err(cx),
1309 })
1310 .ok();
1311 }
1312 },
1313 IconName::Close,
1314 "Close Entry".into(),
1315 {
1316 let panel = panel.downgrade();
1317 let id = entry.id();
1318 move |_window, cx| {
1319 panel
1320 .update(cx, |this, cx| {
1321 this.history_store.update(cx, |history_store, cx| {
1322 history_store.remove_recently_opened_entry(&id, cx);
1323 });
1324 })
1325 .ok();
1326 }
1327 },
1328 );
1329 }
1330
1331 menu = menu.separator();
1332
1333 menu
1334 }
1335
1336 pub fn selected_agent(&self) -> AgentType {
1337 self.selected_agent.clone()
1338 }
1339
1340 fn sync_agent_servers_from_extensions(&mut self, cx: &mut Context<Self>) {
1341 if let Some(extension_store) = ExtensionStore::try_global(cx) {
1342 let (manifests, extensions_dir) = {
1343 let store = extension_store.read(cx);
1344 let installed = store.installed_extensions();
1345 let manifests: Vec<_> = installed
1346 .iter()
1347 .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
1348 .collect();
1349 let extensions_dir = paths::extensions_dir().join("installed");
1350 (manifests, extensions_dir)
1351 };
1352
1353 self.project.update(cx, |project, cx| {
1354 project.agent_server_store().update(cx, |store, cx| {
1355 let manifest_refs: Vec<_> = manifests
1356 .iter()
1357 .map(|(id, manifest)| (id.as_ref(), manifest.as_ref()))
1358 .collect();
1359 store.sync_extension_agents(manifest_refs, extensions_dir, cx);
1360 });
1361 });
1362 }
1363 }
1364
1365 pub fn new_agent_thread(
1366 &mut self,
1367 agent: AgentType,
1368 window: &mut Window,
1369 cx: &mut Context<Self>,
1370 ) {
1371 match agent {
1372 AgentType::TextThread => {
1373 window.dispatch_action(NewTextThread.boxed_clone(), cx);
1374 }
1375 AgentType::NativeAgent => self.external_thread(
1376 Some(crate::ExternalAgent::NativeAgent),
1377 None,
1378 None,
1379 window,
1380 cx,
1381 ),
1382 AgentType::Gemini => {
1383 self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx)
1384 }
1385 AgentType::ClaudeCode => {
1386 self.selected_agent = AgentType::ClaudeCode;
1387 self.serialize(cx);
1388 self.external_thread(
1389 Some(crate::ExternalAgent::ClaudeCode),
1390 None,
1391 None,
1392 window,
1393 cx,
1394 )
1395 }
1396 AgentType::Codex => {
1397 self.selected_agent = AgentType::Codex;
1398 self.serialize(cx);
1399 self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx)
1400 }
1401 AgentType::Custom { name, command } => self.external_thread(
1402 Some(crate::ExternalAgent::Custom { name, command }),
1403 None,
1404 None,
1405 window,
1406 cx,
1407 ),
1408 }
1409 }
1410
1411 pub fn load_agent_thread(
1412 &mut self,
1413 thread: DbThreadMetadata,
1414 window: &mut Window,
1415 cx: &mut Context<Self>,
1416 ) {
1417 self.external_thread(
1418 Some(ExternalAgent::NativeAgent),
1419 Some(thread),
1420 None,
1421 window,
1422 cx,
1423 );
1424 }
1425}
1426
1427impl Focusable for AgentPanel {
1428 fn focus_handle(&self, cx: &App) -> FocusHandle {
1429 match &self.active_view {
1430 ActiveView::ExternalAgentThread { thread_view, .. } => thread_view.focus_handle(cx),
1431 ActiveView::History => self.acp_history.focus_handle(cx),
1432 ActiveView::TextThread {
1433 text_thread_editor, ..
1434 } => text_thread_editor.focus_handle(cx),
1435 ActiveView::Configuration => {
1436 if let Some(configuration) = self.configuration.as_ref() {
1437 configuration.focus_handle(cx)
1438 } else {
1439 cx.focus_handle()
1440 }
1441 }
1442 }
1443 }
1444}
1445
1446fn agent_panel_dock_position(cx: &App) -> DockPosition {
1447 AgentSettings::get_global(cx).dock.into()
1448}
1449
1450impl EventEmitter<PanelEvent> for AgentPanel {}
1451
1452impl Panel for AgentPanel {
1453 fn persistent_name() -> &'static str {
1454 "AgentPanel"
1455 }
1456
1457 fn panel_key() -> &'static str {
1458 AGENT_PANEL_KEY
1459 }
1460
1461 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1462 agent_panel_dock_position(cx)
1463 }
1464
1465 fn position_is_valid(&self, position: DockPosition) -> bool {
1466 position != DockPosition::Bottom
1467 }
1468
1469 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1470 settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1471 settings
1472 .agent
1473 .get_or_insert_default()
1474 .set_dock(position.into());
1475 });
1476 }
1477
1478 fn size(&self, window: &Window, cx: &App) -> Pixels {
1479 let settings = AgentSettings::get_global(cx);
1480 match self.position(window, cx) {
1481 DockPosition::Left | DockPosition::Right => {
1482 self.width.unwrap_or(settings.default_width)
1483 }
1484 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1485 }
1486 }
1487
1488 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1489 match self.position(window, cx) {
1490 DockPosition::Left | DockPosition::Right => self.width = size,
1491 DockPosition::Bottom => self.height = size,
1492 }
1493 self.serialize(cx);
1494 cx.notify();
1495 }
1496
1497 fn set_active(&mut self, _active: bool, _window: &mut Window, _cx: &mut Context<Self>) {}
1498
1499 fn remote_id() -> Option<proto::PanelId> {
1500 Some(proto::PanelId::AssistantPanel)
1501 }
1502
1503 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1504 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
1505 }
1506
1507 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1508 Some("Agent Panel")
1509 }
1510
1511 fn toggle_action(&self) -> Box<dyn Action> {
1512 Box::new(ToggleFocus)
1513 }
1514
1515 fn activation_priority(&self) -> u32 {
1516 3
1517 }
1518
1519 fn enabled(&self, cx: &App) -> bool {
1520 AgentSettings::get_global(cx).enabled(cx)
1521 }
1522
1523 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1524 self.zoomed
1525 }
1526
1527 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1528 self.zoomed = zoomed;
1529 cx.notify();
1530 }
1531}
1532
1533impl AgentPanel {
1534 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1535 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1536
1537 let content = match &self.active_view {
1538 ActiveView::ExternalAgentThread { thread_view } => {
1539 if let Some(title_editor) = thread_view.read(cx).title_editor() {
1540 div()
1541 .w_full()
1542 .on_action({
1543 let thread_view = thread_view.downgrade();
1544 move |_: &menu::Confirm, window, cx| {
1545 if let Some(thread_view) = thread_view.upgrade() {
1546 thread_view.focus_handle(cx).focus(window);
1547 }
1548 }
1549 })
1550 .on_action({
1551 let thread_view = thread_view.downgrade();
1552 move |_: &editor::actions::Cancel, window, cx| {
1553 if let Some(thread_view) = thread_view.upgrade() {
1554 thread_view.focus_handle(cx).focus(window);
1555 }
1556 }
1557 })
1558 .child(title_editor)
1559 .into_any_element()
1560 } else {
1561 Label::new(thread_view.read(cx).title(cx))
1562 .color(Color::Muted)
1563 .truncate()
1564 .into_any_element()
1565 }
1566 }
1567 ActiveView::TextThread {
1568 title_editor,
1569 text_thread_editor,
1570 ..
1571 } => {
1572 let summary = text_thread_editor.read(cx).text_thread().read(cx).summary();
1573
1574 match summary {
1575 TextThreadSummary::Pending => Label::new(TextThreadSummary::DEFAULT)
1576 .color(Color::Muted)
1577 .truncate()
1578 .into_any_element(),
1579 TextThreadSummary::Content(summary) => {
1580 if summary.done {
1581 div()
1582 .w_full()
1583 .child(title_editor.clone())
1584 .into_any_element()
1585 } else {
1586 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1587 .truncate()
1588 .color(Color::Muted)
1589 .into_any_element()
1590 }
1591 }
1592 TextThreadSummary::Error => h_flex()
1593 .w_full()
1594 .child(title_editor.clone())
1595 .child(
1596 IconButton::new("retry-summary-generation", IconName::RotateCcw)
1597 .icon_size(IconSize::Small)
1598 .on_click({
1599 let text_thread_editor = text_thread_editor.clone();
1600 move |_, _window, cx| {
1601 text_thread_editor.update(cx, |text_thread_editor, cx| {
1602 text_thread_editor.regenerate_summary(cx);
1603 });
1604 }
1605 })
1606 .tooltip(move |_window, cx| {
1607 cx.new(|_| {
1608 Tooltip::new("Failed to generate title")
1609 .meta("Click to try again")
1610 })
1611 .into()
1612 }),
1613 )
1614 .into_any_element(),
1615 }
1616 }
1617 ActiveView::History => Label::new("History").truncate().into_any_element(),
1618 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1619 };
1620
1621 h_flex()
1622 .key_context("TitleEditor")
1623 .id("TitleEditor")
1624 .flex_grow()
1625 .w_full()
1626 .max_w_full()
1627 .overflow_x_scroll()
1628 .child(content)
1629 .into_any()
1630 }
1631
1632 fn render_panel_options_menu(
1633 &self,
1634 window: &mut Window,
1635 cx: &mut Context<Self>,
1636 ) -> impl IntoElement {
1637 let user_store = self.user_store.read(cx);
1638 let usage = user_store.model_request_usage();
1639 let account_url = zed_urls::account_url(cx);
1640
1641 let focus_handle = self.focus_handle(cx);
1642
1643 let full_screen_label = if self.is_zoomed(window, cx) {
1644 "Disable Full Screen"
1645 } else {
1646 "Enable Full Screen"
1647 };
1648
1649 let selected_agent = self.selected_agent.clone();
1650
1651 PopoverMenu::new("agent-options-menu")
1652 .trigger_with_tooltip(
1653 IconButton::new("agent-options-menu", IconName::Ellipsis)
1654 .icon_size(IconSize::Small),
1655 {
1656 let focus_handle = focus_handle.clone();
1657 move |_window, cx| {
1658 Tooltip::for_action_in(
1659 "Toggle Agent Menu",
1660 &ToggleOptionsMenu,
1661 &focus_handle,
1662 cx,
1663 )
1664 }
1665 },
1666 )
1667 .anchor(Corner::TopRight)
1668 .with_handle(self.agent_panel_menu_handle.clone())
1669 .menu({
1670 move |window, cx| {
1671 Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
1672 menu = menu.context(focus_handle.clone());
1673 if let Some(usage) = usage {
1674 menu = menu
1675 .header_with_link("Prompt Usage", "Manage", account_url.clone())
1676 .custom_entry(
1677 move |_window, cx| {
1678 let used_percentage = match usage.limit {
1679 UsageLimit::Limited(limit) => {
1680 Some((usage.amount as f32 / limit as f32) * 100.)
1681 }
1682 UsageLimit::Unlimited => None,
1683 };
1684
1685 h_flex()
1686 .flex_1()
1687 .gap_1p5()
1688 .children(used_percentage.map(|percent| {
1689 ProgressBar::new("usage", percent, 100., cx)
1690 }))
1691 .child(
1692 Label::new(match usage.limit {
1693 UsageLimit::Limited(limit) => {
1694 format!("{} / {limit}", usage.amount)
1695 }
1696 UsageLimit::Unlimited => {
1697 format!("{} / ∞", usage.amount)
1698 }
1699 })
1700 .size(LabelSize::Small)
1701 .color(Color::Muted),
1702 )
1703 .into_any_element()
1704 },
1705 move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
1706 )
1707 .separator()
1708 }
1709
1710 menu = menu
1711 .header("MCP Servers")
1712 .action(
1713 "View Server Extensions",
1714 Box::new(zed_actions::Extensions {
1715 category_filter: Some(
1716 zed_actions::ExtensionCategoryFilter::ContextServers,
1717 ),
1718 id: None,
1719 }),
1720 )
1721 .action("Add Custom Server…", Box::new(AddContextServer))
1722 .separator();
1723
1724 menu = menu
1725 .action("Rules", Box::new(OpenRulesLibrary::default()))
1726 .action("Settings", Box::new(OpenSettings))
1727 .separator()
1728 .action(full_screen_label, Box::new(ToggleZoom));
1729
1730 if selected_agent == AgentType::Gemini {
1731 menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
1732 }
1733
1734 menu
1735 }))
1736 }
1737 })
1738 }
1739
1740 fn render_recent_entries_menu(
1741 &self,
1742 icon: IconName,
1743 corner: Corner,
1744 cx: &mut Context<Self>,
1745 ) -> impl IntoElement {
1746 let focus_handle = self.focus_handle(cx);
1747
1748 PopoverMenu::new("agent-nav-menu")
1749 .trigger_with_tooltip(
1750 IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
1751 {
1752 move |_window, cx| {
1753 Tooltip::for_action_in(
1754 "Toggle Recent Threads",
1755 &ToggleNavigationMenu,
1756 &focus_handle,
1757 cx,
1758 )
1759 }
1760 },
1761 )
1762 .anchor(corner)
1763 .with_handle(self.agent_navigation_menu_handle.clone())
1764 .menu({
1765 let menu = self.agent_navigation_menu.clone();
1766 move |window, cx| {
1767 telemetry::event!("View Thread History Clicked");
1768
1769 if let Some(menu) = menu.as_ref() {
1770 menu.update(cx, |_, cx| {
1771 cx.defer_in(window, |menu, window, cx| {
1772 menu.rebuild(window, cx);
1773 });
1774 })
1775 }
1776 menu.clone()
1777 }
1778 })
1779 }
1780
1781 fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
1782 let focus_handle = self.focus_handle(cx);
1783
1784 IconButton::new("go-back", IconName::ArrowLeft)
1785 .icon_size(IconSize::Small)
1786 .on_click(cx.listener(|this, _, window, cx| {
1787 this.go_back(&workspace::GoBack, window, cx);
1788 }))
1789 .tooltip({
1790 move |_window, cx| {
1791 Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
1792 }
1793 })
1794 }
1795
1796 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1797 let agent_server_store = self.project.read(cx).agent_server_store().clone();
1798 let focus_handle = self.focus_handle(cx);
1799
1800 // Get custom icon path for selected agent before building menu (to avoid borrow issues)
1801 let selected_agent_custom_icon =
1802 if let AgentType::Custom { name, .. } = &self.selected_agent {
1803 agent_server_store
1804 .read(cx)
1805 .agent_icon(&ExternalAgentServerName(name.clone()))
1806 } else {
1807 None
1808 };
1809
1810 let active_thread = match &self.active_view {
1811 ActiveView::ExternalAgentThread { thread_view } => {
1812 thread_view.read(cx).as_native_thread(cx)
1813 }
1814 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
1815 };
1816
1817 let new_thread_menu = PopoverMenu::new("new_thread_menu")
1818 .trigger_with_tooltip(
1819 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
1820 {
1821 let focus_handle = focus_handle.clone();
1822 move |_window, cx| {
1823 Tooltip::for_action_in("New…", &ToggleNewThreadMenu, &focus_handle, cx)
1824 }
1825 },
1826 )
1827 .anchor(Corner::TopRight)
1828 .with_handle(self.new_thread_menu_handle.clone())
1829 .menu({
1830 let workspace = self.workspace.clone();
1831 let is_via_collab = workspace
1832 .update(cx, |workspace, cx| {
1833 workspace.project().read(cx).is_via_collab()
1834 })
1835 .unwrap_or_default();
1836
1837 move |window, cx| {
1838 telemetry::event!("New Thread Clicked");
1839
1840 let active_thread = active_thread.clone();
1841 Some(ContextMenu::build(window, cx, |menu, _window, cx| {
1842 menu.context(focus_handle.clone())
1843 .header("Zed Agent")
1844 .when_some(active_thread, |this, active_thread| {
1845 let thread = active_thread.read(cx);
1846
1847 if !thread.is_empty() {
1848 let session_id = thread.id().clone();
1849 this.item(
1850 ContextMenuEntry::new("New From Summary")
1851 .icon(IconName::ThreadFromSummary)
1852 .icon_color(Color::Muted)
1853 .handler(move |window, cx| {
1854 window.dispatch_action(
1855 Box::new(NewNativeAgentThreadFromSummary {
1856 from_session_id: session_id.clone(),
1857 }),
1858 cx,
1859 );
1860 }),
1861 )
1862 } else {
1863 this
1864 }
1865 })
1866 .item(
1867 ContextMenuEntry::new("New Thread")
1868 .action(NewThread.boxed_clone())
1869 .icon(IconName::Thread)
1870 .icon_color(Color::Muted)
1871 .handler({
1872 let workspace = workspace.clone();
1873 move |window, cx| {
1874 if let Some(workspace) = workspace.upgrade() {
1875 workspace.update(cx, |workspace, cx| {
1876 if let Some(panel) =
1877 workspace.panel::<AgentPanel>(cx)
1878 {
1879 panel.update(cx, |panel, cx| {
1880 panel.new_agent_thread(
1881 AgentType::NativeAgent,
1882 window,
1883 cx,
1884 );
1885 });
1886 }
1887 });
1888 }
1889 }
1890 }),
1891 )
1892 .item(
1893 ContextMenuEntry::new("New Text Thread")
1894 .icon(IconName::TextThread)
1895 .icon_color(Color::Muted)
1896 .action(NewTextThread.boxed_clone())
1897 .handler({
1898 let workspace = workspace.clone();
1899 move |window, cx| {
1900 if let Some(workspace) = workspace.upgrade() {
1901 workspace.update(cx, |workspace, cx| {
1902 if let Some(panel) =
1903 workspace.panel::<AgentPanel>(cx)
1904 {
1905 panel.update(cx, |panel, cx| {
1906 panel.new_agent_thread(
1907 AgentType::TextThread,
1908 window,
1909 cx,
1910 );
1911 });
1912 }
1913 });
1914 }
1915 }
1916 }),
1917 )
1918 .separator()
1919 .header("External Agents")
1920 .item(
1921 ContextMenuEntry::new("New Claude Code Thread")
1922 .icon(IconName::AiClaude)
1923 .disabled(is_via_collab)
1924 .icon_color(Color::Muted)
1925 .handler({
1926 let workspace = workspace.clone();
1927 move |window, cx| {
1928 if let Some(workspace) = workspace.upgrade() {
1929 workspace.update(cx, |workspace, cx| {
1930 if let Some(panel) =
1931 workspace.panel::<AgentPanel>(cx)
1932 {
1933 panel.update(cx, |panel, cx| {
1934 panel.new_agent_thread(
1935 AgentType::ClaudeCode,
1936 window,
1937 cx,
1938 );
1939 });
1940 }
1941 });
1942 }
1943 }
1944 }),
1945 )
1946 .item(
1947 ContextMenuEntry::new("New Codex Thread")
1948 .icon(IconName::AiOpenAi)
1949 .disabled(is_via_collab)
1950 .icon_color(Color::Muted)
1951 .handler({
1952 let workspace = workspace.clone();
1953 move |window, cx| {
1954 if let Some(workspace) = workspace.upgrade() {
1955 workspace.update(cx, |workspace, cx| {
1956 if let Some(panel) =
1957 workspace.panel::<AgentPanel>(cx)
1958 {
1959 panel.update(cx, |panel, cx| {
1960 panel.new_agent_thread(
1961 AgentType::Codex,
1962 window,
1963 cx,
1964 );
1965 });
1966 }
1967 });
1968 }
1969 }
1970 }),
1971 )
1972 .item(
1973 ContextMenuEntry::new("New Gemini CLI Thread")
1974 .icon(IconName::AiGemini)
1975 .icon_color(Color::Muted)
1976 .disabled(is_via_collab)
1977 .handler({
1978 let workspace = workspace.clone();
1979 move |window, cx| {
1980 if let Some(workspace) = workspace.upgrade() {
1981 workspace.update(cx, |workspace, cx| {
1982 if let Some(panel) =
1983 workspace.panel::<AgentPanel>(cx)
1984 {
1985 panel.update(cx, |panel, cx| {
1986 panel.new_agent_thread(
1987 AgentType::Gemini,
1988 window,
1989 cx,
1990 );
1991 });
1992 }
1993 });
1994 }
1995 }
1996 }),
1997 )
1998 .map(|mut menu| {
1999 let agent_server_store_read = agent_server_store.read(cx);
2000 let agent_names = agent_server_store_read
2001 .external_agents()
2002 .filter(|name| {
2003 name.0 != GEMINI_NAME
2004 && name.0 != CLAUDE_CODE_NAME
2005 && name.0 != CODEX_NAME
2006 })
2007 .cloned()
2008 .collect::<Vec<_>>();
2009 let custom_settings = cx
2010 .global::<SettingsStore>()
2011 .get::<AllAgentServersSettings>(None)
2012 .custom
2013 .clone();
2014 for agent_name in agent_names {
2015 let icon_path = agent_server_store_read.agent_icon(&agent_name);
2016 let mut entry =
2017 ContextMenuEntry::new(format!("New {} Thread", agent_name));
2018 if let Some(icon_path) = icon_path {
2019 entry = entry.custom_icon_path(icon_path);
2020 } else {
2021 entry = entry.icon(IconName::Terminal);
2022 }
2023 entry = entry
2024 .icon_color(Color::Muted)
2025 .disabled(is_via_collab)
2026 .handler({
2027 let workspace = workspace.clone();
2028 let agent_name = agent_name.clone();
2029 let custom_settings = custom_settings.clone();
2030 move |window, cx| {
2031 if let Some(workspace) = workspace.upgrade() {
2032 workspace.update(cx, |workspace, cx| {
2033 if let Some(panel) =
2034 workspace.panel::<AgentPanel>(cx)
2035 {
2036 panel.update(cx, |panel, cx| {
2037 panel.new_agent_thread(
2038 AgentType::Custom {
2039 name: agent_name
2040 .clone()
2041 .into(),
2042 command: custom_settings
2043 .get(&agent_name.0)
2044 .map(|settings| {
2045 settings
2046 .command
2047 .clone()
2048 })
2049 .unwrap_or(
2050 placeholder_command(
2051 ),
2052 ),
2053 },
2054 window,
2055 cx,
2056 );
2057 });
2058 }
2059 });
2060 }
2061 }
2062 });
2063 menu = menu.item(entry);
2064 }
2065
2066 menu
2067 })
2068 .separator()
2069 .link(
2070 "Add Other Agents",
2071 OpenBrowser {
2072 url: zed_urls::external_agents_docs(cx),
2073 }
2074 .boxed_clone(),
2075 )
2076 }))
2077 }
2078 });
2079
2080 let selected_agent_label = self.selected_agent.label();
2081
2082 let has_custom_icon = selected_agent_custom_icon.is_some();
2083 let selected_agent = div()
2084 .id("selected_agent_icon")
2085 .when_some(selected_agent_custom_icon, |this, icon_path| {
2086 let label = selected_agent_label.clone();
2087 this.px(DynamicSpacing::Base02.rems(cx))
2088 .child(Icon::from_path(icon_path).color(Color::Muted))
2089 .tooltip(move |_window, cx| {
2090 Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
2091 })
2092 })
2093 .when(!has_custom_icon, |this| {
2094 this.when_some(self.selected_agent.icon(), |this, icon| {
2095 let label = selected_agent_label.clone();
2096 this.px(DynamicSpacing::Base02.rems(cx))
2097 .child(Icon::new(icon).color(Color::Muted))
2098 .tooltip(move |_window, cx| {
2099 Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
2100 })
2101 })
2102 })
2103 .into_any_element();
2104
2105 h_flex()
2106 .id("agent-panel-toolbar")
2107 .h(Tab::container_height(cx))
2108 .max_w_full()
2109 .flex_none()
2110 .justify_between()
2111 .gap_2()
2112 .bg(cx.theme().colors().tab_bar_background)
2113 .border_b_1()
2114 .border_color(cx.theme().colors().border)
2115 .child(
2116 h_flex()
2117 .size_full()
2118 .gap(DynamicSpacing::Base04.rems(cx))
2119 .pl(DynamicSpacing::Base04.rems(cx))
2120 .child(match &self.active_view {
2121 ActiveView::History | ActiveView::Configuration => {
2122 self.render_toolbar_back_button(cx).into_any_element()
2123 }
2124 _ => selected_agent.into_any_element(),
2125 })
2126 .child(self.render_title_view(window, cx)),
2127 )
2128 .child(
2129 h_flex()
2130 .flex_none()
2131 .gap(DynamicSpacing::Base02.rems(cx))
2132 .pl(DynamicSpacing::Base04.rems(cx))
2133 .pr(DynamicSpacing::Base06.rems(cx))
2134 .child(new_thread_menu)
2135 .child(self.render_recent_entries_menu(
2136 IconName::MenuAltTemp,
2137 Corner::TopRight,
2138 cx,
2139 ))
2140 .child(self.render_panel_options_menu(window, cx)),
2141 )
2142 }
2143
2144 fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2145 if TrialEndUpsell::dismissed() {
2146 return false;
2147 }
2148
2149 match &self.active_view {
2150 ActiveView::TextThread { .. } => {
2151 if LanguageModelRegistry::global(cx)
2152 .read(cx)
2153 .default_model()
2154 .is_some_and(|model| {
2155 model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2156 })
2157 {
2158 return false;
2159 }
2160 }
2161 ActiveView::ExternalAgentThread { .. }
2162 | ActiveView::History
2163 | ActiveView::Configuration => return false,
2164 }
2165
2166 let plan = self.user_store.read(cx).plan();
2167 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2168
2169 matches!(
2170 plan,
2171 Some(Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree))
2172 ) && has_previous_trial
2173 }
2174
2175 fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
2176 if OnboardingUpsell::dismissed() {
2177 return false;
2178 }
2179
2180 let user_store = self.user_store.read(cx);
2181
2182 if user_store
2183 .plan()
2184 .is_some_and(|plan| matches!(plan, Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro)))
2185 && user_store
2186 .subscription_period()
2187 .and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
2188 .is_some_and(|date| date < chrono::Utc::now())
2189 {
2190 OnboardingUpsell::set_dismissed(true, cx);
2191 return false;
2192 }
2193
2194 match &self.active_view {
2195 ActiveView::History | ActiveView::Configuration => false,
2196 ActiveView::ExternalAgentThread { thread_view, .. }
2197 if thread_view.read(cx).as_native_thread(cx).is_none() =>
2198 {
2199 false
2200 }
2201 _ => {
2202 let history_is_empty = self.history_store.read(cx).is_empty(cx);
2203
2204 let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
2205 .providers()
2206 .iter()
2207 .any(|provider| {
2208 provider.is_authenticated(cx)
2209 && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2210 });
2211
2212 history_is_empty || !has_configured_non_zed_providers
2213 }
2214 }
2215 }
2216
2217 fn render_onboarding(
2218 &self,
2219 _window: &mut Window,
2220 cx: &mut Context<Self>,
2221 ) -> Option<impl IntoElement> {
2222 if !self.should_render_onboarding(cx) {
2223 return None;
2224 }
2225
2226 let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
2227
2228 Some(
2229 div()
2230 .when(text_thread_view, |this| {
2231 this.bg(cx.theme().colors().editor_background)
2232 })
2233 .child(self.onboarding.clone()),
2234 )
2235 }
2236
2237 fn render_trial_end_upsell(
2238 &self,
2239 _window: &mut Window,
2240 cx: &mut Context<Self>,
2241 ) -> Option<impl IntoElement> {
2242 if !self.should_render_trial_end_upsell(cx) {
2243 return None;
2244 }
2245
2246 let plan = self.user_store.read(cx).plan()?;
2247
2248 Some(
2249 v_flex()
2250 .absolute()
2251 .inset_0()
2252 .size_full()
2253 .bg(cx.theme().colors().panel_background)
2254 .opacity(0.85)
2255 .block_mouse_except_scroll()
2256 .child(EndTrialUpsell::new(
2257 plan,
2258 Arc::new({
2259 let this = cx.entity();
2260 move |_, cx| {
2261 this.update(cx, |_this, cx| {
2262 TrialEndUpsell::set_dismissed(true, cx);
2263 cx.notify();
2264 });
2265 }
2266 }),
2267 )),
2268 )
2269 }
2270
2271 fn render_configuration_error(
2272 &self,
2273 border_bottom: bool,
2274 configuration_error: &ConfigurationError,
2275 focus_handle: &FocusHandle,
2276 cx: &mut App,
2277 ) -> impl IntoElement {
2278 let zed_provider_configured = AgentSettings::get_global(cx)
2279 .default_model
2280 .as_ref()
2281 .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev");
2282
2283 let callout = if zed_provider_configured {
2284 Callout::new()
2285 .icon(IconName::Warning)
2286 .severity(Severity::Warning)
2287 .when(border_bottom, |this| {
2288 this.border_position(ui::BorderPosition::Bottom)
2289 })
2290 .title("Sign in to continue using Zed as your LLM provider.")
2291 .actions_slot(
2292 Button::new("sign_in", "Sign In")
2293 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2294 .label_size(LabelSize::Small)
2295 .on_click({
2296 let workspace = self.workspace.clone();
2297 move |_, _, cx| {
2298 let Ok(client) =
2299 workspace.update(cx, |workspace, _| workspace.client().clone())
2300 else {
2301 return;
2302 };
2303
2304 cx.spawn(async move |cx| {
2305 client.sign_in_with_optional_connect(true, cx).await
2306 })
2307 .detach_and_log_err(cx);
2308 }
2309 }),
2310 )
2311 } else {
2312 Callout::new()
2313 .icon(IconName::Warning)
2314 .severity(Severity::Warning)
2315 .when(border_bottom, |this| {
2316 this.border_position(ui::BorderPosition::Bottom)
2317 })
2318 .title(configuration_error.to_string())
2319 .actions_slot(
2320 Button::new("settings", "Configure")
2321 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2322 .label_size(LabelSize::Small)
2323 .key_binding(
2324 KeyBinding::for_action_in(&OpenSettings, focus_handle, cx)
2325 .map(|kb| kb.size(rems_from_px(12.))),
2326 )
2327 .on_click(|_event, window, cx| {
2328 window.dispatch_action(OpenSettings.boxed_clone(), cx)
2329 }),
2330 )
2331 };
2332
2333 match configuration_error {
2334 ConfigurationError::ModelNotFound
2335 | ConfigurationError::ProviderNotAuthenticated(_)
2336 | ConfigurationError::NoProvider => callout.into_any_element(),
2337 }
2338 }
2339
2340 fn render_text_thread(
2341 &self,
2342 text_thread_editor: &Entity<TextThreadEditor>,
2343 buffer_search_bar: &Entity<BufferSearchBar>,
2344 window: &mut Window,
2345 cx: &mut Context<Self>,
2346 ) -> Div {
2347 let mut registrar = buffer_search::DivRegistrar::new(
2348 |this, _, _cx| match &this.active_view {
2349 ActiveView::TextThread {
2350 buffer_search_bar, ..
2351 } => Some(buffer_search_bar.clone()),
2352 _ => None,
2353 },
2354 cx,
2355 );
2356 BufferSearchBar::register(&mut registrar);
2357 registrar
2358 .into_div()
2359 .size_full()
2360 .relative()
2361 .map(|parent| {
2362 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2363 if buffer_search_bar.is_dismissed() {
2364 return parent;
2365 }
2366 parent.child(
2367 div()
2368 .p(DynamicSpacing::Base08.rems(cx))
2369 .border_b_1()
2370 .border_color(cx.theme().colors().border_variant)
2371 .bg(cx.theme().colors().editor_background)
2372 .child(buffer_search_bar.render(window, cx)),
2373 )
2374 })
2375 })
2376 .child(text_thread_editor.clone())
2377 .child(self.render_drag_target(cx))
2378 }
2379
2380 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
2381 let is_local = self.project.read(cx).is_local();
2382 div()
2383 .invisible()
2384 .absolute()
2385 .top_0()
2386 .right_0()
2387 .bottom_0()
2388 .left_0()
2389 .bg(cx.theme().colors().drop_target_background)
2390 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
2391 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
2392 .when(is_local, |this| {
2393 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
2394 })
2395 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
2396 let item = tab.pane.read(cx).item_for_index(tab.ix);
2397 let project_paths = item
2398 .and_then(|item| item.project_path(cx))
2399 .into_iter()
2400 .collect::<Vec<_>>();
2401 this.handle_drop(project_paths, vec![], window, cx);
2402 }))
2403 .on_drop(
2404 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2405 let project_paths = selection
2406 .items()
2407 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
2408 .collect::<Vec<_>>();
2409 this.handle_drop(project_paths, vec![], window, cx);
2410 }),
2411 )
2412 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
2413 let tasks = paths
2414 .paths()
2415 .iter()
2416 .map(|path| {
2417 Workspace::project_path_for_path(this.project.clone(), path, false, cx)
2418 })
2419 .collect::<Vec<_>>();
2420 cx.spawn_in(window, async move |this, cx| {
2421 let mut paths = vec![];
2422 let mut added_worktrees = vec![];
2423 let opened_paths = futures::future::join_all(tasks).await;
2424 for entry in opened_paths {
2425 if let Some((worktree, project_path)) = entry.log_err() {
2426 added_worktrees.push(worktree);
2427 paths.push(project_path);
2428 }
2429 }
2430 this.update_in(cx, |this, window, cx| {
2431 this.handle_drop(paths, added_worktrees, window, cx);
2432 })
2433 .ok();
2434 })
2435 .detach();
2436 }))
2437 }
2438
2439 fn handle_drop(
2440 &mut self,
2441 paths: Vec<ProjectPath>,
2442 added_worktrees: Vec<Entity<Worktree>>,
2443 window: &mut Window,
2444 cx: &mut Context<Self>,
2445 ) {
2446 match &self.active_view {
2447 ActiveView::ExternalAgentThread { thread_view } => {
2448 thread_view.update(cx, |thread_view, cx| {
2449 thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
2450 });
2451 }
2452 ActiveView::TextThread {
2453 text_thread_editor, ..
2454 } => {
2455 text_thread_editor.update(cx, |text_thread_editor, cx| {
2456 TextThreadEditor::insert_dragged_files(
2457 text_thread_editor,
2458 paths,
2459 added_worktrees,
2460 window,
2461 cx,
2462 );
2463 });
2464 }
2465 ActiveView::History | ActiveView::Configuration => {}
2466 }
2467 }
2468
2469 fn key_context(&self) -> KeyContext {
2470 let mut key_context = KeyContext::new_with_defaults();
2471 key_context.add("AgentPanel");
2472 match &self.active_view {
2473 ActiveView::ExternalAgentThread { .. } => key_context.add("acp_thread"),
2474 ActiveView::TextThread { .. } => key_context.add("text_thread"),
2475 ActiveView::History | ActiveView::Configuration => {}
2476 }
2477 key_context
2478 }
2479}
2480
2481impl Render for AgentPanel {
2482 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2483 // WARNING: Changes to this element hierarchy can have
2484 // non-obvious implications to the layout of children.
2485 //
2486 // If you need to change it, please confirm:
2487 // - The message editor expands (cmd-option-esc) correctly
2488 // - When expanded, the buttons at the bottom of the panel are displayed correctly
2489 // - Font size works as expected and can be changed with cmd-+/cmd-
2490 // - Scrolling in all views works as expected
2491 // - Files can be dropped into the panel
2492 let content = v_flex()
2493 .relative()
2494 .size_full()
2495 .justify_between()
2496 .key_context(self.key_context())
2497 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
2498 this.new_thread(action, window, cx);
2499 }))
2500 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
2501 this.open_history(window, cx);
2502 }))
2503 .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
2504 this.open_configuration(window, cx);
2505 }))
2506 .on_action(cx.listener(Self::open_active_thread_as_markdown))
2507 .on_action(cx.listener(Self::deploy_rules_library))
2508 .on_action(cx.listener(Self::go_back))
2509 .on_action(cx.listener(Self::toggle_navigation_menu))
2510 .on_action(cx.listener(Self::toggle_options_menu))
2511 .on_action(cx.listener(Self::increase_font_size))
2512 .on_action(cx.listener(Self::decrease_font_size))
2513 .on_action(cx.listener(Self::reset_font_size))
2514 .on_action(cx.listener(Self::toggle_zoom))
2515 .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
2516 if let Some(thread_view) = this.active_thread_view() {
2517 thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
2518 }
2519 }))
2520 .child(self.render_toolbar(window, cx))
2521 .children(self.render_onboarding(window, cx))
2522 .map(|parent| match &self.active_view {
2523 ActiveView::ExternalAgentThread { thread_view, .. } => parent
2524 .child(thread_view.clone())
2525 .child(self.render_drag_target(cx)),
2526 ActiveView::History => parent.child(self.acp_history.clone()),
2527 ActiveView::TextThread {
2528 text_thread_editor,
2529 buffer_search_bar,
2530 ..
2531 } => {
2532 let model_registry = LanguageModelRegistry::read_global(cx);
2533 let configuration_error =
2534 model_registry.configuration_error(model_registry.default_model(), cx);
2535 parent
2536 .map(|this| {
2537 if !self.should_render_onboarding(cx)
2538 && let Some(err) = configuration_error.as_ref()
2539 {
2540 this.child(self.render_configuration_error(
2541 true,
2542 err,
2543 &self.focus_handle(cx),
2544 cx,
2545 ))
2546 } else {
2547 this
2548 }
2549 })
2550 .child(self.render_text_thread(
2551 text_thread_editor,
2552 buffer_search_bar,
2553 window,
2554 cx,
2555 ))
2556 }
2557 ActiveView::Configuration => parent.children(self.configuration.clone()),
2558 })
2559 .children(self.render_trial_end_upsell(window, cx));
2560
2561 match self.active_view.which_font_size_used() {
2562 WhichFontSize::AgentFont => {
2563 WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
2564 .size_full()
2565 .child(content)
2566 .into_any()
2567 }
2568 _ => content.into_any(),
2569 }
2570 }
2571}
2572
2573struct PromptLibraryInlineAssist {
2574 workspace: WeakEntity<Workspace>,
2575}
2576
2577impl PromptLibraryInlineAssist {
2578 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
2579 Self { workspace }
2580 }
2581}
2582
2583impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
2584 fn assist(
2585 &self,
2586 prompt_editor: &Entity<Editor>,
2587 initial_prompt: Option<String>,
2588 window: &mut Window,
2589 cx: &mut Context<RulesLibrary>,
2590 ) {
2591 InlineAssistant::update_global(cx, |assistant, cx| {
2592 let Some(project) = self
2593 .workspace
2594 .upgrade()
2595 .map(|workspace| workspace.read(cx).project().downgrade())
2596 else {
2597 return;
2598 };
2599 let prompt_store = None;
2600 let thread_store = None;
2601 let context_store = cx.new(|_| ContextStore::new(project.clone()));
2602 assistant.assist(
2603 prompt_editor,
2604 self.workspace.clone(),
2605 context_store,
2606 project,
2607 prompt_store,
2608 thread_store,
2609 initial_prompt,
2610 window,
2611 cx,
2612 )
2613 })
2614 }
2615
2616 fn focus_agent_panel(
2617 &self,
2618 workspace: &mut Workspace,
2619 window: &mut Window,
2620 cx: &mut Context<Workspace>,
2621 ) -> bool {
2622 workspace.focus_panel::<AgentPanel>(window, cx).is_some()
2623 }
2624}
2625
2626pub struct ConcreteAssistantPanelDelegate;
2627
2628impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
2629 fn active_text_thread_editor(
2630 &self,
2631 workspace: &mut Workspace,
2632 _window: &mut Window,
2633 cx: &mut Context<Workspace>,
2634 ) -> Option<Entity<TextThreadEditor>> {
2635 let panel = workspace.panel::<AgentPanel>(cx)?;
2636 panel.read(cx).active_text_thread_editor()
2637 }
2638
2639 fn open_local_text_thread(
2640 &self,
2641 workspace: &mut Workspace,
2642 path: Arc<Path>,
2643 window: &mut Window,
2644 cx: &mut Context<Workspace>,
2645 ) -> Task<Result<()>> {
2646 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
2647 return Task::ready(Err(anyhow!("Agent panel not found")));
2648 };
2649
2650 panel.update(cx, |panel, cx| {
2651 panel.open_saved_text_thread(path, window, cx)
2652 })
2653 }
2654
2655 fn open_remote_text_thread(
2656 &self,
2657 _workspace: &mut Workspace,
2658 _text_thread_id: assistant_text_thread::TextThreadId,
2659 _window: &mut Window,
2660 _cx: &mut Context<Workspace>,
2661 ) -> Task<Result<Entity<TextThreadEditor>>> {
2662 Task::ready(Err(anyhow!("opening remote context not implemented")))
2663 }
2664
2665 fn quote_selection(
2666 &self,
2667 workspace: &mut Workspace,
2668 selection_ranges: Vec<Range<Anchor>>,
2669 buffer: Entity<MultiBuffer>,
2670 window: &mut Window,
2671 cx: &mut Context<Workspace>,
2672 ) {
2673 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
2674 return;
2675 };
2676
2677 if !panel.focus_handle(cx).contains_focused(window, cx) {
2678 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
2679 }
2680
2681 panel.update(cx, |_, cx| {
2682 // Wait to create a new context until the workspace is no longer
2683 // being updated.
2684 cx.defer_in(window, move |panel, window, cx| {
2685 if let Some(thread_view) = panel.active_thread_view() {
2686 thread_view.update(cx, |thread_view, cx| {
2687 thread_view.insert_selections(window, cx);
2688 });
2689 } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
2690 let snapshot = buffer.read(cx).snapshot(cx);
2691 let selection_ranges = selection_ranges
2692 .into_iter()
2693 .map(|range| range.to_point(&snapshot))
2694 .collect::<Vec<_>>();
2695
2696 text_thread_editor.update(cx, |text_thread_editor, cx| {
2697 text_thread_editor.quote_ranges(selection_ranges, snapshot, window, cx)
2698 });
2699 }
2700 });
2701 });
2702 }
2703}
2704
2705struct OnboardingUpsell;
2706
2707impl Dismissable for OnboardingUpsell {
2708 const KEY: &'static str = "dismissed-trial-upsell";
2709}
2710
2711struct TrialEndUpsell;
2712
2713impl Dismissable for TrialEndUpsell {
2714 const KEY: &'static str = "dismissed-trial-end-upsell";
2715}