agent_panel.rs

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