agent_panel.rs

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