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::AcpServerView,
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 AgentThread {
247 thread_view: Entity<AcpServerView>,
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::AgentThread { .. }
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<AcpServerView>> {
735 match &self.active_view {
736 ActiveView::AgentThread { 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::AgentThread { 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::AgentThread { 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::AgentThread { 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::AgentThread { 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::AcpServerView::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(ActiveView::AgentThread { thread_view }, true, window, cx);
1720 }
1721}
1722
1723impl Focusable for AgentPanel {
1724 fn focus_handle(&self, cx: &App) -> FocusHandle {
1725 match &self.active_view {
1726 ActiveView::Uninitialized => self.focus_handle.clone(),
1727 ActiveView::AgentThread { thread_view, .. } => thread_view.focus_handle(cx),
1728 ActiveView::History { kind } => match kind {
1729 HistoryKind::AgentThreads => self.acp_history.focus_handle(cx),
1730 HistoryKind::TextThreads => self.text_thread_history.focus_handle(cx),
1731 },
1732 ActiveView::TextThread {
1733 text_thread_editor, ..
1734 } => text_thread_editor.focus_handle(cx),
1735 ActiveView::Configuration => {
1736 if let Some(configuration) = self.configuration.as_ref() {
1737 configuration.focus_handle(cx)
1738 } else {
1739 self.focus_handle.clone()
1740 }
1741 }
1742 }
1743 }
1744}
1745
1746fn agent_panel_dock_position(cx: &App) -> DockPosition {
1747 AgentSettings::get_global(cx).dock.into()
1748}
1749
1750impl EventEmitter<PanelEvent> for AgentPanel {}
1751
1752impl Panel for AgentPanel {
1753 fn persistent_name() -> &'static str {
1754 "AgentPanel"
1755 }
1756
1757 fn panel_key() -> &'static str {
1758 AGENT_PANEL_KEY
1759 }
1760
1761 fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1762 agent_panel_dock_position(cx)
1763 }
1764
1765 fn position_is_valid(&self, position: DockPosition) -> bool {
1766 position != DockPosition::Bottom
1767 }
1768
1769 fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1770 settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1771 settings
1772 .agent
1773 .get_or_insert_default()
1774 .set_dock(position.into());
1775 });
1776 }
1777
1778 fn size(&self, window: &Window, cx: &App) -> Pixels {
1779 let settings = AgentSettings::get_global(cx);
1780 match self.position(window, cx) {
1781 DockPosition::Left | DockPosition::Right => {
1782 self.width.unwrap_or(settings.default_width)
1783 }
1784 DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1785 }
1786 }
1787
1788 fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1789 match self.position(window, cx) {
1790 DockPosition::Left | DockPosition::Right => self.width = size,
1791 DockPosition::Bottom => self.height = size,
1792 }
1793 self.serialize(cx);
1794 cx.notify();
1795 }
1796
1797 fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1798 if active && matches!(self.active_view, ActiveView::Uninitialized) {
1799 let selected_agent = self.selected_agent.clone();
1800 self.new_agent_thread(selected_agent, window, cx);
1801 }
1802 }
1803
1804 fn remote_id() -> Option<proto::PanelId> {
1805 Some(proto::PanelId::AssistantPanel)
1806 }
1807
1808 fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1809 (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
1810 }
1811
1812 fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1813 Some("Agent Panel")
1814 }
1815
1816 fn toggle_action(&self) -> Box<dyn Action> {
1817 Box::new(ToggleFocus)
1818 }
1819
1820 fn activation_priority(&self) -> u32 {
1821 3
1822 }
1823
1824 fn enabled(&self, cx: &App) -> bool {
1825 AgentSettings::get_global(cx).enabled(cx)
1826 }
1827
1828 fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1829 self.zoomed
1830 }
1831
1832 fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1833 self.zoomed = zoomed;
1834 cx.notify();
1835 }
1836}
1837
1838impl AgentPanel {
1839 fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1840 const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1841
1842 let content = match &self.active_view {
1843 ActiveView::AgentThread { thread_view } => {
1844 let is_generating_title = thread_view
1845 .read(cx)
1846 .as_native_thread(cx)
1847 .map_or(false, |t| t.read(cx).is_generating_title());
1848
1849 if let Some(title_editor) = thread_view
1850 .read(cx)
1851 .as_active_thread()
1852 .and_then(|ready| ready.title_editor.clone())
1853 {
1854 let container = div()
1855 .w_full()
1856 .on_action({
1857 let thread_view = thread_view.downgrade();
1858 move |_: &menu::Confirm, window, cx| {
1859 if let Some(thread_view) = thread_view.upgrade() {
1860 thread_view.focus_handle(cx).focus(window, cx);
1861 }
1862 }
1863 })
1864 .on_action({
1865 let thread_view = thread_view.downgrade();
1866 move |_: &editor::actions::Cancel, window, cx| {
1867 if let Some(thread_view) = thread_view.upgrade() {
1868 thread_view.focus_handle(cx).focus(window, cx);
1869 }
1870 }
1871 })
1872 .child(title_editor);
1873
1874 if is_generating_title {
1875 container
1876 .with_animation(
1877 "generating_title",
1878 Animation::new(Duration::from_secs(2))
1879 .repeat()
1880 .with_easing(pulsating_between(0.4, 0.8)),
1881 |div, delta| div.opacity(delta),
1882 )
1883 .into_any_element()
1884 } else {
1885 container.into_any_element()
1886 }
1887 } else {
1888 Label::new(thread_view.read(cx).title(cx))
1889 .color(Color::Muted)
1890 .truncate()
1891 .into_any_element()
1892 }
1893 }
1894 ActiveView::TextThread {
1895 title_editor,
1896 text_thread_editor,
1897 ..
1898 } => {
1899 let summary = text_thread_editor.read(cx).text_thread().read(cx).summary();
1900
1901 match summary {
1902 TextThreadSummary::Pending => Label::new(TextThreadSummary::DEFAULT)
1903 .color(Color::Muted)
1904 .truncate()
1905 .into_any_element(),
1906 TextThreadSummary::Content(summary) => {
1907 if summary.done {
1908 div()
1909 .w_full()
1910 .child(title_editor.clone())
1911 .into_any_element()
1912 } else {
1913 Label::new(LOADING_SUMMARY_PLACEHOLDER)
1914 .truncate()
1915 .color(Color::Muted)
1916 .with_animation(
1917 "generating_title",
1918 Animation::new(Duration::from_secs(2))
1919 .repeat()
1920 .with_easing(pulsating_between(0.4, 0.8)),
1921 |label, delta| label.alpha(delta),
1922 )
1923 .into_any_element()
1924 }
1925 }
1926 TextThreadSummary::Error => h_flex()
1927 .w_full()
1928 .child(title_editor.clone())
1929 .child(
1930 IconButton::new("retry-summary-generation", IconName::RotateCcw)
1931 .icon_size(IconSize::Small)
1932 .on_click({
1933 let text_thread_editor = text_thread_editor.clone();
1934 move |_, _window, cx| {
1935 text_thread_editor.update(cx, |text_thread_editor, cx| {
1936 text_thread_editor.regenerate_summary(cx);
1937 });
1938 }
1939 })
1940 .tooltip(move |_window, cx| {
1941 cx.new(|_| {
1942 Tooltip::new("Failed to generate title")
1943 .meta("Click to try again")
1944 })
1945 .into()
1946 }),
1947 )
1948 .into_any_element(),
1949 }
1950 }
1951 ActiveView::History { kind } => {
1952 let title = match kind {
1953 HistoryKind::AgentThreads => "History",
1954 HistoryKind::TextThreads => "Text Thread History",
1955 };
1956 Label::new(title).truncate().into_any_element()
1957 }
1958 ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1959 ActiveView::Uninitialized => Label::new("Agent").truncate().into_any_element(),
1960 };
1961
1962 h_flex()
1963 .key_context("TitleEditor")
1964 .id("TitleEditor")
1965 .flex_grow()
1966 .w_full()
1967 .max_w_full()
1968 .overflow_x_scroll()
1969 .child(content)
1970 .into_any()
1971 }
1972
1973 fn handle_regenerate_thread_title(thread_view: Entity<AcpServerView>, cx: &mut App) {
1974 thread_view.update(cx, |thread_view, cx| {
1975 if let Some(thread) = thread_view.as_native_thread(cx) {
1976 thread.update(cx, |thread, cx| {
1977 thread.generate_title(cx);
1978 });
1979 }
1980 });
1981 }
1982
1983 fn handle_regenerate_text_thread_title(
1984 text_thread_editor: Entity<TextThreadEditor>,
1985 cx: &mut App,
1986 ) {
1987 text_thread_editor.update(cx, |text_thread_editor, cx| {
1988 text_thread_editor.regenerate_summary(cx);
1989 });
1990 }
1991
1992 fn render_panel_options_menu(
1993 &self,
1994 window: &mut Window,
1995 cx: &mut Context<Self>,
1996 ) -> impl IntoElement {
1997 let focus_handle = self.focus_handle(cx);
1998
1999 let full_screen_label = if self.is_zoomed(window, cx) {
2000 "Disable Full Screen"
2001 } else {
2002 "Enable Full Screen"
2003 };
2004
2005 let selected_agent = self.selected_agent.clone();
2006
2007 let text_thread_view = match &self.active_view {
2008 ActiveView::TextThread {
2009 text_thread_editor, ..
2010 } => Some(text_thread_editor.clone()),
2011 _ => None,
2012 };
2013 let text_thread_with_messages = match &self.active_view {
2014 ActiveView::TextThread {
2015 text_thread_editor, ..
2016 } => text_thread_editor
2017 .read(cx)
2018 .text_thread()
2019 .read(cx)
2020 .messages(cx)
2021 .any(|message| message.role == language_model::Role::Assistant),
2022 _ => false,
2023 };
2024
2025 let thread_view = match &self.active_view {
2026 ActiveView::AgentThread { thread_view } => Some(thread_view.clone()),
2027 _ => None,
2028 };
2029 let thread_with_messages = match &self.active_view {
2030 ActiveView::AgentThread { thread_view } => {
2031 thread_view.read(cx).has_user_submitted_prompt(cx)
2032 }
2033 _ => false,
2034 };
2035
2036 PopoverMenu::new("agent-options-menu")
2037 .trigger_with_tooltip(
2038 IconButton::new("agent-options-menu", IconName::Ellipsis)
2039 .icon_size(IconSize::Small),
2040 {
2041 let focus_handle = focus_handle.clone();
2042 move |_window, cx| {
2043 Tooltip::for_action_in(
2044 "Toggle Agent Menu",
2045 &ToggleOptionsMenu,
2046 &focus_handle,
2047 cx,
2048 )
2049 }
2050 },
2051 )
2052 .anchor(Corner::TopRight)
2053 .with_handle(self.agent_panel_menu_handle.clone())
2054 .menu({
2055 move |window, cx| {
2056 Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
2057 menu = menu.context(focus_handle.clone());
2058
2059 if thread_with_messages | text_thread_with_messages {
2060 menu = menu.header("Current Thread");
2061
2062 if let Some(text_thread_view) = text_thread_view.as_ref() {
2063 menu = menu
2064 .entry("Regenerate Thread Title", None, {
2065 let text_thread_view = text_thread_view.clone();
2066 move |_, cx| {
2067 Self::handle_regenerate_text_thread_title(
2068 text_thread_view.clone(),
2069 cx,
2070 );
2071 }
2072 })
2073 .separator();
2074 }
2075
2076 if let Some(thread_view) = thread_view.as_ref() {
2077 menu = menu
2078 .entry("Regenerate Thread Title", None, {
2079 let thread_view = thread_view.clone();
2080 move |_, cx| {
2081 Self::handle_regenerate_thread_title(
2082 thread_view.clone(),
2083 cx,
2084 );
2085 }
2086 })
2087 .separator();
2088 }
2089 }
2090
2091 menu = menu
2092 .header("MCP Servers")
2093 .action(
2094 "View Server Extensions",
2095 Box::new(zed_actions::Extensions {
2096 category_filter: Some(
2097 zed_actions::ExtensionCategoryFilter::ContextServers,
2098 ),
2099 id: None,
2100 }),
2101 )
2102 .action("Add Custom Server…", Box::new(AddContextServer))
2103 .separator()
2104 .action("Rules", Box::new(OpenRulesLibrary::default()))
2105 .action("Profiles", Box::new(ManageProfiles::default()))
2106 .action("Settings", Box::new(OpenSettings))
2107 .separator()
2108 .action(full_screen_label, Box::new(ToggleZoom));
2109
2110 if selected_agent == AgentType::Gemini {
2111 menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
2112 }
2113
2114 menu
2115 }))
2116 }
2117 })
2118 }
2119
2120 fn render_recent_entries_menu(
2121 &self,
2122 icon: IconName,
2123 corner: Corner,
2124 cx: &mut Context<Self>,
2125 ) -> impl IntoElement {
2126 let focus_handle = self.focus_handle(cx);
2127
2128 PopoverMenu::new("agent-nav-menu")
2129 .trigger_with_tooltip(
2130 IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
2131 {
2132 move |_window, cx| {
2133 Tooltip::for_action_in(
2134 "Toggle Recently Updated Threads",
2135 &ToggleNavigationMenu,
2136 &focus_handle,
2137 cx,
2138 )
2139 }
2140 },
2141 )
2142 .anchor(corner)
2143 .with_handle(self.agent_navigation_menu_handle.clone())
2144 .menu({
2145 let menu = self.agent_navigation_menu.clone();
2146 move |window, cx| {
2147 telemetry::event!("View Thread History Clicked");
2148
2149 if let Some(menu) = menu.as_ref() {
2150 menu.update(cx, |_, cx| {
2151 cx.defer_in(window, |menu, window, cx| {
2152 menu.rebuild(window, cx);
2153 });
2154 })
2155 }
2156 menu.clone()
2157 }
2158 })
2159 }
2160
2161 fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
2162 let focus_handle = self.focus_handle(cx);
2163
2164 IconButton::new("go-back", IconName::ArrowLeft)
2165 .icon_size(IconSize::Small)
2166 .on_click(cx.listener(|this, _, window, cx| {
2167 this.go_back(&workspace::GoBack, window, cx);
2168 }))
2169 .tooltip({
2170 move |_window, cx| {
2171 Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
2172 }
2173 })
2174 }
2175
2176 fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2177 let agent_server_store = self.project.read(cx).agent_server_store().clone();
2178 let focus_handle = self.focus_handle(cx);
2179
2180 let (selected_agent_custom_icon, selected_agent_label) =
2181 if let AgentType::Custom { name, .. } = &self.selected_agent {
2182 let store = agent_server_store.read(cx);
2183 let icon = store.agent_icon(&ExternalAgentServerName(name.clone()));
2184
2185 let label = store
2186 .agent_display_name(&ExternalAgentServerName(name.clone()))
2187 .unwrap_or_else(|| self.selected_agent.label());
2188 (icon, label)
2189 } else {
2190 (None, self.selected_agent.label())
2191 };
2192
2193 let active_thread = match &self.active_view {
2194 ActiveView::AgentThread { thread_view } => thread_view.read(cx).as_native_thread(cx),
2195 ActiveView::Uninitialized
2196 | ActiveView::TextThread { .. }
2197 | ActiveView::History { .. }
2198 | ActiveView::Configuration => None,
2199 };
2200
2201 let new_thread_menu = PopoverMenu::new("new_thread_menu")
2202 .trigger_with_tooltip(
2203 IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2204 {
2205 let focus_handle = focus_handle.clone();
2206 move |_window, cx| {
2207 Tooltip::for_action_in(
2208 "New Thread…",
2209 &ToggleNewThreadMenu,
2210 &focus_handle,
2211 cx,
2212 )
2213 }
2214 },
2215 )
2216 .anchor(Corner::TopRight)
2217 .with_handle(self.new_thread_menu_handle.clone())
2218 .menu({
2219 let selected_agent = self.selected_agent.clone();
2220 let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
2221
2222 let workspace = self.workspace.clone();
2223 let is_via_collab = workspace
2224 .update(cx, |workspace, cx| {
2225 workspace.project().read(cx).is_via_collab()
2226 })
2227 .unwrap_or_default();
2228
2229 move |window, cx| {
2230 telemetry::event!("New Thread Clicked");
2231
2232 let active_thread = active_thread.clone();
2233 Some(ContextMenu::build(window, cx, |menu, _window, cx| {
2234 menu.context(focus_handle.clone())
2235 .when_some(active_thread, |this, active_thread| {
2236 let thread = active_thread.read(cx);
2237
2238 if !thread.is_empty() {
2239 let session_id = thread.id().clone();
2240 this.item(
2241 ContextMenuEntry::new("New From Summary")
2242 .icon(IconName::ThreadFromSummary)
2243 .icon_color(Color::Muted)
2244 .handler(move |window, cx| {
2245 window.dispatch_action(
2246 Box::new(NewNativeAgentThreadFromSummary {
2247 from_session_id: session_id.clone(),
2248 }),
2249 cx,
2250 );
2251 }),
2252 )
2253 } else {
2254 this
2255 }
2256 })
2257 .item(
2258 ContextMenuEntry::new("Zed Agent")
2259 .when(
2260 is_agent_selected(AgentType::NativeAgent)
2261 | is_agent_selected(AgentType::TextThread),
2262 |this| {
2263 this.action(Box::new(NewExternalAgentThread {
2264 agent: None,
2265 }))
2266 },
2267 )
2268 .icon(IconName::ZedAgent)
2269 .icon_color(Color::Muted)
2270 .handler({
2271 let workspace = workspace.clone();
2272 move |window, cx| {
2273 if let Some(workspace) = workspace.upgrade() {
2274 workspace.update(cx, |workspace, cx| {
2275 if let Some(panel) =
2276 workspace.panel::<AgentPanel>(cx)
2277 {
2278 panel.update(cx, |panel, cx| {
2279 panel.new_agent_thread(
2280 AgentType::NativeAgent,
2281 window,
2282 cx,
2283 );
2284 });
2285 }
2286 });
2287 }
2288 }
2289 }),
2290 )
2291 .item(
2292 ContextMenuEntry::new("Text Thread")
2293 .action(NewTextThread.boxed_clone())
2294 .icon(IconName::TextThread)
2295 .icon_color(Color::Muted)
2296 .handler({
2297 let workspace = workspace.clone();
2298 move |window, cx| {
2299 if let Some(workspace) = workspace.upgrade() {
2300 workspace.update(cx, |workspace, cx| {
2301 if let Some(panel) =
2302 workspace.panel::<AgentPanel>(cx)
2303 {
2304 panel.update(cx, |panel, cx| {
2305 panel.new_agent_thread(
2306 AgentType::TextThread,
2307 window,
2308 cx,
2309 );
2310 });
2311 }
2312 });
2313 }
2314 }
2315 }),
2316 )
2317 .separator()
2318 .header("External Agents")
2319 .item(
2320 ContextMenuEntry::new("Claude Code")
2321 .when(is_agent_selected(AgentType::ClaudeCode), |this| {
2322 this.action(Box::new(NewExternalAgentThread {
2323 agent: None,
2324 }))
2325 })
2326 .icon(IconName::AiClaude)
2327 .disabled(is_via_collab)
2328 .icon_color(Color::Muted)
2329 .handler({
2330 let workspace = workspace.clone();
2331 move |window, cx| {
2332 if let Some(workspace) = workspace.upgrade() {
2333 workspace.update(cx, |workspace, cx| {
2334 if let Some(panel) =
2335 workspace.panel::<AgentPanel>(cx)
2336 {
2337 panel.update(cx, |panel, cx| {
2338 panel.new_agent_thread(
2339 AgentType::ClaudeCode,
2340 window,
2341 cx,
2342 );
2343 });
2344 }
2345 });
2346 }
2347 }
2348 }),
2349 )
2350 .item(
2351 ContextMenuEntry::new("Codex CLI")
2352 .when(is_agent_selected(AgentType::Codex), |this| {
2353 this.action(Box::new(NewExternalAgentThread {
2354 agent: None,
2355 }))
2356 })
2357 .icon(IconName::AiOpenAi)
2358 .disabled(is_via_collab)
2359 .icon_color(Color::Muted)
2360 .handler({
2361 let workspace = workspace.clone();
2362 move |window, cx| {
2363 if let Some(workspace) = workspace.upgrade() {
2364 workspace.update(cx, |workspace, cx| {
2365 if let Some(panel) =
2366 workspace.panel::<AgentPanel>(cx)
2367 {
2368 panel.update(cx, |panel, cx| {
2369 panel.new_agent_thread(
2370 AgentType::Codex,
2371 window,
2372 cx,
2373 );
2374 });
2375 }
2376 });
2377 }
2378 }
2379 }),
2380 )
2381 .item(
2382 ContextMenuEntry::new("Gemini CLI")
2383 .when(is_agent_selected(AgentType::Gemini), |this| {
2384 this.action(Box::new(NewExternalAgentThread {
2385 agent: None,
2386 }))
2387 })
2388 .icon(IconName::AiGemini)
2389 .icon_color(Color::Muted)
2390 .disabled(is_via_collab)
2391 .handler({
2392 let workspace = workspace.clone();
2393 move |window, cx| {
2394 if let Some(workspace) = workspace.upgrade() {
2395 workspace.update(cx, |workspace, cx| {
2396 if let Some(panel) =
2397 workspace.panel::<AgentPanel>(cx)
2398 {
2399 panel.update(cx, |panel, cx| {
2400 panel.new_agent_thread(
2401 AgentType::Gemini,
2402 window,
2403 cx,
2404 );
2405 });
2406 }
2407 });
2408 }
2409 }
2410 }),
2411 )
2412 .map(|mut menu| {
2413 let agent_server_store = agent_server_store.read(cx);
2414 let agent_names = agent_server_store
2415 .external_agents()
2416 .filter(|name| {
2417 name.0 != GEMINI_NAME
2418 && name.0 != CLAUDE_CODE_NAME
2419 && name.0 != CODEX_NAME
2420 })
2421 .cloned()
2422 .collect::<Vec<_>>();
2423
2424 for agent_name in agent_names {
2425 let icon_path = agent_server_store.agent_icon(&agent_name);
2426 let display_name = agent_server_store
2427 .agent_display_name(&agent_name)
2428 .unwrap_or_else(|| agent_name.0.clone());
2429
2430 let mut entry = ContextMenuEntry::new(display_name);
2431
2432 if let Some(icon_path) = icon_path {
2433 entry = entry.custom_icon_svg(icon_path);
2434 } else {
2435 entry = entry.icon(IconName::Sparkle);
2436 }
2437 entry = entry
2438 .when(
2439 is_agent_selected(AgentType::Custom {
2440 name: agent_name.0.clone(),
2441 }),
2442 |this| {
2443 this.action(Box::new(NewExternalAgentThread {
2444 agent: None,
2445 }))
2446 },
2447 )
2448 .icon_color(Color::Muted)
2449 .disabled(is_via_collab)
2450 .handler({
2451 let workspace = workspace.clone();
2452 let agent_name = agent_name.clone();
2453 move |window, cx| {
2454 if let Some(workspace) = workspace.upgrade() {
2455 workspace.update(cx, |workspace, cx| {
2456 if let Some(panel) =
2457 workspace.panel::<AgentPanel>(cx)
2458 {
2459 panel.update(cx, |panel, cx| {
2460 panel.new_agent_thread(
2461 AgentType::Custom {
2462 name: agent_name
2463 .clone()
2464 .into(),
2465 },
2466 window,
2467 cx,
2468 );
2469 });
2470 }
2471 });
2472 }
2473 }
2474 });
2475
2476 menu = menu.item(entry);
2477 }
2478
2479 menu
2480 })
2481 .separator()
2482 .item(
2483 ContextMenuEntry::new("Add More Agents")
2484 .icon(IconName::Plus)
2485 .icon_color(Color::Muted)
2486 .handler({
2487 move |window, cx| {
2488 window.dispatch_action(
2489 Box::new(zed_actions::AcpRegistry),
2490 cx,
2491 )
2492 }
2493 }),
2494 )
2495 }))
2496 }
2497 });
2498
2499 let is_thread_loading = self
2500 .active_thread_view()
2501 .map(|thread| thread.read(cx).is_loading())
2502 .unwrap_or(false);
2503
2504 let has_custom_icon = selected_agent_custom_icon.is_some();
2505
2506 let selected_agent = div()
2507 .id("selected_agent_icon")
2508 .when_some(selected_agent_custom_icon, |this, icon_path| {
2509 this.px_1()
2510 .child(Icon::from_external_svg(icon_path).color(Color::Muted))
2511 })
2512 .when(!has_custom_icon, |this| {
2513 this.when_some(self.selected_agent.icon(), |this, icon| {
2514 this.px_1().child(Icon::new(icon).color(Color::Muted))
2515 })
2516 })
2517 .tooltip(move |_, cx| {
2518 Tooltip::with_meta(selected_agent_label.clone(), None, "Selected Agent", cx)
2519 });
2520
2521 let selected_agent = if is_thread_loading {
2522 selected_agent
2523 .with_animation(
2524 "pulsating-icon",
2525 Animation::new(Duration::from_secs(1))
2526 .repeat()
2527 .with_easing(pulsating_between(0.2, 0.6)),
2528 |icon, delta| icon.opacity(delta),
2529 )
2530 .into_any_element()
2531 } else {
2532 selected_agent.into_any_element()
2533 };
2534
2535 let show_history_menu = self.history_kind_for_selected_agent(cx).is_some();
2536
2537 h_flex()
2538 .id("agent-panel-toolbar")
2539 .h(Tab::container_height(cx))
2540 .max_w_full()
2541 .flex_none()
2542 .justify_between()
2543 .gap_2()
2544 .bg(cx.theme().colors().tab_bar_background)
2545 .border_b_1()
2546 .border_color(cx.theme().colors().border)
2547 .child(
2548 h_flex()
2549 .size_full()
2550 .gap(DynamicSpacing::Base04.rems(cx))
2551 .pl(DynamicSpacing::Base04.rems(cx))
2552 .child(match &self.active_view {
2553 ActiveView::History { .. } | ActiveView::Configuration => {
2554 self.render_toolbar_back_button(cx).into_any_element()
2555 }
2556 _ => selected_agent.into_any_element(),
2557 })
2558 .child(self.render_title_view(window, cx)),
2559 )
2560 .child(
2561 h_flex()
2562 .flex_none()
2563 .gap(DynamicSpacing::Base02.rems(cx))
2564 .pl(DynamicSpacing::Base04.rems(cx))
2565 .pr(DynamicSpacing::Base06.rems(cx))
2566 .child(new_thread_menu)
2567 .when(show_history_menu, |this| {
2568 this.child(self.render_recent_entries_menu(
2569 IconName::MenuAltTemp,
2570 Corner::TopRight,
2571 cx,
2572 ))
2573 })
2574 .child(self.render_panel_options_menu(window, cx)),
2575 )
2576 }
2577
2578 fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2579 if TrialEndUpsell::dismissed() {
2580 return false;
2581 }
2582
2583 match &self.active_view {
2584 ActiveView::TextThread { .. } => {
2585 if LanguageModelRegistry::global(cx)
2586 .read(cx)
2587 .default_model()
2588 .is_some_and(|model| {
2589 model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2590 })
2591 {
2592 return false;
2593 }
2594 }
2595 ActiveView::Uninitialized
2596 | ActiveView::AgentThread { .. }
2597 | ActiveView::History { .. }
2598 | ActiveView::Configuration => return false,
2599 }
2600
2601 let plan = self.user_store.read(cx).plan();
2602 let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2603
2604 plan.is_some_and(|plan| plan == Plan::ZedFree) && has_previous_trial
2605 }
2606
2607 fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
2608 if OnboardingUpsell::dismissed() {
2609 return false;
2610 }
2611
2612 let user_store = self.user_store.read(cx);
2613
2614 if user_store.plan().is_some_and(|plan| plan == Plan::ZedPro)
2615 && user_store
2616 .subscription_period()
2617 .and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
2618 .is_some_and(|date| date < chrono::Utc::now())
2619 {
2620 OnboardingUpsell::set_dismissed(true, cx);
2621 return false;
2622 }
2623
2624 match &self.active_view {
2625 ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
2626 false
2627 }
2628 ActiveView::AgentThread { thread_view, .. }
2629 if thread_view.read(cx).as_native_thread(cx).is_none() =>
2630 {
2631 false
2632 }
2633 _ => {
2634 let history_is_empty = self.acp_history.read(cx).is_empty();
2635
2636 let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
2637 .visible_providers()
2638 .iter()
2639 .any(|provider| {
2640 provider.is_authenticated(cx)
2641 && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2642 });
2643
2644 history_is_empty || !has_configured_non_zed_providers
2645 }
2646 }
2647 }
2648
2649 fn render_onboarding(
2650 &self,
2651 _window: &mut Window,
2652 cx: &mut Context<Self>,
2653 ) -> Option<impl IntoElement> {
2654 if !self.should_render_onboarding(cx) {
2655 return None;
2656 }
2657
2658 let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
2659
2660 Some(
2661 div()
2662 .when(text_thread_view, |this| {
2663 this.bg(cx.theme().colors().editor_background)
2664 })
2665 .child(self.onboarding.clone()),
2666 )
2667 }
2668
2669 fn render_trial_end_upsell(
2670 &self,
2671 _window: &mut Window,
2672 cx: &mut Context<Self>,
2673 ) -> Option<impl IntoElement> {
2674 if !self.should_render_trial_end_upsell(cx) {
2675 return None;
2676 }
2677
2678 Some(
2679 v_flex()
2680 .absolute()
2681 .inset_0()
2682 .size_full()
2683 .bg(cx.theme().colors().panel_background)
2684 .opacity(0.85)
2685 .block_mouse_except_scroll()
2686 .child(EndTrialUpsell::new(Arc::new({
2687 let this = cx.entity();
2688 move |_, cx| {
2689 this.update(cx, |_this, cx| {
2690 TrialEndUpsell::set_dismissed(true, cx);
2691 cx.notify();
2692 });
2693 }
2694 }))),
2695 )
2696 }
2697
2698 fn render_configuration_error(
2699 &self,
2700 border_bottom: bool,
2701 configuration_error: &ConfigurationError,
2702 focus_handle: &FocusHandle,
2703 cx: &mut App,
2704 ) -> impl IntoElement {
2705 let zed_provider_configured = AgentSettings::get_global(cx)
2706 .default_model
2707 .as_ref()
2708 .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev");
2709
2710 let callout = if zed_provider_configured {
2711 Callout::new()
2712 .icon(IconName::Warning)
2713 .severity(Severity::Warning)
2714 .when(border_bottom, |this| {
2715 this.border_position(ui::BorderPosition::Bottom)
2716 })
2717 .title("Sign in to continue using Zed as your LLM provider.")
2718 .actions_slot(
2719 Button::new("sign_in", "Sign In")
2720 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2721 .label_size(LabelSize::Small)
2722 .on_click({
2723 let workspace = self.workspace.clone();
2724 move |_, _, cx| {
2725 let Ok(client) =
2726 workspace.update(cx, |workspace, _| workspace.client().clone())
2727 else {
2728 return;
2729 };
2730
2731 cx.spawn(async move |cx| {
2732 client.sign_in_with_optional_connect(true, cx).await
2733 })
2734 .detach_and_log_err(cx);
2735 }
2736 }),
2737 )
2738 } else {
2739 Callout::new()
2740 .icon(IconName::Warning)
2741 .severity(Severity::Warning)
2742 .when(border_bottom, |this| {
2743 this.border_position(ui::BorderPosition::Bottom)
2744 })
2745 .title(configuration_error.to_string())
2746 .actions_slot(
2747 Button::new("settings", "Configure")
2748 .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2749 .label_size(LabelSize::Small)
2750 .key_binding(
2751 KeyBinding::for_action_in(&OpenSettings, focus_handle, cx)
2752 .map(|kb| kb.size(rems_from_px(12.))),
2753 )
2754 .on_click(|_event, window, cx| {
2755 window.dispatch_action(OpenSettings.boxed_clone(), cx)
2756 }),
2757 )
2758 };
2759
2760 match configuration_error {
2761 ConfigurationError::ModelNotFound
2762 | ConfigurationError::ProviderNotAuthenticated(_)
2763 | ConfigurationError::NoProvider => callout.into_any_element(),
2764 }
2765 }
2766
2767 fn render_text_thread(
2768 &self,
2769 text_thread_editor: &Entity<TextThreadEditor>,
2770 buffer_search_bar: &Entity<BufferSearchBar>,
2771 window: &mut Window,
2772 cx: &mut Context<Self>,
2773 ) -> Div {
2774 let mut registrar = buffer_search::DivRegistrar::new(
2775 |this, _, _cx| match &this.active_view {
2776 ActiveView::TextThread {
2777 buffer_search_bar, ..
2778 } => Some(buffer_search_bar.clone()),
2779 _ => None,
2780 },
2781 cx,
2782 );
2783 BufferSearchBar::register(&mut registrar);
2784 registrar
2785 .into_div()
2786 .size_full()
2787 .relative()
2788 .map(|parent| {
2789 buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2790 if buffer_search_bar.is_dismissed() {
2791 return parent;
2792 }
2793 parent.child(
2794 div()
2795 .p(DynamicSpacing::Base08.rems(cx))
2796 .border_b_1()
2797 .border_color(cx.theme().colors().border_variant)
2798 .bg(cx.theme().colors().editor_background)
2799 .child(buffer_search_bar.render(window, cx)),
2800 )
2801 })
2802 })
2803 .child(text_thread_editor.clone())
2804 .child(self.render_drag_target(cx))
2805 }
2806
2807 fn render_drag_target(&self, cx: &Context<Self>) -> Div {
2808 let is_local = self.project.read(cx).is_local();
2809 div()
2810 .invisible()
2811 .absolute()
2812 .top_0()
2813 .right_0()
2814 .bottom_0()
2815 .left_0()
2816 .bg(cx.theme().colors().drop_target_background)
2817 .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
2818 .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
2819 .when(is_local, |this| {
2820 this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
2821 })
2822 .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
2823 let item = tab.pane.read(cx).item_for_index(tab.ix);
2824 let project_paths = item
2825 .and_then(|item| item.project_path(cx))
2826 .into_iter()
2827 .collect::<Vec<_>>();
2828 this.handle_drop(project_paths, vec![], window, cx);
2829 }))
2830 .on_drop(
2831 cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2832 let project_paths = selection
2833 .items()
2834 .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
2835 .collect::<Vec<_>>();
2836 this.handle_drop(project_paths, vec![], window, cx);
2837 }),
2838 )
2839 .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
2840 let tasks = paths
2841 .paths()
2842 .iter()
2843 .map(|path| {
2844 Workspace::project_path_for_path(this.project.clone(), path, false, cx)
2845 })
2846 .collect::<Vec<_>>();
2847 cx.spawn_in(window, async move |this, cx| {
2848 let mut paths = vec![];
2849 let mut added_worktrees = vec![];
2850 let opened_paths = futures::future::join_all(tasks).await;
2851 for entry in opened_paths {
2852 if let Some((worktree, project_path)) = entry.log_err() {
2853 added_worktrees.push(worktree);
2854 paths.push(project_path);
2855 }
2856 }
2857 this.update_in(cx, |this, window, cx| {
2858 this.handle_drop(paths, added_worktrees, window, cx);
2859 })
2860 .ok();
2861 })
2862 .detach();
2863 }))
2864 }
2865
2866 fn handle_drop(
2867 &mut self,
2868 paths: Vec<ProjectPath>,
2869 added_worktrees: Vec<Entity<Worktree>>,
2870 window: &mut Window,
2871 cx: &mut Context<Self>,
2872 ) {
2873 match &self.active_view {
2874 ActiveView::AgentThread { thread_view } => {
2875 thread_view.update(cx, |thread_view, cx| {
2876 thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
2877 });
2878 }
2879 ActiveView::TextThread {
2880 text_thread_editor, ..
2881 } => {
2882 text_thread_editor.update(cx, |text_thread_editor, cx| {
2883 TextThreadEditor::insert_dragged_files(
2884 text_thread_editor,
2885 paths,
2886 added_worktrees,
2887 window,
2888 cx,
2889 );
2890 });
2891 }
2892 ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
2893 }
2894 }
2895
2896 fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
2897 if !self.show_trust_workspace_message {
2898 return None;
2899 }
2900
2901 let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
2902
2903 Some(
2904 Callout::new()
2905 .icon(IconName::Warning)
2906 .severity(Severity::Warning)
2907 .border_position(ui::BorderPosition::Bottom)
2908 .title("You're in Restricted Mode")
2909 .description(description)
2910 .actions_slot(
2911 Button::new("open-trust-modal", "Configure Project Trust")
2912 .label_size(LabelSize::Small)
2913 .style(ButtonStyle::Outlined)
2914 .on_click({
2915 cx.listener(move |this, _, window, cx| {
2916 this.workspace
2917 .update(cx, |workspace, cx| {
2918 workspace
2919 .show_worktree_trust_security_modal(true, window, cx)
2920 })
2921 .log_err();
2922 })
2923 }),
2924 ),
2925 )
2926 }
2927
2928 fn key_context(&self) -> KeyContext {
2929 let mut key_context = KeyContext::new_with_defaults();
2930 key_context.add("AgentPanel");
2931 match &self.active_view {
2932 ActiveView::AgentThread { .. } => key_context.add("acp_thread"),
2933 ActiveView::TextThread { .. } => key_context.add("text_thread"),
2934 ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
2935 }
2936 key_context
2937 }
2938}
2939
2940impl Render for AgentPanel {
2941 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2942 // WARNING: Changes to this element hierarchy can have
2943 // non-obvious implications to the layout of children.
2944 //
2945 // If you need to change it, please confirm:
2946 // - The message editor expands (cmd-option-esc) correctly
2947 // - When expanded, the buttons at the bottom of the panel are displayed correctly
2948 // - Font size works as expected and can be changed with cmd-+/cmd-
2949 // - Scrolling in all views works as expected
2950 // - Files can be dropped into the panel
2951 let content = v_flex()
2952 .relative()
2953 .size_full()
2954 .justify_between()
2955 .key_context(self.key_context())
2956 .on_action(cx.listener(|this, action: &NewThread, window, cx| {
2957 this.new_thread(action, window, cx);
2958 }))
2959 .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
2960 this.open_history(window, cx);
2961 }))
2962 .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
2963 this.open_configuration(window, cx);
2964 }))
2965 .on_action(cx.listener(Self::open_active_thread_as_markdown))
2966 .on_action(cx.listener(Self::deploy_rules_library))
2967 .on_action(cx.listener(Self::go_back))
2968 .on_action(cx.listener(Self::toggle_navigation_menu))
2969 .on_action(cx.listener(Self::toggle_options_menu))
2970 .on_action(cx.listener(Self::increase_font_size))
2971 .on_action(cx.listener(Self::decrease_font_size))
2972 .on_action(cx.listener(Self::reset_font_size))
2973 .on_action(cx.listener(Self::toggle_zoom))
2974 .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
2975 if let Some(thread_view) = this.active_thread_view() {
2976 thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
2977 }
2978 }))
2979 .child(self.render_toolbar(window, cx))
2980 .children(self.render_workspace_trust_message(cx))
2981 .children(self.render_onboarding(window, cx))
2982 .map(|parent| match &self.active_view {
2983 ActiveView::Uninitialized => parent,
2984 ActiveView::AgentThread { thread_view, .. } => parent
2985 .child(thread_view.clone())
2986 .child(self.render_drag_target(cx)),
2987 ActiveView::History { kind } => match kind {
2988 HistoryKind::AgentThreads => parent.child(self.acp_history.clone()),
2989 HistoryKind::TextThreads => parent.child(self.text_thread_history.clone()),
2990 },
2991 ActiveView::TextThread {
2992 text_thread_editor,
2993 buffer_search_bar,
2994 ..
2995 } => {
2996 let model_registry = LanguageModelRegistry::read_global(cx);
2997 let configuration_error =
2998 model_registry.configuration_error(model_registry.default_model(), cx);
2999 parent
3000 .map(|this| {
3001 if !self.should_render_onboarding(cx)
3002 && let Some(err) = configuration_error.as_ref()
3003 {
3004 this.child(self.render_configuration_error(
3005 true,
3006 err,
3007 &self.focus_handle(cx),
3008 cx,
3009 ))
3010 } else {
3011 this
3012 }
3013 })
3014 .child(self.render_text_thread(
3015 text_thread_editor,
3016 buffer_search_bar,
3017 window,
3018 cx,
3019 ))
3020 }
3021 ActiveView::Configuration => parent.children(self.configuration.clone()),
3022 })
3023 .children(self.render_trial_end_upsell(window, cx));
3024
3025 match self.active_view.which_font_size_used() {
3026 WhichFontSize::AgentFont => {
3027 WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
3028 .size_full()
3029 .child(content)
3030 .into_any()
3031 }
3032 _ => content.into_any(),
3033 }
3034 }
3035}
3036
3037struct PromptLibraryInlineAssist {
3038 workspace: WeakEntity<Workspace>,
3039}
3040
3041impl PromptLibraryInlineAssist {
3042 pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3043 Self { workspace }
3044 }
3045}
3046
3047impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3048 fn assist(
3049 &self,
3050 prompt_editor: &Entity<Editor>,
3051 initial_prompt: Option<String>,
3052 window: &mut Window,
3053 cx: &mut Context<RulesLibrary>,
3054 ) {
3055 InlineAssistant::update_global(cx, |assistant, cx| {
3056 let Some(workspace) = self.workspace.upgrade() else {
3057 return;
3058 };
3059 let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
3060 return;
3061 };
3062 let project = workspace.read(cx).project().downgrade();
3063 let panel = panel.read(cx);
3064 let thread_store = panel.thread_store().clone();
3065 let history = panel.history().downgrade();
3066 assistant.assist(
3067 prompt_editor,
3068 self.workspace.clone(),
3069 project,
3070 thread_store,
3071 None,
3072 history,
3073 initial_prompt,
3074 window,
3075 cx,
3076 );
3077 })
3078 }
3079
3080 fn focus_agent_panel(
3081 &self,
3082 workspace: &mut Workspace,
3083 window: &mut Window,
3084 cx: &mut Context<Workspace>,
3085 ) -> bool {
3086 workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3087 }
3088}
3089
3090pub struct ConcreteAssistantPanelDelegate;
3091
3092impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3093 fn active_text_thread_editor(
3094 &self,
3095 workspace: &mut Workspace,
3096 _window: &mut Window,
3097 cx: &mut Context<Workspace>,
3098 ) -> Option<Entity<TextThreadEditor>> {
3099 let panel = workspace.panel::<AgentPanel>(cx)?;
3100 panel.read(cx).active_text_thread_editor()
3101 }
3102
3103 fn open_local_text_thread(
3104 &self,
3105 workspace: &mut Workspace,
3106 path: Arc<Path>,
3107 window: &mut Window,
3108 cx: &mut Context<Workspace>,
3109 ) -> Task<Result<()>> {
3110 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3111 return Task::ready(Err(anyhow!("Agent panel not found")));
3112 };
3113
3114 panel.update(cx, |panel, cx| {
3115 panel.open_saved_text_thread(path, window, cx)
3116 })
3117 }
3118
3119 fn open_remote_text_thread(
3120 &self,
3121 _workspace: &mut Workspace,
3122 _text_thread_id: assistant_text_thread::TextThreadId,
3123 _window: &mut Window,
3124 _cx: &mut Context<Workspace>,
3125 ) -> Task<Result<Entity<TextThreadEditor>>> {
3126 Task::ready(Err(anyhow!("opening remote context not implemented")))
3127 }
3128
3129 fn quote_selection(
3130 &self,
3131 workspace: &mut Workspace,
3132 selection_ranges: Vec<Range<Anchor>>,
3133 buffer: Entity<MultiBuffer>,
3134 window: &mut Window,
3135 cx: &mut Context<Workspace>,
3136 ) {
3137 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3138 return;
3139 };
3140
3141 if !panel.focus_handle(cx).contains_focused(window, cx) {
3142 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3143 }
3144
3145 panel.update(cx, |_, cx| {
3146 // Wait to create a new context until the workspace is no longer
3147 // being updated.
3148 cx.defer_in(window, move |panel, window, cx| {
3149 if let Some(thread_view) = panel.active_thread_view() {
3150 thread_view.update(cx, |thread_view, cx| {
3151 thread_view.insert_selections(window, cx);
3152 });
3153 } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
3154 let snapshot = buffer.read(cx).snapshot(cx);
3155 let selection_ranges = selection_ranges
3156 .into_iter()
3157 .map(|range| range.to_point(&snapshot))
3158 .collect::<Vec<_>>();
3159
3160 text_thread_editor.update(cx, |text_thread_editor, cx| {
3161 text_thread_editor.quote_ranges(selection_ranges, snapshot, window, cx)
3162 });
3163 }
3164 });
3165 });
3166 }
3167
3168 fn quote_terminal_text(
3169 &self,
3170 workspace: &mut Workspace,
3171 text: String,
3172 window: &mut Window,
3173 cx: &mut Context<Workspace>,
3174 ) {
3175 let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3176 return;
3177 };
3178
3179 if !panel.focus_handle(cx).contains_focused(window, cx) {
3180 workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3181 }
3182
3183 panel.update(cx, |_, cx| {
3184 // Wait to create a new context until the workspace is no longer
3185 // being updated.
3186 cx.defer_in(window, move |panel, window, cx| {
3187 if let Some(thread_view) = panel.active_thread_view() {
3188 thread_view.update(cx, |thread_view, cx| {
3189 thread_view.insert_terminal_text(text, window, cx);
3190 });
3191 } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
3192 text_thread_editor.update(cx, |text_thread_editor, cx| {
3193 text_thread_editor.quote_terminal_text(text, window, cx)
3194 });
3195 }
3196 });
3197 });
3198 }
3199}
3200
3201struct OnboardingUpsell;
3202
3203impl Dismissable for OnboardingUpsell {
3204 const KEY: &'static str = "dismissed-trial-upsell";
3205}
3206
3207struct TrialEndUpsell;
3208
3209impl Dismissable for TrialEndUpsell {
3210 const KEY: &'static str = "dismissed-trial-end-upsell";
3211}
3212
3213#[cfg(feature = "test-support")]
3214impl AgentPanel {
3215 /// Opens an external thread using an arbitrary AgentServer.
3216 ///
3217 /// This is a test-only helper that allows visual tests and integration tests
3218 /// to inject a stub server without modifying production code paths.
3219 /// Not compiled into production builds.
3220 pub fn open_external_thread_with_server(
3221 &mut self,
3222 server: Rc<dyn AgentServer>,
3223 window: &mut Window,
3224 cx: &mut Context<Self>,
3225 ) {
3226 let workspace = self.workspace.clone();
3227 let project = self.project.clone();
3228
3229 let ext_agent = ExternalAgent::Custom {
3230 name: server.name(),
3231 };
3232
3233 self._external_thread(
3234 server, None, None, workspace, project, ext_agent, window, cx,
3235 );
3236 }
3237
3238 /// Returns the currently active thread view, if any.
3239 ///
3240 /// This is a test-only accessor that exposes the private `active_thread_view()`
3241 /// method for test assertions. Not compiled into production builds.
3242 pub fn active_thread_view_for_tests(&self) -> Option<&Entity<AcpServerView>> {
3243 self.active_thread_view()
3244 }
3245}