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