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