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