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