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