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