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