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