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