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