agent_panel.rs

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