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