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 #[ztracing::instrument(skip_all)]
1761 fn render_panel_options_menu(
1762 &self,
1763 window: &mut Window,
1764 cx: &mut Context<Self>,
1765 ) -> impl IntoElement {
1766 let user_store = self.user_store.read(cx);
1767 let usage = user_store.model_request_usage();
1768 let account_url = zed_urls::account_url(cx);
1769
1770 let focus_handle = self.focus_handle(cx);
1771
1772 let full_screen_label = if self.is_zoomed(window, cx) {
1773 "Disable Full Screen"
1774 } else {
1775 "Enable Full Screen"
1776 };
1777
1778 let selected_agent = self.selected_agent.clone();
1779
1780 let text_thread_view = match &self.active_view {
1781 ActiveView::TextThread {
1782 text_thread_editor, ..
1783 } => Some(text_thread_editor.clone()),
1784 _ => None,
1785 };
1786 let text_thread_with_messages = match &self.active_view {
1787 ActiveView::TextThread {
1788 text_thread_editor, ..
1789 } => text_thread_editor
1790 .read(cx)
1791 .text_thread()
1792 .read(cx)
1793 .messages(cx)
1794 .any(|message| message.role == language_model::Role::Assistant),
1795 _ => false,
1796 };
1797
1798 let thread_view = match &self.active_view {
1799 ActiveView::ExternalAgentThread { thread_view } => Some(thread_view.clone()),
1800 _ => None,
1801 };
1802 let thread_with_messages = match &self.active_view {
1803 ActiveView::ExternalAgentThread { thread_view } => {
1804 thread_view.read(cx).has_user_submitted_prompt(cx)
1805 }
1806 _ => false,
1807 };
1808
1809 PopoverMenu::new("agent-options-menu")
1810 .trigger_with_tooltip(
1811 IconButton::new("agent-options-menu", IconName::Ellipsis)
1812 .icon_size(IconSize::Small),
1813 {
1814 let focus_handle = focus_handle.clone();
1815 move |_window, cx| {
1816 Tooltip::for_action_in(
1817 "Toggle Agent Menu",
1818 &ToggleOptionsMenu,
1819 &focus_handle,
1820 cx,
1821 )
1822 }
1823 },
1824 )
1825 .anchor(Corner::TopRight)
1826 .with_handle(self.agent_panel_menu_handle.clone())
1827 .menu({
1828 move |window, cx| {
1829 Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
1830 menu = menu.context(focus_handle.clone());
1831
1832 if let Some(usage) = usage {
1833 menu = menu
1834 .header_with_link("Prompt Usage", "Manage", account_url.clone())
1835 .custom_entry(
1836 move |_window, cx| {
1837 let used_percentage = match usage.limit {
1838 UsageLimit::Limited(limit) => {
1839 Some((usage.amount as f32 / limit as f32) * 100.)
1840 }
1841 UsageLimit::Unlimited => None,
1842 };
1843
1844 h_flex()
1845 .flex_1()
1846 .gap_1p5()
1847 .children(used_percentage.map(|percent| {
1848 ProgressBar::new("usage", percent, 100., cx)
1849 }))
1850 .child(
1851 Label::new(match usage.limit {
1852 UsageLimit::Limited(limit) => {
1853 format!("{} / {limit}", usage.amount)
1854 }
1855 UsageLimit::Unlimited => {
1856 format!("{} / ∞", usage.amount)
1857 }
1858 })
1859 .size(LabelSize::Small)
1860 .color(Color::Muted),
1861 )
1862 .into_any_element()
1863 },
1864 move |_, cx| cx.open_url(&zed_urls::account_url(cx)),
1865 )
1866 .separator()
1867 }
1868
1869 if thread_with_messages | text_thread_with_messages {
1870 menu = menu.header("Current Thread");
1871
1872 if let Some(text_thread_view) = text_thread_view.as_ref() {
1873 menu = menu
1874 .entry("Regenerate Thread Title", None, {
1875 let text_thread_view = text_thread_view.clone();
1876 move |_, cx| {
1877 Self::handle_regenerate_text_thread_title(
1878 text_thread_view.clone(),
1879 cx,
1880 );
1881 }
1882 })
1883 .separator();
1884 }
1885
1886 if let Some(thread_view) = thread_view.as_ref() {
1887 menu = menu
1888 .entry("Regenerate Thread Title", None, {
1889 let thread_view = thread_view.clone();
1890 move |_, cx| {
1891 Self::handle_regenerate_thread_title(
1892 thread_view.clone(),
1893 cx,
1894 );
1895 }
1896 })
1897 .separator();
1898 }
1899 }
1900
1901 menu = menu
1902 .header("MCP Servers")
1903 .action(
1904 "View Server Extensions",
1905 Box::new(zed_actions::Extensions {
1906 category_filter: Some(
1907 zed_actions::ExtensionCategoryFilter::ContextServers,
1908 ),
1909 id: None,
1910 }),
1911 )
1912 .action("Add Custom Server…", Box::new(AddContextServer))
1913 .separator()
1914 .action("Rules", Box::new(OpenRulesLibrary::default()))
1915 .action("Profiles", Box::new(ManageProfiles::default()))
1916 .action("Settings", Box::new(OpenSettings))
1917 .separator()
1918 .action(full_screen_label, Box::new(ToggleZoom));
1919
1920 if selected_agent == AgentType::Gemini {
1921 menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
1922 }
1923
1924 menu
1925 }))
1926 }
1927 })
1928 }
1929
1930 fn render_recent_entries_menu(
1931 &self,
1932 icon: IconName,
1933 corner: Corner,
1934 cx: &mut Context<Self>,
1935 ) -> impl IntoElement {
1936 let focus_handle = self.focus_handle(cx);
1937
1938 PopoverMenu::new("agent-nav-menu")
1939 .trigger_with_tooltip(
1940 IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
1941 {
1942 move |_window, cx| {
1943 Tooltip::for_action_in(
1944 "Toggle Recent Threads",
1945 &ToggleNavigationMenu,
1946 &focus_handle,
1947 cx,
1948 )
1949 }
1950 },
1951 )
1952 .anchor(corner)
1953 .with_handle(self.agent_navigation_menu_handle.clone())
1954 .menu({
1955 let menu = self.agent_navigation_menu.clone();
1956 move |window, cx| {
1957 telemetry::event!("View Thread History Clicked");
1958
1959 if let Some(menu) = menu.as_ref() {
1960 menu.update(cx, |_, cx| {
1961 cx.defer_in(window, |menu, window, cx| {
1962 menu.rebuild(window, cx);
1963 });
1964 })
1965 }
1966 menu.clone()
1967 }
1968 })
1969 }
1970
1971 fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
1972 let focus_handle = self.focus_handle(cx);
1973
1974 IconButton::new("go-back", IconName::ArrowLeft)
1975 .icon_size(IconSize::Small)
1976 .on_click(cx.listener(|this, _, window, cx| {
1977 this.go_back(&workspace::GoBack, window, cx);
1978 }))
1979 .tooltip({
1980 move |_window, cx| {
1981 Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
1982 }
1983 })
1984 }
1985
1986 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1987 let agent_server_store = self.project.read(cx).agent_server_store().clone();
1988 let focus_handle = self.focus_handle(cx);
1989
1990 let (selected_agent_custom_icon, selected_agent_label) =
1991 if let AgentType::Custom { name, .. } = &self.selected_agent {
1992 let store = agent_server_store.read(cx);
1993 let icon = store.agent_icon(&ExternalAgentServerName(name.clone()));
1994
1995 let label = store
1996 .agent_display_name(&ExternalAgentServerName(name.clone()))
1997 .unwrap_or_else(|| self.selected_agent.label());
1998 (icon, label)
1999 } else {
2000 (None, self.selected_agent.label())
2001 };
2002
2003 let active_thread = match &self.active_view {
2004 ActiveView::ExternalAgentThread { thread_view } => {
2005 thread_view.read(cx).as_native_thread(cx)
2006 }
2007 ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
2008 };
2009
2010 let new_thread_menu = PopoverMenu::new("new_thread_menu")
2011 .trigger_with_tooltip(
2012 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2013 {
2014 let focus_handle = focus_handle.clone();
2015 move |_window, cx| {
2016 Tooltip::for_action_in(
2017 "New Thread…",
2018 &ToggleNewThreadMenu,
2019 &focus_handle,
2020 cx,
2021 )
2022 }
2023 },
2024 )
2025 .anchor(Corner::TopRight)
2026 .with_handle(self.new_thread_menu_handle.clone())
2027 .menu({
2028 let selected_agent = self.selected_agent.clone();
2029 let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
2030
2031 let workspace = self.workspace.clone();
2032 let is_via_collab = workspace
2033 .update(cx, |workspace, cx| {
2034 workspace.project().read(cx).is_via_collab()
2035 })
2036 .unwrap_or_default();
2037
2038 move |window, cx| {
2039 telemetry::event!("New Thread Clicked");
2040
2041 let active_thread = active_thread.clone();
2042 Some(ContextMenu::build(window, cx, |menu, _window, cx| {
2043 menu.context(focus_handle.clone())
2044 .when_some(active_thread, |this, active_thread| {
2045 let thread = active_thread.read(cx);
2046
2047 if !thread.is_empty() {
2048 let session_id = thread.id().clone();
2049 this.item(
2050 ContextMenuEntry::new("New From Summary")
2051 .icon(IconName::ThreadFromSummary)
2052 .icon_color(Color::Muted)
2053 .handler(move |window, cx| {
2054 window.dispatch_action(
2055 Box::new(NewNativeAgentThreadFromSummary {
2056 from_session_id: session_id.clone(),
2057 }),
2058 cx,
2059 );
2060 }),
2061 )
2062 } else {
2063 this
2064 }
2065 })
2066 .item(
2067 ContextMenuEntry::new("Zed Agent")
2068 .when(is_agent_selected(AgentType::NativeAgent) | is_agent_selected(AgentType::TextThread) , |this| {
2069 this.action(Box::new(NewExternalAgentThread { agent: None }))
2070 })
2071 .icon(IconName::ZedAgent)
2072 .icon_color(Color::Muted)
2073 .handler({
2074 let workspace = workspace.clone();
2075 move |window, cx| {
2076 if let Some(workspace) = workspace.upgrade() {
2077 workspace.update(cx, |workspace, cx| {
2078 if let Some(panel) =
2079 workspace.panel::<AgentPanel>(cx)
2080 {
2081 panel.update(cx, |panel, cx| {
2082 panel.new_agent_thread(
2083 AgentType::NativeAgent,
2084 window,
2085 cx,
2086 );
2087 });
2088 }
2089 });
2090 }
2091 }
2092 }),
2093 )
2094 .item(
2095 ContextMenuEntry::new("Text Thread")
2096 .action(NewTextThread.boxed_clone())
2097 .icon(IconName::TextThread)
2098 .icon_color(Color::Muted)
2099 .handler({
2100 let workspace = workspace.clone();
2101 move |window, cx| {
2102 if let Some(workspace) = workspace.upgrade() {
2103 workspace.update(cx, |workspace, cx| {
2104 if let Some(panel) =
2105 workspace.panel::<AgentPanel>(cx)
2106 {
2107 panel.update(cx, |panel, cx| {
2108 panel.new_agent_thread(
2109 AgentType::TextThread,
2110 window,
2111 cx,
2112 );
2113 });
2114 }
2115 });
2116 }
2117 }
2118 }),
2119 )
2120 .separator()
2121 .header("External Agents")
2122 .item(
2123 ContextMenuEntry::new("Claude Code")
2124 .when(is_agent_selected(AgentType::ClaudeCode), |this| {
2125 this.action(Box::new(NewExternalAgentThread { agent: None }))
2126 })
2127 .icon(IconName::AiClaude)
2128 .disabled(is_via_collab)
2129 .icon_color(Color::Muted)
2130 .handler({
2131 let workspace = workspace.clone();
2132 move |window, cx| {
2133 if let Some(workspace) = workspace.upgrade() {
2134 workspace.update(cx, |workspace, cx| {
2135 if let Some(panel) =
2136 workspace.panel::<AgentPanel>(cx)
2137 {
2138 panel.update(cx, |panel, cx| {
2139 panel.new_agent_thread(
2140 AgentType::ClaudeCode,
2141 window,
2142 cx,
2143 );
2144 });
2145 }
2146 });
2147 }
2148 }
2149 }),
2150 )
2151 .item(
2152 ContextMenuEntry::new("Codex CLI")
2153 .when(is_agent_selected(AgentType::Codex), |this| {
2154 this.action(Box::new(NewExternalAgentThread { agent: None }))
2155 })
2156 .icon(IconName::AiOpenAi)
2157 .disabled(is_via_collab)
2158 .icon_color(Color::Muted)
2159 .handler({
2160 let workspace = workspace.clone();
2161 move |window, cx| {
2162 if let Some(workspace) = workspace.upgrade() {
2163 workspace.update(cx, |workspace, cx| {
2164 if let Some(panel) =
2165 workspace.panel::<AgentPanel>(cx)
2166 {
2167 panel.update(cx, |panel, cx| {
2168 panel.new_agent_thread(
2169 AgentType::Codex,
2170 window,
2171 cx,
2172 );
2173 });
2174 }
2175 });
2176 }
2177 }
2178 }),
2179 )
2180 .item(
2181 ContextMenuEntry::new("Gemini CLI")
2182 .when(is_agent_selected(AgentType::Gemini), |this| {
2183 this.action(Box::new(NewExternalAgentThread { agent: None }))
2184 })
2185 .icon(IconName::AiGemini)
2186 .icon_color(Color::Muted)
2187 .disabled(is_via_collab)
2188 .handler({
2189 let workspace = workspace.clone();
2190 move |window, cx| {
2191 if let Some(workspace) = workspace.upgrade() {
2192 workspace.update(cx, |workspace, cx| {
2193 if let Some(panel) =
2194 workspace.panel::<AgentPanel>(cx)
2195 {
2196 panel.update(cx, |panel, cx| {
2197 panel.new_agent_thread(
2198 AgentType::Gemini,
2199 window,
2200 cx,
2201 );
2202 });
2203 }
2204 });
2205 }
2206 }
2207 }),
2208 )
2209 .map(|mut menu| {
2210 let agent_server_store = agent_server_store.read(cx);
2211 let agent_names = agent_server_store
2212 .external_agents()
2213 .filter(|name| {
2214 name.0 != GEMINI_NAME
2215 && name.0 != CLAUDE_CODE_NAME
2216 && name.0 != CODEX_NAME
2217 })
2218 .cloned()
2219 .collect::<Vec<_>>();
2220
2221 for agent_name in agent_names {
2222 let icon_path = agent_server_store.agent_icon(&agent_name);
2223 let display_name = agent_server_store
2224 .agent_display_name(&agent_name)
2225 .unwrap_or_else(|| agent_name.0.clone());
2226
2227 let mut entry = ContextMenuEntry::new(display_name);
2228
2229 if let Some(icon_path) = icon_path {
2230 entry = entry.custom_icon_svg(icon_path);
2231 } else {
2232 entry = entry.icon(IconName::Sparkle);
2233 }
2234 entry = entry
2235 .when(
2236 is_agent_selected(AgentType::Custom {
2237 name: agent_name.0.clone(),
2238 }),
2239 |this| {
2240 this.action(Box::new(NewExternalAgentThread { agent: None }))
2241 },
2242 )
2243 .icon_color(Color::Muted)
2244 .disabled(is_via_collab)
2245 .handler({
2246 let workspace = workspace.clone();
2247 let agent_name = agent_name.clone();
2248 move |window, cx| {
2249 if let Some(workspace) = workspace.upgrade() {
2250 workspace.update(cx, |workspace, cx| {
2251 if let Some(panel) =
2252 workspace.panel::<AgentPanel>(cx)
2253 {
2254 panel.update(cx, |panel, cx| {
2255 panel.new_agent_thread(
2256 AgentType::Custom {
2257 name: agent_name
2258 .clone()
2259 .into(),
2260 },
2261 window,
2262 cx,
2263 );
2264 });
2265 }
2266 });
2267 }
2268 }
2269 });
2270
2271 menu = menu.item(entry);
2272 }
2273
2274 menu
2275 })
2276 .separator()
2277 .item(
2278 ContextMenuEntry::new("Add More Agents")
2279 .icon(IconName::Plus)
2280 .icon_color(Color::Muted)
2281 .handler({
2282 move |window, cx| {
2283 window.dispatch_action(Box::new(zed_actions::Extensions {
2284 category_filter: Some(
2285 zed_actions::ExtensionCategoryFilter::AgentServers,
2286 ),
2287 id: None,
2288 }), cx)
2289 }
2290 }),
2291 )
2292 }))
2293 }
2294 });
2295
2296 let is_thread_loading = self
2297 .active_thread_view()
2298 .map(|thread| thread.read(cx).is_loading())
2299 .unwrap_or(false);
2300
2301 let has_custom_icon = selected_agent_custom_icon.is_some();
2302
2303 let selected_agent = div()
2304 .id("selected_agent_icon")
2305 .when_some(selected_agent_custom_icon, |this, icon_path| {
2306 this.px_1()
2307 .child(Icon::from_external_svg(icon_path).color(Color::Muted))
2308 })
2309 .when(!has_custom_icon, |this| {
2310 this.when_some(self.selected_agent.icon(), |this, icon| {
2311 this.px_1().child(Icon::new(icon).color(Color::Muted))
2312 })
2313 })
2314 .tooltip(move |_, cx| {
2315 Tooltip::with_meta(selected_agent_label.clone(), None, "Selected Agent", cx)
2316 });
2317
2318 let selected_agent = if is_thread_loading {
2319 selected_agent
2320 .with_animation(
2321 "pulsating-icon",
2322 Animation::new(Duration::from_secs(1))
2323 .repeat()
2324 .with_easing(pulsating_between(0.2, 0.6)),
2325 |icon, delta| icon.opacity(delta),
2326 )
2327 .into_any_element()
2328 } else {
2329 selected_agent.into_any_element()
2330 };
2331
2332 h_flex()
2333 .id("agent-panel-toolbar")
2334 .h(Tab::container_height(cx))
2335 .max_w_full()
2336 .flex_none()
2337 .justify_between()
2338 .gap_2()
2339 .bg(cx.theme().colors().tab_bar_background)
2340 .border_b_1()
2341 .border_color(cx.theme().colors().border)
2342 .child(
2343 h_flex()
2344 .size_full()
2345 .gap(DynamicSpacing::Base04.rems(cx))
2346 .pl(DynamicSpacing::Base04.rems(cx))
2347 .child(match &self.active_view {
2348 ActiveView::History | ActiveView::Configuration => {
2349 self.render_toolbar_back_button(cx).into_any_element()
2350 }
2351 _ => selected_agent.into_any_element(),
2352 })
2353 .child(self.render_title_view(window, cx)),
2354 )
2355 .child(
2356 h_flex()
2357 .flex_none()
2358 .gap(DynamicSpacing::Base02.rems(cx))
2359 .pl(DynamicSpacing::Base04.rems(cx))
2360 .pr(DynamicSpacing::Base06.rems(cx))
2361 .child(new_thread_menu)
2362 .child(self.render_recent_entries_menu(
2363 IconName::MenuAltTemp,
2364 Corner::TopRight,
2365 cx,
2366 ))
2367 .child(self.render_panel_options_menu(window, cx)),
2368 )
2369 }
2370
2371 fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2372 if TrialEndUpsell::dismissed() {
2373 return false;
2374 }
2375
2376 match &self.active_view {
2377 ActiveView::TextThread { .. } => {
2378 if LanguageModelRegistry::global(cx)
2379 .read(cx)
2380 .default_model()
2381 .is_some_and(|model| {
2382 model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2383 })
2384 {
2385 return false;
2386 }
2387 }
2388 ActiveView::ExternalAgentThread { .. }
2389 | ActiveView::History
2390 | ActiveView::Configuration => return false,
2391 }
2392
2393 let plan = self.user_store.read(cx).plan();
2394 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2395
2396 matches!(
2397 plan,
2398 Some(Plan::V1(PlanV1::ZedFree) | Plan::V2(PlanV2::ZedFree))
2399 ) && has_previous_trial
2400 }
2401
2402 fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
2403 if OnboardingUpsell::dismissed() {
2404 return false;
2405 }
2406
2407 let user_store = self.user_store.read(cx);
2408
2409 if user_store
2410 .plan()
2411 .is_some_and(|plan| matches!(plan, Plan::V1(PlanV1::ZedPro) | Plan::V2(PlanV2::ZedPro)))
2412 && user_store
2413 .subscription_period()
2414 .and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
2415 .is_some_and(|date| date < chrono::Utc::now())
2416 {
2417 OnboardingUpsell::set_dismissed(true, cx);
2418 return false;
2419 }
2420
2421 match &self.active_view {
2422 ActiveView::History | ActiveView::Configuration => false,
2423 ActiveView::ExternalAgentThread { thread_view, .. }
2424 if thread_view.read(cx).as_native_thread(cx).is_none() =>
2425 {
2426 false
2427 }
2428 _ => {
2429 let history_is_empty = self.history_store.read(cx).is_empty(cx);
2430
2431 let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
2432 .visible_providers()
2433 .iter()
2434 .any(|provider| {
2435 provider.is_authenticated(cx)
2436 && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2437 });
2438
2439 history_is_empty || !has_configured_non_zed_providers
2440 }
2441 }
2442 }
2443
2444 fn render_onboarding(
2445 &self,
2446 _window: &mut Window,
2447 cx: &mut Context<Self>,
2448 ) -> Option<impl IntoElement> {
2449 if !self.should_render_onboarding(cx) {
2450 return None;
2451 }
2452
2453 let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
2454
2455 Some(
2456 div()
2457 .when(text_thread_view, |this| {
2458 this.bg(cx.theme().colors().editor_background)
2459 })
2460 .child(self.onboarding.clone()),
2461 )
2462 }
2463
2464 fn render_trial_end_upsell(
2465 &self,
2466 _window: &mut Window,
2467 cx: &mut Context<Self>,
2468 ) -> Option<impl IntoElement> {
2469 if !self.should_render_trial_end_upsell(cx) {
2470 return None;
2471 }
2472
2473 let plan = self.user_store.read(cx).plan()?;
2474
2475 Some(
2476 v_flex()
2477 .absolute()
2478 .inset_0()
2479 .size_full()
2480 .bg(cx.theme().colors().panel_background)
2481 .opacity(0.85)
2482 .block_mouse_except_scroll()
2483 .child(EndTrialUpsell::new(
2484 plan,
2485 Arc::new({
2486 let this = cx.entity();
2487 move |_, cx| {
2488 this.update(cx, |_this, cx| {
2489 TrialEndUpsell::set_dismissed(true, cx);
2490 cx.notify();
2491 });
2492 }
2493 }),
2494 )),
2495 )
2496 }
2497
2498 fn render_configuration_error(
2499 &self,
2500 border_bottom: bool,
2501 configuration_error: &ConfigurationError,
2502 focus_handle: &FocusHandle,
2503 cx: &mut App,
2504 ) -> impl IntoElement {
2505 let zed_provider_configured = AgentSettings::get_global(cx)
2506 .default_model
2507 .as_ref()
2508 .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev");
2509
2510 let callout = if zed_provider_configured {
2511 Callout::new()
2512 .icon(IconName::Warning)
2513 .severity(Severity::Warning)
2514 .when(border_bottom, |this| {
2515 this.border_position(ui::BorderPosition::Bottom)
2516 })
2517 .title("Sign in to continue using Zed as your LLM provider.")
2518 .actions_slot(
2519 Button::new("sign_in", "Sign In")
2520 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2521 .label_size(LabelSize::Small)
2522 .on_click({
2523 let workspace = self.workspace.clone();
2524 move |_, _, cx| {
2525 let Ok(client) =
2526 workspace.update(cx, |workspace, _| workspace.client().clone())
2527 else {
2528 return;
2529 };
2530
2531 cx.spawn(async move |cx| {
2532 client.sign_in_with_optional_connect(true, cx).await
2533 })
2534 .detach_and_log_err(cx);
2535 }
2536 }),
2537 )
2538 } else {
2539 Callout::new()
2540 .icon(IconName::Warning)
2541 .severity(Severity::Warning)
2542 .when(border_bottom, |this| {
2543 this.border_position(ui::BorderPosition::Bottom)
2544 })
2545 .title(configuration_error.to_string())
2546 .actions_slot(
2547 Button::new("settings", "Configure")
2548 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2549 .label_size(LabelSize::Small)
2550 .key_binding(
2551 KeyBinding::for_action_in(&OpenSettings, focus_handle, cx)
2552 .map(|kb| kb.size(rems_from_px(12.))),
2553 )
2554 .on_click(|_event, window, cx| {
2555 window.dispatch_action(OpenSettings.boxed_clone(), cx)
2556 }),
2557 )
2558 };
2559
2560 match configuration_error {
2561 ConfigurationError::ModelNotFound
2562 | ConfigurationError::ProviderNotAuthenticated(_)
2563 | ConfigurationError::NoProvider => callout.into_any_element(),
2564 }
2565 }
2566
2567 #[ztracing::instrument(skip_all)]
2568 fn render_text_thread(
2569 &self,
2570 text_thread_editor: &Entity<TextThreadEditor>,
2571 buffer_search_bar: &Entity<BufferSearchBar>,
2572 window: &mut Window,
2573 cx: &mut Context<Self>,
2574 ) -> Div {
2575 let mut registrar = buffer_search::DivRegistrar::new(
2576 |this, _, _cx| match &this.active_view {
2577 ActiveView::TextThread {
2578 buffer_search_bar, ..
2579 } => Some(buffer_search_bar.clone()),
2580 _ => None,
2581 },
2582 cx,
2583 );
2584 BufferSearchBar::register(&mut registrar);
2585 registrar
2586 .into_div()
2587 .size_full()
2588 .relative()
2589 .map(|parent| {
2590 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2591 if buffer_search_bar.is_dismissed() {
2592 return parent;
2593 }
2594 parent.child(
2595 div()
2596 .p(DynamicSpacing::Base08.rems(cx))
2597 .border_b_1()
2598 .border_color(cx.theme().colors().border_variant)
2599 .bg(cx.theme().colors().editor_background)
2600 .child(buffer_search_bar.render(window, cx)),
2601 )
2602 })
2603 })
2604 .child(text_thread_editor.clone())
2605 .child(self.render_drag_target(cx))
2606 }
2607
2608 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
2609 let is_local = self.project.read(cx).is_local();
2610 div()
2611 .invisible()
2612 .absolute()
2613 .top_0()
2614 .right_0()
2615 .bottom_0()
2616 .left_0()
2617 .bg(cx.theme().colors().drop_target_background)
2618 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
2619 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
2620 .when(is_local, |this| {
2621 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
2622 })
2623 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
2624 let item = tab.pane.read(cx).item_for_index(tab.ix);
2625 let project_paths = item
2626 .and_then(|item| item.project_path(cx))
2627 .into_iter()
2628 .collect::<Vec<_>>();
2629 this.handle_drop(project_paths, vec![], window, cx);
2630 }))
2631 .on_drop(
2632 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2633 let project_paths = selection
2634 .items()
2635 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
2636 .collect::<Vec<_>>();
2637 this.handle_drop(project_paths, vec![], window, cx);
2638 }),
2639 )
2640 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
2641 let tasks = paths
2642 .paths()
2643 .iter()
2644 .map(|path| {
2645 Workspace::project_path_for_path(this.project.clone(), path, false, cx)
2646 })
2647 .collect::<Vec<_>>();
2648 cx.spawn_in(window, async move |this, cx| {
2649 let mut paths = vec![];
2650 let mut added_worktrees = vec![];
2651 let opened_paths = futures::future::join_all(tasks).await;
2652 for entry in opened_paths {
2653 if let Some((worktree, project_path)) = entry.log_err() {
2654 added_worktrees.push(worktree);
2655 paths.push(project_path);
2656 }
2657 }
2658 this.update_in(cx, |this, window, cx| {
2659 this.handle_drop(paths, added_worktrees, window, cx);
2660 })
2661 .ok();
2662 })
2663 .detach();
2664 }))
2665 }
2666
2667 fn handle_drop(
2668 &mut self,
2669 paths: Vec<ProjectPath>,
2670 added_worktrees: Vec<Entity<Worktree>>,
2671 window: &mut Window,
2672 cx: &mut Context<Self>,
2673 ) {
2674 match &self.active_view {
2675 ActiveView::ExternalAgentThread { thread_view } => {
2676 thread_view.update(cx, |thread_view, cx| {
2677 thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
2678 });
2679 }
2680 ActiveView::TextThread {
2681 text_thread_editor, ..
2682 } => {
2683 text_thread_editor.update(cx, |text_thread_editor, cx| {
2684 TextThreadEditor::insert_dragged_files(
2685 text_thread_editor,
2686 paths,
2687 added_worktrees,
2688 window,
2689 cx,
2690 );
2691 });
2692 }
2693 ActiveView::History | ActiveView::Configuration => {}
2694 }
2695 }
2696
2697 fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
2698 if !self.show_trust_workspace_message {
2699 return None;
2700 }
2701
2702 let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
2703
2704 Some(
2705 Callout::new()
2706 .icon(IconName::Warning)
2707 .severity(Severity::Warning)
2708 .border_position(ui::BorderPosition::Bottom)
2709 .title("You're in Restricted Mode")
2710 .description(description)
2711 .actions_slot(
2712 Button::new("open-trust-modal", "Configure Project Trust")
2713 .label_size(LabelSize::Small)
2714 .style(ButtonStyle::Outlined)
2715 .on_click({
2716 cx.listener(move |this, _, window, cx| {
2717 this.workspace
2718 .update(cx, |workspace, cx| {
2719 workspace
2720 .show_worktree_trust_security_modal(true, window, cx)
2721 })
2722 .log_err();
2723 })
2724 }),
2725 ),
2726 )
2727 }
2728
2729 fn key_context(&self) -> KeyContext {
2730 let mut key_context = KeyContext::new_with_defaults();
2731 key_context.add("AgentPanel");
2732 match &self.active_view {
2733 ActiveView::ExternalAgentThread { .. } => key_context.add("acp_thread"),
2734 ActiveView::TextThread { .. } => key_context.add("text_thread"),
2735 ActiveView::History | ActiveView::Configuration => {}
2736 }
2737 key_context
2738 }
2739}
2740
2741impl Render for AgentPanel {
2742 #[ztracing::instrument(skip_all)]
2743 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2744 // WARNING: Changes to this element hierarchy can have
2745 // non-obvious implications to the layout of children.
2746 //
2747 // If you need to change it, please confirm:
2748 // - The message editor expands (cmd-option-esc) correctly
2749 // - When expanded, the buttons at the bottom of the panel are displayed correctly
2750 // - Font size works as expected and can be changed with cmd-+/cmd-
2751 // - Scrolling in all views works as expected
2752 // - Files can be dropped into the panel
2753 let content = v_flex()
2754 .relative()
2755 .size_full()
2756 .justify_between()
2757 .key_context(self.key_context())
2758 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
2759 this.new_thread(action, window, cx);
2760 }))
2761 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
2762 this.open_history(window, cx);
2763 }))
2764 .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
2765 this.open_configuration(window, cx);
2766 }))
2767 .on_action(cx.listener(Self::open_active_thread_as_markdown))
2768 .on_action(cx.listener(Self::deploy_rules_library))
2769 .on_action(cx.listener(Self::go_back))
2770 .on_action(cx.listener(Self::toggle_navigation_menu))
2771 .on_action(cx.listener(Self::toggle_options_menu))
2772 .on_action(cx.listener(Self::increase_font_size))
2773 .on_action(cx.listener(Self::decrease_font_size))
2774 .on_action(cx.listener(Self::reset_font_size))
2775 .on_action(cx.listener(Self::toggle_zoom))
2776 .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
2777 if let Some(thread_view) = this.active_thread_view() {
2778 thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
2779 }
2780 }))
2781 .child(self.render_toolbar(window, cx))
2782 .children(self.render_workspace_trust_message(cx))
2783 .children(self.render_onboarding(window, cx))
2784 .map(|parent| match &self.active_view {
2785 ActiveView::ExternalAgentThread { thread_view, .. } => parent
2786 .child(thread_view.clone())
2787 .child(self.render_drag_target(cx)),
2788 ActiveView::History => parent.child(self.acp_history.clone()),
2789 ActiveView::TextThread {
2790 text_thread_editor,
2791 buffer_search_bar,
2792 ..
2793 } => {
2794 let model_registry = LanguageModelRegistry::read_global(cx);
2795 let configuration_error =
2796 model_registry.configuration_error(model_registry.default_model(), cx);
2797 parent
2798 .map(|this| {
2799 if !self.should_render_onboarding(cx)
2800 && let Some(err) = configuration_error.as_ref()
2801 {
2802 this.child(self.render_configuration_error(
2803 true,
2804 err,
2805 &self.focus_handle(cx),
2806 cx,
2807 ))
2808 } else {
2809 this
2810 }
2811 })
2812 .child(self.render_text_thread(
2813 text_thread_editor,
2814 buffer_search_bar,
2815 window,
2816 cx,
2817 ))
2818 }
2819 ActiveView::Configuration => parent.children(self.configuration.clone()),
2820 })
2821 .children(self.render_trial_end_upsell(window, cx));
2822
2823 match self.active_view.which_font_size_used() {
2824 WhichFontSize::AgentFont => {
2825 WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
2826 .size_full()
2827 .child(content)
2828 .into_any()
2829 }
2830 _ => content.into_any(),
2831 }
2832 }
2833}
2834
2835struct PromptLibraryInlineAssist {
2836 workspace: WeakEntity<Workspace>,
2837}
2838
2839impl PromptLibraryInlineAssist {
2840 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
2841 Self { workspace }
2842 }
2843}
2844
2845impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
2846 fn assist(
2847 &self,
2848 prompt_editor: &Entity<Editor>,
2849 initial_prompt: Option<String>,
2850 window: &mut Window,
2851 cx: &mut Context<RulesLibrary>,
2852 ) {
2853 InlineAssistant::update_global(cx, |assistant, cx| {
2854 let Some(workspace) = self.workspace.upgrade() else {
2855 return;
2856 };
2857 let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
2858 return;
2859 };
2860 let project = workspace.read(cx).project().downgrade();
2861 let thread_store = panel.read(cx).thread_store().clone();
2862 assistant.assist(
2863 prompt_editor,
2864 self.workspace.clone(),
2865 project,
2866 thread_store,
2867 None,
2868 initial_prompt,
2869 window,
2870 cx,
2871 );
2872 })
2873 }
2874
2875 fn focus_agent_panel(
2876 &self,
2877 workspace: &mut Workspace,
2878 window: &mut Window,
2879 cx: &mut Context<Workspace>,
2880 ) -> bool {
2881 workspace.focus_panel::<AgentPanel>(window, cx).is_some()
2882 }
2883}
2884
2885pub struct ConcreteAssistantPanelDelegate;
2886
2887impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
2888 fn active_text_thread_editor(
2889 &self,
2890 workspace: &mut Workspace,
2891 _window: &mut Window,
2892 cx: &mut Context<Workspace>,
2893 ) -> Option<Entity<TextThreadEditor>> {
2894 let panel = workspace.panel::<AgentPanel>(cx)?;
2895 panel.read(cx).active_text_thread_editor()
2896 }
2897
2898 fn open_local_text_thread(
2899 &self,
2900 workspace: &mut Workspace,
2901 path: Arc<Path>,
2902 window: &mut Window,
2903 cx: &mut Context<Workspace>,
2904 ) -> Task<Result<()>> {
2905 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
2906 return Task::ready(Err(anyhow!("Agent panel not found")));
2907 };
2908
2909 panel.update(cx, |panel, cx| {
2910 panel.open_saved_text_thread(path, window, cx)
2911 })
2912 }
2913
2914 fn open_remote_text_thread(
2915 &self,
2916 _workspace: &mut Workspace,
2917 _text_thread_id: assistant_text_thread::TextThreadId,
2918 _window: &mut Window,
2919 _cx: &mut Context<Workspace>,
2920 ) -> Task<Result<Entity<TextThreadEditor>>> {
2921 Task::ready(Err(anyhow!("opening remote context not implemented")))
2922 }
2923
2924 fn quote_selection(
2925 &self,
2926 workspace: &mut Workspace,
2927 selection_ranges: Vec<Range<Anchor>>,
2928 buffer: Entity<MultiBuffer>,
2929 window: &mut Window,
2930 cx: &mut Context<Workspace>,
2931 ) {
2932 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
2933 return;
2934 };
2935
2936 if !panel.focus_handle(cx).contains_focused(window, cx) {
2937 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
2938 }
2939
2940 panel.update(cx, |_, cx| {
2941 // Wait to create a new context until the workspace is no longer
2942 // being updated.
2943 cx.defer_in(window, move |panel, window, cx| {
2944 if let Some(thread_view) = panel.active_thread_view() {
2945 thread_view.update(cx, |thread_view, cx| {
2946 thread_view.insert_selections(window, cx);
2947 });
2948 } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
2949 let snapshot = buffer.read(cx).snapshot(cx);
2950 let selection_ranges = selection_ranges
2951 .into_iter()
2952 .map(|range| range.to_point(&snapshot))
2953 .collect::<Vec<_>>();
2954
2955 text_thread_editor.update(cx, |text_thread_editor, cx| {
2956 text_thread_editor.quote_ranges(selection_ranges, snapshot, window, cx)
2957 });
2958 }
2959 });
2960 });
2961 }
2962}
2963
2964struct OnboardingUpsell;
2965
2966impl Dismissable for OnboardingUpsell {
2967 const KEY: &'static str = "dismissed-trial-upsell";
2968}
2969
2970struct TrialEndUpsell;
2971
2972impl Dismissable for TrialEndUpsell {
2973 const KEY: &'static str = "dismissed-trial-end-upsell";
2974}