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