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    /// Partitions the project's visible worktrees into git-backed repositories
2045    /// and plain (non-git) paths. Git repos will have worktrees created for
2046    /// them; non-git paths are carried over to the new workspace as-is.
2047    ///
2048    /// When multiple worktrees map to the same repository, the most specific
2049    /// match wins (deepest work directory path), with a deterministic
2050    /// tie-break on entity id. Each repository appears at most once.
2051    fn classify_worktrees(
2052        &self,
2053        cx: &App,
2054    ) -> (Vec<Entity<project::git_store::Repository>>, Vec<PathBuf>) {
2055        let project = &self.project;
2056        let repositories = project.read(cx).repositories(cx).clone();
2057        let mut git_repos: Vec<Entity<project::git_store::Repository>> = Vec::new();
2058        let mut non_git_paths: Vec<PathBuf> = Vec::new();
2059        let mut seen_repo_ids = std::collections::HashSet::new();
2060
2061        for worktree in project.read(cx).visible_worktrees(cx) {
2062            let wt_path = worktree.read(cx).abs_path();
2063
2064            let matching_repo = repositories
2065                .iter()
2066                .filter_map(|(id, repo)| {
2067                    let work_dir = repo.read(cx).work_directory_abs_path.clone();
2068                    if wt_path.starts_with(work_dir.as_ref())
2069                        || work_dir.starts_with(wt_path.as_ref())
2070                    {
2071                        Some((*id, repo.clone(), work_dir.as_ref().components().count()))
2072                    } else {
2073                        None
2074                    }
2075                })
2076                .max_by(
2077                    |(left_id, _left_repo, left_depth), (right_id, _right_repo, right_depth)| {
2078                        left_depth
2079                            .cmp(right_depth)
2080                            .then_with(|| left_id.cmp(right_id))
2081                    },
2082                );
2083
2084            if let Some((id, repo, _)) = matching_repo {
2085                if seen_repo_ids.insert(id) {
2086                    git_repos.push(repo);
2087                }
2088            } else {
2089                non_git_paths.push(wt_path.to_path_buf());
2090            }
2091        }
2092
2093        (git_repos, non_git_paths)
2094    }
2095
2096    /// Kicks off an async git-worktree creation for each repository. Returns:
2097    ///
2098    /// - `creation_infos`: a vec of `(repo, new_path, receiver)` tuples—the
2099    ///   receiver resolves once the git worktree command finishes.
2100    /// - `path_remapping`: `(old_work_dir, new_worktree_path)` pairs used
2101    ///   later to remap open editor tabs into the new workspace.
2102    fn start_worktree_creations(
2103        git_repos: &[Entity<project::git_store::Repository>],
2104        branch_name: &str,
2105        worktree_directory_setting: &str,
2106        cx: &mut Context<Self>,
2107    ) -> Result<(
2108        Vec<(
2109            Entity<project::git_store::Repository>,
2110            PathBuf,
2111            futures::channel::oneshot::Receiver<Result<()>>,
2112        )>,
2113        Vec<(PathBuf, PathBuf)>,
2114    )> {
2115        let mut creation_infos = Vec::new();
2116        let mut path_remapping = Vec::new();
2117
2118        for repo in git_repos {
2119            let (work_dir, new_path, receiver) = repo.update(cx, |repo, _cx| {
2120                let original_repo = repo.original_repo_abs_path.clone();
2121                let directory =
2122                    validate_worktree_directory(&original_repo, worktree_directory_setting)?;
2123                let new_path = directory.join(branch_name);
2124                let receiver = repo.create_worktree(branch_name.to_string(), directory, None);
2125                let work_dir = repo.work_directory_abs_path.clone();
2126                anyhow::Ok((work_dir, new_path, receiver))
2127            })?;
2128            path_remapping.push((work_dir.to_path_buf(), new_path.clone()));
2129            creation_infos.push((repo.clone(), new_path, receiver));
2130        }
2131
2132        Ok((creation_infos, path_remapping))
2133    }
2134
2135    /// Waits for every in-flight worktree creation to complete. If any
2136    /// creation fails, all successfully-created worktrees are rolled back
2137    /// (removed) so the project isn't left in a half-migrated state.
2138    async fn await_and_rollback_on_failure(
2139        creation_infos: Vec<(
2140            Entity<project::git_store::Repository>,
2141            PathBuf,
2142            futures::channel::oneshot::Receiver<Result<()>>,
2143        )>,
2144        cx: &mut AsyncWindowContext,
2145    ) -> Result<Vec<PathBuf>> {
2146        let mut created_paths: Vec<PathBuf> = Vec::new();
2147        let mut repos_and_paths: Vec<(Entity<project::git_store::Repository>, PathBuf)> =
2148            Vec::new();
2149        let mut first_error: Option<anyhow::Error> = None;
2150
2151        for (repo, new_path, receiver) in creation_infos {
2152            match receiver.await {
2153                Ok(Ok(())) => {
2154                    created_paths.push(new_path.clone());
2155                    repos_and_paths.push((repo, new_path));
2156                }
2157                Ok(Err(err)) => {
2158                    if first_error.is_none() {
2159                        first_error = Some(err);
2160                    }
2161                }
2162                Err(_canceled) => {
2163                    if first_error.is_none() {
2164                        first_error = Some(anyhow!("Worktree creation was canceled"));
2165                    }
2166                }
2167            }
2168        }
2169
2170        let Some(err) = first_error else {
2171            return Ok(created_paths);
2172        };
2173
2174        // Rollback all successfully created worktrees
2175        let mut rollback_receivers = Vec::new();
2176        for (rollback_repo, rollback_path) in &repos_and_paths {
2177            if let Ok(receiver) = cx.update(|_, cx| {
2178                rollback_repo.update(cx, |repo, _cx| {
2179                    repo.remove_worktree(rollback_path.clone(), true)
2180                })
2181            }) {
2182                rollback_receivers.push((rollback_path.clone(), receiver));
2183            }
2184        }
2185        let mut rollback_failures: Vec<String> = Vec::new();
2186        for (path, receiver) in rollback_receivers {
2187            match receiver.await {
2188                Ok(Ok(())) => {}
2189                Ok(Err(rollback_err)) => {
2190                    log::error!(
2191                        "failed to rollback worktree at {}: {rollback_err}",
2192                        path.display()
2193                    );
2194                    rollback_failures.push(format!("{}: {rollback_err}", path.display()));
2195                }
2196                Err(rollback_err) => {
2197                    log::error!(
2198                        "failed to rollback worktree at {}: {rollback_err}",
2199                        path.display()
2200                    );
2201                    rollback_failures.push(format!("{}: {rollback_err}", path.display()));
2202                }
2203            }
2204        }
2205        let mut error_message = format!("Failed to create worktree: {err}");
2206        if !rollback_failures.is_empty() {
2207            error_message.push_str("\n\nFailed to clean up: ");
2208            error_message.push_str(&rollback_failures.join(", "));
2209        }
2210        Err(anyhow!(error_message))
2211    }
2212
2213    fn set_worktree_creation_error(
2214        &mut self,
2215        message: SharedString,
2216        window: &mut Window,
2217        cx: &mut Context<Self>,
2218    ) {
2219        self.worktree_creation_status = Some(WorktreeCreationStatus::Error(message));
2220        if matches!(self.active_view, ActiveView::Uninitialized) {
2221            let selected_agent = self.selected_agent.clone();
2222            self.new_agent_thread(selected_agent, window, cx);
2223        }
2224        cx.notify();
2225    }
2226
2227    fn handle_worktree_creation_requested(
2228        &mut self,
2229        content: Vec<acp::ContentBlock>,
2230        window: &mut Window,
2231        cx: &mut Context<Self>,
2232    ) {
2233        if matches!(
2234            self.worktree_creation_status,
2235            Some(WorktreeCreationStatus::Creating)
2236        ) {
2237            return;
2238        }
2239
2240        self.worktree_creation_status = Some(WorktreeCreationStatus::Creating);
2241        cx.notify();
2242
2243        let (git_repos, non_git_paths) = self.classify_worktrees(cx);
2244
2245        if git_repos.is_empty() {
2246            self.set_worktree_creation_error(
2247                "No git repositories found in the project".into(),
2248                window,
2249                cx,
2250            );
2251            return;
2252        }
2253
2254        // Kick off branch listing as early as possible so it can run
2255        // concurrently with the remaining synchronous setup work.
2256        let branch_receivers: Vec<_> = git_repos
2257            .iter()
2258            .map(|repo| repo.update(cx, |repo, _cx| repo.branches()))
2259            .collect();
2260
2261        let worktree_directory_setting = ProjectSettings::get_global(cx)
2262            .git
2263            .worktree_directory
2264            .clone();
2265
2266        let (dock_structure, open_file_paths) = self
2267            .workspace
2268            .upgrade()
2269            .map(|workspace| {
2270                let dock_structure = workspace.read(cx).capture_dock_state(window, cx);
2271                let open_file_paths = workspace.read(cx).open_item_abs_paths(cx);
2272                (dock_structure, open_file_paths)
2273            })
2274            .unwrap_or_default();
2275
2276        let workspace = self.workspace.clone();
2277        let window_handle = window
2278            .window_handle()
2279            .downcast::<workspace::MultiWorkspace>();
2280
2281        let task = cx.spawn_in(window, async move |this, cx| {
2282            // Await the branch listings we kicked off earlier.
2283            let mut existing_branches = Vec::new();
2284            for result in futures::future::join_all(branch_receivers).await {
2285                match result {
2286                    Ok(Ok(branches)) => {
2287                        for branch in branches {
2288                            existing_branches.push(branch.name().to_string());
2289                        }
2290                    }
2291                    Ok(Err(err)) => {
2292                        Err::<(), _>(err).log_err();
2293                    }
2294                    Err(_) => {}
2295                }
2296            }
2297
2298            let existing_branch_refs: Vec<&str> =
2299                existing_branches.iter().map(|s| s.as_str()).collect();
2300            let mut rng = rand::rng();
2301            let branch_name =
2302                match crate::branch_names::generate_branch_name(&existing_branch_refs, &mut rng) {
2303                    Some(name) => name,
2304                    None => {
2305                        this.update_in(cx, |this, window, cx| {
2306                            this.set_worktree_creation_error(
2307                                "Failed to generate a branch name: all typewriter names are taken"
2308                                    .into(),
2309                                window,
2310                                cx,
2311                            );
2312                        })?;
2313                        return anyhow::Ok(());
2314                    }
2315                };
2316
2317            let (creation_infos, path_remapping) = match this.update_in(cx, |_this, _window, cx| {
2318                Self::start_worktree_creations(
2319                    &git_repos,
2320                    &branch_name,
2321                    &worktree_directory_setting,
2322                    cx,
2323                )
2324            }) {
2325                Ok(Ok(result)) => result,
2326                Ok(Err(err)) | Err(err) => {
2327                    this.update_in(cx, |this, window, cx| {
2328                        this.set_worktree_creation_error(
2329                            format!("Failed to validate worktree directory: {err}").into(),
2330                            window,
2331                            cx,
2332                        );
2333                    })
2334                    .log_err();
2335                    return anyhow::Ok(());
2336                }
2337            };
2338
2339            let created_paths = match Self::await_and_rollback_on_failure(creation_infos, cx).await
2340            {
2341                Ok(paths) => paths,
2342                Err(err) => {
2343                    this.update_in(cx, |this, window, cx| {
2344                        this.set_worktree_creation_error(format!("{err}").into(), window, cx);
2345                    })?;
2346                    return anyhow::Ok(());
2347                }
2348            };
2349
2350            let mut all_paths = created_paths;
2351            let has_non_git = !non_git_paths.is_empty();
2352            all_paths.extend(non_git_paths.iter().cloned());
2353
2354            let app_state = match workspace.upgrade() {
2355                Some(workspace) => cx.update(|_, cx| workspace.read(cx).app_state().clone())?,
2356                None => {
2357                    this.update_in(cx, |this, window, cx| {
2358                        this.set_worktree_creation_error(
2359                            "Workspace no longer available".into(),
2360                            window,
2361                            cx,
2362                        );
2363                    })?;
2364                    return anyhow::Ok(());
2365                }
2366            };
2367
2368            let this_for_error = this.clone();
2369            if let Err(err) = Self::setup_new_workspace(
2370                this,
2371                all_paths,
2372                app_state,
2373                window_handle,
2374                dock_structure,
2375                open_file_paths,
2376                path_remapping,
2377                non_git_paths,
2378                has_non_git,
2379                content,
2380                cx,
2381            )
2382            .await
2383            {
2384                this_for_error
2385                    .update_in(cx, |this, window, cx| {
2386                        this.set_worktree_creation_error(
2387                            format!("Failed to set up workspace: {err}").into(),
2388                            window,
2389                            cx,
2390                        );
2391                    })
2392                    .log_err();
2393            }
2394            anyhow::Ok(())
2395        });
2396
2397        self._worktree_creation_task = Some(cx.foreground_executor().spawn(async move {
2398            task.await.log_err();
2399        }));
2400    }
2401
2402    async fn setup_new_workspace(
2403        this: WeakEntity<Self>,
2404        all_paths: Vec<PathBuf>,
2405        app_state: Arc<workspace::AppState>,
2406        window_handle: Option<gpui::WindowHandle<workspace::MultiWorkspace>>,
2407        dock_structure: workspace::DockStructure,
2408        open_file_paths: Vec<PathBuf>,
2409        path_remapping: Vec<(PathBuf, PathBuf)>,
2410        non_git_paths: Vec<PathBuf>,
2411        has_non_git: bool,
2412        content: Vec<acp::ContentBlock>,
2413        cx: &mut AsyncWindowContext,
2414    ) -> Result<()> {
2415        let init: Option<
2416            Box<dyn FnOnce(&mut Workspace, &mut Window, &mut gpui::Context<Workspace>) + Send>,
2417        > = Some(Box::new(move |workspace, window, cx| {
2418            workspace.set_dock_structure(dock_structure, window, cx);
2419        }));
2420
2421        let (new_window_handle, _) = cx
2422            .update(|_window, cx| {
2423                Workspace::new_local(all_paths, app_state, window_handle, None, init, false, cx)
2424            })?
2425            .await?;
2426
2427        let new_workspace = new_window_handle.update(cx, |multi_workspace, _window, _cx| {
2428            let workspaces = multi_workspace.workspaces();
2429            workspaces.last().cloned()
2430        })?;
2431
2432        let Some(new_workspace) = new_workspace else {
2433            anyhow::bail!("New workspace was not added to MultiWorkspace");
2434        };
2435
2436        let panels_task = new_window_handle.update(cx, |_, _, cx| {
2437            new_workspace.update(cx, |workspace, _cx| workspace.take_panels_task())
2438        })?;
2439        if let Some(task) = panels_task {
2440            task.await.log_err();
2441        }
2442
2443        let initial_content = AgentInitialContent::ContentBlock {
2444            blocks: content,
2445            auto_submit: true,
2446        };
2447
2448        new_window_handle.update(cx, |_multi_workspace, window, cx| {
2449            new_workspace.update(cx, |workspace, cx| {
2450                if has_non_git {
2451                    let toast_id = workspace::notifications::NotificationId::unique::<AgentPanel>();
2452                    workspace.show_toast(
2453                        workspace::Toast::new(
2454                            toast_id,
2455                            "Some project folders are not git repositories. \
2456                             They were included as-is without creating a worktree.",
2457                        ),
2458                        cx,
2459                    );
2460                }
2461
2462                let remapped_paths: Vec<PathBuf> = open_file_paths
2463                    .iter()
2464                    .filter_map(|original_path| {
2465                        let best_match = path_remapping
2466                            .iter()
2467                            .filter_map(|(old_root, new_root)| {
2468                                original_path.strip_prefix(old_root).ok().map(|relative| {
2469                                    (old_root.components().count(), new_root.join(relative))
2470                                })
2471                            })
2472                            .max_by_key(|(depth, _)| *depth);
2473
2474                        if let Some((_, remapped_path)) = best_match {
2475                            return Some(remapped_path);
2476                        }
2477
2478                        for non_git in &non_git_paths {
2479                            if original_path.starts_with(non_git) {
2480                                return Some(original_path.clone());
2481                            }
2482                        }
2483                        None
2484                    })
2485                    .collect();
2486
2487                if !remapped_paths.is_empty() {
2488                    workspace
2489                        .open_paths(
2490                            remapped_paths,
2491                            workspace::OpenOptions::default(),
2492                            None,
2493                            window,
2494                            cx,
2495                        )
2496                        .detach();
2497                }
2498
2499                workspace.focus_panel::<AgentPanel>(window, cx);
2500                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
2501                    panel.update(cx, |panel, cx| {
2502                        panel.external_thread(None, None, Some(initial_content), window, cx);
2503                    });
2504                }
2505            });
2506        })?;
2507
2508        new_window_handle.update(cx, |multi_workspace, _window, cx| {
2509            multi_workspace.activate(new_workspace.clone(), cx);
2510        })?;
2511
2512        this.update_in(cx, |this, _window, cx| {
2513            this.worktree_creation_status = None;
2514            cx.notify();
2515        })?;
2516
2517        anyhow::Ok(())
2518    }
2519}
2520
2521impl Focusable for AgentPanel {
2522    fn focus_handle(&self, cx: &App) -> FocusHandle {
2523        match &self.active_view {
2524            ActiveView::Uninitialized => self.focus_handle.clone(),
2525            ActiveView::AgentThread { server_view, .. } => server_view.focus_handle(cx),
2526            ActiveView::History { kind } => match kind {
2527                HistoryKind::AgentThreads => self.acp_history.focus_handle(cx),
2528                HistoryKind::TextThreads => self.text_thread_history.focus_handle(cx),
2529            },
2530            ActiveView::TextThread {
2531                text_thread_editor, ..
2532            } => text_thread_editor.focus_handle(cx),
2533            ActiveView::Configuration => {
2534                if let Some(configuration) = self.configuration.as_ref() {
2535                    configuration.focus_handle(cx)
2536                } else {
2537                    self.focus_handle.clone()
2538                }
2539            }
2540        }
2541    }
2542}
2543
2544fn agent_panel_dock_position(cx: &App) -> DockPosition {
2545    AgentSettings::get_global(cx).dock.into()
2546}
2547
2548pub enum AgentPanelEvent {
2549    ActiveViewChanged,
2550}
2551
2552impl EventEmitter<PanelEvent> for AgentPanel {}
2553impl EventEmitter<AgentPanelEvent> for AgentPanel {}
2554
2555impl Panel for AgentPanel {
2556    fn persistent_name() -> &'static str {
2557        "AgentPanel"
2558    }
2559
2560    fn panel_key() -> &'static str {
2561        AGENT_PANEL_KEY
2562    }
2563
2564    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
2565        agent_panel_dock_position(cx)
2566    }
2567
2568    fn position_is_valid(&self, position: DockPosition) -> bool {
2569        position != DockPosition::Bottom
2570    }
2571
2572    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
2573        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
2574            settings
2575                .agent
2576                .get_or_insert_default()
2577                .set_dock(position.into());
2578        });
2579    }
2580
2581    fn size(&self, window: &Window, cx: &App) -> Pixels {
2582        let settings = AgentSettings::get_global(cx);
2583        match self.position(window, cx) {
2584            DockPosition::Left | DockPosition::Right => {
2585                self.width.unwrap_or(settings.default_width)
2586            }
2587            DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
2588        }
2589    }
2590
2591    fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
2592        match self.position(window, cx) {
2593            DockPosition::Left | DockPosition::Right => self.width = size,
2594            DockPosition::Bottom => self.height = size,
2595        }
2596        self.serialize(cx);
2597        cx.notify();
2598    }
2599
2600    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
2601        if active
2602            && matches!(self.active_view, ActiveView::Uninitialized)
2603            && !matches!(
2604                self.worktree_creation_status,
2605                Some(WorktreeCreationStatus::Creating)
2606            )
2607        {
2608            let selected_agent = self.selected_agent.clone();
2609            self.new_agent_thread(selected_agent, window, cx);
2610        }
2611    }
2612
2613    fn remote_id() -> Option<proto::PanelId> {
2614        Some(proto::PanelId::AssistantPanel)
2615    }
2616
2617    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
2618        (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
2619    }
2620
2621    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
2622        Some("Agent Panel")
2623    }
2624
2625    fn toggle_action(&self) -> Box<dyn Action> {
2626        Box::new(ToggleFocus)
2627    }
2628
2629    fn activation_priority(&self) -> u32 {
2630        3
2631    }
2632
2633    fn enabled(&self, cx: &App) -> bool {
2634        AgentSettings::get_global(cx).enabled(cx)
2635    }
2636
2637    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
2638        self.zoomed
2639    }
2640
2641    fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
2642        self.zoomed = zoomed;
2643        cx.notify();
2644    }
2645}
2646
2647impl AgentPanel {
2648    fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
2649        const LOADING_SUMMARY_PLACEHOLDER: &str = "Loading Summary…";
2650
2651        let content = match &self.active_view {
2652            ActiveView::AgentThread { server_view } => {
2653                let is_generating_title = server_view
2654                    .read(cx)
2655                    .as_native_thread(cx)
2656                    .map_or(false, |t| t.read(cx).is_generating_title());
2657
2658                if let Some(title_editor) = server_view
2659                    .read(cx)
2660                    .parent_thread(cx)
2661                    .map(|r| r.read(cx).title_editor.clone())
2662                {
2663                    let container = div()
2664                        .w_full()
2665                        .on_action({
2666                            let thread_view = server_view.downgrade();
2667                            move |_: &menu::Confirm, window, cx| {
2668                                if let Some(thread_view) = thread_view.upgrade() {
2669                                    thread_view.focus_handle(cx).focus(window, cx);
2670                                }
2671                            }
2672                        })
2673                        .on_action({
2674                            let thread_view = server_view.downgrade();
2675                            move |_: &editor::actions::Cancel, window, cx| {
2676                                if let Some(thread_view) = thread_view.upgrade() {
2677                                    thread_view.focus_handle(cx).focus(window, cx);
2678                                }
2679                            }
2680                        })
2681                        .child(title_editor);
2682
2683                    if is_generating_title {
2684                        container
2685                            .with_animation(
2686                                "generating_title",
2687                                Animation::new(Duration::from_secs(2))
2688                                    .repeat()
2689                                    .with_easing(pulsating_between(0.4, 0.8)),
2690                                |div, delta| div.opacity(delta),
2691                            )
2692                            .into_any_element()
2693                    } else {
2694                        container.into_any_element()
2695                    }
2696                } else {
2697                    Label::new(server_view.read(cx).title(cx))
2698                        .color(Color::Muted)
2699                        .truncate()
2700                        .into_any_element()
2701                }
2702            }
2703            ActiveView::TextThread {
2704                title_editor,
2705                text_thread_editor,
2706                ..
2707            } => {
2708                let summary = text_thread_editor.read(cx).text_thread().read(cx).summary();
2709
2710                match summary {
2711                    TextThreadSummary::Pending => Label::new(TextThreadSummary::DEFAULT)
2712                        .color(Color::Muted)
2713                        .truncate()
2714                        .into_any_element(),
2715                    TextThreadSummary::Content(summary) => {
2716                        if summary.done {
2717                            div()
2718                                .w_full()
2719                                .child(title_editor.clone())
2720                                .into_any_element()
2721                        } else {
2722                            Label::new(LOADING_SUMMARY_PLACEHOLDER)
2723                                .truncate()
2724                                .color(Color::Muted)
2725                                .with_animation(
2726                                    "generating_title",
2727                                    Animation::new(Duration::from_secs(2))
2728                                        .repeat()
2729                                        .with_easing(pulsating_between(0.4, 0.8)),
2730                                    |label, delta| label.alpha(delta),
2731                                )
2732                                .into_any_element()
2733                        }
2734                    }
2735                    TextThreadSummary::Error => h_flex()
2736                        .w_full()
2737                        .child(title_editor.clone())
2738                        .child(
2739                            IconButton::new("retry-summary-generation", IconName::RotateCcw)
2740                                .icon_size(IconSize::Small)
2741                                .on_click({
2742                                    let text_thread_editor = text_thread_editor.clone();
2743                                    move |_, _window, cx| {
2744                                        text_thread_editor.update(cx, |text_thread_editor, cx| {
2745                                            text_thread_editor.regenerate_summary(cx);
2746                                        });
2747                                    }
2748                                })
2749                                .tooltip(move |_window, cx| {
2750                                    cx.new(|_| {
2751                                        Tooltip::new("Failed to generate title")
2752                                            .meta("Click to try again")
2753                                    })
2754                                    .into()
2755                                }),
2756                        )
2757                        .into_any_element(),
2758                }
2759            }
2760            ActiveView::History { kind } => {
2761                let title = match kind {
2762                    HistoryKind::AgentThreads => "History",
2763                    HistoryKind::TextThreads => "Text Thread History",
2764                };
2765                Label::new(title).truncate().into_any_element()
2766            }
2767            ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
2768            ActiveView::Uninitialized => Label::new("Agent").truncate().into_any_element(),
2769        };
2770
2771        h_flex()
2772            .key_context("TitleEditor")
2773            .id("TitleEditor")
2774            .flex_grow()
2775            .w_full()
2776            .max_w_full()
2777            .overflow_x_scroll()
2778            .child(content)
2779            .into_any()
2780    }
2781
2782    fn handle_regenerate_thread_title(thread_view: Entity<ConnectionView>, cx: &mut App) {
2783        thread_view.update(cx, |thread_view, cx| {
2784            if let Some(thread) = thread_view.as_native_thread(cx) {
2785                thread.update(cx, |thread, cx| {
2786                    thread.generate_title(cx);
2787                });
2788            }
2789        });
2790    }
2791
2792    fn handle_regenerate_text_thread_title(
2793        text_thread_editor: Entity<TextThreadEditor>,
2794        cx: &mut App,
2795    ) {
2796        text_thread_editor.update(cx, |text_thread_editor, cx| {
2797            text_thread_editor.regenerate_summary(cx);
2798        });
2799    }
2800
2801    fn render_panel_options_menu(
2802        &self,
2803        window: &mut Window,
2804        cx: &mut Context<Self>,
2805    ) -> impl IntoElement {
2806        let focus_handle = self.focus_handle(cx);
2807
2808        let full_screen_label = if self.is_zoomed(window, cx) {
2809            "Disable Full Screen"
2810        } else {
2811            "Enable Full Screen"
2812        };
2813
2814        let text_thread_view = match &self.active_view {
2815            ActiveView::TextThread {
2816                text_thread_editor, ..
2817            } => Some(text_thread_editor.clone()),
2818            _ => None,
2819        };
2820        let text_thread_with_messages = match &self.active_view {
2821            ActiveView::TextThread {
2822                text_thread_editor, ..
2823            } => text_thread_editor
2824                .read(cx)
2825                .text_thread()
2826                .read(cx)
2827                .messages(cx)
2828                .any(|message| message.role == language_model::Role::Assistant),
2829            _ => false,
2830        };
2831
2832        let thread_view = match &self.active_view {
2833            ActiveView::AgentThread { server_view } => Some(server_view.clone()),
2834            _ => None,
2835        };
2836        let thread_with_messages = match &self.active_view {
2837            ActiveView::AgentThread { server_view } => {
2838                server_view.read(cx).has_user_submitted_prompt(cx)
2839            }
2840            _ => false,
2841        };
2842        let has_auth_methods = match &self.active_view {
2843            ActiveView::AgentThread { server_view } => server_view.read(cx).has_auth_methods(),
2844            _ => false,
2845        };
2846
2847        PopoverMenu::new("agent-options-menu")
2848            .trigger_with_tooltip(
2849                IconButton::new("agent-options-menu", IconName::Ellipsis)
2850                    .icon_size(IconSize::Small),
2851                {
2852                    let focus_handle = focus_handle.clone();
2853                    move |_window, cx| {
2854                        Tooltip::for_action_in(
2855                            "Toggle Agent Menu",
2856                            &ToggleOptionsMenu,
2857                            &focus_handle,
2858                            cx,
2859                        )
2860                    }
2861                },
2862            )
2863            .anchor(Corner::TopRight)
2864            .with_handle(self.agent_panel_menu_handle.clone())
2865            .menu({
2866                move |window, cx| {
2867                    Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
2868                        menu = menu.context(focus_handle.clone());
2869
2870                        if thread_with_messages | text_thread_with_messages {
2871                            menu = menu.header("Current Thread");
2872
2873                            if let Some(text_thread_view) = text_thread_view.as_ref() {
2874                                menu = menu
2875                                    .entry("Regenerate Thread Title", None, {
2876                                        let text_thread_view = text_thread_view.clone();
2877                                        move |_, cx| {
2878                                            Self::handle_regenerate_text_thread_title(
2879                                                text_thread_view.clone(),
2880                                                cx,
2881                                            );
2882                                        }
2883                                    })
2884                                    .separator();
2885                            }
2886
2887                            if let Some(thread_view) = thread_view.as_ref() {
2888                                menu = menu
2889                                    .entry("Regenerate Thread Title", None, {
2890                                        let thread_view = thread_view.clone();
2891                                        move |_, cx| {
2892                                            Self::handle_regenerate_thread_title(
2893                                                thread_view.clone(),
2894                                                cx,
2895                                            );
2896                                        }
2897                                    })
2898                                    .separator();
2899                            }
2900                        }
2901
2902                        menu = menu
2903                            .header("MCP Servers")
2904                            .action(
2905                                "View Server Extensions",
2906                                Box::new(zed_actions::Extensions {
2907                                    category_filter: Some(
2908                                        zed_actions::ExtensionCategoryFilter::ContextServers,
2909                                    ),
2910                                    id: None,
2911                                }),
2912                            )
2913                            .action("Add Custom Server…", Box::new(AddContextServer))
2914                            .separator()
2915                            .action("Rules", Box::new(OpenRulesLibrary::default()))
2916                            .action("Profiles", Box::new(ManageProfiles::default()))
2917                            .action("Settings", Box::new(OpenSettings))
2918                            .separator()
2919                            .action(full_screen_label, Box::new(ToggleZoom));
2920
2921                        if has_auth_methods {
2922                            menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
2923                        }
2924
2925                        menu
2926                    }))
2927                }
2928            })
2929    }
2930
2931    fn render_recent_entries_menu(
2932        &self,
2933        icon: IconName,
2934        corner: Corner,
2935        cx: &mut Context<Self>,
2936    ) -> impl IntoElement {
2937        let focus_handle = self.focus_handle(cx);
2938
2939        PopoverMenu::new("agent-nav-menu")
2940            .trigger_with_tooltip(
2941                IconButton::new("agent-nav-menu", icon).icon_size(IconSize::Small),
2942                {
2943                    move |_window, cx| {
2944                        Tooltip::for_action_in(
2945                            "Toggle Recently Updated Threads",
2946                            &ToggleNavigationMenu,
2947                            &focus_handle,
2948                            cx,
2949                        )
2950                    }
2951                },
2952            )
2953            .anchor(corner)
2954            .with_handle(self.agent_navigation_menu_handle.clone())
2955            .menu({
2956                let menu = self.agent_navigation_menu.clone();
2957                move |window, cx| {
2958                    telemetry::event!("View Thread History Clicked");
2959
2960                    if let Some(menu) = menu.as_ref() {
2961                        menu.update(cx, |_, cx| {
2962                            cx.defer_in(window, |menu, window, cx| {
2963                                menu.rebuild(window, cx);
2964                            });
2965                        })
2966                    }
2967                    menu.clone()
2968                }
2969            })
2970    }
2971
2972    fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
2973        let focus_handle = self.focus_handle(cx);
2974
2975        IconButton::new("go-back", IconName::ArrowLeft)
2976            .icon_size(IconSize::Small)
2977            .on_click(cx.listener(|this, _, window, cx| {
2978                this.go_back(&workspace::GoBack, window, cx);
2979            }))
2980            .tooltip({
2981                move |_window, cx| {
2982                    Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
2983                }
2984            })
2985    }
2986
2987    fn project_has_git_repository(&self, cx: &App) -> bool {
2988        !self.project.read(cx).repositories(cx).is_empty()
2989    }
2990
2991    fn render_start_thread_in_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
2992        let has_git_repo = self.project_has_git_repository(cx);
2993        let is_via_collab = self.project.read(cx).is_via_collab();
2994
2995        let is_creating = matches!(
2996            self.worktree_creation_status,
2997            Some(WorktreeCreationStatus::Creating)
2998        );
2999
3000        let current_target = self.start_thread_in;
3001        let trigger_label = self.start_thread_in.label();
3002
3003        let icon = if self.start_thread_in_menu_handle.is_deployed() {
3004            IconName::ChevronUp
3005        } else {
3006            IconName::ChevronDown
3007        };
3008
3009        let trigger_button = Button::new("thread-target-trigger", trigger_label)
3010            .label_size(LabelSize::Small)
3011            .color(Color::Muted)
3012            .icon(icon)
3013            .icon_size(IconSize::XSmall)
3014            .icon_position(IconPosition::End)
3015            .icon_color(Color::Muted)
3016            .disabled(is_creating);
3017
3018        let dock_position = AgentSettings::get_global(cx).dock;
3019        let documentation_side = match dock_position {
3020            settings::DockPosition::Left => DocumentationSide::Right,
3021            settings::DockPosition::Bottom | settings::DockPosition::Right => {
3022                DocumentationSide::Left
3023            }
3024        };
3025
3026        PopoverMenu::new("thread-target-selector")
3027            .trigger(trigger_button)
3028            .anchor(gpui::Corner::BottomRight)
3029            .with_handle(self.start_thread_in_menu_handle.clone())
3030            .menu(move |window, cx| {
3031                let current_target = current_target;
3032                Some(ContextMenu::build(window, cx, move |menu, _window, _cx| {
3033                    let is_local_selected = current_target == StartThreadIn::LocalProject;
3034                    let is_new_worktree_selected = current_target == StartThreadIn::NewWorktree;
3035
3036                    let new_worktree_disabled = !has_git_repo || is_via_collab;
3037
3038                    menu.header("Start Thread In…")
3039                        .item(
3040                            ContextMenuEntry::new("Local Project")
3041                                .icon(StartThreadIn::LocalProject.icon())
3042                                .icon_color(Color::Muted)
3043                                .toggleable(IconPosition::End, is_local_selected)
3044                                .handler(|window, cx| {
3045                                    window
3046                                        .dispatch_action(Box::new(StartThreadIn::LocalProject), cx);
3047                                }),
3048                        )
3049                        .item({
3050                            let entry = ContextMenuEntry::new("New Worktree")
3051                                .icon(StartThreadIn::NewWorktree.icon())
3052                                .icon_color(Color::Muted)
3053                                .toggleable(IconPosition::End, is_new_worktree_selected)
3054                                .disabled(new_worktree_disabled)
3055                                .handler(|window, cx| {
3056                                    window
3057                                        .dispatch_action(Box::new(StartThreadIn::NewWorktree), cx);
3058                                });
3059
3060                            if new_worktree_disabled {
3061                                entry.documentation_aside(documentation_side, move |_| {
3062                                    let reason = if !has_git_repo {
3063                                        "No git repository found in this project."
3064                                    } else {
3065                                        "Not available for remote/collab projects yet."
3066                                    };
3067                                    Label::new(reason)
3068                                        .color(Color::Muted)
3069                                        .size(LabelSize::Small)
3070                                        .into_any_element()
3071                                })
3072                            } else {
3073                                entry
3074                            }
3075                        })
3076                }))
3077            })
3078    }
3079
3080    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3081        let agent_server_store = self.project.read(cx).agent_server_store().clone();
3082        let focus_handle = self.focus_handle(cx);
3083
3084        let (selected_agent_custom_icon, selected_agent_label) =
3085            if let AgentType::Custom { name, .. } = &self.selected_agent {
3086                let store = agent_server_store.read(cx);
3087                let icon = store.agent_icon(&ExternalAgentServerName(name.clone()));
3088
3089                let label = store
3090                    .agent_display_name(&ExternalAgentServerName(name.clone()))
3091                    .unwrap_or_else(|| self.selected_agent.label());
3092                (icon, label)
3093            } else {
3094                (None, self.selected_agent.label())
3095            };
3096
3097        let active_thread = match &self.active_view {
3098            ActiveView::AgentThread { server_view } => server_view.read(cx).as_native_thread(cx),
3099            ActiveView::Uninitialized
3100            | ActiveView::TextThread { .. }
3101            | ActiveView::History { .. }
3102            | ActiveView::Configuration => None,
3103        };
3104
3105        let new_thread_menu = PopoverMenu::new("new_thread_menu")
3106            .trigger_with_tooltip(
3107                IconButton::new("new_thread_menu_btn", IconName::Plus).icon_size(IconSize::Small),
3108                {
3109                    let focus_handle = focus_handle.clone();
3110                    move |_window, cx| {
3111                        Tooltip::for_action_in(
3112                            "New Thread…",
3113                            &ToggleNewThreadMenu,
3114                            &focus_handle,
3115                            cx,
3116                        )
3117                    }
3118                },
3119            )
3120            .anchor(Corner::TopRight)
3121            .with_handle(self.new_thread_menu_handle.clone())
3122            .menu({
3123                let selected_agent = self.selected_agent.clone();
3124                let is_agent_selected = move |agent_type: AgentType| selected_agent == agent_type;
3125
3126                let workspace = self.workspace.clone();
3127                let is_via_collab = workspace
3128                    .update(cx, |workspace, cx| {
3129                        workspace.project().read(cx).is_via_collab()
3130                    })
3131                    .unwrap_or_default();
3132
3133                move |window, cx| {
3134                    telemetry::event!("New Thread Clicked");
3135
3136                    let active_thread = active_thread.clone();
3137                    Some(ContextMenu::build(window, cx, |menu, _window, cx| {
3138                        menu.context(focus_handle.clone())
3139                            .when_some(active_thread, |this, active_thread| {
3140                                let thread = active_thread.read(cx);
3141
3142                                if !thread.is_empty() {
3143                                    let session_id = thread.id().clone();
3144                                    this.item(
3145                                        ContextMenuEntry::new("New From Summary")
3146                                            .icon(IconName::ThreadFromSummary)
3147                                            .icon_color(Color::Muted)
3148                                            .handler(move |window, cx| {
3149                                                window.dispatch_action(
3150                                                    Box::new(NewNativeAgentThreadFromSummary {
3151                                                        from_session_id: session_id.clone(),
3152                                                    }),
3153                                                    cx,
3154                                                );
3155                                            }),
3156                                    )
3157                                } else {
3158                                    this
3159                                }
3160                            })
3161                            .item(
3162                                ContextMenuEntry::new("Zed Agent")
3163                                    .when(
3164                                        is_agent_selected(AgentType::NativeAgent)
3165                                            | is_agent_selected(AgentType::TextThread),
3166                                        |this| {
3167                                            this.action(Box::new(NewExternalAgentThread {
3168                                                agent: None,
3169                                            }))
3170                                        },
3171                                    )
3172                                    .icon(IconName::ZedAgent)
3173                                    .icon_color(Color::Muted)
3174                                    .handler({
3175                                        let workspace = workspace.clone();
3176                                        move |window, cx| {
3177                                            if let Some(workspace) = workspace.upgrade() {
3178                                                workspace.update(cx, |workspace, cx| {
3179                                                    if let Some(panel) =
3180                                                        workspace.panel::<AgentPanel>(cx)
3181                                                    {
3182                                                        panel.update(cx, |panel, cx| {
3183                                                            panel.new_agent_thread(
3184                                                                AgentType::NativeAgent,
3185                                                                window,
3186                                                                cx,
3187                                                            );
3188                                                        });
3189                                                    }
3190                                                });
3191                                            }
3192                                        }
3193                                    }),
3194                            )
3195                            .item(
3196                                ContextMenuEntry::new("Text Thread")
3197                                    .action(NewTextThread.boxed_clone())
3198                                    .icon(IconName::TextThread)
3199                                    .icon_color(Color::Muted)
3200                                    .handler({
3201                                        let workspace = workspace.clone();
3202                                        move |window, cx| {
3203                                            if let Some(workspace) = workspace.upgrade() {
3204                                                workspace.update(cx, |workspace, cx| {
3205                                                    if let Some(panel) =
3206                                                        workspace.panel::<AgentPanel>(cx)
3207                                                    {
3208                                                        panel.update(cx, |panel, cx| {
3209                                                            panel.new_agent_thread(
3210                                                                AgentType::TextThread,
3211                                                                window,
3212                                                                cx,
3213                                                            );
3214                                                        });
3215                                                    }
3216                                                });
3217                                            }
3218                                        }
3219                                    }),
3220                            )
3221                            .separator()
3222                            .header("External Agents")
3223                            .map(|mut menu| {
3224                                let agent_server_store = agent_server_store.read(cx);
3225                                let registry_store =
3226                                    project::AgentRegistryStore::try_global(cx);
3227                                let registry_store_ref =
3228                                    registry_store.as_ref().map(|s| s.read(cx));
3229
3230                                struct AgentMenuItem {
3231                                    id: ExternalAgentServerName,
3232                                    display_name: SharedString,
3233                                }
3234
3235                                let agent_items = agent_server_store
3236                                    .external_agents()
3237                                    .map(|name| {
3238                                        let display_name = agent_server_store
3239                                            .agent_display_name(name)
3240                                            .or_else(|| {
3241                                                registry_store_ref
3242                                                    .as_ref()
3243                                                    .and_then(|store| store.agent(name.0.as_ref()))
3244                                                    .map(|a| a.name().clone())
3245                                            })
3246                                            .unwrap_or_else(|| name.0.clone());
3247                                        AgentMenuItem {
3248                                            id: name.clone(),
3249                                            display_name,
3250                                        }
3251                                    })
3252                                    .sorted_unstable_by_key(|e| e.display_name.to_lowercase())
3253                                    .collect::<Vec<_>>();
3254
3255                                for item in &agent_items {
3256                                    let mut entry =
3257                                        ContextMenuEntry::new(item.display_name.clone());
3258
3259                                    let icon_path = agent_server_store
3260                                        .agent_icon(&item.id)
3261                                        .or_else(|| {
3262                                            registry_store_ref
3263                                                .as_ref()
3264                                                .and_then(|store| store.agent(item.id.0.as_str()))
3265                                                .and_then(|a| a.icon_path().cloned())
3266                                        });
3267
3268                                    if let Some(icon_path) = icon_path {
3269                                        entry = entry.custom_icon_svg(icon_path);
3270                                    } else {
3271                                        entry = entry.icon(IconName::Sparkle);
3272                                    }
3273
3274                                    entry = entry
3275                                        .when(
3276                                            is_agent_selected(AgentType::Custom {
3277                                                name: item.id.0.clone(),
3278                                            }),
3279                                            |this| {
3280                                                this.action(Box::new(
3281                                                    NewExternalAgentThread { agent: None },
3282                                                ))
3283                                            },
3284                                        )
3285                                        .icon_color(Color::Muted)
3286                                        .disabled(is_via_collab)
3287                                        .handler({
3288                                            let workspace = workspace.clone();
3289                                            let agent_id = item.id.clone();
3290                                            move |window, cx| {
3291                                                if let Some(workspace) = workspace.upgrade() {
3292                                                    workspace.update(cx, |workspace, cx| {
3293                                                        if let Some(panel) =
3294                                                            workspace.panel::<AgentPanel>(cx)
3295                                                        {
3296                                                            panel.update(cx, |panel, cx| {
3297                                                                panel.new_agent_thread(
3298                                                                    AgentType::Custom {
3299                                                                        name: agent_id.0.clone(),
3300                                                                    },
3301                                                                    window,
3302                                                                    cx,
3303                                                                );
3304                                                            });
3305                                                        }
3306                                                    });
3307                                                }
3308                                            }
3309                                        });
3310
3311                                    menu = menu.item(entry);
3312                                }
3313
3314                                menu
3315                            })
3316                            .separator()
3317                            .map(|mut menu| {
3318                                let agent_server_store = agent_server_store.read(cx);
3319                                let registry_store =
3320                                    project::AgentRegistryStore::try_global(cx);
3321                                let registry_store_ref =
3322                                    registry_store.as_ref().map(|s| s.read(cx));
3323
3324                                let previous_built_in_ids: &[ExternalAgentServerName] =
3325                                    &[CLAUDE_AGENT_NAME.into(), CODEX_NAME.into(), GEMINI_NAME.into()];
3326
3327                                let promoted_items = previous_built_in_ids
3328                                    .iter()
3329                                    .filter(|id| {
3330                                        !agent_server_store.external_agents.contains_key(*id)
3331                                    })
3332                                    .map(|name| {
3333                                        let display_name = registry_store_ref
3334                                            .as_ref()
3335                                            .and_then(|store| store.agent(name.0.as_ref()))
3336                                            .map(|a| a.name().clone())
3337                                            .unwrap_or_else(|| name.0.clone());
3338                                        (name.clone(), display_name)
3339                                    })
3340                                    .sorted_unstable_by_key(|(_, display_name)| display_name.to_lowercase())
3341                                    .collect::<Vec<_>>();
3342
3343                                for (agent_id, display_name) in &promoted_items {
3344                                    let mut entry =
3345                                        ContextMenuEntry::new(display_name.clone());
3346
3347                                    let icon_path = registry_store_ref
3348                                        .as_ref()
3349                                        .and_then(|store| store.agent(agent_id.0.as_str()))
3350                                        .and_then(|a| a.icon_path().cloned());
3351
3352                                    if let Some(icon_path) = icon_path {
3353                                        entry = entry.custom_icon_svg(icon_path);
3354                                    } else {
3355                                        entry = entry.icon(IconName::Sparkle);
3356                                    }
3357
3358                                    entry = entry
3359                                        .icon_color(Color::Muted)
3360                                        .disabled(is_via_collab)
3361                                        .handler({
3362                                            let workspace = workspace.clone();
3363                                            let agent_id = agent_id.clone();
3364                                            move |window, cx| {
3365                                                let fs = <dyn fs::Fs>::global(cx);
3366                                                let agent_id_string =
3367                                                    agent_id.to_string();
3368                                                settings::update_settings_file(
3369                                                    fs,
3370                                                    cx,
3371                                                    move |settings, _| {
3372                                                        let agent_servers = settings
3373                                                            .agent_servers
3374                                                            .get_or_insert_default();
3375                                                        agent_servers.entry(agent_id_string).or_insert_with(|| {
3376                                                            settings::CustomAgentServerSettings::Registry {
3377                                                                default_mode: None,
3378                                                                default_model: None,
3379                                                                env: Default::default(),
3380                                                                favorite_models: Vec::new(),
3381                                                                default_config_options: Default::default(),
3382                                                                favorite_config_option_values: Default::default(),
3383                                                            }
3384                                                        });
3385                                                    },
3386                                                );
3387
3388                                                if let Some(workspace) = workspace.upgrade() {
3389                                                    workspace.update(cx, |workspace, cx| {
3390                                                        if let Some(panel) =
3391                                                            workspace.panel::<AgentPanel>(cx)
3392                                                        {
3393                                                            panel.update(cx, |panel, cx| {
3394                                                                panel.new_agent_thread(
3395                                                                    AgentType::Custom {
3396                                                                        name: agent_id.0.clone(),
3397                                                                    },
3398                                                                    window,
3399                                                                    cx,
3400                                                                );
3401                                                            });
3402                                                        }
3403                                                    });
3404                                                }
3405                                            }
3406                                        });
3407
3408                                    menu = menu.item(entry);
3409                                }
3410
3411                                menu
3412                            })
3413                            .item(
3414                                ContextMenuEntry::new("Add More Agents")
3415                                    .icon(IconName::Plus)
3416                                    .icon_color(Color::Muted)
3417                                    .handler({
3418                                        move |window, cx| {
3419                                            window.dispatch_action(
3420                                                Box::new(zed_actions::AcpRegistry),
3421                                                cx,
3422                                            )
3423                                        }
3424                                    }),
3425                            )
3426                    }))
3427                }
3428            });
3429
3430        let is_thread_loading = self
3431            .active_thread_view()
3432            .map(|thread| thread.read(cx).is_loading())
3433            .unwrap_or(false);
3434
3435        let has_custom_icon = selected_agent_custom_icon.is_some();
3436
3437        let selected_agent = div()
3438            .id("selected_agent_icon")
3439            .when_some(selected_agent_custom_icon, |this, icon_path| {
3440                this.px_1()
3441                    .child(Icon::from_external_svg(icon_path).color(Color::Muted))
3442            })
3443            .when(!has_custom_icon, |this| {
3444                this.when_some(self.selected_agent.icon(), |this, icon| {
3445                    this.px_1().child(Icon::new(icon).color(Color::Muted))
3446                })
3447            })
3448            .tooltip(move |_, cx| {
3449                Tooltip::with_meta(selected_agent_label.clone(), None, "Selected Agent", cx)
3450            });
3451
3452        let selected_agent = if is_thread_loading {
3453            selected_agent
3454                .with_animation(
3455                    "pulsating-icon",
3456                    Animation::new(Duration::from_secs(1))
3457                        .repeat()
3458                        .with_easing(pulsating_between(0.2, 0.6)),
3459                    |icon, delta| icon.opacity(delta),
3460                )
3461                .into_any_element()
3462        } else {
3463            selected_agent.into_any_element()
3464        };
3465
3466        let show_history_menu = self.history_kind_for_selected_agent(cx).is_some();
3467        let has_v2_flag = cx.has_flag::<AgentV2FeatureFlag>();
3468
3469        h_flex()
3470            .id("agent-panel-toolbar")
3471            .h(Tab::container_height(cx))
3472            .max_w_full()
3473            .flex_none()
3474            .justify_between()
3475            .gap_2()
3476            .bg(cx.theme().colors().tab_bar_background)
3477            .border_b_1()
3478            .border_color(cx.theme().colors().border)
3479            .child(
3480                h_flex()
3481                    .size_full()
3482                    .gap(DynamicSpacing::Base04.rems(cx))
3483                    .pl(DynamicSpacing::Base04.rems(cx))
3484                    .child(match &self.active_view {
3485                        ActiveView::History { .. } | ActiveView::Configuration => {
3486                            self.render_toolbar_back_button(cx).into_any_element()
3487                        }
3488                        _ => selected_agent.into_any_element(),
3489                    })
3490                    .child(self.render_title_view(window, cx)),
3491            )
3492            .child(
3493                h_flex()
3494                    .flex_none()
3495                    .gap(DynamicSpacing::Base02.rems(cx))
3496                    .pl(DynamicSpacing::Base04.rems(cx))
3497                    .pr(DynamicSpacing::Base06.rems(cx))
3498                    .when(
3499                        has_v2_flag
3500                            && cx.has_flag::<AgentGitWorktreesFeatureFlag>()
3501                            && !self.active_thread_has_messages(cx),
3502                        |this| this.child(self.render_start_thread_in_selector(cx)),
3503                    )
3504                    .child(new_thread_menu)
3505                    .when(show_history_menu, |this| {
3506                        this.child(self.render_recent_entries_menu(
3507                            IconName::MenuAltTemp,
3508                            Corner::TopRight,
3509                            cx,
3510                        ))
3511                    })
3512                    .child(self.render_panel_options_menu(window, cx)),
3513            )
3514    }
3515
3516    fn render_worktree_creation_status(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
3517        let status = self.worktree_creation_status.as_ref()?;
3518        match status {
3519            WorktreeCreationStatus::Creating => Some(
3520                h_flex()
3521                    .w_full()
3522                    .px(DynamicSpacing::Base06.rems(cx))
3523                    .py(DynamicSpacing::Base02.rems(cx))
3524                    .gap_2()
3525                    .bg(cx.theme().colors().surface_background)
3526                    .border_b_1()
3527                    .border_color(cx.theme().colors().border)
3528                    .child(SpinnerLabel::new().size(LabelSize::Small))
3529                    .child(
3530                        Label::new("Creating worktree…")
3531                            .color(Color::Muted)
3532                            .size(LabelSize::Small),
3533                    )
3534                    .into_any_element(),
3535            ),
3536            WorktreeCreationStatus::Error(message) => Some(
3537                h_flex()
3538                    .w_full()
3539                    .px(DynamicSpacing::Base06.rems(cx))
3540                    .py(DynamicSpacing::Base02.rems(cx))
3541                    .gap_2()
3542                    .bg(cx.theme().colors().surface_background)
3543                    .border_b_1()
3544                    .border_color(cx.theme().colors().border)
3545                    .child(
3546                        Icon::new(IconName::Warning)
3547                            .size(IconSize::Small)
3548                            .color(Color::Warning),
3549                    )
3550                    .child(
3551                        Label::new(message.clone())
3552                            .color(Color::Warning)
3553                            .size(LabelSize::Small)
3554                            .truncate(),
3555                    )
3556                    .into_any_element(),
3557            ),
3558        }
3559    }
3560
3561    fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
3562        if TrialEndUpsell::dismissed() {
3563            return false;
3564        }
3565
3566        match &self.active_view {
3567            ActiveView::TextThread { .. } => {
3568                if LanguageModelRegistry::global(cx)
3569                    .read(cx)
3570                    .default_model()
3571                    .is_some_and(|model| {
3572                        model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
3573                    })
3574                {
3575                    return false;
3576                }
3577            }
3578            ActiveView::Uninitialized
3579            | ActiveView::AgentThread { .. }
3580            | ActiveView::History { .. }
3581            | ActiveView::Configuration => return false,
3582        }
3583
3584        let plan = self.user_store.read(cx).plan();
3585        let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
3586
3587        plan.is_some_and(|plan| plan == Plan::ZedFree) && has_previous_trial
3588    }
3589
3590    fn should_render_onboarding(&self, cx: &mut Context<Self>) -> bool {
3591        if self.on_boarding_upsell_dismissed.load(Ordering::Acquire) {
3592            return false;
3593        }
3594
3595        let user_store = self.user_store.read(cx);
3596
3597        if user_store.plan().is_some_and(|plan| plan == Plan::ZedPro)
3598            && user_store
3599                .subscription_period()
3600                .and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
3601                .is_some_and(|date| date < chrono::Utc::now())
3602        {
3603            OnboardingUpsell::set_dismissed(true, cx);
3604            self.on_boarding_upsell_dismissed
3605                .store(true, Ordering::Release);
3606            return false;
3607        }
3608
3609        match &self.active_view {
3610            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
3611                false
3612            }
3613            ActiveView::AgentThread { server_view, .. }
3614                if server_view.read(cx).as_native_thread(cx).is_none() =>
3615            {
3616                false
3617            }
3618            _ => {
3619                let history_is_empty = self.acp_history.read(cx).is_empty();
3620
3621                let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
3622                    .visible_providers()
3623                    .iter()
3624                    .any(|provider| {
3625                        provider.is_authenticated(cx)
3626                            && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
3627                    });
3628
3629                history_is_empty || !has_configured_non_zed_providers
3630            }
3631        }
3632    }
3633
3634    fn render_onboarding(
3635        &self,
3636        _window: &mut Window,
3637        cx: &mut Context<Self>,
3638    ) -> Option<impl IntoElement> {
3639        if !self.should_render_onboarding(cx) {
3640            return None;
3641        }
3642
3643        let text_thread_view = matches!(&self.active_view, ActiveView::TextThread { .. });
3644
3645        Some(
3646            div()
3647                .when(text_thread_view, |this| {
3648                    this.bg(cx.theme().colors().editor_background)
3649                })
3650                .child(self.onboarding.clone()),
3651        )
3652    }
3653
3654    fn render_trial_end_upsell(
3655        &self,
3656        _window: &mut Window,
3657        cx: &mut Context<Self>,
3658    ) -> Option<impl IntoElement> {
3659        if !self.should_render_trial_end_upsell(cx) {
3660            return None;
3661        }
3662
3663        Some(
3664            v_flex()
3665                .absolute()
3666                .inset_0()
3667                .size_full()
3668                .bg(cx.theme().colors().panel_background)
3669                .opacity(0.85)
3670                .block_mouse_except_scroll()
3671                .child(EndTrialUpsell::new(Arc::new({
3672                    let this = cx.entity();
3673                    move |_, cx| {
3674                        this.update(cx, |_this, cx| {
3675                            TrialEndUpsell::set_dismissed(true, cx);
3676                            cx.notify();
3677                        });
3678                    }
3679                }))),
3680        )
3681    }
3682
3683    fn emit_configuration_error_telemetry_if_needed(
3684        &mut self,
3685        configuration_error: Option<&ConfigurationError>,
3686    ) {
3687        let error_kind = configuration_error.map(|err| match err {
3688            ConfigurationError::NoProvider => "no_provider",
3689            ConfigurationError::ModelNotFound => "model_not_found",
3690            ConfigurationError::ProviderNotAuthenticated(_) => "provider_not_authenticated",
3691        });
3692
3693        let error_kind_string = error_kind.map(String::from);
3694
3695        if self.last_configuration_error_telemetry == error_kind_string {
3696            return;
3697        }
3698
3699        self.last_configuration_error_telemetry = error_kind_string;
3700
3701        if let Some(kind) = error_kind {
3702            let message = configuration_error
3703                .map(|err| err.to_string())
3704                .unwrap_or_default();
3705
3706            telemetry::event!("Agent Panel Error Shown", kind = kind, message = message,);
3707        }
3708    }
3709
3710    fn render_configuration_error(
3711        &self,
3712        border_bottom: bool,
3713        configuration_error: &ConfigurationError,
3714        focus_handle: &FocusHandle,
3715        cx: &mut App,
3716    ) -> impl IntoElement {
3717        let zed_provider_configured = AgentSettings::get_global(cx)
3718            .default_model
3719            .as_ref()
3720            .is_some_and(|selection| selection.provider.0.as_str() == "zed.dev");
3721
3722        let callout = if zed_provider_configured {
3723            Callout::new()
3724                .icon(IconName::Warning)
3725                .severity(Severity::Warning)
3726                .when(border_bottom, |this| {
3727                    this.border_position(ui::BorderPosition::Bottom)
3728                })
3729                .title("Sign in to continue using Zed as your LLM provider.")
3730                .actions_slot(
3731                    Button::new("sign_in", "Sign In")
3732                        .style(ButtonStyle::Tinted(ui::TintColor::Warning))
3733                        .label_size(LabelSize::Small)
3734                        .on_click({
3735                            let workspace = self.workspace.clone();
3736                            move |_, _, cx| {
3737                                let Ok(client) =
3738                                    workspace.update(cx, |workspace, _| workspace.client().clone())
3739                                else {
3740                                    return;
3741                                };
3742
3743                                cx.spawn(async move |cx| {
3744                                    client.sign_in_with_optional_connect(true, cx).await
3745                                })
3746                                .detach_and_log_err(cx);
3747                            }
3748                        }),
3749                )
3750        } else {
3751            Callout::new()
3752                .icon(IconName::Warning)
3753                .severity(Severity::Warning)
3754                .when(border_bottom, |this| {
3755                    this.border_position(ui::BorderPosition::Bottom)
3756                })
3757                .title(configuration_error.to_string())
3758                .actions_slot(
3759                    Button::new("settings", "Configure")
3760                        .style(ButtonStyle::Tinted(ui::TintColor::Warning))
3761                        .label_size(LabelSize::Small)
3762                        .key_binding(
3763                            KeyBinding::for_action_in(&OpenSettings, focus_handle, cx)
3764                                .map(|kb| kb.size(rems_from_px(12.))),
3765                        )
3766                        .on_click(|_event, window, cx| {
3767                            window.dispatch_action(OpenSettings.boxed_clone(), cx)
3768                        }),
3769                )
3770        };
3771
3772        match configuration_error {
3773            ConfigurationError::ModelNotFound
3774            | ConfigurationError::ProviderNotAuthenticated(_)
3775            | ConfigurationError::NoProvider => callout.into_any_element(),
3776        }
3777    }
3778
3779    fn render_text_thread(
3780        &self,
3781        text_thread_editor: &Entity<TextThreadEditor>,
3782        buffer_search_bar: &Entity<BufferSearchBar>,
3783        window: &mut Window,
3784        cx: &mut Context<Self>,
3785    ) -> Div {
3786        let mut registrar = buffer_search::DivRegistrar::new(
3787            |this, _, _cx| match &this.active_view {
3788                ActiveView::TextThread {
3789                    buffer_search_bar, ..
3790                } => Some(buffer_search_bar.clone()),
3791                _ => None,
3792            },
3793            cx,
3794        );
3795        BufferSearchBar::register(&mut registrar);
3796        registrar
3797            .into_div()
3798            .size_full()
3799            .relative()
3800            .map(|parent| {
3801                buffer_search_bar.update(cx, |buffer_search_bar, cx| {
3802                    if buffer_search_bar.is_dismissed() {
3803                        return parent;
3804                    }
3805                    parent.child(
3806                        div()
3807                            .p(DynamicSpacing::Base08.rems(cx))
3808                            .border_b_1()
3809                            .border_color(cx.theme().colors().border_variant)
3810                            .bg(cx.theme().colors().editor_background)
3811                            .child(buffer_search_bar.render(window, cx)),
3812                    )
3813                })
3814            })
3815            .child(text_thread_editor.clone())
3816            .child(self.render_drag_target(cx))
3817    }
3818
3819    fn render_drag_target(&self, cx: &Context<Self>) -> Div {
3820        let is_local = self.project.read(cx).is_local();
3821        div()
3822            .invisible()
3823            .absolute()
3824            .top_0()
3825            .right_0()
3826            .bottom_0()
3827            .left_0()
3828            .bg(cx.theme().colors().drop_target_background)
3829            .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
3830            .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
3831            .when(is_local, |this| {
3832                this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
3833            })
3834            .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
3835                let item = tab.pane.read(cx).item_for_index(tab.ix);
3836                let project_paths = item
3837                    .and_then(|item| item.project_path(cx))
3838                    .into_iter()
3839                    .collect::<Vec<_>>();
3840                this.handle_drop(project_paths, vec![], window, cx);
3841            }))
3842            .on_drop(
3843                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
3844                    let project_paths = selection
3845                        .items()
3846                        .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
3847                        .collect::<Vec<_>>();
3848                    this.handle_drop(project_paths, vec![], window, cx);
3849                }),
3850            )
3851            .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
3852                let tasks = paths
3853                    .paths()
3854                    .iter()
3855                    .map(|path| {
3856                        Workspace::project_path_for_path(this.project.clone(), path, false, cx)
3857                    })
3858                    .collect::<Vec<_>>();
3859                cx.spawn_in(window, async move |this, cx| {
3860                    let mut paths = vec![];
3861                    let mut added_worktrees = vec![];
3862                    let opened_paths = futures::future::join_all(tasks).await;
3863                    for entry in opened_paths {
3864                        if let Some((worktree, project_path)) = entry.log_err() {
3865                            added_worktrees.push(worktree);
3866                            paths.push(project_path);
3867                        }
3868                    }
3869                    this.update_in(cx, |this, window, cx| {
3870                        this.handle_drop(paths, added_worktrees, window, cx);
3871                    })
3872                    .ok();
3873                })
3874                .detach();
3875            }))
3876    }
3877
3878    fn handle_drop(
3879        &mut self,
3880        paths: Vec<ProjectPath>,
3881        added_worktrees: Vec<Entity<Worktree>>,
3882        window: &mut Window,
3883        cx: &mut Context<Self>,
3884    ) {
3885        match &self.active_view {
3886            ActiveView::AgentThread { server_view } => {
3887                server_view.update(cx, |thread_view, cx| {
3888                    thread_view.insert_dragged_files(paths, added_worktrees, window, cx);
3889                });
3890            }
3891            ActiveView::TextThread {
3892                text_thread_editor, ..
3893            } => {
3894                text_thread_editor.update(cx, |text_thread_editor, cx| {
3895                    TextThreadEditor::insert_dragged_files(
3896                        text_thread_editor,
3897                        paths,
3898                        added_worktrees,
3899                        window,
3900                        cx,
3901                    );
3902                });
3903            }
3904            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
3905        }
3906    }
3907
3908    fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
3909        if !self.show_trust_workspace_message {
3910            return None;
3911        }
3912
3913        let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
3914
3915        Some(
3916            Callout::new()
3917                .icon(IconName::Warning)
3918                .severity(Severity::Warning)
3919                .border_position(ui::BorderPosition::Bottom)
3920                .title("You're in Restricted Mode")
3921                .description(description)
3922                .actions_slot(
3923                    Button::new("open-trust-modal", "Configure Project Trust")
3924                        .label_size(LabelSize::Small)
3925                        .style(ButtonStyle::Outlined)
3926                        .on_click({
3927                            cx.listener(move |this, _, window, cx| {
3928                                this.workspace
3929                                    .update(cx, |workspace, cx| {
3930                                        workspace
3931                                            .show_worktree_trust_security_modal(true, window, cx)
3932                                    })
3933                                    .log_err();
3934                            })
3935                        }),
3936                ),
3937        )
3938    }
3939
3940    fn key_context(&self) -> KeyContext {
3941        let mut key_context = KeyContext::new_with_defaults();
3942        key_context.add("AgentPanel");
3943        match &self.active_view {
3944            ActiveView::AgentThread { .. } => key_context.add("acp_thread"),
3945            ActiveView::TextThread { .. } => key_context.add("text_thread"),
3946            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
3947        }
3948        key_context
3949    }
3950}
3951
3952impl Render for AgentPanel {
3953    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3954        // WARNING: Changes to this element hierarchy can have
3955        // non-obvious implications to the layout of children.
3956        //
3957        // If you need to change it, please confirm:
3958        // - The message editor expands (cmd-option-esc) correctly
3959        // - When expanded, the buttons at the bottom of the panel are displayed correctly
3960        // - Font size works as expected and can be changed with cmd-+/cmd-
3961        // - Scrolling in all views works as expected
3962        // - Files can be dropped into the panel
3963        let content = v_flex()
3964            .relative()
3965            .size_full()
3966            .justify_between()
3967            .key_context(self.key_context())
3968            .on_action(cx.listener(|this, action: &NewThread, window, cx| {
3969                this.new_thread(action, window, cx);
3970            }))
3971            .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
3972                this.open_history(window, cx);
3973            }))
3974            .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
3975                this.open_configuration(window, cx);
3976            }))
3977            .on_action(cx.listener(Self::open_active_thread_as_markdown))
3978            .on_action(cx.listener(Self::deploy_rules_library))
3979            .on_action(cx.listener(Self::go_back))
3980            .on_action(cx.listener(Self::toggle_navigation_menu))
3981            .on_action(cx.listener(Self::toggle_options_menu))
3982            .on_action(cx.listener(Self::increase_font_size))
3983            .on_action(cx.listener(Self::decrease_font_size))
3984            .on_action(cx.listener(Self::reset_font_size))
3985            .on_action(cx.listener(Self::toggle_zoom))
3986            .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
3987                if let Some(thread_view) = this.active_thread_view() {
3988                    thread_view.update(cx, |thread_view, cx| thread_view.reauthenticate(window, cx))
3989                }
3990            }))
3991            .child(self.render_toolbar(window, cx))
3992            .children(self.render_worktree_creation_status(cx))
3993            .children(self.render_workspace_trust_message(cx))
3994            .children(self.render_onboarding(window, cx))
3995            .map(|parent| {
3996                // Emit configuration error telemetry before entering the match to avoid borrow conflicts
3997                if matches!(&self.active_view, ActiveView::TextThread { .. }) {
3998                    let model_registry = LanguageModelRegistry::read_global(cx);
3999                    let configuration_error =
4000                        model_registry.configuration_error(model_registry.default_model(), cx);
4001                    self.emit_configuration_error_telemetry_if_needed(configuration_error.as_ref());
4002                }
4003
4004                match &self.active_view {
4005                    ActiveView::Uninitialized => parent,
4006                    ActiveView::AgentThread { server_view, .. } => parent
4007                        .child(server_view.clone())
4008                        .child(self.render_drag_target(cx)),
4009                    ActiveView::History { kind } => match kind {
4010                        HistoryKind::AgentThreads => parent.child(self.acp_history.clone()),
4011                        HistoryKind::TextThreads => parent.child(self.text_thread_history.clone()),
4012                    },
4013                    ActiveView::TextThread {
4014                        text_thread_editor,
4015                        buffer_search_bar,
4016                        ..
4017                    } => {
4018                        let model_registry = LanguageModelRegistry::read_global(cx);
4019                        let configuration_error =
4020                            model_registry.configuration_error(model_registry.default_model(), cx);
4021
4022                        parent
4023                            .map(|this| {
4024                                if !self.should_render_onboarding(cx)
4025                                    && let Some(err) = configuration_error.as_ref()
4026                                {
4027                                    this.child(self.render_configuration_error(
4028                                        true,
4029                                        err,
4030                                        &self.focus_handle(cx),
4031                                        cx,
4032                                    ))
4033                                } else {
4034                                    this
4035                                }
4036                            })
4037                            .child(self.render_text_thread(
4038                                text_thread_editor,
4039                                buffer_search_bar,
4040                                window,
4041                                cx,
4042                            ))
4043                    }
4044                    ActiveView::Configuration => parent.children(self.configuration.clone()),
4045                }
4046            })
4047            .children(self.render_trial_end_upsell(window, cx));
4048
4049        match self.active_view.which_font_size_used() {
4050            WhichFontSize::AgentFont => {
4051                WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
4052                    .size_full()
4053                    .child(content)
4054                    .into_any()
4055            }
4056            _ => content.into_any(),
4057        }
4058    }
4059}
4060
4061struct PromptLibraryInlineAssist {
4062    workspace: WeakEntity<Workspace>,
4063}
4064
4065impl PromptLibraryInlineAssist {
4066    pub fn new(workspace: WeakEntity<Workspace>) -> Self {
4067        Self { workspace }
4068    }
4069}
4070
4071impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
4072    fn assist(
4073        &self,
4074        prompt_editor: &Entity<Editor>,
4075        initial_prompt: Option<String>,
4076        window: &mut Window,
4077        cx: &mut Context<RulesLibrary>,
4078    ) {
4079        InlineAssistant::update_global(cx, |assistant, cx| {
4080            let Some(workspace) = self.workspace.upgrade() else {
4081                return;
4082            };
4083            let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
4084                return;
4085            };
4086            let project = workspace.read(cx).project().downgrade();
4087            let panel = panel.read(cx);
4088            let thread_store = panel.thread_store().clone();
4089            let history = panel.history().downgrade();
4090            assistant.assist(
4091                prompt_editor,
4092                self.workspace.clone(),
4093                project,
4094                thread_store,
4095                None,
4096                history,
4097                initial_prompt,
4098                window,
4099                cx,
4100            );
4101        })
4102    }
4103
4104    fn focus_agent_panel(
4105        &self,
4106        workspace: &mut Workspace,
4107        window: &mut Window,
4108        cx: &mut Context<Workspace>,
4109    ) -> bool {
4110        workspace.focus_panel::<AgentPanel>(window, cx).is_some()
4111    }
4112}
4113
4114pub struct ConcreteAssistantPanelDelegate;
4115
4116impl AgentPanelDelegate for ConcreteAssistantPanelDelegate {
4117    fn active_text_thread_editor(
4118        &self,
4119        workspace: &mut Workspace,
4120        _window: &mut Window,
4121        cx: &mut Context<Workspace>,
4122    ) -> Option<Entity<TextThreadEditor>> {
4123        let panel = workspace.panel::<AgentPanel>(cx)?;
4124        panel.read(cx).active_text_thread_editor()
4125    }
4126
4127    fn open_local_text_thread(
4128        &self,
4129        workspace: &mut Workspace,
4130        path: Arc<Path>,
4131        window: &mut Window,
4132        cx: &mut Context<Workspace>,
4133    ) -> Task<Result<()>> {
4134        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
4135            return Task::ready(Err(anyhow!("Agent panel not found")));
4136        };
4137
4138        panel.update(cx, |panel, cx| {
4139            panel.open_saved_text_thread(path, window, cx)
4140        })
4141    }
4142
4143    fn open_remote_text_thread(
4144        &self,
4145        _workspace: &mut Workspace,
4146        _text_thread_id: assistant_text_thread::TextThreadId,
4147        _window: &mut Window,
4148        _cx: &mut Context<Workspace>,
4149    ) -> Task<Result<Entity<TextThreadEditor>>> {
4150        Task::ready(Err(anyhow!("opening remote context not implemented")))
4151    }
4152
4153    fn quote_selection(
4154        &self,
4155        workspace: &mut Workspace,
4156        selection_ranges: Vec<Range<Anchor>>,
4157        buffer: Entity<MultiBuffer>,
4158        window: &mut Window,
4159        cx: &mut Context<Workspace>,
4160    ) {
4161        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
4162            return;
4163        };
4164
4165        if !panel.focus_handle(cx).contains_focused(window, cx) {
4166            workspace.toggle_panel_focus::<AgentPanel>(window, cx);
4167        }
4168
4169        panel.update(cx, |_, cx| {
4170            // Wait to create a new context until the workspace is no longer
4171            // being updated.
4172            cx.defer_in(window, move |panel, window, cx| {
4173                if let Some(thread_view) = panel.active_thread_view() {
4174                    thread_view.update(cx, |thread_view, cx| {
4175                        thread_view.insert_selections(window, cx);
4176                    });
4177                } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
4178                    let snapshot = buffer.read(cx).snapshot(cx);
4179                    let selection_ranges = selection_ranges
4180                        .into_iter()
4181                        .map(|range| range.to_point(&snapshot))
4182                        .collect::<Vec<_>>();
4183
4184                    text_thread_editor.update(cx, |text_thread_editor, cx| {
4185                        text_thread_editor.quote_ranges(selection_ranges, snapshot, window, cx)
4186                    });
4187                }
4188            });
4189        });
4190    }
4191
4192    fn quote_terminal_text(
4193        &self,
4194        workspace: &mut Workspace,
4195        text: String,
4196        window: &mut Window,
4197        cx: &mut Context<Workspace>,
4198    ) {
4199        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
4200            return;
4201        };
4202
4203        if !panel.focus_handle(cx).contains_focused(window, cx) {
4204            workspace.toggle_panel_focus::<AgentPanel>(window, cx);
4205        }
4206
4207        panel.update(cx, |_, cx| {
4208            // Wait to create a new context until the workspace is no longer
4209            // being updated.
4210            cx.defer_in(window, move |panel, window, cx| {
4211                if let Some(thread_view) = panel.active_thread_view() {
4212                    thread_view.update(cx, |thread_view, cx| {
4213                        thread_view.insert_terminal_text(text, window, cx);
4214                    });
4215                } else if let Some(text_thread_editor) = panel.active_text_thread_editor() {
4216                    text_thread_editor.update(cx, |text_thread_editor, cx| {
4217                        text_thread_editor.quote_terminal_text(text, window, cx)
4218                    });
4219                }
4220            });
4221        });
4222    }
4223}
4224
4225struct OnboardingUpsell;
4226
4227impl Dismissable for OnboardingUpsell {
4228    const KEY: &'static str = "dismissed-trial-upsell";
4229}
4230
4231struct TrialEndUpsell;
4232
4233impl Dismissable for TrialEndUpsell {
4234    const KEY: &'static str = "dismissed-trial-end-upsell";
4235}
4236
4237/// Test-only helper methods
4238#[cfg(any(test, feature = "test-support"))]
4239impl AgentPanel {
4240    /// Opens an external thread using an arbitrary AgentServer.
4241    ///
4242    /// This is a test-only helper that allows visual tests and integration tests
4243    /// to inject a stub server without modifying production code paths.
4244    /// Not compiled into production builds.
4245    pub fn open_external_thread_with_server(
4246        &mut self,
4247        server: Rc<dyn AgentServer>,
4248        window: &mut Window,
4249        cx: &mut Context<Self>,
4250    ) {
4251        let workspace = self.workspace.clone();
4252        let project = self.project.clone();
4253
4254        let ext_agent = ExternalAgent::Custom {
4255            name: server.name(),
4256        };
4257
4258        self.create_external_thread(
4259            server, None, None, workspace, project, ext_agent, window, cx,
4260        );
4261    }
4262
4263    /// Returns the currently active thread view, if any.
4264    ///
4265    /// This is a test-only accessor that exposes the private `active_thread_view()`
4266    /// method for test assertions. Not compiled into production builds.
4267    pub fn active_thread_view_for_tests(&self) -> Option<&Entity<ConnectionView>> {
4268        self.active_thread_view()
4269    }
4270
4271    /// Sets the start_thread_in value directly, bypassing validation.
4272    ///
4273    /// This is a test-only helper for visual tests that need to show specific
4274    /// start_thread_in states without requiring a real git repository.
4275    pub fn set_start_thread_in_for_tests(&mut self, target: StartThreadIn, cx: &mut Context<Self>) {
4276        self.start_thread_in = target;
4277        cx.notify();
4278    }
4279
4280    /// Returns the current worktree creation status.
4281    ///
4282    /// This is a test-only helper for visual tests.
4283    pub fn worktree_creation_status_for_tests(&self) -> Option<&WorktreeCreationStatus> {
4284        self.worktree_creation_status.as_ref()
4285    }
4286
4287    /// Sets the worktree creation status directly.
4288    ///
4289    /// This is a test-only helper for visual tests that need to show the
4290    /// "Creating worktree…" spinner or error banners.
4291    pub fn set_worktree_creation_status_for_tests(
4292        &mut self,
4293        status: Option<WorktreeCreationStatus>,
4294        cx: &mut Context<Self>,
4295    ) {
4296        self.worktree_creation_status = status;
4297        cx.notify();
4298    }
4299
4300    /// Opens the history view.
4301    ///
4302    /// This is a test-only helper that exposes the private `open_history()`
4303    /// method for visual tests.
4304    pub fn open_history_for_tests(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4305        self.open_history(window, cx);
4306    }
4307
4308    /// Opens the start_thread_in selector popover menu.
4309    ///
4310    /// This is a test-only helper for visual tests.
4311    pub fn open_start_thread_in_menu_for_tests(
4312        &mut self,
4313        window: &mut Window,
4314        cx: &mut Context<Self>,
4315    ) {
4316        self.start_thread_in_menu_handle.show(window, cx);
4317    }
4318
4319    /// Dismisses the start_thread_in dropdown menu.
4320    ///
4321    /// This is a test-only helper for visual tests.
4322    pub fn close_start_thread_in_menu_for_tests(&mut self, cx: &mut Context<Self>) {
4323        self.start_thread_in_menu_handle.hide(cx);
4324    }
4325}
4326
4327#[cfg(test)]
4328mod tests {
4329    use super::*;
4330    use crate::connection_view::tests::{StubAgentServer, init_test};
4331    use assistant_text_thread::TextThreadStore;
4332    use feature_flags::FeatureFlagAppExt;
4333    use fs::FakeFs;
4334    use gpui::{TestAppContext, VisualTestContext};
4335    use project::Project;
4336    use serde_json::json;
4337    use workspace::MultiWorkspace;
4338
4339    #[gpui::test]
4340    async fn test_active_thread_serialize_and_load_round_trip(cx: &mut TestAppContext) {
4341        init_test(cx);
4342        cx.update(|cx| {
4343            cx.update_flags(true, vec!["agent-v2".to_string()]);
4344            agent::ThreadStore::init_global(cx);
4345            language_model::LanguageModelRegistry::test(cx);
4346        });
4347
4348        // --- Create a MultiWorkspace window with two workspaces ---
4349        let fs = FakeFs::new(cx.executor());
4350        let project_a = Project::test(fs.clone(), [], cx).await;
4351        let project_b = Project::test(fs, [], cx).await;
4352
4353        let multi_workspace =
4354            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
4355
4356        let workspace_a = multi_workspace
4357            .read_with(cx, |multi_workspace, _cx| {
4358                multi_workspace.workspace().clone()
4359            })
4360            .unwrap();
4361
4362        let workspace_b = multi_workspace
4363            .update(cx, |multi_workspace, window, cx| {
4364                multi_workspace.test_add_workspace(project_b.clone(), window, cx)
4365            })
4366            .unwrap();
4367
4368        workspace_a.update(cx, |workspace, _cx| {
4369            workspace.set_random_database_id();
4370        });
4371        workspace_b.update(cx, |workspace, _cx| {
4372            workspace.set_random_database_id();
4373        });
4374
4375        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4376
4377        // --- Set up workspace A: width=300, with an active thread ---
4378        let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
4379            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_a.clone(), cx));
4380            cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
4381        });
4382
4383        panel_a.update(cx, |panel, _cx| {
4384            panel.width = Some(px(300.0));
4385        });
4386
4387        panel_a.update_in(cx, |panel, window, cx| {
4388            panel.open_external_thread_with_server(
4389                Rc::new(StubAgentServer::default_response()),
4390                window,
4391                cx,
4392            );
4393        });
4394
4395        cx.run_until_parked();
4396
4397        panel_a.read_with(cx, |panel, cx| {
4398            assert!(
4399                panel.active_agent_thread(cx).is_some(),
4400                "workspace A should have an active thread after connection"
4401            );
4402        });
4403
4404        let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent.clone());
4405
4406        // --- Set up workspace B: ClaudeCode, width=400, no active thread ---
4407        let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
4408            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_b.clone(), cx));
4409            cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
4410        });
4411
4412        panel_b.update(cx, |panel, _cx| {
4413            panel.width = Some(px(400.0));
4414            panel.selected_agent = AgentType::Custom {
4415                name: "claude-acp".into(),
4416            };
4417        });
4418
4419        // --- Serialize both panels ---
4420        panel_a.update(cx, |panel, cx| panel.serialize(cx));
4421        panel_b.update(cx, |panel, cx| panel.serialize(cx));
4422        cx.run_until_parked();
4423
4424        // --- Load fresh panels for each workspace and verify independent state ---
4425        let prompt_builder = Arc::new(prompt_store::PromptBuilder::new(None).unwrap());
4426
4427        let async_cx = cx.update(|window, cx| window.to_async(cx));
4428        let loaded_a = AgentPanel::load(workspace_a.downgrade(), prompt_builder.clone(), async_cx)
4429            .await
4430            .expect("panel A load should succeed");
4431        cx.run_until_parked();
4432
4433        let async_cx = cx.update(|window, cx| window.to_async(cx));
4434        let loaded_b = AgentPanel::load(workspace_b.downgrade(), prompt_builder.clone(), async_cx)
4435            .await
4436            .expect("panel B load should succeed");
4437        cx.run_until_parked();
4438
4439        // Workspace A should restore its thread, width, and agent type
4440        loaded_a.read_with(cx, |panel, _cx| {
4441            assert_eq!(
4442                panel.width,
4443                Some(px(300.0)),
4444                "workspace A width should be restored"
4445            );
4446            assert_eq!(
4447                panel.selected_agent, agent_type_a,
4448                "workspace A agent type should be restored"
4449            );
4450            assert!(
4451                panel.active_thread_view().is_some(),
4452                "workspace A should have its active thread restored"
4453            );
4454        });
4455
4456        // Workspace B should restore its own width and agent type, with no thread
4457        loaded_b.read_with(cx, |panel, _cx| {
4458            assert_eq!(
4459                panel.width,
4460                Some(px(400.0)),
4461                "workspace B width should be restored"
4462            );
4463            assert_eq!(
4464                panel.selected_agent,
4465                AgentType::Custom {
4466                    name: "claude-acp".into()
4467                },
4468                "workspace B agent type should be restored"
4469            );
4470            assert!(
4471                panel.active_thread_view().is_none(),
4472                "workspace B should have no active thread"
4473            );
4474        });
4475    }
4476
4477    // Simple regression test
4478    #[gpui::test]
4479    async fn test_new_text_thread_action_handler(cx: &mut TestAppContext) {
4480        init_test(cx);
4481
4482        let fs = FakeFs::new(cx.executor());
4483
4484        cx.update(|cx| {
4485            cx.update_flags(true, vec!["agent-v2".to_string()]);
4486            agent::ThreadStore::init_global(cx);
4487            language_model::LanguageModelRegistry::test(cx);
4488            let slash_command_registry =
4489                assistant_slash_command::SlashCommandRegistry::default_global(cx);
4490            slash_command_registry
4491                .register_command(assistant_slash_commands::DefaultSlashCommand, false);
4492            <dyn fs::Fs>::set_global(fs.clone(), cx);
4493        });
4494
4495        let project = Project::test(fs.clone(), [], cx).await;
4496
4497        let multi_workspace =
4498            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4499
4500        let workspace_a = multi_workspace
4501            .read_with(cx, |multi_workspace, _cx| {
4502                multi_workspace.workspace().clone()
4503            })
4504            .unwrap();
4505
4506        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4507
4508        workspace_a.update_in(cx, |workspace, window, cx| {
4509            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
4510            let panel =
4511                cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
4512            workspace.add_panel(panel, window, cx);
4513        });
4514
4515        cx.run_until_parked();
4516
4517        workspace_a.update_in(cx, |_, window, cx| {
4518            window.dispatch_action(NewTextThread.boxed_clone(), cx);
4519        });
4520
4521        cx.run_until_parked();
4522    }
4523
4524    #[gpui::test]
4525    async fn test_thread_target_local_project(cx: &mut TestAppContext) {
4526        init_test(cx);
4527        cx.update(|cx| {
4528            cx.update_flags(true, vec!["agent-v2".to_string()]);
4529            agent::ThreadStore::init_global(cx);
4530            language_model::LanguageModelRegistry::test(cx);
4531        });
4532
4533        let fs = FakeFs::new(cx.executor());
4534        fs.insert_tree(
4535            "/project",
4536            json!({
4537                ".git": {},
4538                "src": {
4539                    "main.rs": "fn main() {}"
4540                }
4541            }),
4542        )
4543        .await;
4544        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
4545
4546        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
4547
4548        let multi_workspace =
4549            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4550
4551        let workspace = multi_workspace
4552            .read_with(cx, |multi_workspace, _cx| {
4553                multi_workspace.workspace().clone()
4554            })
4555            .unwrap();
4556
4557        workspace.update(cx, |workspace, _cx| {
4558            workspace.set_random_database_id();
4559        });
4560
4561        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4562
4563        // Wait for the project to discover the git repository.
4564        cx.run_until_parked();
4565
4566        let panel = workspace.update_in(cx, |workspace, window, cx| {
4567            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
4568            let panel =
4569                cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
4570            workspace.add_panel(panel.clone(), window, cx);
4571            panel
4572        });
4573
4574        cx.run_until_parked();
4575
4576        // Default thread target should be LocalProject.
4577        panel.read_with(cx, |panel, _cx| {
4578            assert_eq!(
4579                *panel.start_thread_in(),
4580                StartThreadIn::LocalProject,
4581                "default thread target should be LocalProject"
4582            );
4583        });
4584
4585        // Start a new thread with the default LocalProject target.
4586        // Use StubAgentServer so the thread connects immediately in tests.
4587        panel.update_in(cx, |panel, window, cx| {
4588            panel.open_external_thread_with_server(
4589                Rc::new(StubAgentServer::default_response()),
4590                window,
4591                cx,
4592            );
4593        });
4594
4595        cx.run_until_parked();
4596
4597        // MultiWorkspace should still have exactly one workspace (no worktree created).
4598        multi_workspace
4599            .read_with(cx, |multi_workspace, _cx| {
4600                assert_eq!(
4601                    multi_workspace.workspaces().len(),
4602                    1,
4603                    "LocalProject should not create a new workspace"
4604                );
4605            })
4606            .unwrap();
4607
4608        // The thread should be active in the panel.
4609        panel.read_with(cx, |panel, cx| {
4610            assert!(
4611                panel.active_agent_thread(cx).is_some(),
4612                "a thread should be running in the current workspace"
4613            );
4614        });
4615
4616        // The thread target should still be LocalProject (unchanged).
4617        panel.read_with(cx, |panel, _cx| {
4618            assert_eq!(
4619                *panel.start_thread_in(),
4620                StartThreadIn::LocalProject,
4621                "thread target should remain LocalProject"
4622            );
4623        });
4624
4625        // No worktree creation status should be set.
4626        panel.read_with(cx, |panel, _cx| {
4627            assert!(
4628                panel.worktree_creation_status.is_none(),
4629                "no worktree creation should have occurred"
4630            );
4631        });
4632    }
4633
4634    #[gpui::test]
4635    async fn test_thread_target_serialization_round_trip(cx: &mut TestAppContext) {
4636        init_test(cx);
4637        cx.update(|cx| {
4638            cx.update_flags(
4639                true,
4640                vec!["agent-v2".to_string(), "agent-git-worktrees".to_string()],
4641            );
4642            agent::ThreadStore::init_global(cx);
4643            language_model::LanguageModelRegistry::test(cx);
4644        });
4645
4646        let fs = FakeFs::new(cx.executor());
4647        fs.insert_tree(
4648            "/project",
4649            json!({
4650                ".git": {},
4651                "src": {
4652                    "main.rs": "fn main() {}"
4653                }
4654            }),
4655        )
4656        .await;
4657        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
4658
4659        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
4660
4661        let multi_workspace =
4662            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4663
4664        let workspace = multi_workspace
4665            .read_with(cx, |multi_workspace, _cx| {
4666                multi_workspace.workspace().clone()
4667            })
4668            .unwrap();
4669
4670        workspace.update(cx, |workspace, _cx| {
4671            workspace.set_random_database_id();
4672        });
4673
4674        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4675
4676        // Wait for the project to discover the git repository.
4677        cx.run_until_parked();
4678
4679        let panel = workspace.update_in(cx, |workspace, window, cx| {
4680            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
4681            let panel =
4682                cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
4683            workspace.add_panel(panel.clone(), window, cx);
4684            panel
4685        });
4686
4687        cx.run_until_parked();
4688
4689        // Default should be LocalProject.
4690        panel.read_with(cx, |panel, _cx| {
4691            assert_eq!(*panel.start_thread_in(), StartThreadIn::LocalProject);
4692        });
4693
4694        // Change thread target to NewWorktree.
4695        panel.update(cx, |panel, cx| {
4696            panel.set_start_thread_in(&StartThreadIn::NewWorktree, cx);
4697        });
4698
4699        panel.read_with(cx, |panel, _cx| {
4700            assert_eq!(
4701                *panel.start_thread_in(),
4702                StartThreadIn::NewWorktree,
4703                "thread target should be NewWorktree after set_thread_target"
4704            );
4705        });
4706
4707        // Let serialization complete.
4708        cx.run_until_parked();
4709
4710        // Load a fresh panel from the serialized data.
4711        let prompt_builder = Arc::new(prompt_store::PromptBuilder::new(None).unwrap());
4712        let async_cx = cx.update(|window, cx| window.to_async(cx));
4713        let loaded_panel =
4714            AgentPanel::load(workspace.downgrade(), prompt_builder.clone(), async_cx)
4715                .await
4716                .expect("panel load should succeed");
4717        cx.run_until_parked();
4718
4719        loaded_panel.read_with(cx, |panel, _cx| {
4720            assert_eq!(
4721                *panel.start_thread_in(),
4722                StartThreadIn::NewWorktree,
4723                "thread target should survive serialization round-trip"
4724            );
4725        });
4726    }
4727
4728    #[gpui::test]
4729    async fn test_thread_target_deserialization_falls_back_when_worktree_flag_disabled(
4730        cx: &mut TestAppContext,
4731    ) {
4732        init_test(cx);
4733        cx.update(|cx| {
4734            cx.update_flags(
4735                true,
4736                vec!["agent-v2".to_string(), "agent-git-worktrees".to_string()],
4737            );
4738            agent::ThreadStore::init_global(cx);
4739            language_model::LanguageModelRegistry::test(cx);
4740        });
4741
4742        let fs = FakeFs::new(cx.executor());
4743        fs.insert_tree(
4744            "/project",
4745            json!({
4746                ".git": {},
4747                "src": {
4748                    "main.rs": "fn main() {}"
4749                }
4750            }),
4751        )
4752        .await;
4753        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
4754
4755        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
4756
4757        let multi_workspace =
4758            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4759
4760        let workspace = multi_workspace
4761            .read_with(cx, |multi_workspace, _cx| {
4762                multi_workspace.workspace().clone()
4763            })
4764            .unwrap();
4765
4766        workspace.update(cx, |workspace, _cx| {
4767            workspace.set_random_database_id();
4768        });
4769
4770        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4771
4772        // Wait for the project to discover the git repository.
4773        cx.run_until_parked();
4774
4775        let panel = workspace.update_in(cx, |workspace, window, cx| {
4776            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
4777            let panel =
4778                cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
4779            workspace.add_panel(panel.clone(), window, cx);
4780            panel
4781        });
4782
4783        cx.run_until_parked();
4784
4785        panel.update(cx, |panel, cx| {
4786            panel.set_start_thread_in(&StartThreadIn::NewWorktree, cx);
4787        });
4788
4789        panel.read_with(cx, |panel, _cx| {
4790            assert_eq!(
4791                *panel.start_thread_in(),
4792                StartThreadIn::NewWorktree,
4793                "thread target should be NewWorktree before reload"
4794            );
4795        });
4796
4797        // Let serialization complete.
4798        cx.run_until_parked();
4799
4800        // Disable worktree flag and reload panel from serialized data.
4801        cx.update(|_, cx| {
4802            cx.update_flags(true, vec!["agent-v2".to_string()]);
4803        });
4804
4805        let prompt_builder = Arc::new(prompt_store::PromptBuilder::new(None).unwrap());
4806        let async_cx = cx.update(|window, cx| window.to_async(cx));
4807        let loaded_panel =
4808            AgentPanel::load(workspace.downgrade(), prompt_builder.clone(), async_cx)
4809                .await
4810                .expect("panel load should succeed");
4811        cx.run_until_parked();
4812
4813        loaded_panel.read_with(cx, |panel, _cx| {
4814            assert_eq!(
4815                *panel.start_thread_in(),
4816                StartThreadIn::LocalProject,
4817                "thread target should fall back to LocalProject when worktree flag is disabled"
4818            );
4819        });
4820    }
4821
4822    #[gpui::test]
4823    async fn test_set_active_blocked_during_worktree_creation(cx: &mut TestAppContext) {
4824        init_test(cx);
4825
4826        let fs = FakeFs::new(cx.executor());
4827        cx.update(|cx| {
4828            cx.update_flags(true, vec!["agent-v2".to_string()]);
4829            agent::ThreadStore::init_global(cx);
4830            language_model::LanguageModelRegistry::test(cx);
4831            <dyn fs::Fs>::set_global(fs.clone(), cx);
4832        });
4833
4834        fs.insert_tree(
4835            "/project",
4836            json!({
4837                ".git": {},
4838                "src": {
4839                    "main.rs": "fn main() {}"
4840                }
4841            }),
4842        )
4843        .await;
4844
4845        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
4846
4847        let multi_workspace =
4848            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4849
4850        let workspace = multi_workspace
4851            .read_with(cx, |multi_workspace, _cx| {
4852                multi_workspace.workspace().clone()
4853            })
4854            .unwrap();
4855
4856        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4857
4858        let panel = workspace.update_in(cx, |workspace, window, cx| {
4859            let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
4860            let panel =
4861                cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx));
4862            workspace.add_panel(panel.clone(), window, cx);
4863            panel
4864        });
4865
4866        cx.run_until_parked();
4867
4868        // Simulate worktree creation in progress and reset to Uninitialized
4869        panel.update_in(cx, |panel, window, cx| {
4870            panel.worktree_creation_status = Some(WorktreeCreationStatus::Creating);
4871            panel.active_view = ActiveView::Uninitialized;
4872            Panel::set_active(panel, true, window, cx);
4873            assert!(
4874                matches!(panel.active_view, ActiveView::Uninitialized),
4875                "set_active should not create a thread while worktree is being created"
4876            );
4877        });
4878
4879        // Clear the creation status and use open_external_thread_with_server
4880        // (which bypasses new_agent_thread) to verify the panel can transition
4881        // out of Uninitialized. We can't call set_active directly because
4882        // new_agent_thread requires full agent server infrastructure.
4883        panel.update_in(cx, |panel, window, cx| {
4884            panel.worktree_creation_status = None;
4885            panel.active_view = ActiveView::Uninitialized;
4886            panel.open_external_thread_with_server(
4887                Rc::new(StubAgentServer::default_response()),
4888                window,
4889                cx,
4890            );
4891        });
4892
4893        cx.run_until_parked();
4894
4895        panel.read_with(cx, |panel, _cx| {
4896            assert!(
4897                !matches!(panel.active_view, ActiveView::Uninitialized),
4898                "panel should transition out of Uninitialized once worktree creation is cleared"
4899            );
4900        });
4901    }
4902}