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