agent_panel.rs

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