agent_panel.rs

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