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