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