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