agent_panel.rs

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