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