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