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