agent_panel.rs

   1use std::{
   2    ops::Range,
   3    path::{Path, PathBuf},
   4    rc::Rc,
   5    sync::{
   6        Arc,
   7        atomic::{AtomicBool, Ordering},
   8    },
   9    time::Duration,
  10};
  11
  12use acp_thread::{AcpThread, AgentSessionInfo, MentionUri};
  13use agent::{ContextServerRegistry, SharedThread, ThreadStore};
  14use agent_client_protocol as acp;
  15use agent_servers::AgentServer;
  16use db::kvp::{Dismissable, KEY_VALUE_STORE};
  17use itertools::Itertools;
  18use project::{
  19    ExternalAgentServerName,
  20    agent_server_store::{CLAUDE_AGENT_NAME, CODEX_NAME, GEMINI_NAME},
  21};
  22use serde::{Deserialize, Serialize};
  23use settings::{LanguageModelProviderSetting, LanguageModelSelection};
  24
  25use feature_flags::{AgentGitWorktreesFeatureFlag, AgentV2FeatureFlag, FeatureFlagAppExt as _};
  26use zed_actions::agent::{OpenClaudeAgentOnboardingModal, ReauthenticateAgent, ReviewBranchDiff};
  27
  28use crate::ManageProfiles;
  29use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal};
  30use crate::{
  31    AddContextServer, AgentDiffPane, ConnectionView, CopyThreadToClipboard, Follow,
  32    InlineAssistant, LoadThreadFromClipboard, NewTextThread, NewThread, OpenActiveThreadAsMarkdown,
  33    OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, StartThreadIn,
  34    ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu,
  35    agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
  36    connection_view::{AcpThreadViewEvent, ThreadView},
  37    slash_command::SlashCommandCompletionProvider,
  38    text_thread_editor::{AgentPanelDelegate, TextThreadEditor, make_lsp_adapter_delegate},
  39    ui::EndTrialUpsell,
  40};
  41use crate::{
  42    AgentInitialContent, ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary,
  43};
  44use crate::{
  45    ExpandMessageEditor, ThreadHistory, ThreadHistoryEvent,
  46    text_thread_history::{TextThreadHistory, TextThreadHistoryEvent},
  47};
  48use agent_settings::AgentSettings;
  49use ai_onboarding::AgentPanelOnboarding;
  50use anyhow::{Result, anyhow};
  51use assistant_slash_command::SlashCommandWorkingSet;
  52use assistant_text_thread::{TextThread, TextThreadEvent, TextThreadSummary};
  53use client::UserStore;
  54use cloud_api_types::Plan;
  55use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
  56use extension::ExtensionEvents;
  57use extension_host::ExtensionStore;
  58use fs::Fs;
  59use git::repository::validate_worktree_directory;
  60use gpui::{
  61    Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner,
  62    DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels,
  63    Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
  64};
  65use language::LanguageRegistry;
  66use language_model::{ConfigurationError, LanguageModelRegistry};
  67use project::project_settings::ProjectSettings;
  68use project::{Project, ProjectPath, Worktree};
  69use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
  70use rand::Rng as _;
  71use rules_library::{RulesLibrary, open_rules_library};
  72use search::{BufferSearchBar, buffer_search};
  73use settings::{Settings, update_settings_file};
  74use theme::ThemeSettings;
  75use ui::{
  76    Button, Callout, ContextMenu, ContextMenuEntry, DocumentationSide, KeyBinding, PopoverMenu,
  77    PopoverMenuHandle, SpinnerLabel, Tab, Tooltip, prelude::*, utils::WithRemSize,
  78};
  79use util::ResultExt as _;
  80use workspace::{
  81    CollaboratorId, DraggedSelection, DraggedTab, ToggleZoom, ToolbarItemView, Workspace,
  82    WorkspaceId,
  83    dock::{DockPosition, Panel, PanelEvent},
  84};
  85use zed_actions::{
  86    DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
  87    agent::{OpenAcpOnboardingModal, OpenSettings, ResetAgentZoom, ResetOnboarding},
  88    assistant::{OpenRulesLibrary, Toggle, ToggleFocus},
  89};
  90
  91const AGENT_PANEL_KEY: &str = "agent_panel";
  92const RECENTLY_UPDATED_MENU_LIMIT: usize = 6;
  93const DEFAULT_THREAD_TITLE: &str = "New Thread";
  94
  95fn read_serialized_panel(workspace_id: workspace::WorkspaceId) -> Option<SerializedAgentPanel> {
  96    let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY);
  97    let key = i64::from(workspace_id).to_string();
  98    scope
  99        .read(&key)
 100        .log_err()
 101        .flatten()
 102        .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())
 103}
 104
 105async fn save_serialized_panel(
 106    workspace_id: workspace::WorkspaceId,
 107    panel: SerializedAgentPanel,
 108) -> Result<()> {
 109    let scope = KEY_VALUE_STORE.scoped(AGENT_PANEL_KEY);
 110    let key = i64::from(workspace_id).to_string();
 111    scope.write(key, serde_json::to_string(&panel)?).await?;
 112    Ok(())
 113}
 114
 115/// Migration: reads the original single-panel format stored under the
 116/// `"agent_panel"` KVP key before per-workspace keying was introduced.
 117fn read_legacy_serialized_panel() -> Option<SerializedAgentPanel> {
 118    KEY_VALUE_STORE
 119        .read_kvp(AGENT_PANEL_KEY)
 120        .log_err()
 121        .flatten()
 122        .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())
 123}
 124
 125#[derive(Serialize, Deserialize, Debug, Clone)]
 126struct SerializedAgentPanel {
 127    width: Option<Pixels>,
 128    selected_agent: Option<AgentType>,
 129    #[serde(default)]
 130    last_active_thread: Option<SerializedActiveThread>,
 131    #[serde(default)]
 132    start_thread_in: Option<StartThreadIn>,
 133}
 134
 135#[derive(Serialize, Deserialize, Debug, Clone)]
 136struct SerializedActiveThread {
 137    session_id: String,
 138    agent_type: AgentType,
 139    title: Option<String>,
 140    cwd: Option<std::path::PathBuf>,
 141}
 142
 143pub fn init(cx: &mut App) {
 144    cx.observe_new(
 145        |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
 146            workspace
 147                .register_action(|workspace, action: &NewThread, window, cx| {
 148                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 149                        panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
 150                        workspace.focus_panel::<AgentPanel>(window, cx);
 151                    }
 152                })
 153                .register_action(
 154                    |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| {
 155                        if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 156                            panel.update(cx, |panel, cx| {
 157                                panel.new_native_agent_thread_from_summary(action, window, cx)
 158                            });
 159                            workspace.focus_panel::<AgentPanel>(window, cx);
 160                        }
 161                    },
 162                )
 163                .register_action(|workspace, _: &ExpandMessageEditor, 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.expand_message_editor(window, cx));
 167                    }
 168                })
 169                .register_action(|workspace, _: &OpenHistory, window, cx| {
 170                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 171                        workspace.focus_panel::<AgentPanel>(window, cx);
 172                        panel.update(cx, |panel, cx| panel.open_history(window, cx));
 173                    }
 174                })
 175                .register_action(|workspace, _: &OpenSettings, window, cx| {
 176                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 177                        workspace.focus_panel::<AgentPanel>(window, cx);
 178                        panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
 179                    }
 180                })
 181                .register_action(|workspace, _: &NewTextThread, window, cx| {
 182                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 183                        workspace.focus_panel::<AgentPanel>(window, cx);
 184                        panel.update(cx, |panel, cx| {
 185                            panel.new_text_thread(window, cx);
 186                        });
 187                    }
 188                })
 189                .register_action(|workspace, action: &NewExternalAgentThread, window, cx| {
 190                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 191                        workspace.focus_panel::<AgentPanel>(window, cx);
 192                        panel.update(cx, |panel, cx| {
 193                            panel.external_thread(action.agent.clone(), None, None, window, cx)
 194                        });
 195                    }
 196                })
 197                .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
 198                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 199                        workspace.focus_panel::<AgentPanel>(window, cx);
 200                        panel.update(cx, |panel, cx| {
 201                            panel.deploy_rules_library(action, window, cx)
 202                        });
 203                    }
 204                })
 205                .register_action(|workspace, _: &Follow, window, cx| {
 206                    workspace.follow(CollaboratorId::Agent, window, cx);
 207                })
 208                .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
 209                    let thread = workspace
 210                        .panel::<AgentPanel>(cx)
 211                        .and_then(|panel| panel.read(cx).active_thread_view().cloned())
 212                        .and_then(|thread_view| {
 213                            thread_view
 214                                .read(cx)
 215                                .active_thread()
 216                                .map(|r| r.read(cx).thread.clone())
 217                        });
 218
 219                    if let Some(thread) = thread {
 220                        AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
 221                    }
 222                })
 223                .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
 224                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 225                        workspace.focus_panel::<AgentPanel>(window, cx);
 226                        panel.update(cx, |panel, cx| {
 227                            panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
 228                        });
 229                    }
 230                })
 231                .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
 232                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 233                        workspace.focus_panel::<AgentPanel>(window, cx);
 234                        panel.update(cx, |panel, cx| {
 235                            panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
 236                        });
 237                    }
 238                })
 239                .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| {
 240                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 241                        workspace.focus_panel::<AgentPanel>(window, cx);
 242                        panel.update(cx, |panel, cx| {
 243                            panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx);
 244                        });
 245                    }
 246                })
 247                .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
 248                    AcpOnboardingModal::toggle(workspace, window, cx)
 249                })
 250                .register_action(
 251                    |workspace, _: &OpenClaudeAgentOnboardingModal, window, cx| {
 252                        ClaudeCodeOnboardingModal::toggle(workspace, window, cx)
 253                    },
 254                )
 255                .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
 256                    window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
 257                    window.refresh();
 258                })
 259                .register_action(|workspace, _: &ResetTrialUpsell, _window, cx| {
 260                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 261                        panel.update(cx, |panel, _| {
 262                            panel
 263                                .on_boarding_upsell_dismissed
 264                                .store(false, Ordering::Release);
 265                        });
 266                    }
 267                    OnboardingUpsell::set_dismissed(false, cx);
 268                })
 269                .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
 270                    TrialEndUpsell::set_dismissed(false, cx);
 271                })
 272                .register_action(|workspace, _: &ResetAgentZoom, window, cx| {
 273                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 274                        panel.update(cx, |panel, cx| {
 275                            panel.reset_agent_zoom(window, cx);
 276                        });
 277                    }
 278                })
 279                .register_action(|workspace, _: &CopyThreadToClipboard, window, cx| {
 280                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 281                        panel.update(cx, |panel, cx| {
 282                            panel.copy_thread_to_clipboard(window, cx);
 283                        });
 284                    }
 285                })
 286                .register_action(|workspace, _: &LoadThreadFromClipboard, window, cx| {
 287                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 288                        workspace.focus_panel::<AgentPanel>(window, cx);
 289                        panel.update(cx, |panel, cx| {
 290                            panel.load_thread_from_clipboard(window, cx);
 291                        });
 292                    }
 293                })
 294                .register_action(|workspace, action: &ReviewBranchDiff, window, cx| {
 295                    let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
 296                        return;
 297                    };
 298
 299                    let mention_uri = MentionUri::GitDiff {
 300                        base_ref: action.base_ref.to_string(),
 301                    };
 302                    let diff_uri = mention_uri.to_uri().to_string();
 303
 304                    let content_blocks = vec![
 305                        acp::ContentBlock::Text(acp::TextContent::new(
 306                            "Please review this branch diff carefully. Point out any issues, \
 307                             potential bugs, or improvement opportunities you find.\n\n"
 308                                .to_string(),
 309                        )),
 310                        acp::ContentBlock::Resource(acp::EmbeddedResource::new(
 311                            acp::EmbeddedResourceResource::TextResourceContents(
 312                                acp::TextResourceContents::new(
 313                                    action.diff_text.to_string(),
 314                                    diff_uri,
 315                                ),
 316                            ),
 317                        )),
 318                    ];
 319
 320                    workspace.focus_panel::<AgentPanel>(window, cx);
 321
 322                    panel.update(cx, |panel, cx| {
 323                        panel.external_thread(
 324                            None,
 325                            None,
 326                            Some(AgentInitialContent::ContentBlock {
 327                                blocks: content_blocks,
 328                                auto_submit: true,
 329                            }),
 330                            window,
 331                            cx,
 332                        );
 333                    });
 334                })
 335                .register_action(|workspace, action: &StartThreadIn, _window, cx| {
 336                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 337                        panel.update(cx, |panel, cx| {
 338                            panel.set_start_thread_in(action, cx);
 339                        });
 340                    }
 341                });
 342        },
 343    )
 344    .detach();
 345}
 346
 347#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 348enum HistoryKind {
 349    AgentThreads,
 350    TextThreads,
 351}
 352
 353enum ActiveView {
 354    Uninitialized,
 355    AgentThread {
 356        server_view: Entity<ConnectionView>,
 357    },
 358    TextThread {
 359        text_thread_editor: Entity<TextThreadEditor>,
 360        title_editor: Entity<Editor>,
 361        buffer_search_bar: Entity<BufferSearchBar>,
 362        _subscriptions: Vec<gpui::Subscription>,
 363    },
 364    History {
 365        kind: HistoryKind,
 366    },
 367    Configuration,
 368}
 369
 370enum WhichFontSize {
 371    AgentFont,
 372    BufferFont,
 373    None,
 374}
 375
 376// TODO unify this with ExternalAgent
 377#[derive(Debug, Default, Clone, PartialEq, Serialize)]
 378pub enum AgentType {
 379    #[default]
 380    NativeAgent,
 381    TextThread,
 382    Custom {
 383        name: SharedString,
 384    },
 385}
 386
 387// Custom impl handles legacy variant names from before the built-in agents were moved to
 388// the registry: "ClaudeAgent" -> Custom { name: "claude-acp" }, "Codex" -> Custom { name:
 389// "codex-acp" }, "Gemini" -> Custom { name: "gemini" }.
 390// Can be removed at some point in the future and go back to #[derive(Deserialize)].
 391impl<'de> Deserialize<'de> for AgentType {
 392    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
 393    where
 394        D: serde::Deserializer<'de>,
 395    {
 396        let value = serde_json::Value::deserialize(deserializer)?;
 397
 398        if let Some(s) = value.as_str() {
 399            return match s {
 400                "NativeAgent" => Ok(Self::NativeAgent),
 401                "TextThread" => Ok(Self::TextThread),
 402                "ClaudeAgent" | "ClaudeCode" => Ok(Self::Custom {
 403                    name: CLAUDE_AGENT_NAME.into(),
 404                }),
 405                "Codex" => Ok(Self::Custom {
 406                    name: CODEX_NAME.into(),
 407                }),
 408                "Gemini" => Ok(Self::Custom {
 409                    name: GEMINI_NAME.into(),
 410                }),
 411                other => Err(serde::de::Error::unknown_variant(
 412                    other,
 413                    &[
 414                        "NativeAgent",
 415                        "TextThread",
 416                        "Custom",
 417                        "ClaudeAgent",
 418                        "ClaudeCode",
 419                        "Codex",
 420                        "Gemini",
 421                    ],
 422                )),
 423            };
 424        }
 425
 426        if let Some(obj) = value.as_object() {
 427            if let Some(inner) = obj.get("Custom") {
 428                #[derive(Deserialize)]
 429                struct CustomFields {
 430                    name: SharedString,
 431                }
 432                let fields: CustomFields =
 433                    serde_json::from_value(inner.clone()).map_err(serde::de::Error::custom)?;
 434                return Ok(Self::Custom { name: fields.name });
 435            }
 436        }
 437
 438        Err(serde::de::Error::custom(
 439            "expected a string variant or {\"Custom\": {\"name\": ...}}",
 440        ))
 441    }
 442}
 443
 444impl AgentType {
 445    pub fn is_native(&self) -> bool {
 446        matches!(self, Self::NativeAgent)
 447    }
 448
 449    fn label(&self) -> SharedString {
 450        match self {
 451            Self::NativeAgent | Self::TextThread => "Zed Agent".into(),
 452            Self::Custom { name, .. } => name.into(),
 453        }
 454    }
 455
 456    fn icon(&self) -> Option<IconName> {
 457        match self {
 458            Self::NativeAgent | Self::TextThread => None,
 459            Self::Custom { .. } => Some(IconName::Sparkle),
 460        }
 461    }
 462}
 463
 464impl From<ExternalAgent> for AgentType {
 465    fn from(value: ExternalAgent) -> Self {
 466        match value {
 467            ExternalAgent::Custom { name } => Self::Custom { name },
 468            ExternalAgent::NativeAgent => Self::NativeAgent,
 469        }
 470    }
 471}
 472
 473impl StartThreadIn {
 474    fn label(&self) -> SharedString {
 475        match self {
 476            Self::LocalProject => "Local Project".into(),
 477            Self::NewWorktree => "New Worktree".into(),
 478        }
 479    }
 480
 481    fn icon(&self) -> IconName {
 482        match self {
 483            Self::LocalProject => IconName::Screen,
 484            Self::NewWorktree => IconName::GitBranchPlus,
 485        }
 486    }
 487}
 488
 489#[derive(Clone, Debug)]
 490#[allow(dead_code)]
 491pub enum WorktreeCreationStatus {
 492    Creating,
 493    Error(SharedString),
 494}
 495
 496impl ActiveView {
 497    pub fn which_font_size_used(&self) -> WhichFontSize {
 498        match self {
 499            ActiveView::Uninitialized
 500            | ActiveView::AgentThread { .. }
 501            | ActiveView::History { .. } => WhichFontSize::AgentFont,
 502            ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
 503            ActiveView::Configuration => WhichFontSize::None,
 504        }
 505    }
 506
 507    pub fn text_thread(
 508        text_thread_editor: Entity<TextThreadEditor>,
 509        language_registry: Arc<LanguageRegistry>,
 510        window: &mut Window,
 511        cx: &mut App,
 512    ) -> Self {
 513        let title = text_thread_editor.read(cx).title(cx).to_string();
 514
 515        let editor = cx.new(|cx| {
 516            let mut editor = Editor::single_line(window, cx);
 517            editor.set_text(title, window, cx);
 518            editor
 519        });
 520
 521        // This is a workaround for `editor.set_text` emitting a `BufferEdited` event, which would
 522        // cause a custom summary to be set. The presence of this custom summary would cause
 523        // summarization to not happen.
 524        let mut suppress_first_edit = true;
 525
 526        let subscriptions = vec![
 527            window.subscribe(&editor, cx, {
 528                {
 529                    let text_thread_editor = text_thread_editor.clone();
 530                    move |editor, event, window, cx| match event {
 531                        EditorEvent::BufferEdited => {
 532                            if suppress_first_edit {
 533                                suppress_first_edit = false;
 534                                return;
 535                            }
 536                            let new_summary = editor.read(cx).text(cx);
 537
 538                            text_thread_editor.update(cx, |text_thread_editor, cx| {
 539                                text_thread_editor
 540                                    .text_thread()
 541                                    .update(cx, |text_thread, cx| {
 542                                        text_thread.set_custom_summary(new_summary, cx);
 543                                    })
 544                            })
 545                        }
 546                        EditorEvent::Blurred => {
 547                            if editor.read(cx).text(cx).is_empty() {
 548                                let summary = text_thread_editor
 549                                    .read(cx)
 550                                    .text_thread()
 551                                    .read(cx)
 552                                    .summary()
 553                                    .or_default();
 554
 555                                editor.update(cx, |editor, cx| {
 556                                    editor.set_text(summary, window, cx);
 557                                });
 558                            }
 559                        }
 560                        _ => {}
 561                    }
 562                }
 563            }),
 564            window.subscribe(&text_thread_editor.read(cx).text_thread().clone(), cx, {
 565                let editor = editor.clone();
 566                move |text_thread, event, window, cx| match event {
 567                    TextThreadEvent::SummaryGenerated => {
 568                        let summary = text_thread.read(cx).summary().or_default();
 569
 570                        editor.update(cx, |editor, cx| {
 571                            editor.set_text(summary, window, cx);
 572                        })
 573                    }
 574                    TextThreadEvent::PathChanged { .. } => {}
 575                    _ => {}
 576                }
 577            }),
 578        ];
 579
 580        let buffer_search_bar =
 581            cx.new(|cx| BufferSearchBar::new(Some(language_registry), window, cx));
 582        buffer_search_bar.update(cx, |buffer_search_bar, cx| {
 583            buffer_search_bar.set_active_pane_item(Some(&text_thread_editor), window, cx)
 584        });
 585
 586        Self::TextThread {
 587            text_thread_editor,
 588            title_editor: editor,
 589            buffer_search_bar,
 590            _subscriptions: subscriptions,
 591        }
 592    }
 593}
 594
 595pub struct AgentPanel {
 596    workspace: WeakEntity<Workspace>,
 597    /// Workspace id is used as a database key
 598    workspace_id: Option<WorkspaceId>,
 599    user_store: Entity<UserStore>,
 600    project: Entity<Project>,
 601    fs: Arc<dyn Fs>,
 602    language_registry: Arc<LanguageRegistry>,
 603    acp_history: Entity<ThreadHistory>,
 604    text_thread_history: Entity<TextThreadHistory>,
 605    thread_store: Entity<ThreadStore>,
 606    text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
 607    prompt_store: Option<Entity<PromptStore>>,
 608    context_server_registry: Entity<ContextServerRegistry>,
 609    configuration: Option<Entity<AgentConfiguration>>,
 610    configuration_subscription: Option<Subscription>,
 611    focus_handle: FocusHandle,
 612    active_view: ActiveView,
 613    previous_view: Option<ActiveView>,
 614    _active_view_observation: Option<Subscription>,
 615    new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
 616    start_thread_in_menu_handle: PopoverMenuHandle<ContextMenu>,
 617    agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
 618    agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
 619    agent_navigation_menu: Option<Entity<ContextMenu>>,
 620    _extension_subscription: Option<Subscription>,
 621    width: Option<Pixels>,
 622    height: Option<Pixels>,
 623    zoomed: bool,
 624    pending_serialization: Option<Task<Result<()>>>,
 625    onboarding: Entity<AgentPanelOnboarding>,
 626    selected_agent: AgentType,
 627    start_thread_in: StartThreadIn,
 628    worktree_creation_status: Option<WorktreeCreationStatus>,
 629    _thread_view_subscription: Option<Subscription>,
 630    _worktree_creation_task: Option<Task<()>>,
 631    show_trust_workspace_message: bool,
 632    last_configuration_error_telemetry: Option<String>,
 633    on_boarding_upsell_dismissed: AtomicBool,
 634}
 635
 636impl AgentPanel {
 637    fn serialize(&mut self, cx: &mut App) {
 638        let Some(workspace_id) = self.workspace_id else {
 639            return;
 640        };
 641
 642        let width = self.width;
 643        let selected_agent = self.selected_agent.clone();
 644        let start_thread_in = Some(self.start_thread_in);
 645
 646        let last_active_thread = self.active_agent_thread(cx).map(|thread| {
 647            let thread = thread.read(cx);
 648            let title = thread.title();
 649            SerializedActiveThread {
 650                session_id: thread.session_id().0.to_string(),
 651                agent_type: self.selected_agent.clone(),
 652                title: if title.as_ref() != DEFAULT_THREAD_TITLE {
 653                    Some(title.to_string())
 654                } else {
 655                    None
 656                },
 657                cwd: None,
 658            }
 659        });
 660
 661        self.pending_serialization = Some(cx.background_spawn(async move {
 662            save_serialized_panel(
 663                workspace_id,
 664                SerializedAgentPanel {
 665                    width,
 666                    selected_agent: Some(selected_agent),
 667                    last_active_thread,
 668                    start_thread_in,
 669                },
 670            )
 671            .await?;
 672            anyhow::Ok(())
 673        }));
 674    }
 675
 676    pub fn load(
 677        workspace: WeakEntity<Workspace>,
 678        prompt_builder: Arc<PromptBuilder>,
 679        mut cx: AsyncWindowContext,
 680    ) -> Task<Result<Entity<Self>>> {
 681        let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
 682        cx.spawn(async move |cx| {
 683            let prompt_store = match prompt_store {
 684                Ok(prompt_store) => prompt_store.await.ok(),
 685                Err(_) => None,
 686            };
 687            let workspace_id = workspace
 688                .read_with(cx, |workspace, _| workspace.database_id())
 689                .ok()
 690                .flatten();
 691
 692            let serialized_panel = cx
 693                .background_spawn(async move {
 694                    workspace_id
 695                        .and_then(read_serialized_panel)
 696                        .or_else(read_legacy_serialized_panel)
 697                })
 698                .await;
 699
 700            let slash_commands = Arc::new(SlashCommandWorkingSet::default());
 701            let text_thread_store = workspace
 702                .update(cx, |workspace, cx| {
 703                    let project = workspace.project().clone();
 704                    assistant_text_thread::TextThreadStore::new(
 705                        project,
 706                        prompt_builder,
 707                        slash_commands,
 708                        cx,
 709                    )
 710                })?
 711                .await?;
 712
 713            let last_active_thread = if let Some(thread_info) = serialized_panel
 714                .as_ref()
 715                .and_then(|p| p.last_active_thread.clone())
 716            {
 717                if thread_info.agent_type.is_native() {
 718                    let session_id = acp::SessionId::new(thread_info.session_id.clone());
 719                    let load_result = cx.update(|_window, cx| {
 720                        let thread_store = ThreadStore::global(cx);
 721                        thread_store.update(cx, |store, cx| store.load_thread(session_id, cx))
 722                    });
 723                    let thread_exists = if let Ok(task) = load_result {
 724                        task.await.ok().flatten().is_some()
 725                    } else {
 726                        false
 727                    };
 728                    if thread_exists {
 729                        Some(thread_info)
 730                    } else {
 731                        log::warn!(
 732                            "last active thread {} not found in database, skipping restoration",
 733                            thread_info.session_id
 734                        );
 735                        None
 736                    }
 737                } else {
 738                    Some(thread_info)
 739                }
 740            } else {
 741                None
 742            };
 743
 744            let panel = workspace.update_in(cx, |workspace, window, cx| {
 745                let panel =
 746                    cx.new(|cx| Self::new(workspace, text_thread_store, prompt_store, window, cx));
 747
 748                if let Some(serialized_panel) = &serialized_panel {
 749                    panel.update(cx, |panel, cx| {
 750                        panel.width = serialized_panel.width.map(|w| w.round());
 751                        if let Some(selected_agent) = serialized_panel.selected_agent.clone() {
 752                            panel.selected_agent = selected_agent;
 753                        }
 754                        if let Some(start_thread_in) = serialized_panel.start_thread_in {
 755                            let is_worktree_flag_enabled =
 756                                cx.has_flag::<AgentGitWorktreesFeatureFlag>();
 757                            let is_valid = match &start_thread_in {
 758                                StartThreadIn::LocalProject => true,
 759                                StartThreadIn::NewWorktree => {
 760                                    let project = panel.project.read(cx);
 761                                    is_worktree_flag_enabled && !project.is_via_collab()
 762                                }
 763                            };
 764                            if is_valid {
 765                                panel.start_thread_in = start_thread_in;
 766                            } else {
 767                                log::info!(
 768                                    "deserialized start_thread_in {:?} is no longer valid, falling back to LocalProject",
 769                                    start_thread_in,
 770                                );
 771                            }
 772                        }
 773                        cx.notify();
 774                    });
 775                }
 776
 777                if let Some(thread_info) = last_active_thread {
 778                    let agent_type = thread_info.agent_type.clone();
 779                    let session_info = AgentSessionInfo {
 780                        session_id: acp::SessionId::new(thread_info.session_id),
 781                        cwd: thread_info.cwd,
 782                        title: thread_info.title.map(SharedString::from),
 783                        updated_at: None,
 784                        meta: None,
 785                    };
 786                    panel.update(cx, |panel, cx| {
 787                        panel.selected_agent = agent_type;
 788                        panel.load_agent_thread(session_info, window, cx);
 789                    });
 790                }
 791                panel
 792            })?;
 793
 794            Ok(panel)
 795        })
 796    }
 797
 798    pub(crate) fn new(
 799        workspace: &Workspace,
 800        text_thread_store: Entity<assistant_text_thread::TextThreadStore>,
 801        prompt_store: Option<Entity<PromptStore>>,
 802        window: &mut Window,
 803        cx: &mut Context<Self>,
 804    ) -> Self {
 805        let fs = workspace.app_state().fs.clone();
 806        let user_store = workspace.app_state().user_store.clone();
 807        let project = workspace.project();
 808        let language_registry = project.read(cx).languages().clone();
 809        let client = workspace.client().clone();
 810        let workspace_id = workspace.database_id();
 811        let workspace = workspace.weak_handle();
 812
 813        let context_server_registry =
 814            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
 815
 816        let thread_store = ThreadStore::global(cx);
 817        let acp_history = cx.new(|cx| ThreadHistory::new(None, window, cx));
 818        let text_thread_history =
 819            cx.new(|cx| TextThreadHistory::new(text_thread_store.clone(), window, cx));
 820        cx.subscribe_in(
 821            &acp_history,
 822            window,
 823            |this, _, event, window, cx| match event {
 824                ThreadHistoryEvent::Open(thread) => {
 825                    this.load_agent_thread(thread.clone(), window, cx);
 826                }
 827            },
 828        )
 829        .detach();
 830        cx.subscribe_in(
 831            &text_thread_history,
 832            window,
 833            |this, _, event, window, cx| match event {
 834                TextThreadHistoryEvent::Open(thread) => {
 835                    this.open_saved_text_thread(thread.path.clone(), window, cx)
 836                        .detach_and_log_err(cx);
 837                }
 838            },
 839        )
 840        .detach();
 841
 842        let active_view = ActiveView::Uninitialized;
 843
 844        let weak_panel = cx.entity().downgrade();
 845
 846        window.defer(cx, move |window, cx| {
 847            let panel = weak_panel.clone();
 848            let agent_navigation_menu =
 849                ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
 850                    if let Some(panel) = panel.upgrade() {
 851                        if let Some(kind) = panel.read(cx).history_kind_for_selected_agent(cx) {
 852                            menu =
 853                                Self::populate_recently_updated_menu_section(menu, panel, kind, cx);
 854                            let view_all_label = match kind {
 855                                HistoryKind::AgentThreads => "View All",
 856                                HistoryKind::TextThreads => "View All Text Threads",
 857                            };
 858                            menu = menu.action(view_all_label, Box::new(OpenHistory));
 859                        }
 860                    }
 861
 862                    menu = menu
 863                        .fixed_width(px(320.).into())
 864                        .keep_open_on_confirm(false)
 865                        .key_context("NavigationMenu");
 866
 867                    menu
 868                });
 869            weak_panel
 870                .update(cx, |panel, cx| {
 871                    cx.subscribe_in(
 872                        &agent_navigation_menu,
 873                        window,
 874                        |_, menu, _: &DismissEvent, window, cx| {
 875                            menu.update(cx, |menu, _| {
 876                                menu.clear_selected();
 877                            });
 878                            cx.focus_self(window);
 879                        },
 880                    )
 881                    .detach();
 882                    panel.agent_navigation_menu = Some(agent_navigation_menu);
 883                })
 884                .ok();
 885        });
 886
 887        let weak_panel = cx.entity().downgrade();
 888        let onboarding = cx.new(|cx| {
 889            AgentPanelOnboarding::new(
 890                user_store.clone(),
 891                client,
 892                move |_window, cx| {
 893                    weak_panel
 894                        .update(cx, |panel, _| {
 895                            panel
 896                                .on_boarding_upsell_dismissed
 897                                .store(true, Ordering::Release);
 898                        })
 899                        .ok();
 900                    OnboardingUpsell::set_dismissed(true, cx);
 901                },
 902                cx,
 903            )
 904        });
 905
 906        // Subscribe to extension events to sync agent servers when extensions change
 907        let extension_subscription = if let Some(extension_events) = ExtensionEvents::try_global(cx)
 908        {
 909            Some(
 910                cx.subscribe(&extension_events, |this, _source, event, cx| match event {
 911                    extension::Event::ExtensionInstalled(_)
 912                    | extension::Event::ExtensionUninstalled(_)
 913                    | extension::Event::ExtensionsInstalledChanged => {
 914                        this.sync_agent_servers_from_extensions(cx);
 915                    }
 916                    _ => {}
 917                }),
 918            )
 919        } else {
 920            None
 921        };
 922
 923        let mut panel = Self {
 924            workspace_id,
 925            active_view,
 926            workspace,
 927            user_store,
 928            project: project.clone(),
 929            fs: fs.clone(),
 930            language_registry,
 931            text_thread_store,
 932            prompt_store,
 933            configuration: None,
 934            configuration_subscription: None,
 935            focus_handle: cx.focus_handle(),
 936            context_server_registry,
 937            previous_view: None,
 938            _active_view_observation: None,
 939            new_thread_menu_handle: PopoverMenuHandle::default(),
 940            start_thread_in_menu_handle: PopoverMenuHandle::default(),
 941            agent_panel_menu_handle: PopoverMenuHandle::default(),
 942            agent_navigation_menu_handle: PopoverMenuHandle::default(),
 943            agent_navigation_menu: None,
 944            _extension_subscription: extension_subscription,
 945            width: None,
 946            height: None,
 947            zoomed: false,
 948            pending_serialization: None,
 949            onboarding,
 950            acp_history,
 951            text_thread_history,
 952            thread_store,
 953            selected_agent: AgentType::default(),
 954            start_thread_in: StartThreadIn::default(),
 955            worktree_creation_status: None,
 956            _thread_view_subscription: None,
 957            _worktree_creation_task: None,
 958            show_trust_workspace_message: false,
 959            last_configuration_error_telemetry: None,
 960            on_boarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed()),
 961        };
 962
 963        // Initial sync of agent servers from extensions
 964        panel.sync_agent_servers_from_extensions(cx);
 965        panel
 966    }
 967
 968    pub fn toggle_focus(
 969        workspace: &mut Workspace,
 970        _: &ToggleFocus,
 971        window: &mut Window,
 972        cx: &mut Context<Workspace>,
 973    ) {
 974        if workspace
 975            .panel::<Self>(cx)
 976            .is_some_and(|panel| panel.read(cx).enabled(cx))
 977        {
 978            workspace.toggle_panel_focus::<Self>(window, cx);
 979        }
 980    }
 981
 982    pub fn toggle(
 983        workspace: &mut Workspace,
 984        _: &Toggle,
 985        window: &mut Window,
 986        cx: &mut Context<Workspace>,
 987    ) {
 988        if workspace
 989            .panel::<Self>(cx)
 990            .is_some_and(|panel| panel.read(cx).enabled(cx))
 991        {
 992            if !workspace.toggle_panel_focus::<Self>(window, cx) {
 993                workspace.close_panel::<Self>(window, cx);
 994            }
 995        }
 996    }
 997
 998    pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
 999        &self.prompt_store
1000    }
1001
1002    pub fn thread_store(&self) -> &Entity<ThreadStore> {
1003        &self.thread_store
1004    }
1005
1006    pub fn history(&self) -> &Entity<ThreadHistory> {
1007        &self.acp_history
1008    }
1009
1010    pub fn open_thread(
1011        &mut self,
1012        thread: AgentSessionInfo,
1013        window: &mut Window,
1014        cx: &mut Context<Self>,
1015    ) {
1016        self.external_thread(
1017            Some(crate::ExternalAgent::NativeAgent),
1018            Some(thread),
1019            None,
1020            window,
1021            cx,
1022        );
1023    }
1024
1025    pub(crate) fn context_server_registry(&self) -> &Entity<ContextServerRegistry> {
1026        &self.context_server_registry
1027    }
1028
1029    pub fn is_visible(workspace: &Entity<Workspace>, cx: &App) -> bool {
1030        let workspace_read = workspace.read(cx);
1031
1032        workspace_read
1033            .panel::<AgentPanel>(cx)
1034            .map(|panel| {
1035                let panel_id = Entity::entity_id(&panel);
1036
1037                workspace_read.all_docks().iter().any(|dock| {
1038                    dock.read(cx)
1039                        .visible_panel()
1040                        .is_some_and(|visible_panel| visible_panel.panel_id() == panel_id)
1041                })
1042            })
1043            .unwrap_or(false)
1044    }
1045
1046    pub(crate) fn active_thread_view(&self) -> Option<&Entity<ConnectionView>> {
1047        match &self.active_view {
1048            ActiveView::AgentThread { server_view, .. } => Some(server_view),
1049            ActiveView::Uninitialized
1050            | ActiveView::TextThread { .. }
1051            | ActiveView::History { .. }
1052            | ActiveView::Configuration => None,
1053        }
1054    }
1055
1056    fn new_thread(&mut self, _action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
1057        self.new_agent_thread(AgentType::NativeAgent, window, cx);
1058    }
1059
1060    fn new_native_agent_thread_from_summary(
1061        &mut self,
1062        action: &NewNativeAgentThreadFromSummary,
1063        window: &mut Window,
1064        cx: &mut Context<Self>,
1065    ) {
1066        let Some(thread) = self
1067            .acp_history
1068            .read(cx)
1069            .session_for_id(&action.from_session_id)
1070        else {
1071            return;
1072        };
1073
1074        self.external_thread(
1075            Some(ExternalAgent::NativeAgent),
1076            None,
1077            Some(AgentInitialContent::ThreadSummary(thread)),
1078            window,
1079            cx,
1080        );
1081    }
1082
1083    fn new_text_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1084        telemetry::event!("Agent Thread Started", agent = "zed-text");
1085
1086        let context = self
1087            .text_thread_store
1088            .update(cx, |context_store, cx| context_store.create(cx));
1089        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project, cx)
1090            .log_err()
1091            .flatten();
1092
1093        let text_thread_editor = cx.new(|cx| {
1094            let mut editor = TextThreadEditor::for_text_thread(
1095                context,
1096                self.fs.clone(),
1097                self.workspace.clone(),
1098                self.project.clone(),
1099                lsp_adapter_delegate,
1100                window,
1101                cx,
1102            );
1103            editor.insert_default_prompt(window, cx);
1104            editor
1105        });
1106
1107        if self.selected_agent != AgentType::TextThread {
1108            self.selected_agent = AgentType::TextThread;
1109            self.serialize(cx);
1110        }
1111
1112        self.set_active_view(
1113            ActiveView::text_thread(
1114                text_thread_editor.clone(),
1115                self.language_registry.clone(),
1116                window,
1117                cx,
1118            ),
1119            true,
1120            window,
1121            cx,
1122        );
1123        text_thread_editor.focus_handle(cx).focus(window, cx);
1124    }
1125
1126    fn external_thread(
1127        &mut self,
1128        agent_choice: Option<crate::ExternalAgent>,
1129        resume_thread: Option<AgentSessionInfo>,
1130        initial_content: Option<AgentInitialContent>,
1131        window: &mut Window,
1132        cx: &mut Context<Self>,
1133    ) {
1134        let workspace = self.workspace.clone();
1135        let project = self.project.clone();
1136        let fs = self.fs.clone();
1137        let is_via_collab = self.project.read(cx).is_via_collab();
1138
1139        const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
1140
1141        #[derive(Serialize, Deserialize)]
1142        struct LastUsedExternalAgent {
1143            agent: crate::ExternalAgent,
1144        }
1145
1146        let thread_store = self.thread_store.clone();
1147
1148        cx.spawn_in(window, async move |this, cx| {
1149            let ext_agent = match agent_choice {
1150                Some(agent) => {
1151                    cx.background_spawn({
1152                        let agent = agent.clone();
1153                        async move {
1154                            if let Some(serialized) =
1155                                serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
1156                            {
1157                                KEY_VALUE_STORE
1158                                    .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
1159                                    .await
1160                                    .log_err();
1161                            }
1162                        }
1163                    })
1164                    .detach();
1165
1166                    agent
1167                }
1168                None => {
1169                    if is_via_collab {
1170                        ExternalAgent::NativeAgent
1171                    } else {
1172                        cx.background_spawn(async move {
1173                            KEY_VALUE_STORE.read_kvp(LAST_USED_EXTERNAL_AGENT_KEY)
1174                        })
1175                        .await
1176                        .log_err()
1177                        .flatten()
1178                        .and_then(|value| {
1179                            serde_json::from_str::<LastUsedExternalAgent>(&value).log_err()
1180                        })
1181                        .map(|agent| agent.agent)
1182                        .unwrap_or(ExternalAgent::NativeAgent)
1183                    }
1184                }
1185            };
1186
1187            let server = ext_agent.server(fs, thread_store);
1188            this.update_in(cx, |agent_panel, window, cx| {
1189                agent_panel.create_external_thread(
1190                    server,
1191                    resume_thread,
1192                    initial_content,
1193                    workspace,
1194                    project,
1195                    ext_agent,
1196                    window,
1197                    cx,
1198                );
1199            })?;
1200
1201            anyhow::Ok(())
1202        })
1203        .detach_and_log_err(cx);
1204    }
1205
1206    fn deploy_rules_library(
1207        &mut self,
1208        action: &OpenRulesLibrary,
1209        _window: &mut Window,
1210        cx: &mut Context<Self>,
1211    ) {
1212        open_rules_library(
1213            self.language_registry.clone(),
1214            Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
1215            Rc::new(|| {
1216                Rc::new(SlashCommandCompletionProvider::new(
1217                    Arc::new(SlashCommandWorkingSet::default()),
1218                    None,
1219                    None,
1220                ))
1221            }),
1222            action
1223                .prompt_to_select
1224                .map(|uuid| UserPromptId(uuid).into()),
1225            cx,
1226        )
1227        .detach_and_log_err(cx);
1228    }
1229
1230    fn expand_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1231        let Some(thread_view) = self.active_thread_view() else {
1232            return;
1233        };
1234
1235        let Some(active_thread) = thread_view.read(cx).active_thread().cloned() else {
1236            return;
1237        };
1238
1239        active_thread.update(cx, |active_thread, cx| {
1240            active_thread.expand_message_editor(&ExpandMessageEditor, window, cx);
1241            active_thread.focus_handle(cx).focus(window, cx);
1242        })
1243    }
1244
1245    fn history_kind_for_selected_agent(&self, cx: &App) -> Option<HistoryKind> {
1246        match self.selected_agent {
1247            AgentType::NativeAgent => Some(HistoryKind::AgentThreads),
1248            AgentType::TextThread => Some(HistoryKind::TextThreads),
1249            AgentType::Custom { .. } => {
1250                if self.acp_history.read(cx).has_session_list() {
1251                    Some(HistoryKind::AgentThreads)
1252                } else {
1253                    None
1254                }
1255            }
1256        }
1257    }
1258
1259    fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1260        let Some(kind) = self.history_kind_for_selected_agent(cx) else {
1261            return;
1262        };
1263
1264        if let ActiveView::History { kind: active_kind } = self.active_view {
1265            if active_kind == kind {
1266                if let Some(previous_view) = self.previous_view.take() {
1267                    self.set_active_view(previous_view, true, window, cx);
1268                }
1269                return;
1270            }
1271        }
1272
1273        self.set_active_view(ActiveView::History { kind }, true, window, cx);
1274        cx.notify();
1275    }
1276
1277    pub(crate) fn open_saved_text_thread(
1278        &mut self,
1279        path: Arc<Path>,
1280        window: &mut Window,
1281        cx: &mut Context<Self>,
1282    ) -> Task<Result<()>> {
1283        let text_thread_task = self
1284            .text_thread_store
1285            .update(cx, |store, cx| store.open_local(path, cx));
1286        cx.spawn_in(window, async move |this, cx| {
1287            let text_thread = text_thread_task.await?;
1288            this.update_in(cx, |this, window, cx| {
1289                this.open_text_thread(text_thread, window, cx);
1290            })
1291        })
1292    }
1293
1294    pub(crate) fn open_text_thread(
1295        &mut self,
1296        text_thread: Entity<TextThread>,
1297        window: &mut Window,
1298        cx: &mut Context<Self>,
1299    ) {
1300        let lsp_adapter_delegate = make_lsp_adapter_delegate(&self.project.clone(), cx)
1301            .log_err()
1302            .flatten();
1303        let editor = cx.new(|cx| {
1304            TextThreadEditor::for_text_thread(
1305                text_thread,
1306                self.fs.clone(),
1307                self.workspace.clone(),
1308                self.project.clone(),
1309                lsp_adapter_delegate,
1310                window,
1311                cx,
1312            )
1313        });
1314
1315        if self.selected_agent != AgentType::TextThread {
1316            self.selected_agent = AgentType::TextThread;
1317            self.serialize(cx);
1318        }
1319
1320        self.set_active_view(
1321            ActiveView::text_thread(editor, self.language_registry.clone(), window, cx),
1322            true,
1323            window,
1324            cx,
1325        );
1326    }
1327
1328    pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
1329        match self.active_view {
1330            ActiveView::Configuration | ActiveView::History { .. } => {
1331                if let Some(previous_view) = self.previous_view.take() {
1332                    self.set_active_view(previous_view, true, window, cx);
1333                }
1334                cx.notify();
1335            }
1336            _ => {}
1337        }
1338    }
1339
1340    pub fn toggle_navigation_menu(
1341        &mut self,
1342        _: &ToggleNavigationMenu,
1343        window: &mut Window,
1344        cx: &mut Context<Self>,
1345    ) {
1346        if self.history_kind_for_selected_agent(cx).is_none() {
1347            return;
1348        }
1349        self.agent_navigation_menu_handle.toggle(window, cx);
1350    }
1351
1352    pub fn toggle_options_menu(
1353        &mut self,
1354        _: &ToggleOptionsMenu,
1355        window: &mut Window,
1356        cx: &mut Context<Self>,
1357    ) {
1358        self.agent_panel_menu_handle.toggle(window, cx);
1359    }
1360
1361    pub fn toggle_new_thread_menu(
1362        &mut self,
1363        _: &ToggleNewThreadMenu,
1364        window: &mut Window,
1365        cx: &mut Context<Self>,
1366    ) {
1367        self.new_thread_menu_handle.toggle(window, cx);
1368    }
1369
1370    pub fn increase_font_size(
1371        &mut self,
1372        action: &IncreaseBufferFontSize,
1373        _: &mut Window,
1374        cx: &mut Context<Self>,
1375    ) {
1376        self.handle_font_size_action(action.persist, px(1.0), cx);
1377    }
1378
1379    pub fn decrease_font_size(
1380        &mut self,
1381        action: &DecreaseBufferFontSize,
1382        _: &mut Window,
1383        cx: &mut Context<Self>,
1384    ) {
1385        self.handle_font_size_action(action.persist, px(-1.0), cx);
1386    }
1387
1388    fn handle_font_size_action(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
1389        match self.active_view.which_font_size_used() {
1390            WhichFontSize::AgentFont => {
1391                if persist {
1392                    update_settings_file(self.fs.clone(), cx, move |settings, cx| {
1393                        let agent_ui_font_size =
1394                            ThemeSettings::get_global(cx).agent_ui_font_size(cx) + delta;
1395                        let agent_buffer_font_size =
1396                            ThemeSettings::get_global(cx).agent_buffer_font_size(cx) + delta;
1397
1398                        let _ = settings
1399                            .theme
1400                            .agent_ui_font_size
1401                            .insert(f32::from(theme::clamp_font_size(agent_ui_font_size)).into());
1402                        let _ = settings.theme.agent_buffer_font_size.insert(
1403                            f32::from(theme::clamp_font_size(agent_buffer_font_size)).into(),
1404                        );
1405                    });
1406                } else {
1407                    theme::adjust_agent_ui_font_size(cx, |size| size + delta);
1408                    theme::adjust_agent_buffer_font_size(cx, |size| size + delta);
1409                }
1410            }
1411            WhichFontSize::BufferFont => {
1412                // Prompt editor uses the buffer font size, so allow the action to propagate to the
1413                // default handler that changes that font size.
1414                cx.propagate();
1415            }
1416            WhichFontSize::None => {}
1417        }
1418    }
1419
1420    pub fn reset_font_size(
1421        &mut self,
1422        action: &ResetBufferFontSize,
1423        _: &mut Window,
1424        cx: &mut Context<Self>,
1425    ) {
1426        if action.persist {
1427            update_settings_file(self.fs.clone(), cx, move |settings, _| {
1428                settings.theme.agent_ui_font_size = None;
1429                settings.theme.agent_buffer_font_size = None;
1430            });
1431        } else {
1432            theme::reset_agent_ui_font_size(cx);
1433            theme::reset_agent_buffer_font_size(cx);
1434        }
1435    }
1436
1437    pub fn reset_agent_zoom(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1438        theme::reset_agent_ui_font_size(cx);
1439        theme::reset_agent_buffer_font_size(cx);
1440    }
1441
1442    pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1443        if self.zoomed {
1444            cx.emit(PanelEvent::ZoomOut);
1445        } else {
1446            if !self.focus_handle(cx).contains_focused(window, cx) {
1447                cx.focus_self(window);
1448            }
1449            cx.emit(PanelEvent::ZoomIn);
1450        }
1451    }
1452
1453    pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1454        let agent_server_store = self.project.read(cx).agent_server_store().clone();
1455        let context_server_store = self.project.read(cx).context_server_store();
1456        let fs = self.fs.clone();
1457
1458        self.set_active_view(ActiveView::Configuration, true, window, cx);
1459        self.configuration = Some(cx.new(|cx| {
1460            AgentConfiguration::new(
1461                fs,
1462                agent_server_store,
1463                context_server_store,
1464                self.context_server_registry.clone(),
1465                self.language_registry.clone(),
1466                self.workspace.clone(),
1467                window,
1468                cx,
1469            )
1470        }));
1471
1472        if let Some(configuration) = self.configuration.as_ref() {
1473            self.configuration_subscription = Some(cx.subscribe_in(
1474                configuration,
1475                window,
1476                Self::handle_agent_configuration_event,
1477            ));
1478
1479            configuration.focus_handle(cx).focus(window, cx);
1480        }
1481    }
1482
1483    pub(crate) fn open_active_thread_as_markdown(
1484        &mut self,
1485        _: &OpenActiveThreadAsMarkdown,
1486        window: &mut Window,
1487        cx: &mut Context<Self>,
1488    ) {
1489        if let Some(workspace) = self.workspace.upgrade()
1490            && let Some(thread_view) = self.active_thread_view()
1491            && let Some(active_thread) = thread_view.read(cx).active_thread().cloned()
1492        {
1493            active_thread.update(cx, |thread, cx| {
1494                thread
1495                    .open_thread_as_markdown(workspace, window, cx)
1496                    .detach_and_log_err(cx);
1497            });
1498        }
1499    }
1500
1501    fn copy_thread_to_clipboard(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1502        let Some(thread) = self.active_native_agent_thread(cx) else {
1503            Self::show_deferred_toast(&self.workspace, "No active native thread to copy", cx);
1504            return;
1505        };
1506
1507        let workspace = self.workspace.clone();
1508        let load_task = thread.read(cx).to_db(cx);
1509
1510        cx.spawn_in(window, async move |_this, cx| {
1511            let db_thread = load_task.await;
1512            let shared_thread = SharedThread::from_db_thread(&db_thread);
1513            let thread_data = shared_thread.to_bytes()?;
1514            let encoded = base64::Engine::encode(&base64::prelude::BASE64_STANDARD, &thread_data);
1515
1516            cx.update(|_window, cx| {
1517                cx.write_to_clipboard(ClipboardItem::new_string(encoded));
1518                if let Some(workspace) = workspace.upgrade() {
1519                    workspace.update(cx, |workspace, cx| {
1520                        struct ThreadCopiedToast;
1521                        workspace.show_toast(
1522                            workspace::Toast::new(
1523                                workspace::notifications::NotificationId::unique::<ThreadCopiedToast>(),
1524                                "Thread copied to clipboard (base64 encoded)",
1525                            )
1526                            .autohide(),
1527                            cx,
1528                        );
1529                    });
1530                }
1531            })?;
1532
1533            anyhow::Ok(())
1534        })
1535        .detach_and_log_err(cx);
1536    }
1537
1538    fn show_deferred_toast(
1539        workspace: &WeakEntity<workspace::Workspace>,
1540        message: &'static str,
1541        cx: &mut App,
1542    ) {
1543        let workspace = workspace.clone();
1544        cx.defer(move |cx| {
1545            if let Some(workspace) = workspace.upgrade() {
1546                workspace.update(cx, |workspace, cx| {
1547                    struct ClipboardToast;
1548                    workspace.show_toast(
1549                        workspace::Toast::new(
1550                            workspace::notifications::NotificationId::unique::<ClipboardToast>(),
1551                            message,
1552                        )
1553                        .autohide(),
1554                        cx,
1555                    );
1556                });
1557            }
1558        });
1559    }
1560
1561    fn load_thread_from_clipboard(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1562        let Some(clipboard) = cx.read_from_clipboard() else {
1563            Self::show_deferred_toast(&self.workspace, "No clipboard content available", cx);
1564            return;
1565        };
1566
1567        let Some(encoded) = clipboard.text() else {
1568            Self::show_deferred_toast(&self.workspace, "Clipboard does not contain text", cx);
1569            return;
1570        };
1571
1572        let thread_data = match base64::Engine::decode(&base64::prelude::BASE64_STANDARD, &encoded)
1573        {
1574            Ok(data) => data,
1575            Err(_) => {
1576                Self::show_deferred_toast(
1577                    &self.workspace,
1578                    "Failed to decode clipboard content (expected base64)",
1579                    cx,
1580                );
1581                return;
1582            }
1583        };
1584
1585        let shared_thread = match SharedThread::from_bytes(&thread_data) {
1586            Ok(thread) => thread,
1587            Err(_) => {
1588                Self::show_deferred_toast(
1589                    &self.workspace,
1590                    "Failed to parse thread data from clipboard",
1591                    cx,
1592                );
1593                return;
1594            }
1595        };
1596
1597        let db_thread = shared_thread.to_db_thread();
1598        let session_id = acp::SessionId::new(uuid::Uuid::new_v4().to_string());
1599        let thread_store = self.thread_store.clone();
1600        let title = db_thread.title.clone();
1601        let workspace = self.workspace.clone();
1602
1603        cx.spawn_in(window, async move |this, cx| {
1604            thread_store
1605                .update(&mut cx.clone(), |store, cx| {
1606                    store.save_thread(session_id.clone(), db_thread, Default::default(), cx)
1607                })
1608                .await?;
1609
1610            let thread_metadata = acp_thread::AgentSessionInfo {
1611                session_id,
1612                cwd: None,
1613                title: Some(title),
1614                updated_at: Some(chrono::Utc::now()),
1615                meta: None,
1616            };
1617
1618            this.update_in(cx, |this, window, cx| {
1619                this.open_thread(thread_metadata, window, cx);
1620            })?;
1621
1622            this.update_in(cx, |_, _window, cx| {
1623                if let Some(workspace) = workspace.upgrade() {
1624                    workspace.update(cx, |workspace, cx| {
1625                        struct ThreadLoadedToast;
1626                        workspace.show_toast(
1627                            workspace::Toast::new(
1628                                workspace::notifications::NotificationId::unique::<ThreadLoadedToast>(),
1629                                "Thread loaded from clipboard",
1630                            )
1631                            .autohide(),
1632                            cx,
1633                        );
1634                    });
1635                }
1636            })?;
1637
1638            anyhow::Ok(())
1639        })
1640        .detach_and_log_err(cx);
1641    }
1642
1643    fn handle_agent_configuration_event(
1644        &mut self,
1645        _entity: &Entity<AgentConfiguration>,
1646        event: &AssistantConfigurationEvent,
1647        window: &mut Window,
1648        cx: &mut Context<Self>,
1649    ) {
1650        match event {
1651            AssistantConfigurationEvent::NewThread(provider) => {
1652                if LanguageModelRegistry::read_global(cx)
1653                    .default_model()
1654                    .is_none_or(|model| model.provider.id() != provider.id())
1655                    && let Some(model) = provider.default_model(cx)
1656                {
1657                    update_settings_file(self.fs.clone(), cx, move |settings, _| {
1658                        let provider = model.provider_id().0.to_string();
1659                        let enable_thinking = model.supports_thinking();
1660                        let effort = model
1661                            .default_effort_level()
1662                            .map(|effort| effort.value.to_string());
1663                        let model = model.id().0.to_string();
1664                        settings
1665                            .agent
1666                            .get_or_insert_default()
1667                            .set_model(LanguageModelSelection {
1668                                provider: LanguageModelProviderSetting(provider),
1669                                model,
1670                                enable_thinking,
1671                                effort,
1672                            })
1673                    });
1674                }
1675
1676                self.new_thread(&NewThread, window, cx);
1677                if let Some((thread, model)) = self
1678                    .active_native_agent_thread(cx)
1679                    .zip(provider.default_model(cx))
1680                {
1681                    thread.update(cx, |thread, cx| {
1682                        thread.set_model(model, cx);
1683                    });
1684                }
1685            }
1686        }
1687    }
1688
1689    pub fn as_active_server_view(&self) -> Option<&Entity<ConnectionView>> {
1690        match &self.active_view {
1691            ActiveView::AgentThread { server_view } => Some(server_view),
1692            _ => None,
1693        }
1694    }
1695
1696    pub fn as_active_thread_view(&self, cx: &App) -> Option<Entity<ThreadView>> {
1697        let server_view = self.as_active_server_view()?;
1698        server_view.read(cx).active_thread().cloned()
1699    }
1700
1701    pub fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
1702        match &self.active_view {
1703            ActiveView::AgentThread { server_view, .. } => server_view
1704                .read(cx)
1705                .active_thread()
1706                .map(|r| r.read(cx).thread.clone()),
1707            _ => None,
1708        }
1709    }
1710
1711    pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
1712        match &self.active_view {
1713            ActiveView::AgentThread { server_view, .. } => {
1714                server_view.read(cx).as_native_thread(cx)
1715            }
1716            _ => None,
1717        }
1718    }
1719
1720    pub(crate) fn active_text_thread_editor(&self) -> Option<Entity<TextThreadEditor>> {
1721        match &self.active_view {
1722            ActiveView::TextThread {
1723                text_thread_editor, ..
1724            } => Some(text_thread_editor.clone()),
1725            _ => None,
1726        }
1727    }
1728
1729    fn set_active_view(
1730        &mut self,
1731        new_view: ActiveView,
1732        focus: bool,
1733        window: &mut Window,
1734        cx: &mut Context<Self>,
1735    ) {
1736        let was_in_agent_history = matches!(
1737            self.active_view,
1738            ActiveView::History {
1739                kind: HistoryKind::AgentThreads
1740            }
1741        );
1742        let current_is_uninitialized = matches!(self.active_view, ActiveView::Uninitialized);
1743        let current_is_history = matches!(self.active_view, ActiveView::History { .. });
1744        let new_is_history = matches!(new_view, ActiveView::History { .. });
1745
1746        let current_is_config = matches!(self.active_view, ActiveView::Configuration);
1747        let new_is_config = matches!(new_view, ActiveView::Configuration);
1748
1749        let current_is_special = current_is_history || current_is_config;
1750        let new_is_special = new_is_history || new_is_config;
1751
1752        if current_is_uninitialized || (current_is_special && !new_is_special) {
1753            self.active_view = new_view;
1754        } else if !current_is_special && new_is_special {
1755            self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
1756        } else {
1757            if !new_is_special {
1758                self.previous_view = None;
1759            }
1760            self.active_view = new_view;
1761        }
1762
1763        // Subscribe to the active ThreadView's events (e.g. FirstSendRequested)
1764        // so the panel can intercept the first send for worktree creation.
1765        // Re-subscribe whenever the ConnectionView changes, since the inner
1766        // ThreadView may have been replaced (e.g. navigating between threads).
1767        self._active_view_observation = match &self.active_view {
1768            ActiveView::AgentThread { server_view } => {
1769                self._thread_view_subscription =
1770                    Self::subscribe_to_active_thread_view(server_view, window, cx);
1771                Some(
1772                    cx.observe_in(server_view, window, |this, server_view, window, cx| {
1773                        this._thread_view_subscription =
1774                            Self::subscribe_to_active_thread_view(&server_view, window, cx);
1775                        cx.emit(AgentPanelEvent::ActiveViewChanged);
1776                        this.serialize(cx);
1777                        cx.notify();
1778                    }),
1779                )
1780            }
1781            _ => {
1782                self._thread_view_subscription = None;
1783                None
1784            }
1785        };
1786
1787        let is_in_agent_history = matches!(
1788            self.active_view,
1789            ActiveView::History {
1790                kind: HistoryKind::AgentThreads
1791            }
1792        );
1793
1794        if !was_in_agent_history && is_in_agent_history {
1795            self.acp_history
1796                .update(cx, |history, cx| history.refresh_full_history(cx));
1797        }
1798
1799        if focus {
1800            self.focus_handle(cx).focus(window, cx);
1801        }
1802        cx.emit(AgentPanelEvent::ActiveViewChanged);
1803    }
1804
1805    fn populate_recently_updated_menu_section(
1806        mut menu: ContextMenu,
1807        panel: Entity<Self>,
1808        kind: HistoryKind,
1809        cx: &mut Context<ContextMenu>,
1810    ) -> ContextMenu {
1811        match kind {
1812            HistoryKind::AgentThreads => {
1813                let entries = panel
1814                    .read(cx)
1815                    .acp_history
1816                    .read(cx)
1817                    .sessions()
1818                    .iter()
1819                    .take(RECENTLY_UPDATED_MENU_LIMIT)
1820                    .cloned()
1821                    .collect::<Vec<_>>();
1822
1823                if entries.is_empty() {
1824                    return menu;
1825                }
1826
1827                menu = menu.header("Recently Updated");
1828
1829                for entry in entries {
1830                    let title = entry
1831                        .title
1832                        .as_ref()
1833                        .filter(|title| !title.is_empty())
1834                        .cloned()
1835                        .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE));
1836
1837                    menu = menu.entry(title, None, {
1838                        let panel = panel.downgrade();
1839                        let entry = entry.clone();
1840                        move |window, cx| {
1841                            let entry = entry.clone();
1842                            panel
1843                                .update(cx, move |this, cx| {
1844                                    this.load_agent_thread(entry.clone(), window, cx);
1845                                })
1846                                .ok();
1847                        }
1848                    });
1849                }
1850            }
1851            HistoryKind::TextThreads => {
1852                let entries = panel
1853                    .read(cx)
1854                    .text_thread_store
1855                    .read(cx)
1856                    .ordered_text_threads()
1857                    .take(RECENTLY_UPDATED_MENU_LIMIT)
1858                    .cloned()
1859                    .collect::<Vec<_>>();
1860
1861                if entries.is_empty() {
1862                    return menu;
1863                }
1864
1865                menu = menu.header("Recent Text Threads");
1866
1867                for entry in entries {
1868                    let title = if entry.title.is_empty() {
1869                        SharedString::new_static(DEFAULT_THREAD_TITLE)
1870                    } else {
1871                        entry.title.clone()
1872                    };
1873
1874                    menu = menu.entry(title, None, {
1875                        let panel = panel.downgrade();
1876                        let entry = entry.clone();
1877                        move |window, cx| {
1878                            let path = entry.path.clone();
1879                            panel
1880                                .update(cx, move |this, cx| {
1881                                    this.open_saved_text_thread(path.clone(), window, cx)
1882                                        .detach_and_log_err(cx);
1883                                })
1884                                .ok();
1885                        }
1886                    });
1887                }
1888            }
1889        }
1890
1891        menu.separator()
1892    }
1893
1894    pub fn selected_agent(&self) -> AgentType {
1895        self.selected_agent.clone()
1896    }
1897
1898    fn subscribe_to_active_thread_view(
1899        server_view: &Entity<ConnectionView>,
1900        window: &mut Window,
1901        cx: &mut Context<Self>,
1902    ) -> Option<Subscription> {
1903        server_view.read(cx).active_thread().cloned().map(|tv| {
1904            cx.subscribe_in(
1905                &tv,
1906                window,
1907                |this, view, event: &AcpThreadViewEvent, window, cx| match event {
1908                    AcpThreadViewEvent::FirstSendRequested { content } => {
1909                        this.handle_first_send_requested(view.clone(), content.clone(), window, cx);
1910                    }
1911                },
1912            )
1913        })
1914    }
1915
1916    pub fn start_thread_in(&self) -> &StartThreadIn {
1917        &self.start_thread_in
1918    }
1919
1920    fn set_start_thread_in(&mut self, action: &StartThreadIn, cx: &mut Context<Self>) {
1921        if matches!(action, StartThreadIn::NewWorktree)
1922            && !cx.has_flag::<AgentGitWorktreesFeatureFlag>()
1923        {
1924            return;
1925        }
1926
1927        let new_target = match *action {
1928            StartThreadIn::LocalProject => StartThreadIn::LocalProject,
1929            StartThreadIn::NewWorktree => {
1930                if !self.project_has_git_repository(cx) {
1931                    log::error!(
1932                        "set_start_thread_in: cannot use NewWorktree without a git repository"
1933                    );
1934                    return;
1935                }
1936                if self.project.read(cx).is_via_collab() {
1937                    log::error!("set_start_thread_in: cannot use NewWorktree in a collab project");
1938                    return;
1939                }
1940                StartThreadIn::NewWorktree
1941            }
1942        };
1943        self.start_thread_in = new_target;
1944        self.serialize(cx);
1945        cx.notify();
1946    }
1947
1948    fn selected_external_agent(&self) -> Option<ExternalAgent> {
1949        match &self.selected_agent {
1950            AgentType::NativeAgent => Some(ExternalAgent::NativeAgent),
1951            AgentType::Custom { name } => Some(ExternalAgent::Custom { name: name.clone() }),
1952            AgentType::TextThread => None,
1953        }
1954    }
1955
1956    fn sync_agent_servers_from_extensions(&mut self, cx: &mut Context<Self>) {
1957        if let Some(extension_store) = ExtensionStore::try_global(cx) {
1958            let (manifests, extensions_dir) = {
1959                let store = extension_store.read(cx);
1960                let installed = store.installed_extensions();
1961                let manifests: Vec<_> = installed
1962                    .iter()
1963                    .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
1964                    .collect();
1965                let extensions_dir = paths::extensions_dir().join("installed");
1966                (manifests, extensions_dir)
1967            };
1968
1969            self.project.update(cx, |project, cx| {
1970                project.agent_server_store().update(cx, |store, cx| {
1971                    let manifest_refs: Vec<_> = manifests
1972                        .iter()
1973                        .map(|(id, manifest)| (id.as_ref(), manifest.as_ref()))
1974                        .collect();
1975                    store.sync_extension_agents(manifest_refs, extensions_dir, cx);
1976                });
1977            });
1978        }
1979    }
1980
1981    pub fn new_external_thread_with_text(
1982        &mut self,
1983        initial_text: Option<String>,
1984        window: &mut Window,
1985        cx: &mut Context<Self>,
1986    ) {
1987        self.external_thread(
1988            None,
1989            None,
1990            initial_text.map(|text| AgentInitialContent::ContentBlock {
1991                blocks: vec![acp::ContentBlock::Text(acp::TextContent::new(text))],
1992                auto_submit: false,
1993            }),
1994            window,
1995            cx,
1996        );
1997    }
1998
1999    pub fn new_agent_thread(
2000        &mut self,
2001        agent: AgentType,
2002        window: &mut Window,
2003        cx: &mut Context<Self>,
2004    ) {
2005        match agent {
2006            AgentType::TextThread => {
2007                window.dispatch_action(NewTextThread.boxed_clone(), cx);
2008            }
2009            AgentType::NativeAgent => self.external_thread(
2010                Some(crate::ExternalAgent::NativeAgent),
2011                None,
2012                None,
2013                window,
2014                cx,
2015            ),
2016            AgentType::Custom { name } => self.external_thread(
2017                Some(crate::ExternalAgent::Custom { name }),
2018                None,
2019                None,
2020                window,
2021                cx,
2022            ),
2023        }
2024    }
2025
2026    pub fn load_agent_thread(
2027        &mut self,
2028        thread: AgentSessionInfo,
2029        window: &mut Window,
2030        cx: &mut Context<Self>,
2031    ) {
2032        let Some(agent) = self.selected_external_agent() else {
2033            return;
2034        };
2035        self.external_thread(Some(agent), Some(thread), None, window, cx);
2036    }
2037
2038    pub(crate) fn create_external_thread(
2039        &mut self,
2040        server: Rc<dyn AgentServer>,
2041        resume_thread: Option<AgentSessionInfo>,
2042        initial_content: Option<AgentInitialContent>,
2043        workspace: WeakEntity<Workspace>,
2044        project: Entity<Project>,
2045        ext_agent: ExternalAgent,
2046        window: &mut Window,
2047        cx: &mut Context<Self>,
2048    ) {
2049        let selected_agent = AgentType::from(ext_agent);
2050        if self.selected_agent != selected_agent {
2051            self.selected_agent = selected_agent;
2052            self.serialize(cx);
2053        }
2054        let thread_store = server
2055            .clone()
2056            .downcast::<agent::NativeAgentServer>()
2057            .is_some()
2058            .then(|| self.thread_store.clone());
2059
2060        let server_view = cx.new(|cx| {
2061            crate::ConnectionView::new(
2062                server,
2063                resume_thread,
2064                initial_content,
2065                workspace.clone(),
2066                project,
2067                thread_store,
2068                self.prompt_store.clone(),
2069                self.acp_history.clone(),
2070                window,
2071                cx,
2072            )
2073        });
2074
2075        self.set_active_view(ActiveView::AgentThread { server_view }, true, window, cx);
2076    }
2077
2078    fn active_thread_has_messages(&self, cx: &App) -> bool {
2079        self.active_agent_thread(cx)
2080            .is_some_and(|thread| !thread.read(cx).entries().is_empty())
2081    }
2082
2083    fn handle_first_send_requested(
2084        &mut self,
2085        thread_view: Entity<ThreadView>,
2086        content: Vec<acp::ContentBlock>,
2087        window: &mut Window,
2088        cx: &mut Context<Self>,
2089    ) {
2090        if self.start_thread_in == StartThreadIn::NewWorktree {
2091            self.handle_worktree_creation_requested(content, window, cx);
2092        } else {
2093            cx.defer_in(window, move |_this, window, cx| {
2094                thread_view.update(cx, |thread_view, cx| {
2095                    let editor = thread_view.message_editor.clone();
2096                    thread_view.send_impl(editor, window, cx);
2097                });
2098            });
2099        }
2100    }
2101
2102    fn generate_agent_branch_name() -> String {
2103        let mut rng = rand::rng();
2104        let id: String = (0..8)
2105            .map(|_| {
2106                let idx: u8 = rng.random_range(0..36);
2107                if idx < 10 {
2108                    (b'0' + idx) as char
2109                } else {
2110                    (b'a' + idx - 10) as char
2111                }
2112            })
2113            .collect();
2114        format!("agent-{id}")
2115    }
2116
2117    /// Partitions the project's visible worktrees into git-backed repositories
2118    /// and plain (non-git) paths. Git repos will have worktrees created for
2119    /// them; non-git paths are carried over to the new workspace as-is.
2120    ///
2121    /// When multiple worktrees map to the same repository, the most specific
2122    /// match wins (deepest work directory path), with a deterministic
2123    /// tie-break on entity id. Each repository appears at most once.
2124    fn classify_worktrees(
2125        &self,
2126        cx: &App,
2127    ) -> (Vec<Entity<project::git_store::Repository>>, Vec<PathBuf>) {
2128        let project = &self.project;
2129        let repositories = project.read(cx).repositories(cx).clone();
2130        let mut git_repos: Vec<Entity<project::git_store::Repository>> = Vec::new();
2131        let mut non_git_paths: Vec<PathBuf> = Vec::new();
2132        let mut seen_repo_ids = std::collections::HashSet::new();
2133
2134        for worktree in project.read(cx).visible_worktrees(cx) {
2135            let wt_path = worktree.read(cx).abs_path();
2136
2137            let matching_repo = repositories
2138                .iter()
2139                .filter_map(|(id, repo)| {
2140                    let work_dir = repo.read(cx).work_directory_abs_path.clone();
2141                    if wt_path.starts_with(work_dir.as_ref())
2142                        || work_dir.starts_with(wt_path.as_ref())
2143                    {
2144                        Some((*id, repo.clone(), work_dir.as_ref().components().count()))
2145                    } else {
2146                        None
2147                    }
2148                })
2149                .max_by(
2150                    |(left_id, _left_repo, left_depth), (right_id, _right_repo, right_depth)| {
2151                        left_depth
2152                            .cmp(right_depth)
2153                            .then_with(|| left_id.cmp(right_id))
2154                    },
2155                );
2156
2157            if let Some((id, repo, _)) = matching_repo {
2158                if seen_repo_ids.insert(id) {
2159                    git_repos.push(repo);
2160                }
2161            } else {
2162                non_git_paths.push(wt_path.to_path_buf());
2163            }
2164        }
2165
2166        (git_repos, non_git_paths)
2167    }
2168
2169    /// Kicks off an async git-worktree creation for each repository. Returns:
2170    ///
2171    /// - `creation_infos`: a vec of `(repo, new_path, receiver)` tuples—the
2172    ///   receiver resolves once the git worktree command finishes.
2173    /// - `path_remapping`: `(old_work_dir, new_worktree_path)` pairs used
2174    ///   later to remap open editor tabs into the new workspace.
2175    fn start_worktree_creations(
2176        git_repos: &[Entity<project::git_store::Repository>],
2177        branch_name: &str,
2178        worktree_directory_setting: &str,
2179        cx: &mut Context<Self>,
2180    ) -> Result<(
2181        Vec<(
2182            Entity<project::git_store::Repository>,
2183            PathBuf,
2184            futures::channel::oneshot::Receiver<Result<()>>,
2185        )>,
2186        Vec<(PathBuf, PathBuf)>,
2187    )> {
2188        let mut creation_infos = Vec::new();
2189        let mut path_remapping = Vec::new();
2190
2191        for repo in git_repos {
2192            let (work_dir, new_path, receiver) = repo.update(cx, |repo, _cx| {
2193                let original_repo = repo.original_repo_abs_path.clone();
2194                let directory =
2195                    validate_worktree_directory(&original_repo, worktree_directory_setting)?;
2196                let new_path = directory.join(branch_name);
2197                let receiver = repo.create_worktree(branch_name.to_string(), directory, None);
2198                let work_dir = repo.work_directory_abs_path.clone();
2199                anyhow::Ok((work_dir, new_path, receiver))
2200            })?;
2201            path_remapping.push((work_dir.to_path_buf(), new_path.clone()));
2202            creation_infos.push((repo.clone(), new_path, receiver));
2203        }
2204
2205        Ok((creation_infos, path_remapping))
2206    }
2207
2208    /// Waits for every in-flight worktree creation to complete. If any
2209    /// creation fails, all successfully-created worktrees are rolled back
2210    /// (removed) so the project isn't left in a half-migrated state.
2211    async fn await_and_rollback_on_failure(
2212        creation_infos: Vec<(
2213            Entity<project::git_store::Repository>,
2214            PathBuf,
2215            futures::channel::oneshot::Receiver<Result<()>>,
2216        )>,
2217        cx: &mut AsyncWindowContext,
2218    ) -> Result<Vec<PathBuf>> {
2219        let mut created_paths: Vec<PathBuf> = Vec::new();
2220        let mut repos_and_paths: Vec<(Entity<project::git_store::Repository>, PathBuf)> =
2221            Vec::new();
2222        let mut first_error: Option<anyhow::Error> = None;
2223
2224        for (repo, new_path, receiver) in creation_infos {
2225            match receiver.await {
2226                Ok(Ok(())) => {
2227                    created_paths.push(new_path.clone());
2228                    repos_and_paths.push((repo, new_path));
2229                }
2230                Ok(Err(err)) => {
2231                    if first_error.is_none() {
2232                        first_error = Some(err);
2233                    }
2234                }
2235                Err(_canceled) => {
2236                    if first_error.is_none() {
2237                        first_error = Some(anyhow!("Worktree creation was canceled"));
2238                    }
2239                }
2240            }
2241        }
2242
2243        let Some(err) = first_error else {
2244            return Ok(created_paths);
2245        };
2246
2247        // Rollback all successfully created worktrees
2248        let mut rollback_receivers = Vec::new();
2249        for (rollback_repo, rollback_path) in &repos_and_paths {
2250            if let Ok(receiver) = cx.update(|_, cx| {
2251                rollback_repo.update(cx, |repo, _cx| {
2252                    repo.remove_worktree(rollback_path.clone(), true)
2253                })
2254            }) {
2255                rollback_receivers.push((rollback_path.clone(), receiver));
2256            }
2257        }
2258        let mut rollback_failures: Vec<String> = Vec::new();
2259        for (path, receiver) in rollback_receivers {
2260            match receiver.await {
2261                Ok(Ok(())) => {}
2262                Ok(Err(rollback_err)) => {
2263                    log::error!(
2264                        "failed to rollback worktree at {}: {rollback_err}",
2265                        path.display()
2266                    );
2267                    rollback_failures.push(format!("{}: {rollback_err}", path.display()));
2268                }
2269                Err(rollback_err) => {
2270                    log::error!(
2271                        "failed to rollback worktree at {}: {rollback_err}",
2272                        path.display()
2273                    );
2274                    rollback_failures.push(format!("{}: {rollback_err}", path.display()));
2275                }
2276            }
2277        }
2278        let mut error_message = format!("Failed to create worktree: {err}");
2279        if !rollback_failures.is_empty() {
2280            error_message.push_str("\n\nFailed to clean up: ");
2281            error_message.push_str(&rollback_failures.join(", "));
2282        }
2283        Err(anyhow!(error_message))
2284    }
2285
2286    fn set_worktree_creation_error(
2287        &mut self,
2288        message: SharedString,
2289        window: &mut Window,
2290        cx: &mut Context<Self>,
2291    ) {
2292        self.worktree_creation_status = Some(WorktreeCreationStatus::Error(message));
2293        if matches!(self.active_view, ActiveView::Uninitialized) {
2294            let selected_agent = self.selected_agent.clone();
2295            self.new_agent_thread(selected_agent, window, cx);
2296        }
2297        cx.notify();
2298    }
2299
2300    fn handle_worktree_creation_requested(
2301        &mut self,
2302        content: Vec<acp::ContentBlock>,
2303        window: &mut Window,
2304        cx: &mut Context<Self>,
2305    ) {
2306        if matches!(
2307            self.worktree_creation_status,
2308            Some(WorktreeCreationStatus::Creating)
2309        ) {
2310            return;
2311        }
2312
2313        self.worktree_creation_status = Some(WorktreeCreationStatus::Creating);
2314        cx.notify();
2315
2316        let branch_name = Self::generate_agent_branch_name();
2317
2318        let (git_repos, non_git_paths) = self.classify_worktrees(cx);
2319
2320        if git_repos.is_empty() {
2321            self.set_worktree_creation_error(
2322                "No git repositories found in the project".into(),
2323                window,
2324                cx,
2325            );
2326            return;
2327        }
2328
2329        let worktree_directory_setting = ProjectSettings::get_global(cx)
2330            .git
2331            .worktree_directory
2332            .clone();
2333
2334        let (creation_infos, path_remapping) = match Self::start_worktree_creations(
2335            &git_repos,
2336            &branch_name,
2337            &worktree_directory_setting,
2338            cx,
2339        ) {
2340            Ok(result) => result,
2341            Err(err) => {
2342                self.set_worktree_creation_error(
2343                    format!("Failed to validate worktree directory: {err}").into(),
2344                    window,
2345                    cx,
2346                );
2347                return;
2348            }
2349        };
2350
2351        let (dock_structure, open_file_paths) = self
2352            .workspace
2353            .upgrade()
2354            .map(|workspace| {
2355                let dock_structure = workspace.read(cx).capture_dock_state(window, cx);
2356                let open_file_paths = workspace.read(cx).open_item_abs_paths(cx);
2357                (dock_structure, open_file_paths)
2358            })
2359            .unwrap_or_default();
2360
2361        let workspace = self.workspace.clone();
2362        let window_handle = window
2363            .window_handle()
2364            .downcast::<workspace::MultiWorkspace>();
2365
2366        let task = cx.spawn_in(window, async move |this, cx| {
2367            let created_paths = match Self::await_and_rollback_on_failure(creation_infos, cx).await
2368            {
2369                Ok(paths) => paths,
2370                Err(err) => {
2371                    this.update_in(cx, |this, window, cx| {
2372                        this.set_worktree_creation_error(format!("{err}").into(), window, cx);
2373                    })?;
2374                    return anyhow::Ok(());
2375                }
2376            };
2377
2378            let mut all_paths = created_paths;
2379            let has_non_git = !non_git_paths.is_empty();
2380            all_paths.extend(non_git_paths.iter().cloned());
2381
2382            let app_state = match workspace.upgrade() {
2383                Some(workspace) => cx.update(|_, cx| workspace.read(cx).app_state().clone())?,
2384                None => {
2385                    this.update_in(cx, |this, window, cx| {
2386                        this.set_worktree_creation_error(
2387                            "Workspace no longer available".into(),
2388                            window,
2389                            cx,
2390                        );
2391                    })?;
2392                    return anyhow::Ok(());
2393                }
2394            };
2395
2396            let this_for_error = this.clone();
2397            if let Err(err) = Self::setup_new_workspace(
2398                this,
2399                all_paths,
2400                app_state,
2401                window_handle,
2402                dock_structure,
2403                open_file_paths,
2404                path_remapping,
2405                non_git_paths,
2406                has_non_git,
2407                content,
2408                cx,
2409            )
2410            .await
2411            {
2412                this_for_error
2413                    .update_in(cx, |this, window, cx| {
2414                        this.set_worktree_creation_error(
2415                            format!("Failed to set up workspace: {err}").into(),
2416                            window,
2417                            cx,
2418                        );
2419                    })
2420                    .log_err();
2421            }
2422            anyhow::Ok(())
2423        });
2424
2425        self._worktree_creation_task = Some(cx.foreground_executor().spawn(async move {
2426            task.await.log_err();
2427        }));
2428    }
2429
2430    async fn setup_new_workspace(
2431        this: WeakEntity<Self>,
2432        all_paths: Vec<PathBuf>,
2433        app_state: Arc<workspace::AppState>,
2434        window_handle: Option<gpui::WindowHandle<workspace::MultiWorkspace>>,
2435        dock_structure: workspace::DockStructure,
2436        open_file_paths: Vec<PathBuf>,
2437        path_remapping: Vec<(PathBuf, PathBuf)>,
2438        non_git_paths: Vec<PathBuf>,
2439        has_non_git: bool,
2440        content: Vec<acp::ContentBlock>,
2441        cx: &mut AsyncWindowContext,
2442    ) -> Result<()> {
2443        let init: Option<
2444            Box<dyn FnOnce(&mut Workspace, &mut Window, &mut gpui::Context<Workspace>) + Send>,
2445        > = Some(Box::new(move |workspace, window, cx| {
2446            workspace.set_dock_structure(dock_structure, window, cx);
2447        }));
2448
2449        let (new_window_handle, _) = cx
2450            .update(|_window, cx| {
2451                Workspace::new_local(all_paths, app_state, window_handle, None, init, false, cx)
2452            })?
2453            .await?;
2454
2455        let new_workspace = new_window_handle.update(cx, |multi_workspace, _window, _cx| {
2456            let workspaces = multi_workspace.workspaces();
2457            workspaces.last().cloned()
2458        })?;
2459
2460        let Some(new_workspace) = new_workspace else {
2461            anyhow::bail!("New workspace was not added to MultiWorkspace");
2462        };
2463
2464        let panels_task = new_window_handle.update(cx, |_, _, cx| {
2465            new_workspace.update(cx, |workspace, _cx| workspace.take_panels_task())
2466        })?;
2467        if let Some(task) = panels_task {
2468            task.await.log_err();
2469        }
2470
2471        let initial_content = AgentInitialContent::ContentBlock {
2472            blocks: content,
2473            auto_submit: true,
2474        };
2475
2476        new_window_handle.update(cx, |_multi_workspace, window, cx| {
2477            new_workspace.update(cx, |workspace, cx| {
2478                if has_non_git {
2479                    let toast_id = workspace::notifications::NotificationId::unique::<AgentPanel>();
2480                    workspace.show_toast(
2481                        workspace::Toast::new(
2482                            toast_id,
2483                            "Some project folders are not git repositories. \
2484                             They were included as-is without creating a worktree.",
2485                        ),
2486                        cx,
2487                    );
2488                }
2489
2490                let remapped_paths: Vec<PathBuf> = open_file_paths
2491                    .iter()
2492                    .filter_map(|original_path| {
2493                        let best_match = path_remapping
2494                            .iter()
2495                            .filter_map(|(old_root, new_root)| {
2496                                original_path.strip_prefix(old_root).ok().map(|relative| {
2497                                    (old_root.components().count(), new_root.join(relative))
2498                                })
2499                            })
2500                            .max_by_key(|(depth, _)| *depth);
2501
2502                        if let Some((_, remapped_path)) = best_match {
2503                            return Some(remapped_path);
2504                        }
2505
2506                        for non_git in &non_git_paths {
2507                            if original_path.starts_with(non_git) {
2508                                return Some(original_path.clone());
2509                            }
2510                        }
2511                        None
2512                    })
2513                    .collect();
2514
2515                if !remapped_paths.is_empty() {
2516                    workspace
2517                        .open_paths(
2518                            remapped_paths,
2519                            workspace::OpenOptions::default(),
2520                            None,
2521                            window,
2522                            cx,
2523                        )
2524                        .detach();
2525                }
2526
2527                workspace.focus_panel::<AgentPanel>(window, cx);
2528                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
2529                    panel.update(cx, |panel, cx| {
2530                        panel.external_thread(None, None, Some(initial_content), window, cx);
2531                    });
2532                }
2533            });
2534        })?;
2535
2536        new_window_handle.update(cx, |multi_workspace, _window, cx| {
2537            multi_workspace.activate(new_workspace.clone(), cx);
2538        })?;
2539
2540        this.update_in(cx, |this, _window, cx| {
2541            this.worktree_creation_status = None;
2542            cx.notify();
2543        })?;
2544
2545        anyhow::Ok(())
2546    }
2547}
2548
2549impl Focusable for AgentPanel {
2550    fn focus_handle(&self, cx: &App) -> FocusHandle {
2551        match &self.active_view {
2552            ActiveView::Uninitialized => self.focus_handle.clone(),
2553            ActiveView::AgentThread { server_view, .. } => server_view.focus_handle(cx),
2554            ActiveView::History { kind } => match kind {
2555                HistoryKind::AgentThreads => self.acp_history.focus_handle(cx),
2556                HistoryKind::TextThreads => self.text_thread_history.focus_handle(cx),
2557            },
2558            ActiveView::TextThread {
2559                text_thread_editor, ..
2560            } => text_thread_editor.focus_handle(cx),
2561            ActiveView::Configuration => {
2562                if let Some(configuration) = self.configuration.as_ref() {
2563                    configuration.focus_handle(cx)
2564                } else {
2565                    self.focus_handle.clone()
2566                }
2567            }
2568        }
2569    }
2570}
2571
2572fn agent_panel_dock_position(cx: &App) -> DockPosition {
2573    AgentSettings::get_global(cx).dock.into()
2574}
2575
2576pub enum AgentPanelEvent {
2577    ActiveViewChanged,
2578}
2579
2580impl EventEmitter<PanelEvent> for AgentPanel {}
2581impl EventEmitter<AgentPanelEvent> for AgentPanel {}
2582
2583impl Panel for AgentPanel {
2584    fn persistent_name() -> &'static str {
2585        "AgentPanel"
2586    }
2587
2588    fn panel_key() -> &'static str {
2589        AGENT_PANEL_KEY
2590    }
2591
2592    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
2593        agent_panel_dock_position(cx)
2594    }
2595
2596    fn position_is_valid(&self, position: DockPosition) -> bool {
2597        position != DockPosition::Bottom
2598    }
2599
2600    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
2601        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
2602            settings
2603                .agent
2604                .get_or_insert_default()
2605                .set_dock(position.into());
2606        });
2607    }
2608
2609    fn size(&self, window: &Window, cx: &App) -> Pixels {
2610        let settings = AgentSettings::get_global(cx);
2611        match self.position(window, cx) {
2612            DockPosition::Left | DockPosition::Right => {
2613                self.width.unwrap_or(settings.default_width)
2614            }
2615            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
2616        }
2617    }
2618
2619    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
2620        match self.position(window, cx) {
2621            DockPosition::Left | DockPosition::Right => self.width = size,
2622            DockPosition::Bottom => self.height = size,
2623        }
2624        self.serialize(cx);
2625        cx.notify();
2626    }
2627
2628    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
2629        if active
2630            && matches!(self.active_view, ActiveView::Uninitialized)
2631            && !matches!(
2632                self.worktree_creation_status,
2633                Some(WorktreeCreationStatus::Creating)
2634            )
2635        {
2636            let selected_agent = self.selected_agent.clone();
2637            self.new_agent_thread(selected_agent, window, cx);
2638        }
2639    }
2640
2641    fn remote_id() -> Option<proto::PanelId> {
2642        Some(proto::PanelId::AssistantPanel)
2643    }
2644
2645    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
2646        (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
2647    }
2648
2649    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
2650        Some("Agent Panel")
2651    }
2652
2653    fn toggle_action(&self) -> Box<dyn Action> {
2654        Box::new(ToggleFocus)
2655    }
2656
2657    fn activation_priority(&self) -> u32 {
2658        3
2659    }
2660
2661    fn enabled(&self, cx: &App) -> bool {
2662        AgentSettings::get_global(cx).enabled(cx)
2663    }
2664
2665    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
2666        self.zoomed
2667    }
2668
2669    fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
2670        self.zoomed = zoomed;
2671        cx.notify();
2672    }
2673}
2674
2675impl AgentPanel {
2676    fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
2677        const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
2678
2679        let content = match &self.active_view {
2680            ActiveView::AgentThread { server_view } => {
2681                let is_generating_title = server_view
2682                    .read(cx)
2683                    .as_native_thread(cx)
2684                    .map_or(false, |t| t.read(cx).is_generating_title());
2685
2686                if let Some(title_editor) = server_view
2687                    .read(cx)
2688                    .parent_thread(cx)
2689                    .map(|r| r.read(cx).title_editor.clone())
2690                {
2691                    let container = div()
2692                        .w_full()
2693                        .on_action({
2694                            let thread_view = server_view.downgrade();
2695                            move |_: &menu::Confirm, window, cx| {
2696                                if let Some(thread_view) = thread_view.upgrade() {
2697                                    thread_view.focus_handle(cx).focus(window, cx);
2698                                }
2699                            }
2700                        })
2701                        .on_action({
2702                            let thread_view = server_view.downgrade();
2703                            move |_: &editor::actions::Cancel, window, cx| {
2704                                if let Some(thread_view) = thread_view.upgrade() {
2705                                    thread_view.focus_handle(cx).focus(window, cx);
2706                                }
2707                            }
2708                        })
2709                        .child(title_editor);
2710
2711                    if is_generating_title {
2712                        container
2713                            .with_animation(
2714                                "generating_title",
2715                                Animation::new(Duration::from_secs(2))
2716                                    .repeat()
2717                                    .with_easing(pulsating_between(0.4, 0.8)),
2718                                |div, delta| div.opacity(delta),
2719                            )
2720                            .into_any_element()
2721                    } else {
2722                        container.into_any_element()
2723                    }
2724                } else {
2725                    Label::new(server_view.read(cx).title(cx))
2726                        .color(Color::Muted)
2727                        .truncate()
2728                        .into_any_element()
2729                }
2730            }
2731            ActiveView::TextThread {
2732                title_editor,
2733                text_thread_editor,
2734                ..
2735            } => {
2736                let summary = text_thread_editor.read(cx).text_thread().read(cx).summary();
2737
2738                match summary {
2739                    TextThreadSummary::Pending => Label::new(TextThreadSummary::DEFAULT)
2740                        .color(Color::Muted)
2741                        .truncate()
2742                        .into_any_element(),
2743                    TextThreadSummary::Content(summary) => {
2744                        if summary.done {
2745                            div()
2746                                .w_full()
2747                                .child(title_editor.clone())
2748                                .into_any_element()
2749                        } else {
2750                            Label::new(LOADING_SUMMARY_PLACEHOLDER)
2751                                .truncate()
2752                                .color(Color::Muted)
2753                                .with_animation(
2754                                    "generating_title",
2755                                    Animation::new(Duration::from_secs(2))
2756                                        .repeat()
2757                                        .with_easing(pulsating_between(0.4, 0.8)),
2758                                    |label, delta| label.alpha(delta),
2759                                )
2760                                .into_any_element()
2761                        }
2762                    }
2763                    TextThreadSummary::Error => h_flex()
2764                        .w_full()
2765                        .child(title_editor.clone())
2766                        .child(
2767                            IconButton::new("retry-summary-generation", IconName::RotateCcw)
2768                                .icon_size(IconSize::Small)
2769                                .on_click({
2770                                    let text_thread_editor = text_thread_editor.clone();
2771                                    move |_, _window, cx| {
2772                                        text_thread_editor.update(cx, |text_thread_editor, cx| {
2773                                            text_thread_editor.regenerate_summary(cx);
2774                                        });
2775                                    }
2776                                })
2777                                .tooltip(move |_window, cx| {
2778                                    cx.new(|_| {
2779                                        Tooltip::new("Failed to generate title")
2780                                            .meta("Click to try again")
2781                                    })
2782                                    .into()
2783                                }),
2784                        )
2785                        .into_any_element(),
2786                }
2787            }
2788            ActiveView::History { kind } => {
2789                let title = match kind {
2790                    HistoryKind::AgentThreads => "History",
2791                    HistoryKind::TextThreads => "Text Thread History",
2792                };
2793                Label::new(title).truncate().into_any_element()
2794            }
2795            ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
2796            ActiveView::Uninitialized => Label::new("Agent").truncate().into_any_element(),
2797        };
2798
2799        h_flex()
2800            .key_context("TitleEditor")
2801            .id("TitleEditor")
2802            .flex_grow()
2803            .w_full()
2804            .max_w_full()
2805            .overflow_x_scroll()
2806            .child(content)
2807            .into_any()
2808    }
2809
2810    fn handle_regenerate_thread_title(thread_view: Entity<ConnectionView>, cx: &mut App) {
2811        thread_view.update(cx, |thread_view, cx| {
2812            if let Some(thread) = thread_view.as_native_thread(cx) {
2813                thread.update(cx, |thread, cx| {
2814                    thread.generate_title(cx);
2815                });
2816            }
2817        });
2818    }
2819
2820    fn handle_regenerate_text_thread_title(
2821        text_thread_editor: Entity<TextThreadEditor>,
2822        cx: &mut App,
2823    ) {
2824        text_thread_editor.update(cx, |text_thread_editor, cx| {
2825            text_thread_editor.regenerate_summary(cx);
2826        });
2827    }
2828
2829    fn render_panel_options_menu(
2830        &self,
2831        window: &mut Window,
2832        cx: &mut Context<Self>,
2833    ) -> impl IntoElement {
2834        let focus_handle = self.focus_handle(cx);
2835
2836        let full_screen_label = if self.is_zoomed(window, cx) {
2837            "Disable Full Screen"
2838        } else {
2839            "Enable Full Screen"
2840        };
2841
2842        let text_thread_view = match &self.active_view {
2843            ActiveView::TextThread {
2844                text_thread_editor, ..
2845            } => Some(text_thread_editor.clone()),
2846            _ => None,
2847        };
2848        let text_thread_with_messages = match &self.active_view {
2849            ActiveView::TextThread {
2850                text_thread_editor, ..
2851            } => text_thread_editor
2852                .read(cx)
2853                .text_thread()
2854                .read(cx)
2855                .messages(cx)
2856                .any(|message| message.role == language_model::Role::Assistant),
2857            _ => false,
2858        };
2859
2860        let thread_view = match &self.active_view {
2861            ActiveView::AgentThread { server_view } => Some(server_view.clone()),
2862            _ => None,
2863        };
2864        let thread_with_messages = match &self.active_view {
2865            ActiveView::AgentThread { server_view } => {
2866                server_view.read(cx).has_user_submitted_prompt(cx)
2867            }
2868            _ => false,
2869        };
2870        let has_auth_methods = match &self.active_view {
2871            ActiveView::AgentThread { server_view } => server_view.read(cx).has_auth_methods(),
2872            _ => false,
2873        };
2874
2875        PopoverMenu::new("agent-options-menu")
2876            .trigger_with_tooltip(
2877                IconButton::new("agent-options-menu", IconName::Ellipsis)
2878                    .icon_size(IconSize::Small),
2879                {
2880                    let focus_handle = focus_handle.clone();
2881                    move |_window, cx| {
2882                        Tooltip::for_action_in(
2883                            "Toggle Agent Menu",
2884                            &ToggleOptionsMenu,
2885                            &focus_handle,
2886                            cx,
2887                        )
2888                    }
2889                },
2890            )
2891            .anchor(Corner::TopRight)
2892            .with_handle(self.agent_panel_menu_handle.clone())
2893            .menu({
2894                move |window, cx| {
2895                    Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
2896                        menu = menu.context(focus_handle.clone());
2897
2898                        if thread_with_messages | text_thread_with_messages {
2899                            menu = menu.header("Current Thread");
2900
2901                            if let Some(text_thread_view) = text_thread_view.as_ref() {
2902                                menu = menu
2903                                    .entry("Regenerate Thread Title", None, {
2904                                        let text_thread_view = text_thread_view.clone();
2905                                        move |_, cx| {
2906                                            Self::handle_regenerate_text_thread_title(
2907                                                text_thread_view.clone(),
2908                                                cx,
2909                                            );
2910                                        }
2911                                    })
2912                                    .separator();
2913                            }
2914
2915                            if let Some(thread_view) = thread_view.as_ref() {
2916                                menu = menu
2917                                    .entry("Regenerate Thread Title", None, {
2918                                        let thread_view = thread_view.clone();
2919                                        move |_, cx| {
2920                                            Self::handle_regenerate_thread_title(
2921                                                thread_view.clone(),
2922                                                cx,
2923                                            );
2924                                        }
2925                                    })
2926                                    .separator();
2927                            }
2928                        }
2929
2930                        menu = menu
2931                            .header("MCP Servers")
2932                            .action(
2933                                "View Server Extensions",
2934                                Box::new(zed_actions::Extensions {
2935                                    category_filter: Some(
2936                                        zed_actions::ExtensionCategoryFilter::ContextServers,
2937                                    ),
2938                                    id: None,
2939                                }),
2940                            )
2941                            .action("Add Custom Server…", Box::new(AddContextServer))
2942                            .separator()
2943                            .action("Rules", Box::new(OpenRulesLibrary::default()))
2944                            .action("Profiles", Box::new(ManageProfiles::default()))
2945                            .action("Settings", Box::new(OpenSettings))
2946                            .separator()
2947                            .action(full_screen_label, Box::new(ToggleZoom));
2948
2949                        if has_auth_methods {
2950                            menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
2951                        }
2952
2953                        menu
2954                    }))
2955                }
2956            })
2957    }
2958
2959    fn render_recent_entries_menu(
2960        &self,
2961        icon: IconName,
2962        corner: Corner,
2963        cx: &mut Context<Self>,
2964    ) -> impl IntoElement {
2965        let focus_handle = self.focus_handle(cx);
2966
2967        PopoverMenu::new("agent-nav-menu")
2968            .trigger_with_tooltip(
2969                IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
2970                {
2971                    move |_window, cx| {
2972                        Tooltip::for_action_in(
2973                            "Toggle Recently Updated Threads",
2974                            &ToggleNavigationMenu,
2975                            &focus_handle,
2976                            cx,
2977                        )
2978                    }
2979                },
2980            )
2981            .anchor(corner)
2982            .with_handle(self.agent_navigation_menu_handle.clone())
2983            .menu({
2984                let menu = self.agent_navigation_menu.clone();
2985                move |window, cx| {
2986                    telemetry::event!("View Thread History Clicked");
2987
2988                    if let Some(menu) = menu.as_ref() {
2989                        menu.update(cx, |_, cx| {
2990                            cx.defer_in(window, |menu, window, cx| {
2991                                menu.rebuild(window, cx);
2992                            });
2993                        })
2994                    }
2995                    menu.clone()
2996                }
2997            })
2998    }
2999
3000    fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
3001        let focus_handle = self.focus_handle(cx);
3002
3003        IconButton::new("go-back", IconName::ArrowLeft)
3004            .icon_size(IconSize::Small)
3005            .on_click(cx.listener(|this, _, window, cx| {
3006                this.go_back(&workspace::GoBack, window, cx);
3007            }))
3008            .tooltip({
3009                move |_window, cx| {
3010                    Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
3011                }
3012            })
3013    }
3014
3015    fn project_has_git_repository(&self, cx: &App) -> bool {
3016        !self.project.read(cx).repositories(cx).is_empty()
3017    }
3018
3019    fn render_start_thread_in_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
3020        let has_git_repo = self.project_has_git_repository(cx);
3021        let is_via_collab = self.project.read(cx).is_via_collab();
3022
3023        let is_creating = matches!(
3024            self.worktree_creation_status,
3025            Some(WorktreeCreationStatus::Creating)
3026        );
3027
3028        let current_target = self.start_thread_in;
3029        let trigger_label = self.start_thread_in.label();
3030
3031        let icon = if self.start_thread_in_menu_handle.is_deployed() {
3032            IconName::ChevronUp
3033        } else {
3034            IconName::ChevronDown
3035        };
3036
3037        let trigger_button = Button::new("thread-target-trigger", trigger_label)
3038            .label_size(LabelSize::Small)
3039            .color(Color::Muted)
3040            .icon(icon)
3041            .icon_size(IconSize::XSmall)
3042            .icon_position(IconPosition::End)
3043            .icon_color(Color::Muted)
3044            .disabled(is_creating);
3045
3046        let dock_position = AgentSettings::get_global(cx).dock;
3047        let documentation_side = match dock_position {
3048            settings::DockPosition::Left => DocumentationSide::Right,
3049            settings::DockPosition::Bottom | settings::DockPosition::Right => {
3050                DocumentationSide::Left
3051            }
3052        };
3053
3054        PopoverMenu::new("thread-target-selector")
3055            .trigger(trigger_button)
3056            .anchor(gpui::Corner::BottomRight)
3057            .with_handle(self.start_thread_in_menu_handle.clone())
3058            .menu(move |window, cx| {
3059                let current_target = current_target;
3060                Some(ContextMenu::build(window, cx, move |menu, _window, _cx| {
3061                    let is_local_selected = current_target == StartThreadIn::LocalProject;
3062                    let is_new_worktree_selected = current_target == StartThreadIn::NewWorktree;
3063
3064                    let new_worktree_disabled = !has_git_repo || is_via_collab;
3065
3066                    menu.header("Start Thread In…")
3067                        .item(
3068                            ContextMenuEntry::new("Local Project")
3069                                .icon(StartThreadIn::LocalProject.icon())
3070                                .icon_color(Color::Muted)
3071                                .toggleable(IconPosition::End, is_local_selected)
3072                                .handler(|window, cx| {
3073                                    window
3074                                        .dispatch_action(Box::new(StartThreadIn::LocalProject), cx);
3075                                }),
3076                        )
3077                        .item({
3078                            let entry = ContextMenuEntry::new("New Worktree")
3079                                .icon(StartThreadIn::NewWorktree.icon())
3080                                .icon_color(Color::Muted)
3081                                .toggleable(IconPosition::End, is_new_worktree_selected)
3082                                .disabled(new_worktree_disabled)
3083                                .handler(|window, cx| {
3084                                    window
3085                                        .dispatch_action(Box::new(StartThreadIn::NewWorktree), cx);
3086                                });
3087
3088                            if new_worktree_disabled {
3089                                entry.documentation_aside(documentation_side, move |_| {
3090                                    let reason = if !has_git_repo {
3091                                        "No git repository found in this project."
3092                                    } else {
3093                                        "Not available for remote/collab projects yet."
3094                                    };
3095                                    Label::new(reason)
3096                                        .color(Color::Muted)
3097                                        .size(LabelSize::Small)
3098                                        .into_any_element()
3099                                })
3100                            } else {
3101                                entry
3102                            }
3103                        })
3104                }))
3105            })
3106    }
3107
3108    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3109        let agent_server_store = self.project.read(cx).agent_server_store().clone();
3110        let focus_handle = self.focus_handle(cx);
3111
3112        let (selected_agent_custom_icon, selected_agent_label) =
3113            if let AgentType::Custom { name, .. } = &self.selected_agent {
3114                let store = agent_server_store.read(cx);
3115                let icon = store.agent_icon(&ExternalAgentServerName(name.clone()));
3116
3117                let label = store
3118                    .agent_display_name(&ExternalAgentServerName(name.clone()))
3119                    .unwrap_or_else(|| self.selected_agent.label());
3120                (icon, label)
3121            } else {
3122                (None, self.selected_agent.label())
3123            };
3124
3125        let active_thread = match &self.active_view {
3126            ActiveView::AgentThread { server_view } => server_view.read(cx).as_native_thread(cx),
3127            ActiveView::Uninitialized
3128            | ActiveView::TextThread { .. }
3129            | ActiveView::History { .. }
3130            | ActiveView::Configuration => None,
3131        };
3132
3133        let new_thread_menu = PopoverMenu::new("new_thread_menu")
3134            .trigger_with_tooltip(
3135                IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
3136                {
3137                    let focus_handle = focus_handle.clone();
3138                    move |_window, cx| {
3139                        Tooltip::for_action_in(
3140                            "New Thread…",
3141                            &ToggleNewThreadMenu,
3142                            &focus_handle,
3143                            cx,
3144                        )
3145                    }
3146                },
3147            )
3148            .anchor(Corner::TopRight)
3149            .with_handle(self.new_thread_menu_handle.clone())
3150            .menu({
3151                let selected_agent = self.selected_agent.clone();
3152                let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
3153
3154                let workspace = self.workspace.clone();
3155                let is_via_collab = workspace
3156                    .update(cx, |workspace, cx| {
3157                        workspace.project().read(cx).is_via_collab()
3158                    })
3159                    .unwrap_or_default();
3160
3161                move |window, cx| {
3162                    telemetry::event!("New Thread Clicked");
3163
3164                    let active_thread = active_thread.clone();
3165                    Some(ContextMenu::build(window, cx, |menu, _window, cx| {
3166                        menu.context(focus_handle.clone())
3167                            .when_some(active_thread, |this, active_thread| {
3168                                let thread = active_thread.read(cx);
3169
3170                                if !thread.is_empty() {
3171                                    let session_id = thread.id().clone();
3172                                    this.item(
3173                                        ContextMenuEntry::new("New From Summary")
3174                                            .icon(IconName::ThreadFromSummary)
3175                                            .icon_color(Color::Muted)
3176                                            .handler(move |window, cx| {
3177                                                window.dispatch_action(
3178                                                    Box::new(NewNativeAgentThreadFromSummary {
3179                                                        from_session_id: session_id.clone(),
3180                                                    }),
3181                                                    cx,
3182                                                );
3183                                            }),
3184                                    )
3185                                } else {
3186                                    this
3187                                }
3188                            })
3189                            .item(
3190                                ContextMenuEntry::new("Zed Agent")
3191                                    .when(
3192                                        is_agent_selected(AgentType::NativeAgent)
3193                                            | is_agent_selected(AgentType::TextThread),
3194                                        |this| {
3195                                            this.action(Box::new(NewExternalAgentThread {
3196                                                agent: None,
3197                                            }))
3198                                        },
3199                                    )
3200                                    .icon(IconName::ZedAgent)
3201                                    .icon_color(Color::Muted)
3202                                    .handler({
3203                                        let workspace = workspace.clone();
3204                                        move |window, cx| {
3205                                            if let Some(workspace) = workspace.upgrade() {
3206                                                workspace.update(cx, |workspace, cx| {
3207                                                    if let Some(panel) =
3208                                                        workspace.panel::<AgentPanel>(cx)
3209                                                    {
3210                                                        panel.update(cx, |panel, cx| {
3211                                                            panel.new_agent_thread(
3212                                                                AgentType::NativeAgent,
3213                                                                window,
3214                                                                cx,
3215                                                            );
3216                                                        });
3217                                                    }
3218                                                });
3219                                            }
3220                                        }
3221                                    }),
3222                            )
3223                            .item(
3224                                ContextMenuEntry::new("Text Thread")
3225                                    .action(NewTextThread.boxed_clone())
3226                                    .icon(IconName::TextThread)
3227                                    .icon_color(Color::Muted)
3228                                    .handler({
3229                                        let workspace = workspace.clone();
3230                                        move |window, cx| {
3231                                            if let Some(workspace) = workspace.upgrade() {
3232                                                workspace.update(cx, |workspace, cx| {
3233                                                    if let Some(panel) =
3234                                                        workspace.panel::<AgentPanel>(cx)
3235                                                    {
3236                                                        panel.update(cx, |panel, cx| {
3237                                                            panel.new_agent_thread(
3238                                                                AgentType::TextThread,
3239                                                                window,
3240                                                                cx,
3241                                                            );
3242                                                        });
3243                                                    }
3244                                                });
3245                                            }
3246                                        }
3247                                    }),
3248                            )
3249                            .separator()
3250                            .header("External Agents")
3251                            .map(|mut menu| {
3252                                let agent_server_store = agent_server_store.read(cx);
3253                                let registry_store =
3254                                    project::AgentRegistryStore::try_global(cx);
3255                                let registry_store_ref =
3256                                    registry_store.as_ref().map(|s| s.read(cx));
3257
3258                                struct AgentMenuItem {
3259                                    id: ExternalAgentServerName,
3260                                    display_name: SharedString,
3261                                }
3262
3263                                let agent_items = agent_server_store
3264                                    .external_agents()
3265                                    .map(|name| {
3266                                        let display_name = agent_server_store
3267                                            .agent_display_name(name)
3268                                            .or_else(|| {
3269                                                registry_store_ref
3270                                                    .as_ref()
3271                                                    .and_then(|store| store.agent(name.0.as_ref()))
3272                                                    .map(|a| a.name().clone())
3273                                            })
3274                                            .unwrap_or_else(|| name.0.clone());
3275                                        AgentMenuItem {
3276                                            id: name.clone(),
3277                                            display_name,
3278                                        }
3279                                    })
3280                                    .sorted_unstable_by_key(|e| e.display_name.to_lowercase())
3281                                    .collect::<Vec<_>>();
3282
3283                                for item in &agent_items {
3284                                    let mut entry =
3285                                        ContextMenuEntry::new(item.display_name.clone());
3286
3287                                    let icon_path = agent_server_store
3288                                        .agent_icon(&item.id)
3289                                        .or_else(|| {
3290                                            registry_store_ref
3291                                                .as_ref()
3292                                                .and_then(|store| store.agent(item.id.0.as_str()))
3293                                                .and_then(|a| a.icon_path().cloned())
3294                                        });
3295
3296                                    if let Some(icon_path) = icon_path {
3297                                        entry = entry.custom_icon_svg(icon_path);
3298                                    } else {
3299                                        entry = entry.icon(IconName::Sparkle);
3300                                    }
3301
3302                                    entry = entry
3303                                        .when(
3304                                            is_agent_selected(AgentType::Custom {
3305                                                name: item.id.0.clone(),
3306                                            }),
3307                                            |this| {
3308                                                this.action(Box::new(
3309                                                    NewExternalAgentThread { agent: None },
3310                                                ))
3311                                            },
3312                                        )
3313                                        .icon_color(Color::Muted)
3314                                        .disabled(is_via_collab)
3315                                        .handler({
3316                                            let workspace = workspace.clone();
3317                                            let agent_id = item.id.clone();
3318                                            move |window, cx| {
3319                                                if let Some(workspace) = workspace.upgrade() {
3320                                                    workspace.update(cx, |workspace, cx| {
3321                                                        if let Some(panel) =
3322                                                            workspace.panel::<AgentPanel>(cx)
3323                                                        {
3324                                                            panel.update(cx, |panel, cx| {
3325                                                                panel.new_agent_thread(
3326                                                                    AgentType::Custom {
3327                                                                        name: agent_id.0.clone(),
3328                                                                    },
3329                                                                    window,
3330                                                                    cx,
3331                                                                );
3332                                                            });
3333                                                        }
3334                                                    });
3335                                                }
3336                                            }
3337                                        });
3338
3339                                    menu = menu.item(entry);
3340                                }
3341
3342                                menu
3343                            })
3344                            .separator()
3345                            .map(|mut menu| {
3346                                let agent_server_store = agent_server_store.read(cx);
3347                                let registry_store =
3348                                    project::AgentRegistryStore::try_global(cx);
3349                                let registry_store_ref =
3350                                    registry_store.as_ref().map(|s| s.read(cx));
3351
3352                                let previous_built_in_ids: &[ExternalAgentServerName] =
3353                                    &[CLAUDE_AGENT_NAME.into(), CODEX_NAME.into(), GEMINI_NAME.into()];
3354
3355                                let promoted_items = previous_built_in_ids
3356                                    .iter()
3357                                    .filter(|id| {
3358                                        !agent_server_store.external_agents.contains_key(*id)
3359                                    })
3360                                    .filter_map(|name| {
3361                                        let display_name = registry_store_ref
3362                                            .as_ref()
3363                                            .and_then(|store| store.agent(name.0.as_ref()))
3364                                            .map(|a| a.name().clone())?;
3365                                        Some((name.clone(), display_name))
3366                                    })
3367                                    .sorted_unstable_by_key(|(_, display_name)| display_name.to_lowercase())
3368                                    .collect::<Vec<_>>();
3369
3370                                for (agent_id, display_name) in &promoted_items {
3371                                    let mut entry =
3372                                        ContextMenuEntry::new(display_name.clone());
3373
3374                                    let icon_path = registry_store_ref
3375                                        .as_ref()
3376                                        .and_then(|store| store.agent(agent_id.0.as_str()))
3377                                        .and_then(|a| a.icon_path().cloned());
3378
3379                                    if let Some(icon_path) = icon_path {
3380                                        entry = entry.custom_icon_svg(icon_path);
3381                                    } else {
3382                                        entry = entry.icon(IconName::Sparkle);
3383                                    }
3384
3385                                    entry = entry
3386                                        .icon_color(Color::Muted)
3387                                        .disabled(is_via_collab)
3388                                        .handler({
3389                                            let workspace = workspace.clone();
3390                                            let agent_id = agent_id.clone();
3391                                            move |window, cx| {
3392                                                let fs = <dyn fs::Fs>::global(cx);
3393                                                let agent_id_string =
3394                                                    agent_id.to_string();
3395                                                settings::update_settings_file(
3396                                                    fs,
3397                                                    cx,
3398                                                    move |settings, _| {
3399                                                        let agent_servers = settings
3400                                                            .agent_servers
3401                                                            .get_or_insert_default();
3402                                                        agent_servers.entry(agent_id_string).or_insert_with(|| {
3403                                                            settings::CustomAgentServerSettings::Registry {
3404                                                                default_mode: None,
3405                                                                default_model: None,
3406                                                                env: Default::default(),
3407                                                                favorite_models: Vec::new(),
3408                                                                default_config_options: Default::default(),
3409                                                                favorite_config_option_values: Default::default(),
3410                                                            }
3411                                                        });
3412                                                    },
3413                                                );
3414
3415                                                if let Some(workspace) = workspace.upgrade() {
3416                                                    workspace.update(cx, |workspace, cx| {
3417                                                        if let Some(panel) =
3418                                                            workspace.panel::<AgentPanel>(cx)
3419                                                        {
3420                                                            panel.update(cx, |panel, cx| {
3421                                                                panel.new_agent_thread(
3422                                                                    AgentType::Custom {
3423                                                                        name: agent_id.0.clone(),
3424                                                                    },
3425                                                                    window,
3426                                                                    cx,
3427                                                                );
3428                                                            });
3429                                                        }
3430                                                    });
3431                                                }
3432                                            }
3433                                        });
3434
3435                                    menu = menu.item(entry);
3436                                }
3437
3438                                menu
3439                            })
3440                            .item(
3441                                ContextMenuEntry::new("Add More Agents")
3442                                    .icon(IconName::Plus)
3443                                    .icon_color(Color::Muted)
3444                                    .handler({
3445                                        move |window, cx| {
3446                                            window.dispatch_action(
3447                                                Box::new(zed_actions::AcpRegistry),
3448                                                cx,
3449                                            )
3450                                        }
3451                                    }),
3452                            )
3453                    }))
3454                }
3455            });
3456
3457        let is_thread_loading = self
3458            .active_thread_view()
3459            .map(|thread| thread.read(cx).is_loading())
3460            .unwrap_or(false);
3461
3462        let has_custom_icon = selected_agent_custom_icon.is_some();
3463
3464        let selected_agent = div()
3465            .id("selected_agent_icon")
3466            .when_some(selected_agent_custom_icon, |this, icon_path| {
3467                this.px_1()
3468                    .child(Icon::from_external_svg(icon_path).color(Color::Muted))
3469            })
3470            .when(!has_custom_icon, |this| {
3471                this.when_some(self.selected_agent.icon(), |this, icon| {
3472                    this.px_1().child(Icon::new(icon).color(Color::Muted))
3473                })
3474            })
3475            .tooltip(move |_, cx| {
3476                Tooltip::with_meta(selected_agent_label.clone(), None, "Selected Agent", cx)
3477            });
3478
3479        let selected_agent = if is_thread_loading {
3480            selected_agent
3481                .with_animation(
3482                    "pulsating-icon",
3483                    Animation::new(Duration::from_secs(1))
3484                        .repeat()
3485                        .with_easing(pulsating_between(0.2, 0.6)),
3486                    |icon, delta| icon.opacity(delta),
3487                )
3488                .into_any_element()
3489        } else {
3490            selected_agent.into_any_element()
3491        };
3492
3493        let show_history_menu = self.history_kind_for_selected_agent(cx).is_some();
3494        let has_v2_flag = cx.has_flag::<AgentV2FeatureFlag>();
3495
3496        h_flex()
3497            .id("agent-panel-toolbar")
3498            .h(Tab::container_height(cx))
3499            .max_w_full()
3500            .flex_none()
3501            .justify_between()
3502            .gap_2()
3503            .bg(cx.theme().colors().tab_bar_background)
3504            .border_b_1()
3505            .border_color(cx.theme().colors().border)
3506            .child(
3507                h_flex()
3508                    .size_full()
3509                    .gap(DynamicSpacing::Base04.rems(cx))
3510                    .pl(DynamicSpacing::Base04.rems(cx))
3511                    .child(match &self.active_view {
3512                        ActiveView::History { .. } | ActiveView::Configuration => {
3513                            self.render_toolbar_back_button(cx).into_any_element()
3514                        }
3515                        _ => selected_agent.into_any_element(),
3516                    })
3517                    .child(self.render_title_view(window, cx)),
3518            )
3519            .child(
3520                h_flex()
3521                    .flex_none()
3522                    .gap(DynamicSpacing::Base02.rems(cx))
3523                    .pl(DynamicSpacing::Base04.rems(cx))
3524                    .pr(DynamicSpacing::Base06.rems(cx))
3525                    .when(
3526                        has_v2_flag
3527                            && cx.has_flag::<AgentGitWorktreesFeatureFlag>()
3528                            && !self.active_thread_has_messages(cx),
3529                        |this| this.child(self.render_start_thread_in_selector(cx)),
3530                    )
3531                    .child(new_thread_menu)
3532                    .when(show_history_menu, |this| {
3533                        this.child(self.render_recent_entries_menu(
3534                            IconName::MenuAltTemp,
3535                            Corner::TopRight,
3536                            cx,
3537                        ))
3538                    })
3539                    .child(self.render_panel_options_menu(window, cx)),
3540            )
3541    }
3542
3543    fn render_worktree_creation_status(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
3544        let status = self.worktree_creation_status.as_ref()?;
3545        match status {
3546            WorktreeCreationStatus::Creating => Some(
3547                h_flex()
3548                    .w_full()
3549                    .px(DynamicSpacing::Base06.rems(cx))
3550                    .py(DynamicSpacing::Base02.rems(cx))
3551                    .gap_2()
3552                    .bg(cx.theme().colors().surface_background)
3553                    .border_b_1()
3554                    .border_color(cx.theme().colors().border)
3555                    .child(SpinnerLabel::new().size(LabelSize::Small))
3556                    .child(
3557                        Label::new("Creating worktree…")
3558                            .color(Color::Muted)
3559                            .size(LabelSize::Small),
3560                    )
3561                    .into_any_element(),
3562            ),
3563            WorktreeCreationStatus::Error(message) => Some(
3564                h_flex()
3565                    .w_full()
3566                    .px(DynamicSpacing::Base06.rems(cx))
3567                    .py(DynamicSpacing::Base02.rems(cx))
3568                    .gap_2()
3569                    .bg(cx.theme().colors().surface_background)
3570                    .border_b_1()
3571                    .border_color(cx.theme().colors().border)
3572                    .child(
3573                        Icon::new(IconName::Warning)
3574                            .size(IconSize::Small)
3575                            .color(Color::Warning),
3576                    )
3577                    .child(
3578                        Label::new(message.clone())
3579                            .color(Color::Warning)
3580                            .size(LabelSize::Small)
3581                            .truncate(),
3582                    )
3583                    .into_any_element(),
3584            ),
3585        }
3586    }
3587
3588    fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
3589        if TrialEndUpsell::dismissed() {
3590            return false;
3591        }
3592
3593        match &self.active_view {
3594            ActiveView::TextThread { .. } => {
3595                if LanguageModelRegistry::global(cx)
3596                    .read(cx)
3597                    .default_model()
3598                    .is_some_and(|model| {
3599                        model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
3600                    })
3601                {
3602                    return false;
3603                }
3604            }
3605            ActiveView::Uninitialized
3606            | ActiveView::AgentThread { .. }
3607            | ActiveView::History { .. }
3608            | ActiveView::Configuration => return false,
3609        }
3610
3611        let plan = self.user_store.read(cx).plan();
3612        let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
3613
3614        plan.is_some_and(|plan| plan == Plan::ZedFree) && has_previous_trial
3615    }
3616
3617    fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
3618        if self.on_boarding_upsell_dismissed.load(Ordering::Acquire) {
3619            return false;
3620        }
3621
3622        let user_store = self.user_store.read(cx);
3623
3624        if user_store.plan().is_some_and(|plan| plan == Plan::ZedPro)
3625            && user_store
3626                .subscription_period()
3627                .and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
3628                .is_some_and(|date| date < chrono::Utc::now())
3629        {
3630            OnboardingUpsell::set_dismissed(true, cx);
3631            self.on_boarding_upsell_dismissed
3632                .store(true, Ordering::Release);
3633            return false;
3634        }
3635
3636        match &self.active_view {
3637            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
3638                false
3639            }
3640            ActiveView::AgentThread { server_view, .. }
3641                if server_view.read(cx).as_native_thread(cx).is_none() =>
3642            {
3643                false
3644            }
3645            _ => {
3646                let history_is_empty = self.acp_history.read(cx).is_empty();
3647
3648                let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
3649                    .visible_providers()
3650                    .iter()
3651                    .any(|provider| {
3652                        provider.is_authenticated(cx)
3653                            && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
3654                    });
3655
3656                history_is_empty || !has_configured_non_zed_providers
3657            }
3658        }
3659    }
3660
3661    fn render_onboarding(
3662        &self,
3663        _window: &mut Window,
3664        cx: &mut Context<Self>,
3665    ) -> Option<impl IntoElement> {
3666        if !self.should_render_onboarding(cx) {
3667            return None;
3668        }
3669
3670        let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
3671
3672        Some(
3673            div()
3674                .when(text_thread_view, |this| {
3675                    this.bg(cx.theme().colors().editor_background)
3676                })
3677                .child(self.onboarding.clone()),
3678        )
3679    }
3680
3681    fn render_trial_end_upsell(
3682        &self,
3683        _window: &mut Window,
3684        cx: &mut Context<Self>,
3685    ) -> Option<impl IntoElement> {
3686        if !self.should_render_trial_end_upsell(cx) {
3687            return None;
3688        }
3689
3690        Some(
3691            v_flex()
3692                .absolute()
3693                .inset_0()
3694                .size_full()
3695                .bg(cx.theme().colors().panel_background)
3696                .opacity(0.85)
3697                .block_mouse_except_scroll()
3698                .child(EndTrialUpsell::new(Arc::new({
3699                    let this = cx.entity();
3700                    move |_, cx| {
3701                        this.update(cx, |_this, cx| {
3702                            TrialEndUpsell::set_dismissed(true, cx);
3703                            cx.notify();
3704                        });
3705                    }
3706                }))),
3707        )
3708    }
3709
3710    fn emit_configuration_error_telemetry_if_needed(
3711        &mut self,
3712        configuration_error: Option<&ConfigurationError>,
3713    ) {
3714        let error_kind = configuration_error.map(|err| match err {
3715            ConfigurationError::NoProvider => "no_provider",
3716            ConfigurationError::ModelNotFound => "model_not_found",
3717            ConfigurationError::ProviderNotAuthenticated(_) => "provider_not_authenticated",
3718        });
3719
3720        let error_kind_string = error_kind.map(String::from);
3721
3722        if self.last_configuration_error_telemetry == error_kind_string {
3723            return;
3724        }
3725
3726        self.last_configuration_error_telemetry = error_kind_string;
3727
3728        if let Some(kind) = error_kind {
3729            let message = configuration_error
3730                .map(|err| err.to_string())
3731                .unwrap_or_default();
3732
3733            telemetry::event!("Agent Panel Error Shown", kind = kind, message = message,);
3734        }
3735    }
3736
3737    fn render_configuration_error(
3738        &self,
3739        border_bottom: bool,
3740        configuration_error: &ConfigurationError,
3741        focus_handle: &FocusHandle,
3742        cx: &mut App,
3743    ) -> impl IntoElement {
3744        let zed_provider_configured = AgentSettings::get_global(cx)
3745            .default_model
3746            .as_ref()
3747            .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev");
3748
3749        let callout = if zed_provider_configured {
3750            Callout::new()
3751                .icon(IconName::Warning)
3752                .severity(Severity::Warning)
3753                .when(border_bottom, |this| {
3754                    this.border_position(ui::BorderPosition::Bottom)
3755                })
3756                .title("Sign in to continue using Zed as your LLM provider.")
3757                .actions_slot(
3758                    Button::new("sign_in", "Sign In")
3759                        .style(ButtonStyle::Tinted(ui::TintColor::Warning))
3760                        .label_size(LabelSize::Small)
3761                        .on_click({
3762                            let workspace = self.workspace.clone();
3763                            move |_, _, cx| {
3764                                let Ok(client) =
3765                                    workspace.update(cx, |workspace, _| workspace.client().clone())
3766                                else {
3767                                    return;
3768                                };
3769
3770                                cx.spawn(async move |cx| {
3771                                    client.sign_in_with_optional_connect(true, cx).await
3772                                })
3773                                .detach_and_log_err(cx);
3774                            }
3775                        }),
3776                )
3777        } else {
3778            Callout::new()
3779                .icon(IconName::Warning)
3780                .severity(Severity::Warning)
3781                .when(border_bottom, |this| {
3782                    this.border_position(ui::BorderPosition::Bottom)
3783                })
3784                .title(configuration_error.to_string())
3785                .actions_slot(
3786                    Button::new("settings", "Configure")
3787                        .style(ButtonStyle::Tinted(ui::TintColor::Warning))
3788                        .label_size(LabelSize::Small)
3789                        .key_binding(
3790                            KeyBinding::for_action_in(&OpenSettings, focus_handle, cx)
3791                                .map(|kb| kb.size(rems_from_px(12.))),
3792                        )
3793                        .on_click(|_event, window, cx| {
3794                            window.dispatch_action(OpenSettings.boxed_clone(), cx)
3795                        }),
3796                )
3797        };
3798
3799        match configuration_error {
3800            ConfigurationError::ModelNotFound
3801            | ConfigurationError::ProviderNotAuthenticated(_)
3802            | ConfigurationError::NoProvider => callout.into_any_element(),
3803        }
3804    }
3805
3806    fn render_text_thread(
3807        &self,
3808        text_thread_editor: &Entity<TextThreadEditor>,
3809        buffer_search_bar: &Entity<BufferSearchBar>,
3810        window: &mut Window,
3811        cx: &mut Context<Self>,
3812    ) -> Div {
3813        let mut registrar = buffer_search::DivRegistrar::new(
3814            |this, _, _cx| match &this.active_view {
3815                ActiveView::TextThread {
3816                    buffer_search_bar, ..
3817                } => Some(buffer_search_bar.clone()),
3818                _ => None,
3819            },
3820            cx,
3821        );
3822        BufferSearchBar::register(&mut registrar);
3823        registrar
3824            .into_div()
3825            .size_full()
3826            .relative()
3827            .map(|parent| {
3828                buffer_search_bar.update(cx, |buffer_search_bar, cx| {
3829                    if buffer_search_bar.is_dismissed() {
3830                        return parent;
3831                    }
3832                    parent.child(
3833                        div()
3834                            .p(DynamicSpacing::Base08.rems(cx))
3835                            .border_b_1()
3836                            .border_color(cx.theme().colors().border_variant)
3837                            .bg(cx.theme().colors().editor_background)
3838                            .child(buffer_search_bar.render(window, cx)),
3839                    )
3840                })
3841            })
3842            .child(text_thread_editor.clone())
3843            .child(self.render_drag_target(cx))
3844    }
3845
3846    fn render_drag_target(&self, cx: &Context<Self>) -> Div {
3847        let is_local = self.project.read(cx).is_local();
3848        div()
3849            .invisible()
3850            .absolute()
3851            .top_0()
3852            .right_0()
3853            .bottom_0()
3854            .left_0()
3855            .bg(cx.theme().colors().drop_target_background)
3856            .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
3857            .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
3858            .when(is_local, |this| {
3859                this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
3860            })
3861            .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
3862                let item = tab.pane.read(cx).item_for_index(tab.ix);
3863                let project_paths = item
3864                    .and_then(|item| item.project_path(cx))
3865                    .into_iter()
3866                    .collect::<Vec<_>>();
3867                this.handle_drop(project_paths, vec![], window, cx);
3868            }))
3869            .on_drop(
3870                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
3871                    let project_paths = selection
3872                        .items()
3873                        .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
3874                        .collect::<Vec<_>>();
3875                    this.handle_drop(project_paths, vec![], window, cx);
3876                }),
3877            )
3878            .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
3879                let tasks = paths
3880                    .paths()
3881                    .iter()
3882                    .map(|path| {
3883                        Workspace::project_path_for_path(this.project.clone(), path, false, cx)
3884                    })
3885                    .collect::<Vec<_>>();
3886                cx.spawn_in(window, async move |this, cx| {
3887                    let mut paths = vec![];
3888                    let mut added_worktrees = vec![];
3889                    let opened_paths = futures::future::join_all(tasks).await;
3890                    for entry in opened_paths {
3891                        if let Some((worktree, project_path)) = entry.log_err() {
3892                            added_worktrees.push(worktree);
3893                            paths.push(project_path);
3894                        }
3895                    }
3896                    this.update_in(cx, |this, window, cx| {
3897                        this.handle_drop(paths, added_worktrees, window, cx);
3898                    })
3899                    .ok();
3900                })
3901                .detach();
3902            }))
3903    }
3904
3905    fn handle_drop(
3906        &mut self,
3907        paths: Vec<ProjectPath>,
3908        added_worktrees: Vec<Entity<Worktree>>,
3909        window: &mut Window,
3910        cx: &mut Context<Self>,
3911    ) {
3912        match &self.active_view {
3913            ActiveView::AgentThread { server_view } => {
3914                server_view.update(cx, |thread_view, cx| {
3915                    thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
3916                });
3917            }
3918            ActiveView::TextThread {
3919                text_thread_editor, ..
3920            } => {
3921                text_thread_editor.update(cx, |text_thread_editor, cx| {
3922                    TextThreadEditor::insert_dragged_files(
3923                        text_thread_editor,
3924                        paths,
3925                        added_worktrees,
3926                        window,
3927                        cx,
3928                    );
3929                });
3930            }
3931            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
3932        }
3933    }
3934
3935    fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
3936        if !self.show_trust_workspace_message {
3937            return None;
3938        }
3939
3940        let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
3941
3942        Some(
3943            Callout::new()
3944                .icon(IconName::Warning)
3945                .severity(Severity::Warning)
3946                .border_position(ui::BorderPosition::Bottom)
3947                .title("You're in Restricted Mode")
3948                .description(description)
3949                .actions_slot(
3950                    Button::new("open-trust-modal", "Configure Project Trust")
3951                        .label_size(LabelSize::Small)
3952                        .style(ButtonStyle::Outlined)
3953                        .on_click({
3954                            cx.listener(move |this, _, window, cx| {
3955                                this.workspace
3956                                    .update(cx, |workspace, cx| {
3957                                        workspace
3958                                            .show_worktree_trust_security_modal(true, window, cx)
3959                                    })
3960                                    .log_err();
3961                            })
3962                        }),
3963                ),
3964        )
3965    }
3966
3967    fn key_context(&self) -> KeyContext {
3968        let mut key_context = KeyContext::new_with_defaults();
3969        key_context.add("AgentPanel");
3970        match &self.active_view {
3971            ActiveView::AgentThread { .. } => key_context.add("acp_thread"),
3972            ActiveView::TextThread { .. } => key_context.add("text_thread"),
3973            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
3974        }
3975        key_context
3976    }
3977}
3978
3979impl Render for AgentPanel {
3980    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3981        // WARNING: Changes to this element hierarchy can have
3982        // non-obvious implications to the layout of children.
3983        //
3984        // If you need to change it, please confirm:
3985        // - The message editor expands (cmd-option-esc) correctly
3986        // - When expanded, the buttons at the bottom of the panel are displayed correctly
3987        // - Font size works as expected and can be changed with cmd-+/cmd-
3988        // - Scrolling in all views works as expected
3989        // - Files can be dropped into the panel
3990        let content = v_flex()
3991            .relative()
3992            .size_full()
3993            .justify_between()
3994            .key_context(self.key_context())
3995            .on_action(cx.listener(|this, action: &NewThread, window, cx| {
3996                this.new_thread(action, window, cx);
3997            }))
3998            .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
3999                this.open_history(window, cx);
4000            }))
4001            .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
4002                this.open_configuration(window, cx);
4003            }))
4004            .on_action(cx.listener(Self::open_active_thread_as_markdown))
4005            .on_action(cx.listener(Self::deploy_rules_library))
4006            .on_action(cx.listener(Self::go_back))
4007            .on_action(cx.listener(Self::toggle_navigation_menu))
4008            .on_action(cx.listener(Self::toggle_options_menu))
4009            .on_action(cx.listener(Self::increase_font_size))
4010            .on_action(cx.listener(Self::decrease_font_size))
4011            .on_action(cx.listener(Self::reset_font_size))
4012            .on_action(cx.listener(Self::toggle_zoom))
4013            .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
4014                if let Some(thread_view) = this.active_thread_view() {
4015                    thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
4016                }
4017            }))
4018            .child(self.render_toolbar(window, cx))
4019            .children(self.render_worktree_creation_status(cx))
4020            .children(self.render_workspace_trust_message(cx))
4021            .children(self.render_onboarding(window, cx))
4022            .map(|parent| {
4023                // Emit configuration error telemetry before entering the match to avoid borrow conflicts
4024                if matches!(&self.active_view, ActiveView::TextThread { .. }) {
4025                    let model_registry = LanguageModelRegistry::read_global(cx);
4026                    let configuration_error =
4027                        model_registry.configuration_error(model_registry.default_model(), cx);
4028                    self.emit_configuration_error_telemetry_if_needed(configuration_error.as_ref());
4029                }
4030
4031                match &self.active_view {
4032                    ActiveView::Uninitialized => parent,
4033                    ActiveView::AgentThread { server_view, .. } => parent
4034                        .child(server_view.clone())
4035                        .child(self.render_drag_target(cx)),
4036                    ActiveView::History { kind } => match kind {
4037                        HistoryKind::AgentThreads => parent.child(self.acp_history.clone()),
4038                        HistoryKind::TextThreads => parent.child(self.text_thread_history.clone()),
4039                    },
4040                    ActiveView::TextThread {
4041                        text_thread_editor,
4042                        buffer_search_bar,
4043                        ..
4044                    } => {
4045                        let model_registry = LanguageModelRegistry::read_global(cx);
4046                        let configuration_error =
4047                            model_registry.configuration_error(model_registry.default_model(), cx);
4048
4049                        parent
4050                            .map(|this| {
4051                                if !self.should_render_onboarding(cx)
4052                                    && let Some(err) = configuration_error.as_ref()
4053                                {
4054                                    this.child(self.render_configuration_error(
4055                                        true,
4056                                        err,
4057                                        &self.focus_handle(cx),
4058                                        cx,
4059                                    ))
4060                                } else {
4061                                    this
4062                                }
4063                            })
4064                            .child(self.render_text_thread(
4065                                text_thread_editor,
4066                                buffer_search_bar,
4067                                window,
4068                                cx,
4069                            ))
4070                    }
4071                    ActiveView::Configuration => parent.children(self.configuration.clone()),
4072                }
4073            })
4074            .children(self.render_trial_end_upsell(window, cx));
4075
4076        match self.active_view.which_font_size_used() {
4077            WhichFontSize::AgentFont => {
4078                WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
4079                    .size_full()
4080                    .child(content)
4081                    .into_any()
4082            }
4083            _ => content.into_any(),
4084        }
4085    }
4086}
4087
4088struct PromptLibraryInlineAssist {
4089    workspace: WeakEntity<Workspace>,
4090}
4091
4092impl PromptLibraryInlineAssist {
4093    pub fn new(workspace: WeakEntity<Workspace>) -> Self {
4094        Self { workspace }
4095    }
4096}
4097
4098impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
4099    fn assist(
4100        &self,
4101        prompt_editor: &Entity<Editor>,
4102        initial_prompt: Option<String>,
4103        window: &mut Window,
4104        cx: &mut Context<RulesLibrary>,
4105    ) {
4106        InlineAssistant::update_global(cx, |assistant, cx| {
4107            let Some(workspace) = self.workspace.upgrade() else {
4108                return;
4109            };
4110            let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
4111                return;
4112            };
4113            let project = workspace.read(cx).project().downgrade();
4114            let panel = panel.read(cx);
4115            let thread_store = panel.thread_store().clone();
4116            let history = panel.history().downgrade();
4117            assistant.assist(
4118                prompt_editor,
4119                self.workspace.clone(),
4120                project,
4121                thread_store,
4122                None,
4123                history,
4124                initial_prompt,
4125                window,
4126                cx,
4127            );
4128        })
4129    }
4130
4131    fn focus_agent_panel(
4132        &self,
4133        workspace: &mut Workspace,
4134        window: &mut Window,
4135        cx: &mut Context<Workspace>,
4136    ) -> bool {
4137        workspace.focus_panel::<AgentPanel>(window, cx).is_some()
4138    }
4139}
4140
4141pub struct ConcreteAssistantPanelDelegate;
4142
4143impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
4144    fn active_text_thread_editor(
4145        &self,
4146        workspace: &mut Workspace,
4147        _window: &mut Window,
4148        cx: &mut Context<Workspace>,
4149    ) -> Option<Entity<TextThreadEditor>> {
4150        let panel = workspace.panel::<AgentPanel>(cx)?;
4151        panel.read(cx).active_text_thread_editor()
4152    }
4153
4154    fn open_local_text_thread(
4155        &self,
4156        workspace: &mut Workspace,
4157        path: Arc<Path>,
4158        window: &mut Window,
4159        cx: &mut Context<Workspace>,
4160    ) -> Task<Result<()>> {
4161        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
4162            return Task::ready(Err(anyhow!("Agent panel not found")));
4163        };
4164
4165        panel.update(cx, |panel, cx| {
4166            panel.open_saved_text_thread(path, window, cx)
4167        })
4168    }
4169
4170    fn open_remote_text_thread(
4171        &self,
4172        _workspace: &mut Workspace,
4173        _text_thread_id: assistant_text_thread::TextThreadId,
4174        _window: &mut Window,
4175        _cx: &mut Context<Workspace>,
4176    ) -> Task<Result<Entity<TextThreadEditor>>> {
4177        Task::ready(Err(anyhow!("opening remote context not implemented")))
4178    }
4179
4180    fn quote_selection(
4181        &self,
4182        workspace: &mut Workspace,
4183        selection_ranges: Vec<Range<Anchor>>,
4184        buffer: Entity<MultiBuffer>,
4185        window: &mut Window,
4186        cx: &mut Context<Workspace>,
4187    ) {
4188        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
4189            return;
4190        };
4191
4192        if !panel.focus_handle(cx).contains_focused(window, cx) {
4193            workspace.toggle_panel_focus::<AgentPanel>(window, cx);
4194        }
4195
4196        panel.update(cx, |_, cx| {
4197            // Wait to create a new context until the workspace is no longer
4198            // being updated.
4199            cx.defer_in(window, move |panel, window, cx| {
4200                if let Some(thread_view) = panel.active_thread_view() {
4201                    thread_view.update(cx, |thread_view, cx| {
4202                        thread_view.insert_selections(window, cx);
4203                    });
4204                } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
4205                    let snapshot = buffer.read(cx).snapshot(cx);
4206                    let selection_ranges = selection_ranges
4207                        .into_iter()
4208                        .map(|range| range.to_point(&snapshot))
4209                        .collect::<Vec<_>>();
4210
4211                    text_thread_editor.update(cx, |text_thread_editor, cx| {
4212                        text_thread_editor.quote_ranges(selection_ranges, snapshot, window, cx)
4213                    });
4214                }
4215            });
4216        });
4217    }
4218
4219    fn quote_terminal_text(
4220        &self,
4221        workspace: &mut Workspace,
4222        text: String,
4223        window: &mut Window,
4224        cx: &mut Context<Workspace>,
4225    ) {
4226        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
4227            return;
4228        };
4229
4230        if !panel.focus_handle(cx).contains_focused(window, cx) {
4231            workspace.toggle_panel_focus::<AgentPanel>(window, cx);
4232        }
4233
4234        panel.update(cx, |_, cx| {
4235            // Wait to create a new context until the workspace is no longer
4236            // being updated.
4237            cx.defer_in(window, move |panel, window, cx| {
4238                if let Some(thread_view) = panel.active_thread_view() {
4239                    thread_view.update(cx, |thread_view, cx| {
4240                        thread_view.insert_terminal_text(text, window, cx);
4241                    });
4242                } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
4243                    text_thread_editor.update(cx, |text_thread_editor, cx| {
4244                        text_thread_editor.quote_terminal_text(text, window, cx)
4245                    });
4246                }
4247            });
4248        });
4249    }
4250}
4251
4252struct OnboardingUpsell;
4253
4254impl Dismissable for OnboardingUpsell {
4255    const KEY: &'static str = "dismissed-trial-upsell";
4256}
4257
4258struct TrialEndUpsell;
4259
4260impl Dismissable for TrialEndUpsell {
4261    const KEY: &'static str = "dismissed-trial-end-upsell";
4262}
4263
4264/// Test-only helper methods
4265#[cfg(any(test, feature = "test-support"))]
4266impl AgentPanel {
4267    /// Opens an external thread using an arbitrary AgentServer.
4268    ///
4269    /// This is a test-only helper that allows visual tests and integration tests
4270    /// to inject a stub server without modifying production code paths.
4271    /// Not compiled into production builds.
4272    pub fn open_external_thread_with_server(
4273        &mut self,
4274        server: Rc<dyn AgentServer>,
4275        window: &mut Window,
4276        cx: &mut Context<Self>,
4277    ) {
4278        let workspace = self.workspace.clone();
4279        let project = self.project.clone();
4280
4281        let ext_agent = ExternalAgent::Custom {
4282            name: server.name(),
4283        };
4284
4285        self.create_external_thread(
4286            server, None, None, workspace, project, ext_agent, window, cx,
4287        );
4288    }
4289
4290    /// Returns the currently active thread view, if any.
4291    ///
4292    /// This is a test-only accessor that exposes the private `active_thread_view()`
4293    /// method for test assertions. Not compiled into production builds.
4294    pub fn active_thread_view_for_tests(&self) -> Option<&Entity<ConnectionView>> {
4295        self.active_thread_view()
4296    }
4297
4298    /// Sets the start_thread_in value directly, bypassing validation.
4299    ///
4300    /// This is a test-only helper for visual tests that need to show specific
4301    /// start_thread_in states without requiring a real git repository.
4302    pub fn set_start_thread_in_for_tests(&mut self, target: StartThreadIn, cx: &mut Context<Self>) {
4303        self.start_thread_in = target;
4304        cx.notify();
4305    }
4306
4307    /// Returns the current worktree creation status.
4308    ///
4309    /// This is a test-only helper for visual tests.
4310    pub fn worktree_creation_status_for_tests(&self) -> Option<&WorktreeCreationStatus> {
4311        self.worktree_creation_status.as_ref()
4312    }
4313
4314    /// Sets the worktree creation status directly.
4315    ///
4316    /// This is a test-only helper for visual tests that need to show the
4317    /// "Creating worktree…" spinner or error banners.
4318    pub fn set_worktree_creation_status_for_tests(
4319        &mut self,
4320        status: Option<WorktreeCreationStatus>,
4321        cx: &mut Context<Self>,
4322    ) {
4323        self.worktree_creation_status = status;
4324        cx.notify();
4325    }
4326
4327    /// Opens the history view.
4328    ///
4329    /// This is a test-only helper that exposes the private `open_history()`
4330    /// method for visual tests.
4331    pub fn open_history_for_tests(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4332        self.open_history(window, cx);
4333    }
4334
4335    /// Opens the start_thread_in selector popover menu.
4336    ///
4337    /// This is a test-only helper for visual tests.
4338    pub fn open_start_thread_in_menu_for_tests(
4339        &mut self,
4340        window: &mut Window,
4341        cx: &mut Context<Self>,
4342    ) {
4343        self.start_thread_in_menu_handle.show(window, cx);
4344    }
4345
4346    /// Dismisses the start_thread_in dropdown menu.
4347    ///
4348    /// This is a test-only helper for visual tests.
4349    pub fn close_start_thread_in_menu_for_tests(&mut self, cx: &mut Context<Self>) {
4350        self.start_thread_in_menu_handle.hide(cx);
4351    }
4352}
4353
4354#[cfg(test)]
4355mod tests {
4356    use super::*;
4357    use crate::connection_view::tests::{StubAgentServer, init_test};
4358    use assistant_text_thread::TextThreadStore;
4359    use feature_flags::FeatureFlagAppExt;
4360    use fs::FakeFs;
4361    use gpui::{TestAppContext, VisualTestContext};
4362    use project::Project;
4363    use serde_json::json;
4364    use workspace::MultiWorkspace;
4365
4366    #[gpui::test]
4367    async fn test_active_thread_serialize_and_load_round_trip(cx: &mut TestAppContext) {
4368        init_test(cx);
4369        cx.update(|cx| {
4370            cx.update_flags(true, vec!["agent-v2".to_string()]);
4371            agent::ThreadStore::init_global(cx);
4372            language_model::LanguageModelRegistry::test(cx);
4373        });
4374
4375        // --- Create a MultiWorkspace window with two workspaces ---
4376        let fs = FakeFs::new(cx.executor());
4377        let project_a = Project::test(fs.clone(), [], cx).await;
4378        let project_b = Project::test(fs, [], cx).await;
4379
4380        let multi_workspace =
4381            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
4382
4383        let workspace_a = multi_workspace
4384            .read_with(cx, |multi_workspace, _cx| {
4385                multi_workspace.workspace().clone()
4386            })
4387            .unwrap();
4388
4389        let workspace_b = multi_workspace
4390            .update(cx, |multi_workspace, window, cx| {
4391                multi_workspace.test_add_workspace(project_b.clone(), window, cx)
4392            })
4393            .unwrap();
4394
4395        workspace_a.update(cx, |workspace, _cx| {
4396            workspace.set_random_database_id();
4397        });
4398        workspace_b.update(cx, |workspace, _cx| {
4399            workspace.set_random_database_id();
4400        });
4401
4402        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4403
4404        // --- Set up workspace A: width=300, with an active thread ---
4405        let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
4406            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_a.clone(), cx));
4407            cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
4408        });
4409
4410        panel_a.update(cx, |panel, _cx| {
4411            panel.width = Some(px(300.0));
4412        });
4413
4414        panel_a.update_in(cx, |panel, window, cx| {
4415            panel.open_external_thread_with_server(
4416                Rc::new(StubAgentServer::default_response()),
4417                window,
4418                cx,
4419            );
4420        });
4421
4422        cx.run_until_parked();
4423
4424        panel_a.read_with(cx, |panel, cx| {
4425            assert!(
4426                panel.active_agent_thread(cx).is_some(),
4427                "workspace A should have an active thread after connection"
4428            );
4429        });
4430
4431        let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent.clone());
4432
4433        // --- Set up workspace B: ClaudeCode, width=400, no active thread ---
4434        let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
4435            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_b.clone(), cx));
4436            cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
4437        });
4438
4439        panel_b.update(cx, |panel, _cx| {
4440            panel.width = Some(px(400.0));
4441            panel.selected_agent = AgentType::Custom {
4442                name: "claude-acp".into(),
4443            };
4444        });
4445
4446        // --- Serialize both panels ---
4447        panel_a.update(cx, |panel, cx| panel.serialize(cx));
4448        panel_b.update(cx, |panel, cx| panel.serialize(cx));
4449        cx.run_until_parked();
4450
4451        // --- Load fresh panels for each workspace and verify independent state ---
4452        let prompt_builder = Arc::new(prompt_store::PromptBuilder::new(None).unwrap());
4453
4454        let async_cx = cx.update(|window, cx| window.to_async(cx));
4455        let loaded_a = AgentPanel::load(workspace_a.downgrade(), prompt_builder.clone(), async_cx)
4456            .await
4457            .expect("panel A load should succeed");
4458        cx.run_until_parked();
4459
4460        let async_cx = cx.update(|window, cx| window.to_async(cx));
4461        let loaded_b = AgentPanel::load(workspace_b.downgrade(), prompt_builder.clone(), async_cx)
4462            .await
4463            .expect("panel B load should succeed");
4464        cx.run_until_parked();
4465
4466        // Workspace A should restore its thread, width, and agent type
4467        loaded_a.read_with(cx, |panel, _cx| {
4468            assert_eq!(
4469                panel.width,
4470                Some(px(300.0)),
4471                "workspace A width should be restored"
4472            );
4473            assert_eq!(
4474                panel.selected_agent, agent_type_a,
4475                "workspace A agent type should be restored"
4476            );
4477            assert!(
4478                panel.active_thread_view().is_some(),
4479                "workspace A should have its active thread restored"
4480            );
4481        });
4482
4483        // Workspace B should restore its own width and agent type, with no thread
4484        loaded_b.read_with(cx, |panel, _cx| {
4485            assert_eq!(
4486                panel.width,
4487                Some(px(400.0)),
4488                "workspace B width should be restored"
4489            );
4490            assert_eq!(
4491                panel.selected_agent,
4492                AgentType::Custom {
4493                    name: "claude-acp".into()
4494                },
4495                "workspace B agent type should be restored"
4496            );
4497            assert!(
4498                panel.active_thread_view().is_none(),
4499                "workspace B should have no active thread"
4500            );
4501        });
4502    }
4503
4504    // Simple regression test
4505    #[gpui::test]
4506    async fn test_new_text_thread_action_handler(cx: &mut TestAppContext) {
4507        init_test(cx);
4508
4509        let fs = FakeFs::new(cx.executor());
4510
4511        cx.update(|cx| {
4512            cx.update_flags(true, vec!["agent-v2".to_string()]);
4513            agent::ThreadStore::init_global(cx);
4514            language_model::LanguageModelRegistry::test(cx);
4515            let slash_command_registry =
4516                assistant_slash_command::SlashCommandRegistry::default_global(cx);
4517            slash_command_registry
4518                .register_command(assistant_slash_commands::DefaultSlashCommand, false);
4519            <dyn fs::Fs>::set_global(fs.clone(), cx);
4520        });
4521
4522        let project = Project::test(fs.clone(), [], cx).await;
4523
4524        let multi_workspace =
4525            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4526
4527        let workspace_a = multi_workspace
4528            .read_with(cx, |multi_workspace, _cx| {
4529                multi_workspace.workspace().clone()
4530            })
4531            .unwrap();
4532
4533        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4534
4535        workspace_a.update_in(cx, |workspace, window, cx| {
4536            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
4537            let panel =
4538                cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
4539            workspace.add_panel(panel, window, cx);
4540        });
4541
4542        cx.run_until_parked();
4543
4544        workspace_a.update_in(cx, |_, window, cx| {
4545            window.dispatch_action(NewTextThread.boxed_clone(), cx);
4546        });
4547
4548        cx.run_until_parked();
4549    }
4550
4551    #[gpui::test]
4552    async fn test_thread_target_local_project(cx: &mut TestAppContext) {
4553        init_test(cx);
4554        cx.update(|cx| {
4555            cx.update_flags(true, vec!["agent-v2".to_string()]);
4556            agent::ThreadStore::init_global(cx);
4557            language_model::LanguageModelRegistry::test(cx);
4558        });
4559
4560        let fs = FakeFs::new(cx.executor());
4561        fs.insert_tree(
4562            "/project",
4563            json!({
4564                ".git": {},
4565                "src": {
4566                    "main.rs": "fn main() {}"
4567                }
4568            }),
4569        )
4570        .await;
4571        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
4572
4573        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
4574
4575        let multi_workspace =
4576            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4577
4578        let workspace = multi_workspace
4579            .read_with(cx, |multi_workspace, _cx| {
4580                multi_workspace.workspace().clone()
4581            })
4582            .unwrap();
4583
4584        workspace.update(cx, |workspace, _cx| {
4585            workspace.set_random_database_id();
4586        });
4587
4588        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4589
4590        // Wait for the project to discover the git repository.
4591        cx.run_until_parked();
4592
4593        let panel = workspace.update_in(cx, |workspace, window, cx| {
4594            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
4595            let panel =
4596                cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
4597            workspace.add_panel(panel.clone(), window, cx);
4598            panel
4599        });
4600
4601        cx.run_until_parked();
4602
4603        // Default thread target should be LocalProject.
4604        panel.read_with(cx, |panel, _cx| {
4605            assert_eq!(
4606                *panel.start_thread_in(),
4607                StartThreadIn::LocalProject,
4608                "default thread target should be LocalProject"
4609            );
4610        });
4611
4612        // Start a new thread with the default LocalProject target.
4613        // Use StubAgentServer so the thread connects immediately in tests.
4614        panel.update_in(cx, |panel, window, cx| {
4615            panel.open_external_thread_with_server(
4616                Rc::new(StubAgentServer::default_response()),
4617                window,
4618                cx,
4619            );
4620        });
4621
4622        cx.run_until_parked();
4623
4624        // MultiWorkspace should still have exactly one workspace (no worktree created).
4625        multi_workspace
4626            .read_with(cx, |multi_workspace, _cx| {
4627                assert_eq!(
4628                    multi_workspace.workspaces().len(),
4629                    1,
4630                    "LocalProject should not create a new workspace"
4631                );
4632            })
4633            .unwrap();
4634
4635        // The thread should be active in the panel.
4636        panel.read_with(cx, |panel, cx| {
4637            assert!(
4638                panel.active_agent_thread(cx).is_some(),
4639                "a thread should be running in the current workspace"
4640            );
4641        });
4642
4643        // The thread target should still be LocalProject (unchanged).
4644        panel.read_with(cx, |panel, _cx| {
4645            assert_eq!(
4646                *panel.start_thread_in(),
4647                StartThreadIn::LocalProject,
4648                "thread target should remain LocalProject"
4649            );
4650        });
4651
4652        // No worktree creation status should be set.
4653        panel.read_with(cx, |panel, _cx| {
4654            assert!(
4655                panel.worktree_creation_status.is_none(),
4656                "no worktree creation should have occurred"
4657            );
4658        });
4659    }
4660
4661    #[gpui::test]
4662    async fn test_thread_target_serialization_round_trip(cx: &mut TestAppContext) {
4663        init_test(cx);
4664        cx.update(|cx| {
4665            cx.update_flags(
4666                true,
4667                vec!["agent-v2".to_string(), "agent-git-worktrees".to_string()],
4668            );
4669            agent::ThreadStore::init_global(cx);
4670            language_model::LanguageModelRegistry::test(cx);
4671        });
4672
4673        let fs = FakeFs::new(cx.executor());
4674        fs.insert_tree(
4675            "/project",
4676            json!({
4677                ".git": {},
4678                "src": {
4679                    "main.rs": "fn main() {}"
4680                }
4681            }),
4682        )
4683        .await;
4684        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
4685
4686        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
4687
4688        let multi_workspace =
4689            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4690
4691        let workspace = multi_workspace
4692            .read_with(cx, |multi_workspace, _cx| {
4693                multi_workspace.workspace().clone()
4694            })
4695            .unwrap();
4696
4697        workspace.update(cx, |workspace, _cx| {
4698            workspace.set_random_database_id();
4699        });
4700
4701        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4702
4703        // Wait for the project to discover the git repository.
4704        cx.run_until_parked();
4705
4706        let panel = workspace.update_in(cx, |workspace, window, cx| {
4707            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
4708            let panel =
4709                cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
4710            workspace.add_panel(panel.clone(), window, cx);
4711            panel
4712        });
4713
4714        cx.run_until_parked();
4715
4716        // Default should be LocalProject.
4717        panel.read_with(cx, |panel, _cx| {
4718            assert_eq!(*panel.start_thread_in(), StartThreadIn::LocalProject);
4719        });
4720
4721        // Change thread target to NewWorktree.
4722        panel.update(cx, |panel, cx| {
4723            panel.set_start_thread_in(&StartThreadIn::NewWorktree, cx);
4724        });
4725
4726        panel.read_with(cx, |panel, _cx| {
4727            assert_eq!(
4728                *panel.start_thread_in(),
4729                StartThreadIn::NewWorktree,
4730                "thread target should be NewWorktree after set_thread_target"
4731            );
4732        });
4733
4734        // Let serialization complete.
4735        cx.run_until_parked();
4736
4737        // Load a fresh panel from the serialized data.
4738        let prompt_builder = Arc::new(prompt_store::PromptBuilder::new(None).unwrap());
4739        let async_cx = cx.update(|window, cx| window.to_async(cx));
4740        let loaded_panel =
4741            AgentPanel::load(workspace.downgrade(), prompt_builder.clone(), async_cx)
4742                .await
4743                .expect("panel load should succeed");
4744        cx.run_until_parked();
4745
4746        loaded_panel.read_with(cx, |panel, _cx| {
4747            assert_eq!(
4748                *panel.start_thread_in(),
4749                StartThreadIn::NewWorktree,
4750                "thread target should survive serialization round-trip"
4751            );
4752        });
4753    }
4754
4755    #[gpui::test]
4756    async fn test_thread_target_deserialization_falls_back_when_worktree_flag_disabled(
4757        cx: &mut TestAppContext,
4758    ) {
4759        init_test(cx);
4760        cx.update(|cx| {
4761            cx.update_flags(
4762                true,
4763                vec!["agent-v2".to_string(), "agent-git-worktrees".to_string()],
4764            );
4765            agent::ThreadStore::init_global(cx);
4766            language_model::LanguageModelRegistry::test(cx);
4767        });
4768
4769        let fs = FakeFs::new(cx.executor());
4770        fs.insert_tree(
4771            "/project",
4772            json!({
4773                ".git": {},
4774                "src": {
4775                    "main.rs": "fn main() {}"
4776                }
4777            }),
4778        )
4779        .await;
4780        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
4781
4782        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
4783
4784        let multi_workspace =
4785            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4786
4787        let workspace = multi_workspace
4788            .read_with(cx, |multi_workspace, _cx| {
4789                multi_workspace.workspace().clone()
4790            })
4791            .unwrap();
4792
4793        workspace.update(cx, |workspace, _cx| {
4794            workspace.set_random_database_id();
4795        });
4796
4797        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4798
4799        // Wait for the project to discover the git repository.
4800        cx.run_until_parked();
4801
4802        let panel = workspace.update_in(cx, |workspace, window, cx| {
4803            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
4804            let panel =
4805                cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
4806            workspace.add_panel(panel.clone(), window, cx);
4807            panel
4808        });
4809
4810        cx.run_until_parked();
4811
4812        panel.update(cx, |panel, cx| {
4813            panel.set_start_thread_in(&StartThreadIn::NewWorktree, cx);
4814        });
4815
4816        panel.read_with(cx, |panel, _cx| {
4817            assert_eq!(
4818                *panel.start_thread_in(),
4819                StartThreadIn::NewWorktree,
4820                "thread target should be NewWorktree before reload"
4821            );
4822        });
4823
4824        // Let serialization complete.
4825        cx.run_until_parked();
4826
4827        // Disable worktree flag and reload panel from serialized data.
4828        cx.update(|_, cx| {
4829            cx.update_flags(true, vec!["agent-v2".to_string()]);
4830        });
4831
4832        let prompt_builder = Arc::new(prompt_store::PromptBuilder::new(None).unwrap());
4833        let async_cx = cx.update(|window, cx| window.to_async(cx));
4834        let loaded_panel =
4835            AgentPanel::load(workspace.downgrade(), prompt_builder.clone(), async_cx)
4836                .await
4837                .expect("panel load should succeed");
4838        cx.run_until_parked();
4839
4840        loaded_panel.read_with(cx, |panel, _cx| {
4841            assert_eq!(
4842                *panel.start_thread_in(),
4843                StartThreadIn::LocalProject,
4844                "thread target should fall back to LocalProject when worktree flag is disabled"
4845            );
4846        });
4847    }
4848
4849    #[gpui::test]
4850    async fn test_set_active_blocked_during_worktree_creation(cx: &mut TestAppContext) {
4851        init_test(cx);
4852
4853        let fs = FakeFs::new(cx.executor());
4854        cx.update(|cx| {
4855            cx.update_flags(true, vec!["agent-v2".to_string()]);
4856            agent::ThreadStore::init_global(cx);
4857            language_model::LanguageModelRegistry::test(cx);
4858            <dyn fs::Fs>::set_global(fs.clone(), cx);
4859        });
4860
4861        fs.insert_tree(
4862            "/project",
4863            json!({
4864                ".git": {},
4865                "src": {
4866                    "main.rs": "fn main() {}"
4867                }
4868            }),
4869        )
4870        .await;
4871
4872        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
4873
4874        let multi_workspace =
4875            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4876
4877        let workspace = multi_workspace
4878            .read_with(cx, |multi_workspace, _cx| {
4879                multi_workspace.workspace().clone()
4880            })
4881            .unwrap();
4882
4883        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4884
4885        let panel = workspace.update_in(cx, |workspace, window, cx| {
4886            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
4887            let panel =
4888                cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
4889            workspace.add_panel(panel.clone(), window, cx);
4890            panel
4891        });
4892
4893        cx.run_until_parked();
4894
4895        // Simulate worktree creation in progress and reset to Uninitialized
4896        panel.update_in(cx, |panel, window, cx| {
4897            panel.worktree_creation_status = Some(WorktreeCreationStatus::Creating);
4898            panel.active_view = ActiveView::Uninitialized;
4899            Panel::set_active(panel, true, window, cx);
4900            assert!(
4901                matches!(panel.active_view, ActiveView::Uninitialized),
4902                "set_active should not create a thread while worktree is being created"
4903            );
4904        });
4905
4906        // Clear the creation status and use open_external_thread_with_server
4907        // (which bypasses new_agent_thread) to verify the panel can transition
4908        // out of Uninitialized. We can't call set_active directly because
4909        // new_agent_thread requires full agent server infrastructure.
4910        panel.update_in(cx, |panel, window, cx| {
4911            panel.worktree_creation_status = None;
4912            panel.active_view = ActiveView::Uninitialized;
4913            panel.open_external_thread_with_server(
4914                Rc::new(StubAgentServer::default_response()),
4915                window,
4916                cx,
4917            );
4918        });
4919
4920        cx.run_until_parked();
4921
4922        panel.read_with(cx, |panel, _cx| {
4923            assert!(
4924                !matches!(panel.active_view, ActiveView::Uninitialized),
4925                "panel should transition out of Uninitialized once worktree creation is cleared"
4926            );
4927        });
4928    }
4929
4930    #[test]
4931    fn test_deserialize_legacy_agent_type_variants() {
4932        assert_eq!(
4933            serde_json::from_str::<AgentType>(r#""ClaudeAgent""#).unwrap(),
4934            AgentType::Custom {
4935                name: CLAUDE_AGENT_NAME.into(),
4936            },
4937        );
4938        assert_eq!(
4939            serde_json::from_str::<AgentType>(r#""ClaudeCode""#).unwrap(),
4940            AgentType::Custom {
4941                name: CLAUDE_AGENT_NAME.into(),
4942            },
4943        );
4944        assert_eq!(
4945            serde_json::from_str::<AgentType>(r#""Codex""#).unwrap(),
4946            AgentType::Custom {
4947                name: CODEX_NAME.into(),
4948            },
4949        );
4950        assert_eq!(
4951            serde_json::from_str::<AgentType>(r#""Gemini""#).unwrap(),
4952            AgentType::Custom {
4953                name: GEMINI_NAME.into(),
4954            },
4955        );
4956    }
4957
4958    #[test]
4959    fn test_deserialize_current_agent_type_variants() {
4960        assert_eq!(
4961            serde_json::from_str::<AgentType>(r#""NativeAgent""#).unwrap(),
4962            AgentType::NativeAgent,
4963        );
4964        assert_eq!(
4965            serde_json::from_str::<AgentType>(r#""TextThread""#).unwrap(),
4966            AgentType::TextThread,
4967        );
4968        assert_eq!(
4969            serde_json::from_str::<AgentType>(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(),
4970            AgentType::Custom {
4971                name: "my-agent".into(),
4972            },
4973        );
4974    }
4975
4976    #[test]
4977    fn test_deserialize_legacy_serialized_panel() {
4978        let json = serde_json::json!({
4979            "width": 300.0,
4980            "selected_agent": "ClaudeAgent",
4981            "last_active_thread": {
4982                "session_id": "test-session",
4983                "agent_type": "Codex",
4984            },
4985        });
4986
4987        let panel: SerializedAgentPanel = serde_json::from_value(json).unwrap();
4988        assert_eq!(
4989            panel.selected_agent,
4990            Some(AgentType::Custom {
4991                name: CLAUDE_AGENT_NAME.into(),
4992            }),
4993        );
4994        let thread = panel.last_active_thread.unwrap();
4995        assert_eq!(
4996            thread.agent_type,
4997            AgentType::Custom {
4998                name: CODEX_NAME.into(),
4999            },
5000        );
5001    }
5002}