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