agent_panel.rs

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