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