agent_panel.rs

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