agent_panel.rs

   1use std::{
   2    ops::Range,
   3    path::Path,
   4    rc::Rc,
   5    sync::{
   6        Arc,
   7        atomic::{AtomicBool, Ordering},
   8    },
   9    time::Duration,
  10};
  11
  12use acp_thread::{AcpThread, AgentSessionInfo, MentionUri};
  13use agent::{ContextServerRegistry, SharedThread, ThreadStore};
  14use agent_client_protocol as acp;
  15use agent_servers::AgentServer;
  16use db::kvp::{Dismissable, KEY_VALUE_STORE};
  17use project::{
  18    ExternalAgentServerName,
  19    agent_server_store::{CLAUDE_AGENT_NAME, CODEX_NAME, GEMINI_NAME},
  20};
  21use serde::{Deserialize, Serialize};
  22use settings::{LanguageModelProviderSetting, LanguageModelSelection};
  23
  24use zed_actions::agent::{OpenClaudeAgentOnboardingModal, ReauthenticateAgent, ReviewBranchDiff};
  25
  26use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
  27use crate::{
  28    AddContextServer, AgentDiffPane, CopyThreadToClipboard, Follow, InlineAssistant,
  29    LoadThreadFromClipboard, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff,
  30    OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, ToggleNavigationMenu, ToggleNewThreadMenu,
  31    ToggleOptionsMenu,
  32    acp::AcpServerView,
  33    agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
  34    slash_command::SlashCommandCompletionProvider,
  35    text_thread_editor::{AgentPanelDelegate, TextThreadEditor, make_lsp_adapter_delegate},
  36    ui::EndTrialUpsell,
  37};
  38use crate::{
  39    AgentInitialContent, ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary,
  40};
  41use crate::{
  42    ExpandMessageEditor,
  43    acp::{AcpThreadHistory, ThreadHistoryEvent},
  44    text_thread_history::{TextThreadHistory, TextThreadHistoryEvent},
  45};
  46use crate::{ManageProfiles, acp::thread_view::AcpThreadView};
  47use agent_settings::AgentSettings;
  48use ai_onboarding::AgentPanelOnboarding;
  49use anyhow::{Result, anyhow};
  50use assistant_slash_command::SlashCommandWorkingSet;
  51use assistant_text_thread::{TextThread, TextThreadEvent, TextThreadSummary};
  52use client::UserStore;
  53use cloud_api_types::Plan;
  54use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
  55use extension::ExtensionEvents;
  56use extension_host::ExtensionStore;
  57use fs::Fs;
  58use gpui::{
  59    Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner,
  60    DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels,
  61    Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
  62};
  63use language::LanguageRegistry;
  64use language_model::{ConfigurationError, LanguageModelRegistry};
  65use project::{Project, ProjectPath, Worktree};
  66use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
  67use rules_library::{RulesLibrary, open_rules_library};
  68use search::{BufferSearchBar, buffer_search};
  69use settings::{Settings, update_settings_file};
  70use theme::ThemeSettings;
  71use ui::{
  72    Callout, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, Tab,
  73    Tooltip, prelude::*, utils::WithRemSize,
  74};
  75use util::ResultExt as _;
  76use workspace::{
  77    CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
  78    WorkspaceId,
  79    dock::{DockPosition, Panel, PanelEvent},
  80};
  81use zed_actions::{
  82    DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
  83    agent::{OpenAcpOnboardingModal, OpenSettings, ResetAgentZoom, ResetOnboarding},
  84    assistant::{OpenRulesLibrary, Toggle, ToggleFocus},
  85};
  86
  87const AGENT_PANEL_KEY: &str = "agent_panel";
  88const RECENTLY_UPDATED_MENU_LIMIT: usize = 6;
  89const DEFAULT_THREAD_TITLE: &str = "New Thread";
  90
  91fn read_serialized_panel(workspace_id: workspace::WorkspaceId) -> Option<SerializedAgentPanel> {
  92    let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY);
  93    let key = i64::from(workspace_id).to_string();
  94    scope
  95        .read(&key)
  96        .log_err()
  97        .flatten()
  98        .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())
  99}
 100
 101async fn save_serialized_panel(
 102    workspace_id: workspace::WorkspaceId,
 103    panel: SerializedAgentPanel,
 104) -> Result<()> {
 105    let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY);
 106    let key = i64::from(workspace_id).to_string();
 107    scope.write(key, serde_json::to_string(&panel)?).await?;
 108    Ok(())
 109}
 110
 111/// Migration: reads the original single-panel format stored under the
 112/// `"agent_panel"` KVP key before per-workspace keying was introduced.
 113fn read_legacy_serialized_panel() -> Option<SerializedAgentPanel> {
 114    KEY_VALUE_STORE
 115        .read_kvp(AGENT_PANEL_KEY)
 116        .log_err()
 117        .flatten()
 118        .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())
 119}
 120
 121#[derive(Serialize, Deserialize, Debug, Clone)]
 122struct SerializedAgentPanel {
 123    width: Option<Pixels>,
 124    selected_agent: Option<AgentType>,
 125    #[serde(default)]
 126    last_active_thread: Option<SerializedActiveThread>,
 127}
 128
 129#[derive(Serialize, Deserialize, Debug, Clone)]
 130struct SerializedActiveThread {
 131    session_id: String,
 132    agent_type: AgentType,
 133    title: Option<String>,
 134    cwd: Option<std::path::PathBuf>,
 135}
 136
 137pub fn init(cx: &mut App) {
 138    cx.observe_new(
 139        |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
 140            workspace
 141                .register_action(|workspace, action: &NewThread, window, cx| {
 142                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 143                        panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
 144                        workspace.focus_panel::<AgentPanel>(window, cx);
 145                    }
 146                })
 147                .register_action(
 148                    |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| {
 149                        if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 150                            panel.update(cx, |panel, cx| {
 151                                panel.new_native_agent_thread_from_summary(action, window, cx)
 152                            });
 153                            workspace.focus_panel::<AgentPanel>(window, cx);
 154                        }
 155                    },
 156                )
 157                .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
 158                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 159                        workspace.focus_panel::<AgentPanel>(window, cx);
 160                        panel.update(cx, |panel, cx| panel.expand_message_editor(window, cx));
 161                    }
 162                })
 163                .register_action(|workspace, _: &OpenHistory, window, cx| {
 164                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 165                        workspace.focus_panel::<AgentPanel>(window, cx);
 166                        panel.update(cx, |panel, cx| panel.open_history(window, cx));
 167                    }
 168                })
 169                .register_action(|workspace, _: &OpenSettings, window, cx| {
 170                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 171                        workspace.focus_panel::<AgentPanel>(window, cx);
 172                        panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
 173                    }
 174                })
 175                .register_action(|workspace, _: &NewTextThread, window, cx| {
 176                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 177                        workspace.focus_panel::<AgentPanel>(window, cx);
 178                        panel.update(cx, |panel, cx| {
 179                            panel.new_text_thread(window, cx);
 180                        });
 181                    }
 182                })
 183                .register_action(|workspace, action: &NewExternalAgentThread, window, cx| {
 184                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 185                        workspace.focus_panel::<AgentPanel>(window, cx);
 186                        panel.update(cx, |panel, cx| {
 187                            panel.external_thread(action.agent.clone(), None, None, window, cx)
 188                        });
 189                    }
 190                })
 191                .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
 192                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 193                        workspace.focus_panel::<AgentPanel>(window, cx);
 194                        panel.update(cx, |panel, cx| {
 195                            panel.deploy_rules_library(action, window, cx)
 196                        });
 197                    }
 198                })
 199                .register_action(|workspace, _: &Follow, window, cx| {
 200                    workspace.follow(CollaboratorId::Agent, window, cx);
 201                })
 202                .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
 203                    let thread = workspace
 204                        .panel::<AgentPanel>(cx)
 205                        .and_then(|panel| panel.read(cx).active_thread_view().cloned())
 206                        .and_then(|thread_view| {
 207                            thread_view
 208                                .read(cx)
 209                                .active_thread()
 210                                .map(|r| r.read(cx).thread.clone())
 211                        });
 212
 213                    if let Some(thread) = thread {
 214                        AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
 215                    }
 216                })
 217                .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
 218                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 219                        workspace.focus_panel::<AgentPanel>(window, cx);
 220                        panel.update(cx, |panel, cx| {
 221                            panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
 222                        });
 223                    }
 224                })
 225                .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
 226                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 227                        workspace.focus_panel::<AgentPanel>(window, cx);
 228                        panel.update(cx, |panel, cx| {
 229                            panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
 230                        });
 231                    }
 232                })
 233                .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| {
 234                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 235                        workspace.focus_panel::<AgentPanel>(window, cx);
 236                        panel.update(cx, |panel, cx| {
 237                            panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx);
 238                        });
 239                    }
 240                })
 241                .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
 242                    AcpOnboardingModal::toggle(workspace, window, cx)
 243                })
 244                .register_action(
 245                    |workspace, _: &OpenClaudeAgentOnboardingModal, window, cx| {
 246                        ClaudeCodeOnboardingModal::toggle(workspace, window, cx)
 247                    },
 248                )
 249                .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
 250                    window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
 251                    window.refresh();
 252                })
 253                .register_action(|workspace, _: &ResetTrialUpsell, _window, cx| {
 254                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 255                        panel.update(cx, |panel, _| {
 256                            panel
 257                                .on_boarding_upsell_dismissed
 258                                .store(false, Ordering::Release);
 259                        });
 260                    }
 261                    OnboardingUpsell::set_dismissed(false, cx);
 262                })
 263                .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
 264                    TrialEndUpsell::set_dismissed(false, cx);
 265                })
 266                .register_action(|workspace, _: &ResetAgentZoom, window, cx| {
 267                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 268                        panel.update(cx, |panel, cx| {
 269                            panel.reset_agent_zoom(window, cx);
 270                        });
 271                    }
 272                })
 273                .register_action(|workspace, _: &CopyThreadToClipboard, window, cx| {
 274                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 275                        panel.update(cx, |panel, cx| {
 276                            panel.copy_thread_to_clipboard(window, cx);
 277                        });
 278                    }
 279                })
 280                .register_action(|workspace, _: &LoadThreadFromClipboard, window, cx| {
 281                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 282                        workspace.focus_panel::<AgentPanel>(window, cx);
 283                        panel.update(cx, |panel, cx| {
 284                            panel.load_thread_from_clipboard(window, cx);
 285                        });
 286                    }
 287                })
 288                .register_action(|workspace, action: &ReviewBranchDiff, window, cx| {
 289                    let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
 290                        return;
 291                    };
 292
 293                    let mention_uri = MentionUri::GitDiff {
 294                        base_ref: action.base_ref.to_string(),
 295                    };
 296                    let diff_uri = mention_uri.to_uri().to_string();
 297
 298                    let content_blocks = vec![
 299                        acp::ContentBlock::Text(acp::TextContent::new(
 300                            "Please review this branch diff carefully. Point out any issues, \
 301                             potential bugs, or improvement opportunities you find.\n\n"
 302                                .to_string(),
 303                        )),
 304                        acp::ContentBlock::Resource(acp::EmbeddedResource::new(
 305                            acp::EmbeddedResourceResource::TextResourceContents(
 306                                acp::TextResourceContents::new(
 307                                    action.diff_text.to_string(),
 308                                    diff_uri,
 309                                ),
 310                            ),
 311                        )),
 312                    ];
 313
 314                    workspace.focus_panel::<AgentPanel>(window, cx);
 315
 316                    panel.update(cx, |panel, cx| {
 317                        panel.external_thread(
 318                            None,
 319                            None,
 320                            Some(AgentInitialContent::ContentBlock {
 321                                blocks: content_blocks,
 322                                auto_submit: true,
 323                            }),
 324                            window,
 325                            cx,
 326                        );
 327                    });
 328                });
 329        },
 330    )
 331    .detach();
 332}
 333
 334#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 335enum HistoryKind {
 336    AgentThreads,
 337    TextThreads,
 338}
 339
 340enum ActiveView {
 341    Uninitialized,
 342    AgentThread {
 343        server_view: Entity<AcpServerView>,
 344    },
 345    TextThread {
 346        text_thread_editor: Entity<TextThreadEditor>,
 347        title_editor: Entity<Editor>,
 348        buffer_search_bar: Entity<BufferSearchBar>,
 349        _subscriptions: Vec<gpui::Subscription>,
 350    },
 351    History {
 352        kind: HistoryKind,
 353    },
 354    Configuration,
 355}
 356
 357enum WhichFontSize {
 358    AgentFont,
 359    BufferFont,
 360    None,
 361}
 362
 363// TODO unify this with ExternalAgent
 364#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
 365pub enum AgentType {
 366    #[default]
 367    NativeAgent,
 368    TextThread,
 369    Gemini,
 370    ClaudeAgent,
 371    Codex,
 372    Custom {
 373        name: SharedString,
 374    },
 375}
 376
 377impl AgentType {
 378    fn label(&self) -> SharedString {
 379        match self {
 380            Self::NativeAgent | Self::TextThread => "Zed Agent".into(),
 381            Self::Gemini => "Gemini CLI".into(),
 382            Self::ClaudeAgent => "Claude Agent".into(),
 383            Self::Codex => "Codex".into(),
 384            Self::Custom { name, .. } => name.into(),
 385        }
 386    }
 387
 388    fn icon(&self) -> Option<IconName> {
 389        match self {
 390            Self::NativeAgent | Self::TextThread => None,
 391            Self::Gemini => Some(IconName::AiGemini),
 392            Self::ClaudeAgent => Some(IconName::AiClaude),
 393            Self::Codex => Some(IconName::AiOpenAi),
 394            Self::Custom { .. } => Some(IconName::Sparkle),
 395        }
 396    }
 397}
 398
 399impl From<ExternalAgent> for AgentType {
 400    fn from(value: ExternalAgent) -> Self {
 401        match value {
 402            ExternalAgent::Gemini => Self::Gemini,
 403            ExternalAgent::ClaudeCode => Self::ClaudeAgent,
 404            ExternalAgent::Codex => Self::Codex,
 405            ExternalAgent::Custom { name } => Self::Custom { name },
 406            ExternalAgent::NativeAgent => Self::NativeAgent,
 407        }
 408    }
 409}
 410
 411impl ActiveView {
 412    pub fn which_font_size_used(&self) -> WhichFontSize {
 413        match self {
 414            ActiveView::Uninitialized
 415            | ActiveView::AgentThread { .. }
 416            | ActiveView::History { .. } => WhichFontSize::AgentFont,
 417            ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
 418            ActiveView::Configuration => WhichFontSize::None,
 419        }
 420    }
 421
 422    pub fn text_thread(
 423        text_thread_editor: Entity<TextThreadEditor>,
 424        language_registry: Arc<LanguageRegistry>,
 425        window: &mut Window,
 426        cx: &mut App,
 427    ) -> Self {
 428        let title = text_thread_editor.read(cx).title(cx).to_string();
 429
 430        let editor = cx.new(|cx| {
 431            let mut editor = Editor::single_line(window, cx);
 432            editor.set_text(title, window, cx);
 433            editor
 434        });
 435
 436        // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would
 437        // cause a custom summary to be set. The presence of this custom summary would cause
 438        // summarization to not happen.
 439        let mut suppress_first_edit = true;
 440
 441        let subscriptions = vec![
 442            window.subscribe(&editor, cx, {
 443                {
 444                    let text_thread_editor = text_thread_editor.clone();
 445                    move |editor, event, window, cx| match event {
 446                        EditorEvent::BufferEdited => {
 447                            if suppress_first_edit {
 448                                suppress_first_edit = false;
 449                                return;
 450                            }
 451                            let new_summary = editor.read(cx).text(cx);
 452
 453                            text_thread_editor.update(cx, |text_thread_editor, cx| {
 454                                text_thread_editor
 455                                    .text_thread()
 456                                    .update(cx, |text_thread, cx| {
 457                                        text_thread.set_custom_summary(new_summary, cx);
 458                                    })
 459                            })
 460                        }
 461                        EditorEvent::Blurred => {
 462                            if editor.read(cx).text(cx).is_empty() {
 463                                let summary = text_thread_editor
 464                                    .read(cx)
 465                                    .text_thread()
 466                                    .read(cx)
 467                                    .summary()
 468                                    .or_default();
 469
 470                                editor.update(cx, |editor, cx| {
 471                                    editor.set_text(summary, window, cx);
 472                                });
 473                            }
 474                        }
 475                        _ => {}
 476                    }
 477                }
 478            }),
 479            window.subscribe(&text_thread_editor.read(cx).text_thread().clone(), cx, {
 480                let editor = editor.clone();
 481                move |text_thread, event, window, cx| match event {
 482                    TextThreadEvent::SummaryGenerated => {
 483                        let summary = text_thread.read(cx).summary().or_default();
 484
 485                        editor.update(cx, |editor, cx| {
 486                            editor.set_text(summary, window, cx);
 487                        })
 488                    }
 489                    TextThreadEvent::PathChanged { .. } => {}
 490                    _ => {}
 491                }
 492            }),
 493        ];
 494
 495        let buffer_search_bar =
 496            cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx));
 497        buffer_search_bar.update(cx, |buffer_search_bar, cx| {
 498            buffer_search_bar.set_active_pane_item(Some(&text_thread_editor), window, cx)
 499        });
 500
 501        Self::TextThread {
 502            text_thread_editor,
 503            title_editor: editor,
 504            buffer_search_bar,
 505            _subscriptions: subscriptions,
 506        }
 507    }
 508}
 509
 510pub struct AgentPanel {
 511    workspace: WeakEntity<Workspace>,
 512    /// Workspace id is used as a database key
 513    workspace_id: Option<WorkspaceId>,
 514    user_store: Entity<UserStore>,
 515    project: Entity<Project>,
 516    fs: Arc<dyn Fs>,
 517    language_registry: Arc<LanguageRegistry>,
 518    acp_history: Entity<AcpThreadHistory>,
 519    text_thread_history: Entity<TextThreadHistory>,
 520    thread_store: Entity<ThreadStore>,
 521    text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
 522    prompt_store: Option<Entity<PromptStore>>,
 523    context_server_registry: Entity<ContextServerRegistry>,
 524    configuration: Option<Entity<AgentConfiguration>>,
 525    configuration_subscription: Option<Subscription>,
 526    focus_handle: FocusHandle,
 527    active_view: ActiveView,
 528    previous_view: Option<ActiveView>,
 529    _active_view_observation: Option<Subscription>,
 530    new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
 531    agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
 532    agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
 533    agent_navigation_menu: Option<Entity<ContextMenu>>,
 534    _extension_subscription: Option<Subscription>,
 535    width: Option<Pixels>,
 536    height: Option<Pixels>,
 537    zoomed: bool,
 538    pending_serialization: Option<Task<Result<()>>>,
 539    onboarding: Entity<AgentPanelOnboarding>,
 540    selected_agent: AgentType,
 541    show_trust_workspace_message: bool,
 542    last_configuration_error_telemetry: Option<String>,
 543    on_boarding_upsell_dismissed: AtomicBool,
 544}
 545
 546impl AgentPanel {
 547    fn serialize(&mut self, cx: &mut App) {
 548        let Some(workspace_id) = self.workspace_id else {
 549            return;
 550        };
 551
 552        let width = self.width;
 553        let selected_agent = self.selected_agent.clone();
 554
 555        let last_active_thread = self.active_agent_thread(cx).map(|thread| {
 556            let thread = thread.read(cx);
 557            let title = thread.title();
 558            SerializedActiveThread {
 559                session_id: thread.session_id().0.to_string(),
 560                agent_type: self.selected_agent.clone(),
 561                title: if title.as_ref() != DEFAULT_THREAD_TITLE {
 562                    Some(title.to_string())
 563                } else {
 564                    None
 565                },
 566                cwd: None,
 567            }
 568        });
 569
 570        self.pending_serialization = Some(cx.background_spawn(async move {
 571            save_serialized_panel(
 572                workspace_id,
 573                SerializedAgentPanel {
 574                    width,
 575                    selected_agent: Some(selected_agent),
 576                    last_active_thread,
 577                },
 578            )
 579            .await?;
 580            anyhow::Ok(())
 581        }));
 582    }
 583
 584    pub fn load(
 585        workspace: WeakEntity<Workspace>,
 586        prompt_builder: Arc<PromptBuilder>,
 587        mut cx: AsyncWindowContext,
 588    ) -> Task<Result<Entity<Self>>> {
 589        let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
 590        cx.spawn(async move |cx| {
 591            let prompt_store = match prompt_store {
 592                Ok(prompt_store) => prompt_store.await.ok(),
 593                Err(_) => None,
 594            };
 595            let workspace_id = workspace
 596                .read_with(cx, |workspace, _| workspace.database_id())
 597                .ok()
 598                .flatten();
 599
 600            let serialized_panel = cx
 601                .background_spawn(async move {
 602                    workspace_id
 603                        .and_then(read_serialized_panel)
 604                        .or_else(read_legacy_serialized_panel)
 605                })
 606                .await;
 607
 608            let slash_commands = Arc::new(SlashCommandWorkingSet::default());
 609            let text_thread_store = workspace
 610                .update(cx, |workspace, cx| {
 611                    let project = workspace.project().clone();
 612                    assistant_text_thread::TextThreadStore::new(
 613                        project,
 614                        prompt_builder,
 615                        slash_commands,
 616                        cx,
 617                    )
 618                })?
 619                .await?;
 620
 621            let panel = workspace.update_in(cx, |workspace, window, cx| {
 622                let panel =
 623                    cx.new(|cx| Self::new(workspace, text_thread_store, prompt_store, window, cx));
 624
 625                if let Some(serialized_panel) = &serialized_panel {
 626                    panel.update(cx, |panel, cx| {
 627                        panel.width = serialized_panel.width.map(|w| w.round());
 628                        if let Some(selected_agent) = serialized_panel.selected_agent.clone() {
 629                            panel.selected_agent = selected_agent;
 630                        }
 631                        cx.notify();
 632                    });
 633                }
 634
 635                panel
 636            })?;
 637
 638            if let Some(thread_info) = serialized_panel.and_then(|p| p.last_active_thread) {
 639                let session_id = acp::SessionId::new(thread_info.session_id.clone());
 640                let load_task = panel.update(cx, |panel, cx| {
 641                    let thread_store = panel.thread_store.clone();
 642                    thread_store.update(cx, |store, cx| store.load_thread(session_id, cx))
 643                });
 644                let thread_exists = load_task
 645                    .await
 646                    .map(|thread: Option<agent::DbThread>| thread.is_some())
 647                    .unwrap_or(false);
 648
 649                if thread_exists {
 650                    panel.update_in(cx, |panel, window, cx| {
 651                        panel.selected_agent = thread_info.agent_type.clone();
 652                        let session_info = AgentSessionInfo {
 653                            session_id: acp::SessionId::new(thread_info.session_id),
 654                            cwd: thread_info.cwd,
 655                            title: thread_info.title.map(SharedString::from),
 656                            updated_at: None,
 657                            meta: None,
 658                        };
 659                        panel.load_agent_thread(session_info, window, cx);
 660                    })?;
 661                } else {
 662                    log::error!(
 663                        "could not restore last active thread: \
 664                         no thread found in database with ID {:?}",
 665                        thread_info.session_id
 666                    );
 667                }
 668            }
 669
 670            Ok(panel)
 671        })
 672    }
 673
 674    pub(crate) fn new(
 675        workspace: &Workspace,
 676        text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
 677        prompt_store: Option<Entity<PromptStore>>,
 678        window: &mut Window,
 679        cx: &mut Context<Self>,
 680    ) -> Self {
 681        let fs = workspace.app_state().fs.clone();
 682        let user_store = workspace.app_state().user_store.clone();
 683        let project = workspace.project();
 684        let language_registry = project.read(cx).languages().clone();
 685        let client = workspace.client().clone();
 686        let workspace_id = workspace.database_id();
 687        let workspace = workspace.weak_handle();
 688
 689        let context_server_registry =
 690            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
 691
 692        let thread_store = ThreadStore::global(cx);
 693        let acp_history = cx.new(|cx| AcpThreadHistory::new(None, window, cx));
 694        let text_thread_history =
 695            cx.new(|cx| TextThreadHistory::new(text_thread_store.clone(), window, cx));
 696        cx.subscribe_in(
 697            &acp_history,
 698            window,
 699            |this, _, event, window, cx| match event {
 700                ThreadHistoryEvent::Open(thread) => {
 701                    this.load_agent_thread(thread.clone(), window, cx);
 702                }
 703            },
 704        )
 705        .detach();
 706        cx.subscribe_in(
 707            &text_thread_history,
 708            window,
 709            |this, _, event, window, cx| match event {
 710                TextThreadHistoryEvent::Open(thread) => {
 711                    this.open_saved_text_thread(thread.path.clone(), window, cx)
 712                        .detach_and_log_err(cx);
 713                }
 714            },
 715        )
 716        .detach();
 717
 718        let active_view = ActiveView::Uninitialized;
 719
 720        let weak_panel = cx.entity().downgrade();
 721
 722        window.defer(cx, move |window, cx| {
 723            let panel = weak_panel.clone();
 724            let agent_navigation_menu =
 725                ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
 726                    if let Some(panel) = panel.upgrade() {
 727                        if let Some(kind) = panel.read(cx).history_kind_for_selected_agent(cx) {
 728                            menu =
 729                                Self::populate_recently_updated_menu_section(menu, panel, kind, cx);
 730                            let view_all_label = match kind {
 731                                HistoryKind::AgentThreads => "View All",
 732                                HistoryKind::TextThreads => "View All Text Threads",
 733                            };
 734                            menu = menu.action(view_all_label, Box::new(OpenHistory));
 735                        }
 736                    }
 737
 738                    menu = menu
 739                        .fixed_width(px(320.).into())
 740                        .keep_open_on_confirm(false)
 741                        .key_context("NavigationMenu");
 742
 743                    menu
 744                });
 745            weak_panel
 746                .update(cx, |panel, cx| {
 747                    cx.subscribe_in(
 748                        &agent_navigation_menu,
 749                        window,
 750                        |_, menu, _: &DismissEvent, window, cx| {
 751                            menu.update(cx, |menu, _| {
 752                                menu.clear_selected();
 753                            });
 754                            cx.focus_self(window);
 755                        },
 756                    )
 757                    .detach();
 758                    panel.agent_navigation_menu = Some(agent_navigation_menu);
 759                })
 760                .ok();
 761        });
 762
 763        let weak_panel = cx.entity().downgrade();
 764        let onboarding = cx.new(|cx| {
 765            AgentPanelOnboarding::new(
 766                user_store.clone(),
 767                client,
 768                move |_window, cx| {
 769                    weak_panel
 770                        .update(cx, |panel, _| {
 771                            panel
 772                                .on_boarding_upsell_dismissed
 773                                .store(true, Ordering::Release);
 774                        })
 775                        .ok();
 776                    OnboardingUpsell::set_dismissed(true, cx);
 777                },
 778                cx,
 779            )
 780        });
 781
 782        // Subscribe to extension events to sync agent servers when extensions change
 783        let extension_subscription = if let Some(extension_events) = ExtensionEvents::try_global(cx)
 784        {
 785            Some(
 786                cx.subscribe(&extension_events, |this, _source, event, cx| match event {
 787                    extension::Event::ExtensionInstalled(_)
 788                    | extension::Event::ExtensionUninstalled(_)
 789                    | extension::Event::ExtensionsInstalledChanged => {
 790                        this.sync_agent_servers_from_extensions(cx);
 791                    }
 792                    _ => {}
 793                }),
 794            )
 795        } else {
 796            None
 797        };
 798
 799        let mut panel = Self {
 800            workspace_id,
 801            active_view,
 802            workspace,
 803            user_store,
 804            project: project.clone(),
 805            fs: fs.clone(),
 806            language_registry,
 807            text_thread_store,
 808            prompt_store,
 809            configuration: None,
 810            configuration_subscription: None,
 811            focus_handle: cx.focus_handle(),
 812            context_server_registry,
 813            previous_view: None,
 814            _active_view_observation: None,
 815            new_thread_menu_handle: PopoverMenuHandle::default(),
 816            agent_panel_menu_handle: PopoverMenuHandle::default(),
 817            agent_navigation_menu_handle: PopoverMenuHandle::default(),
 818            agent_navigation_menu: None,
 819            _extension_subscription: extension_subscription,
 820            width: None,
 821            height: None,
 822            zoomed: false,
 823            pending_serialization: None,
 824            onboarding,
 825            acp_history,
 826            text_thread_history,
 827            thread_store,
 828            selected_agent: AgentType::default(),
 829            show_trust_workspace_message: false,
 830            last_configuration_error_telemetry: None,
 831            on_boarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed()),
 832        };
 833
 834        // Initial sync of agent servers from extensions
 835        panel.sync_agent_servers_from_extensions(cx);
 836        panel
 837    }
 838
 839    pub fn toggle_focus(
 840        workspace: &mut Workspace,
 841        _: &ToggleFocus,
 842        window: &mut Window,
 843        cx: &mut Context<Workspace>,
 844    ) {
 845        if workspace
 846            .panel::<Self>(cx)
 847            .is_some_and(|panel| panel.read(cx).enabled(cx))
 848        {
 849            workspace.toggle_panel_focus::<Self>(window, cx);
 850        }
 851    }
 852
 853    pub fn toggle(
 854        workspace: &mut Workspace,
 855        _: &Toggle,
 856        window: &mut Window,
 857        cx: &mut Context<Workspace>,
 858    ) {
 859        if workspace
 860            .panel::<Self>(cx)
 861            .is_some_and(|panel| panel.read(cx).enabled(cx))
 862        {
 863            if !workspace.toggle_panel_focus::<Self>(window, cx) {
 864                workspace.close_panel::<Self>(window, cx);
 865            }
 866        }
 867    }
 868
 869    pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
 870        &self.prompt_store
 871    }
 872
 873    pub fn thread_store(&self) -> &Entity<ThreadStore> {
 874        &self.thread_store
 875    }
 876
 877    pub fn history(&self) -> &Entity<AcpThreadHistory> {
 878        &self.acp_history
 879    }
 880
 881    pub fn open_thread(
 882        &mut self,
 883        thread: AgentSessionInfo,
 884        window: &mut Window,
 885        cx: &mut Context<Self>,
 886    ) {
 887        self.external_thread(
 888            Some(crate::ExternalAgent::NativeAgent),
 889            Some(thread),
 890            None,
 891            window,
 892            cx,
 893        );
 894    }
 895
 896    pub(crate) fn context_server_registry(&self) -> &Entity<ContextServerRegistry> {
 897        &self.context_server_registry
 898    }
 899
 900    pub fn is_visible(workspace: &Entity<Workspace>, cx: &App) -> bool {
 901        let workspace_read = workspace.read(cx);
 902
 903        workspace_read
 904            .panel::<AgentPanel>(cx)
 905            .map(|panel| {
 906                let panel_id = Entity::entity_id(&panel);
 907
 908                workspace_read.all_docks().iter().any(|dock| {
 909                    dock.read(cx)
 910                        .visible_panel()
 911                        .is_some_and(|visible_panel| visible_panel.panel_id() == panel_id)
 912                })
 913            })
 914            .unwrap_or(false)
 915    }
 916
 917    pub(crate) fn active_thread_view(&self) -> Option<&Entity<AcpServerView>> {
 918        match &self.active_view {
 919            ActiveView::AgentThread { server_view, .. } => Some(server_view),
 920            ActiveView::Uninitialized
 921            | ActiveView::TextThread { .. }
 922            | ActiveView::History { .. }
 923            | ActiveView::Configuration => None,
 924        }
 925    }
 926
 927    fn new_thread(&mut self, _action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
 928        self.new_agent_thread(AgentType::NativeAgent, window, cx);
 929    }
 930
 931    fn new_native_agent_thread_from_summary(
 932        &mut self,
 933        action: &NewNativeAgentThreadFromSummary,
 934        window: &mut Window,
 935        cx: &mut Context<Self>,
 936    ) {
 937        let Some(thread) = self
 938            .acp_history
 939            .read(cx)
 940            .session_for_id(&action.from_session_id)
 941        else {
 942            return;
 943        };
 944
 945        self.external_thread(
 946            Some(ExternalAgent::NativeAgent),
 947            None,
 948            Some(AgentInitialContent::ThreadSummary(thread)),
 949            window,
 950            cx,
 951        );
 952    }
 953
 954    fn new_text_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 955        telemetry::event!("Agent Thread Started", agent = "zed-text");
 956
 957        let context = self
 958            .text_thread_store
 959            .update(cx, |context_store, cx| context_store.create(cx));
 960        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
 961            .log_err()
 962            .flatten();
 963
 964        let text_thread_editor = cx.new(|cx| {
 965            let mut editor = TextThreadEditor::for_text_thread(
 966                context,
 967                self.fs.clone(),
 968                self.workspace.clone(),
 969                self.project.clone(),
 970                lsp_adapter_delegate,
 971                window,
 972                cx,
 973            );
 974            editor.insert_default_prompt(window, cx);
 975            editor
 976        });
 977
 978        if self.selected_agent != AgentType::TextThread {
 979            self.selected_agent = AgentType::TextThread;
 980            self.serialize(cx);
 981        }
 982
 983        self.set_active_view(
 984            ActiveView::text_thread(
 985                text_thread_editor.clone(),
 986                self.language_registry.clone(),
 987                window,
 988                cx,
 989            ),
 990            true,
 991            window,
 992            cx,
 993        );
 994        text_thread_editor.focus_handle(cx).focus(window, cx);
 995    }
 996
 997    fn external_thread(
 998        &mut self,
 999        agent_choice: Option<crate::ExternalAgent>,
1000        resume_thread: Option<AgentSessionInfo>,
1001        initial_content: Option<AgentInitialContent>,
1002        window: &mut Window,
1003        cx: &mut Context<Self>,
1004    ) {
1005        let workspace = self.workspace.clone();
1006        let project = self.project.clone();
1007        let fs = self.fs.clone();
1008        let is_via_collab = self.project.read(cx).is_via_collab();
1009
1010        const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
1011
1012        #[derive(Serialize, Deserialize)]
1013        struct LastUsedExternalAgent {
1014            agent: crate::ExternalAgent,
1015        }
1016
1017        let thread_store = self.thread_store.clone();
1018
1019        cx.spawn_in(window, async move |this, cx| {
1020            let ext_agent = match agent_choice {
1021                Some(agent) => {
1022                    cx.background_spawn({
1023                        let agent = agent.clone();
1024                        async move {
1025                            if let Some(serialized) =
1026                                serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
1027                            {
1028                                KEY_VALUE_STORE
1029                                    .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
1030                                    .await
1031                                    .log_err();
1032                            }
1033                        }
1034                    })
1035                    .detach();
1036
1037                    agent
1038                }
1039                None => {
1040                    if is_via_collab {
1041                        ExternalAgent::NativeAgent
1042                    } else {
1043                        cx.background_spawn(async move {
1044                            KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
1045                        })
1046                        .await
1047                        .log_err()
1048                        .flatten()
1049                        .and_then(|value| {
1050                            serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
1051                        })
1052                        .map(|agent| agent.agent)
1053                        .unwrap_or(ExternalAgent::NativeAgent)
1054                    }
1055                }
1056            };
1057
1058            let server = ext_agent.server(fs, thread_store);
1059            this.update_in(cx, |agent_panel, window, cx| {
1060                agent_panel._external_thread(
1061                    server,
1062                    resume_thread,
1063                    initial_content,
1064                    workspace,
1065                    project,
1066                    ext_agent,
1067                    window,
1068                    cx,
1069                );
1070            })?;
1071
1072            anyhow::Ok(())
1073        })
1074        .detach_and_log_err(cx);
1075    }
1076
1077    fn deploy_rules_library(
1078        &mut self,
1079        action: &OpenRulesLibrary,
1080        _window: &mut Window,
1081        cx: &mut Context<Self>,
1082    ) {
1083        open_rules_library(
1084            self.language_registry.clone(),
1085            Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
1086            Rc::new(|| {
1087                Rc::new(SlashCommandCompletionProvider::new(
1088                    Arc::new(SlashCommandWorkingSet::default()),
1089                    None,
1090                    None,
1091                ))
1092            }),
1093            action
1094                .prompt_to_select
1095                .map(|uuid| UserPromptId(uuid).into()),
1096            cx,
1097        )
1098        .detach_and_log_err(cx);
1099    }
1100
1101    fn expand_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1102        let Some(thread_view) = self.active_thread_view() else {
1103            return;
1104        };
1105
1106        let Some(active_thread) = thread_view.read(cx).active_thread().cloned() else {
1107            return;
1108        };
1109
1110        active_thread.update(cx, |active_thread, cx| {
1111            active_thread.expand_message_editor(&ExpandMessageEditor, window, cx);
1112            active_thread.focus_handle(cx).focus(window, cx);
1113        })
1114    }
1115
1116    fn history_kind_for_selected_agent(&self, cx: &App) -> Option<HistoryKind> {
1117        match self.selected_agent {
1118            AgentType::NativeAgent => Some(HistoryKind::AgentThreads),
1119            AgentType::TextThread => Some(HistoryKind::TextThreads),
1120            AgentType::Gemini
1121            | AgentType::ClaudeAgent
1122            | AgentType::Codex
1123            | AgentType::Custom { .. } => {
1124                if self.acp_history.read(cx).has_session_list() {
1125                    Some(HistoryKind::AgentThreads)
1126                } else {
1127                    None
1128                }
1129            }
1130        }
1131    }
1132
1133    fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1134        let Some(kind) = self.history_kind_for_selected_agent(cx) else {
1135            return;
1136        };
1137
1138        if let ActiveView::History { kind: active_kind } = self.active_view {
1139            if active_kind == kind {
1140                if let Some(previous_view) = self.previous_view.take() {
1141                    self.set_active_view(previous_view, true, window, cx);
1142                }
1143                return;
1144            }
1145        }
1146
1147        self.set_active_view(ActiveView::History { kind }, true, window, cx);
1148        cx.notify();
1149    }
1150
1151    pub(crate) fn open_saved_text_thread(
1152        &mut self,
1153        path: Arc<Path>,
1154        window: &mut Window,
1155        cx: &mut Context<Self>,
1156    ) -> Task<Result<()>> {
1157        let text_thread_task = self
1158            .text_thread_store
1159            .update(cx, |store, cx| store.open_local(path, cx));
1160        cx.spawn_in(window, async move |this, cx| {
1161            let text_thread = text_thread_task.await?;
1162            this.update_in(cx, |this, window, cx| {
1163                this.open_text_thread(text_thread, window, cx);
1164            })
1165        })
1166    }
1167
1168    pub(crate) fn open_text_thread(
1169        &mut self,
1170        text_thread: Entity<TextThread>,
1171        window: &mut Window,
1172        cx: &mut Context<Self>,
1173    ) {
1174        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx)
1175            .log_err()
1176            .flatten();
1177        let editor = cx.new(|cx| {
1178            TextThreadEditor::for_text_thread(
1179                text_thread,
1180                self.fs.clone(),
1181                self.workspace.clone(),
1182                self.project.clone(),
1183                lsp_adapter_delegate,
1184                window,
1185                cx,
1186            )
1187        });
1188
1189        if self.selected_agent != AgentType::TextThread {
1190            self.selected_agent = AgentType::TextThread;
1191            self.serialize(cx);
1192        }
1193
1194        self.set_active_view(
1195            ActiveView::text_thread(editor, self.language_registry.clone(), window, cx),
1196            true,
1197            window,
1198            cx,
1199        );
1200    }
1201
1202    pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
1203        match self.active_view {
1204            ActiveView::Configuration | ActiveView::History { .. } => {
1205                if let Some(previous_view) = self.previous_view.take() {
1206                    self.set_active_view(previous_view, true, window, cx);
1207                }
1208                cx.notify();
1209            }
1210            _ => {}
1211        }
1212    }
1213
1214    pub fn toggle_navigation_menu(
1215        &mut self,
1216        _: &ToggleNavigationMenu,
1217        window: &mut Window,
1218        cx: &mut Context<Self>,
1219    ) {
1220        if self.history_kind_for_selected_agent(cx).is_none() {
1221            return;
1222        }
1223        self.agent_navigation_menu_handle.toggle(window, cx);
1224    }
1225
1226    pub fn toggle_options_menu(
1227        &mut self,
1228        _: &ToggleOptionsMenu,
1229        window: &mut Window,
1230        cx: &mut Context<Self>,
1231    ) {
1232        self.agent_panel_menu_handle.toggle(window, cx);
1233    }
1234
1235    pub fn toggle_new_thread_menu(
1236        &mut self,
1237        _: &ToggleNewThreadMenu,
1238        window: &mut Window,
1239        cx: &mut Context<Self>,
1240    ) {
1241        self.new_thread_menu_handle.toggle(window, cx);
1242    }
1243
1244    pub fn increase_font_size(
1245        &mut self,
1246        action: &IncreaseBufferFontSize,
1247        _: &mut Window,
1248        cx: &mut Context<Self>,
1249    ) {
1250        self.handle_font_size_action(action.persist, px(1.0), cx);
1251    }
1252
1253    pub fn decrease_font_size(
1254        &mut self,
1255        action: &DecreaseBufferFontSize,
1256        _: &mut Window,
1257        cx: &mut Context<Self>,
1258    ) {
1259        self.handle_font_size_action(action.persist, px(-1.0), cx);
1260    }
1261
1262    fn handle_font_size_action(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
1263        match self.active_view.which_font_size_used() {
1264            WhichFontSize::AgentFont => {
1265                if persist {
1266                    update_settings_file(self.fs.clone(), cx, move |settings, cx| {
1267                        let agent_ui_font_size =
1268                            ThemeSettings::get_global(cx).agent_ui_font_size(cx) + delta;
1269                        let agent_buffer_font_size =
1270                            ThemeSettings::get_global(cx).agent_buffer_font_size(cx) + delta;
1271
1272                        let _ = settings
1273                            .theme
1274                            .agent_ui_font_size
1275                            .insert(f32::from(theme::clamp_font_size(agent_ui_font_size)).into());
1276                        let _ = settings.theme.agent_buffer_font_size.insert(
1277                            f32::from(theme::clamp_font_size(agent_buffer_font_size)).into(),
1278                        );
1279                    });
1280                } else {
1281                    theme::adjust_agent_ui_font_size(cx, |size| size + delta);
1282                    theme::adjust_agent_buffer_font_size(cx, |size| size + delta);
1283                }
1284            }
1285            WhichFontSize::BufferFont => {
1286                // Prompt editor uses the buffer font size, so allow the action to propagate to the
1287                // default handler that changes that font size.
1288                cx.propagate();
1289            }
1290            WhichFontSize::None => {}
1291        }
1292    }
1293
1294    pub fn reset_font_size(
1295        &mut self,
1296        action: &ResetBufferFontSize,
1297        _: &mut Window,
1298        cx: &mut Context<Self>,
1299    ) {
1300        if action.persist {
1301            update_settings_file(self.fs.clone(), cx, move |settings, _| {
1302                settings.theme.agent_ui_font_size = None;
1303                settings.theme.agent_buffer_font_size = None;
1304            });
1305        } else {
1306            theme::reset_agent_ui_font_size(cx);
1307            theme::reset_agent_buffer_font_size(cx);
1308        }
1309    }
1310
1311    pub fn reset_agent_zoom(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1312        theme::reset_agent_ui_font_size(cx);
1313        theme::reset_agent_buffer_font_size(cx);
1314    }
1315
1316    pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1317        if self.zoomed {
1318            cx.emit(PanelEvent::ZoomOut);
1319        } else {
1320            if !self.focus_handle(cx).contains_focused(window, cx) {
1321                cx.focus_self(window);
1322            }
1323            cx.emit(PanelEvent::ZoomIn);
1324        }
1325    }
1326
1327    pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1328        let agent_server_store = self.project.read(cx).agent_server_store().clone();
1329        let context_server_store = self.project.read(cx).context_server_store();
1330        let fs = self.fs.clone();
1331
1332        self.set_active_view(ActiveView::Configuration, true, window, cx);
1333        self.configuration = Some(cx.new(|cx| {
1334            AgentConfiguration::new(
1335                fs,
1336                agent_server_store,
1337                context_server_store,
1338                self.context_server_registry.clone(),
1339                self.language_registry.clone(),
1340                self.workspace.clone(),
1341                window,
1342                cx,
1343            )
1344        }));
1345
1346        if let Some(configuration) = self.configuration.as_ref() {
1347            self.configuration_subscription = Some(cx.subscribe_in(
1348                configuration,
1349                window,
1350                Self::handle_agent_configuration_event,
1351            ));
1352
1353            configuration.focus_handle(cx).focus(window, cx);
1354        }
1355    }
1356
1357    pub(crate) fn open_active_thread_as_markdown(
1358        &mut self,
1359        _: &OpenActiveThreadAsMarkdown,
1360        window: &mut Window,
1361        cx: &mut Context<Self>,
1362    ) {
1363        if let Some(workspace) = self.workspace.upgrade()
1364            && let Some(thread_view) = self.active_thread_view()
1365            && let Some(active_thread) = thread_view.read(cx).active_thread().cloned()
1366        {
1367            active_thread.update(cx, |thread, cx| {
1368                thread
1369                    .open_thread_as_markdown(workspace, window, cx)
1370                    .detach_and_log_err(cx);
1371            });
1372        }
1373    }
1374
1375    fn copy_thread_to_clipboard(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1376        let Some(thread) = self.active_native_agent_thread(cx) else {
1377            Self::show_deferred_toast(&self.workspace, "No active native thread to copy", cx);
1378            return;
1379        };
1380
1381        let workspace = self.workspace.clone();
1382        let load_task = thread.read(cx).to_db(cx);
1383
1384        cx.spawn_in(window, async move |_this, cx| {
1385            let db_thread = load_task.await;
1386            let shared_thread = SharedThread::from_db_thread(&db_thread);
1387            let thread_data = shared_thread.to_bytes()?;
1388            let encoded = base64::Engine::encode(&base64::prelude::BASE64_STANDARD, &thread_data);
1389
1390            cx.update(|_window, cx| {
1391                cx.write_to_clipboard(ClipboardItem::new_string(encoded));
1392                if let Some(workspace) = workspace.upgrade() {
1393                    workspace.update(cx, |workspace, cx| {
1394                        struct ThreadCopiedToast;
1395                        workspace.show_toast(
1396                            workspace::Toast::new(
1397                                workspace::notifications::NotificationId::unique::<ThreadCopiedToast>(),
1398                                "Thread copied to clipboard (base64 encoded)",
1399                            )
1400                            .autohide(),
1401                            cx,
1402                        );
1403                    });
1404                }
1405            })?;
1406
1407            anyhow::Ok(())
1408        })
1409        .detach_and_log_err(cx);
1410    }
1411
1412    fn show_deferred_toast(
1413        workspace: &WeakEntity<workspace::Workspace>,
1414        message: &'static str,
1415        cx: &mut App,
1416    ) {
1417        let workspace = workspace.clone();
1418        cx.defer(move |cx| {
1419            if let Some(workspace) = workspace.upgrade() {
1420                workspace.update(cx, |workspace, cx| {
1421                    struct ClipboardToast;
1422                    workspace.show_toast(
1423                        workspace::Toast::new(
1424                            workspace::notifications::NotificationId::unique::<ClipboardToast>(),
1425                            message,
1426                        )
1427                        .autohide(),
1428                        cx,
1429                    );
1430                });
1431            }
1432        });
1433    }
1434
1435    fn load_thread_from_clipboard(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1436        let Some(clipboard) = cx.read_from_clipboard() else {
1437            Self::show_deferred_toast(&self.workspace, "No clipboard content available", cx);
1438            return;
1439        };
1440
1441        let Some(encoded) = clipboard.text() else {
1442            Self::show_deferred_toast(&self.workspace, "Clipboard does not contain text", cx);
1443            return;
1444        };
1445
1446        let thread_data = match base64::Engine::decode(&base64::prelude::BASE64_STANDARD, &encoded)
1447        {
1448            Ok(data) => data,
1449            Err(_) => {
1450                Self::show_deferred_toast(
1451                    &self.workspace,
1452                    "Failed to decode clipboard content (expected base64)",
1453                    cx,
1454                );
1455                return;
1456            }
1457        };
1458
1459        let shared_thread = match SharedThread::from_bytes(&thread_data) {
1460            Ok(thread) => thread,
1461            Err(_) => {
1462                Self::show_deferred_toast(
1463                    &self.workspace,
1464                    "Failed to parse thread data from clipboard",
1465                    cx,
1466                );
1467                return;
1468            }
1469        };
1470
1471        let db_thread = shared_thread.to_db_thread();
1472        let session_id = acp::SessionId::new(uuid::Uuid::new_v4().to_string());
1473        let thread_store = self.thread_store.clone();
1474        let title = db_thread.title.clone();
1475        let workspace = self.workspace.clone();
1476
1477        cx.spawn_in(window, async move |this, cx| {
1478            thread_store
1479                .update(&mut cx.clone(), |store, cx| {
1480                    store.save_thread(session_id.clone(), db_thread, cx)
1481                })
1482                .await?;
1483
1484            let thread_metadata = acp_thread::AgentSessionInfo {
1485                session_id,
1486                cwd: None,
1487                title: Some(title),
1488                updated_at: Some(chrono::Utc::now()),
1489                meta: None,
1490            };
1491
1492            this.update_in(cx, |this, window, cx| {
1493                this.open_thread(thread_metadata, window, cx);
1494            })?;
1495
1496            this.update_in(cx, |_, _window, cx| {
1497                if let Some(workspace) = workspace.upgrade() {
1498                    workspace.update(cx, |workspace, cx| {
1499                        struct ThreadLoadedToast;
1500                        workspace.show_toast(
1501                            workspace::Toast::new(
1502                                workspace::notifications::NotificationId::unique::<ThreadLoadedToast>(),
1503                                "Thread loaded from clipboard",
1504                            )
1505                            .autohide(),
1506                            cx,
1507                        );
1508                    });
1509                }
1510            })?;
1511
1512            anyhow::Ok(())
1513        })
1514        .detach_and_log_err(cx);
1515    }
1516
1517    fn handle_agent_configuration_event(
1518        &mut self,
1519        _entity: &Entity<AgentConfiguration>,
1520        event: &AssistantConfigurationEvent,
1521        window: &mut Window,
1522        cx: &mut Context<Self>,
1523    ) {
1524        match event {
1525            AssistantConfigurationEvent::NewThread(provider) => {
1526                if LanguageModelRegistry::read_global(cx)
1527                    .default_model()
1528                    .is_none_or(|model| model.provider.id() != provider.id())
1529                    && let Some(model) = provider.default_model(cx)
1530                {
1531                    update_settings_file(self.fs.clone(), cx, move |settings, _| {
1532                        let provider = model.provider_id().0.to_string();
1533                        let enable_thinking = model.supports_thinking();
1534                        let effort = model
1535                            .default_effort_level()
1536                            .map(|effort| effort.value.to_string());
1537                        let model = model.id().0.to_string();
1538                        settings
1539                            .agent
1540                            .get_or_insert_default()
1541                            .set_model(LanguageModelSelection {
1542                                provider: LanguageModelProviderSetting(provider),
1543                                model,
1544                                enable_thinking,
1545                                effort,
1546                            })
1547                    });
1548                }
1549
1550                self.new_thread(&NewThread, window, cx);
1551                if let Some((thread, model)) = self
1552                    .active_native_agent_thread(cx)
1553                    .zip(provider.default_model(cx))
1554                {
1555                    thread.update(cx, |thread, cx| {
1556                        thread.set_model(model, cx);
1557                    });
1558                }
1559            }
1560        }
1561    }
1562
1563    pub fn as_active_server_view(&self) -> Option<&Entity<AcpServerView>> {
1564        match &self.active_view {
1565            ActiveView::AgentThread { server_view } => Some(server_view),
1566            _ => None,
1567        }
1568    }
1569
1570    pub fn as_active_thread_view(&self, cx: &App) -> Option<Entity<AcpThreadView>> {
1571        let server_view = self.as_active_server_view()?;
1572        server_view.read(cx).active_thread().cloned()
1573    }
1574
1575    pub fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
1576        match &self.active_view {
1577            ActiveView::AgentThread { server_view, .. } => server_view
1578                .read(cx)
1579                .active_thread()
1580                .map(|r| r.read(cx).thread.clone()),
1581            _ => None,
1582        }
1583    }
1584
1585    pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
1586        match &self.active_view {
1587            ActiveView::AgentThread { server_view, .. } => {
1588                server_view.read(cx).as_native_thread(cx)
1589            }
1590            _ => None,
1591        }
1592    }
1593
1594    pub(crate) fn active_text_thread_editor(&self) -> Option<Entity<TextThreadEditor>> {
1595        match &self.active_view {
1596            ActiveView::TextThread {
1597                text_thread_editor, ..
1598            } => Some(text_thread_editor.clone()),
1599            _ => None,
1600        }
1601    }
1602
1603    fn set_active_view(
1604        &mut self,
1605        new_view: ActiveView,
1606        focus: bool,
1607        window: &mut Window,
1608        cx: &mut Context<Self>,
1609    ) {
1610        let was_in_agent_history = matches!(
1611            self.active_view,
1612            ActiveView::History {
1613                kind: HistoryKind::AgentThreads
1614            }
1615        );
1616        let current_is_uninitialized = matches!(self.active_view, ActiveView::Uninitialized);
1617        let current_is_history = matches!(self.active_view, ActiveView::History { .. });
1618        let new_is_history = matches!(new_view, ActiveView::History { .. });
1619
1620        let current_is_config = matches!(self.active_view, ActiveView::Configuration);
1621        let new_is_config = matches!(new_view, ActiveView::Configuration);
1622
1623        let current_is_special = current_is_history || current_is_config;
1624        let new_is_special = new_is_history || new_is_config;
1625
1626        if current_is_uninitialized || (current_is_special && !new_is_special) {
1627            self.active_view = new_view;
1628        } else if !current_is_special && new_is_special {
1629            self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1630        } else {
1631            if !new_is_special {
1632                self.previous_view = None;
1633            }
1634            self.active_view = new_view;
1635        }
1636
1637        self._active_view_observation = match &self.active_view {
1638            ActiveView::AgentThread { server_view } => {
1639                Some(cx.observe(server_view, |this, _, cx| {
1640                    cx.emit(AgentPanelEvent::ActiveViewChanged);
1641                    this.serialize(cx);
1642                    cx.notify();
1643                }))
1644            }
1645            _ => None,
1646        };
1647
1648        let is_in_agent_history = matches!(
1649            self.active_view,
1650            ActiveView::History {
1651                kind: HistoryKind::AgentThreads
1652            }
1653        );
1654
1655        if !was_in_agent_history && is_in_agent_history {
1656            self.acp_history
1657                .update(cx, |history, cx| history.refresh_full_history(cx));
1658        }
1659
1660        if focus {
1661            self.focus_handle(cx).focus(window, cx);
1662        }
1663        cx.emit(AgentPanelEvent::ActiveViewChanged);
1664    }
1665
1666    fn populate_recently_updated_menu_section(
1667        mut menu: ContextMenu,
1668        panel: Entity<Self>,
1669        kind: HistoryKind,
1670        cx: &mut Context<ContextMenu>,
1671    ) -> ContextMenu {
1672        match kind {
1673            HistoryKind::AgentThreads => {
1674                let entries = panel
1675                    .read(cx)
1676                    .acp_history
1677                    .read(cx)
1678                    .sessions()
1679                    .iter()
1680                    .take(RECENTLY_UPDATED_MENU_LIMIT)
1681                    .cloned()
1682                    .collect::<Vec<_>>();
1683
1684                if entries.is_empty() {
1685                    return menu;
1686                }
1687
1688                menu = menu.header("Recently Updated");
1689
1690                for entry in entries {
1691                    let title = entry
1692                        .title
1693                        .as_ref()
1694                        .filter(|title| !title.is_empty())
1695                        .cloned()
1696                        .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE));
1697
1698                    menu = menu.entry(title, None, {
1699                        let panel = panel.downgrade();
1700                        let entry = entry.clone();
1701                        move |window, cx| {
1702                            let entry = entry.clone();
1703                            panel
1704                                .update(cx, move |this, cx| {
1705                                    this.load_agent_thread(entry.clone(), window, cx);
1706                                })
1707                                .ok();
1708                        }
1709                    });
1710                }
1711            }
1712            HistoryKind::TextThreads => {
1713                let entries = panel
1714                    .read(cx)
1715                    .text_thread_store
1716                    .read(cx)
1717                    .ordered_text_threads()
1718                    .take(RECENTLY_UPDATED_MENU_LIMIT)
1719                    .cloned()
1720                    .collect::<Vec<_>>();
1721
1722                if entries.is_empty() {
1723                    return menu;
1724                }
1725
1726                menu = menu.header("Recent Text Threads");
1727
1728                for entry in entries {
1729                    let title = if entry.title.is_empty() {
1730                        SharedString::new_static(DEFAULT_THREAD_TITLE)
1731                    } else {
1732                        entry.title.clone()
1733                    };
1734
1735                    menu = menu.entry(title, None, {
1736                        let panel = panel.downgrade();
1737                        let entry = entry.clone();
1738                        move |window, cx| {
1739                            let path = entry.path.clone();
1740                            panel
1741                                .update(cx, move |this, cx| {
1742                                    this.open_saved_text_thread(path.clone(), window, cx)
1743                                        .detach_and_log_err(cx);
1744                                })
1745                                .ok();
1746                        }
1747                    });
1748                }
1749            }
1750        }
1751
1752        menu.separator()
1753    }
1754
1755    pub fn selected_agent(&self) -> AgentType {
1756        self.selected_agent.clone()
1757    }
1758
1759    fn selected_external_agent(&self) -> Option<ExternalAgent> {
1760        match &self.selected_agent {
1761            AgentType::NativeAgent => Some(ExternalAgent::NativeAgent),
1762            AgentType::Gemini => Some(ExternalAgent::Gemini),
1763            AgentType::ClaudeAgent => Some(ExternalAgent::ClaudeCode),
1764            AgentType::Codex => Some(ExternalAgent::Codex),
1765            AgentType::Custom { name } => Some(ExternalAgent::Custom { name: name.clone() }),
1766            AgentType::TextThread => None,
1767        }
1768    }
1769
1770    fn sync_agent_servers_from_extensions(&mut self, cx: &mut Context<Self>) {
1771        if let Some(extension_store) = ExtensionStore::try_global(cx) {
1772            let (manifests, extensions_dir) = {
1773                let store = extension_store.read(cx);
1774                let installed = store.installed_extensions();
1775                let manifests: Vec<_> = installed
1776                    .iter()
1777                    .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
1778                    .collect();
1779                let extensions_dir = paths::extensions_dir().join("installed");
1780                (manifests, extensions_dir)
1781            };
1782
1783            self.project.update(cx, |project, cx| {
1784                project.agent_server_store().update(cx, |store, cx| {
1785                    let manifest_refs: Vec<_> = manifests
1786                        .iter()
1787                        .map(|(id, manifest)| (id.as_ref(), manifest.as_ref()))
1788                        .collect();
1789                    store.sync_extension_agents(manifest_refs, extensions_dir, cx);
1790                });
1791            });
1792        }
1793    }
1794
1795    pub fn new_external_thread_with_text(
1796        &mut self,
1797        initial_text: Option<String>,
1798        window: &mut Window,
1799        cx: &mut Context<Self>,
1800    ) {
1801        self.external_thread(
1802            None,
1803            None,
1804            initial_text.map(|text| AgentInitialContent::ContentBlock {
1805                blocks: vec![acp::ContentBlock::Text(acp::TextContent::new(text))],
1806                auto_submit: false,
1807            }),
1808            window,
1809            cx,
1810        );
1811    }
1812
1813    pub fn new_agent_thread(
1814        &mut self,
1815        agent: AgentType,
1816        window: &mut Window,
1817        cx: &mut Context<Self>,
1818    ) {
1819        match agent {
1820            AgentType::TextThread => {
1821                window.dispatch_action(NewTextThread.boxed_clone(), cx);
1822            }
1823            AgentType::NativeAgent => self.external_thread(
1824                Some(crate::ExternalAgent::NativeAgent),
1825                None,
1826                None,
1827                window,
1828                cx,
1829            ),
1830            AgentType::Gemini => {
1831                self.external_thread(Some(crate::ExternalAgent::Gemini), None, None, window, cx)
1832            }
1833            AgentType::ClaudeAgent => {
1834                self.selected_agent = AgentType::ClaudeAgent;
1835                self.serialize(cx);
1836                self.external_thread(
1837                    Some(crate::ExternalAgent::ClaudeCode),
1838                    None,
1839                    None,
1840                    window,
1841                    cx,
1842                )
1843            }
1844            AgentType::Codex => {
1845                self.selected_agent = AgentType::Codex;
1846                self.serialize(cx);
1847                self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx)
1848            }
1849            AgentType::Custom { name } => self.external_thread(
1850                Some(crate::ExternalAgent::Custom { name }),
1851                None,
1852                None,
1853                window,
1854                cx,
1855            ),
1856        }
1857    }
1858
1859    pub fn load_agent_thread(
1860        &mut self,
1861        thread: AgentSessionInfo,
1862        window: &mut Window,
1863        cx: &mut Context<Self>,
1864    ) {
1865        let Some(agent) = self.selected_external_agent() else {
1866            return;
1867        };
1868        self.external_thread(Some(agent), Some(thread), None, window, cx);
1869    }
1870
1871    fn _external_thread(
1872        &mut self,
1873        server: Rc<dyn AgentServer>,
1874        resume_thread: Option<AgentSessionInfo>,
1875        initial_content: Option<AgentInitialContent>,
1876        workspace: WeakEntity<Workspace>,
1877        project: Entity<Project>,
1878        ext_agent: ExternalAgent,
1879        window: &mut Window,
1880        cx: &mut Context<Self>,
1881    ) {
1882        let selected_agent = AgentType::from(ext_agent);
1883        if self.selected_agent != selected_agent {
1884            self.selected_agent = selected_agent;
1885            self.serialize(cx);
1886        }
1887        let thread_store = server
1888            .clone()
1889            .downcast::<agent::NativeAgentServer>()
1890            .is_some()
1891            .then(|| self.thread_store.clone());
1892
1893        let server_view = cx.new(|cx| {
1894            crate::acp::AcpServerView::new(
1895                server,
1896                resume_thread,
1897                initial_content,
1898                workspace.clone(),
1899                project,
1900                thread_store,
1901                self.prompt_store.clone(),
1902                self.acp_history.clone(),
1903                window,
1904                cx,
1905            )
1906        });
1907
1908        self.set_active_view(ActiveView::AgentThread { server_view }, true, window, cx);
1909    }
1910}
1911
1912impl Focusable for AgentPanel {
1913    fn focus_handle(&self, cx: &App) -> FocusHandle {
1914        match &self.active_view {
1915            ActiveView::Uninitialized => self.focus_handle.clone(),
1916            ActiveView::AgentThread { server_view, .. } => server_view.focus_handle(cx),
1917            ActiveView::History { kind } => match kind {
1918                HistoryKind::AgentThreads => self.acp_history.focus_handle(cx),
1919                HistoryKind::TextThreads => self.text_thread_history.focus_handle(cx),
1920            },
1921            ActiveView::TextThread {
1922                text_thread_editor, ..
1923            } => text_thread_editor.focus_handle(cx),
1924            ActiveView::Configuration => {
1925                if let Some(configuration) = self.configuration.as_ref() {
1926                    configuration.focus_handle(cx)
1927                } else {
1928                    self.focus_handle.clone()
1929                }
1930            }
1931        }
1932    }
1933}
1934
1935fn agent_panel_dock_position(cx: &App) -> DockPosition {
1936    AgentSettings::get_global(cx).dock.into()
1937}
1938
1939pub enum AgentPanelEvent {
1940    ActiveViewChanged,
1941}
1942
1943impl EventEmitter<PanelEvent> for AgentPanel {}
1944impl EventEmitter<AgentPanelEvent> for AgentPanel {}
1945
1946impl Panel for AgentPanel {
1947    fn persistent_name() -> &'static str {
1948        "AgentPanel"
1949    }
1950
1951    fn panel_key() -> &'static str {
1952        AGENT_PANEL_KEY
1953    }
1954
1955    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
1956        agent_panel_dock_position(cx)
1957    }
1958
1959    fn position_is_valid(&self, position: DockPosition) -> bool {
1960        position != DockPosition::Bottom
1961    }
1962
1963    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
1964        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
1965            settings
1966                .agent
1967                .get_or_insert_default()
1968                .set_dock(position.into());
1969        });
1970    }
1971
1972    fn size(&self, window: &Window, cx: &App) -> Pixels {
1973        let settings = AgentSettings::get_global(cx);
1974        match self.position(window, cx) {
1975            DockPosition::Left | DockPosition::Right => {
1976                self.width.unwrap_or(settings.default_width)
1977            }
1978            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
1979        }
1980    }
1981
1982    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
1983        match self.position(window, cx) {
1984            DockPosition::Left | DockPosition::Right => self.width = size,
1985            DockPosition::Bottom => self.height = size,
1986        }
1987        self.serialize(cx);
1988        cx.notify();
1989    }
1990
1991    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
1992        if active && matches!(self.active_view, ActiveView::Uninitialized) {
1993            let selected_agent = self.selected_agent.clone();
1994            self.new_agent_thread(selected_agent, window, cx);
1995        }
1996    }
1997
1998    fn remote_id() -> Option<proto::PanelId> {
1999        Some(proto::PanelId::AssistantPanel)
2000    }
2001
2002    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
2003        (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
2004    }
2005
2006    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
2007        Some("Agent Panel")
2008    }
2009
2010    fn toggle_action(&self) -> Box<dyn Action> {
2011        Box::new(ToggleFocus)
2012    }
2013
2014    fn activation_priority(&self) -> u32 {
2015        3
2016    }
2017
2018    fn enabled(&self, cx: &App) -> bool {
2019        AgentSettings::get_global(cx).enabled(cx)
2020    }
2021
2022    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
2023        self.zoomed
2024    }
2025
2026    fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
2027        self.zoomed = zoomed;
2028        cx.notify();
2029    }
2030}
2031
2032impl AgentPanel {
2033    fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
2034        const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
2035
2036        let content = match &self.active_view {
2037            ActiveView::AgentThread { server_view } => {
2038                let is_generating_title = server_view
2039                    .read(cx)
2040                    .as_native_thread(cx)
2041                    .map_or(false, |t| t.read(cx).is_generating_title());
2042
2043                if let Some(title_editor) = server_view
2044                    .read(cx)
2045                    .parent_thread(cx)
2046                    .map(|r| r.read(cx).title_editor.clone())
2047                {
2048                    let container = div()
2049                        .w_full()
2050                        .on_action({
2051                            let thread_view = server_view.downgrade();
2052                            move |_: &menu::Confirm, window, cx| {
2053                                if let Some(thread_view) = thread_view.upgrade() {
2054                                    thread_view.focus_handle(cx).focus(window, cx);
2055                                }
2056                            }
2057                        })
2058                        .on_action({
2059                            let thread_view = server_view.downgrade();
2060                            move |_: &editor::actions::Cancel, window, cx| {
2061                                if let Some(thread_view) = thread_view.upgrade() {
2062                                    thread_view.focus_handle(cx).focus(window, cx);
2063                                }
2064                            }
2065                        })
2066                        .child(title_editor);
2067
2068                    if is_generating_title {
2069                        container
2070                            .with_animation(
2071                                "generating_title",
2072                                Animation::new(Duration::from_secs(2))
2073                                    .repeat()
2074                                    .with_easing(pulsating_between(0.4, 0.8)),
2075                                |div, delta| div.opacity(delta),
2076                            )
2077                            .into_any_element()
2078                    } else {
2079                        container.into_any_element()
2080                    }
2081                } else {
2082                    Label::new(server_view.read(cx).title(cx))
2083                        .color(Color::Muted)
2084                        .truncate()
2085                        .into_any_element()
2086                }
2087            }
2088            ActiveView::TextThread {
2089                title_editor,
2090                text_thread_editor,
2091                ..
2092            } => {
2093                let summary = text_thread_editor.read(cx).text_thread().read(cx).summary();
2094
2095                match summary {
2096                    TextThreadSummary::Pending => Label::new(TextThreadSummary::DEFAULT)
2097                        .color(Color::Muted)
2098                        .truncate()
2099                        .into_any_element(),
2100                    TextThreadSummary::Content(summary) => {
2101                        if summary.done {
2102                            div()
2103                                .w_full()
2104                                .child(title_editor.clone())
2105                                .into_any_element()
2106                        } else {
2107                            Label::new(LOADING_SUMMARY_PLACEHOLDER)
2108                                .truncate()
2109                                .color(Color::Muted)
2110                                .with_animation(
2111                                    "generating_title",
2112                                    Animation::new(Duration::from_secs(2))
2113                                        .repeat()
2114                                        .with_easing(pulsating_between(0.4, 0.8)),
2115                                    |label, delta| label.alpha(delta),
2116                                )
2117                                .into_any_element()
2118                        }
2119                    }
2120                    TextThreadSummary::Error => h_flex()
2121                        .w_full()
2122                        .child(title_editor.clone())
2123                        .child(
2124                            IconButton::new("retry-summary-generation", IconName::RotateCcw)
2125                                .icon_size(IconSize::Small)
2126                                .on_click({
2127                                    let text_thread_editor = text_thread_editor.clone();
2128                                    move |_, _window, cx| {
2129                                        text_thread_editor.update(cx, |text_thread_editor, cx| {
2130                                            text_thread_editor.regenerate_summary(cx);
2131                                        });
2132                                    }
2133                                })
2134                                .tooltip(move |_window, cx| {
2135                                    cx.new(|_| {
2136                                        Tooltip::new("Failed to generate title")
2137                                            .meta("Click to try again")
2138                                    })
2139                                    .into()
2140                                }),
2141                        )
2142                        .into_any_element(),
2143                }
2144            }
2145            ActiveView::History { kind } => {
2146                let title = match kind {
2147                    HistoryKind::AgentThreads => "History",
2148                    HistoryKind::TextThreads => "Text Thread History",
2149                };
2150                Label::new(title).truncate().into_any_element()
2151            }
2152            ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
2153            ActiveView::Uninitialized => Label::new("Agent").truncate().into_any_element(),
2154        };
2155
2156        h_flex()
2157            .key_context("TitleEditor")
2158            .id("TitleEditor")
2159            .flex_grow()
2160            .w_full()
2161            .max_w_full()
2162            .overflow_x_scroll()
2163            .child(content)
2164            .into_any()
2165    }
2166
2167    fn handle_regenerate_thread_title(thread_view: Entity<AcpServerView>, cx: &mut App) {
2168        thread_view.update(cx, |thread_view, cx| {
2169            if let Some(thread) = thread_view.as_native_thread(cx) {
2170                thread.update(cx, |thread, cx| {
2171                    thread.generate_title(cx);
2172                });
2173            }
2174        });
2175    }
2176
2177    fn handle_regenerate_text_thread_title(
2178        text_thread_editor: Entity<TextThreadEditor>,
2179        cx: &mut App,
2180    ) {
2181        text_thread_editor.update(cx, |text_thread_editor, cx| {
2182            text_thread_editor.regenerate_summary(cx);
2183        });
2184    }
2185
2186    fn render_panel_options_menu(
2187        &self,
2188        window: &mut Window,
2189        cx: &mut Context<Self>,
2190    ) -> impl IntoElement {
2191        let focus_handle = self.focus_handle(cx);
2192
2193        let full_screen_label = if self.is_zoomed(window, cx) {
2194            "Disable Full Screen"
2195        } else {
2196            "Enable Full Screen"
2197        };
2198
2199        let selected_agent = self.selected_agent.clone();
2200
2201        let text_thread_view = match &self.active_view {
2202            ActiveView::TextThread {
2203                text_thread_editor, ..
2204            } => Some(text_thread_editor.clone()),
2205            _ => None,
2206        };
2207        let text_thread_with_messages = match &self.active_view {
2208            ActiveView::TextThread {
2209                text_thread_editor, ..
2210            } => text_thread_editor
2211                .read(cx)
2212                .text_thread()
2213                .read(cx)
2214                .messages(cx)
2215                .any(|message| message.role == language_model::Role::Assistant),
2216            _ => false,
2217        };
2218
2219        let thread_view = match &self.active_view {
2220            ActiveView::AgentThread { server_view } => Some(server_view.clone()),
2221            _ => None,
2222        };
2223        let thread_with_messages = match &self.active_view {
2224            ActiveView::AgentThread { server_view } => {
2225                server_view.read(cx).has_user_submitted_prompt(cx)
2226            }
2227            _ => false,
2228        };
2229
2230        PopoverMenu::new("agent-options-menu")
2231            .trigger_with_tooltip(
2232                IconButton::new("agent-options-menu", IconName::Ellipsis)
2233                    .icon_size(IconSize::Small),
2234                {
2235                    let focus_handle = focus_handle.clone();
2236                    move |_window, cx| {
2237                        Tooltip::for_action_in(
2238                            "Toggle Agent Menu",
2239                            &ToggleOptionsMenu,
2240                            &focus_handle,
2241                            cx,
2242                        )
2243                    }
2244                },
2245            )
2246            .anchor(Corner::TopRight)
2247            .with_handle(self.agent_panel_menu_handle.clone())
2248            .menu({
2249                move |window, cx| {
2250                    Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
2251                        menu = menu.context(focus_handle.clone());
2252
2253                        if thread_with_messages | text_thread_with_messages {
2254                            menu = menu.header("Current Thread");
2255
2256                            if let Some(text_thread_view) = text_thread_view.as_ref() {
2257                                menu = menu
2258                                    .entry("Regenerate Thread Title", None, {
2259                                        let text_thread_view = text_thread_view.clone();
2260                                        move |_, cx| {
2261                                            Self::handle_regenerate_text_thread_title(
2262                                                text_thread_view.clone(),
2263                                                cx,
2264                                            );
2265                                        }
2266                                    })
2267                                    .separator();
2268                            }
2269
2270                            if let Some(thread_view) = thread_view.as_ref() {
2271                                menu = menu
2272                                    .entry("Regenerate Thread Title", None, {
2273                                        let thread_view = thread_view.clone();
2274                                        move |_, cx| {
2275                                            Self::handle_regenerate_thread_title(
2276                                                thread_view.clone(),
2277                                                cx,
2278                                            );
2279                                        }
2280                                    })
2281                                    .separator();
2282                            }
2283                        }
2284
2285                        menu = menu
2286                            .header("MCP Servers")
2287                            .action(
2288                                "View Server Extensions",
2289                                Box::new(zed_actions::Extensions {
2290                                    category_filter: Some(
2291                                        zed_actions::ExtensionCategoryFilter::ContextServers,
2292                                    ),
2293                                    id: None,
2294                                }),
2295                            )
2296                            .action("Add Custom Server…", Box::new(AddContextServer))
2297                            .separator()
2298                            .action("Rules", Box::new(OpenRulesLibrary::default()))
2299                            .action("Profiles", Box::new(ManageProfiles::default()))
2300                            .action("Settings", Box::new(OpenSettings))
2301                            .separator()
2302                            .action(full_screen_label, Box::new(ToggleZoom));
2303
2304                        if selected_agent == AgentType::Gemini {
2305                            menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
2306                        }
2307
2308                        menu
2309                    }))
2310                }
2311            })
2312    }
2313
2314    fn render_recent_entries_menu(
2315        &self,
2316        icon: IconName,
2317        corner: Corner,
2318        cx: &mut Context<Self>,
2319    ) -> impl IntoElement {
2320        let focus_handle = self.focus_handle(cx);
2321
2322        PopoverMenu::new("agent-nav-menu")
2323            .trigger_with_tooltip(
2324                IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
2325                {
2326                    move |_window, cx| {
2327                        Tooltip::for_action_in(
2328                            "Toggle Recently Updated Threads",
2329                            &ToggleNavigationMenu,
2330                            &focus_handle,
2331                            cx,
2332                        )
2333                    }
2334                },
2335            )
2336            .anchor(corner)
2337            .with_handle(self.agent_navigation_menu_handle.clone())
2338            .menu({
2339                let menu = self.agent_navigation_menu.clone();
2340                move |window, cx| {
2341                    telemetry::event!("View Thread History Clicked");
2342
2343                    if let Some(menu) = menu.as_ref() {
2344                        menu.update(cx, |_, cx| {
2345                            cx.defer_in(window, |menu, window, cx| {
2346                                menu.rebuild(window, cx);
2347                            });
2348                        })
2349                    }
2350                    menu.clone()
2351                }
2352            })
2353    }
2354
2355    fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
2356        let focus_handle = self.focus_handle(cx);
2357
2358        IconButton::new("go-back", IconName::ArrowLeft)
2359            .icon_size(IconSize::Small)
2360            .on_click(cx.listener(|this, _, window, cx| {
2361                this.go_back(&workspace::GoBack, window, cx);
2362            }))
2363            .tooltip({
2364                move |_window, cx| {
2365                    Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
2366                }
2367            })
2368    }
2369
2370    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
2371        let agent_server_store = self.project.read(cx).agent_server_store().clone();
2372        let focus_handle = self.focus_handle(cx);
2373
2374        let (selected_agent_custom_icon, selected_agent_label) =
2375            if let AgentType::Custom { name, .. } = &self.selected_agent {
2376                let store = agent_server_store.read(cx);
2377                let icon = store.agent_icon(&ExternalAgentServerName(name.clone()));
2378
2379                let label = store
2380                    .agent_display_name(&ExternalAgentServerName(name.clone()))
2381                    .unwrap_or_else(|| self.selected_agent.label());
2382                (icon, label)
2383            } else {
2384                (None, self.selected_agent.label())
2385            };
2386
2387        let active_thread = match &self.active_view {
2388            ActiveView::AgentThread { server_view } => server_view.read(cx).as_native_thread(cx),
2389            ActiveView::Uninitialized
2390            | ActiveView::TextThread { .. }
2391            | ActiveView::History { .. }
2392            | ActiveView::Configuration => None,
2393        };
2394
2395        let new_thread_menu = PopoverMenu::new("new_thread_menu")
2396            .trigger_with_tooltip(
2397                IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
2398                {
2399                    let focus_handle = focus_handle.clone();
2400                    move |_window, cx| {
2401                        Tooltip::for_action_in(
2402                            "New Thread…",
2403                            &ToggleNewThreadMenu,
2404                            &focus_handle,
2405                            cx,
2406                        )
2407                    }
2408                },
2409            )
2410            .anchor(Corner::TopRight)
2411            .with_handle(self.new_thread_menu_handle.clone())
2412            .menu({
2413                let selected_agent = self.selected_agent.clone();
2414                let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
2415
2416                let workspace = self.workspace.clone();
2417                let is_via_collab = workspace
2418                    .update(cx, |workspace, cx| {
2419                        workspace.project().read(cx).is_via_collab()
2420                    })
2421                    .unwrap_or_default();
2422
2423                move |window, cx| {
2424                    telemetry::event!("New Thread Clicked");
2425
2426                    let active_thread = active_thread.clone();
2427                    Some(ContextMenu::build(window, cx, |menu, _window, cx| {
2428                        menu.context(focus_handle.clone())
2429                            .when_some(active_thread, |this, active_thread| {
2430                                let thread = active_thread.read(cx);
2431
2432                                if !thread.is_empty() {
2433                                    let session_id = thread.id().clone();
2434                                    this.item(
2435                                        ContextMenuEntry::new("New From Summary")
2436                                            .icon(IconName::ThreadFromSummary)
2437                                            .icon_color(Color::Muted)
2438                                            .handler(move |window, cx| {
2439                                                window.dispatch_action(
2440                                                    Box::new(NewNativeAgentThreadFromSummary {
2441                                                        from_session_id: session_id.clone(),
2442                                                    }),
2443                                                    cx,
2444                                                );
2445                                            }),
2446                                    )
2447                                } else {
2448                                    this
2449                                }
2450                            })
2451                            .item(
2452                                ContextMenuEntry::new("Zed Agent")
2453                                    .when(
2454                                        is_agent_selected(AgentType::NativeAgent)
2455                                            | is_agent_selected(AgentType::TextThread),
2456                                        |this| {
2457                                            this.action(Box::new(NewExternalAgentThread {
2458                                                agent: None,
2459                                            }))
2460                                        },
2461                                    )
2462                                    .icon(IconName::ZedAgent)
2463                                    .icon_color(Color::Muted)
2464                                    .handler({
2465                                        let workspace = workspace.clone();
2466                                        move |window, cx| {
2467                                            if let Some(workspace) = workspace.upgrade() {
2468                                                workspace.update(cx, |workspace, cx| {
2469                                                    if let Some(panel) =
2470                                                        workspace.panel::<AgentPanel>(cx)
2471                                                    {
2472                                                        panel.update(cx, |panel, cx| {
2473                                                            panel.new_agent_thread(
2474                                                                AgentType::NativeAgent,
2475                                                                window,
2476                                                                cx,
2477                                                            );
2478                                                        });
2479                                                    }
2480                                                });
2481                                            }
2482                                        }
2483                                    }),
2484                            )
2485                            .item(
2486                                ContextMenuEntry::new("Text Thread")
2487                                    .action(NewTextThread.boxed_clone())
2488                                    .icon(IconName::TextThread)
2489                                    .icon_color(Color::Muted)
2490                                    .handler({
2491                                        let workspace = workspace.clone();
2492                                        move |window, cx| {
2493                                            if let Some(workspace) = workspace.upgrade() {
2494                                                workspace.update(cx, |workspace, cx| {
2495                                                    if let Some(panel) =
2496                                                        workspace.panel::<AgentPanel>(cx)
2497                                                    {
2498                                                        panel.update(cx, |panel, cx| {
2499                                                            panel.new_agent_thread(
2500                                                                AgentType::TextThread,
2501                                                                window,
2502                                                                cx,
2503                                                            );
2504                                                        });
2505                                                    }
2506                                                });
2507                                            }
2508                                        }
2509                                    }),
2510                            )
2511                            .separator()
2512                            .header("External Agents")
2513                            .item(
2514                                ContextMenuEntry::new("Claude Agent")
2515                                    .when(is_agent_selected(AgentType::ClaudeAgent), |this| {
2516                                        this.action(Box::new(NewExternalAgentThread {
2517                                            agent: None,
2518                                        }))
2519                                    })
2520                                    .icon(IconName::AiClaude)
2521                                    .disabled(is_via_collab)
2522                                    .icon_color(Color::Muted)
2523                                    .handler({
2524                                        let workspace = workspace.clone();
2525                                        move |window, cx| {
2526                                            if let Some(workspace) = workspace.upgrade() {
2527                                                workspace.update(cx, |workspace, cx| {
2528                                                    if let Some(panel) =
2529                                                        workspace.panel::<AgentPanel>(cx)
2530                                                    {
2531                                                        panel.update(cx, |panel, cx| {
2532                                                            panel.new_agent_thread(
2533                                                                AgentType::ClaudeAgent,
2534                                                                window,
2535                                                                cx,
2536                                                            );
2537                                                        });
2538                                                    }
2539                                                });
2540                                            }
2541                                        }
2542                                    }),
2543                            )
2544                            .item(
2545                                ContextMenuEntry::new("Codex CLI")
2546                                    .when(is_agent_selected(AgentType::Codex), |this| {
2547                                        this.action(Box::new(NewExternalAgentThread {
2548                                            agent: None,
2549                                        }))
2550                                    })
2551                                    .icon(IconName::AiOpenAi)
2552                                    .disabled(is_via_collab)
2553                                    .icon_color(Color::Muted)
2554                                    .handler({
2555                                        let workspace = workspace.clone();
2556                                        move |window, cx| {
2557                                            if let Some(workspace) = workspace.upgrade() {
2558                                                workspace.update(cx, |workspace, cx| {
2559                                                    if let Some(panel) =
2560                                                        workspace.panel::<AgentPanel>(cx)
2561                                                    {
2562                                                        panel.update(cx, |panel, cx| {
2563                                                            panel.new_agent_thread(
2564                                                                AgentType::Codex,
2565                                                                window,
2566                                                                cx,
2567                                                            );
2568                                                        });
2569                                                    }
2570                                                });
2571                                            }
2572                                        }
2573                                    }),
2574                            )
2575                            .item(
2576                                ContextMenuEntry::new("Gemini CLI")
2577                                    .when(is_agent_selected(AgentType::Gemini), |this| {
2578                                        this.action(Box::new(NewExternalAgentThread {
2579                                            agent: None,
2580                                        }))
2581                                    })
2582                                    .icon(IconName::AiGemini)
2583                                    .icon_color(Color::Muted)
2584                                    .disabled(is_via_collab)
2585                                    .handler({
2586                                        let workspace = workspace.clone();
2587                                        move |window, cx| {
2588                                            if let Some(workspace) = workspace.upgrade() {
2589                                                workspace.update(cx, |workspace, cx| {
2590                                                    if let Some(panel) =
2591                                                        workspace.panel::<AgentPanel>(cx)
2592                                                    {
2593                                                        panel.update(cx, |panel, cx| {
2594                                                            panel.new_agent_thread(
2595                                                                AgentType::Gemini,
2596                                                                window,
2597                                                                cx,
2598                                                            );
2599                                                        });
2600                                                    }
2601                                                });
2602                                            }
2603                                        }
2604                                    }),
2605                            )
2606                            .map(|mut menu| {
2607                                let agent_server_store = agent_server_store.read(cx);
2608                                let agent_names = agent_server_store
2609                                    .external_agents()
2610                                    .filter(|name| {
2611                                        name.0 != GEMINI_NAME
2612                                            && name.0 != CLAUDE_AGENT_NAME
2613                                            && name.0 != CODEX_NAME
2614                                    })
2615                                    .cloned()
2616                                    .collect::<Vec<_>>();
2617
2618                                for agent_name in agent_names {
2619                                    let icon_path = agent_server_store.agent_icon(&agent_name);
2620                                    let display_name = agent_server_store
2621                                        .agent_display_name(&agent_name)
2622                                        .unwrap_or_else(|| agent_name.0.clone());
2623
2624                                    let mut entry = ContextMenuEntry::new(display_name);
2625
2626                                    if let Some(icon_path) = icon_path {
2627                                        entry = entry.custom_icon_svg(icon_path);
2628                                    } else {
2629                                        entry = entry.icon(IconName::Sparkle);
2630                                    }
2631                                    entry = entry
2632                                        .when(
2633                                            is_agent_selected(AgentType::Custom {
2634                                                name: agent_name.0.clone(),
2635                                            }),
2636                                            |this| {
2637                                                this.action(Box::new(NewExternalAgentThread {
2638                                                    agent: None,
2639                                                }))
2640                                            },
2641                                        )
2642                                        .icon_color(Color::Muted)
2643                                        .disabled(is_via_collab)
2644                                        .handler({
2645                                            let workspace = workspace.clone();
2646                                            let agent_name = agent_name.clone();
2647                                            move |window, cx| {
2648                                                if let Some(workspace) = workspace.upgrade() {
2649                                                    workspace.update(cx, |workspace, cx| {
2650                                                        if let Some(panel) =
2651                                                            workspace.panel::<AgentPanel>(cx)
2652                                                        {
2653                                                            panel.update(cx, |panel, cx| {
2654                                                                panel.new_agent_thread(
2655                                                                    AgentType::Custom {
2656                                                                        name: agent_name
2657                                                                            .clone()
2658                                                                            .into(),
2659                                                                    },
2660                                                                    window,
2661                                                                    cx,
2662                                                                );
2663                                                            });
2664                                                        }
2665                                                    });
2666                                                }
2667                                            }
2668                                        });
2669
2670                                    menu = menu.item(entry);
2671                                }
2672
2673                                menu
2674                            })
2675                            .separator()
2676                            .item(
2677                                ContextMenuEntry::new("Add More Agents")
2678                                    .icon(IconName::Plus)
2679                                    .icon_color(Color::Muted)
2680                                    .handler({
2681                                        move |window, cx| {
2682                                            window.dispatch_action(
2683                                                Box::new(zed_actions::AcpRegistry),
2684                                                cx,
2685                                            )
2686                                        }
2687                                    }),
2688                            )
2689                    }))
2690                }
2691            });
2692
2693        let is_thread_loading = self
2694            .active_thread_view()
2695            .map(|thread| thread.read(cx).is_loading())
2696            .unwrap_or(false);
2697
2698        let has_custom_icon = selected_agent_custom_icon.is_some();
2699
2700        let selected_agent = div()
2701            .id("selected_agent_icon")
2702            .when_some(selected_agent_custom_icon, |this, icon_path| {
2703                this.px_1()
2704                    .child(Icon::from_external_svg(icon_path).color(Color::Muted))
2705            })
2706            .when(!has_custom_icon, |this| {
2707                this.when_some(self.selected_agent.icon(), |this, icon| {
2708                    this.px_1().child(Icon::new(icon).color(Color::Muted))
2709                })
2710            })
2711            .tooltip(move |_, cx| {
2712                Tooltip::with_meta(selected_agent_label.clone(), None, "Selected Agent", cx)
2713            });
2714
2715        let selected_agent = if is_thread_loading {
2716            selected_agent
2717                .with_animation(
2718                    "pulsating-icon",
2719                    Animation::new(Duration::from_secs(1))
2720                        .repeat()
2721                        .with_easing(pulsating_between(0.2, 0.6)),
2722                    |icon, delta| icon.opacity(delta),
2723                )
2724                .into_any_element()
2725        } else {
2726            selected_agent.into_any_element()
2727        };
2728
2729        let show_history_menu = self.history_kind_for_selected_agent(cx).is_some();
2730
2731        h_flex()
2732            .id("agent-panel-toolbar")
2733            .h(Tab::container_height(cx))
2734            .max_w_full()
2735            .flex_none()
2736            .justify_between()
2737            .gap_2()
2738            .bg(cx.theme().colors().tab_bar_background)
2739            .border_b_1()
2740            .border_color(cx.theme().colors().border)
2741            .child(
2742                h_flex()
2743                    .size_full()
2744                    .gap(DynamicSpacing::Base04.rems(cx))
2745                    .pl(DynamicSpacing::Base04.rems(cx))
2746                    .child(match &self.active_view {
2747                        ActiveView::History { .. } | ActiveView::Configuration => {
2748                            self.render_toolbar_back_button(cx).into_any_element()
2749                        }
2750                        _ => selected_agent.into_any_element(),
2751                    })
2752                    .child(self.render_title_view(window, cx)),
2753            )
2754            .child(
2755                h_flex()
2756                    .flex_none()
2757                    .gap(DynamicSpacing::Base02.rems(cx))
2758                    .pl(DynamicSpacing::Base04.rems(cx))
2759                    .pr(DynamicSpacing::Base06.rems(cx))
2760                    .child(new_thread_menu)
2761                    .when(show_history_menu, |this| {
2762                        this.child(self.render_recent_entries_menu(
2763                            IconName::MenuAltTemp,
2764                            Corner::TopRight,
2765                            cx,
2766                        ))
2767                    })
2768                    .child(self.render_panel_options_menu(window, cx)),
2769            )
2770    }
2771
2772    fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
2773        if TrialEndUpsell::dismissed() {
2774            return false;
2775        }
2776
2777        match &self.active_view {
2778            ActiveView::TextThread { .. } => {
2779                if LanguageModelRegistry::global(cx)
2780                    .read(cx)
2781                    .default_model()
2782                    .is_some_and(|model| {
2783                        model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2784                    })
2785                {
2786                    return false;
2787                }
2788            }
2789            ActiveView::Uninitialized
2790            | ActiveView::AgentThread { .. }
2791            | ActiveView::History { .. }
2792            | ActiveView::Configuration => return false,
2793        }
2794
2795        let plan = self.user_store.read(cx).plan();
2796        let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
2797
2798        plan.is_some_and(|plan| plan == Plan::ZedFree) && has_previous_trial
2799    }
2800
2801    fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
2802        if self.on_boarding_upsell_dismissed.load(Ordering::Acquire) {
2803            return false;
2804        }
2805
2806        let user_store = self.user_store.read(cx);
2807
2808        if user_store.plan().is_some_and(|plan| plan == Plan::ZedPro)
2809            && user_store
2810                .subscription_period()
2811                .and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
2812                .is_some_and(|date| date < chrono::Utc::now())
2813        {
2814            OnboardingUpsell::set_dismissed(true, cx);
2815            self.on_boarding_upsell_dismissed
2816                .store(true, Ordering::Release);
2817            return false;
2818        }
2819
2820        match &self.active_view {
2821            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
2822                false
2823            }
2824            ActiveView::AgentThread { server_view, .. }
2825                if server_view.read(cx).as_native_thread(cx).is_none() =>
2826            {
2827                false
2828            }
2829            _ => {
2830                let history_is_empty = self.acp_history.read(cx).is_empty();
2831
2832                let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
2833                    .visible_providers()
2834                    .iter()
2835                    .any(|provider| {
2836                        provider.is_authenticated(cx)
2837                            && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
2838                    });
2839
2840                history_is_empty || !has_configured_non_zed_providers
2841            }
2842        }
2843    }
2844
2845    fn render_onboarding(
2846        &self,
2847        _window: &mut Window,
2848        cx: &mut Context<Self>,
2849    ) -> Option<impl IntoElement> {
2850        if !self.should_render_onboarding(cx) {
2851            return None;
2852        }
2853
2854        let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
2855
2856        Some(
2857            div()
2858                .when(text_thread_view, |this| {
2859                    this.bg(cx.theme().colors().editor_background)
2860                })
2861                .child(self.onboarding.clone()),
2862        )
2863    }
2864
2865    fn render_trial_end_upsell(
2866        &self,
2867        _window: &mut Window,
2868        cx: &mut Context<Self>,
2869    ) -> Option<impl IntoElement> {
2870        if !self.should_render_trial_end_upsell(cx) {
2871            return None;
2872        }
2873
2874        Some(
2875            v_flex()
2876                .absolute()
2877                .inset_0()
2878                .size_full()
2879                .bg(cx.theme().colors().panel_background)
2880                .opacity(0.85)
2881                .block_mouse_except_scroll()
2882                .child(EndTrialUpsell::new(Arc::new({
2883                    let this = cx.entity();
2884                    move |_, cx| {
2885                        this.update(cx, |_this, cx| {
2886                            TrialEndUpsell::set_dismissed(true, cx);
2887                            cx.notify();
2888                        });
2889                    }
2890                }))),
2891        )
2892    }
2893
2894    fn emit_configuration_error_telemetry_if_needed(
2895        &mut self,
2896        configuration_error: Option<&ConfigurationError>,
2897    ) {
2898        let error_kind = configuration_error.map(|err| match err {
2899            ConfigurationError::NoProvider => "no_provider",
2900            ConfigurationError::ModelNotFound => "model_not_found",
2901            ConfigurationError::ProviderNotAuthenticated(_) => "provider_not_authenticated",
2902        });
2903
2904        let error_kind_string = error_kind.map(String::from);
2905
2906        if self.last_configuration_error_telemetry == error_kind_string {
2907            return;
2908        }
2909
2910        self.last_configuration_error_telemetry = error_kind_string;
2911
2912        if let Some(kind) = error_kind {
2913            let message = configuration_error
2914                .map(|err| err.to_string())
2915                .unwrap_or_default();
2916
2917            telemetry::event!("Agent Panel Error Shown", kind = kind, message = message,);
2918        }
2919    }
2920
2921    fn render_configuration_error(
2922        &self,
2923        border_bottom: bool,
2924        configuration_error: &ConfigurationError,
2925        focus_handle: &FocusHandle,
2926        cx: &mut App,
2927    ) -> impl IntoElement {
2928        let zed_provider_configured = AgentSettings::get_global(cx)
2929            .default_model
2930            .as_ref()
2931            .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev");
2932
2933        let callout = if zed_provider_configured {
2934            Callout::new()
2935                .icon(IconName::Warning)
2936                .severity(Severity::Warning)
2937                .when(border_bottom, |this| {
2938                    this.border_position(ui::BorderPosition::Bottom)
2939                })
2940                .title("Sign in to continue using Zed as your LLM provider.")
2941                .actions_slot(
2942                    Button::new("sign_in", "Sign In")
2943                        .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2944                        .label_size(LabelSize::Small)
2945                        .on_click({
2946                            let workspace = self.workspace.clone();
2947                            move |_, _, cx| {
2948                                let Ok(client) =
2949                                    workspace.update(cx, |workspace, _| workspace.client().clone())
2950                                else {
2951                                    return;
2952                                };
2953
2954                                cx.spawn(async move |cx| {
2955                                    client.sign_in_with_optional_connect(true, cx).await
2956                                })
2957                                .detach_and_log_err(cx);
2958                            }
2959                        }),
2960                )
2961        } else {
2962            Callout::new()
2963                .icon(IconName::Warning)
2964                .severity(Severity::Warning)
2965                .when(border_bottom, |this| {
2966                    this.border_position(ui::BorderPosition::Bottom)
2967                })
2968                .title(configuration_error.to_string())
2969                .actions_slot(
2970                    Button::new("settings", "Configure")
2971                        .style(ButtonStyle::Tinted(ui::TintColor::Warning))
2972                        .label_size(LabelSize::Small)
2973                        .key_binding(
2974                            KeyBinding::for_action_in(&OpenSettings, focus_handle, cx)
2975                                .map(|kb| kb.size(rems_from_px(12.))),
2976                        )
2977                        .on_click(|_event, window, cx| {
2978                            window.dispatch_action(OpenSettings.boxed_clone(), cx)
2979                        }),
2980                )
2981        };
2982
2983        match configuration_error {
2984            ConfigurationError::ModelNotFound
2985            | ConfigurationError::ProviderNotAuthenticated(_)
2986            | ConfigurationError::NoProvider => callout.into_any_element(),
2987        }
2988    }
2989
2990    fn render_text_thread(
2991        &self,
2992        text_thread_editor: &Entity<TextThreadEditor>,
2993        buffer_search_bar: &Entity<BufferSearchBar>,
2994        window: &mut Window,
2995        cx: &mut Context<Self>,
2996    ) -> Div {
2997        let mut registrar = buffer_search::DivRegistrar::new(
2998            |this, _, _cx| match &this.active_view {
2999                ActiveView::TextThread {
3000                    buffer_search_bar, ..
3001                } => Some(buffer_search_bar.clone()),
3002                _ => None,
3003            },
3004            cx,
3005        );
3006        BufferSearchBar::register(&mut registrar);
3007        registrar
3008            .into_div()
3009            .size_full()
3010            .relative()
3011            .map(|parent| {
3012                buffer_search_bar.update(cx, |buffer_search_bar, cx| {
3013                    if buffer_search_bar.is_dismissed() {
3014                        return parent;
3015                    }
3016                    parent.child(
3017                        div()
3018                            .p(DynamicSpacing::Base08.rems(cx))
3019                            .border_b_1()
3020                            .border_color(cx.theme().colors().border_variant)
3021                            .bg(cx.theme().colors().editor_background)
3022                            .child(buffer_search_bar.render(window, cx)),
3023                    )
3024                })
3025            })
3026            .child(text_thread_editor.clone())
3027            .child(self.render_drag_target(cx))
3028    }
3029
3030    fn render_drag_target(&self, cx: &Context<Self>) -> Div {
3031        let is_local = self.project.read(cx).is_local();
3032        div()
3033            .invisible()
3034            .absolute()
3035            .top_0()
3036            .right_0()
3037            .bottom_0()
3038            .left_0()
3039            .bg(cx.theme().colors().drop_target_background)
3040            .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
3041            .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
3042            .when(is_local, |this| {
3043                this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
3044            })
3045            .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
3046                let item = tab.pane.read(cx).item_for_index(tab.ix);
3047                let project_paths = item
3048                    .and_then(|item| item.project_path(cx))
3049                    .into_iter()
3050                    .collect::<Vec<_>>();
3051                this.handle_drop(project_paths, vec![], window, cx);
3052            }))
3053            .on_drop(
3054                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
3055                    let project_paths = selection
3056                        .items()
3057                        .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
3058                        .collect::<Vec<_>>();
3059                    this.handle_drop(project_paths, vec![], window, cx);
3060                }),
3061            )
3062            .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
3063                let tasks = paths
3064                    .paths()
3065                    .iter()
3066                    .map(|path| {
3067                        Workspace::project_path_for_path(this.project.clone(), path, false, cx)
3068                    })
3069                    .collect::<Vec<_>>();
3070                cx.spawn_in(window, async move |this, cx| {
3071                    let mut paths = vec![];
3072                    let mut added_worktrees = vec![];
3073                    let opened_paths = futures::future::join_all(tasks).await;
3074                    for entry in opened_paths {
3075                        if let Some((worktree, project_path)) = entry.log_err() {
3076                            added_worktrees.push(worktree);
3077                            paths.push(project_path);
3078                        }
3079                    }
3080                    this.update_in(cx, |this, window, cx| {
3081                        this.handle_drop(paths, added_worktrees, window, cx);
3082                    })
3083                    .ok();
3084                })
3085                .detach();
3086            }))
3087    }
3088
3089    fn handle_drop(
3090        &mut self,
3091        paths: Vec<ProjectPath>,
3092        added_worktrees: Vec<Entity<Worktree>>,
3093        window: &mut Window,
3094        cx: &mut Context<Self>,
3095    ) {
3096        match &self.active_view {
3097            ActiveView::AgentThread { server_view } => {
3098                server_view.update(cx, |thread_view, cx| {
3099                    thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
3100                });
3101            }
3102            ActiveView::TextThread {
3103                text_thread_editor, ..
3104            } => {
3105                text_thread_editor.update(cx, |text_thread_editor, cx| {
3106                    TextThreadEditor::insert_dragged_files(
3107                        text_thread_editor,
3108                        paths,
3109                        added_worktrees,
3110                        window,
3111                        cx,
3112                    );
3113                });
3114            }
3115            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
3116        }
3117    }
3118
3119    fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
3120        if !self.show_trust_workspace_message {
3121            return None;
3122        }
3123
3124        let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
3125
3126        Some(
3127            Callout::new()
3128                .icon(IconName::Warning)
3129                .severity(Severity::Warning)
3130                .border_position(ui::BorderPosition::Bottom)
3131                .title("You're in Restricted Mode")
3132                .description(description)
3133                .actions_slot(
3134                    Button::new("open-trust-modal", "Configure Project Trust")
3135                        .label_size(LabelSize::Small)
3136                        .style(ButtonStyle::Outlined)
3137                        .on_click({
3138                            cx.listener(move |this, _, window, cx| {
3139                                this.workspace
3140                                    .update(cx, |workspace, cx| {
3141                                        workspace
3142                                            .show_worktree_trust_security_modal(true, window, cx)
3143                                    })
3144                                    .log_err();
3145                            })
3146                        }),
3147                ),
3148        )
3149    }
3150
3151    fn key_context(&self) -> KeyContext {
3152        let mut key_context = KeyContext::new_with_defaults();
3153        key_context.add("AgentPanel");
3154        match &self.active_view {
3155            ActiveView::AgentThread { .. } => key_context.add("acp_thread"),
3156            ActiveView::TextThread { .. } => key_context.add("text_thread"),
3157            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
3158        }
3159        key_context
3160    }
3161}
3162
3163impl Render for AgentPanel {
3164    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3165        // WARNING: Changes to this element hierarchy can have
3166        // non-obvious implications to the layout of children.
3167        //
3168        // If you need to change it, please confirm:
3169        // - The message editor expands (cmd-option-esc) correctly
3170        // - When expanded, the buttons at the bottom of the panel are displayed correctly
3171        // - Font size works as expected and can be changed with cmd-+/cmd-
3172        // - Scrolling in all views works as expected
3173        // - Files can be dropped into the panel
3174        let content = v_flex()
3175            .relative()
3176            .size_full()
3177            .justify_between()
3178            .key_context(self.key_context())
3179            .on_action(cx.listener(|this, action: &NewThread, window, cx| {
3180                this.new_thread(action, window, cx);
3181            }))
3182            .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
3183                this.open_history(window, cx);
3184            }))
3185            .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
3186                this.open_configuration(window, cx);
3187            }))
3188            .on_action(cx.listener(Self::open_active_thread_as_markdown))
3189            .on_action(cx.listener(Self::deploy_rules_library))
3190            .on_action(cx.listener(Self::go_back))
3191            .on_action(cx.listener(Self::toggle_navigation_menu))
3192            .on_action(cx.listener(Self::toggle_options_menu))
3193            .on_action(cx.listener(Self::increase_font_size))
3194            .on_action(cx.listener(Self::decrease_font_size))
3195            .on_action(cx.listener(Self::reset_font_size))
3196            .on_action(cx.listener(Self::toggle_zoom))
3197            .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
3198                if let Some(thread_view) = this.active_thread_view() {
3199                    thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
3200                }
3201            }))
3202            .child(self.render_toolbar(window, cx))
3203            .children(self.render_workspace_trust_message(cx))
3204            .children(self.render_onboarding(window, cx))
3205            .map(|parent| {
3206                // Emit configuration error telemetry before entering the match to avoid borrow conflicts
3207                if matches!(&self.active_view, ActiveView::TextThread { .. }) {
3208                    let model_registry = LanguageModelRegistry::read_global(cx);
3209                    let configuration_error =
3210                        model_registry.configuration_error(model_registry.default_model(), cx);
3211                    self.emit_configuration_error_telemetry_if_needed(configuration_error.as_ref());
3212                }
3213
3214                match &self.active_view {
3215                    ActiveView::Uninitialized => parent,
3216                    ActiveView::AgentThread { server_view, .. } => parent
3217                        .child(server_view.clone())
3218                        .child(self.render_drag_target(cx)),
3219                    ActiveView::History { kind } => match kind {
3220                        HistoryKind::AgentThreads => parent.child(self.acp_history.clone()),
3221                        HistoryKind::TextThreads => parent.child(self.text_thread_history.clone()),
3222                    },
3223                    ActiveView::TextThread {
3224                        text_thread_editor,
3225                        buffer_search_bar,
3226                        ..
3227                    } => {
3228                        let model_registry = LanguageModelRegistry::read_global(cx);
3229                        let configuration_error =
3230                            model_registry.configuration_error(model_registry.default_model(), cx);
3231
3232                        parent
3233                            .map(|this| {
3234                                if !self.should_render_onboarding(cx)
3235                                    && let Some(err) = configuration_error.as_ref()
3236                                {
3237                                    this.child(self.render_configuration_error(
3238                                        true,
3239                                        err,
3240                                        &self.focus_handle(cx),
3241                                        cx,
3242                                    ))
3243                                } else {
3244                                    this
3245                                }
3246                            })
3247                            .child(self.render_text_thread(
3248                                text_thread_editor,
3249                                buffer_search_bar,
3250                                window,
3251                                cx,
3252                            ))
3253                    }
3254                    ActiveView::Configuration => parent.children(self.configuration.clone()),
3255                }
3256            })
3257            .children(self.render_trial_end_upsell(window, cx));
3258
3259        match self.active_view.which_font_size_used() {
3260            WhichFontSize::AgentFont => {
3261                WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
3262                    .size_full()
3263                    .child(content)
3264                    .into_any()
3265            }
3266            _ => content.into_any(),
3267        }
3268    }
3269}
3270
3271struct PromptLibraryInlineAssist {
3272    workspace: WeakEntity<Workspace>,
3273}
3274
3275impl PromptLibraryInlineAssist {
3276    pub fn new(workspace: WeakEntity<Workspace>) -> Self {
3277        Self { workspace }
3278    }
3279}
3280
3281impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
3282    fn assist(
3283        &self,
3284        prompt_editor: &Entity<Editor>,
3285        initial_prompt: Option<String>,
3286        window: &mut Window,
3287        cx: &mut Context<RulesLibrary>,
3288    ) {
3289        InlineAssistant::update_global(cx, |assistant, cx| {
3290            let Some(workspace) = self.workspace.upgrade() else {
3291                return;
3292            };
3293            let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
3294                return;
3295            };
3296            let project = workspace.read(cx).project().downgrade();
3297            let panel = panel.read(cx);
3298            let thread_store = panel.thread_store().clone();
3299            let history = panel.history().downgrade();
3300            assistant.assist(
3301                prompt_editor,
3302                self.workspace.clone(),
3303                project,
3304                thread_store,
3305                None,
3306                history,
3307                initial_prompt,
3308                window,
3309                cx,
3310            );
3311        })
3312    }
3313
3314    fn focus_agent_panel(
3315        &self,
3316        workspace: &mut Workspace,
3317        window: &mut Window,
3318        cx: &mut Context<Workspace>,
3319    ) -> bool {
3320        workspace.focus_panel::<AgentPanel>(window, cx).is_some()
3321    }
3322}
3323
3324pub struct ConcreteAssistantPanelDelegate;
3325
3326impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
3327    fn active_text_thread_editor(
3328        &self,
3329        workspace: &mut Workspace,
3330        _window: &mut Window,
3331        cx: &mut Context<Workspace>,
3332    ) -> Option<Entity<TextThreadEditor>> {
3333        let panel = workspace.panel::<AgentPanel>(cx)?;
3334        panel.read(cx).active_text_thread_editor()
3335    }
3336
3337    fn open_local_text_thread(
3338        &self,
3339        workspace: &mut Workspace,
3340        path: Arc<Path>,
3341        window: &mut Window,
3342        cx: &mut Context<Workspace>,
3343    ) -> Task<Result<()>> {
3344        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3345            return Task::ready(Err(anyhow!("Agent panel not found")));
3346        };
3347
3348        panel.update(cx, |panel, cx| {
3349            panel.open_saved_text_thread(path, window, cx)
3350        })
3351    }
3352
3353    fn open_remote_text_thread(
3354        &self,
3355        _workspace: &mut Workspace,
3356        _text_thread_id: assistant_text_thread::TextThreadId,
3357        _window: &mut Window,
3358        _cx: &mut Context<Workspace>,
3359    ) -> Task<Result<Entity<TextThreadEditor>>> {
3360        Task::ready(Err(anyhow!("opening remote context not implemented")))
3361    }
3362
3363    fn quote_selection(
3364        &self,
3365        workspace: &mut Workspace,
3366        selection_ranges: Vec<Range<Anchor>>,
3367        buffer: Entity<MultiBuffer>,
3368        window: &mut Window,
3369        cx: &mut Context<Workspace>,
3370    ) {
3371        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3372            return;
3373        };
3374
3375        if !panel.focus_handle(cx).contains_focused(window, cx) {
3376            workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3377        }
3378
3379        panel.update(cx, |_, cx| {
3380            // Wait to create a new context until the workspace is no longer
3381            // being updated.
3382            cx.defer_in(window, move |panel, window, cx| {
3383                if let Some(thread_view) = panel.active_thread_view() {
3384                    thread_view.update(cx, |thread_view, cx| {
3385                        thread_view.insert_selections(window, cx);
3386                    });
3387                } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
3388                    let snapshot = buffer.read(cx).snapshot(cx);
3389                    let selection_ranges = selection_ranges
3390                        .into_iter()
3391                        .map(|range| range.to_point(&snapshot))
3392                        .collect::<Vec<_>>();
3393
3394                    text_thread_editor.update(cx, |text_thread_editor, cx| {
3395                        text_thread_editor.quote_ranges(selection_ranges, snapshot, window, cx)
3396                    });
3397                }
3398            });
3399        });
3400    }
3401
3402    fn quote_terminal_text(
3403        &self,
3404        workspace: &mut Workspace,
3405        text: String,
3406        window: &mut Window,
3407        cx: &mut Context<Workspace>,
3408    ) {
3409        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
3410            return;
3411        };
3412
3413        if !panel.focus_handle(cx).contains_focused(window, cx) {
3414            workspace.toggle_panel_focus::<AgentPanel>(window, cx);
3415        }
3416
3417        panel.update(cx, |_, cx| {
3418            // Wait to create a new context until the workspace is no longer
3419            // being updated.
3420            cx.defer_in(window, move |panel, window, cx| {
3421                if let Some(thread_view) = panel.active_thread_view() {
3422                    thread_view.update(cx, |thread_view, cx| {
3423                        thread_view.insert_terminal_text(text, window, cx);
3424                    });
3425                } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
3426                    text_thread_editor.update(cx, |text_thread_editor, cx| {
3427                        text_thread_editor.quote_terminal_text(text, window, cx)
3428                    });
3429                }
3430            });
3431        });
3432    }
3433}
3434
3435struct OnboardingUpsell;
3436
3437impl Dismissable for OnboardingUpsell {
3438    const KEY: &'static str = "dismissed-trial-upsell";
3439}
3440
3441struct TrialEndUpsell;
3442
3443impl Dismissable for TrialEndUpsell {
3444    const KEY: &'static str = "dismissed-trial-end-upsell";
3445}
3446
3447/// Test-only helper methods
3448#[cfg(any(test, feature = "test-support"))]
3449impl AgentPanel {
3450    /// Opens an external thread using an arbitrary AgentServer.
3451    ///
3452    /// This is a test-only helper that allows visual tests and integration tests
3453    /// to inject a stub server without modifying production code paths.
3454    /// Not compiled into production builds.
3455    pub fn open_external_thread_with_server(
3456        &mut self,
3457        server: Rc<dyn AgentServer>,
3458        window: &mut Window,
3459        cx: &mut Context<Self>,
3460    ) {
3461        let workspace = self.workspace.clone();
3462        let project = self.project.clone();
3463
3464        let ext_agent = ExternalAgent::Custom {
3465            name: server.name(),
3466        };
3467
3468        self._external_thread(
3469            server, None, None, workspace, project, ext_agent, window, cx,
3470        );
3471    }
3472
3473    /// Returns the currently active thread view, if any.
3474    ///
3475    /// This is a test-only accessor that exposes the private `active_thread_view()`
3476    /// method for test assertions. Not compiled into production builds.
3477    pub fn active_thread_view_for_tests(&self) -> Option<&Entity<AcpServerView>> {
3478        self.active_thread_view()
3479    }
3480}
3481
3482#[cfg(test)]
3483mod tests {
3484    use super::*;
3485    use crate::acp::thread_view::tests::{StubAgentServer, init_test};
3486    use assistant_text_thread::TextThreadStore;
3487    use feature_flags::FeatureFlagAppExt;
3488    use fs::FakeFs;
3489    use gpui::{TestAppContext, VisualTestContext};
3490    use project::Project;
3491    use workspace::MultiWorkspace;
3492
3493    #[gpui::test]
3494    async fn test_active_thread_serialize_and_load_round_trip(cx: &mut TestAppContext) {
3495        init_test(cx);
3496        cx.update(|cx| {
3497            cx.update_flags(true, vec!["agent-v2".to_string()]);
3498            agent::ThreadStore::init_global(cx);
3499            language_model::LanguageModelRegistry::test(cx);
3500        });
3501
3502        // --- Create a MultiWorkspace window with two workspaces ---
3503        let fs = FakeFs::new(cx.executor());
3504        let project_a = Project::test(fs.clone(), [], cx).await;
3505        let project_b = Project::test(fs, [], cx).await;
3506
3507        let multi_workspace =
3508            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
3509
3510        let workspace_a = multi_workspace
3511            .read_with(cx, |multi_workspace, _cx| {
3512                multi_workspace.workspace().clone()
3513            })
3514            .unwrap();
3515
3516        let workspace_b = multi_workspace
3517            .update(cx, |multi_workspace, window, cx| {
3518                multi_workspace.test_add_workspace(project_b.clone(), window, cx)
3519            })
3520            .unwrap();
3521
3522        workspace_a.update(cx, |workspace, _cx| {
3523            workspace.set_random_database_id();
3524        });
3525        workspace_b.update(cx, |workspace, _cx| {
3526            workspace.set_random_database_id();
3527        });
3528
3529        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
3530
3531        // --- Set up workspace A: width=300, with an active thread ---
3532        let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
3533            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_a.clone(), cx));
3534            cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
3535        });
3536
3537        panel_a.update(cx, |panel, _cx| {
3538            panel.width = Some(px(300.0));
3539        });
3540
3541        panel_a.update_in(cx, |panel, window, cx| {
3542            panel.open_external_thread_with_server(
3543                Rc::new(StubAgentServer::default_response()),
3544                window,
3545                cx,
3546            );
3547        });
3548
3549        cx.run_until_parked();
3550
3551        panel_a.read_with(cx, |panel, cx| {
3552            assert!(
3553                panel.active_agent_thread(cx).is_some(),
3554                "workspace A should have an active thread after connection"
3555            );
3556        });
3557
3558        let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent.clone());
3559
3560        // --- Set up workspace B: ClaudeCode, width=400, no active thread ---
3561        let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
3562            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_b.clone(), cx));
3563            cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
3564        });
3565
3566        panel_b.update(cx, |panel, _cx| {
3567            panel.width = Some(px(400.0));
3568            panel.selected_agent = AgentType::ClaudeAgent;
3569        });
3570
3571        // --- Serialize both panels ---
3572        panel_a.update(cx, |panel, cx| panel.serialize(cx));
3573        panel_b.update(cx, |panel, cx| panel.serialize(cx));
3574        cx.run_until_parked();
3575
3576        // --- Load fresh panels for each workspace and verify independent state ---
3577        let prompt_builder = Arc::new(prompt_store::PromptBuilder::new(None).unwrap());
3578
3579        let async_cx = cx.update(|window, cx| window.to_async(cx));
3580        let loaded_a = AgentPanel::load(workspace_a.downgrade(), prompt_builder.clone(), async_cx)
3581            .await
3582            .expect("panel A load should succeed");
3583        cx.run_until_parked();
3584
3585        let async_cx = cx.update(|window, cx| window.to_async(cx));
3586        let loaded_b = AgentPanel::load(workspace_b.downgrade(), prompt_builder.clone(), async_cx)
3587            .await
3588            .expect("panel B load should succeed");
3589        cx.run_until_parked();
3590
3591        // Workspace A should restore width and agent type, but the thread
3592        // should NOT be restored because the stub agent never persisted it
3593        // to the database (the load-side validation skips missing threads).
3594        loaded_a.read_with(cx, |panel, _cx| {
3595            assert_eq!(
3596                panel.width,
3597                Some(px(300.0)),
3598                "workspace A width should be restored"
3599            );
3600            assert_eq!(
3601                panel.selected_agent, agent_type_a,
3602                "workspace A agent type should be restored"
3603            );
3604        });
3605
3606        // Workspace B should restore its own width and agent type, with no thread
3607        loaded_b.read_with(cx, |panel, _cx| {
3608            assert_eq!(
3609                panel.width,
3610                Some(px(400.0)),
3611                "workspace B width should be restored"
3612            );
3613            assert_eq!(
3614                panel.selected_agent,
3615                AgentType::ClaudeAgent,
3616                "workspace B agent type should be restored"
3617            );
3618            assert!(
3619                panel.active_thread_view().is_none(),
3620                "workspace B should have no active thread"
3621            );
3622        });
3623    }
3624
3625    // Simple regression test
3626    #[gpui::test]
3627    async fn test_new_text_thread_action_handler(cx: &mut TestAppContext) {
3628        init_test(cx);
3629
3630        let fs = FakeFs::new(cx.executor());
3631
3632        cx.update(|cx| {
3633            cx.update_flags(true, vec!["agent-v2".to_string()]);
3634            agent::ThreadStore::init_global(cx);
3635            language_model::LanguageModelRegistry::test(cx);
3636            let slash_command_registry =
3637                assistant_slash_command::SlashCommandRegistry::default_global(cx);
3638            slash_command_registry
3639                .register_command(assistant_slash_commands::DefaultSlashCommand, false);
3640            <dyn fs::Fs>::set_global(fs.clone(), cx);
3641        });
3642
3643        let project = Project::test(fs.clone(), [], cx).await;
3644
3645        let multi_workspace =
3646            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
3647
3648        let workspace_a = multi_workspace
3649            .read_with(cx, |multi_workspace, _cx| {
3650                multi_workspace.workspace().clone()
3651            })
3652            .unwrap();
3653
3654        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
3655
3656        workspace_a.update_in(cx, |workspace, window, cx| {
3657            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
3658            let panel =
3659                cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
3660            workspace.add_panel(panel, window, cx);
3661        });
3662
3663        cx.run_until_parked();
3664
3665        workspace_a.update_in(cx, |_, window, cx| {
3666            window.dispatch_action(NewTextThread.boxed_clone(), cx);
3667        });
3668
3669        cx.run_until_parked();
3670    }
3671}