agent_panel.rs

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