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