agent_panel.rs

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