agent_panel.rs

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