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