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