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