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