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