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