agent_panel.rs

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