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