agent_panel.rs

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