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