agent_panel.rs

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