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