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