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