agent_panel.rs

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