agent_panel.rs

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