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