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