agent_panel.rs

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