agent_panel.rs

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