agent_panel.rs

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