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