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