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