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