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