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