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