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