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::AcpServerView,
  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    AgentThread {
 247        thread_view: Entity<AcpServerView>,
 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::AgentThread { .. }
 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<AcpServerView>> {
 735        match &self.active_view {
 736            ActiveView::AgentThread { 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::AgentThread { 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::AgentThread { 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::AgentThread { 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::AgentThread { 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::AcpServerView::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(ActiveView::AgentThread { thread_view }, true, window, cx);
1720    }
1721}
1722
1723impl Focusable for AgentPanel {
1724    fn focus_handle(&self, cx: &App) -> FocusHandle {
1725        match &self.active_view {
1726            ActiveView::Uninitialized => self.focus_handle.clone(),
1727            ActiveView::AgentThread { thread_view, .. } => thread_view.focus_handle(cx),
1728            ActiveView::History { kind } => match kind {
1729                HistoryKind::AgentThreads => self.acp_history.focus_handle(cx),
1730                HistoryKind::TextThreads => self.text_thread_history.focus_handle(cx),
1731            },
1732            ActiveView::TextThread {
1733                text_thread_editor, ..
1734            } => text_thread_editor.focus_handle(cx),
1735            ActiveView::Configuration => {
1736                if let Some(configuration) = self.configuration.as_ref() {
1737                    configuration.focus_handle(cx)
1738                } else {
1739                    self.focus_handle.clone()
1740                }
1741            }
1742        }
1743    }
1744}
1745
1746fn agent_panel_dock_position(cx: &App) -> DockPosition {
1747    AgentSettings::get_global(cx).dock.into()
1748}
1749
1750impl EventEmitter<PanelEvent> for AgentPanel {}
1751
1752impl Panel for AgentPanel {
1753    fn persistent_name() -> &'static str {
1754        "AgentPanel"
1755    }
1756
1757    fn panel_key() -> &'static str {
1758        AGENT_PANEL_KEY
1759    }
1760
1761    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1762        agent_panel_dock_position(cx)
1763    }
1764
1765    fn position_is_valid(&self, position: DockPosition) -> bool {
1766        position != DockPosition::Bottom
1767    }
1768
1769    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1770        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1771            settings
1772                .agent
1773                .get_or_insert_default()
1774                .set_dock(position.into());
1775        });
1776    }
1777
1778    fn size(&self, window: &Window, cx: &App) -> Pixels {
1779        let settings = AgentSettings::get_global(cx);
1780        match self.position(window, cx) {
1781            DockPosition::Left | DockPosition::Right => {
1782                self.width.unwrap_or(settings.default_width)
1783            }
1784            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1785        }
1786    }
1787
1788    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1789        match self.position(window, cx) {
1790            DockPosition::Left | DockPosition::Right => self.width = size,
1791            DockPosition::Bottom => self.height = size,
1792        }
1793        self.serialize(cx);
1794        cx.notify();
1795    }
1796
1797    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1798        if active && matches!(self.active_view, ActiveView::Uninitialized) {
1799            let selected_agent = self.selected_agent.clone();
1800            self.new_agent_thread(selected_agent, window, cx);
1801        }
1802    }
1803
1804    fn remote_id() -> Option<proto::PanelId> {
1805        Some(proto::PanelId::AssistantPanel)
1806    }
1807
1808    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1809        (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
1810    }
1811
1812    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1813        Some("Agent Panel")
1814    }
1815
1816    fn toggle_action(&self) -> Box<dyn Action> {
1817        Box::new(ToggleFocus)
1818    }
1819
1820    fn activation_priority(&self) -> u32 {
1821        3
1822    }
1823
1824    fn enabled(&self, cx: &App) -> bool {
1825        AgentSettings::get_global(cx).enabled(cx)
1826    }
1827
1828    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1829        self.zoomed
1830    }
1831
1832    fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1833        self.zoomed = zoomed;
1834        cx.notify();
1835    }
1836}
1837
1838impl AgentPanel {
1839    fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1840        const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1841
1842        let content = match &self.active_view {
1843            ActiveView::AgentThread { thread_view } => {
1844                let is_generating_title = thread_view
1845                    .read(cx)
1846                    .as_native_thread(cx)
1847                    .map_or(false, |t| t.read(cx).is_generating_title());
1848
1849                if let Some(title_editor) = thread_view
1850                    .read(cx)
1851                    .as_active_thread()
1852                    .and_then(|ready| ready.title_editor.clone())
1853                {
1854                    let container = div()
1855                        .w_full()
1856                        .on_action({
1857                            let thread_view = thread_view.downgrade();
1858                            move |_: &menu::Confirm, window, cx| {
1859                                if let Some(thread_view) = thread_view.upgrade() {
1860                                    thread_view.focus_handle(cx).focus(window, cx);
1861                                }
1862                            }
1863                        })
1864                        .on_action({
1865                            let thread_view = thread_view.downgrade();
1866                            move |_: &editor::actions::Cancel, window, cx| {
1867                                if let Some(thread_view) = thread_view.upgrade() {
1868                                    thread_view.focus_handle(cx).focus(window, cx);
1869                                }
1870                            }
1871                        })
1872                        .child(title_editor);
1873
1874                    if is_generating_title {
1875                        container
1876                            .with_animation(
1877                                "generating_title",
1878                                Animation::new(Duration::from_secs(2))
1879                                    .repeat()
1880                                    .with_easing(pulsating_between(0.4, 0.8)),
1881                                |div, delta| div.opacity(delta),
1882                            )
1883                            .into_any_element()
1884                    } else {
1885                        container.into_any_element()
1886                    }
1887                } else {
1888                    Label::new(thread_view.read(cx).title(cx))
1889                        .color(Color::Muted)
1890                        .truncate()
1891                        .into_any_element()
1892                }
1893            }
1894            ActiveView::TextThread {
1895                title_editor,
1896                text_thread_editor,
1897                ..
1898            } => {
1899                let summary = text_thread_editor.read(cx).text_thread().read(cx).summary();
1900
1901                match summary {
1902                    TextThreadSummary::Pending => Label::new(TextThreadSummary::DEFAULT)
1903                        .color(Color::Muted)
1904                        .truncate()
1905                        .into_any_element(),
1906                    TextThreadSummary::Content(summary) => {
1907                        if summary.done {
1908                            div()
1909                                .w_full()
1910                                .child(title_editor.clone())
1911                                .into_any_element()
1912                        } else {
1913                            Label::new(LOADING_SUMMARY_PLACEHOLDER)
1914                                .truncate()
1915                                .color(Color::Muted)
1916                                .with_animation(
1917                                    "generating_title",
1918                                    Animation::new(Duration::from_secs(2))
1919                                        .repeat()
1920                                        .with_easing(pulsating_between(0.4, 0.8)),
1921                                    |label, delta| label.alpha(delta),
1922                                )
1923                                .into_any_element()
1924                        }
1925                    }
1926                    TextThreadSummary::Error => h_flex()
1927                        .w_full()
1928                        .child(title_editor.clone())
1929                        .child(
1930                            IconButton::new("retry-summary-generation", IconName::RotateCcw)
1931                                .icon_size(IconSize::Small)
1932                                .on_click({
1933                                    let text_thread_editor = text_thread_editor.clone();
1934                                    move |_, _window, cx| {
1935                                        text_thread_editor.update(cx, |text_thread_editor, cx| {
1936                                            text_thread_editor.regenerate_summary(cx);
1937                                        });
1938                                    }
1939                                })
1940                                .tooltip(move |_window, cx| {
1941                                    cx.new(|_| {
1942                                        Tooltip::new("Failed to generate title")
1943                                            .meta("Click to try again")
1944                                    })
1945                                    .into()
1946                                }),
1947                        )
1948                        .into_any_element(),
1949                }
1950            }
1951            ActiveView::History { kind } => {
1952                let title = match kind {
1953                    HistoryKind::AgentThreads => "History",
1954                    HistoryKind::TextThreads => "Text Thread History",
1955                };
1956                Label::new(title).truncate().into_any_element()
1957            }
1958            ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1959            ActiveView::Uninitialized => Label::new("Agent").truncate().into_any_element(),
1960        };
1961
1962        h_flex()
1963            .key_context("TitleEditor")
1964            .id("TitleEditor")
1965            .flex_grow()
1966            .w_full()
1967            .max_w_full()
1968            .overflow_x_scroll()
1969            .child(content)
1970            .into_any()
1971    }
1972
1973    fn handle_regenerate_thread_title(thread_view: Entity<AcpServerView>, cx: &mut App) {
1974        thread_view.update(cx, |thread_view, cx| {
1975            if let Some(thread) = thread_view.as_native_thread(cx) {
1976                thread.update(cx, |thread, cx| {
1977                    thread.generate_title(cx);
1978                });
1979            }
1980        });
1981    }
1982
1983    fn handle_regenerate_text_thread_title(
1984        text_thread_editor: Entity<TextThreadEditor>,
1985        cx: &mut App,
1986    ) {
1987        text_thread_editor.update(cx, |text_thread_editor, cx| {
1988            text_thread_editor.regenerate_summary(cx);
1989        });
1990    }
1991
1992    fn render_panel_options_menu(
1993        &self,
1994        window: &mut Window,
1995        cx: &mut Context<Self>,
1996    ) -> impl IntoElement {
1997        let focus_handle = self.focus_handle(cx);
1998
1999        let full_screen_label = if self.is_zoomed(window, cx) {
2000            "Disable Full Screen"
2001        } else {
2002            "Enable Full Screen"
2003        };
2004
2005        let selected_agent = self.selected_agent.clone();
2006
2007        let text_thread_view = match &self.active_view {
2008            ActiveView::TextThread {
2009                text_thread_editor, ..
2010            } => Some(text_thread_editor.clone()),
2011            _ => None,
2012        };
2013        let text_thread_with_messages = match &self.active_view {
2014            ActiveView::TextThread {
2015                text_thread_editor, ..
2016            } => text_thread_editor
2017                .read(cx)
2018                .text_thread()
2019                .read(cx)
2020                .messages(cx)
2021                .any(|message| message.role == language_model::Role::Assistant),
2022            _ => false,
2023        };
2024
2025        let thread_view = match &self.active_view {
2026            ActiveView::AgentThread { thread_view } => Some(thread_view.clone()),
2027            _ => None,
2028        };
2029        let thread_with_messages = match &self.active_view {
2030            ActiveView::AgentThread { thread_view } => {
2031                thread_view.read(cx).has_user_submitted_prompt(cx)
2032            }
2033            _ => false,
2034        };
2035
2036        PopoverMenu::new("agent-options-menu")
2037            .trigger_with_tooltip(
2038                IconButton::new("agent-options-menu", IconName::Ellipsis)
2039                    .icon_size(IconSize::Small),
2040                {
2041                    let focus_handle = focus_handle.clone();
2042                    move |_window, cx| {
2043                        Tooltip::for_action_in(
2044                            "Toggle Agent Menu",
2045                            &ToggleOptionsMenu,
2046                            &focus_handle,
2047                            cx,
2048                        )
2049                    }
2050                },
2051            )
2052            .anchor(Corner::TopRight)
2053            .with_handle(self.agent_panel_menu_handle.clone())
2054            .menu({
2055                move |window, cx| {
2056                    Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
2057                        menu = menu.context(focus_handle.clone());
2058
2059                        if thread_with_messages | text_thread_with_messages {
2060                            menu = menu.header("Current Thread");
2061
2062                            if let Some(text_thread_view) = text_thread_view.as_ref() {
2063                                menu = menu
2064                                    .entry("Regenerate Thread Title", None, {
2065                                        let text_thread_view = text_thread_view.clone();
2066                                        move |_, cx| {
2067                                            Self::handle_regenerate_text_thread_title(
2068                                                text_thread_view.clone(),
2069                                                cx,
2070                                            );
2071                                        }
2072                                    })
2073                                    .separator();
2074                            }
2075
2076                            if let Some(thread_view) = thread_view.as_ref() {
2077                                menu = menu
2078                                    .entry("Regenerate Thread Title", None, {
2079                                        let thread_view = thread_view.clone();
2080                                        move |_, cx| {
2081                                            Self::handle_regenerate_thread_title(
2082                                                thread_view.clone(),
2083                                                cx,
2084                                            );
2085                                        }
2086                                    })
2087                                    .separator();
2088                            }
2089                        }
2090
2091                        menu = menu
2092                            .header("MCP Servers")
2093                            .action(
2094                                "View Server Extensions",
2095                                Box::new(zed_actions::Extensions {
2096                                    category_filter: Some(
2097                                        zed_actions::ExtensionCategoryFilter::ContextServers,
2098                                    ),
2099                                    id: None,
2100                                }),
2101                            )
2102                            .action("Add Custom Server…", Box::new(AddContextServer))
2103                            .separator()
2104                            .action("Rules", Box::new(OpenRulesLibrary::default()))
2105                            .action("Profiles", Box::new(ManageProfiles::default()))
2106                            .action("Settings", Box::new(OpenSettings))
2107                            .separator()
2108                            .action(full_screen_label, Box::new(ToggleZoom));
2109
2110                        if selected_agent == AgentType::Gemini {
2111                            menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
2112                        }
2113
2114                        menu
2115                    }))
2116                }
2117            })
2118    }
2119
2120    fn render_recent_entries_menu(
2121        &self,
2122        icon: IconName,
2123        corner: Corner,
2124        cx: &mut Context<Self>,
2125    ) -> impl IntoElement {
2126        let focus_handle = self.focus_handle(cx);
2127
2128        PopoverMenu::new("agent-nav-menu")
2129            .trigger_with_tooltip(
2130                IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
2131                {
2132                    move |_window, cx| {
2133                        Tooltip::for_action_in(
2134                            "Toggle Recently Updated Threads",
2135                            &ToggleNavigationMenu,
2136                            &focus_handle,
2137                            cx,
2138                        )
2139                    }
2140                },
2141            )
2142            .anchor(corner)
2143            .with_handle(self.agent_navigation_menu_handle.clone())
2144            .menu({
2145                let menu = self.agent_navigation_menu.clone();
2146                move |window, cx| {
2147                    telemetry::event!("View Thread History Clicked");
2148
2149                    if let Some(menu) = menu.as_ref() {
2150                        menu.update(cx, |_, cx| {
2151                            cx.defer_in(window, |menu, window, cx| {
2152                                menu.rebuild(window, cx);
2153                            });
2154                        })
2155                    }
2156                    menu.clone()
2157                }
2158            })
2159    }
2160
2161    fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
2162        let focus_handle = self.focus_handle(cx);
2163
2164        IconButton::new("go-back", IconName::ArrowLeft)
2165            .icon_size(IconSize::Small)
2166            .on_click(cx.listener(|this, _, window, cx| {
2167                this.go_back(&workspace::GoBack, window, cx);
2168            }))
2169            .tooltip({
2170                move |_window, cx| {
2171                    Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
2172                }
2173            })
2174    }
2175
2176    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2177        let agent_server_store = self.project.read(cx).agent_server_store().clone();
2178        let focus_handle = self.focus_handle(cx);
2179
2180        let (selected_agent_custom_icon, selected_agent_label) =
2181            if let AgentType::Custom { name, .. } = &self.selected_agent {
2182                let store = agent_server_store.read(cx);
2183                let icon = store.agent_icon(&ExternalAgentServerName(name.clone()));
2184
2185                let label = store
2186                    .agent_display_name(&ExternalAgentServerName(name.clone()))
2187                    .unwrap_or_else(|| self.selected_agent.label());
2188                (icon, label)
2189            } else {
2190                (None, self.selected_agent.label())
2191            };
2192
2193        let active_thread = match &self.active_view {
2194            ActiveView::AgentThread { thread_view } => thread_view.read(cx).as_native_thread(cx),
2195            ActiveView::Uninitialized
2196            | ActiveView::TextThread { .. }
2197            | ActiveView::History { .. }
2198            | ActiveView::Configuration => None,
2199        };
2200
2201        let new_thread_menu = PopoverMenu::new("new_thread_menu")
2202            .trigger_with_tooltip(
2203                IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2204                {
2205                    let focus_handle = focus_handle.clone();
2206                    move |_window, cx| {
2207                        Tooltip::for_action_in(
2208                            "New Thread…",
2209                            &ToggleNewThreadMenu,
2210                            &focus_handle,
2211                            cx,
2212                        )
2213                    }
2214                },
2215            )
2216            .anchor(Corner::TopRight)
2217            .with_handle(self.new_thread_menu_handle.clone())
2218            .menu({
2219                let selected_agent = self.selected_agent.clone();
2220                let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
2221
2222                let workspace = self.workspace.clone();
2223                let is_via_collab = workspace
2224                    .update(cx, |workspace, cx| {
2225                        workspace.project().read(cx).is_via_collab()
2226                    })
2227                    .unwrap_or_default();
2228
2229                move |window, cx| {
2230                    telemetry::event!("New Thread Clicked");
2231
2232                    let active_thread = active_thread.clone();
2233                    Some(ContextMenu::build(window, cx, |menu, _window, cx| {
2234                        menu.context(focus_handle.clone())
2235                            .when_some(active_thread, |this, active_thread| {
2236                                let thread = active_thread.read(cx);
2237
2238                                if !thread.is_empty() {
2239                                    let session_id = thread.id().clone();
2240                                    this.item(
2241                                        ContextMenuEntry::new("New From Summary")
2242                                            .icon(IconName::ThreadFromSummary)
2243                                            .icon_color(Color::Muted)
2244                                            .handler(move |window, cx| {
2245                                                window.dispatch_action(
2246                                                    Box::new(NewNativeAgentThreadFromSummary {
2247                                                        from_session_id: session_id.clone(),
2248                                                    }),
2249                                                    cx,
2250                                                );
2251                                            }),
2252                                    )
2253                                } else {
2254                                    this
2255                                }
2256                            })
2257                            .item(
2258                                ContextMenuEntry::new("Zed Agent")
2259                                    .when(
2260                                        is_agent_selected(AgentType::NativeAgent)
2261                                            | is_agent_selected(AgentType::TextThread),
2262                                        |this| {
2263                                            this.action(Box::new(NewExternalAgentThread {
2264                                                agent: None,
2265                                            }))
2266                                        },
2267                                    )
2268                                    .icon(IconName::ZedAgent)
2269                                    .icon_color(Color::Muted)
2270                                    .handler({
2271                                        let workspace = workspace.clone();
2272                                        move |window, cx| {
2273                                            if let Some(workspace) = workspace.upgrade() {
2274                                                workspace.update(cx, |workspace, cx| {
2275                                                    if let Some(panel) =
2276                                                        workspace.panel::<AgentPanel>(cx)
2277                                                    {
2278                                                        panel.update(cx, |panel, cx| {
2279                                                            panel.new_agent_thread(
2280                                                                AgentType::NativeAgent,
2281                                                                window,
2282                                                                cx,
2283                                                            );
2284                                                        });
2285                                                    }
2286                                                });
2287                                            }
2288                                        }
2289                                    }),
2290                            )
2291                            .item(
2292                                ContextMenuEntry::new("Text Thread")
2293                                    .action(NewTextThread.boxed_clone())
2294                                    .icon(IconName::TextThread)
2295                                    .icon_color(Color::Muted)
2296                                    .handler({
2297                                        let workspace = workspace.clone();
2298                                        move |window, cx| {
2299                                            if let Some(workspace) = workspace.upgrade() {
2300                                                workspace.update(cx, |workspace, cx| {
2301                                                    if let Some(panel) =
2302                                                        workspace.panel::<AgentPanel>(cx)
2303                                                    {
2304                                                        panel.update(cx, |panel, cx| {
2305                                                            panel.new_agent_thread(
2306                                                                AgentType::TextThread,
2307                                                                window,
2308                                                                cx,
2309                                                            );
2310                                                        });
2311                                                    }
2312                                                });
2313                                            }
2314                                        }
2315                                    }),
2316                            )
2317                            .separator()
2318                            .header("External Agents")
2319                            .item(
2320                                ContextMenuEntry::new("Claude Code")
2321                                    .when(is_agent_selected(AgentType::ClaudeCode), |this| {
2322                                        this.action(Box::new(NewExternalAgentThread {
2323                                            agent: None,
2324                                        }))
2325                                    })
2326                                    .icon(IconName::AiClaude)
2327                                    .disabled(is_via_collab)
2328                                    .icon_color(Color::Muted)
2329                                    .handler({
2330                                        let workspace = workspace.clone();
2331                                        move |window, cx| {
2332                                            if let Some(workspace) = workspace.upgrade() {
2333                                                workspace.update(cx, |workspace, cx| {
2334                                                    if let Some(panel) =
2335                                                        workspace.panel::<AgentPanel>(cx)
2336                                                    {
2337                                                        panel.update(cx, |panel, cx| {
2338                                                            panel.new_agent_thread(
2339                                                                AgentType::ClaudeCode,
2340                                                                window,
2341                                                                cx,
2342                                                            );
2343                                                        });
2344                                                    }
2345                                                });
2346                                            }
2347                                        }
2348                                    }),
2349                            )
2350                            .item(
2351                                ContextMenuEntry::new("Codex CLI")
2352                                    .when(is_agent_selected(AgentType::Codex), |this| {
2353                                        this.action(Box::new(NewExternalAgentThread {
2354                                            agent: None,
2355                                        }))
2356                                    })
2357                                    .icon(IconName::AiOpenAi)
2358                                    .disabled(is_via_collab)
2359                                    .icon_color(Color::Muted)
2360                                    .handler({
2361                                        let workspace = workspace.clone();
2362                                        move |window, cx| {
2363                                            if let Some(workspace) = workspace.upgrade() {
2364                                                workspace.update(cx, |workspace, cx| {
2365                                                    if let Some(panel) =
2366                                                        workspace.panel::<AgentPanel>(cx)
2367                                                    {
2368                                                        panel.update(cx, |panel, cx| {
2369                                                            panel.new_agent_thread(
2370                                                                AgentType::Codex,
2371                                                                window,
2372                                                                cx,
2373                                                            );
2374                                                        });
2375                                                    }
2376                                                });
2377                                            }
2378                                        }
2379                                    }),
2380                            )
2381                            .item(
2382                                ContextMenuEntry::new("Gemini CLI")
2383                                    .when(is_agent_selected(AgentType::Gemini), |this| {
2384                                        this.action(Box::new(NewExternalAgentThread {
2385                                            agent: None,
2386                                        }))
2387                                    })
2388                                    .icon(IconName::AiGemini)
2389                                    .icon_color(Color::Muted)
2390                                    .disabled(is_via_collab)
2391                                    .handler({
2392                                        let workspace = workspace.clone();
2393                                        move |window, cx| {
2394                                            if let Some(workspace) = workspace.upgrade() {
2395                                                workspace.update(cx, |workspace, cx| {
2396                                                    if let Some(panel) =
2397                                                        workspace.panel::<AgentPanel>(cx)
2398                                                    {
2399                                                        panel.update(cx, |panel, cx| {
2400                                                            panel.new_agent_thread(
2401                                                                AgentType::Gemini,
2402                                                                window,
2403                                                                cx,
2404                                                            );
2405                                                        });
2406                                                    }
2407                                                });
2408                                            }
2409                                        }
2410                                    }),
2411                            )
2412                            .map(|mut menu| {
2413                                let agent_server_store = agent_server_store.read(cx);
2414                                let agent_names = agent_server_store
2415                                    .external_agents()
2416                                    .filter(|name| {
2417                                        name.0 != GEMINI_NAME
2418                                            && name.0 != CLAUDE_CODE_NAME
2419                                            && name.0 != CODEX_NAME
2420                                    })
2421                                    .cloned()
2422                                    .collect::<Vec<_>>();
2423
2424                                for agent_name in agent_names {
2425                                    let icon_path = agent_server_store.agent_icon(&agent_name);
2426                                    let display_name = agent_server_store
2427                                        .agent_display_name(&agent_name)
2428                                        .unwrap_or_else(|| agent_name.0.clone());
2429
2430                                    let mut entry = ContextMenuEntry::new(display_name);
2431
2432                                    if let Some(icon_path) = icon_path {
2433                                        entry = entry.custom_icon_svg(icon_path);
2434                                    } else {
2435                                        entry = entry.icon(IconName::Sparkle);
2436                                    }
2437                                    entry = entry
2438                                        .when(
2439                                            is_agent_selected(AgentType::Custom {
2440                                                name: agent_name.0.clone(),
2441                                            }),
2442                                            |this| {
2443                                                this.action(Box::new(NewExternalAgentThread {
2444                                                    agent: None,
2445                                                }))
2446                                            },
2447                                        )
2448                                        .icon_color(Color::Muted)
2449                                        .disabled(is_via_collab)
2450                                        .handler({
2451                                            let workspace = workspace.clone();
2452                                            let agent_name = agent_name.clone();
2453                                            move |window, cx| {
2454                                                if let Some(workspace) = workspace.upgrade() {
2455                                                    workspace.update(cx, |workspace, cx| {
2456                                                        if let Some(panel) =
2457                                                            workspace.panel::<AgentPanel>(cx)
2458                                                        {
2459                                                            panel.update(cx, |panel, cx| {
2460                                                                panel.new_agent_thread(
2461                                                                    AgentType::Custom {
2462                                                                        name: agent_name
2463                                                                            .clone()
2464                                                                            .into(),
2465                                                                    },
2466                                                                    window,
2467                                                                    cx,
2468                                                                );
2469                                                            });
2470                                                        }
2471                                                    });
2472                                                }
2473                                            }
2474                                        });
2475
2476                                    menu = menu.item(entry);
2477                                }
2478
2479                                menu
2480                            })
2481                            .separator()
2482                            .item(
2483                                ContextMenuEntry::new("Add More Agents")
2484                                    .icon(IconName::Plus)
2485                                    .icon_color(Color::Muted)
2486                                    .handler({
2487                                        move |window, cx| {
2488                                            window.dispatch_action(
2489                                                Box::new(zed_actions::AcpRegistry),
2490                                                cx,
2491                                            )
2492                                        }
2493                                    }),
2494                            )
2495                    }))
2496                }
2497            });
2498
2499        let is_thread_loading = self
2500            .active_thread_view()
2501            .map(|thread| thread.read(cx).is_loading())
2502            .unwrap_or(false);
2503
2504        let has_custom_icon = selected_agent_custom_icon.is_some();
2505
2506        let selected_agent = div()
2507            .id("selected_agent_icon")
2508            .when_some(selected_agent_custom_icon, |this, icon_path| {
2509                this.px_1()
2510                    .child(Icon::from_external_svg(icon_path).color(Color::Muted))
2511            })
2512            .when(!has_custom_icon, |this| {
2513                this.when_some(self.selected_agent.icon(), |this, icon| {
2514                    this.px_1().child(Icon::new(icon).color(Color::Muted))
2515                })
2516            })
2517            .tooltip(move |_, cx| {
2518                Tooltip::with_meta(selected_agent_label.clone(), None, "Selected Agent", cx)
2519            });
2520
2521        let selected_agent = if is_thread_loading {
2522            selected_agent
2523                .with_animation(
2524                    "pulsating-icon",
2525                    Animation::new(Duration::from_secs(1))
2526                        .repeat()
2527                        .with_easing(pulsating_between(0.2, 0.6)),
2528                    |icon, delta| icon.opacity(delta),
2529                )
2530                .into_any_element()
2531        } else {
2532            selected_agent.into_any_element()
2533        };
2534
2535        let show_history_menu = self.history_kind_for_selected_agent(cx).is_some();
2536
2537        h_flex()
2538            .id("agent-panel-toolbar")
2539            .h(Tab::container_height(cx))
2540            .max_w_full()
2541            .flex_none()
2542            .justify_between()
2543            .gap_2()
2544            .bg(cx.theme().colors().tab_bar_background)
2545            .border_b_1()
2546            .border_color(cx.theme().colors().border)
2547            .child(
2548                h_flex()
2549                    .size_full()
2550                    .gap(DynamicSpacing::Base04.rems(cx))
2551                    .pl(DynamicSpacing::Base04.rems(cx))
2552                    .child(match &self.active_view {
2553                        ActiveView::History { .. } | ActiveView::Configuration => {
2554                            self.render_toolbar_back_button(cx).into_any_element()
2555                        }
2556                        _ => selected_agent.into_any_element(),
2557                    })
2558                    .child(self.render_title_view(window, cx)),
2559            )
2560            .child(
2561                h_flex()
2562                    .flex_none()
2563                    .gap(DynamicSpacing::Base02.rems(cx))
2564                    .pl(DynamicSpacing::Base04.rems(cx))
2565                    .pr(DynamicSpacing::Base06.rems(cx))
2566                    .child(new_thread_menu)
2567                    .when(show_history_menu, |this| {
2568                        this.child(self.render_recent_entries_menu(
2569                            IconName::MenuAltTemp,
2570                            Corner::TopRight,
2571                            cx,
2572                        ))
2573                    })
2574                    .child(self.render_panel_options_menu(window, cx)),
2575            )
2576    }
2577
2578    fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2579        if TrialEndUpsell::dismissed() {
2580            return false;
2581        }
2582
2583        match &self.active_view {
2584            ActiveView::TextThread { .. } => {
2585                if LanguageModelRegistry::global(cx)
2586                    .read(cx)
2587                    .default_model()
2588                    .is_some_and(|model| {
2589                        model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2590                    })
2591                {
2592                    return false;
2593                }
2594            }
2595            ActiveView::Uninitialized
2596            | ActiveView::AgentThread { .. }
2597            | ActiveView::History { .. }
2598            | ActiveView::Configuration => return false,
2599        }
2600
2601        let plan = self.user_store.read(cx).plan();
2602        let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2603
2604        plan.is_some_and(|plan| plan == Plan::ZedFree) && has_previous_trial
2605    }
2606
2607    fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
2608        if OnboardingUpsell::dismissed() {
2609            return false;
2610        }
2611
2612        let user_store = self.user_store.read(cx);
2613
2614        if user_store.plan().is_some_and(|plan| plan == Plan::ZedPro)
2615            && user_store
2616                .subscription_period()
2617                .and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
2618                .is_some_and(|date| date < chrono::Utc::now())
2619        {
2620            OnboardingUpsell::set_dismissed(true, cx);
2621            return false;
2622        }
2623
2624        match &self.active_view {
2625            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
2626                false
2627            }
2628            ActiveView::AgentThread { thread_view, .. }
2629                if thread_view.read(cx).as_native_thread(cx).is_none() =>
2630            {
2631                false
2632            }
2633            _ => {
2634                let history_is_empty = self.acp_history.read(cx).is_empty();
2635
2636                let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
2637                    .visible_providers()
2638                    .iter()
2639                    .any(|provider| {
2640                        provider.is_authenticated(cx)
2641                            && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2642                    });
2643
2644                history_is_empty || !has_configured_non_zed_providers
2645            }
2646        }
2647    }
2648
2649    fn render_onboarding(
2650        &self,
2651        _window: &mut Window,
2652        cx: &mut Context<Self>,
2653    ) -> Option<impl IntoElement> {
2654        if !self.should_render_onboarding(cx) {
2655            return None;
2656        }
2657
2658        let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
2659
2660        Some(
2661            div()
2662                .when(text_thread_view, |this| {
2663                    this.bg(cx.theme().colors().editor_background)
2664                })
2665                .child(self.onboarding.clone()),
2666        )
2667    }
2668
2669    fn render_trial_end_upsell(
2670        &self,
2671        _window: &mut Window,
2672        cx: &mut Context<Self>,
2673    ) -> Option<impl IntoElement> {
2674        if !self.should_render_trial_end_upsell(cx) {
2675            return None;
2676        }
2677
2678        Some(
2679            v_flex()
2680                .absolute()
2681                .inset_0()
2682                .size_full()
2683                .bg(cx.theme().colors().panel_background)
2684                .opacity(0.85)
2685                .block_mouse_except_scroll()
2686                .child(EndTrialUpsell::new(Arc::new({
2687                    let this = cx.entity();
2688                    move |_, cx| {
2689                        this.update(cx, |_this, cx| {
2690                            TrialEndUpsell::set_dismissed(true, cx);
2691                            cx.notify();
2692                        });
2693                    }
2694                }))),
2695        )
2696    }
2697
2698    fn render_configuration_error(
2699        &self,
2700        border_bottom: bool,
2701        configuration_error: &ConfigurationError,
2702        focus_handle: &FocusHandle,
2703        cx: &mut App,
2704    ) -> impl IntoElement {
2705        let zed_provider_configured = AgentSettings::get_global(cx)
2706            .default_model
2707            .as_ref()
2708            .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev");
2709
2710        let callout = if zed_provider_configured {
2711            Callout::new()
2712                .icon(IconName::Warning)
2713                .severity(Severity::Warning)
2714                .when(border_bottom, |this| {
2715                    this.border_position(ui::BorderPosition::Bottom)
2716                })
2717                .title("Sign in to continue using Zed as your LLM provider.")
2718                .actions_slot(
2719                    Button::new("sign_in", "Sign In")
2720                        .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2721                        .label_size(LabelSize::Small)
2722                        .on_click({
2723                            let workspace = self.workspace.clone();
2724                            move |_, _, cx| {
2725                                let Ok(client) =
2726                                    workspace.update(cx, |workspace, _| workspace.client().clone())
2727                                else {
2728                                    return;
2729                                };
2730
2731                                cx.spawn(async move |cx| {
2732                                    client.sign_in_with_optional_connect(true, cx).await
2733                                })
2734                                .detach_and_log_err(cx);
2735                            }
2736                        }),
2737                )
2738        } else {
2739            Callout::new()
2740                .icon(IconName::Warning)
2741                .severity(Severity::Warning)
2742                .when(border_bottom, |this| {
2743                    this.border_position(ui::BorderPosition::Bottom)
2744                })
2745                .title(configuration_error.to_string())
2746                .actions_slot(
2747                    Button::new("settings", "Configure")
2748                        .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2749                        .label_size(LabelSize::Small)
2750                        .key_binding(
2751                            KeyBinding::for_action_in(&OpenSettings, focus_handle, cx)
2752                                .map(|kb| kb.size(rems_from_px(12.))),
2753                        )
2754                        .on_click(|_event, window, cx| {
2755                            window.dispatch_action(OpenSettings.boxed_clone(), cx)
2756                        }),
2757                )
2758        };
2759
2760        match configuration_error {
2761            ConfigurationError::ModelNotFound
2762            | ConfigurationError::ProviderNotAuthenticated(_)
2763            | ConfigurationError::NoProvider => callout.into_any_element(),
2764        }
2765    }
2766
2767    fn render_text_thread(
2768        &self,
2769        text_thread_editor: &Entity<TextThreadEditor>,
2770        buffer_search_bar: &Entity<BufferSearchBar>,
2771        window: &mut Window,
2772        cx: &mut Context<Self>,
2773    ) -> Div {
2774        let mut registrar = buffer_search::DivRegistrar::new(
2775            |this, _, _cx| match &this.active_view {
2776                ActiveView::TextThread {
2777                    buffer_search_bar, ..
2778                } => Some(buffer_search_bar.clone()),
2779                _ => None,
2780            },
2781            cx,
2782        );
2783        BufferSearchBar::register(&mut registrar);
2784        registrar
2785            .into_div()
2786            .size_full()
2787            .relative()
2788            .map(|parent| {
2789                buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2790                    if buffer_search_bar.is_dismissed() {
2791                        return parent;
2792                    }
2793                    parent.child(
2794                        div()
2795                            .p(DynamicSpacing::Base08.rems(cx))
2796                            .border_b_1()
2797                            .border_color(cx.theme().colors().border_variant)
2798                            .bg(cx.theme().colors().editor_background)
2799                            .child(buffer_search_bar.render(window, cx)),
2800                    )
2801                })
2802            })
2803            .child(text_thread_editor.clone())
2804            .child(self.render_drag_target(cx))
2805    }
2806
2807    fn render_drag_target(&self, cx: &Context<Self>) -> Div {
2808        let is_local = self.project.read(cx).is_local();
2809        div()
2810            .invisible()
2811            .absolute()
2812            .top_0()
2813            .right_0()
2814            .bottom_0()
2815            .left_0()
2816            .bg(cx.theme().colors().drop_target_background)
2817            .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
2818            .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
2819            .when(is_local, |this| {
2820                this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
2821            })
2822            .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
2823                let item = tab.pane.read(cx).item_for_index(tab.ix);
2824                let project_paths = item
2825                    .and_then(|item| item.project_path(cx))
2826                    .into_iter()
2827                    .collect::<Vec<_>>();
2828                this.handle_drop(project_paths, vec![], window, cx);
2829            }))
2830            .on_drop(
2831                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2832                    let project_paths = selection
2833                        .items()
2834                        .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
2835                        .collect::<Vec<_>>();
2836                    this.handle_drop(project_paths, vec![], window, cx);
2837                }),
2838            )
2839            .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
2840                let tasks = paths
2841                    .paths()
2842                    .iter()
2843                    .map(|path| {
2844                        Workspace::project_path_for_path(this.project.clone(), path, false, cx)
2845                    })
2846                    .collect::<Vec<_>>();
2847                cx.spawn_in(window, async move |this, cx| {
2848                    let mut paths = vec![];
2849                    let mut added_worktrees = vec![];
2850                    let opened_paths = futures::future::join_all(tasks).await;
2851                    for entry in opened_paths {
2852                        if let Some((worktree, project_path)) = entry.log_err() {
2853                            added_worktrees.push(worktree);
2854                            paths.push(project_path);
2855                        }
2856                    }
2857                    this.update_in(cx, |this, window, cx| {
2858                        this.handle_drop(paths, added_worktrees, window, cx);
2859                    })
2860                    .ok();
2861                })
2862                .detach();
2863            }))
2864    }
2865
2866    fn handle_drop(
2867        &mut self,
2868        paths: Vec<ProjectPath>,
2869        added_worktrees: Vec<Entity<Worktree>>,
2870        window: &mut Window,
2871        cx: &mut Context<Self>,
2872    ) {
2873        match &self.active_view {
2874            ActiveView::AgentThread { thread_view } => {
2875                thread_view.update(cx, |thread_view, cx| {
2876                    thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
2877                });
2878            }
2879            ActiveView::TextThread {
2880                text_thread_editor, ..
2881            } => {
2882                text_thread_editor.update(cx, |text_thread_editor, cx| {
2883                    TextThreadEditor::insert_dragged_files(
2884                        text_thread_editor,
2885                        paths,
2886                        added_worktrees,
2887                        window,
2888                        cx,
2889                    );
2890                });
2891            }
2892            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
2893        }
2894    }
2895
2896    fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
2897        if !self.show_trust_workspace_message {
2898            return None;
2899        }
2900
2901        let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
2902
2903        Some(
2904            Callout::new()
2905                .icon(IconName::Warning)
2906                .severity(Severity::Warning)
2907                .border_position(ui::BorderPosition::Bottom)
2908                .title("You're in Restricted Mode")
2909                .description(description)
2910                .actions_slot(
2911                    Button::new("open-trust-modal", "Configure Project Trust")
2912                        .label_size(LabelSize::Small)
2913                        .style(ButtonStyle::Outlined)
2914                        .on_click({
2915                            cx.listener(move |this, _, window, cx| {
2916                                this.workspace
2917                                    .update(cx, |workspace, cx| {
2918                                        workspace
2919                                            .show_worktree_trust_security_modal(true, window, cx)
2920                                    })
2921                                    .log_err();
2922                            })
2923                        }),
2924                ),
2925        )
2926    }
2927
2928    fn key_context(&self) -> KeyContext {
2929        let mut key_context = KeyContext::new_with_defaults();
2930        key_context.add("AgentPanel");
2931        match &self.active_view {
2932            ActiveView::AgentThread { .. } => key_context.add("acp_thread"),
2933            ActiveView::TextThread { .. } => key_context.add("text_thread"),
2934            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
2935        }
2936        key_context
2937    }
2938}
2939
2940impl Render for AgentPanel {
2941    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2942        // WARNING: Changes to this element hierarchy can have
2943        // non-obvious implications to the layout of children.
2944        //
2945        // If you need to change it, please confirm:
2946        // - The message editor expands (cmd-option-esc) correctly
2947        // - When expanded, the buttons at the bottom of the panel are displayed correctly
2948        // - Font size works as expected and can be changed with cmd-+/cmd-
2949        // - Scrolling in all views works as expected
2950        // - Files can be dropped into the panel
2951        let content = v_flex()
2952            .relative()
2953            .size_full()
2954            .justify_between()
2955            .key_context(self.key_context())
2956            .on_action(cx.listener(|this, action: &NewThread, window, cx| {
2957                this.new_thread(action, window, cx);
2958            }))
2959            .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
2960                this.open_history(window, cx);
2961            }))
2962            .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
2963                this.open_configuration(window, cx);
2964            }))
2965            .on_action(cx.listener(Self::open_active_thread_as_markdown))
2966            .on_action(cx.listener(Self::deploy_rules_library))
2967            .on_action(cx.listener(Self::go_back))
2968            .on_action(cx.listener(Self::toggle_navigation_menu))
2969            .on_action(cx.listener(Self::toggle_options_menu))
2970            .on_action(cx.listener(Self::increase_font_size))
2971            .on_action(cx.listener(Self::decrease_font_size))
2972            .on_action(cx.listener(Self::reset_font_size))
2973            .on_action(cx.listener(Self::toggle_zoom))
2974            .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
2975                if let Some(thread_view) = this.active_thread_view() {
2976                    thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
2977                }
2978            }))
2979            .child(self.render_toolbar(window, cx))
2980            .children(self.render_workspace_trust_message(cx))
2981            .children(self.render_onboarding(window, cx))
2982            .map(|parent| match &self.active_view {
2983                ActiveView::Uninitialized => parent,
2984                ActiveView::AgentThread { thread_view, .. } => parent
2985                    .child(thread_view.clone())
2986                    .child(self.render_drag_target(cx)),
2987                ActiveView::History { kind } => match kind {
2988                    HistoryKind::AgentThreads => parent.child(self.acp_history.clone()),
2989                    HistoryKind::TextThreads => parent.child(self.text_thread_history.clone()),
2990                },
2991                ActiveView::TextThread {
2992                    text_thread_editor,
2993                    buffer_search_bar,
2994                    ..
2995                } => {
2996                    let model_registry = LanguageModelRegistry::read_global(cx);
2997                    let configuration_error =
2998                        model_registry.configuration_error(model_registry.default_model(), cx);
2999                    parent
3000                        .map(|this| {
3001                            if !self.should_render_onboarding(cx)
3002                                && let Some(err) = configuration_error.as_ref()
3003                            {
3004                                this.child(self.render_configuration_error(
3005                                    true,
3006                                    err,
3007                                    &self.focus_handle(cx),
3008                                    cx,
3009                                ))
3010                            } else {
3011                                this
3012                            }
3013                        })
3014                        .child(self.render_text_thread(
3015                            text_thread_editor,
3016                            buffer_search_bar,
3017                            window,
3018                            cx,
3019                        ))
3020                }
3021                ActiveView::Configuration => parent.children(self.configuration.clone()),
3022            })
3023            .children(self.render_trial_end_upsell(window, cx));
3024
3025        match self.active_view.which_font_size_used() {
3026            WhichFontSize::AgentFont => {
3027                WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
3028                    .size_full()
3029                    .child(content)
3030                    .into_any()
3031            }
3032            _ => content.into_any(),
3033        }
3034    }
3035}
3036
3037struct PromptLibraryInlineAssist {
3038    workspace: WeakEntity<Workspace>,
3039}
3040
3041impl PromptLibraryInlineAssist {
3042    pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3043        Self { workspace }
3044    }
3045}
3046
3047impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3048    fn assist(
3049        &self,
3050        prompt_editor: &Entity<Editor>,
3051        initial_prompt: Option<String>,
3052        window: &mut Window,
3053        cx: &mut Context<RulesLibrary>,
3054    ) {
3055        InlineAssistant::update_global(cx, |assistant, cx| {
3056            let Some(workspace) = self.workspace.upgrade() else {
3057                return;
3058            };
3059            let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
3060                return;
3061            };
3062            let project = workspace.read(cx).project().downgrade();
3063            let panel = panel.read(cx);
3064            let thread_store = panel.thread_store().clone();
3065            let history = panel.history().downgrade();
3066            assistant.assist(
3067                prompt_editor,
3068                self.workspace.clone(),
3069                project,
3070                thread_store,
3071                None,
3072                history,
3073                initial_prompt,
3074                window,
3075                cx,
3076            );
3077        })
3078    }
3079
3080    fn focus_agent_panel(
3081        &self,
3082        workspace: &mut Workspace,
3083        window: &mut Window,
3084        cx: &mut Context<Workspace>,
3085    ) -> bool {
3086        workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3087    }
3088}
3089
3090pub struct ConcreteAssistantPanelDelegate;
3091
3092impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3093    fn active_text_thread_editor(
3094        &self,
3095        workspace: &mut Workspace,
3096        _window: &mut Window,
3097        cx: &mut Context<Workspace>,
3098    ) -> Option<Entity<TextThreadEditor>> {
3099        let panel = workspace.panel::<AgentPanel>(cx)?;
3100        panel.read(cx).active_text_thread_editor()
3101    }
3102
3103    fn open_local_text_thread(
3104        &self,
3105        workspace: &mut Workspace,
3106        path: Arc<Path>,
3107        window: &mut Window,
3108        cx: &mut Context<Workspace>,
3109    ) -> Task<Result<()>> {
3110        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3111            return Task::ready(Err(anyhow!("Agent panel not found")));
3112        };
3113
3114        panel.update(cx, |panel, cx| {
3115            panel.open_saved_text_thread(path, window, cx)
3116        })
3117    }
3118
3119    fn open_remote_text_thread(
3120        &self,
3121        _workspace: &mut Workspace,
3122        _text_thread_id: assistant_text_thread::TextThreadId,
3123        _window: &mut Window,
3124        _cx: &mut Context<Workspace>,
3125    ) -> Task<Result<Entity<TextThreadEditor>>> {
3126        Task::ready(Err(anyhow!("opening remote context not implemented")))
3127    }
3128
3129    fn quote_selection(
3130        &self,
3131        workspace: &mut Workspace,
3132        selection_ranges: Vec<Range<Anchor>>,
3133        buffer: Entity<MultiBuffer>,
3134        window: &mut Window,
3135        cx: &mut Context<Workspace>,
3136    ) {
3137        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3138            return;
3139        };
3140
3141        if !panel.focus_handle(cx).contains_focused(window, cx) {
3142            workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3143        }
3144
3145        panel.update(cx, |_, cx| {
3146            // Wait to create a new context until the workspace is no longer
3147            // being updated.
3148            cx.defer_in(window, move |panel, window, cx| {
3149                if let Some(thread_view) = panel.active_thread_view() {
3150                    thread_view.update(cx, |thread_view, cx| {
3151                        thread_view.insert_selections(window, cx);
3152                    });
3153                } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
3154                    let snapshot = buffer.read(cx).snapshot(cx);
3155                    let selection_ranges = selection_ranges
3156                        .into_iter()
3157                        .map(|range| range.to_point(&snapshot))
3158                        .collect::<Vec<_>>();
3159
3160                    text_thread_editor.update(cx, |text_thread_editor, cx| {
3161                        text_thread_editor.quote_ranges(selection_ranges, snapshot, window, cx)
3162                    });
3163                }
3164            });
3165        });
3166    }
3167
3168    fn quote_terminal_text(
3169        &self,
3170        workspace: &mut Workspace,
3171        text: String,
3172        window: &mut Window,
3173        cx: &mut Context<Workspace>,
3174    ) {
3175        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3176            return;
3177        };
3178
3179        if !panel.focus_handle(cx).contains_focused(window, cx) {
3180            workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3181        }
3182
3183        panel.update(cx, |_, cx| {
3184            // Wait to create a new context until the workspace is no longer
3185            // being updated.
3186            cx.defer_in(window, move |panel, window, cx| {
3187                if let Some(thread_view) = panel.active_thread_view() {
3188                    thread_view.update(cx, |thread_view, cx| {
3189                        thread_view.insert_terminal_text(text, window, cx);
3190                    });
3191                } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
3192                    text_thread_editor.update(cx, |text_thread_editor, cx| {
3193                        text_thread_editor.quote_terminal_text(text, window, cx)
3194                    });
3195                }
3196            });
3197        });
3198    }
3199}
3200
3201struct OnboardingUpsell;
3202
3203impl Dismissable for OnboardingUpsell {
3204    const KEY: &'static str = "dismissed-trial-upsell";
3205}
3206
3207struct TrialEndUpsell;
3208
3209impl Dismissable for TrialEndUpsell {
3210    const KEY: &'static str = "dismissed-trial-end-upsell";
3211}
3212
3213#[cfg(feature = "test-support")]
3214impl AgentPanel {
3215    /// Opens an external thread using an arbitrary AgentServer.
3216    ///
3217    /// This is a test-only helper that allows visual tests and integration tests
3218    /// to inject a stub server without modifying production code paths.
3219    /// Not compiled into production builds.
3220    pub fn open_external_thread_with_server(
3221        &mut self,
3222        server: Rc<dyn AgentServer>,
3223        window: &mut Window,
3224        cx: &mut Context<Self>,
3225    ) {
3226        let workspace = self.workspace.clone();
3227        let project = self.project.clone();
3228
3229        let ext_agent = ExternalAgent::Custom {
3230            name: server.name(),
3231        };
3232
3233        self._external_thread(
3234            server, None, None, workspace, project, ext_agent, window, cx,
3235        );
3236    }
3237
3238    /// Returns the currently active thread view, if any.
3239    ///
3240    /// This is a test-only accessor that exposes the private `active_thread_view()`
3241    /// method for test assertions. Not compiled into production builds.
3242    pub fn active_thread_view_for_tests(&self) -> Option<&Entity<AcpServerView>> {
3243        self.active_thread_view()
3244    }
3245}