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