agent_panel.rs

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