agent_panel.rs

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