agent_panel.rs

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