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