agent_panel.rs

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