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