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