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