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