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            Self::show_deferred_toast(&self.workspace, "No active native thread to copy", cx);
1213            return;
1214        };
1215
1216        let workspace = self.workspace.clone();
1217        let load_task = thread.read(cx).to_db(cx);
1218
1219        cx.spawn_in(window, async move |_this, cx| {
1220            let db_thread = load_task.await;
1221            let shared_thread = SharedThread::from_db_thread(&db_thread);
1222            let thread_data = shared_thread.to_bytes()?;
1223            let encoded = base64::Engine::encode(&base64::prelude::BASE64_STANDARD, &thread_data);
1224
1225            cx.update(|_window, cx| {
1226                cx.write_to_clipboard(ClipboardItem::new_string(encoded));
1227                if let Some(workspace) = workspace.upgrade() {
1228                    workspace.update(cx, |workspace, cx| {
1229                        struct ThreadCopiedToast;
1230                        workspace.show_toast(
1231                            workspace::Toast::new(
1232                                workspace::notifications::NotificationId::unique::<ThreadCopiedToast>(),
1233                                "Thread copied to clipboard (base64 encoded)",
1234                            )
1235                            .autohide(),
1236                            cx,
1237                        );
1238                    });
1239                }
1240            })?;
1241
1242            anyhow::Ok(())
1243        })
1244        .detach_and_log_err(cx);
1245    }
1246
1247    fn show_deferred_toast(
1248        workspace: &WeakEntity<workspace::Workspace>,
1249        message: &'static str,
1250        cx: &mut App,
1251    ) {
1252        let workspace = workspace.clone();
1253        cx.defer(move |cx| {
1254            if let Some(workspace) = workspace.upgrade() {
1255                workspace.update(cx, |workspace, cx| {
1256                    struct ClipboardToast;
1257                    workspace.show_toast(
1258                        workspace::Toast::new(
1259                            workspace::notifications::NotificationId::unique::<ClipboardToast>(),
1260                            message,
1261                        )
1262                        .autohide(),
1263                        cx,
1264                    );
1265                });
1266            }
1267        });
1268    }
1269
1270    fn load_thread_from_clipboard(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1271        let Some(clipboard) = cx.read_from_clipboard() else {
1272            Self::show_deferred_toast(&self.workspace, "No clipboard content available", cx);
1273            return;
1274        };
1275
1276        let Some(encoded) = clipboard.text() else {
1277            Self::show_deferred_toast(&self.workspace, "Clipboard does not contain text", cx);
1278            return;
1279        };
1280
1281        let thread_data = match base64::Engine::decode(&base64::prelude::BASE64_STANDARD, &encoded)
1282        {
1283            Ok(data) => data,
1284            Err(_) => {
1285                Self::show_deferred_toast(
1286                    &self.workspace,
1287                    "Failed to decode clipboard content (expected base64)",
1288                    cx,
1289                );
1290                return;
1291            }
1292        };
1293
1294        let shared_thread = match SharedThread::from_bytes(&thread_data) {
1295            Ok(thread) => thread,
1296            Err(_) => {
1297                Self::show_deferred_toast(
1298                    &self.workspace,
1299                    "Failed to parse thread data from clipboard",
1300                    cx,
1301                );
1302                return;
1303            }
1304        };
1305
1306        let db_thread = shared_thread.to_db_thread();
1307        let session_id = acp::SessionId::new(uuid::Uuid::new_v4().to_string());
1308        let thread_store = self.thread_store.clone();
1309        let title = db_thread.title.clone();
1310        let workspace = self.workspace.clone();
1311
1312        cx.spawn_in(window, async move |this, cx| {
1313            thread_store
1314                .update(&mut cx.clone(), |store, cx| {
1315                    store.save_thread(session_id.clone(), db_thread, cx)
1316                })
1317                .await?;
1318
1319            let thread_metadata = acp_thread::AgentSessionInfo {
1320                session_id,
1321                cwd: None,
1322                title: Some(title),
1323                updated_at: Some(chrono::Utc::now()),
1324                meta: None,
1325            };
1326
1327            this.update_in(cx, |this, window, cx| {
1328                this.open_thread(thread_metadata, window, cx);
1329            })?;
1330
1331            this.update_in(cx, |_, _window, cx| {
1332                if let Some(workspace) = workspace.upgrade() {
1333                    workspace.update(cx, |workspace, cx| {
1334                        struct ThreadLoadedToast;
1335                        workspace.show_toast(
1336                            workspace::Toast::new(
1337                                workspace::notifications::NotificationId::unique::<ThreadLoadedToast>(),
1338                                "Thread loaded from clipboard",
1339                            )
1340                            .autohide(),
1341                            cx,
1342                        );
1343                    });
1344                }
1345            })?;
1346
1347            anyhow::Ok(())
1348        })
1349        .detach_and_log_err(cx);
1350    }
1351
1352    fn handle_agent_configuration_event(
1353        &mut self,
1354        _entity: &Entity<AgentConfiguration>,
1355        event: &AssistantConfigurationEvent,
1356        window: &mut Window,
1357        cx: &mut Context<Self>,
1358    ) {
1359        match event {
1360            AssistantConfigurationEvent::NewThread(provider) => {
1361                if LanguageModelRegistry::read_global(cx)
1362                    .default_model()
1363                    .is_none_or(|model| model.provider.id() != provider.id())
1364                    && let Some(model) = provider.default_model(cx)
1365                {
1366                    update_settings_file(self.fs.clone(), cx, move |settings, _| {
1367                        let provider = model.provider_id().0.to_string();
1368                        let model = model.id().0.to_string();
1369                        settings
1370                            .agent
1371                            .get_or_insert_default()
1372                            .set_model(LanguageModelSelection {
1373                                provider: LanguageModelProviderSetting(provider),
1374                                model,
1375                                enable_thinking: false,
1376                                effort: None,
1377                            })
1378                    });
1379                }
1380
1381                self.new_thread(&NewThread, window, cx);
1382                if let Some((thread, model)) = self
1383                    .active_native_agent_thread(cx)
1384                    .zip(provider.default_model(cx))
1385                {
1386                    thread.update(cx, |thread, cx| {
1387                        thread.set_model(model, cx);
1388                    });
1389                }
1390            }
1391        }
1392    }
1393
1394    pub(crate) fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
1395        match &self.active_view {
1396            ActiveView::AgentThread { thread_view, .. } => thread_view
1397                .read(cx)
1398                .active_thread()
1399                .map(|r| r.read(cx).thread.clone()),
1400            _ => None,
1401        }
1402    }
1403
1404    pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
1405        match &self.active_view {
1406            ActiveView::AgentThread { thread_view, .. } => {
1407                thread_view.read(cx).as_native_thread(cx)
1408            }
1409            _ => None,
1410        }
1411    }
1412
1413    pub(crate) fn active_text_thread_editor(&self) -> Option<Entity<TextThreadEditor>> {
1414        match &self.active_view {
1415            ActiveView::TextThread {
1416                text_thread_editor, ..
1417            } => Some(text_thread_editor.clone()),
1418            _ => None,
1419        }
1420    }
1421
1422    fn set_active_view(
1423        &mut self,
1424        new_view: ActiveView,
1425        focus: bool,
1426        window: &mut Window,
1427        cx: &mut Context<Self>,
1428    ) {
1429        let current_is_uninitialized = matches!(self.active_view, ActiveView::Uninitialized);
1430        let current_is_history = matches!(self.active_view, ActiveView::History { .. });
1431        let new_is_history = matches!(new_view, ActiveView::History { .. });
1432
1433        let current_is_config = matches!(self.active_view, ActiveView::Configuration);
1434        let new_is_config = matches!(new_view, ActiveView::Configuration);
1435
1436        let current_is_special = current_is_history || current_is_config;
1437        let new_is_special = new_is_history || new_is_config;
1438
1439        if current_is_uninitialized || (current_is_special && !new_is_special) {
1440            self.active_view = new_view;
1441        } else if !current_is_special && new_is_special {
1442            self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1443        } else {
1444            if !new_is_special {
1445                self.previous_view = None;
1446            }
1447            self.active_view = new_view;
1448        }
1449
1450        if focus {
1451            self.focus_handle(cx).focus(window, cx);
1452        }
1453    }
1454
1455    fn populate_recently_updated_menu_section(
1456        mut menu: ContextMenu,
1457        panel: Entity<Self>,
1458        kind: HistoryKind,
1459        cx: &mut Context<ContextMenu>,
1460    ) -> ContextMenu {
1461        match kind {
1462            HistoryKind::AgentThreads => {
1463                let entries = panel
1464                    .read(cx)
1465                    .acp_history
1466                    .read(cx)
1467                    .sessions()
1468                    .iter()
1469                    .take(RECENTLY_UPDATED_MENU_LIMIT)
1470                    .cloned()
1471                    .collect::<Vec<_>>();
1472
1473                if entries.is_empty() {
1474                    return menu;
1475                }
1476
1477                menu = menu.header("Recently Updated");
1478
1479                for entry in entries {
1480                    let title = entry
1481                        .title
1482                        .as_ref()
1483                        .filter(|title| !title.is_empty())
1484                        .cloned()
1485                        .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE));
1486
1487                    menu = menu.entry(title, None, {
1488                        let panel = panel.downgrade();
1489                        let entry = entry.clone();
1490                        move |window, cx| {
1491                            let entry = entry.clone();
1492                            panel
1493                                .update(cx, move |this, cx| {
1494                                    this.load_agent_thread(entry.clone(), window, cx);
1495                                })
1496                                .ok();
1497                        }
1498                    });
1499                }
1500            }
1501            HistoryKind::TextThreads => {
1502                let entries = panel
1503                    .read(cx)
1504                    .text_thread_store
1505                    .read(cx)
1506                    .ordered_text_threads()
1507                    .take(RECENTLY_UPDATED_MENU_LIMIT)
1508                    .cloned()
1509                    .collect::<Vec<_>>();
1510
1511                if entries.is_empty() {
1512                    return menu;
1513                }
1514
1515                menu = menu.header("Recent Text Threads");
1516
1517                for entry in entries {
1518                    let title = if entry.title.is_empty() {
1519                        SharedString::new_static(DEFAULT_THREAD_TITLE)
1520                    } else {
1521                        entry.title.clone()
1522                    };
1523
1524                    menu = menu.entry(title, None, {
1525                        let panel = panel.downgrade();
1526                        let entry = entry.clone();
1527                        move |window, cx| {
1528                            let path = entry.path.clone();
1529                            panel
1530                                .update(cx, move |this, cx| {
1531                                    this.open_saved_text_thread(path.clone(), window, cx)
1532                                        .detach_and_log_err(cx);
1533                                })
1534                                .ok();
1535                        }
1536                    });
1537                }
1538            }
1539        }
1540
1541        menu.separator()
1542    }
1543
1544    pub fn selected_agent(&self) -> AgentType {
1545        self.selected_agent.clone()
1546    }
1547
1548    fn selected_external_agent(&self) -> Option<ExternalAgent> {
1549        match &self.selected_agent {
1550            AgentType::NativeAgent => Some(ExternalAgent::NativeAgent),
1551            AgentType::Gemini => Some(ExternalAgent::Gemini),
1552            AgentType::ClaudeAgent => Some(ExternalAgent::ClaudeCode),
1553            AgentType::Codex => Some(ExternalAgent::Codex),
1554            AgentType::Custom { name } => Some(ExternalAgent::Custom { name: name.clone() }),
1555            AgentType::TextThread => None,
1556        }
1557    }
1558
1559    fn sync_agent_servers_from_extensions(&mut self, cx: &mut Context<Self>) {
1560        if let Some(extension_store) = ExtensionStore::try_global(cx) {
1561            let (manifests, extensions_dir) = {
1562                let store = extension_store.read(cx);
1563                let installed = store.installed_extensions();
1564                let manifests: Vec<_> = installed
1565                    .iter()
1566                    .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
1567                    .collect();
1568                let extensions_dir = paths::extensions_dir().join("installed");
1569                (manifests, extensions_dir)
1570            };
1571
1572            self.project.update(cx, |project, cx| {
1573                project.agent_server_store().update(cx, |store, cx| {
1574                    let manifest_refs: Vec<_> = manifests
1575                        .iter()
1576                        .map(|(id, manifest)| (id.as_ref(), manifest.as_ref()))
1577                        .collect();
1578                    store.sync_extension_agents(manifest_refs, extensions_dir, cx);
1579                });
1580            });
1581        }
1582    }
1583
1584    pub fn new_external_thread_with_text(
1585        &mut self,
1586        initial_text: Option<String>,
1587        window: &mut Window,
1588        cx: &mut Context<Self>,
1589    ) {
1590        self.external_thread(
1591            None,
1592            None,
1593            initial_text.map(ExternalAgentInitialContent::Text),
1594            window,
1595            cx,
1596        );
1597    }
1598
1599    pub fn new_agent_thread(
1600        &mut self,
1601        agent: AgentType,
1602        window: &mut Window,
1603        cx: &mut Context<Self>,
1604    ) {
1605        match agent {
1606            AgentType::TextThread => {
1607                window.dispatch_action(NewTextThread.boxed_clone(), cx);
1608            }
1609            AgentType::NativeAgent => self.external_thread(
1610                Some(crate::ExternalAgent::NativeAgent),
1611                None,
1612                None,
1613                window,
1614                cx,
1615            ),
1616            AgentType::Gemini => {
1617                self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx)
1618            }
1619            AgentType::ClaudeAgent => {
1620                self.selected_agent = AgentType::ClaudeAgent;
1621                self.serialize(cx);
1622                self.external_thread(
1623                    Some(crate::ExternalAgent::ClaudeCode),
1624                    None,
1625                    None,
1626                    window,
1627                    cx,
1628                )
1629            }
1630            AgentType::Codex => {
1631                self.selected_agent = AgentType::Codex;
1632                self.serialize(cx);
1633                self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx)
1634            }
1635            AgentType::Custom { name } => self.external_thread(
1636                Some(crate::ExternalAgent::Custom { name }),
1637                None,
1638                None,
1639                window,
1640                cx,
1641            ),
1642        }
1643    }
1644
1645    pub fn load_agent_thread(
1646        &mut self,
1647        thread: AgentSessionInfo,
1648        window: &mut Window,
1649        cx: &mut Context<Self>,
1650    ) {
1651        let Some(agent) = self.selected_external_agent() else {
1652            return;
1653        };
1654        self.external_thread(Some(agent), Some(thread), None, window, cx);
1655    }
1656
1657    fn _external_thread(
1658        &mut self,
1659        server: Rc<dyn AgentServer>,
1660        resume_thread: Option<AgentSessionInfo>,
1661        initial_content: Option<ExternalAgentInitialContent>,
1662        workspace: WeakEntity<Workspace>,
1663        project: Entity<Project>,
1664        ext_agent: ExternalAgent,
1665        window: &mut Window,
1666        cx: &mut Context<Self>,
1667    ) {
1668        let selected_agent = AgentType::from(ext_agent);
1669        if self.selected_agent != selected_agent {
1670            self.selected_agent = selected_agent;
1671            self.serialize(cx);
1672        }
1673        let thread_store = server
1674            .clone()
1675            .downcast::<agent::NativeAgentServer>()
1676            .is_some()
1677            .then(|| self.thread_store.clone());
1678
1679        let thread_view = cx.new(|cx| {
1680            crate::acp::AcpServerView::new(
1681                server,
1682                resume_thread,
1683                initial_content,
1684                workspace.clone(),
1685                project,
1686                thread_store,
1687                self.prompt_store.clone(),
1688                self.acp_history.clone(),
1689                window,
1690                cx,
1691            )
1692        });
1693
1694        self.set_active_view(ActiveView::AgentThread { thread_view }, true, window, cx);
1695    }
1696}
1697
1698impl Focusable for AgentPanel {
1699    fn focus_handle(&self, cx: &App) -> FocusHandle {
1700        match &self.active_view {
1701            ActiveView::Uninitialized => self.focus_handle.clone(),
1702            ActiveView::AgentThread { thread_view, .. } => thread_view.focus_handle(cx),
1703            ActiveView::History { kind } => match kind {
1704                HistoryKind::AgentThreads => self.acp_history.focus_handle(cx),
1705                HistoryKind::TextThreads => self.text_thread_history.focus_handle(cx),
1706            },
1707            ActiveView::TextThread {
1708                text_thread_editor, ..
1709            } => text_thread_editor.focus_handle(cx),
1710            ActiveView::Configuration => {
1711                if let Some(configuration) = self.configuration.as_ref() {
1712                    configuration.focus_handle(cx)
1713                } else {
1714                    self.focus_handle.clone()
1715                }
1716            }
1717        }
1718    }
1719}
1720
1721fn agent_panel_dock_position(cx: &App) -> DockPosition {
1722    AgentSettings::get_global(cx).dock.into()
1723}
1724
1725impl EventEmitter<PanelEvent> for AgentPanel {}
1726
1727impl Panel for AgentPanel {
1728    fn persistent_name() -> &'static str {
1729        "AgentPanel"
1730    }
1731
1732    fn panel_key() -> &'static str {
1733        AGENT_PANEL_KEY
1734    }
1735
1736    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1737        agent_panel_dock_position(cx)
1738    }
1739
1740    fn position_is_valid(&self, position: DockPosition) -> bool {
1741        position != DockPosition::Bottom
1742    }
1743
1744    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1745        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1746            settings
1747                .agent
1748                .get_or_insert_default()
1749                .set_dock(position.into());
1750        });
1751    }
1752
1753    fn size(&self, window: &Window, cx: &App) -> Pixels {
1754        let settings = AgentSettings::get_global(cx);
1755        match self.position(window, cx) {
1756            DockPosition::Left | DockPosition::Right => {
1757                self.width.unwrap_or(settings.default_width)
1758            }
1759            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1760        }
1761    }
1762
1763    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1764        match self.position(window, cx) {
1765            DockPosition::Left | DockPosition::Right => self.width = size,
1766            DockPosition::Bottom => self.height = size,
1767        }
1768        self.serialize(cx);
1769        cx.notify();
1770    }
1771
1772    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1773        if active && matches!(self.active_view, ActiveView::Uninitialized) {
1774            let selected_agent = self.selected_agent.clone();
1775            self.new_agent_thread(selected_agent, window, cx);
1776        }
1777    }
1778
1779    fn remote_id() -> Option<proto::PanelId> {
1780        Some(proto::PanelId::AssistantPanel)
1781    }
1782
1783    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
1784        (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
1785    }
1786
1787    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
1788        Some("Agent Panel")
1789    }
1790
1791    fn toggle_action(&self) -> Box<dyn Action> {
1792        Box::new(ToggleFocus)
1793    }
1794
1795    fn activation_priority(&self) -> u32 {
1796        3
1797    }
1798
1799    fn enabled(&self, cx: &App) -> bool {
1800        AgentSettings::get_global(cx).enabled(cx)
1801    }
1802
1803    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
1804        self.zoomed
1805    }
1806
1807    fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
1808        self.zoomed = zoomed;
1809        cx.notify();
1810    }
1811}
1812
1813impl AgentPanel {
1814    fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
1815        const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
1816
1817        let content = match &self.active_view {
1818            ActiveView::AgentThread { thread_view } => {
1819                let is_generating_title = thread_view
1820                    .read(cx)
1821                    .as_native_thread(cx)
1822                    .map_or(false, |t| t.read(cx).is_generating_title());
1823
1824                if let Some(title_editor) = thread_view
1825                    .read(cx)
1826                    .parent_thread(cx)
1827                    .and_then(|r| r.read(cx).title_editor.clone())
1828                {
1829                    let container = div()
1830                        .w_full()
1831                        .on_action({
1832                            let thread_view = thread_view.downgrade();
1833                            move |_: &menu::Confirm, window, cx| {
1834                                if let Some(thread_view) = thread_view.upgrade() {
1835                                    thread_view.focus_handle(cx).focus(window, cx);
1836                                }
1837                            }
1838                        })
1839                        .on_action({
1840                            let thread_view = thread_view.downgrade();
1841                            move |_: &editor::actions::Cancel, window, cx| {
1842                                if let Some(thread_view) = thread_view.upgrade() {
1843                                    thread_view.focus_handle(cx).focus(window, cx);
1844                                }
1845                            }
1846                        })
1847                        .child(title_editor);
1848
1849                    if is_generating_title {
1850                        container
1851                            .with_animation(
1852                                "generating_title",
1853                                Animation::new(Duration::from_secs(2))
1854                                    .repeat()
1855                                    .with_easing(pulsating_between(0.4, 0.8)),
1856                                |div, delta| div.opacity(delta),
1857                            )
1858                            .into_any_element()
1859                    } else {
1860                        container.into_any_element()
1861                    }
1862                } else {
1863                    Label::new(thread_view.read(cx).title(cx))
1864                        .color(Color::Muted)
1865                        .truncate()
1866                        .into_any_element()
1867                }
1868            }
1869            ActiveView::TextThread {
1870                title_editor,
1871                text_thread_editor,
1872                ..
1873            } => {
1874                let summary = text_thread_editor.read(cx).text_thread().read(cx).summary();
1875
1876                match summary {
1877                    TextThreadSummary::Pending => Label::new(TextThreadSummary::DEFAULT)
1878                        .color(Color::Muted)
1879                        .truncate()
1880                        .into_any_element(),
1881                    TextThreadSummary::Content(summary) => {
1882                        if summary.done {
1883                            div()
1884                                .w_full()
1885                                .child(title_editor.clone())
1886                                .into_any_element()
1887                        } else {
1888                            Label::new(LOADING_SUMMARY_PLACEHOLDER)
1889                                .truncate()
1890                                .color(Color::Muted)
1891                                .with_animation(
1892                                    "generating_title",
1893                                    Animation::new(Duration::from_secs(2))
1894                                        .repeat()
1895                                        .with_easing(pulsating_between(0.4, 0.8)),
1896                                    |label, delta| label.alpha(delta),
1897                                )
1898                                .into_any_element()
1899                        }
1900                    }
1901                    TextThreadSummary::Error => h_flex()
1902                        .w_full()
1903                        .child(title_editor.clone())
1904                        .child(
1905                            IconButton::new("retry-summary-generation", IconName::RotateCcw)
1906                                .icon_size(IconSize::Small)
1907                                .on_click({
1908                                    let text_thread_editor = text_thread_editor.clone();
1909                                    move |_, _window, cx| {
1910                                        text_thread_editor.update(cx, |text_thread_editor, cx| {
1911                                            text_thread_editor.regenerate_summary(cx);
1912                                        });
1913                                    }
1914                                })
1915                                .tooltip(move |_window, cx| {
1916                                    cx.new(|_| {
1917                                        Tooltip::new("Failed to generate title")
1918                                            .meta("Click to try again")
1919                                    })
1920                                    .into()
1921                                }),
1922                        )
1923                        .into_any_element(),
1924                }
1925            }
1926            ActiveView::History { kind } => {
1927                let title = match kind {
1928                    HistoryKind::AgentThreads => "History",
1929                    HistoryKind::TextThreads => "Text Thread History",
1930                };
1931                Label::new(title).truncate().into_any_element()
1932            }
1933            ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
1934            ActiveView::Uninitialized => Label::new("Agent").truncate().into_any_element(),
1935        };
1936
1937        h_flex()
1938            .key_context("TitleEditor")
1939            .id("TitleEditor")
1940            .flex_grow()
1941            .w_full()
1942            .max_w_full()
1943            .overflow_x_scroll()
1944            .child(content)
1945            .into_any()
1946    }
1947
1948    fn handle_regenerate_thread_title(thread_view: Entity<AcpServerView>, cx: &mut App) {
1949        thread_view.update(cx, |thread_view, cx| {
1950            if let Some(thread) = thread_view.as_native_thread(cx) {
1951                thread.update(cx, |thread, cx| {
1952                    thread.generate_title(cx);
1953                });
1954            }
1955        });
1956    }
1957
1958    fn handle_regenerate_text_thread_title(
1959        text_thread_editor: Entity<TextThreadEditor>,
1960        cx: &mut App,
1961    ) {
1962        text_thread_editor.update(cx, |text_thread_editor, cx| {
1963            text_thread_editor.regenerate_summary(cx);
1964        });
1965    }
1966
1967    fn render_panel_options_menu(
1968        &self,
1969        window: &mut Window,
1970        cx: &mut Context<Self>,
1971    ) -> impl IntoElement {
1972        let focus_handle = self.focus_handle(cx);
1973
1974        let full_screen_label = if self.is_zoomed(window, cx) {
1975            "Disable Full Screen"
1976        } else {
1977            "Enable Full Screen"
1978        };
1979
1980        let selected_agent = self.selected_agent.clone();
1981
1982        let text_thread_view = match &self.active_view {
1983            ActiveView::TextThread {
1984                text_thread_editor, ..
1985            } => Some(text_thread_editor.clone()),
1986            _ => None,
1987        };
1988        let text_thread_with_messages = match &self.active_view {
1989            ActiveView::TextThread {
1990                text_thread_editor, ..
1991            } => text_thread_editor
1992                .read(cx)
1993                .text_thread()
1994                .read(cx)
1995                .messages(cx)
1996                .any(|message| message.role == language_model::Role::Assistant),
1997            _ => false,
1998        };
1999
2000        let thread_view = match &self.active_view {
2001            ActiveView::AgentThread { thread_view } => Some(thread_view.clone()),
2002            _ => None,
2003        };
2004        let thread_with_messages = match &self.active_view {
2005            ActiveView::AgentThread { thread_view } => {
2006                thread_view.read(cx).has_user_submitted_prompt(cx)
2007            }
2008            _ => false,
2009        };
2010
2011        PopoverMenu::new("agent-options-menu")
2012            .trigger_with_tooltip(
2013                IconButton::new("agent-options-menu", IconName::Ellipsis)
2014                    .icon_size(IconSize::Small),
2015                {
2016                    let focus_handle = focus_handle.clone();
2017                    move |_window, cx| {
2018                        Tooltip::for_action_in(
2019                            "Toggle Agent Menu",
2020                            &ToggleOptionsMenu,
2021                            &focus_handle,
2022                            cx,
2023                        )
2024                    }
2025                },
2026            )
2027            .anchor(Corner::TopRight)
2028            .with_handle(self.agent_panel_menu_handle.clone())
2029            .menu({
2030                move |window, cx| {
2031                    Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
2032                        menu = menu.context(focus_handle.clone());
2033
2034                        if thread_with_messages | text_thread_with_messages {
2035                            menu = menu.header("Current Thread");
2036
2037                            if let Some(text_thread_view) = text_thread_view.as_ref() {
2038                                menu = menu
2039                                    .entry("Regenerate Thread Title", None, {
2040                                        let text_thread_view = text_thread_view.clone();
2041                                        move |_, cx| {
2042                                            Self::handle_regenerate_text_thread_title(
2043                                                text_thread_view.clone(),
2044                                                cx,
2045                                            );
2046                                        }
2047                                    })
2048                                    .separator();
2049                            }
2050
2051                            if let Some(thread_view) = thread_view.as_ref() {
2052                                menu = menu
2053                                    .entry("Regenerate Thread Title", None, {
2054                                        let thread_view = thread_view.clone();
2055                                        move |_, cx| {
2056                                            Self::handle_regenerate_thread_title(
2057                                                thread_view.clone(),
2058                                                cx,
2059                                            );
2060                                        }
2061                                    })
2062                                    .separator();
2063                            }
2064                        }
2065
2066                        menu = menu
2067                            .header("MCP Servers")
2068                            .action(
2069                                "View Server Extensions",
2070                                Box::new(zed_actions::Extensions {
2071                                    category_filter: Some(
2072                                        zed_actions::ExtensionCategoryFilter::ContextServers,
2073                                    ),
2074                                    id: None,
2075                                }),
2076                            )
2077                            .action("Add Custom Server…", Box::new(AddContextServer))
2078                            .separator()
2079                            .action("Rules", Box::new(OpenRulesLibrary::default()))
2080                            .action("Profiles", Box::new(ManageProfiles::default()))
2081                            .action("Settings", Box::new(OpenSettings))
2082                            .separator()
2083                            .action(full_screen_label, Box::new(ToggleZoom));
2084
2085                        if selected_agent == AgentType::Gemini {
2086                            menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
2087                        }
2088
2089                        menu
2090                    }))
2091                }
2092            })
2093    }
2094
2095    fn render_recent_entries_menu(
2096        &self,
2097        icon: IconName,
2098        corner: Corner,
2099        cx: &mut Context<Self>,
2100    ) -> impl IntoElement {
2101        let focus_handle = self.focus_handle(cx);
2102
2103        PopoverMenu::new("agent-nav-menu")
2104            .trigger_with_tooltip(
2105                IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
2106                {
2107                    move |_window, cx| {
2108                        Tooltip::for_action_in(
2109                            "Toggle Recently Updated Threads",
2110                            &ToggleNavigationMenu,
2111                            &focus_handle,
2112                            cx,
2113                        )
2114                    }
2115                },
2116            )
2117            .anchor(corner)
2118            .with_handle(self.agent_navigation_menu_handle.clone())
2119            .menu({
2120                let menu = self.agent_navigation_menu.clone();
2121                move |window, cx| {
2122                    telemetry::event!("View Thread History Clicked");
2123
2124                    if let Some(menu) = menu.as_ref() {
2125                        menu.update(cx, |_, cx| {
2126                            cx.defer_in(window, |menu, window, cx| {
2127                                menu.rebuild(window, cx);
2128                            });
2129                        })
2130                    }
2131                    menu.clone()
2132                }
2133            })
2134    }
2135
2136    fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
2137        let focus_handle = self.focus_handle(cx);
2138
2139        IconButton::new("go-back", IconName::ArrowLeft)
2140            .icon_size(IconSize::Small)
2141            .on_click(cx.listener(|this, _, window, cx| {
2142                this.go_back(&workspace::GoBack, window, cx);
2143            }))
2144            .tooltip({
2145                move |_window, cx| {
2146                    Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
2147                }
2148            })
2149    }
2150
2151    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2152        let agent_server_store = self.project.read(cx).agent_server_store().clone();
2153        let focus_handle = self.focus_handle(cx);
2154
2155        let (selected_agent_custom_icon, selected_agent_label) =
2156            if let AgentType::Custom { name, .. } = &self.selected_agent {
2157                let store = agent_server_store.read(cx);
2158                let icon = store.agent_icon(&ExternalAgentServerName(name.clone()));
2159
2160                let label = store
2161                    .agent_display_name(&ExternalAgentServerName(name.clone()))
2162                    .unwrap_or_else(|| self.selected_agent.label());
2163                (icon, label)
2164            } else {
2165                (None, self.selected_agent.label())
2166            };
2167
2168        let active_thread = match &self.active_view {
2169            ActiveView::AgentThread { thread_view } => thread_view.read(cx).as_native_thread(cx),
2170            ActiveView::Uninitialized
2171            | ActiveView::TextThread { .. }
2172            | ActiveView::History { .. }
2173            | ActiveView::Configuration => None,
2174        };
2175
2176        let new_thread_menu = PopoverMenu::new("new_thread_menu")
2177            .trigger_with_tooltip(
2178                IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2179                {
2180                    let focus_handle = focus_handle.clone();
2181                    move |_window, cx| {
2182                        Tooltip::for_action_in(
2183                            "New Thread…",
2184                            &ToggleNewThreadMenu,
2185                            &focus_handle,
2186                            cx,
2187                        )
2188                    }
2189                },
2190            )
2191            .anchor(Corner::TopRight)
2192            .with_handle(self.new_thread_menu_handle.clone())
2193            .menu({
2194                let selected_agent = self.selected_agent.clone();
2195                let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
2196
2197                let workspace = self.workspace.clone();
2198                let is_via_collab = workspace
2199                    .update(cx, |workspace, cx| {
2200                        workspace.project().read(cx).is_via_collab()
2201                    })
2202                    .unwrap_or_default();
2203
2204                move |window, cx| {
2205                    telemetry::event!("New Thread Clicked");
2206
2207                    let active_thread = active_thread.clone();
2208                    Some(ContextMenu::build(window, cx, |menu, _window, cx| {
2209                        menu.context(focus_handle.clone())
2210                            .when_some(active_thread, |this, active_thread| {
2211                                let thread = active_thread.read(cx);
2212
2213                                if !thread.is_empty() {
2214                                    let session_id = thread.id().clone();
2215                                    this.item(
2216                                        ContextMenuEntry::new("New From Summary")
2217                                            .icon(IconName::ThreadFromSummary)
2218                                            .icon_color(Color::Muted)
2219                                            .handler(move |window, cx| {
2220                                                window.dispatch_action(
2221                                                    Box::new(NewNativeAgentThreadFromSummary {
2222                                                        from_session_id: session_id.clone(),
2223                                                    }),
2224                                                    cx,
2225                                                );
2226                                            }),
2227                                    )
2228                                } else {
2229                                    this
2230                                }
2231                            })
2232                            .item(
2233                                ContextMenuEntry::new("Zed Agent")
2234                                    .when(
2235                                        is_agent_selected(AgentType::NativeAgent)
2236                                            | is_agent_selected(AgentType::TextThread),
2237                                        |this| {
2238                                            this.action(Box::new(NewExternalAgentThread {
2239                                                agent: None,
2240                                            }))
2241                                        },
2242                                    )
2243                                    .icon(IconName::ZedAgent)
2244                                    .icon_color(Color::Muted)
2245                                    .handler({
2246                                        let workspace = workspace.clone();
2247                                        move |window, cx| {
2248                                            if let Some(workspace) = workspace.upgrade() {
2249                                                workspace.update(cx, |workspace, cx| {
2250                                                    if let Some(panel) =
2251                                                        workspace.panel::<AgentPanel>(cx)
2252                                                    {
2253                                                        panel.update(cx, |panel, cx| {
2254                                                            panel.new_agent_thread(
2255                                                                AgentType::NativeAgent,
2256                                                                window,
2257                                                                cx,
2258                                                            );
2259                                                        });
2260                                                    }
2261                                                });
2262                                            }
2263                                        }
2264                                    }),
2265                            )
2266                            .item(
2267                                ContextMenuEntry::new("Text Thread")
2268                                    .action(NewTextThread.boxed_clone())
2269                                    .icon(IconName::TextThread)
2270                                    .icon_color(Color::Muted)
2271                                    .handler({
2272                                        let workspace = workspace.clone();
2273                                        move |window, cx| {
2274                                            if let Some(workspace) = workspace.upgrade() {
2275                                                workspace.update(cx, |workspace, cx| {
2276                                                    if let Some(panel) =
2277                                                        workspace.panel::<AgentPanel>(cx)
2278                                                    {
2279                                                        panel.update(cx, |panel, cx| {
2280                                                            panel.new_agent_thread(
2281                                                                AgentType::TextThread,
2282                                                                window,
2283                                                                cx,
2284                                                            );
2285                                                        });
2286                                                    }
2287                                                });
2288                                            }
2289                                        }
2290                                    }),
2291                            )
2292                            .separator()
2293                            .header("External Agents")
2294                            .item(
2295                                ContextMenuEntry::new("Claude Agent")
2296                                    .when(is_agent_selected(AgentType::ClaudeAgent), |this| {
2297                                        this.action(Box::new(NewExternalAgentThread {
2298                                            agent: None,
2299                                        }))
2300                                    })
2301                                    .icon(IconName::AiClaude)
2302                                    .disabled(is_via_collab)
2303                                    .icon_color(Color::Muted)
2304                                    .handler({
2305                                        let workspace = workspace.clone();
2306                                        move |window, cx| {
2307                                            if let Some(workspace) = workspace.upgrade() {
2308                                                workspace.update(cx, |workspace, cx| {
2309                                                    if let Some(panel) =
2310                                                        workspace.panel::<AgentPanel>(cx)
2311                                                    {
2312                                                        panel.update(cx, |panel, cx| {
2313                                                            panel.new_agent_thread(
2314                                                                AgentType::ClaudeAgent,
2315                                                                window,
2316                                                                cx,
2317                                                            );
2318                                                        });
2319                                                    }
2320                                                });
2321                                            }
2322                                        }
2323                                    }),
2324                            )
2325                            .item(
2326                                ContextMenuEntry::new("Codex CLI")
2327                                    .when(is_agent_selected(AgentType::Codex), |this| {
2328                                        this.action(Box::new(NewExternalAgentThread {
2329                                            agent: None,
2330                                        }))
2331                                    })
2332                                    .icon(IconName::AiOpenAi)
2333                                    .disabled(is_via_collab)
2334                                    .icon_color(Color::Muted)
2335                                    .handler({
2336                                        let workspace = workspace.clone();
2337                                        move |window, cx| {
2338                                            if let Some(workspace) = workspace.upgrade() {
2339                                                workspace.update(cx, |workspace, cx| {
2340                                                    if let Some(panel) =
2341                                                        workspace.panel::<AgentPanel>(cx)
2342                                                    {
2343                                                        panel.update(cx, |panel, cx| {
2344                                                            panel.new_agent_thread(
2345                                                                AgentType::Codex,
2346                                                                window,
2347                                                                cx,
2348                                                            );
2349                                                        });
2350                                                    }
2351                                                });
2352                                            }
2353                                        }
2354                                    }),
2355                            )
2356                            .item(
2357                                ContextMenuEntry::new("Gemini CLI")
2358                                    .when(is_agent_selected(AgentType::Gemini), |this| {
2359                                        this.action(Box::new(NewExternalAgentThread {
2360                                            agent: None,
2361                                        }))
2362                                    })
2363                                    .icon(IconName::AiGemini)
2364                                    .icon_color(Color::Muted)
2365                                    .disabled(is_via_collab)
2366                                    .handler({
2367                                        let workspace = workspace.clone();
2368                                        move |window, cx| {
2369                                            if let Some(workspace) = workspace.upgrade() {
2370                                                workspace.update(cx, |workspace, cx| {
2371                                                    if let Some(panel) =
2372                                                        workspace.panel::<AgentPanel>(cx)
2373                                                    {
2374                                                        panel.update(cx, |panel, cx| {
2375                                                            panel.new_agent_thread(
2376                                                                AgentType::Gemini,
2377                                                                window,
2378                                                                cx,
2379                                                            );
2380                                                        });
2381                                                    }
2382                                                });
2383                                            }
2384                                        }
2385                                    }),
2386                            )
2387                            .map(|mut menu| {
2388                                let agent_server_store = agent_server_store.read(cx);
2389                                let agent_names = agent_server_store
2390                                    .external_agents()
2391                                    .filter(|name| {
2392                                        name.0 != GEMINI_NAME
2393                                            && name.0 != CLAUDE_AGENT_NAME
2394                                            && name.0 != CODEX_NAME
2395                                    })
2396                                    .cloned()
2397                                    .collect::<Vec<_>>();
2398
2399                                for agent_name in agent_names {
2400                                    let icon_path = agent_server_store.agent_icon(&agent_name);
2401                                    let display_name = agent_server_store
2402                                        .agent_display_name(&agent_name)
2403                                        .unwrap_or_else(|| agent_name.0.clone());
2404
2405                                    let mut entry = ContextMenuEntry::new(display_name);
2406
2407                                    if let Some(icon_path) = icon_path {
2408                                        entry = entry.custom_icon_svg(icon_path);
2409                                    } else {
2410                                        entry = entry.icon(IconName::Sparkle);
2411                                    }
2412                                    entry = entry
2413                                        .when(
2414                                            is_agent_selected(AgentType::Custom {
2415                                                name: agent_name.0.clone(),
2416                                            }),
2417                                            |this| {
2418                                                this.action(Box::new(NewExternalAgentThread {
2419                                                    agent: None,
2420                                                }))
2421                                            },
2422                                        )
2423                                        .icon_color(Color::Muted)
2424                                        .disabled(is_via_collab)
2425                                        .handler({
2426                                            let workspace = workspace.clone();
2427                                            let agent_name = agent_name.clone();
2428                                            move |window, cx| {
2429                                                if let Some(workspace) = workspace.upgrade() {
2430                                                    workspace.update(cx, |workspace, cx| {
2431                                                        if let Some(panel) =
2432                                                            workspace.panel::<AgentPanel>(cx)
2433                                                        {
2434                                                            panel.update(cx, |panel, cx| {
2435                                                                panel.new_agent_thread(
2436                                                                    AgentType::Custom {
2437                                                                        name: agent_name
2438                                                                            .clone()
2439                                                                            .into(),
2440                                                                    },
2441                                                                    window,
2442                                                                    cx,
2443                                                                );
2444                                                            });
2445                                                        }
2446                                                    });
2447                                                }
2448                                            }
2449                                        });
2450
2451                                    menu = menu.item(entry);
2452                                }
2453
2454                                menu
2455                            })
2456                            .separator()
2457                            .item(
2458                                ContextMenuEntry::new("Add More Agents")
2459                                    .icon(IconName::Plus)
2460                                    .icon_color(Color::Muted)
2461                                    .handler({
2462                                        move |window, cx| {
2463                                            window.dispatch_action(
2464                                                Box::new(zed_actions::AcpRegistry),
2465                                                cx,
2466                                            )
2467                                        }
2468                                    }),
2469                            )
2470                    }))
2471                }
2472            });
2473
2474        let is_thread_loading = self
2475            .active_thread_view()
2476            .map(|thread| thread.read(cx).is_loading())
2477            .unwrap_or(false);
2478
2479        let has_custom_icon = selected_agent_custom_icon.is_some();
2480
2481        let selected_agent = div()
2482            .id("selected_agent_icon")
2483            .when_some(selected_agent_custom_icon, |this, icon_path| {
2484                this.px_1()
2485                    .child(Icon::from_external_svg(icon_path).color(Color::Muted))
2486            })
2487            .when(!has_custom_icon, |this| {
2488                this.when_some(self.selected_agent.icon(), |this, icon| {
2489                    this.px_1().child(Icon::new(icon).color(Color::Muted))
2490                })
2491            })
2492            .tooltip(move |_, cx| {
2493                Tooltip::with_meta(selected_agent_label.clone(), None, "Selected Agent", cx)
2494            });
2495
2496        let selected_agent = if is_thread_loading {
2497            selected_agent
2498                .with_animation(
2499                    "pulsating-icon",
2500                    Animation::new(Duration::from_secs(1))
2501                        .repeat()
2502                        .with_easing(pulsating_between(0.2, 0.6)),
2503                    |icon, delta| icon.opacity(delta),
2504                )
2505                .into_any_element()
2506        } else {
2507            selected_agent.into_any_element()
2508        };
2509
2510        let show_history_menu = self.history_kind_for_selected_agent(cx).is_some();
2511
2512        h_flex()
2513            .id("agent-panel-toolbar")
2514            .h(Tab::container_height(cx))
2515            .max_w_full()
2516            .flex_none()
2517            .justify_between()
2518            .gap_2()
2519            .bg(cx.theme().colors().tab_bar_background)
2520            .border_b_1()
2521            .border_color(cx.theme().colors().border)
2522            .child(
2523                h_flex()
2524                    .size_full()
2525                    .gap(DynamicSpacing::Base04.rems(cx))
2526                    .pl(DynamicSpacing::Base04.rems(cx))
2527                    .child(match &self.active_view {
2528                        ActiveView::History { .. } | ActiveView::Configuration => {
2529                            self.render_toolbar_back_button(cx).into_any_element()
2530                        }
2531                        _ => selected_agent.into_any_element(),
2532                    })
2533                    .child(self.render_title_view(window, cx)),
2534            )
2535            .child(
2536                h_flex()
2537                    .flex_none()
2538                    .gap(DynamicSpacing::Base02.rems(cx))
2539                    .pl(DynamicSpacing::Base04.rems(cx))
2540                    .pr(DynamicSpacing::Base06.rems(cx))
2541                    .child(new_thread_menu)
2542                    .when(show_history_menu, |this| {
2543                        this.child(self.render_recent_entries_menu(
2544                            IconName::MenuAltTemp,
2545                            Corner::TopRight,
2546                            cx,
2547                        ))
2548                    })
2549                    .child(self.render_panel_options_menu(window, cx)),
2550            )
2551    }
2552
2553    fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2554        if TrialEndUpsell::dismissed() {
2555            return false;
2556        }
2557
2558        match &self.active_view {
2559            ActiveView::TextThread { .. } => {
2560                if LanguageModelRegistry::global(cx)
2561                    .read(cx)
2562                    .default_model()
2563                    .is_some_and(|model| {
2564                        model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2565                    })
2566                {
2567                    return false;
2568                }
2569            }
2570            ActiveView::Uninitialized
2571            | ActiveView::AgentThread { .. }
2572            | ActiveView::History { .. }
2573            | ActiveView::Configuration => return false,
2574        }
2575
2576        let plan = self.user_store.read(cx).plan();
2577        let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2578
2579        plan.is_some_and(|plan| plan == Plan::ZedFree) && has_previous_trial
2580    }
2581
2582    fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
2583        if OnboardingUpsell::dismissed() {
2584            return false;
2585        }
2586
2587        let user_store = self.user_store.read(cx);
2588
2589        if user_store.plan().is_some_and(|plan| plan == Plan::ZedPro)
2590            && user_store
2591                .subscription_period()
2592                .and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
2593                .is_some_and(|date| date < chrono::Utc::now())
2594        {
2595            OnboardingUpsell::set_dismissed(true, cx);
2596            return false;
2597        }
2598
2599        match &self.active_view {
2600            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
2601                false
2602            }
2603            ActiveView::AgentThread { thread_view, .. }
2604                if thread_view.read(cx).as_native_thread(cx).is_none() =>
2605            {
2606                false
2607            }
2608            _ => {
2609                let history_is_empty = self.acp_history.read(cx).is_empty();
2610
2611                let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
2612                    .visible_providers()
2613                    .iter()
2614                    .any(|provider| {
2615                        provider.is_authenticated(cx)
2616                            && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2617                    });
2618
2619                history_is_empty || !has_configured_non_zed_providers
2620            }
2621        }
2622    }
2623
2624    fn render_onboarding(
2625        &self,
2626        _window: &mut Window,
2627        cx: &mut Context<Self>,
2628    ) -> Option<impl IntoElement> {
2629        if !self.should_render_onboarding(cx) {
2630            return None;
2631        }
2632
2633        let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
2634
2635        Some(
2636            div()
2637                .when(text_thread_view, |this| {
2638                    this.bg(cx.theme().colors().editor_background)
2639                })
2640                .child(self.onboarding.clone()),
2641        )
2642    }
2643
2644    fn render_trial_end_upsell(
2645        &self,
2646        _window: &mut Window,
2647        cx: &mut Context<Self>,
2648    ) -> Option<impl IntoElement> {
2649        if !self.should_render_trial_end_upsell(cx) {
2650            return None;
2651        }
2652
2653        Some(
2654            v_flex()
2655                .absolute()
2656                .inset_0()
2657                .size_full()
2658                .bg(cx.theme().colors().panel_background)
2659                .opacity(0.85)
2660                .block_mouse_except_scroll()
2661                .child(EndTrialUpsell::new(Arc::new({
2662                    let this = cx.entity();
2663                    move |_, cx| {
2664                        this.update(cx, |_this, cx| {
2665                            TrialEndUpsell::set_dismissed(true, cx);
2666                            cx.notify();
2667                        });
2668                    }
2669                }))),
2670        )
2671    }
2672
2673    fn emit_configuration_error_telemetry_if_needed(
2674        &mut self,
2675        configuration_error: Option<&ConfigurationError>,
2676    ) {
2677        let error_kind = configuration_error.map(|err| match err {
2678            ConfigurationError::NoProvider => "no_provider",
2679            ConfigurationError::ModelNotFound => "model_not_found",
2680            ConfigurationError::ProviderNotAuthenticated(_) => "provider_not_authenticated",
2681        });
2682
2683        let error_kind_string = error_kind.map(String::from);
2684
2685        if self.last_configuration_error_telemetry == error_kind_string {
2686            return;
2687        }
2688
2689        self.last_configuration_error_telemetry = error_kind_string;
2690
2691        if let Some(kind) = error_kind {
2692            let message = configuration_error
2693                .map(|err| err.to_string())
2694                .unwrap_or_default();
2695
2696            telemetry::event!("Agent Panel Error Shown", kind = kind, message = message,);
2697        }
2698    }
2699
2700    fn render_configuration_error(
2701        &self,
2702        border_bottom: bool,
2703        configuration_error: &ConfigurationError,
2704        focus_handle: &FocusHandle,
2705        cx: &mut App,
2706    ) -> impl IntoElement {
2707        let zed_provider_configured = AgentSettings::get_global(cx)
2708            .default_model
2709            .as_ref()
2710            .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev");
2711
2712        let callout = if zed_provider_configured {
2713            Callout::new()
2714                .icon(IconName::Warning)
2715                .severity(Severity::Warning)
2716                .when(border_bottom, |this| {
2717                    this.border_position(ui::BorderPosition::Bottom)
2718                })
2719                .title("Sign in to continue using Zed as your LLM provider.")
2720                .actions_slot(
2721                    Button::new("sign_in", "Sign In")
2722                        .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2723                        .label_size(LabelSize::Small)
2724                        .on_click({
2725                            let workspace = self.workspace.clone();
2726                            move |_, _, cx| {
2727                                let Ok(client) =
2728                                    workspace.update(cx, |workspace, _| workspace.client().clone())
2729                                else {
2730                                    return;
2731                                };
2732
2733                                cx.spawn(async move |cx| {
2734                                    client.sign_in_with_optional_connect(true, cx).await
2735                                })
2736                                .detach_and_log_err(cx);
2737                            }
2738                        }),
2739                )
2740        } else {
2741            Callout::new()
2742                .icon(IconName::Warning)
2743                .severity(Severity::Warning)
2744                .when(border_bottom, |this| {
2745                    this.border_position(ui::BorderPosition::Bottom)
2746                })
2747                .title(configuration_error.to_string())
2748                .actions_slot(
2749                    Button::new("settings", "Configure")
2750                        .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2751                        .label_size(LabelSize::Small)
2752                        .key_binding(
2753                            KeyBinding::for_action_in(&OpenSettings, focus_handle, cx)
2754                                .map(|kb| kb.size(rems_from_px(12.))),
2755                        )
2756                        .on_click(|_event, window, cx| {
2757                            window.dispatch_action(OpenSettings.boxed_clone(), cx)
2758                        }),
2759                )
2760        };
2761
2762        match configuration_error {
2763            ConfigurationError::ModelNotFound
2764            | ConfigurationError::ProviderNotAuthenticated(_)
2765            | ConfigurationError::NoProvider => callout.into_any_element(),
2766        }
2767    }
2768
2769    fn render_text_thread(
2770        &self,
2771        text_thread_editor: &Entity<TextThreadEditor>,
2772        buffer_search_bar: &Entity<BufferSearchBar>,
2773        window: &mut Window,
2774        cx: &mut Context<Self>,
2775    ) -> Div {
2776        let mut registrar = buffer_search::DivRegistrar::new(
2777            |this, _, _cx| match &this.active_view {
2778                ActiveView::TextThread {
2779                    buffer_search_bar, ..
2780                } => Some(buffer_search_bar.clone()),
2781                _ => None,
2782            },
2783            cx,
2784        );
2785        BufferSearchBar::register(&mut registrar);
2786        registrar
2787            .into_div()
2788            .size_full()
2789            .relative()
2790            .map(|parent| {
2791                buffer_search_bar.update(cx, |buffer_search_bar, cx| {
2792                    if buffer_search_bar.is_dismissed() {
2793                        return parent;
2794                    }
2795                    parent.child(
2796                        div()
2797                            .p(DynamicSpacing::Base08.rems(cx))
2798                            .border_b_1()
2799                            .border_color(cx.theme().colors().border_variant)
2800                            .bg(cx.theme().colors().editor_background)
2801                            .child(buffer_search_bar.render(window, cx)),
2802                    )
2803                })
2804            })
2805            .child(text_thread_editor.clone())
2806            .child(self.render_drag_target(cx))
2807    }
2808
2809    fn render_drag_target(&self, cx: &Context<Self>) -> Div {
2810        let is_local = self.project.read(cx).is_local();
2811        div()
2812            .invisible()
2813            .absolute()
2814            .top_0()
2815            .right_0()
2816            .bottom_0()
2817            .left_0()
2818            .bg(cx.theme().colors().drop_target_background)
2819            .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
2820            .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
2821            .when(is_local, |this| {
2822                this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
2823            })
2824            .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
2825                let item = tab.pane.read(cx).item_for_index(tab.ix);
2826                let project_paths = item
2827                    .and_then(|item| item.project_path(cx))
2828                    .into_iter()
2829                    .collect::<Vec<_>>();
2830                this.handle_drop(project_paths, vec![], window, cx);
2831            }))
2832            .on_drop(
2833                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
2834                    let project_paths = selection
2835                        .items()
2836                        .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
2837                        .collect::<Vec<_>>();
2838                    this.handle_drop(project_paths, vec![], window, cx);
2839                }),
2840            )
2841            .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
2842                let tasks = paths
2843                    .paths()
2844                    .iter()
2845                    .map(|path| {
2846                        Workspace::project_path_for_path(this.project.clone(), path, false, cx)
2847                    })
2848                    .collect::<Vec<_>>();
2849                cx.spawn_in(window, async move |this, cx| {
2850                    let mut paths = vec![];
2851                    let mut added_worktrees = vec![];
2852                    let opened_paths = futures::future::join_all(tasks).await;
2853                    for entry in opened_paths {
2854                        if let Some((worktree, project_path)) = entry.log_err() {
2855                            added_worktrees.push(worktree);
2856                            paths.push(project_path);
2857                        }
2858                    }
2859                    this.update_in(cx, |this, window, cx| {
2860                        this.handle_drop(paths, added_worktrees, window, cx);
2861                    })
2862                    .ok();
2863                })
2864                .detach();
2865            }))
2866    }
2867
2868    fn handle_drop(
2869        &mut self,
2870        paths: Vec<ProjectPath>,
2871        added_worktrees: Vec<Entity<Worktree>>,
2872        window: &mut Window,
2873        cx: &mut Context<Self>,
2874    ) {
2875        match &self.active_view {
2876            ActiveView::AgentThread { thread_view } => {
2877                thread_view.update(cx, |thread_view, cx| {
2878                    thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
2879                });
2880            }
2881            ActiveView::TextThread {
2882                text_thread_editor, ..
2883            } => {
2884                text_thread_editor.update(cx, |text_thread_editor, cx| {
2885                    TextThreadEditor::insert_dragged_files(
2886                        text_thread_editor,
2887                        paths,
2888                        added_worktrees,
2889                        window,
2890                        cx,
2891                    );
2892                });
2893            }
2894            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
2895        }
2896    }
2897
2898    fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
2899        if !self.show_trust_workspace_message {
2900            return None;
2901        }
2902
2903        let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
2904
2905        Some(
2906            Callout::new()
2907                .icon(IconName::Warning)
2908                .severity(Severity::Warning)
2909                .border_position(ui::BorderPosition::Bottom)
2910                .title("You're in Restricted Mode")
2911                .description(description)
2912                .actions_slot(
2913                    Button::new("open-trust-modal", "Configure Project Trust")
2914                        .label_size(LabelSize::Small)
2915                        .style(ButtonStyle::Outlined)
2916                        .on_click({
2917                            cx.listener(move |this, _, window, cx| {
2918                                this.workspace
2919                                    .update(cx, |workspace, cx| {
2920                                        workspace
2921                                            .show_worktree_trust_security_modal(true, window, cx)
2922                                    })
2923                                    .log_err();
2924                            })
2925                        }),
2926                ),
2927        )
2928    }
2929
2930    fn key_context(&self) -> KeyContext {
2931        let mut key_context = KeyContext::new_with_defaults();
2932        key_context.add("AgentPanel");
2933        match &self.active_view {
2934            ActiveView::AgentThread { .. } => key_context.add("acp_thread"),
2935            ActiveView::TextThread { .. } => key_context.add("text_thread"),
2936            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
2937        }
2938        key_context
2939    }
2940}
2941
2942impl Render for AgentPanel {
2943    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2944        // WARNING: Changes to this element hierarchy can have
2945        // non-obvious implications to the layout of children.
2946        //
2947        // If you need to change it, please confirm:
2948        // - The message editor expands (cmd-option-esc) correctly
2949        // - When expanded, the buttons at the bottom of the panel are displayed correctly
2950        // - Font size works as expected and can be changed with cmd-+/cmd-
2951        // - Scrolling in all views works as expected
2952        // - Files can be dropped into the panel
2953        let content = v_flex()
2954            .relative()
2955            .size_full()
2956            .justify_between()
2957            .key_context(self.key_context())
2958            .on_action(cx.listener(|this, action: &NewThread, window, cx| {
2959                this.new_thread(action, window, cx);
2960            }))
2961            .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
2962                this.open_history(window, cx);
2963            }))
2964            .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
2965                this.open_configuration(window, cx);
2966            }))
2967            .on_action(cx.listener(Self::open_active_thread_as_markdown))
2968            .on_action(cx.listener(Self::deploy_rules_library))
2969            .on_action(cx.listener(Self::go_back))
2970            .on_action(cx.listener(Self::toggle_navigation_menu))
2971            .on_action(cx.listener(Self::toggle_options_menu))
2972            .on_action(cx.listener(Self::increase_font_size))
2973            .on_action(cx.listener(Self::decrease_font_size))
2974            .on_action(cx.listener(Self::reset_font_size))
2975            .on_action(cx.listener(Self::toggle_zoom))
2976            .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
2977                if let Some(thread_view) = this.active_thread_view() {
2978                    thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
2979                }
2980            }))
2981            .child(self.render_toolbar(window, cx))
2982            .children(self.render_workspace_trust_message(cx))
2983            .children(self.render_onboarding(window, cx))
2984            .map(|parent| {
2985                // Emit configuration error telemetry before entering the match to avoid borrow conflicts
2986                if matches!(&self.active_view, ActiveView::TextThread { .. }) {
2987                    let model_registry = LanguageModelRegistry::read_global(cx);
2988                    let configuration_error =
2989                        model_registry.configuration_error(model_registry.default_model(), cx);
2990                    self.emit_configuration_error_telemetry_if_needed(configuration_error.as_ref());
2991                }
2992
2993                match &self.active_view {
2994                    ActiveView::Uninitialized => parent,
2995                    ActiveView::AgentThread { thread_view, .. } => parent
2996                        .child(thread_view.clone())
2997                        .child(self.render_drag_target(cx)),
2998                    ActiveView::History { kind } => match kind {
2999                        HistoryKind::AgentThreads => parent.child(self.acp_history.clone()),
3000                        HistoryKind::TextThreads => parent.child(self.text_thread_history.clone()),
3001                    },
3002                    ActiveView::TextThread {
3003                        text_thread_editor,
3004                        buffer_search_bar,
3005                        ..
3006                    } => {
3007                        let model_registry = LanguageModelRegistry::read_global(cx);
3008                        let configuration_error =
3009                            model_registry.configuration_error(model_registry.default_model(), cx);
3010
3011                        parent
3012                            .map(|this| {
3013                                if !self.should_render_onboarding(cx)
3014                                    && let Some(err) = configuration_error.as_ref()
3015                                {
3016                                    this.child(self.render_configuration_error(
3017                                        true,
3018                                        err,
3019                                        &self.focus_handle(cx),
3020                                        cx,
3021                                    ))
3022                                } else {
3023                                    this
3024                                }
3025                            })
3026                            .child(self.render_text_thread(
3027                                text_thread_editor,
3028                                buffer_search_bar,
3029                                window,
3030                                cx,
3031                            ))
3032                    }
3033                    ActiveView::Configuration => parent.children(self.configuration.clone()),
3034                }
3035            })
3036            .children(self.render_trial_end_upsell(window, cx));
3037
3038        match self.active_view.which_font_size_used() {
3039            WhichFontSize::AgentFont => {
3040                WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
3041                    .size_full()
3042                    .child(content)
3043                    .into_any()
3044            }
3045            _ => content.into_any(),
3046        }
3047    }
3048}
3049
3050struct PromptLibraryInlineAssist {
3051    workspace: WeakEntity<Workspace>,
3052}
3053
3054impl PromptLibraryInlineAssist {
3055    pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3056        Self { workspace }
3057    }
3058}
3059
3060impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3061    fn assist(
3062        &self,
3063        prompt_editor: &Entity<Editor>,
3064        initial_prompt: Option<String>,
3065        window: &mut Window,
3066        cx: &mut Context<RulesLibrary>,
3067    ) {
3068        InlineAssistant::update_global(cx, |assistant, cx| {
3069            let Some(workspace) = self.workspace.upgrade() else {
3070                return;
3071            };
3072            let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
3073                return;
3074            };
3075            let project = workspace.read(cx).project().downgrade();
3076            let panel = panel.read(cx);
3077            let thread_store = panel.thread_store().clone();
3078            let history = panel.history().downgrade();
3079            assistant.assist(
3080                prompt_editor,
3081                self.workspace.clone(),
3082                project,
3083                thread_store,
3084                None,
3085                history,
3086                initial_prompt,
3087                window,
3088                cx,
3089            );
3090        })
3091    }
3092
3093    fn focus_agent_panel(
3094        &self,
3095        workspace: &mut Workspace,
3096        window: &mut Window,
3097        cx: &mut Context<Workspace>,
3098    ) -> bool {
3099        workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3100    }
3101}
3102
3103pub struct ConcreteAssistantPanelDelegate;
3104
3105impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3106    fn active_text_thread_editor(
3107        &self,
3108        workspace: &mut Workspace,
3109        _window: &mut Window,
3110        cx: &mut Context<Workspace>,
3111    ) -> Option<Entity<TextThreadEditor>> {
3112        let panel = workspace.panel::<AgentPanel>(cx)?;
3113        panel.read(cx).active_text_thread_editor()
3114    }
3115
3116    fn open_local_text_thread(
3117        &self,
3118        workspace: &mut Workspace,
3119        path: Arc<Path>,
3120        window: &mut Window,
3121        cx: &mut Context<Workspace>,
3122    ) -> Task<Result<()>> {
3123        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3124            return Task::ready(Err(anyhow!("Agent panel not found")));
3125        };
3126
3127        panel.update(cx, |panel, cx| {
3128            panel.open_saved_text_thread(path, window, cx)
3129        })
3130    }
3131
3132    fn open_remote_text_thread(
3133        &self,
3134        _workspace: &mut Workspace,
3135        _text_thread_id: assistant_text_thread::TextThreadId,
3136        _window: &mut Window,
3137        _cx: &mut Context<Workspace>,
3138    ) -> Task<Result<Entity<TextThreadEditor>>> {
3139        Task::ready(Err(anyhow!("opening remote context not implemented")))
3140    }
3141
3142    fn quote_selection(
3143        &self,
3144        workspace: &mut Workspace,
3145        selection_ranges: Vec<Range<Anchor>>,
3146        buffer: Entity<MultiBuffer>,
3147        window: &mut Window,
3148        cx: &mut Context<Workspace>,
3149    ) {
3150        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3151            return;
3152        };
3153
3154        if !panel.focus_handle(cx).contains_focused(window, cx) {
3155            workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3156        }
3157
3158        panel.update(cx, |_, cx| {
3159            // Wait to create a new context until the workspace is no longer
3160            // being updated.
3161            cx.defer_in(window, move |panel, window, cx| {
3162                if let Some(thread_view) = panel.active_thread_view() {
3163                    thread_view.update(cx, |thread_view, cx| {
3164                        thread_view.insert_selections(window, cx);
3165                    });
3166                } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
3167                    let snapshot = buffer.read(cx).snapshot(cx);
3168                    let selection_ranges = selection_ranges
3169                        .into_iter()
3170                        .map(|range| range.to_point(&snapshot))
3171                        .collect::<Vec<_>>();
3172
3173                    text_thread_editor.update(cx, |text_thread_editor, cx| {
3174                        text_thread_editor.quote_ranges(selection_ranges, snapshot, window, cx)
3175                    });
3176                }
3177            });
3178        });
3179    }
3180
3181    fn quote_terminal_text(
3182        &self,
3183        workspace: &mut Workspace,
3184        text: String,
3185        window: &mut Window,
3186        cx: &mut Context<Workspace>,
3187    ) {
3188        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3189            return;
3190        };
3191
3192        if !panel.focus_handle(cx).contains_focused(window, cx) {
3193            workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3194        }
3195
3196        panel.update(cx, |_, cx| {
3197            // Wait to create a new context until the workspace is no longer
3198            // being updated.
3199            cx.defer_in(window, move |panel, window, cx| {
3200                if let Some(thread_view) = panel.active_thread_view() {
3201                    thread_view.update(cx, |thread_view, cx| {
3202                        thread_view.insert_terminal_text(text, window, cx);
3203                    });
3204                } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
3205                    text_thread_editor.update(cx, |text_thread_editor, cx| {
3206                        text_thread_editor.quote_terminal_text(text, window, cx)
3207                    });
3208                }
3209            });
3210        });
3211    }
3212}
3213
3214struct OnboardingUpsell;
3215
3216impl Dismissable for OnboardingUpsell {
3217    const KEY: &'static str = "dismissed-trial-upsell";
3218}
3219
3220struct TrialEndUpsell;
3221
3222impl Dismissable for TrialEndUpsell {
3223    const KEY: &'static str = "dismissed-trial-end-upsell";
3224}
3225
3226#[cfg(feature = "test-support")]
3227impl AgentPanel {
3228    /// Opens an external thread using an arbitrary AgentServer.
3229    ///
3230    /// This is a test-only helper that allows visual tests and integration tests
3231    /// to inject a stub server without modifying production code paths.
3232    /// Not compiled into production builds.
3233    pub fn open_external_thread_with_server(
3234        &mut self,
3235        server: Rc<dyn AgentServer>,
3236        window: &mut Window,
3237        cx: &mut Context<Self>,
3238    ) {
3239        let workspace = self.workspace.clone();
3240        let project = self.project.clone();
3241
3242        let ext_agent = ExternalAgent::Custom {
3243            name: server.name(),
3244        };
3245
3246        self._external_thread(
3247            server, None, None, workspace, project, ext_agent, window, cx,
3248        );
3249    }
3250
3251    /// Returns the currently active thread view, if any.
3252    ///
3253    /// This is a test-only accessor that exposes the private `active_thread_view()`
3254    /// method for test assertions. Not compiled into production builds.
3255    pub fn active_thread_view_for_tests(&self) -> Option<&Entity<AcpServerView>> {
3256        self.active_thread_view()
3257    }
3258}