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