agent_panel.rs

   1use std::{
   2    path::PathBuf,
   3    rc::Rc,
   4    sync::{
   5        Arc,
   6        atomic::{AtomicBool, Ordering},
   7    },
   8    time::Duration,
   9};
  10
  11use acp_thread::{AcpThread, MentionUri, ThreadStatus};
  12use agent::{ContextServerRegistry, SharedThread, ThreadStore};
  13use agent_client_protocol as acp;
  14use agent_servers::AgentServer;
  15use collections::HashSet;
  16use db::kvp::{Dismissable, KeyValueStore};
  17use itertools::Itertools;
  18use project::AgentId;
  19use serde::{Deserialize, Serialize};
  20use settings::{LanguageModelProviderSetting, LanguageModelSelection};
  21
  22use zed_actions::agent::{
  23    AddSelectionToThread, ConflictContent, ReauthenticateAgent, ResolveConflictedFilesWithAgent,
  24    ResolveConflictsWithAgent, ReviewBranchDiff,
  25};
  26
  27use crate::thread_metadata_store::ThreadMetadataStore;
  28use crate::{
  29    AddContextServer, AgentDiffPane, ConversationView, CopyThreadToClipboard, CycleStartThreadIn,
  30    Follow, InlineAssistant, LoadThreadFromClipboard, NewThread, NewWorktreeBranchTarget,
  31    OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell,
  32    StartThreadIn, ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu,
  33    agent_configuration::{AgentConfiguration, AssistantConfigurationEvent},
  34    conversation_view::{AcpThreadViewEvent, ThreadView},
  35    thread_branch_picker::ThreadBranchPicker,
  36    thread_worktree_picker::ThreadWorktreePicker,
  37    ui::EndTrialUpsell,
  38};
  39use crate::{
  40    Agent, AgentInitialContent, ExternalSourcePrompt, NewExternalAgentThread,
  41    NewNativeAgentThreadFromSummary,
  42};
  43use crate::{DEFAULT_THREAD_TITLE, ui::AcpOnboardingModal};
  44use crate::{ExpandMessageEditor, ThreadHistoryView};
  45use crate::{ManageProfiles, ThreadHistoryViewEvent};
  46use crate::{ThreadHistory, agent_connection_store::AgentConnectionStore};
  47use agent_settings::{AgentSettings, WindowLayout};
  48use ai_onboarding::AgentPanelOnboarding;
  49use anyhow::{Context as _, Result, anyhow};
  50use client::UserStore;
  51use cloud_api_types::Plan;
  52use collections::HashMap;
  53use editor::Editor;
  54use extension::ExtensionEvents;
  55use extension_host::ExtensionStore;
  56use fs::Fs;
  57use gpui::{
  58    Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner,
  59    DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels,
  60    Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
  61};
  62use language::LanguageRegistry;
  63use language_model::LanguageModelRegistry;
  64use project::git_store::{GitStoreEvent, RepositoryEvent};
  65use project::project_settings::ProjectSettings;
  66use project::{Project, ProjectPath, Worktree, linked_worktree_short_name};
  67use prompt_store::{PromptStore, UserPromptId};
  68use remote::RemoteConnectionOptions;
  69use rules_library::{RulesLibrary, open_rules_library};
  70use settings::TerminalDockPosition;
  71use settings::{Settings, update_settings_file};
  72use terminal::terminal_settings::TerminalSettings;
  73use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
  74use theme_settings::ThemeSettings;
  75use ui::{
  76    Button, ButtonLike, Callout, CommonAnimationExt, ContextMenu, ContextMenuEntry, PopoverMenu,
  77    PopoverMenuHandle, Tab, Tooltip, prelude::*, utils::WithRemSize,
  78};
  79use util::{ResultExt as _, debug_panic};
  80use workspace::{
  81    CollaboratorId, DraggedSelection, DraggedTab, PathList, SerializedPathList,
  82    ToggleWorkspaceSidebar, ToggleZoom, Workspace, WorkspaceId,
  83    dock::{DockPosition, Panel, PanelEvent},
  84};
  85use zed_actions::{
  86    DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize,
  87    agent::{OpenAcpOnboardingModal, OpenSettings, ResetAgentZoom, ResetOnboarding},
  88    assistant::{OpenRulesLibrary, Toggle, ToggleFocus},
  89};
  90
  91const AGENT_PANEL_KEY: &str = "agent_panel";
  92const RECENTLY_UPDATED_MENU_LIMIT: usize = 6;
  93const LAST_USED_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
  94
  95#[derive(Serialize, Deserialize)]
  96struct LastUsedAgent {
  97    agent: Agent,
  98}
  99
 100/// Reads the most recently used agent across all workspaces. Used as a fallback
 101/// when opening a workspace that has no per-workspace agent preference yet.
 102fn read_global_last_used_agent(kvp: &KeyValueStore) -> Option<Agent> {
 103    kvp.read_kvp(LAST_USED_AGENT_KEY)
 104        .log_err()
 105        .flatten()
 106        .and_then(|json| serde_json::from_str::<LastUsedAgent>(&json).log_err())
 107        .map(|entry| entry.agent)
 108}
 109
 110async fn write_global_last_used_agent(kvp: KeyValueStore, agent: Agent) {
 111    if let Some(json) = serde_json::to_string(&LastUsedAgent { agent }).log_err() {
 112        kvp.write_kvp(LAST_USED_AGENT_KEY.to_string(), json)
 113            .await
 114            .log_err();
 115    }
 116}
 117
 118fn read_serialized_panel(
 119    workspace_id: workspace::WorkspaceId,
 120    kvp: &KeyValueStore,
 121) -> Option<SerializedAgentPanel> {
 122    let scope = kvp.scoped(AGENT_PANEL_KEY);
 123    let key = i64::from(workspace_id).to_string();
 124    scope
 125        .read(&key)
 126        .log_err()
 127        .flatten()
 128        .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())
 129}
 130
 131async fn save_serialized_panel(
 132    workspace_id: workspace::WorkspaceId,
 133    panel: SerializedAgentPanel,
 134    kvp: KeyValueStore,
 135) -> Result<()> {
 136    let scope = kvp.scoped(AGENT_PANEL_KEY);
 137    let key = i64::from(workspace_id).to_string();
 138    scope.write(key, serde_json::to_string(&panel)?).await?;
 139    Ok(())
 140}
 141
 142/// Migration: reads the original single-panel format stored under the
 143/// `"agent_panel"` KVP key before per-workspace keying was introduced.
 144fn read_legacy_serialized_panel(kvp: &KeyValueStore) -> Option<SerializedAgentPanel> {
 145    kvp.read_kvp(AGENT_PANEL_KEY)
 146        .log_err()
 147        .flatten()
 148        .and_then(|json| serde_json::from_str::<SerializedAgentPanel>(&json).log_err())
 149}
 150
 151#[derive(Serialize, Deserialize, Debug)]
 152struct SerializedAgentPanel {
 153    selected_agent: Option<Agent>,
 154    #[serde(default)]
 155    last_active_thread: Option<SerializedActiveThread>,
 156    #[serde(default)]
 157    start_thread_in: Option<StartThreadIn>,
 158}
 159
 160#[derive(Serialize, Deserialize, Debug)]
 161struct SerializedActiveThread {
 162    session_id: String,
 163    agent_type: Agent,
 164    title: Option<String>,
 165    work_dirs: Option<SerializedPathList>,
 166}
 167
 168pub fn init(cx: &mut App) {
 169    cx.observe_new(
 170        |workspace: &mut Workspace, _window, _cx: &mut Context<Workspace>| {
 171            workspace
 172                .register_action(|workspace, action: &NewThread, window, cx| {
 173                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 174                        panel.update(cx, |panel, cx| panel.new_thread(action, window, cx));
 175                        workspace.focus_panel::<AgentPanel>(window, cx);
 176                    }
 177                })
 178                .register_action(
 179                    |workspace, action: &NewNativeAgentThreadFromSummary, window, cx| {
 180                        if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 181                            panel.update(cx, |panel, cx| {
 182                                panel.new_native_agent_thread_from_summary(action, window, cx)
 183                            });
 184                            workspace.focus_panel::<AgentPanel>(window, cx);
 185                        }
 186                    },
 187                )
 188                .register_action(|workspace, _: &ExpandMessageEditor, window, cx| {
 189                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 190                        workspace.focus_panel::<AgentPanel>(window, cx);
 191                        panel.update(cx, |panel, cx| panel.expand_message_editor(window, cx));
 192                    }
 193                })
 194                .register_action(|workspace, _: &OpenHistory, window, cx| {
 195                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 196                        workspace.focus_panel::<AgentPanel>(window, cx);
 197                        panel.update(cx, |panel, cx| panel.open_history(window, cx));
 198                    }
 199                })
 200                .register_action(|workspace, _: &OpenSettings, window, cx| {
 201                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 202                        workspace.focus_panel::<AgentPanel>(window, cx);
 203                        panel.update(cx, |panel, cx| panel.open_configuration(window, cx));
 204                    }
 205                })
 206                .register_action(|workspace, action: &NewExternalAgentThread, window, cx| {
 207                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 208                        workspace.focus_panel::<AgentPanel>(window, cx);
 209                        panel.update(cx, |panel, cx| {
 210                            let initial_content = panel.take_active_draft_initial_content(cx);
 211                            panel.external_thread(
 212                                action.agent.clone(),
 213                                None,
 214                                None,
 215                                None,
 216                                initial_content,
 217                                true,
 218                                window,
 219                                cx,
 220                            )
 221                        });
 222                    }
 223                })
 224                .register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
 225                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 226                        workspace.focus_panel::<AgentPanel>(window, cx);
 227                        panel.update(cx, |panel, cx| {
 228                            panel.deploy_rules_library(action, window, cx)
 229                        });
 230                    }
 231                })
 232                .register_action(|workspace, _: &Follow, window, cx| {
 233                    workspace.follow(CollaboratorId::Agent, window, cx);
 234                })
 235                .register_action(|workspace, _: &OpenAgentDiff, window, cx| {
 236                    let thread = workspace
 237                        .panel::<AgentPanel>(cx)
 238                        .and_then(|panel| panel.read(cx).active_conversation_view().cloned())
 239                        .and_then(|conversation| {
 240                            conversation
 241                                .read(cx)
 242                                .active_thread()
 243                                .map(|r| r.read(cx).thread.clone())
 244                        });
 245
 246                    if let Some(thread) = thread {
 247                        AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
 248                    }
 249                })
 250                .register_action(|workspace, _: &ToggleNavigationMenu, window, cx| {
 251                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 252                        workspace.focus_panel::<AgentPanel>(window, cx);
 253                        panel.update(cx, |panel, cx| {
 254                            panel.toggle_navigation_menu(&ToggleNavigationMenu, window, cx);
 255                        });
 256                    }
 257                })
 258                .register_action(|workspace, _: &ToggleOptionsMenu, window, cx| {
 259                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 260                        workspace.focus_panel::<AgentPanel>(window, cx);
 261                        panel.update(cx, |panel, cx| {
 262                            panel.toggle_options_menu(&ToggleOptionsMenu, window, cx);
 263                        });
 264                    }
 265                })
 266                .register_action(|workspace, _: &ToggleNewThreadMenu, window, cx| {
 267                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 268                        workspace.focus_panel::<AgentPanel>(window, cx);
 269                        panel.update(cx, |panel, cx| {
 270                            panel.toggle_new_thread_menu(&ToggleNewThreadMenu, window, cx);
 271                        });
 272                    }
 273                })
 274                .register_action(|workspace, _: &OpenAcpOnboardingModal, window, cx| {
 275                    AcpOnboardingModal::toggle(workspace, window, cx)
 276                })
 277                .register_action(|_workspace, _: &ResetOnboarding, window, cx| {
 278                    window.dispatch_action(workspace::RestoreBanner.boxed_clone(), cx);
 279                    window.refresh();
 280                })
 281                .register_action(|workspace, _: &ResetTrialUpsell, _window, cx| {
 282                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 283                        panel.update(cx, |panel, _| {
 284                            panel
 285                                .new_user_onboarding_upsell_dismissed
 286                                .store(false, Ordering::Release);
 287                        });
 288                    }
 289                    OnboardingUpsell::set_dismissed(false, cx);
 290                })
 291                .register_action(|_workspace, _: &ResetTrialEndUpsell, _window, cx| {
 292                    TrialEndUpsell::set_dismissed(false, cx);
 293                })
 294                .register_action(|workspace, _: &ResetAgentZoom, window, cx| {
 295                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 296                        panel.update(cx, |panel, cx| {
 297                            panel.reset_agent_zoom(window, cx);
 298                        });
 299                    }
 300                })
 301                .register_action(|workspace, _: &CopyThreadToClipboard, window, cx| {
 302                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 303                        panel.update(cx, |panel, cx| {
 304                            panel.copy_thread_to_clipboard(window, cx);
 305                        });
 306                    }
 307                })
 308                .register_action(|workspace, _: &LoadThreadFromClipboard, window, cx| {
 309                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 310                        workspace.focus_panel::<AgentPanel>(window, cx);
 311                        panel.update(cx, |panel, cx| {
 312                            panel.load_thread_from_clipboard(window, cx);
 313                        });
 314                    }
 315                })
 316                .register_action(|workspace, action: &ReviewBranchDiff, window, cx| {
 317                    let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
 318                        return;
 319                    };
 320
 321                    let mention_uri = MentionUri::GitDiff {
 322                        base_ref: action.base_ref.to_string(),
 323                    };
 324                    let diff_uri = mention_uri.to_uri().to_string();
 325
 326                    let content_blocks = vec![
 327                        acp::ContentBlock::Text(acp::TextContent::new(
 328                            "Please review this branch diff carefully. Point out any issues, \
 329                             potential bugs, or improvement opportunities you find.\n\n"
 330                                .to_string(),
 331                        )),
 332                        acp::ContentBlock::Resource(acp::EmbeddedResource::new(
 333                            acp::EmbeddedResourceResource::TextResourceContents(
 334                                acp::TextResourceContents::new(
 335                                    action.diff_text.to_string(),
 336                                    diff_uri,
 337                                ),
 338                            ),
 339                        )),
 340                    ];
 341
 342                    workspace.focus_panel::<AgentPanel>(window, cx);
 343
 344                    panel.update(cx, |panel, cx| {
 345                        panel.external_thread(
 346                            None,
 347                            None,
 348                            None,
 349                            None,
 350                            Some(AgentInitialContent::ContentBlock {
 351                                blocks: content_blocks,
 352                                auto_submit: true,
 353                            }),
 354                            true,
 355                            window,
 356                            cx,
 357                        );
 358                    });
 359                })
 360                .register_action(
 361                    |workspace, action: &ResolveConflictsWithAgent, window, cx| {
 362                        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
 363                            return;
 364                        };
 365
 366                        let content_blocks = build_conflict_resolution_prompt(&action.conflicts);
 367
 368                        workspace.focus_panel::<AgentPanel>(window, cx);
 369
 370                        panel.update(cx, |panel, cx| {
 371                            panel.external_thread(
 372                                None,
 373                                None,
 374                                None,
 375                                None,
 376                                Some(AgentInitialContent::ContentBlock {
 377                                    blocks: content_blocks,
 378                                    auto_submit: true,
 379                                }),
 380                                true,
 381                                window,
 382                                cx,
 383                            );
 384                        });
 385                    },
 386                )
 387                .register_action(
 388                    |workspace, action: &ResolveConflictedFilesWithAgent, window, cx| {
 389                        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
 390                            return;
 391                        };
 392
 393                        let content_blocks =
 394                            build_conflicted_files_resolution_prompt(&action.conflicted_file_paths);
 395
 396                        workspace.focus_panel::<AgentPanel>(window, cx);
 397
 398                        panel.update(cx, |panel, cx| {
 399                            panel.external_thread(
 400                                None,
 401                                None,
 402                                None,
 403                                None,
 404                                Some(AgentInitialContent::ContentBlock {
 405                                    blocks: content_blocks,
 406                                    auto_submit: true,
 407                                }),
 408                                true,
 409                                window,
 410                                cx,
 411                            );
 412                        });
 413                    },
 414                )
 415                .register_action(|workspace, action: &StartThreadIn, window, cx| {
 416                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 417                        panel.update(cx, |panel, cx| {
 418                            panel.set_start_thread_in(action, window, cx);
 419                        });
 420                    }
 421                })
 422                .register_action(|workspace, _: &CycleStartThreadIn, window, cx| {
 423                    if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
 424                        panel.update(cx, |panel, cx| {
 425                            panel.cycle_start_thread_in(window, cx);
 426                        });
 427                    }
 428                })
 429                .register_action(
 430                    |workspace: &mut Workspace, _: &AddSelectionToThread, window, cx| {
 431                        let active_editor = workspace
 432                            .active_item(cx)
 433                            .and_then(|item| item.act_as::<Editor>(cx));
 434                        let has_editor_selection = active_editor.is_some_and(|editor| {
 435                            editor.update(cx, |editor, cx| {
 436                                editor.has_non_empty_selection(&editor.display_snapshot(cx))
 437                            })
 438                        });
 439
 440                        let has_terminal_selection = workspace
 441                            .active_item(cx)
 442                            .and_then(|item| item.act_as::<TerminalView>(cx))
 443                            .is_some_and(|terminal_view| {
 444                                terminal_view
 445                                    .read(cx)
 446                                    .terminal()
 447                                    .read(cx)
 448                                    .last_content
 449                                    .selection_text
 450                                    .as_ref()
 451                                    .is_some_and(|text| !text.is_empty())
 452                            });
 453
 454                        let has_terminal_panel_selection =
 455                            workspace.panel::<TerminalPanel>(cx).is_some_and(|panel| {
 456                                let position = match TerminalSettings::get_global(cx).dock {
 457                                    TerminalDockPosition::Left => DockPosition::Left,
 458                                    TerminalDockPosition::Bottom => DockPosition::Bottom,
 459                                    TerminalDockPosition::Right => DockPosition::Right,
 460                                };
 461                                let dock_is_open =
 462                                    workspace.dock_at_position(position).read(cx).is_open();
 463                                dock_is_open && !panel.read(cx).terminal_selections(cx).is_empty()
 464                            });
 465
 466                        if !has_editor_selection
 467                            && !has_terminal_selection
 468                            && !has_terminal_panel_selection
 469                        {
 470                            return;
 471                        }
 472
 473                        let Some(panel) = workspace.panel::<AgentPanel>(cx) else {
 474                            return;
 475                        };
 476
 477                        if !panel.focus_handle(cx).contains_focused(window, cx) {
 478                            workspace.toggle_panel_focus::<AgentPanel>(window, cx);
 479                        }
 480
 481                        panel.update(cx, |_, cx| {
 482                            cx.defer_in(window, move |panel, window, cx| {
 483                                if let Some(conversation_view) = panel.active_conversation_view() {
 484                                    conversation_view.update(cx, |conversation_view, cx| {
 485                                        conversation_view.insert_selections(window, cx);
 486                                    });
 487                                }
 488                            });
 489                        });
 490                    },
 491                );
 492        },
 493    )
 494    .detach();
 495}
 496
 497fn conflict_resource_block(conflict: &ConflictContent) -> acp::ContentBlock {
 498    let mention_uri = MentionUri::MergeConflict {
 499        file_path: conflict.file_path.clone(),
 500    };
 501    acp::ContentBlock::Resource(acp::EmbeddedResource::new(
 502        acp::EmbeddedResourceResource::TextResourceContents(acp::TextResourceContents::new(
 503            conflict.conflict_text.clone(),
 504            mention_uri.to_uri().to_string(),
 505        )),
 506    ))
 507}
 508
 509fn build_conflict_resolution_prompt(conflicts: &[ConflictContent]) -> Vec<acp::ContentBlock> {
 510    if conflicts.is_empty() {
 511        return Vec::new();
 512    }
 513
 514    let mut blocks = Vec::new();
 515
 516    if conflicts.len() == 1 {
 517        let conflict = &conflicts[0];
 518
 519        blocks.push(acp::ContentBlock::Text(acp::TextContent::new(
 520            "Please resolve the following merge conflict in ",
 521        )));
 522        let mention = MentionUri::File {
 523            abs_path: PathBuf::from(conflict.file_path.clone()),
 524        };
 525        blocks.push(acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
 526            mention.name(),
 527            mention.to_uri(),
 528        )));
 529
 530        blocks.push(acp::ContentBlock::Text(acp::TextContent::new(
 531            indoc::formatdoc!(
 532                "\nThe conflict is between branch `{ours}` (ours) and `{theirs}` (theirs).
 533
 534                Analyze both versions carefully and resolve the conflict by editing \
 535                the file directly. Choose the resolution that best preserves the intent \
 536                of both changes, or combine them if appropriate.
 537
 538                ",
 539                ours = conflict.ours_branch_name,
 540                theirs = conflict.theirs_branch_name,
 541            ),
 542        )));
 543    } else {
 544        let n = conflicts.len();
 545        let unique_files: HashSet<&str> = conflicts.iter().map(|c| c.file_path.as_str()).collect();
 546        let ours = &conflicts[0].ours_branch_name;
 547        let theirs = &conflicts[0].theirs_branch_name;
 548        blocks.push(acp::ContentBlock::Text(acp::TextContent::new(
 549            indoc::formatdoc!(
 550                "Please resolve all {n} merge conflicts below.
 551
 552                The conflicts are between branch `{ours}` (ours) and `{theirs}` (theirs).
 553
 554                For each conflict, analyze both versions carefully and resolve them \
 555                by editing the file{suffix} directly. Choose resolutions that best preserve \
 556                the intent of both changes, or combine them if appropriate.
 557
 558                ",
 559                suffix = if unique_files.len() > 1 { "s" } else { "" },
 560            ),
 561        )));
 562    }
 563
 564    for conflict in conflicts {
 565        blocks.push(conflict_resource_block(conflict));
 566    }
 567
 568    blocks
 569}
 570
 571fn build_conflicted_files_resolution_prompt(
 572    conflicted_file_paths: &[String],
 573) -> Vec<acp::ContentBlock> {
 574    if conflicted_file_paths.is_empty() {
 575        return Vec::new();
 576    }
 577
 578    let instruction = indoc::indoc!(
 579        "The following files have unresolved merge conflicts. Please open each \
 580         file, find the conflict markers (`<<<<<<<` / `=======` / `>>>>>>>`), \
 581         and resolve every conflict by editing the files directly.
 582
 583         Choose resolutions that best preserve the intent of both changes, \
 584         or combine them if appropriate.
 585
 586         Files with conflicts:
 587         ",
 588    );
 589
 590    let mut content = vec![acp::ContentBlock::Text(acp::TextContent::new(instruction))];
 591    for path in conflicted_file_paths {
 592        let mention = MentionUri::File {
 593            abs_path: PathBuf::from(path),
 594        };
 595        content.push(acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
 596            mention.name(),
 597            mention.to_uri(),
 598        )));
 599        content.push(acp::ContentBlock::Text(acp::TextContent::new("\n")));
 600    }
 601    content
 602}
 603
 604enum ActiveView {
 605    Uninitialized,
 606    AgentThread {
 607        conversation_view: Entity<ConversationView>,
 608    },
 609    History {
 610        view: Entity<ThreadHistoryView>,
 611    },
 612    Configuration,
 613}
 614
 615enum WhichFontSize {
 616    AgentFont,
 617    None,
 618}
 619
 620struct StartThreadInLabel {
 621    prefix: Option<SharedString>,
 622    label: SharedString,
 623    suffix: Option<SharedString>,
 624}
 625
 626impl StartThreadIn {
 627    fn trigger_label(&self, project: &Project, cx: &App) -> StartThreadInLabel {
 628        match self {
 629            Self::LocalProject => {
 630                let suffix = project.active_repository(cx).and_then(|repo| {
 631                    let repo = repo.read(cx);
 632                    let work_dir = &repo.original_repo_abs_path;
 633                    let visible_paths: Vec<_> = project
 634                        .visible_worktrees(cx)
 635                        .map(|wt| wt.read(cx).abs_path().to_path_buf())
 636                        .collect();
 637
 638                    for linked in repo.linked_worktrees() {
 639                        if visible_paths.contains(&linked.path) {
 640                            return Some(SharedString::from(format!(
 641                                "({})",
 642                                linked.display_name()
 643                            )));
 644                        }
 645                    }
 646
 647                    if let Some(name) = linked_worktree_short_name(
 648                        repo.original_repo_abs_path.as_ref(),
 649                        repo.work_directory_abs_path.as_ref(),
 650                    ) {
 651                        if visible_paths
 652                            .iter()
 653                            .any(|p| p.as_path() == repo.work_directory_abs_path.as_ref())
 654                        {
 655                            return Some(SharedString::from(format!("({})", name)));
 656                        }
 657                    }
 658
 659                    if visible_paths
 660                        .iter()
 661                        .any(|p| p.as_path() == work_dir.as_ref())
 662                    {
 663                        return Some("(main)".into());
 664                    }
 665
 666                    None
 667                });
 668
 669                StartThreadInLabel {
 670                    prefix: None,
 671                    label: "Current Worktree".into(),
 672                    suffix,
 673                }
 674            }
 675            Self::NewWorktree {
 676                worktree_name: Some(worktree_name),
 677                ..
 678            } => StartThreadInLabel {
 679                prefix: Some("New:".into()),
 680                label: worktree_name.clone().into(),
 681                suffix: None,
 682            },
 683            Self::NewWorktree { .. } => StartThreadInLabel {
 684                prefix: None,
 685                label: "New Git Worktree".into(),
 686                suffix: None,
 687            },
 688            Self::LinkedWorktree { display_name, .. } => StartThreadInLabel {
 689                prefix: Some("From:".into()),
 690                label: display_name.clone().into(),
 691                suffix: None,
 692            },
 693        }
 694    }
 695
 696    fn branch_trigger_label(&self, project: &Project, cx: &App) -> Option<StartThreadInLabel> {
 697        match self {
 698            Self::NewWorktree { branch_target, .. } => {
 699                let (branch_name, is_occupied) = match branch_target {
 700                    NewWorktreeBranchTarget::CurrentBranch => {
 701                        let name: SharedString = if project.repositories(cx).len() > 1 {
 702                            "current branches".into()
 703                        } else {
 704                            project
 705                                .active_repository(cx)
 706                                .and_then(|repo| {
 707                                    repo.read(cx)
 708                                        .branch
 709                                        .as_ref()
 710                                        .map(|branch| SharedString::from(branch.name().to_string()))
 711                                })
 712                                .unwrap_or_else(|| "HEAD".into())
 713                        };
 714                        (name, false)
 715                    }
 716                    NewWorktreeBranchTarget::ExistingBranch { name } => {
 717                        let occupied = Self::is_branch_occupied(name, project, cx);
 718                        (name.clone().into(), occupied)
 719                    }
 720                    NewWorktreeBranchTarget::CreateBranch {
 721                        from_ref: Some(from_ref),
 722                        ..
 723                    } => {
 724                        let occupied = Self::is_branch_occupied(from_ref, project, cx);
 725                        (from_ref.clone().into(), occupied)
 726                    }
 727                    NewWorktreeBranchTarget::CreateBranch { name, .. } => {
 728                        (name.clone().into(), false)
 729                    }
 730                };
 731
 732                let prefix = if is_occupied {
 733                    Some("New From:".into())
 734                } else {
 735                    None
 736                };
 737
 738                Some(StartThreadInLabel {
 739                    prefix,
 740                    label: branch_name,
 741                    suffix: None,
 742                })
 743            }
 744            _ => None,
 745        }
 746    }
 747
 748    fn is_branch_occupied(branch_name: &str, project: &Project, cx: &App) -> bool {
 749        project.repositories(cx).values().any(|repo| {
 750            repo.read(cx)
 751                .linked_worktrees
 752                .iter()
 753                .any(|wt| wt.branch_name() == Some(branch_name))
 754        })
 755    }
 756}
 757
 758#[derive(Clone, Debug)]
 759#[allow(dead_code)]
 760pub enum WorktreeCreationStatus {
 761    Creating,
 762    Error(SharedString),
 763}
 764
 765#[derive(Clone, Debug)]
 766enum WorktreeCreationArgs {
 767    New {
 768        worktree_name: Option<String>,
 769        branch_target: NewWorktreeBranchTarget,
 770    },
 771    Linked {
 772        worktree_path: PathBuf,
 773    },
 774}
 775
 776impl ActiveView {
 777    pub fn which_font_size_used(&self) -> WhichFontSize {
 778        match self {
 779            ActiveView::Uninitialized
 780            | ActiveView::AgentThread { .. }
 781            | ActiveView::History { .. } => WhichFontSize::AgentFont,
 782            ActiveView::Configuration => WhichFontSize::None,
 783        }
 784    }
 785}
 786
 787pub struct AgentPanel {
 788    workspace: WeakEntity<Workspace>,
 789    /// Workspace id is used as a database key
 790    workspace_id: Option<WorkspaceId>,
 791    user_store: Entity<UserStore>,
 792    project: Entity<Project>,
 793    fs: Arc<dyn Fs>,
 794    language_registry: Arc<LanguageRegistry>,
 795    thread_store: Entity<ThreadStore>,
 796    prompt_store: Option<Entity<PromptStore>>,
 797    connection_store: Entity<AgentConnectionStore>,
 798    context_server_registry: Entity<ContextServerRegistry>,
 799    configuration: Option<Entity<AgentConfiguration>>,
 800    configuration_subscription: Option<Subscription>,
 801    focus_handle: FocusHandle,
 802    active_view: ActiveView,
 803    previous_view: Option<ActiveView>,
 804    background_threads: HashMap<acp::SessionId, Entity<ConversationView>>,
 805    new_thread_menu_handle: PopoverMenuHandle<ContextMenu>,
 806    start_thread_in_menu_handle: PopoverMenuHandle<ThreadWorktreePicker>,
 807    thread_branch_menu_handle: PopoverMenuHandle<ThreadBranchPicker>,
 808    agent_panel_menu_handle: PopoverMenuHandle<ContextMenu>,
 809    agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
 810    agent_navigation_menu: Option<Entity<ContextMenu>>,
 811    _extension_subscription: Option<Subscription>,
 812    _project_subscription: Subscription,
 813    _git_store_subscription: Subscription,
 814    zoomed: bool,
 815    pending_serialization: Option<Task<Result<()>>>,
 816    new_user_onboarding: Entity<AgentPanelOnboarding>,
 817    new_user_onboarding_upsell_dismissed: AtomicBool,
 818    agent_layout_onboarding: Entity<ai_onboarding::AgentLayoutOnboarding>,
 819    agent_layout_onboarding_dismissed: AtomicBool,
 820    selected_agent: Agent,
 821    start_thread_in: StartThreadIn,
 822    worktree_creation_status: Option<WorktreeCreationStatus>,
 823    _thread_view_subscription: Option<Subscription>,
 824    _active_thread_focus_subscription: Option<Subscription>,
 825    _worktree_creation_task: Option<Task<()>>,
 826    show_trust_workspace_message: bool,
 827    _active_view_observation: Option<Subscription>,
 828}
 829
 830impl AgentPanel {
 831    fn serialize(&mut self, cx: &mut App) {
 832        let Some(workspace_id) = self.workspace_id else {
 833            return;
 834        };
 835
 836        let selected_agent = self.selected_agent.clone();
 837        let start_thread_in = Some(self.start_thread_in.clone());
 838
 839        let last_active_thread = self.active_agent_thread(cx).map(|thread| {
 840            let thread = thread.read(cx);
 841            let title = thread.title();
 842            let work_dirs = thread.work_dirs().cloned();
 843            SerializedActiveThread {
 844                session_id: thread.session_id().0.to_string(),
 845                agent_type: self.selected_agent.clone(),
 846                title: title.map(|t| t.to_string()),
 847                work_dirs: work_dirs.map(|dirs| dirs.serialize()),
 848            }
 849        });
 850
 851        let kvp = KeyValueStore::global(cx);
 852        self.pending_serialization = Some(cx.background_spawn(async move {
 853            save_serialized_panel(
 854                workspace_id,
 855                SerializedAgentPanel {
 856                    selected_agent: Some(selected_agent),
 857                    last_active_thread,
 858                    start_thread_in,
 859                },
 860                kvp,
 861            )
 862            .await?;
 863            anyhow::Ok(())
 864        }));
 865    }
 866
 867    pub fn load(
 868        workspace: WeakEntity<Workspace>,
 869        mut cx: AsyncWindowContext,
 870    ) -> Task<Result<Entity<Self>>> {
 871        let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
 872        let kvp = cx.update(|_window, cx| KeyValueStore::global(cx)).ok();
 873        cx.spawn(async move |cx| {
 874            let prompt_store = match prompt_store {
 875                Ok(prompt_store) => prompt_store.await.ok(),
 876                Err(_) => None,
 877            };
 878            let workspace_id = workspace
 879                .read_with(cx, |workspace, _| workspace.database_id())
 880                .ok()
 881                .flatten();
 882
 883            let (serialized_panel, global_last_used_agent) = cx
 884                .background_spawn(async move {
 885                    match kvp {
 886                        Some(kvp) => {
 887                            let panel = workspace_id
 888                                .and_then(|id| read_serialized_panel(id, &kvp))
 889                                .or_else(|| read_legacy_serialized_panel(&kvp));
 890                            let global_agent = read_global_last_used_agent(&kvp);
 891                            (panel, global_agent)
 892                        }
 893                        None => (None, None),
 894                    }
 895                })
 896                .await;
 897
 898            let last_active_thread = if let Some(thread_info) = serialized_panel
 899                .as_ref()
 900                .and_then(|p| p.last_active_thread.as_ref())
 901            {
 902                let session_id = acp::SessionId::new(thread_info.session_id.clone());
 903                let has_metadata = cx
 904                    .update(|_window, cx| {
 905                        let store = ThreadMetadataStore::global(cx);
 906                        store.read(cx).entry(&session_id).is_some()
 907                    })
 908                    .unwrap_or(false);
 909                if has_metadata {
 910                    Some(thread_info)
 911                } else {
 912                    log::warn!(
 913                        "last active thread {} has no metadata, skipping restoration",
 914                        thread_info.session_id
 915                    );
 916                    None
 917                }
 918            } else {
 919                None
 920            };
 921
 922            let panel = workspace.update_in(cx, |workspace, window, cx| {
 923                let panel =
 924                    cx.new(|cx| Self::new(workspace, prompt_store, window, cx));
 925
 926                panel.update(cx, |panel, cx| {
 927                    let is_via_collab = panel.project.read(cx).is_via_collab();
 928
 929                    // Only apply a non-native global fallback to local projects.
 930                    // Collab workspaces only support NativeAgent, so inheriting a
 931                    // custom agent would cause set_active → new_agent_thread_inner
 932                    // to bypass the collab guard in external_thread.
 933                    let global_fallback = global_last_used_agent
 934                        .filter(|agent| !is_via_collab || agent.is_native());
 935
 936                    if let Some(serialized_panel) = &serialized_panel {
 937                        if let Some(selected_agent) = serialized_panel.selected_agent.clone() {
 938                            panel.selected_agent = selected_agent;
 939                        } else if let Some(agent) = global_fallback {
 940                            panel.selected_agent = agent;
 941                        }
 942                        if let Some(ref start_thread_in) = serialized_panel.start_thread_in {
 943                            let is_valid = match &start_thread_in {
 944                                StartThreadIn::LocalProject => true,
 945                                StartThreadIn::NewWorktree { .. } => {
 946                                    let project = panel.project.read(cx);
 947                                    !project.is_via_collab()
 948                                }
 949                                StartThreadIn::LinkedWorktree { path, .. } => {
 950                                    path.exists()
 951                                }
 952                            };
 953                            if is_valid {
 954                                panel.start_thread_in = start_thread_in.clone();
 955                            } else {
 956                                log::info!(
 957                                    "deserialized start_thread_in {:?} is no longer valid, falling back to LocalProject",
 958                                    start_thread_in,
 959                                );
 960                            }
 961                        }
 962                    } else if let Some(agent) = global_fallback {
 963                        panel.selected_agent = agent;
 964                    }
 965                    cx.notify();
 966                });
 967
 968                if let Some(thread_info) = last_active_thread {
 969                    let agent = thread_info.agent_type.clone();
 970                    panel.update(cx, |panel, cx| {
 971                        panel.selected_agent = agent.clone();
 972                        panel.load_agent_thread(
 973                            agent,
 974                            thread_info.session_id.clone().into(),
 975                            thread_info.work_dirs.as_ref().map(|dirs| PathList::deserialize(dirs)),
 976                            thread_info.title.as_ref().map(|t| t.clone().into()),
 977                            false,
 978                            window,
 979                            cx,
 980                        );
 981                    });
 982                }
 983                panel
 984            })?;
 985
 986            Ok(panel)
 987        })
 988    }
 989
 990    pub(crate) fn new(
 991        workspace: &Workspace,
 992        prompt_store: Option<Entity<PromptStore>>,
 993        window: &mut Window,
 994        cx: &mut Context<Self>,
 995    ) -> Self {
 996        let fs = workspace.app_state().fs.clone();
 997        let user_store = workspace.app_state().user_store.clone();
 998        let project = workspace.project();
 999        let language_registry = project.read(cx).languages().clone();
1000        let client = workspace.client().clone();
1001        let workspace_id = workspace.database_id();
1002        let workspace = workspace.weak_handle();
1003
1004        let context_server_registry =
1005            cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
1006
1007        let thread_store = ThreadStore::global(cx);
1008
1009        let active_view = ActiveView::Uninitialized;
1010
1011        let weak_panel = cx.entity().downgrade();
1012
1013        window.defer(cx, move |window, cx| {
1014            let panel = weak_panel.clone();
1015            let agent_navigation_menu =
1016                ContextMenu::build_persistent(window, cx, move |mut menu, window, cx| {
1017                    if let Some(panel) = panel.upgrade() {
1018                        if let Some(history) = panel
1019                            .update(cx, |panel, cx| panel.history_for_selected_agent(window, cx))
1020                        {
1021                            menu = Self::populate_recently_updated_menu_section(
1022                                menu, panel, history, cx,
1023                            );
1024                            menu = menu.action("View All", Box::new(OpenHistory));
1025                        }
1026                    }
1027
1028                    menu = menu
1029                        .fixed_width(px(320.).into())
1030                        .keep_open_on_confirm(false)
1031                        .key_context("NavigationMenu");
1032
1033                    menu
1034                });
1035            weak_panel
1036                .update(cx, |panel, cx| {
1037                    cx.subscribe_in(
1038                        &agent_navigation_menu,
1039                        window,
1040                        |_, menu, _: &DismissEvent, window, cx| {
1041                            menu.update(cx, |menu, _| {
1042                                menu.clear_selected();
1043                            });
1044                            cx.focus_self(window);
1045                        },
1046                    )
1047                    .detach();
1048                    panel.agent_navigation_menu = Some(agent_navigation_menu);
1049                })
1050                .ok();
1051        });
1052
1053        let weak_panel = cx.entity().downgrade();
1054        let onboarding = cx.new(|cx| {
1055            AgentPanelOnboarding::new(
1056                user_store.clone(),
1057                client,
1058                move |_window, cx| {
1059                    weak_panel
1060                        .update(cx, |panel, cx| {
1061                            panel.dismiss_ai_onboarding(cx);
1062                        })
1063                        .ok();
1064                },
1065                cx,
1066            )
1067        });
1068
1069        let weak_panel = cx.entity().downgrade();
1070
1071        let layout = AgentSettings::get_layout(cx);
1072        let is_agent_layout = matches!(layout, WindowLayout::Agent(_));
1073
1074        let agent_layout_onboarding = cx.new(|_cx| ai_onboarding::AgentLayoutOnboarding {
1075            use_agent_layout: Arc::new({
1076                let fs = fs.clone();
1077                let weak_panel = weak_panel.clone();
1078                move |_window, cx| {
1079                    AgentSettings::set_layout(WindowLayout::Agent(None), fs.clone(), cx);
1080                    weak_panel
1081                        .update(cx, |panel, cx| {
1082                            panel.dismiss_agent_layout_onboarding(cx);
1083                        })
1084                        .ok();
1085                }
1086            }),
1087            revert_to_editor_layout: Arc::new({
1088                let fs = fs.clone();
1089                let weak_panel = weak_panel.clone();
1090                move |_window, cx| {
1091                    AgentSettings::set_layout(WindowLayout::Editor(None), fs.clone(), cx);
1092                    weak_panel
1093                        .update(cx, |panel, cx| {
1094                            panel.dismiss_agent_layout_onboarding(cx);
1095                        })
1096                        .ok();
1097                }
1098            }),
1099            dismissed: Arc::new(move |_window, cx| {
1100                weak_panel
1101                    .update(cx, |panel, cx| {
1102                        panel.dismiss_agent_layout_onboarding(cx);
1103                    })
1104                    .ok();
1105            }),
1106            is_agent_layout,
1107        });
1108
1109        // Subscribe to extension events to sync agent servers when extensions change
1110        let extension_subscription = if let Some(extension_events) = ExtensionEvents::try_global(cx)
1111        {
1112            Some(
1113                cx.subscribe(&extension_events, |this, _source, event, cx| match event {
1114                    extension::Event::ExtensionInstalled(_)
1115                    | extension::Event::ExtensionUninstalled(_)
1116                    | extension::Event::ExtensionsInstalledChanged => {
1117                        this.sync_agent_servers_from_extensions(cx);
1118                    }
1119                    _ => {}
1120                }),
1121            )
1122        } else {
1123            None
1124        };
1125
1126        let connection_store = cx.new(|cx| {
1127            let mut store = AgentConnectionStore::new(project.clone(), cx);
1128            // Register the native agent right away, so that it is available for
1129            // the inline assistant etc.
1130            store.request_connection(
1131                Agent::NativeAgent,
1132                Agent::NativeAgent.server(fs.clone(), thread_store.clone()),
1133                cx,
1134            );
1135            store
1136        });
1137        let _project_subscription =
1138            cx.subscribe(&project, |this, _project, event, cx| match event {
1139                project::Event::WorktreeAdded(_)
1140                | project::Event::WorktreeRemoved(_)
1141                | project::Event::WorktreeOrderChanged => {
1142                    this.update_thread_work_dirs(cx);
1143                }
1144                _ => {}
1145            });
1146        let git_store = project.read(cx).git_store().clone();
1147        let _git_store_subscription = cx.subscribe(&git_store, |this, _, event, cx| {
1148            let should_sync = matches!(
1149                event,
1150                GitStoreEvent::ActiveRepositoryChanged(_)
1151                    | GitStoreEvent::RepositoryAdded
1152                    | GitStoreEvent::RepositoryRemoved(_)
1153                    | GitStoreEvent::RepositoryUpdated(
1154                        _,
1155                        RepositoryEvent::HeadChanged
1156                            | RepositoryEvent::BranchListChanged
1157                            | RepositoryEvent::GitWorktreeListChanged,
1158                        _,
1159                    )
1160            );
1161
1162            if should_sync {
1163                this.sync_start_thread_in_with_git_state(cx);
1164            }
1165        });
1166
1167        let mut panel = Self {
1168            workspace_id,
1169            active_view,
1170            workspace,
1171            user_store,
1172            project: project.clone(),
1173            fs: fs.clone(),
1174            language_registry,
1175            prompt_store,
1176            connection_store,
1177            configuration: None,
1178            configuration_subscription: None,
1179            focus_handle: cx.focus_handle(),
1180            context_server_registry,
1181            previous_view: None,
1182            background_threads: HashMap::default(),
1183            new_thread_menu_handle: PopoverMenuHandle::default(),
1184            start_thread_in_menu_handle: PopoverMenuHandle::default(),
1185            thread_branch_menu_handle: PopoverMenuHandle::default(),
1186            agent_panel_menu_handle: PopoverMenuHandle::default(),
1187            agent_navigation_menu_handle: PopoverMenuHandle::default(),
1188            agent_navigation_menu: None,
1189            _extension_subscription: extension_subscription,
1190            _project_subscription,
1191            _git_store_subscription,
1192            zoomed: false,
1193            pending_serialization: None,
1194            new_user_onboarding: onboarding,
1195            agent_layout_onboarding,
1196            thread_store,
1197            selected_agent: Agent::default(),
1198            start_thread_in: StartThreadIn::default(),
1199            worktree_creation_status: None,
1200            _thread_view_subscription: None,
1201            _active_thread_focus_subscription: None,
1202            _worktree_creation_task: None,
1203            show_trust_workspace_message: false,
1204            new_user_onboarding_upsell_dismissed: AtomicBool::new(OnboardingUpsell::dismissed(cx)),
1205            agent_layout_onboarding_dismissed: AtomicBool::new(AgentLayoutOnboarding::dismissed(
1206                cx,
1207            )),
1208            _active_view_observation: None,
1209        };
1210
1211        // Initial sync of agent servers from extensions
1212        panel.sync_agent_servers_from_extensions(cx);
1213        panel
1214    }
1215
1216    pub fn toggle_focus(
1217        workspace: &mut Workspace,
1218        _: &ToggleFocus,
1219        window: &mut Window,
1220        cx: &mut Context<Workspace>,
1221    ) {
1222        if workspace
1223            .panel::<Self>(cx)
1224            .is_some_and(|panel| panel.read(cx).enabled(cx))
1225        {
1226            workspace.toggle_panel_focus::<Self>(window, cx);
1227        }
1228    }
1229
1230    pub fn toggle(
1231        workspace: &mut Workspace,
1232        _: &Toggle,
1233        window: &mut Window,
1234        cx: &mut Context<Workspace>,
1235    ) {
1236        if workspace
1237            .panel::<Self>(cx)
1238            .is_some_and(|panel| panel.read(cx).enabled(cx))
1239        {
1240            if !workspace.toggle_panel_focus::<Self>(window, cx) {
1241                workspace.close_panel::<Self>(window, cx);
1242            }
1243        }
1244    }
1245
1246    pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
1247        &self.prompt_store
1248    }
1249
1250    pub fn thread_store(&self) -> &Entity<ThreadStore> {
1251        &self.thread_store
1252    }
1253
1254    pub fn connection_store(&self) -> &Entity<AgentConnectionStore> {
1255        &self.connection_store
1256    }
1257
1258    pub fn open_thread(
1259        &mut self,
1260        session_id: acp::SessionId,
1261        work_dirs: Option<PathList>,
1262        title: Option<SharedString>,
1263        window: &mut Window,
1264        cx: &mut Context<Self>,
1265    ) {
1266        self.external_thread(
1267            Some(crate::Agent::NativeAgent),
1268            Some(session_id),
1269            work_dirs,
1270            title,
1271            None,
1272            true,
1273            window,
1274            cx,
1275        );
1276    }
1277
1278    pub(crate) fn context_server_registry(&self) -> &Entity<ContextServerRegistry> {
1279        &self.context_server_registry
1280    }
1281
1282    pub fn is_visible(workspace: &Entity<Workspace>, cx: &App) -> bool {
1283        let workspace_read = workspace.read(cx);
1284
1285        workspace_read
1286            .panel::<AgentPanel>(cx)
1287            .map(|panel| {
1288                let panel_id = Entity::entity_id(&panel);
1289
1290                workspace_read.all_docks().iter().any(|dock| {
1291                    dock.read(cx)
1292                        .visible_panel()
1293                        .is_some_and(|visible_panel| visible_panel.panel_id() == panel_id)
1294                })
1295            })
1296            .unwrap_or(false)
1297    }
1298
1299    /// Reset the panel to the uninitialized state, clearing any active
1300    /// thread without creating a new draft. Running threads are retained
1301    /// in the background. The sidebar suppresses the uninitialized state
1302    /// so no "Draft" entry appears.
1303    pub fn clear_active_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1304        self.set_active_view(ActiveView::Uninitialized, false, window, cx);
1305    }
1306
1307    pub fn new_thread(&mut self, _action: &NewThread, window: &mut Window, cx: &mut Context<Self>) {
1308        self.reset_start_thread_in_to_default(cx);
1309        let initial_content = self.take_active_draft_initial_content(cx);
1310        self.external_thread(None, None, None, None, initial_content, true, window, cx);
1311    }
1312
1313    fn take_active_draft_initial_content(
1314        &mut self,
1315        cx: &mut Context<Self>,
1316    ) -> Option<AgentInitialContent> {
1317        self.active_thread_view(cx).and_then(|thread_view| {
1318            thread_view.update(cx, |thread_view, cx| {
1319                let draft_blocks = thread_view
1320                    .thread
1321                    .read(cx)
1322                    .draft_prompt()
1323                    .map(|draft| draft.to_vec())
1324                    .filter(|draft| !draft.is_empty());
1325
1326                let draft_blocks = draft_blocks.or_else(|| {
1327                    let text = thread_view.message_editor.read(cx).text(cx);
1328                    if text.trim().is_empty() {
1329                        None
1330                    } else {
1331                        Some(vec![acp::ContentBlock::Text(acp::TextContent::new(text))])
1332                    }
1333                });
1334
1335                draft_blocks.map(|blocks| AgentInitialContent::ContentBlock {
1336                    blocks,
1337                    auto_submit: false,
1338                })
1339            })
1340        })
1341    }
1342
1343    fn new_native_agent_thread_from_summary(
1344        &mut self,
1345        action: &NewNativeAgentThreadFromSummary,
1346        window: &mut Window,
1347        cx: &mut Context<Self>,
1348    ) {
1349        let session_id = action.from_session_id.clone();
1350
1351        let Some(history) = self
1352            .connection_store
1353            .read(cx)
1354            .entry(&Agent::NativeAgent)
1355            .and_then(|e| e.read(cx).history().cloned())
1356        else {
1357            debug_panic!("Native agent is not registered");
1358            return;
1359        };
1360
1361        cx.spawn_in(window, async move |this, cx| {
1362            this.update_in(cx, |this, window, cx| {
1363                let thread = history
1364                    .read(cx)
1365                    .session_for_id(&session_id)
1366                    .context("Session not found")?;
1367
1368                this.external_thread(
1369                    Some(Agent::NativeAgent),
1370                    None,
1371                    None,
1372                    None,
1373                    Some(AgentInitialContent::ThreadSummary {
1374                        session_id: thread.session_id,
1375                        title: thread.title,
1376                    }),
1377                    true,
1378                    window,
1379                    cx,
1380                );
1381                anyhow::Ok(())
1382            })
1383        })
1384        .detach_and_log_err(cx);
1385    }
1386
1387    fn external_thread(
1388        &mut self,
1389        agent_choice: Option<crate::Agent>,
1390        resume_session_id: Option<acp::SessionId>,
1391        work_dirs: Option<PathList>,
1392        title: Option<SharedString>,
1393        initial_content: Option<AgentInitialContent>,
1394        focus: bool,
1395        window: &mut Window,
1396        cx: &mut Context<Self>,
1397    ) {
1398        let workspace = self.workspace.clone();
1399        let project = self.project.clone();
1400        let fs = self.fs.clone();
1401        let thread_store = self.thread_store.clone();
1402
1403        let agent = agent_choice.unwrap_or_else(|| {
1404            if self.project.read(cx).is_via_collab() {
1405                Agent::NativeAgent
1406            } else {
1407                self.selected_agent.clone()
1408            }
1409        });
1410
1411        let server = agent.server(fs, thread_store);
1412        self.create_agent_thread(
1413            server,
1414            resume_session_id,
1415            work_dirs,
1416            title,
1417            initial_content,
1418            workspace,
1419            project,
1420            agent,
1421            focus,
1422            window,
1423            cx,
1424        );
1425    }
1426
1427    fn deploy_rules_library(
1428        &mut self,
1429        action: &OpenRulesLibrary,
1430        _window: &mut Window,
1431        cx: &mut Context<Self>,
1432    ) {
1433        open_rules_library(
1434            self.language_registry.clone(),
1435            Box::new(PromptLibraryInlineAssist::new(self.workspace.clone())),
1436            action
1437                .prompt_to_select
1438                .map(|uuid| UserPromptId(uuid).into()),
1439            cx,
1440        )
1441        .detach_and_log_err(cx);
1442    }
1443
1444    fn expand_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1445        let Some(conversation_view) = self.active_conversation_view() else {
1446            return;
1447        };
1448
1449        let Some(active_thread) = conversation_view.read(cx).active_thread().cloned() else {
1450            return;
1451        };
1452
1453        active_thread.update(cx, |active_thread, cx| {
1454            active_thread.expand_message_editor(&ExpandMessageEditor, window, cx);
1455            active_thread.focus_handle(cx).focus(window, cx);
1456        })
1457    }
1458
1459    fn has_history_for_selected_agent(&self, cx: &App) -> bool {
1460        match &self.selected_agent {
1461            Agent::NativeAgent => true,
1462            Agent::Custom { .. } => self
1463                .connection_store
1464                .read(cx)
1465                .entry(&self.selected_agent)
1466                .map_or(false, |entry| entry.read(cx).history().is_some()),
1467        }
1468    }
1469
1470    fn history_for_selected_agent(
1471        &self,
1472        window: &mut Window,
1473        cx: &mut Context<Self>,
1474    ) -> Option<Entity<ThreadHistoryView>> {
1475        let agent = self.selected_agent.clone();
1476        let history = self
1477            .connection_store
1478            .read(cx)
1479            .entry(&agent)?
1480            .read(cx)
1481            .history()?
1482            .clone();
1483        Some(self.create_thread_history_view(agent, history, window, cx))
1484    }
1485
1486    fn create_thread_history_view(
1487        &self,
1488        agent: Agent,
1489        history: Entity<ThreadHistory>,
1490        window: &mut Window,
1491        cx: &mut Context<Self>,
1492    ) -> Entity<ThreadHistoryView> {
1493        let view = cx.new(|cx| ThreadHistoryView::new(history.clone(), window, cx));
1494        cx.subscribe_in(
1495            &view,
1496            window,
1497            move |this, _, event, window, cx| match event {
1498                ThreadHistoryViewEvent::Open(thread) => {
1499                    this.load_agent_thread(
1500                        agent.clone(),
1501                        thread.session_id.clone(),
1502                        thread.work_dirs.clone(),
1503                        thread.title.clone(),
1504                        true,
1505                        window,
1506                        cx,
1507                    );
1508                }
1509            },
1510        )
1511        .detach();
1512        view
1513    }
1514
1515    fn open_history(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1516        let Some(view) = self.history_for_selected_agent(window, cx) else {
1517            return;
1518        };
1519
1520        if let ActiveView::History { view: active_view } = &self.active_view {
1521            if active_view == &view {
1522                if let Some(previous_view) = self.previous_view.take() {
1523                    self.set_active_view(previous_view, true, window, cx);
1524                }
1525                return;
1526            }
1527        }
1528
1529        self.set_active_view(ActiveView::History { view }, true, window, cx);
1530        cx.notify();
1531    }
1532
1533    pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
1534        match self.active_view {
1535            ActiveView::Configuration | ActiveView::History { .. } => {
1536                if let Some(previous_view) = self.previous_view.take() {
1537                    self.set_active_view(previous_view, true, window, cx);
1538                }
1539                cx.notify();
1540            }
1541            _ => {}
1542        }
1543    }
1544
1545    pub fn toggle_navigation_menu(
1546        &mut self,
1547        _: &ToggleNavigationMenu,
1548        window: &mut Window,
1549        cx: &mut Context<Self>,
1550    ) {
1551        if !self.has_history_for_selected_agent(cx) {
1552            return;
1553        }
1554        self.agent_navigation_menu_handle.toggle(window, cx);
1555    }
1556
1557    pub fn toggle_options_menu(
1558        &mut self,
1559        _: &ToggleOptionsMenu,
1560        window: &mut Window,
1561        cx: &mut Context<Self>,
1562    ) {
1563        self.agent_panel_menu_handle.toggle(window, cx);
1564    }
1565
1566    pub fn toggle_new_thread_menu(
1567        &mut self,
1568        _: &ToggleNewThreadMenu,
1569        window: &mut Window,
1570        cx: &mut Context<Self>,
1571    ) {
1572        self.new_thread_menu_handle.toggle(window, cx);
1573    }
1574
1575    pub fn increase_font_size(
1576        &mut self,
1577        action: &IncreaseBufferFontSize,
1578        _: &mut Window,
1579        cx: &mut Context<Self>,
1580    ) {
1581        self.handle_font_size_action(action.persist, px(1.0), cx);
1582    }
1583
1584    pub fn decrease_font_size(
1585        &mut self,
1586        action: &DecreaseBufferFontSize,
1587        _: &mut Window,
1588        cx: &mut Context<Self>,
1589    ) {
1590        self.handle_font_size_action(action.persist, px(-1.0), cx);
1591    }
1592
1593    fn handle_font_size_action(&mut self, persist: bool, delta: Pixels, cx: &mut Context<Self>) {
1594        match self.active_view.which_font_size_used() {
1595            WhichFontSize::AgentFont => {
1596                if persist {
1597                    update_settings_file(self.fs.clone(), cx, move |settings, cx| {
1598                        let agent_ui_font_size =
1599                            ThemeSettings::get_global(cx).agent_ui_font_size(cx) + delta;
1600                        let agent_buffer_font_size =
1601                            ThemeSettings::get_global(cx).agent_buffer_font_size(cx) + delta;
1602
1603                        let _ = settings.theme.agent_ui_font_size.insert(
1604                            f32::from(theme_settings::clamp_font_size(agent_ui_font_size)).into(),
1605                        );
1606                        let _ = settings.theme.agent_buffer_font_size.insert(
1607                            f32::from(theme_settings::clamp_font_size(agent_buffer_font_size))
1608                                .into(),
1609                        );
1610                    });
1611                } else {
1612                    theme_settings::adjust_agent_ui_font_size(cx, |size| size + delta);
1613                    theme_settings::adjust_agent_buffer_font_size(cx, |size| size + delta);
1614                }
1615            }
1616            WhichFontSize::None => {}
1617        }
1618    }
1619
1620    pub fn reset_font_size(
1621        &mut self,
1622        action: &ResetBufferFontSize,
1623        _: &mut Window,
1624        cx: &mut Context<Self>,
1625    ) {
1626        if action.persist {
1627            update_settings_file(self.fs.clone(), cx, move |settings, _| {
1628                settings.theme.agent_ui_font_size = None;
1629                settings.theme.agent_buffer_font_size = None;
1630            });
1631        } else {
1632            theme_settings::reset_agent_ui_font_size(cx);
1633            theme_settings::reset_agent_buffer_font_size(cx);
1634        }
1635    }
1636
1637    pub fn reset_agent_zoom(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
1638        theme_settings::reset_agent_ui_font_size(cx);
1639        theme_settings::reset_agent_buffer_font_size(cx);
1640    }
1641
1642    pub fn toggle_zoom(&mut self, _: &ToggleZoom, window: &mut Window, cx: &mut Context<Self>) {
1643        if self.zoomed {
1644            cx.emit(PanelEvent::ZoomOut);
1645        } else {
1646            if !self.focus_handle(cx).contains_focused(window, cx) {
1647                cx.focus_self(window);
1648            }
1649            cx.emit(PanelEvent::ZoomIn);
1650        }
1651    }
1652
1653    pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1654        let agent_server_store = self.project.read(cx).agent_server_store().clone();
1655        let context_server_store = self.project.read(cx).context_server_store();
1656        let fs = self.fs.clone();
1657
1658        self.set_active_view(ActiveView::Configuration, true, window, cx);
1659        self.configuration = Some(cx.new(|cx| {
1660            AgentConfiguration::new(
1661                fs,
1662                agent_server_store,
1663                self.connection_store.clone(),
1664                context_server_store,
1665                self.context_server_registry.clone(),
1666                self.language_registry.clone(),
1667                self.workspace.clone(),
1668                window,
1669                cx,
1670            )
1671        }));
1672
1673        if let Some(configuration) = self.configuration.as_ref() {
1674            self.configuration_subscription = Some(cx.subscribe_in(
1675                configuration,
1676                window,
1677                Self::handle_agent_configuration_event,
1678            ));
1679
1680            configuration.focus_handle(cx).focus(window, cx);
1681        }
1682    }
1683
1684    pub(crate) fn open_active_thread_as_markdown(
1685        &mut self,
1686        _: &OpenActiveThreadAsMarkdown,
1687        window: &mut Window,
1688        cx: &mut Context<Self>,
1689    ) {
1690        if let Some(workspace) = self.workspace.upgrade()
1691            && let Some(conversation_view) = self.active_conversation_view()
1692            && let Some(active_thread) = conversation_view.read(cx).active_thread().cloned()
1693        {
1694            active_thread.update(cx, |thread, cx| {
1695                thread
1696                    .open_thread_as_markdown(workspace, window, cx)
1697                    .detach_and_log_err(cx);
1698            });
1699        }
1700    }
1701
1702    fn copy_thread_to_clipboard(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1703        let Some(thread) = self.active_native_agent_thread(cx) else {
1704            Self::show_deferred_toast(&self.workspace, "No active native thread to copy", cx);
1705            return;
1706        };
1707
1708        let workspace = self.workspace.clone();
1709        let load_task = thread.read(cx).to_db(cx);
1710
1711        cx.spawn_in(window, async move |_this, cx| {
1712            let db_thread = load_task.await;
1713            let shared_thread = SharedThread::from_db_thread(&db_thread);
1714            let thread_data = shared_thread.to_bytes()?;
1715            let encoded = base64::Engine::encode(&base64::prelude::BASE64_STANDARD, &thread_data);
1716
1717            cx.update(|_window, cx| {
1718                cx.write_to_clipboard(ClipboardItem::new_string(encoded));
1719                if let Some(workspace) = workspace.upgrade() {
1720                    workspace.update(cx, |workspace, cx| {
1721                        struct ThreadCopiedToast;
1722                        workspace.show_toast(
1723                            workspace::Toast::new(
1724                                workspace::notifications::NotificationId::unique::<ThreadCopiedToast>(),
1725                                "Thread copied to clipboard (base64 encoded)",
1726                            )
1727                            .autohide(),
1728                            cx,
1729                        );
1730                    });
1731                }
1732            })?;
1733
1734            anyhow::Ok(())
1735        })
1736        .detach_and_log_err(cx);
1737    }
1738
1739    fn show_deferred_toast(
1740        workspace: &WeakEntity<workspace::Workspace>,
1741        message: &'static str,
1742        cx: &mut App,
1743    ) {
1744        let workspace = workspace.clone();
1745        cx.defer(move |cx| {
1746            if let Some(workspace) = workspace.upgrade() {
1747                workspace.update(cx, |workspace, cx| {
1748                    struct ClipboardToast;
1749                    workspace.show_toast(
1750                        workspace::Toast::new(
1751                            workspace::notifications::NotificationId::unique::<ClipboardToast>(),
1752                            message,
1753                        )
1754                        .autohide(),
1755                        cx,
1756                    );
1757                });
1758            }
1759        });
1760    }
1761
1762    fn load_thread_from_clipboard(&mut self, window: &mut Window, cx: &mut Context<Self>) {
1763        let Some(clipboard) = cx.read_from_clipboard() else {
1764            Self::show_deferred_toast(&self.workspace, "No clipboard content available", cx);
1765            return;
1766        };
1767
1768        let Some(encoded) = clipboard.text() else {
1769            Self::show_deferred_toast(&self.workspace, "Clipboard does not contain text", cx);
1770            return;
1771        };
1772
1773        let thread_data = match base64::Engine::decode(&base64::prelude::BASE64_STANDARD, &encoded)
1774        {
1775            Ok(data) => data,
1776            Err(_) => {
1777                Self::show_deferred_toast(
1778                    &self.workspace,
1779                    "Failed to decode clipboard content (expected base64)",
1780                    cx,
1781                );
1782                return;
1783            }
1784        };
1785
1786        let shared_thread = match SharedThread::from_bytes(&thread_data) {
1787            Ok(thread) => thread,
1788            Err(_) => {
1789                Self::show_deferred_toast(
1790                    &self.workspace,
1791                    "Failed to parse thread data from clipboard",
1792                    cx,
1793                );
1794                return;
1795            }
1796        };
1797
1798        let db_thread = shared_thread.to_db_thread();
1799        let session_id = acp::SessionId::new(uuid::Uuid::new_v4().to_string());
1800        let thread_store = self.thread_store.clone();
1801        let title = db_thread.title.clone();
1802        let workspace = self.workspace.clone();
1803
1804        cx.spawn_in(window, async move |this, cx| {
1805            thread_store
1806                .update(&mut cx.clone(), |store, cx| {
1807                    store.save_thread(session_id.clone(), db_thread, Default::default(), cx)
1808                })
1809                .await?;
1810
1811            this.update_in(cx, |this, window, cx| {
1812                this.open_thread(session_id, None, Some(title), window, cx);
1813            })?;
1814
1815            this.update_in(cx, |_, _window, cx| {
1816                if let Some(workspace) = workspace.upgrade() {
1817                    workspace.update(cx, |workspace, cx| {
1818                        struct ThreadLoadedToast;
1819                        workspace.show_toast(
1820                            workspace::Toast::new(
1821                                workspace::notifications::NotificationId::unique::<ThreadLoadedToast>(),
1822                                "Thread loaded from clipboard",
1823                            )
1824                            .autohide(),
1825                            cx,
1826                        );
1827                    });
1828                }
1829            })?;
1830
1831            anyhow::Ok(())
1832        })
1833        .detach_and_log_err(cx);
1834    }
1835
1836    fn handle_agent_configuration_event(
1837        &mut self,
1838        _entity: &Entity<AgentConfiguration>,
1839        event: &AssistantConfigurationEvent,
1840        window: &mut Window,
1841        cx: &mut Context<Self>,
1842    ) {
1843        match event {
1844            AssistantConfigurationEvent::NewThread(provider) => {
1845                if LanguageModelRegistry::read_global(cx)
1846                    .default_model()
1847                    .is_none_or(|model| model.provider.id() != provider.id())
1848                    && let Some(model) = provider.default_model(cx)
1849                {
1850                    update_settings_file(self.fs.clone(), cx, move |settings, _| {
1851                        let provider = model.provider_id().0.to_string();
1852                        let enable_thinking = model.supports_thinking();
1853                        let effort = model
1854                            .default_effort_level()
1855                            .map(|effort| effort.value.to_string());
1856                        let model = model.id().0.to_string();
1857                        settings
1858                            .agent
1859                            .get_or_insert_default()
1860                            .set_model(LanguageModelSelection {
1861                                provider: LanguageModelProviderSetting(provider),
1862                                model,
1863                                enable_thinking,
1864                                effort,
1865                                speed: None,
1866                            })
1867                    });
1868                }
1869
1870                self.new_thread(&NewThread, window, cx);
1871                if let Some((thread, model)) = self
1872                    .active_native_agent_thread(cx)
1873                    .zip(provider.default_model(cx))
1874                {
1875                    thread.update(cx, |thread, cx| {
1876                        thread.set_model(model, cx);
1877                    });
1878                }
1879            }
1880        }
1881    }
1882
1883    pub fn workspace_id(&self) -> Option<WorkspaceId> {
1884        self.workspace_id
1885    }
1886
1887    pub fn background_threads(&self) -> &HashMap<acp::SessionId, Entity<ConversationView>> {
1888        &self.background_threads
1889    }
1890
1891    pub fn active_conversation_view(&self) -> Option<&Entity<ConversationView>> {
1892        match &self.active_view {
1893            ActiveView::AgentThread { conversation_view } => Some(conversation_view),
1894            _ => None,
1895        }
1896    }
1897
1898    pub fn conversation_views(&self) -> Vec<Entity<ConversationView>> {
1899        self.active_conversation_view()
1900            .into_iter()
1901            .cloned()
1902            .chain(self.background_threads.values().cloned())
1903            .collect()
1904    }
1905
1906    pub fn active_thread_view(&self, cx: &App) -> Option<Entity<ThreadView>> {
1907        let server_view = self.active_conversation_view()?;
1908        server_view.read(cx).active_thread().cloned()
1909    }
1910
1911    pub fn active_agent_thread(&self, cx: &App) -> Option<Entity<AcpThread>> {
1912        match &self.active_view {
1913            ActiveView::AgentThread {
1914                conversation_view, ..
1915            } => conversation_view
1916                .read(cx)
1917                .active_thread()
1918                .map(|r| r.read(cx).thread.clone()),
1919            _ => None,
1920        }
1921    }
1922
1923    /// Returns the primary thread views for all retained connections: the
1924    pub fn is_background_thread(&self, session_id: &acp::SessionId) -> bool {
1925        self.background_threads.contains_key(session_id)
1926    }
1927
1928    pub fn cancel_thread(&self, session_id: &acp::SessionId, cx: &mut Context<Self>) -> bool {
1929        let conversation_views = self
1930            .active_conversation_view()
1931            .into_iter()
1932            .chain(self.background_threads.values());
1933
1934        for conversation_view in conversation_views {
1935            if let Some(thread_view) = conversation_view.read(cx).thread_view(session_id) {
1936                thread_view.update(cx, |view, cx| view.cancel_generation(cx));
1937                return true;
1938            }
1939        }
1940        false
1941    }
1942
1943    /// active thread plus any background threads that are still running or
1944    /// completed but unseen.
1945    pub fn parent_threads(&self, cx: &App) -> Vec<Entity<ThreadView>> {
1946        let mut views = Vec::new();
1947
1948        if let Some(server_view) = self.active_conversation_view() {
1949            if let Some(thread_view) = server_view.read(cx).root_thread(cx) {
1950                views.push(thread_view);
1951            }
1952        }
1953
1954        for server_view in self.background_threads.values() {
1955            if let Some(thread_view) = server_view.read(cx).root_thread(cx) {
1956                views.push(thread_view);
1957            }
1958        }
1959
1960        views
1961    }
1962
1963    fn update_thread_work_dirs(&self, cx: &mut Context<Self>) {
1964        let new_work_dirs = self.project.read(cx).default_path_list(cx);
1965
1966        if let Some(conversation_view) = self.active_conversation_view() {
1967            conversation_view.update(cx, |conversation_view, cx| {
1968                conversation_view.set_work_dirs(new_work_dirs.clone(), cx);
1969            });
1970        }
1971
1972        for conversation_view in self.background_threads.values() {
1973            conversation_view.update(cx, |conversation_view, cx| {
1974                conversation_view.set_work_dirs(new_work_dirs.clone(), cx);
1975            });
1976        }
1977    }
1978
1979    fn retain_running_thread(&mut self, old_view: ActiveView, cx: &mut Context<Self>) {
1980        let ActiveView::AgentThread { conversation_view } = old_view else {
1981            return;
1982        };
1983
1984        let Some(thread_view) = conversation_view.read(cx).root_thread(cx) else {
1985            return;
1986        };
1987
1988        if thread_view.read(cx).thread.read(cx).entries().is_empty() {
1989            return;
1990        }
1991
1992        self.background_threads
1993            .insert(thread_view.read(cx).id.clone(), conversation_view);
1994        self.cleanup_background_threads(cx);
1995    }
1996
1997    /// We keep threads that are:
1998    /// - Still running
1999    /// - Do not support reloading the full session
2000    /// - Have had the most recent events (up to 5 idle threads)
2001    fn cleanup_background_threads(&mut self, cx: &App) {
2002        let mut potential_removals = self
2003            .background_threads
2004            .iter()
2005            .filter(|(_id, view)| {
2006                let Some(thread_view) = view.read(cx).root_thread(cx) else {
2007                    return true;
2008                };
2009                let thread = thread_view.read(cx).thread.read(cx);
2010                thread.connection().supports_load_session() && thread.status() == ThreadStatus::Idle
2011            })
2012            .collect::<Vec<_>>();
2013
2014        const MAX_IDLE_BACKGROUND_THREADS: usize = 5;
2015
2016        potential_removals.sort_unstable_by_key(|(_, view)| view.read(cx).updated_at(cx));
2017        let n = potential_removals
2018            .len()
2019            .saturating_sub(MAX_IDLE_BACKGROUND_THREADS);
2020        let to_remove = potential_removals
2021            .into_iter()
2022            .map(|(id, _)| id.clone())
2023            .take(n)
2024            .collect::<Vec<_>>();
2025        for id in to_remove {
2026            self.background_threads.remove(&id);
2027        }
2028    }
2029
2030    pub(crate) fn active_native_agent_thread(&self, cx: &App) -> Option<Entity<agent::Thread>> {
2031        match &self.active_view {
2032            ActiveView::AgentThread {
2033                conversation_view, ..
2034            } => conversation_view.read(cx).as_native_thread(cx),
2035            _ => None,
2036        }
2037    }
2038
2039    fn set_active_view(
2040        &mut self,
2041        new_view: ActiveView,
2042        focus: bool,
2043        window: &mut Window,
2044        cx: &mut Context<Self>,
2045    ) {
2046        let was_in_agent_history = matches!(self.active_view, ActiveView::History { .. });
2047        let current_is_uninitialized = matches!(self.active_view, ActiveView::Uninitialized);
2048        let current_is_history = matches!(self.active_view, ActiveView::History { .. });
2049        let new_is_history = matches!(new_view, ActiveView::History { .. });
2050
2051        let current_is_config = matches!(self.active_view, ActiveView::Configuration);
2052        let new_is_config = matches!(new_view, ActiveView::Configuration);
2053
2054        let current_is_overlay = current_is_history || current_is_config;
2055        let new_is_overlay = new_is_history || new_is_config;
2056
2057        if current_is_uninitialized || (current_is_overlay && !new_is_overlay) {
2058            self.active_view = new_view;
2059        } else if !current_is_overlay && new_is_overlay {
2060            self.previous_view = Some(std::mem::replace(&mut self.active_view, new_view));
2061        } else {
2062            let old_view = std::mem::replace(&mut self.active_view, new_view);
2063            if !new_is_overlay {
2064                if let Some(previous) = self.previous_view.take() {
2065                    self.retain_running_thread(previous, cx);
2066                }
2067            }
2068            self.retain_running_thread(old_view, cx);
2069        }
2070
2071        // Subscribe to the active ThreadView's events (e.g. FirstSendRequested)
2072        // so the panel can intercept the first send for worktree creation.
2073        // Re-subscribe whenever the ConnectionView changes, since the inner
2074        // ThreadView may have been replaced (e.g. navigating between threads).
2075        self._active_view_observation = match &self.active_view {
2076            ActiveView::AgentThread { conversation_view } => {
2077                self._thread_view_subscription =
2078                    Self::subscribe_to_active_thread_view(conversation_view, window, cx);
2079                let focus_handle = conversation_view.focus_handle(cx);
2080                self._active_thread_focus_subscription =
2081                    Some(cx.on_focus_in(&focus_handle, window, |_this, _window, cx| {
2082                        cx.emit(AgentPanelEvent::ThreadFocused);
2083                        cx.notify();
2084                    }));
2085                Some(cx.observe_in(
2086                    conversation_view,
2087                    window,
2088                    |this, server_view, window, cx| {
2089                        this._thread_view_subscription =
2090                            Self::subscribe_to_active_thread_view(&server_view, window, cx);
2091                        cx.emit(AgentPanelEvent::ActiveViewChanged);
2092                        this.serialize(cx);
2093                        cx.notify();
2094                    },
2095                ))
2096            }
2097            _ => {
2098                self._thread_view_subscription = None;
2099                self._active_thread_focus_subscription = None;
2100                None
2101            }
2102        };
2103
2104        if let ActiveView::History { view } = &self.active_view {
2105            if !was_in_agent_history {
2106                view.update(cx, |view, cx| {
2107                    view.history()
2108                        .update(cx, |history, cx| history.refresh_full_history(cx))
2109                });
2110            }
2111        }
2112
2113        if focus {
2114            self.focus_handle(cx).focus(window, cx);
2115        }
2116        cx.emit(AgentPanelEvent::ActiveViewChanged);
2117    }
2118
2119    fn populate_recently_updated_menu_section(
2120        mut menu: ContextMenu,
2121        panel: Entity<Self>,
2122        view: Entity<ThreadHistoryView>,
2123        cx: &mut Context<ContextMenu>,
2124    ) -> ContextMenu {
2125        let entries = view
2126            .read(cx)
2127            .history()
2128            .read(cx)
2129            .sessions()
2130            .iter()
2131            .take(RECENTLY_UPDATED_MENU_LIMIT)
2132            .cloned()
2133            .collect::<Vec<_>>();
2134
2135        if entries.is_empty() {
2136            return menu;
2137        }
2138
2139        menu = menu.header("Recently Updated");
2140
2141        for entry in entries {
2142            let title = entry
2143                .title
2144                .as_ref()
2145                .filter(|title| !title.is_empty())
2146                .cloned()
2147                .unwrap_or_else(|| SharedString::new_static(DEFAULT_THREAD_TITLE));
2148
2149            menu = menu.entry(title, None, {
2150                let panel = panel.downgrade();
2151                let entry = entry.clone();
2152                move |window, cx| {
2153                    let entry = entry.clone();
2154                    panel
2155                        .update(cx, move |this, cx| {
2156                            if let Some(agent) = this.selected_agent() {
2157                                this.load_agent_thread(
2158                                    agent,
2159                                    entry.session_id.clone(),
2160                                    entry.work_dirs.clone(),
2161                                    entry.title.clone(),
2162                                    true,
2163                                    window,
2164                                    cx,
2165                                );
2166                            }
2167                        })
2168                        .ok();
2169                }
2170            });
2171        }
2172
2173        menu.separator()
2174    }
2175
2176    fn subscribe_to_active_thread_view(
2177        server_view: &Entity<ConversationView>,
2178        window: &mut Window,
2179        cx: &mut Context<Self>,
2180    ) -> Option<Subscription> {
2181        server_view.read(cx).active_thread().cloned().map(|tv| {
2182            cx.subscribe_in(
2183                &tv,
2184                window,
2185                |this, view, event: &AcpThreadViewEvent, window, cx| match event {
2186                    AcpThreadViewEvent::FirstSendRequested { content } => {
2187                        this.handle_first_send_requested(view.clone(), content.clone(), window, cx);
2188                    }
2189                    AcpThreadViewEvent::MessageSentOrQueued => {
2190                        let session_id = view.read(cx).thread.read(cx).session_id().clone();
2191                        cx.emit(AgentPanelEvent::MessageSentOrQueued { session_id });
2192                    }
2193                },
2194            )
2195        })
2196    }
2197
2198    pub fn start_thread_in(&self) -> &StartThreadIn {
2199        &self.start_thread_in
2200    }
2201
2202    fn set_start_thread_in(
2203        &mut self,
2204        action: &StartThreadIn,
2205        window: &mut Window,
2206        cx: &mut Context<Self>,
2207    ) {
2208        let new_target = match action {
2209            StartThreadIn::LocalProject => StartThreadIn::LocalProject,
2210            StartThreadIn::NewWorktree { .. } => {
2211                if !self.project_has_git_repository(cx) {
2212                    log::error!(
2213                        "set_start_thread_in: cannot use worktree mode without a git repository"
2214                    );
2215                    return;
2216                }
2217                if self.project.read(cx).is_via_collab() {
2218                    log::error!(
2219                        "set_start_thread_in: cannot use worktree mode in a collab project"
2220                    );
2221                    return;
2222                }
2223                action.clone()
2224            }
2225            StartThreadIn::LinkedWorktree { .. } => {
2226                if !self.project_has_git_repository(cx) {
2227                    log::error!(
2228                        "set_start_thread_in: cannot use LinkedWorktree without a git repository"
2229                    );
2230                    return;
2231                }
2232                if self.project.read(cx).is_via_collab() {
2233                    log::error!(
2234                        "set_start_thread_in: cannot use LinkedWorktree in a collab project"
2235                    );
2236                    return;
2237                }
2238                action.clone()
2239            }
2240        };
2241        self.start_thread_in = new_target;
2242        if let Some(thread) = self.active_thread_view(cx) {
2243            thread.update(cx, |thread, cx| thread.focus_handle(cx).focus(window, cx));
2244        }
2245        self.serialize(cx);
2246        cx.notify();
2247    }
2248
2249    fn cycle_start_thread_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
2250        let next = match &self.start_thread_in {
2251            StartThreadIn::LocalProject => StartThreadIn::NewWorktree {
2252                worktree_name: None,
2253                branch_target: NewWorktreeBranchTarget::default(),
2254            },
2255            StartThreadIn::NewWorktree { .. } | StartThreadIn::LinkedWorktree { .. } => {
2256                StartThreadIn::LocalProject
2257            }
2258        };
2259        self.set_start_thread_in(&next, window, cx);
2260    }
2261
2262    fn reset_start_thread_in_to_default(&mut self, cx: &mut Context<Self>) {
2263        use settings::{NewThreadLocation, Settings};
2264        let default = AgentSettings::get_global(cx).new_thread_location;
2265        let start_thread_in = match default {
2266            NewThreadLocation::LocalProject => StartThreadIn::LocalProject,
2267            NewThreadLocation::NewWorktree => {
2268                if self.project_has_git_repository(cx) {
2269                    StartThreadIn::NewWorktree {
2270                        worktree_name: None,
2271                        branch_target: NewWorktreeBranchTarget::default(),
2272                    }
2273                } else {
2274                    StartThreadIn::LocalProject
2275                }
2276            }
2277        };
2278        if self.start_thread_in != start_thread_in {
2279            self.start_thread_in = start_thread_in;
2280            self.serialize(cx);
2281            cx.notify();
2282        }
2283    }
2284
2285    fn sync_start_thread_in_with_git_state(&mut self, cx: &mut Context<Self>) {
2286        if matches!(self.start_thread_in, StartThreadIn::LocalProject) {
2287            return;
2288        }
2289
2290        let visible_worktree_paths: Vec<_> = self
2291            .project
2292            .read(cx)
2293            .visible_worktrees(cx)
2294            .map(|worktree| worktree.read(cx).abs_path().to_path_buf())
2295            .collect();
2296        let repositories = self.project.read(cx).repositories(cx);
2297        let linked_worktrees = if repositories.len() > 1 {
2298            Vec::new()
2299        } else {
2300            repositories
2301                .values()
2302                .flat_map(|repo| repo.read(cx).linked_worktrees().iter().cloned())
2303                .filter(|worktree| !visible_worktree_paths.contains(&worktree.path))
2304                .collect::<Vec<_>>()
2305        };
2306
2307        let updated_start_thread_in = match &self.start_thread_in {
2308            StartThreadIn::NewWorktree {
2309                worktree_name: Some(worktree_name),
2310                branch_target,
2311            } => {
2312                let normalized_worktree_name = worktree_name.replace(' ', "-");
2313                linked_worktrees
2314                    .iter()
2315                    .find(|worktree| {
2316                        worktree.display_name() == normalized_worktree_name
2317                            && self.linked_worktree_matches_branch_target(
2318                                worktree,
2319                                branch_target,
2320                                cx,
2321                            )
2322                    })
2323                    .map(|worktree| StartThreadIn::LinkedWorktree {
2324                        path: worktree.path.clone(),
2325                        display_name: worktree.display_name().to_string(),
2326                    })
2327            }
2328            StartThreadIn::LinkedWorktree { path, .. } => linked_worktrees
2329                .iter()
2330                .find(|worktree| worktree.path == *path)
2331                .map(|worktree| StartThreadIn::LinkedWorktree {
2332                    path: worktree.path.clone(),
2333                    display_name: worktree.display_name().to_string(),
2334                })
2335                .or(Some(StartThreadIn::LocalProject)),
2336            _ => None,
2337        };
2338
2339        if let Some(updated_start_thread_in) = updated_start_thread_in {
2340            if self.start_thread_in != updated_start_thread_in {
2341                self.start_thread_in = updated_start_thread_in;
2342                self.serialize(cx);
2343            }
2344            cx.notify();
2345        }
2346    }
2347
2348    fn linked_worktree_matches_branch_target(
2349        &self,
2350        worktree: &git::repository::Worktree,
2351        branch_target: &NewWorktreeBranchTarget,
2352        cx: &App,
2353    ) -> bool {
2354        let active_repository = self.project.read(cx).active_repository(cx);
2355        let current_branch_name = active_repository.as_ref().and_then(|repo| {
2356            repo.read(cx)
2357                .branch
2358                .as_ref()
2359                .map(|branch| branch.name().to_string())
2360        });
2361        let existing_branch_names = active_repository
2362            .as_ref()
2363            .map(|repo| {
2364                repo.read(cx)
2365                    .branch_list
2366                    .iter()
2367                    .map(|branch| branch.name().to_string())
2368                    .collect::<HashSet<_>>()
2369            })
2370            .unwrap_or_default();
2371
2372        match branch_target {
2373            NewWorktreeBranchTarget::CurrentBranch => {
2374                current_branch_name.as_deref() == worktree.branch_name()
2375            }
2376            NewWorktreeBranchTarget::ExistingBranch { name } => {
2377                existing_branch_names.contains(name)
2378                    && worktree.branch_name() == Some(name.as_str())
2379            }
2380            NewWorktreeBranchTarget::CreateBranch { name, .. } => {
2381                !existing_branch_names.contains(name)
2382                    && worktree.branch_name() == Some(name.as_str())
2383            }
2384        }
2385    }
2386
2387    pub(crate) fn selected_agent(&self) -> Option<Agent> {
2388        Some(self.selected_agent.clone())
2389    }
2390
2391    fn sync_agent_servers_from_extensions(&mut self, cx: &mut Context<Self>) {
2392        if let Some(extension_store) = ExtensionStore::try_global(cx) {
2393            let (manifests, extensions_dir) = {
2394                let store = extension_store.read(cx);
2395                let installed = store.installed_extensions();
2396                let manifests: Vec<_> = installed
2397                    .iter()
2398                    .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
2399                    .collect();
2400                let extensions_dir = paths::extensions_dir().join("installed");
2401                (manifests, extensions_dir)
2402            };
2403
2404            self.project.update(cx, |project, cx| {
2405                project.agent_server_store().update(cx, |store, cx| {
2406                    let manifest_refs: Vec<_> = manifests
2407                        .iter()
2408                        .map(|(id, manifest)| (id.as_ref(), manifest.as_ref()))
2409                        .collect();
2410                    store.sync_extension_agents(manifest_refs, extensions_dir, cx);
2411                });
2412            });
2413        }
2414    }
2415
2416    pub fn new_agent_thread_with_external_source_prompt(
2417        &mut self,
2418        external_source_prompt: Option<ExternalSourcePrompt>,
2419        window: &mut Window,
2420        cx: &mut Context<Self>,
2421    ) {
2422        self.external_thread(
2423            None,
2424            None,
2425            None,
2426            None,
2427            external_source_prompt.map(AgentInitialContent::from),
2428            true,
2429            window,
2430            cx,
2431        );
2432    }
2433
2434    pub fn new_agent_thread(&mut self, agent: Agent, window: &mut Window, cx: &mut Context<Self>) {
2435        self.reset_start_thread_in_to_default(cx);
2436        self.new_agent_thread_inner(agent, true, window, cx);
2437    }
2438
2439    fn new_agent_thread_inner(
2440        &mut self,
2441        agent: Agent,
2442        focus: bool,
2443        window: &mut Window,
2444        cx: &mut Context<Self>,
2445    ) {
2446        let initial_content = self.take_active_draft_initial_content(cx);
2447        self.external_thread(
2448            Some(agent),
2449            None,
2450            None,
2451            None,
2452            initial_content,
2453            focus,
2454            window,
2455            cx,
2456        );
2457    }
2458
2459    pub fn load_agent_thread(
2460        &mut self,
2461        agent: Agent,
2462        session_id: acp::SessionId,
2463        work_dirs: Option<PathList>,
2464        title: Option<SharedString>,
2465        focus: bool,
2466        window: &mut Window,
2467        cx: &mut Context<Self>,
2468    ) {
2469        if let Some(store) = ThreadMetadataStore::try_global(cx) {
2470            store.update(cx, |store, cx| store.unarchive(&session_id, cx));
2471        }
2472
2473        if let Some(conversation_view) = self.background_threads.remove(&session_id) {
2474            self.set_active_view(
2475                ActiveView::AgentThread { conversation_view },
2476                focus,
2477                window,
2478                cx,
2479            );
2480            return;
2481        }
2482
2483        if let ActiveView::AgentThread { conversation_view } = &self.active_view {
2484            if conversation_view
2485                .read(cx)
2486                .active_thread()
2487                .map(|t| t.read(cx).id.clone())
2488                == Some(session_id.clone())
2489            {
2490                cx.emit(AgentPanelEvent::ActiveViewChanged);
2491                return;
2492            }
2493        }
2494
2495        if let Some(ActiveView::AgentThread { conversation_view }) = &self.previous_view {
2496            if conversation_view
2497                .read(cx)
2498                .active_thread()
2499                .map(|t| t.read(cx).id.clone())
2500                == Some(session_id.clone())
2501            {
2502                let view = self.previous_view.take().unwrap();
2503                self.set_active_view(view, focus, window, cx);
2504                return;
2505            }
2506        }
2507
2508        self.external_thread(
2509            Some(agent),
2510            Some(session_id),
2511            work_dirs,
2512            title,
2513            None,
2514            focus,
2515            window,
2516            cx,
2517        );
2518    }
2519
2520    pub(crate) fn create_agent_thread(
2521        &mut self,
2522        server: Rc<dyn AgentServer>,
2523        resume_session_id: Option<acp::SessionId>,
2524        work_dirs: Option<PathList>,
2525        title: Option<SharedString>,
2526        initial_content: Option<AgentInitialContent>,
2527        workspace: WeakEntity<Workspace>,
2528        project: Entity<Project>,
2529        agent: Agent,
2530        focus: bool,
2531        window: &mut Window,
2532        cx: &mut Context<Self>,
2533    ) {
2534        if self.selected_agent != agent {
2535            self.selected_agent = agent.clone();
2536            self.serialize(cx);
2537        }
2538
2539        cx.background_spawn({
2540            let kvp = KeyValueStore::global(cx);
2541            let agent = agent.clone();
2542            async move {
2543                write_global_last_used_agent(kvp, agent).await;
2544            }
2545        })
2546        .detach();
2547
2548        let thread_store = server
2549            .clone()
2550            .downcast::<agent::NativeAgentServer>()
2551            .is_some()
2552            .then(|| self.thread_store.clone());
2553
2554        let connection_store = self.connection_store.clone();
2555
2556        let conversation_view = cx.new(|cx| {
2557            crate::ConversationView::new(
2558                server,
2559                connection_store,
2560                agent,
2561                resume_session_id,
2562                work_dirs,
2563                title,
2564                initial_content,
2565                workspace.clone(),
2566                project,
2567                thread_store,
2568                self.prompt_store.clone(),
2569                window,
2570                cx,
2571            )
2572        });
2573
2574        cx.observe(&conversation_view, |this, server_view, cx| {
2575            let is_active = this
2576                .active_conversation_view()
2577                .is_some_and(|active| active.entity_id() == server_view.entity_id());
2578            if is_active {
2579                cx.emit(AgentPanelEvent::ActiveViewChanged);
2580                this.serialize(cx);
2581            } else {
2582                cx.emit(AgentPanelEvent::BackgroundThreadChanged);
2583            }
2584            cx.notify();
2585        })
2586        .detach();
2587
2588        self.set_active_view(
2589            ActiveView::AgentThread { conversation_view },
2590            focus,
2591            window,
2592            cx,
2593        );
2594    }
2595
2596    fn active_thread_has_messages(&self, cx: &App) -> bool {
2597        self.active_agent_thread(cx)
2598            .is_some_and(|thread| !thread.read(cx).entries().is_empty())
2599    }
2600
2601    pub fn active_thread_is_draft(&self, cx: &App) -> bool {
2602        self.active_conversation_view().is_some() && !self.active_thread_has_messages(cx)
2603    }
2604
2605    fn handle_first_send_requested(
2606        &mut self,
2607        thread_view: Entity<ThreadView>,
2608        content: Vec<acp::ContentBlock>,
2609        window: &mut Window,
2610        cx: &mut Context<Self>,
2611    ) {
2612        match &self.start_thread_in {
2613            StartThreadIn::NewWorktree {
2614                worktree_name,
2615                branch_target,
2616            } => {
2617                self.handle_worktree_requested(
2618                    content,
2619                    WorktreeCreationArgs::New {
2620                        worktree_name: worktree_name.clone(),
2621                        branch_target: branch_target.clone(),
2622                    },
2623                    window,
2624                    cx,
2625                );
2626            }
2627            StartThreadIn::LinkedWorktree { path, .. } => {
2628                self.handle_worktree_requested(
2629                    content,
2630                    WorktreeCreationArgs::Linked {
2631                        worktree_path: path.clone(),
2632                    },
2633                    window,
2634                    cx,
2635                );
2636            }
2637            StartThreadIn::LocalProject => {
2638                cx.defer_in(window, move |_this, window, cx| {
2639                    thread_view.update(cx, |thread_view, cx| {
2640                        let editor = thread_view.message_editor.clone();
2641                        thread_view.send_impl(editor, window, cx);
2642                    });
2643                });
2644            }
2645        }
2646    }
2647
2648    // TODO: The mapping from workspace root paths to git repositories needs a
2649    // unified approach across the codebase: this method, `sidebar::is_root_repo`,
2650    // thread persistence (which PathList is saved to the database), and thread
2651    // querying (which PathList is used to read threads back). All of these need
2652    // to agree on how repos are resolved for a given workspace, especially in
2653    // multi-root and nested-repo configurations.
2654    /// Partitions the project's visible worktrees into git-backed repositories
2655    /// and plain (non-git) paths. Git repos will have worktrees created for
2656    /// them; non-git paths are carried over to the new workspace as-is.
2657    ///
2658    /// When multiple worktrees map to the same repository, the most specific
2659    /// match wins (deepest work directory path), with a deterministic
2660    /// tie-break on entity id. Each repository appears at most once.
2661    fn classify_worktrees(
2662        &self,
2663        cx: &App,
2664    ) -> (Vec<Entity<project::git_store::Repository>>, Vec<PathBuf>) {
2665        let project = &self.project;
2666        let repositories = project.read(cx).repositories(cx).clone();
2667        let mut git_repos: Vec<Entity<project::git_store::Repository>> = Vec::new();
2668        let mut non_git_paths: Vec<PathBuf> = Vec::new();
2669        let mut seen_repo_ids = std::collections::HashSet::new();
2670
2671        for worktree in project.read(cx).visible_worktrees(cx) {
2672            let wt_path = worktree.read(cx).abs_path();
2673
2674            let matching_repo = repositories
2675                .iter()
2676                .filter_map(|(id, repo)| {
2677                    let work_dir = repo.read(cx).work_directory_abs_path.clone();
2678                    if wt_path.starts_with(work_dir.as_ref())
2679                        || work_dir.starts_with(wt_path.as_ref())
2680                    {
2681                        Some((*id, repo.clone(), work_dir.as_ref().components().count()))
2682                    } else {
2683                        None
2684                    }
2685                })
2686                .max_by(
2687                    |(left_id, _left_repo, left_depth), (right_id, _right_repo, right_depth)| {
2688                        left_depth
2689                            .cmp(right_depth)
2690                            .then_with(|| left_id.cmp(right_id))
2691                    },
2692                );
2693
2694            if let Some((id, repo, _)) = matching_repo {
2695                if seen_repo_ids.insert(id) {
2696                    git_repos.push(repo);
2697                }
2698            } else {
2699                non_git_paths.push(wt_path.to_path_buf());
2700            }
2701        }
2702
2703        (git_repos, non_git_paths)
2704    }
2705
2706    fn resolve_worktree_branch_target(
2707        branch_target: &NewWorktreeBranchTarget,
2708        existing_branches: &HashSet<String>,
2709        occupied_branches: &HashSet<String>,
2710    ) -> Result<(String, bool, Option<String>)> {
2711        let generate_branch_name = || -> Result<String> {
2712            let refs: Vec<&str> = existing_branches.iter().map(|s| s.as_str()).collect();
2713            let mut rng = rand::rng();
2714            crate::branch_names::generate_branch_name(&refs, &mut rng)
2715                .ok_or_else(|| anyhow!("Failed to generate a unique branch name"))
2716        };
2717
2718        match branch_target {
2719            NewWorktreeBranchTarget::CreateBranch { name, from_ref } => {
2720                Ok((name.clone(), false, from_ref.clone()))
2721            }
2722            NewWorktreeBranchTarget::ExistingBranch { name } => {
2723                if occupied_branches.contains(name) {
2724                    Ok((generate_branch_name()?, false, Some(name.clone())))
2725                } else {
2726                    Ok((name.clone(), true, None))
2727                }
2728            }
2729            NewWorktreeBranchTarget::CurrentBranch => Ok((generate_branch_name()?, false, None)),
2730        }
2731    }
2732
2733    /// Kicks off an async git-worktree creation for each repository. Returns:
2734    ///
2735    /// - `creation_infos`: a vec of `(repo, new_path, receiver)` tuples—the
2736    ///   receiver resolves once the git worktree command finishes.
2737    /// - `path_remapping`: `(old_work_dir, new_worktree_path)` pairs used
2738    ///   later to remap open editor tabs into the new workspace.
2739    fn start_worktree_creations(
2740        git_repos: &[Entity<project::git_store::Repository>],
2741        worktree_name: Option<String>,
2742        branch_name: &str,
2743        use_existing_branch: bool,
2744        start_point: Option<String>,
2745        worktree_directory_setting: &str,
2746        cx: &mut Context<Self>,
2747    ) -> Result<(
2748        Vec<(
2749            Entity<project::git_store::Repository>,
2750            PathBuf,
2751            futures::channel::oneshot::Receiver<Result<()>>,
2752        )>,
2753        Vec<(PathBuf, PathBuf)>,
2754    )> {
2755        let mut creation_infos = Vec::new();
2756        let mut path_remapping = Vec::new();
2757
2758        let worktree_name = worktree_name.unwrap_or_else(|| branch_name.to_string());
2759
2760        for repo in git_repos {
2761            let (work_dir, new_path, receiver) = repo.update(cx, |repo, _cx| {
2762                let new_path =
2763                    repo.path_for_new_linked_worktree(&worktree_name, worktree_directory_setting)?;
2764                let target = if use_existing_branch {
2765                    debug_assert!(
2766                        git_repos.len() == 1,
2767                        "use_existing_branch should only be true for a single repo"
2768                    );
2769                    git::repository::CreateWorktreeTarget::ExistingBranch {
2770                        branch_name: branch_name.to_string(),
2771                    }
2772                } else {
2773                    git::repository::CreateWorktreeTarget::NewBranch {
2774                        branch_name: branch_name.to_string(),
2775                        base_sha: start_point.clone(),
2776                    }
2777                };
2778                let receiver = repo.create_worktree(target, new_path.clone());
2779                let work_dir = repo.work_directory_abs_path.clone();
2780                anyhow::Ok((work_dir, new_path, receiver))
2781            })?;
2782            path_remapping.push((work_dir.to_path_buf(), new_path.clone()));
2783            creation_infos.push((repo.clone(), new_path, receiver));
2784        }
2785
2786        Ok((creation_infos, path_remapping))
2787    }
2788
2789    /// Waits for every in-flight worktree creation to complete. If any
2790    /// creation fails, all successfully-created worktrees are rolled back
2791    /// (removed) so the project isn't left in a half-migrated state.
2792    async fn await_and_rollback_on_failure(
2793        creation_infos: Vec<(
2794            Entity<project::git_store::Repository>,
2795            PathBuf,
2796            futures::channel::oneshot::Receiver<Result<()>>,
2797        )>,
2798        fs: Arc<dyn Fs>,
2799        cx: &mut AsyncWindowContext,
2800    ) -> Result<Vec<PathBuf>> {
2801        let mut created_paths: Vec<PathBuf> = Vec::new();
2802        let mut repos_and_paths: Vec<(Entity<project::git_store::Repository>, PathBuf)> =
2803            Vec::new();
2804        let mut first_error: Option<anyhow::Error> = None;
2805
2806        for (repo, new_path, receiver) in creation_infos {
2807            repos_and_paths.push((repo.clone(), new_path.clone()));
2808            match receiver.await {
2809                Ok(Ok(())) => {
2810                    created_paths.push(new_path);
2811                }
2812                Ok(Err(err)) => {
2813                    if first_error.is_none() {
2814                        first_error = Some(err);
2815                    }
2816                }
2817                Err(_canceled) => {
2818                    if first_error.is_none() {
2819                        first_error = Some(anyhow!("Worktree creation was canceled"));
2820                    }
2821                }
2822            }
2823        }
2824
2825        let Some(err) = first_error else {
2826            return Ok(created_paths);
2827        };
2828
2829        // Rollback all attempted worktrees (both successful and failed)
2830        let mut rollback_futures = Vec::new();
2831        for (rollback_repo, rollback_path) in &repos_and_paths {
2832            let receiver = cx
2833                .update(|_, cx| {
2834                    rollback_repo.update(cx, |repo, _cx| {
2835                        repo.remove_worktree(rollback_path.clone(), true)
2836                    })
2837                })
2838                .ok();
2839
2840            rollback_futures.push((rollback_path.clone(), receiver));
2841        }
2842
2843        let mut rollback_failures: Vec<String> = Vec::new();
2844        for (path, receiver_opt) in rollback_futures {
2845            let mut git_remove_failed = false;
2846
2847            if let Some(receiver) = receiver_opt {
2848                match receiver.await {
2849                    Ok(Ok(())) => {}
2850                    Ok(Err(rollback_err)) => {
2851                        log::error!(
2852                            "git worktree remove failed for {}: {rollback_err}",
2853                            path.display()
2854                        );
2855                        git_remove_failed = true;
2856                    }
2857                    Err(canceled) => {
2858                        log::error!(
2859                            "git worktree remove failed for {}: {canceled}",
2860                            path.display()
2861                        );
2862                        git_remove_failed = true;
2863                    }
2864                }
2865            } else {
2866                log::error!(
2867                    "failed to dispatch git worktree remove for {}",
2868                    path.display()
2869                );
2870                git_remove_failed = true;
2871            }
2872
2873            // `git worktree remove` normally removes this directory, but since
2874            // `git worktree remove` failed (or wasn't dispatched), manually rm the directory.
2875            if git_remove_failed {
2876                if let Err(fs_err) = fs
2877                    .remove_dir(
2878                        &path,
2879                        fs::RemoveOptions {
2880                            recursive: true,
2881                            ignore_if_not_exists: true,
2882                        },
2883                    )
2884                    .await
2885                {
2886                    let msg = format!("{}: failed to remove directory: {fs_err}", path.display());
2887                    log::error!("{}", msg);
2888                    rollback_failures.push(msg);
2889                }
2890            }
2891        }
2892        let mut error_message = format!("Failed to create worktree: {err}");
2893        if !rollback_failures.is_empty() {
2894            error_message.push_str("\n\nFailed to clean up: ");
2895            error_message.push_str(&rollback_failures.join(", "));
2896        }
2897        Err(anyhow!(error_message))
2898    }
2899
2900    fn set_worktree_creation_error(
2901        &mut self,
2902        message: SharedString,
2903        window: &mut Window,
2904        cx: &mut Context<Self>,
2905    ) {
2906        self.worktree_creation_status = Some(WorktreeCreationStatus::Error(message));
2907        if matches!(self.active_view, ActiveView::Uninitialized) {
2908            let selected_agent = self.selected_agent.clone();
2909            self.new_agent_thread(selected_agent, window, cx);
2910        }
2911        cx.notify();
2912    }
2913
2914    fn handle_worktree_requested(
2915        &mut self,
2916        content: Vec<acp::ContentBlock>,
2917        args: WorktreeCreationArgs,
2918        window: &mut Window,
2919        cx: &mut Context<Self>,
2920    ) {
2921        if matches!(
2922            self.worktree_creation_status,
2923            Some(WorktreeCreationStatus::Creating)
2924        ) {
2925            return;
2926        }
2927
2928        self.worktree_creation_status = Some(WorktreeCreationStatus::Creating);
2929        cx.notify();
2930
2931        let (git_repos, non_git_paths) = self.classify_worktrees(cx);
2932
2933        if matches!(args, WorktreeCreationArgs::New { .. }) && git_repos.is_empty() {
2934            self.set_worktree_creation_error(
2935                "No git repositories found in the project".into(),
2936                window,
2937                cx,
2938            );
2939            return;
2940        }
2941
2942        let (branch_receivers, worktree_receivers, worktree_directory_setting) =
2943            if matches!(args, WorktreeCreationArgs::New { .. }) {
2944                (
2945                    Some(
2946                        git_repos
2947                            .iter()
2948                            .map(|repo| repo.update(cx, |repo, _cx| repo.branches()))
2949                            .collect::<Vec<_>>(),
2950                    ),
2951                    Some(
2952                        git_repos
2953                            .iter()
2954                            .map(|repo| repo.update(cx, |repo, _cx| repo.worktrees()))
2955                            .collect::<Vec<_>>(),
2956                    ),
2957                    Some(
2958                        ProjectSettings::get_global(cx)
2959                            .git
2960                            .worktree_directory
2961                            .clone(),
2962                    ),
2963                )
2964            } else {
2965                (None, None, None)
2966            };
2967
2968        let active_file_path = self.workspace.upgrade().and_then(|workspace| {
2969            let workspace = workspace.read(cx);
2970            let active_item = workspace.active_item(cx)?;
2971            let project_path = active_item.project_path(cx)?;
2972            workspace
2973                .project()
2974                .read(cx)
2975                .absolute_path(&project_path, cx)
2976        });
2977
2978        let remote_connection_options = self.project.read(cx).remote_connection_options(cx);
2979
2980        if remote_connection_options.is_some() {
2981            let is_disconnected = self
2982                .project
2983                .read(cx)
2984                .remote_client()
2985                .is_some_and(|client| client.read(cx).is_disconnected());
2986            if is_disconnected {
2987                self.set_worktree_creation_error(
2988                    "Cannot create worktree: remote connection is not active".into(),
2989                    window,
2990                    cx,
2991                );
2992                return;
2993            }
2994        }
2995
2996        let workspace = self.workspace.clone();
2997        let window_handle = window
2998            .window_handle()
2999            .downcast::<workspace::MultiWorkspace>();
3000
3001        let selected_agent = self.selected_agent();
3002
3003        let task = cx.spawn_in(window, async move |this, cx| {
3004            let (all_paths, path_remapping, has_non_git) = match args {
3005                WorktreeCreationArgs::New {
3006                    worktree_name,
3007                    branch_target,
3008                } => {
3009                    let branch_receivers = branch_receivers
3010                        .expect("branch receivers must be prepared for new worktree creation");
3011                    let worktree_receivers = worktree_receivers
3012                        .expect("worktree receivers must be prepared for new worktree creation");
3013                    let worktree_directory_setting = worktree_directory_setting
3014                        .expect("worktree directory must be prepared for new worktree creation");
3015
3016                    let mut existing_branches = HashSet::default();
3017                    for result in futures::future::join_all(branch_receivers).await {
3018                        match result {
3019                            Ok(Ok(branches)) => {
3020                                for branch in branches {
3021                                    existing_branches.insert(branch.name().to_string());
3022                                }
3023                            }
3024                            Ok(Err(err)) => {
3025                                Err::<(), _>(err).log_err();
3026                            }
3027                            Err(_) => {}
3028                        }
3029                    }
3030
3031                    let mut occupied_branches = HashSet::default();
3032                    for result in futures::future::join_all(worktree_receivers).await {
3033                        match result {
3034                            Ok(Ok(worktrees)) => {
3035                                for worktree in worktrees {
3036                                    if let Some(branch_name) = worktree.branch_name() {
3037                                        occupied_branches.insert(branch_name.to_string());
3038                                    }
3039                                }
3040                            }
3041                            Ok(Err(err)) => {
3042                                Err::<(), _>(err).log_err();
3043                            }
3044                            Err(_) => {}
3045                        }
3046                    }
3047
3048                    let (branch_name, use_existing_branch, start_point) =
3049                        match Self::resolve_worktree_branch_target(
3050                            &branch_target,
3051                            &existing_branches,
3052                            &occupied_branches,
3053                        ) {
3054                            Ok(target) => target,
3055                            Err(err) => {
3056                                this.update_in(cx, |this, window, cx| {
3057                                    this.set_worktree_creation_error(
3058                                        err.to_string().into(),
3059                                        window,
3060                                        cx,
3061                                    );
3062                                })?;
3063                                return anyhow::Ok(());
3064                            }
3065                        };
3066
3067                    let (creation_infos, path_remapping) =
3068                        match this.update_in(cx, |_this, _window, cx| {
3069                            Self::start_worktree_creations(
3070                                &git_repos,
3071                                worktree_name,
3072                                &branch_name,
3073                                use_existing_branch,
3074                                start_point,
3075                                &worktree_directory_setting,
3076                                cx,
3077                            )
3078                        }) {
3079                            Ok(Ok(result)) => result,
3080                            Ok(Err(err)) | Err(err) => {
3081                                this.update_in(cx, |this, window, cx| {
3082                                    this.set_worktree_creation_error(
3083                                        format!("Failed to validate worktree directory: {err}")
3084                                            .into(),
3085                                        window,
3086                                        cx,
3087                                    );
3088                                })
3089                                .log_err();
3090                                return anyhow::Ok(());
3091                            }
3092                        };
3093
3094                    let fs = cx.update(|_, cx| <dyn Fs>::global(cx))?;
3095
3096                    let created_paths =
3097                        match Self::await_and_rollback_on_failure(creation_infos, fs, cx).await {
3098                            Ok(paths) => paths,
3099                            Err(err) => {
3100                                this.update_in(cx, |this, window, cx| {
3101                                    this.set_worktree_creation_error(
3102                                        format!("{err}").into(),
3103                                        window,
3104                                        cx,
3105                                    );
3106                                })?;
3107                                return anyhow::Ok(());
3108                            }
3109                        };
3110
3111                    let mut all_paths = created_paths;
3112                    let has_non_git = !non_git_paths.is_empty();
3113                    all_paths.extend(non_git_paths.iter().cloned());
3114                    (all_paths, path_remapping, has_non_git)
3115                }
3116                WorktreeCreationArgs::Linked { worktree_path } => {
3117                    let mut all_paths = vec![worktree_path];
3118                    let has_non_git = !non_git_paths.is_empty();
3119                    all_paths.extend(non_git_paths.iter().cloned());
3120                    (all_paths, Vec::new(), has_non_git)
3121                }
3122            };
3123
3124            if workspace.upgrade().is_none() {
3125                this.update_in(cx, |this, window, cx| {
3126                    this.set_worktree_creation_error(
3127                        "Workspace no longer available".into(),
3128                        window,
3129                        cx,
3130                    );
3131                })?;
3132                return anyhow::Ok(());
3133            }
3134
3135            let this_for_error = this.clone();
3136            if let Err(err) = Self::open_worktree_workspace_and_start_thread(
3137                this,
3138                all_paths,
3139                window_handle,
3140                active_file_path,
3141                path_remapping,
3142                non_git_paths,
3143                has_non_git,
3144                content,
3145                selected_agent,
3146                remote_connection_options,
3147                cx,
3148            )
3149            .await
3150            {
3151                this_for_error
3152                    .update_in(cx, |this, window, cx| {
3153                        this.set_worktree_creation_error(
3154                            format!("Failed to set up workspace: {err}").into(),
3155                            window,
3156                            cx,
3157                        );
3158                    })
3159                    .log_err();
3160            }
3161            anyhow::Ok(())
3162        });
3163
3164        self._worktree_creation_task = Some(cx.background_spawn(async move {
3165            task.await.log_err();
3166        }));
3167    }
3168
3169    async fn open_worktree_workspace_and_start_thread(
3170        this: WeakEntity<Self>,
3171        all_paths: Vec<PathBuf>,
3172        window_handle: Option<gpui::WindowHandle<workspace::MultiWorkspace>>,
3173        active_file_path: Option<PathBuf>,
3174        path_remapping: Vec<(PathBuf, PathBuf)>,
3175        non_git_paths: Vec<PathBuf>,
3176        has_non_git: bool,
3177        content: Vec<acp::ContentBlock>,
3178        selected_agent: Option<Agent>,
3179        remote_connection_options: Option<RemoteConnectionOptions>,
3180        cx: &mut AsyncWindowContext,
3181    ) -> Result<()> {
3182        let window_handle = window_handle
3183            .ok_or_else(|| anyhow!("No window handle available for workspace creation"))?;
3184
3185        let workspace_task = window_handle.update(cx, |multi_workspace, window, cx| {
3186            let path_list = PathList::new(&all_paths);
3187            let active_workspace = multi_workspace.workspace().clone();
3188
3189            multi_workspace.find_or_create_workspace(
3190                path_list,
3191                remote_connection_options,
3192                None,
3193                move |connection_options, window, cx| {
3194                    remote_connection::connect_with_modal(
3195                        &active_workspace,
3196                        connection_options,
3197                        window,
3198                        cx,
3199                    )
3200                },
3201                window,
3202                cx,
3203            )
3204        })?;
3205
3206        let new_workspace = workspace_task.await?;
3207
3208        let panels_task = new_workspace.update(cx, |workspace, _cx| workspace.take_panels_task());
3209
3210        if let Some(task) = panels_task {
3211            task.await.log_err();
3212        }
3213
3214        new_workspace
3215            .update(cx, |workspace, cx| {
3216                workspace.project().read(cx).wait_for_initial_scan(cx)
3217            })
3218            .await;
3219
3220        new_workspace
3221            .update(cx, |workspace, cx| {
3222                let repos = workspace
3223                    .project()
3224                    .read(cx)
3225                    .repositories(cx)
3226                    .values()
3227                    .cloned()
3228                    .collect::<Vec<_>>();
3229
3230                let tasks = repos
3231                    .into_iter()
3232                    .map(|repo| repo.update(cx, |repo, _| repo.barrier()));
3233                futures::future::join_all(tasks)
3234            })
3235            .await;
3236
3237        let initial_content = AgentInitialContent::ContentBlock {
3238            blocks: content,
3239            auto_submit: true,
3240        };
3241
3242        window_handle.update(cx, |_multi_workspace, window, cx| {
3243            new_workspace.update(cx, |workspace, cx| {
3244                if has_non_git {
3245                    let toast_id = workspace::notifications::NotificationId::unique::<AgentPanel>();
3246                    workspace.show_toast(
3247                        workspace::Toast::new(
3248                            toast_id,
3249                            "Some project folders are not git repositories. \
3250                             They were included as-is without creating a worktree.",
3251                        ),
3252                        cx,
3253                    );
3254                }
3255
3256                // If we had an active buffer, remap its path and reopen it.
3257                let had_active_file = active_file_path.is_some();
3258                let remapped_active_path = active_file_path.and_then(|original_path| {
3259                    let best_match = path_remapping
3260                        .iter()
3261                        .filter_map(|(old_root, new_root)| {
3262                            original_path.strip_prefix(old_root).ok().map(|relative| {
3263                                (old_root.components().count(), new_root.join(relative))
3264                            })
3265                        })
3266                        .max_by_key(|(depth, _)| *depth);
3267
3268                    if let Some((_, remapped_path)) = best_match {
3269                        return Some(remapped_path);
3270                    }
3271
3272                    for non_git in &non_git_paths {
3273                        if original_path.starts_with(non_git) {
3274                            return Some(original_path);
3275                        }
3276                    }
3277                    None
3278                });
3279
3280                if had_active_file && remapped_active_path.is_none() {
3281                    log::warn!(
3282                        "Active file could not be remapped to the new worktree; it will not be reopened"
3283                    );
3284                }
3285
3286                if let Some(path) = remapped_active_path {
3287                    let open_task = workspace.open_paths(
3288                        vec![path],
3289                        workspace::OpenOptions::default(),
3290                        None,
3291                        window,
3292                        cx,
3293                    );
3294                    cx.spawn(async move |_, _| -> anyhow::Result<()> {
3295                        for item in open_task.await.into_iter().flatten() {
3296                            item?;
3297                        }
3298                        Ok(())
3299                    })
3300                    .detach_and_log_err(cx);
3301                }
3302
3303                workspace.focus_panel::<AgentPanel>(window, cx);
3304
3305                // If no active buffer was open, zoom the agent panel
3306                // (equivalent to cmd-esc fullscreen behavior).
3307                // This must happen after focus_panel, which activates
3308                // and opens the panel in the dock.
3309
3310                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
3311                    panel.update(cx, |panel, cx| {
3312                        panel.external_thread(
3313                            selected_agent,
3314                            None,
3315                            None,
3316                            None,
3317                            Some(initial_content),
3318                            true,
3319                            window,
3320                            cx,
3321                        );
3322                    });
3323                }
3324            });
3325        })?;
3326
3327        window_handle.update(cx, |multi_workspace, window, cx| {
3328            multi_workspace.activate(new_workspace.clone(), window, cx);
3329
3330            new_workspace.update(cx, |workspace, cx| {
3331                workspace.run_create_worktree_tasks(window, cx);
3332            })
3333        })?;
3334
3335        this.update_in(cx, |this, window, cx| {
3336            this.worktree_creation_status = None;
3337
3338            if let Some(thread_view) = this.active_thread_view(cx) {
3339                thread_view.update(cx, |thread_view, cx| {
3340                    thread_view
3341                        .message_editor
3342                        .update(cx, |editor, cx| editor.clear(window, cx));
3343                });
3344            }
3345
3346            this.start_thread_in = StartThreadIn::LocalProject;
3347            this.serialize(cx);
3348            cx.notify();
3349        })?;
3350
3351        anyhow::Ok(())
3352    }
3353}
3354
3355impl Focusable for AgentPanel {
3356    fn focus_handle(&self, cx: &App) -> FocusHandle {
3357        match &self.active_view {
3358            ActiveView::Uninitialized => self.focus_handle.clone(),
3359            ActiveView::AgentThread {
3360                conversation_view, ..
3361            } => conversation_view.focus_handle(cx),
3362            ActiveView::History { view } => view.read(cx).focus_handle(cx),
3363            ActiveView::Configuration => {
3364                if let Some(configuration) = self.configuration.as_ref() {
3365                    configuration.focus_handle(cx)
3366                } else {
3367                    self.focus_handle.clone()
3368                }
3369            }
3370        }
3371    }
3372}
3373
3374fn agent_panel_dock_position(cx: &App) -> DockPosition {
3375    AgentSettings::get_global(cx).dock.into()
3376}
3377
3378pub enum AgentPanelEvent {
3379    ActiveViewChanged,
3380    ThreadFocused,
3381    BackgroundThreadChanged,
3382    MessageSentOrQueued { session_id: acp::SessionId },
3383}
3384
3385impl EventEmitter<PanelEvent> for AgentPanel {}
3386impl EventEmitter<AgentPanelEvent> for AgentPanel {}
3387
3388impl Panel for AgentPanel {
3389    fn persistent_name() -> &'static str {
3390        "AgentPanel"
3391    }
3392
3393    fn panel_key() -> &'static str {
3394        AGENT_PANEL_KEY
3395    }
3396
3397    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
3398        agent_panel_dock_position(cx)
3399    }
3400
3401    fn position_is_valid(&self, position: DockPosition) -> bool {
3402        position != DockPosition::Bottom
3403    }
3404
3405    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
3406        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
3407            settings
3408                .agent
3409                .get_or_insert_default()
3410                .set_dock(position.into());
3411        });
3412    }
3413
3414    fn default_size(&self, window: &Window, cx: &App) -> Pixels {
3415        let settings = AgentSettings::get_global(cx);
3416        match self.position(window, cx) {
3417            DockPosition::Left | DockPosition::Right => settings.default_width,
3418            DockPosition::Bottom => settings.default_height,
3419        }
3420    }
3421
3422    fn supports_flexible_size(&self) -> bool {
3423        true
3424    }
3425
3426    fn has_flexible_size(&self, _window: &Window, cx: &App) -> bool {
3427        AgentSettings::get_global(cx).flexible
3428    }
3429
3430    fn set_flexible_size(&mut self, flexible: bool, _window: &mut Window, cx: &mut Context<Self>) {
3431        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
3432            settings
3433                .agent
3434                .get_or_insert_default()
3435                .set_flexible_size(flexible);
3436        });
3437    }
3438
3439    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
3440        if active
3441            && matches!(self.active_view, ActiveView::Uninitialized)
3442            && !matches!(
3443                self.worktree_creation_status,
3444                Some(WorktreeCreationStatus::Creating)
3445            )
3446        {
3447            let selected_agent = self.selected_agent.clone();
3448            self.new_agent_thread_inner(selected_agent, false, window, cx);
3449        }
3450    }
3451
3452    fn remote_id() -> Option<proto::PanelId> {
3453        Some(proto::PanelId::AssistantPanel)
3454    }
3455
3456    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
3457        (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
3458    }
3459
3460    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
3461        Some("Agent Panel")
3462    }
3463
3464    fn toggle_action(&self) -> Box<dyn Action> {
3465        Box::new(ToggleFocus)
3466    }
3467
3468    fn activation_priority(&self) -> u32 {
3469        0
3470    }
3471
3472    fn enabled(&self, cx: &App) -> bool {
3473        AgentSettings::get_global(cx).enabled(cx)
3474    }
3475
3476    fn is_agent_panel(&self) -> bool {
3477        true
3478    }
3479
3480    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
3481        self.zoomed
3482    }
3483
3484    fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
3485        self.zoomed = zoomed;
3486        cx.notify();
3487    }
3488}
3489
3490impl AgentPanel {
3491    fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
3492        let content = match &self.active_view {
3493            ActiveView::AgentThread { conversation_view } => {
3494                let server_view_ref = conversation_view.read(cx);
3495                let is_generating_title = server_view_ref.as_native_thread(cx).is_some()
3496                    && server_view_ref.root_thread(cx).map_or(false, |tv| {
3497                        tv.read(cx).thread.read(cx).has_provisional_title()
3498                    });
3499
3500                if let Some(title_editor) = server_view_ref
3501                    .root_thread(cx)
3502                    .map(|r| r.read(cx).title_editor.clone())
3503                {
3504                    if is_generating_title {
3505                        Label::new(DEFAULT_THREAD_TITLE)
3506                            .color(Color::Muted)
3507                            .truncate()
3508                            .with_animation(
3509                                "generating_title",
3510                                Animation::new(Duration::from_secs(2))
3511                                    .repeat()
3512                                    .with_easing(pulsating_between(0.4, 0.8)),
3513                                |label, delta| label.alpha(delta),
3514                            )
3515                            .into_any_element()
3516                    } else {
3517                        div()
3518                            .w_full()
3519                            .on_action({
3520                                let conversation_view = conversation_view.downgrade();
3521                                move |_: &menu::Confirm, window, cx| {
3522                                    if let Some(conversation_view) = conversation_view.upgrade() {
3523                                        conversation_view.focus_handle(cx).focus(window, cx);
3524                                    }
3525                                }
3526                            })
3527                            .on_action({
3528                                let conversation_view = conversation_view.downgrade();
3529                                move |_: &editor::actions::Cancel, window, cx| {
3530                                    if let Some(conversation_view) = conversation_view.upgrade() {
3531                                        conversation_view.focus_handle(cx).focus(window, cx);
3532                                    }
3533                                }
3534                            })
3535                            .child(title_editor)
3536                            .into_any_element()
3537                    }
3538                } else {
3539                    Label::new(conversation_view.read(cx).title(cx))
3540                        .color(Color::Muted)
3541                        .truncate()
3542                        .into_any_element()
3543                }
3544            }
3545            ActiveView::History { .. } => Label::new("History").truncate().into_any_element(),
3546            ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
3547            ActiveView::Uninitialized => Label::new("Agent").truncate().into_any_element(),
3548        };
3549
3550        h_flex()
3551            .key_context("TitleEditor")
3552            .id("TitleEditor")
3553            .flex_grow()
3554            .w_full()
3555            .max_w_full()
3556            .overflow_x_scroll()
3557            .child(content)
3558            .into_any()
3559    }
3560
3561    fn handle_regenerate_thread_title(conversation_view: Entity<ConversationView>, cx: &mut App) {
3562        conversation_view.update(cx, |conversation_view, cx| {
3563            if let Some(thread) = conversation_view.as_native_thread(cx) {
3564                thread.update(cx, |thread, cx| {
3565                    thread.generate_title(cx);
3566                });
3567            }
3568        });
3569    }
3570
3571    fn render_panel_options_menu(
3572        &self,
3573        _window: &mut Window,
3574        cx: &mut Context<Self>,
3575    ) -> impl IntoElement {
3576        let focus_handle = self.focus_handle(cx);
3577
3578        let conversation_view = match &self.active_view {
3579            ActiveView::AgentThread { conversation_view } => Some(conversation_view.clone()),
3580            _ => None,
3581        };
3582        let thread_with_messages = match &self.active_view {
3583            ActiveView::AgentThread { conversation_view } => {
3584                conversation_view.read(cx).has_user_submitted_prompt(cx)
3585            }
3586            _ => false,
3587        };
3588        let has_auth_methods = match &self.active_view {
3589            ActiveView::AgentThread { conversation_view } => {
3590                conversation_view.read(cx).has_auth_methods()
3591            }
3592            _ => false,
3593        };
3594
3595        PopoverMenu::new("agent-options-menu")
3596            .trigger_with_tooltip(
3597                IconButton::new("agent-options-menu", IconName::Ellipsis)
3598                    .icon_size(IconSize::Small),
3599                {
3600                    let focus_handle = focus_handle.clone();
3601                    move |_window, cx| {
3602                        Tooltip::for_action_in(
3603                            "Toggle Agent Menu",
3604                            &ToggleOptionsMenu,
3605                            &focus_handle,
3606                            cx,
3607                        )
3608                    }
3609                },
3610            )
3611            .anchor(Corner::TopRight)
3612            .with_handle(self.agent_panel_menu_handle.clone())
3613            .menu({
3614                move |window, cx| {
3615                    Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
3616                        menu = menu.context(focus_handle.clone());
3617
3618                        if thread_with_messages {
3619                            menu = menu.header("Current Thread");
3620
3621                            if let Some(conversation_view) = conversation_view.as_ref() {
3622                                menu = menu
3623                                    .entry("Regenerate Thread Title", None, {
3624                                        let conversation_view = conversation_view.clone();
3625                                        move |_, cx| {
3626                                            Self::handle_regenerate_thread_title(
3627                                                conversation_view.clone(),
3628                                                cx,
3629                                            );
3630                                        }
3631                                    })
3632                                    .separator();
3633                            }
3634                        }
3635
3636                        menu = menu
3637                            .header("MCP Servers")
3638                            .action(
3639                                "View Server Extensions",
3640                                Box::new(zed_actions::Extensions {
3641                                    category_filter: Some(
3642                                        zed_actions::ExtensionCategoryFilter::ContextServers,
3643                                    ),
3644                                    id: None,
3645                                }),
3646                            )
3647                            .action("Add Custom Server…", Box::new(AddContextServer))
3648                            .separator()
3649                            .action("Rules", Box::new(OpenRulesLibrary::default()))
3650                            .action("Profiles", Box::new(ManageProfiles::default()))
3651                            .action("Settings", Box::new(OpenSettings))
3652                            .separator()
3653                            .action("Toggle Threads Sidebar", Box::new(ToggleWorkspaceSidebar));
3654
3655                        if has_auth_methods {
3656                            menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
3657                        }
3658
3659                        menu
3660                    }))
3661                }
3662            })
3663    }
3664
3665    fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
3666        let focus_handle = self.focus_handle(cx);
3667
3668        IconButton::new("go-back", IconName::ArrowLeft)
3669            .icon_size(IconSize::Small)
3670            .on_click(cx.listener(|this, _, window, cx| {
3671                this.go_back(&workspace::GoBack, window, cx);
3672            }))
3673            .tooltip({
3674                move |_window, cx| {
3675                    Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
3676                }
3677            })
3678    }
3679
3680    fn project_has_git_repository(&self, cx: &App) -> bool {
3681        !self.project.read(cx).repositories(cx).is_empty()
3682    }
3683
3684    fn render_start_thread_in_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
3685        let focus_handle = self.focus_handle(cx);
3686
3687        let is_creating = matches!(
3688            self.worktree_creation_status,
3689            Some(WorktreeCreationStatus::Creating)
3690        );
3691
3692        let trigger_parts = self
3693            .start_thread_in
3694            .trigger_label(self.project.read(cx), cx);
3695
3696        let icon = if self.start_thread_in_menu_handle.is_deployed() {
3697            IconName::ChevronUp
3698        } else {
3699            IconName::ChevronDown
3700        };
3701
3702        let trigger_button = ButtonLike::new("thread-target-trigger")
3703            .disabled(is_creating)
3704            .when_some(trigger_parts.prefix, |this, prefix| {
3705                this.child(Label::new(prefix).color(Color::Muted))
3706            })
3707            .child(Label::new(trigger_parts.label))
3708            .when_some(trigger_parts.suffix, |this, suffix| {
3709                this.child(Label::new(suffix).color(Color::Muted))
3710            })
3711            .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted));
3712
3713        let project = self.project.clone();
3714        let current_target = self.start_thread_in.clone();
3715        let fs = self.fs.clone();
3716
3717        PopoverMenu::new("thread-target-selector")
3718            .trigger_with_tooltip(trigger_button, {
3719                move |_window, cx| {
3720                    Tooltip::for_action_in(
3721                        "Start Thread In…",
3722                        &CycleStartThreadIn,
3723                        &focus_handle,
3724                        cx,
3725                    )
3726                }
3727            })
3728            .menu(move |window, cx| {
3729                let fs = fs.clone();
3730                Some(cx.new(|cx| {
3731                    ThreadWorktreePicker::new(project.clone(), &current_target, fs, window, cx)
3732                }))
3733            })
3734            .with_handle(self.start_thread_in_menu_handle.clone())
3735            .anchor(Corner::TopLeft)
3736            .offset(gpui::Point {
3737                x: px(1.0),
3738                y: px(1.0),
3739            })
3740    }
3741
3742    fn render_new_worktree_branch_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
3743        let is_creating = matches!(
3744            self.worktree_creation_status,
3745            Some(WorktreeCreationStatus::Creating)
3746        );
3747
3748        let project_ref = self.project.read(cx);
3749        let trigger_parts = self
3750            .start_thread_in
3751            .branch_trigger_label(project_ref, cx)
3752            .unwrap_or_else(|| StartThreadInLabel {
3753                prefix: Some("From:".into()),
3754                label: "HEAD".into(),
3755                suffix: None,
3756            });
3757
3758        let icon = if self.thread_branch_menu_handle.is_deployed() {
3759            IconName::ChevronUp
3760        } else {
3761            IconName::ChevronDown
3762        };
3763
3764        let trigger_button = ButtonLike::new("thread-branch-trigger")
3765            .disabled(is_creating)
3766            .when_some(trigger_parts.prefix, |this, prefix| {
3767                this.child(Label::new(prefix).color(Color::Muted))
3768            })
3769            .child(Label::new(trigger_parts.label))
3770            .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted));
3771
3772        let project = self.project.clone();
3773        let current_target = self.start_thread_in.clone();
3774
3775        PopoverMenu::new("thread-branch-selector")
3776            .trigger_with_tooltip(trigger_button, Tooltip::text("Choose Worktree Branch…"))
3777            .menu(move |window, cx| {
3778                Some(cx.new(|cx| {
3779                    ThreadBranchPicker::new(project.clone(), &current_target, window, cx)
3780                }))
3781            })
3782            .with_handle(self.thread_branch_menu_handle.clone())
3783            .anchor(Corner::TopLeft)
3784            .offset(gpui::Point {
3785                x: px(1.0),
3786                y: px(1.0),
3787            })
3788    }
3789
3790    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3791        let agent_server_store = self.project.read(cx).agent_server_store().clone();
3792        let has_visible_worktrees = self.project.read(cx).visible_worktrees(cx).next().is_some();
3793        let focus_handle = self.focus_handle(cx);
3794
3795        let (selected_agent_custom_icon, selected_agent_label) =
3796            if let Agent::Custom { id, .. } = &self.selected_agent {
3797                let store = agent_server_store.read(cx);
3798                let icon = store.agent_icon(&id);
3799
3800                let label = store
3801                    .agent_display_name(&id)
3802                    .unwrap_or_else(|| self.selected_agent.label());
3803                (icon, label)
3804            } else {
3805                (None, self.selected_agent.label())
3806            };
3807
3808        let active_thread = match &self.active_view {
3809            ActiveView::AgentThread { conversation_view } => {
3810                conversation_view.read(cx).as_native_thread(cx)
3811            }
3812            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
3813                None
3814            }
3815        };
3816
3817        let new_thread_menu_builder: Rc<
3818            dyn Fn(&mut Window, &mut App) -> Option<Entity<ContextMenu>>,
3819        > = {
3820            let selected_agent = self.selected_agent.clone();
3821            let is_agent_selected = move |agent: Agent| selected_agent == agent;
3822
3823            let workspace = self.workspace.clone();
3824            let is_via_collab = workspace
3825                .update(cx, |workspace, cx| {
3826                    workspace.project().read(cx).is_via_collab()
3827                })
3828                .unwrap_or_default();
3829
3830            let focus_handle = focus_handle.clone();
3831            let agent_server_store = agent_server_store;
3832
3833            Rc::new(move |window, cx| {
3834                telemetry::event!("New Thread Clicked");
3835
3836                let active_thread = active_thread.clone();
3837                Some(ContextMenu::build(window, cx, |menu, _window, cx| {
3838                    menu.context(focus_handle.clone())
3839                        .when_some(active_thread, |this, active_thread| {
3840                            let thread = active_thread.read(cx);
3841
3842                            if !thread.is_empty() {
3843                                let session_id = thread.id().clone();
3844                                this.item(
3845                                    ContextMenuEntry::new("New From Summary")
3846                                        .icon(IconName::ThreadFromSummary)
3847                                        .icon_color(Color::Muted)
3848                                        .handler(move |window, cx| {
3849                                            window.dispatch_action(
3850                                                Box::new(NewNativeAgentThreadFromSummary {
3851                                                    from_session_id: session_id.clone(),
3852                                                }),
3853                                                cx,
3854                                            );
3855                                        }),
3856                                )
3857                            } else {
3858                                this
3859                            }
3860                        })
3861                        .item(
3862                            ContextMenuEntry::new("Zed Agent")
3863                                .when(is_agent_selected(Agent::NativeAgent), |this| {
3864                                    this.action(Box::new(NewExternalAgentThread { agent: None }))
3865                                })
3866                                .icon(IconName::ZedAgent)
3867                                .icon_color(Color::Muted)
3868                                .handler({
3869                                    let workspace = workspace.clone();
3870                                    move |window, cx| {
3871                                        if let Some(workspace) = workspace.upgrade() {
3872                                            workspace.update(cx, |workspace, cx| {
3873                                                if let Some(panel) =
3874                                                    workspace.panel::<AgentPanel>(cx)
3875                                                {
3876                                                    panel.update(cx, |panel, cx| {
3877                                                        panel.new_agent_thread(
3878                                                            Agent::NativeAgent,
3879                                                            window,
3880                                                            cx,
3881                                                        );
3882                                                    });
3883                                                }
3884                                            });
3885                                        }
3886                                    }
3887                                }),
3888                        )
3889                        .map(|mut menu| {
3890                            let agent_server_store = agent_server_store.read(cx);
3891                            let registry_store = project::AgentRegistryStore::try_global(cx);
3892                            let registry_store_ref = registry_store.as_ref().map(|s| s.read(cx));
3893
3894                            struct AgentMenuItem {
3895                                id: AgentId,
3896                                display_name: SharedString,
3897                            }
3898
3899                            let agent_items = agent_server_store
3900                                .external_agents()
3901                                .map(|agent_id| {
3902                                    let display_name = agent_server_store
3903                                        .agent_display_name(agent_id)
3904                                        .or_else(|| {
3905                                            registry_store_ref
3906                                                .as_ref()
3907                                                .and_then(|store| store.agent(agent_id))
3908                                                .map(|a| a.name().clone())
3909                                        })
3910                                        .unwrap_or_else(|| agent_id.0.clone());
3911                                    AgentMenuItem {
3912                                        id: agent_id.clone(),
3913                                        display_name,
3914                                    }
3915                                })
3916                                .sorted_unstable_by_key(|e| e.display_name.to_lowercase())
3917                                .collect::<Vec<_>>();
3918
3919                            if !agent_items.is_empty() {
3920                                menu = menu.separator().header("External Agents");
3921                            }
3922                            for item in &agent_items {
3923                                let mut entry = ContextMenuEntry::new(item.display_name.clone());
3924
3925                                let icon_path =
3926                                    agent_server_store.agent_icon(&item.id).or_else(|| {
3927                                        registry_store_ref
3928                                            .as_ref()
3929                                            .and_then(|store| store.agent(&item.id))
3930                                            .and_then(|a| a.icon_path().cloned())
3931                                    });
3932
3933                                if let Some(icon_path) = icon_path {
3934                                    entry = entry.custom_icon_svg(icon_path);
3935                                } else {
3936                                    entry = entry.icon(IconName::Sparkle);
3937                                }
3938
3939                                entry = entry
3940                                    .when(
3941                                        is_agent_selected(Agent::Custom {
3942                                            id: item.id.clone(),
3943                                        }),
3944                                        |this| {
3945                                            this.action(Box::new(NewExternalAgentThread {
3946                                                agent: None,
3947                                            }))
3948                                        },
3949                                    )
3950                                    .icon_color(Color::Muted)
3951                                    .disabled(is_via_collab)
3952                                    .handler({
3953                                        let workspace = workspace.clone();
3954                                        let agent_id = item.id.clone();
3955                                        move |window, cx| {
3956                                            if let Some(workspace) = workspace.upgrade() {
3957                                                workspace.update(cx, |workspace, cx| {
3958                                                    if let Some(panel) =
3959                                                        workspace.panel::<AgentPanel>(cx)
3960                                                    {
3961                                                        panel.update(cx, |panel, cx| {
3962                                                            panel.new_agent_thread(
3963                                                                Agent::Custom {
3964                                                                    id: agent_id.clone(),
3965                                                                },
3966                                                                window,
3967                                                                cx,
3968                                                            );
3969                                                        });
3970                                                    }
3971                                                });
3972                                            }
3973                                        }
3974                                    });
3975
3976                                menu = menu.item(entry);
3977                            }
3978
3979                            menu
3980                        })
3981                        .separator()
3982                        .item(
3983                            ContextMenuEntry::new("Add More Agents")
3984                                .icon(IconName::Plus)
3985                                .icon_color(Color::Muted)
3986                                .handler({
3987                                    move |window, cx| {
3988                                        window
3989                                            .dispatch_action(Box::new(zed_actions::AcpRegistry), cx)
3990                                    }
3991                                }),
3992                        )
3993                }))
3994            })
3995        };
3996
3997        let is_thread_loading = self
3998            .active_conversation_view()
3999            .map(|thread| thread.read(cx).is_loading())
4000            .unwrap_or(false);
4001
4002        let has_custom_icon = selected_agent_custom_icon.is_some();
4003        let selected_agent_custom_icon_for_button = selected_agent_custom_icon.clone();
4004        let selected_agent_builtin_icon = self.selected_agent.icon();
4005        let selected_agent_label_for_tooltip = selected_agent_label.clone();
4006
4007        let selected_agent = div()
4008            .id("selected_agent_icon")
4009            .when_some(selected_agent_custom_icon, |this, icon_path| {
4010                this.px_1()
4011                    .child(Icon::from_external_svg(icon_path).color(Color::Muted))
4012            })
4013            .when(!has_custom_icon, |this| {
4014                this.when_some(selected_agent_builtin_icon, |this, icon| {
4015                    this.px_1().child(Icon::new(icon).color(Color::Muted))
4016                })
4017            })
4018            .tooltip(move |_, cx| {
4019                Tooltip::with_meta(
4020                    selected_agent_label_for_tooltip.clone(),
4021                    None,
4022                    "Selected Agent",
4023                    cx,
4024                )
4025            });
4026
4027        let selected_agent = if is_thread_loading {
4028            selected_agent
4029                .with_animation(
4030                    "pulsating-icon",
4031                    Animation::new(Duration::from_secs(1))
4032                        .repeat()
4033                        .with_easing(pulsating_between(0.2, 0.6)),
4034                    |icon, delta| icon.opacity(delta),
4035                )
4036                .into_any_element()
4037        } else {
4038            selected_agent.into_any_element()
4039        };
4040
4041        let is_empty_state = !self.active_thread_has_messages(cx);
4042
4043        let is_in_history_or_config = matches!(
4044            &self.active_view,
4045            ActiveView::History { .. } | ActiveView::Configuration
4046        );
4047
4048        let is_full_screen = self.is_zoomed(window, cx);
4049        let full_screen_button = if is_full_screen {
4050            IconButton::new("disable-full-screen", IconName::Minimize)
4051                .icon_size(IconSize::Small)
4052                .tooltip(move |_, cx| Tooltip::for_action("Disable Full Screen", &ToggleZoom, cx))
4053                .on_click(cx.listener(move |this, _, window, cx| {
4054                    this.toggle_zoom(&ToggleZoom, window, cx);
4055                }))
4056        } else {
4057            IconButton::new("enable-full-screen", IconName::Maximize)
4058                .icon_size(IconSize::Small)
4059                .tooltip(move |_, cx| Tooltip::for_action("Enable Full Screen", &ToggleZoom, cx))
4060                .on_click(cx.listener(move |this, _, window, cx| {
4061                    this.toggle_zoom(&ToggleZoom, window, cx);
4062                }))
4063        };
4064
4065        let use_v2_empty_toolbar = is_empty_state && !is_in_history_or_config;
4066
4067        let max_content_width = AgentSettings::get_global(cx).max_content_width;
4068
4069        let base_container = h_flex()
4070            .size_full()
4071            // TODO: This is only until we remove Agent settings from the panel.
4072            .when(!is_in_history_or_config, |this| {
4073                this.max_w(max_content_width).mx_auto()
4074            })
4075            .flex_none()
4076            .justify_between()
4077            .gap_2();
4078
4079        let toolbar_content = if use_v2_empty_toolbar {
4080            let (chevron_icon, icon_color, label_color) =
4081                if self.new_thread_menu_handle.is_deployed() {
4082                    (IconName::ChevronUp, Color::Accent, Color::Accent)
4083                } else {
4084                    (IconName::ChevronDown, Color::Muted, Color::Default)
4085                };
4086
4087            let agent_icon = if let Some(icon_path) = selected_agent_custom_icon_for_button {
4088                Icon::from_external_svg(icon_path)
4089                    .size(IconSize::Small)
4090                    .color(icon_color)
4091            } else {
4092                let icon_name = selected_agent_builtin_icon.unwrap_or(IconName::ZedAgent);
4093                Icon::new(icon_name).size(IconSize::Small).color(icon_color)
4094            };
4095
4096            let agent_selector_button = Button::new("agent-selector-trigger", selected_agent_label)
4097                .start_icon(agent_icon)
4098                .color(label_color)
4099                .end_icon(
4100                    Icon::new(chevron_icon)
4101                        .color(icon_color)
4102                        .size(IconSize::XSmall),
4103                );
4104
4105            let agent_selector_menu = PopoverMenu::new("new_thread_menu")
4106                .trigger_with_tooltip(agent_selector_button, {
4107                    move |_window, cx| {
4108                        Tooltip::for_action_in(
4109                            "New Thread…",
4110                            &ToggleNewThreadMenu,
4111                            &focus_handle,
4112                            cx,
4113                        )
4114                    }
4115                })
4116                .menu({
4117                    let builder = new_thread_menu_builder.clone();
4118                    move |window, cx| builder(window, cx)
4119                })
4120                .with_handle(self.new_thread_menu_handle.clone())
4121                .anchor(Corner::TopLeft)
4122                .offset(gpui::Point {
4123                    x: px(1.0),
4124                    y: px(1.0),
4125                });
4126
4127            base_container
4128                .child(
4129                    h_flex()
4130                        .size_full()
4131                        .gap(DynamicSpacing::Base04.rems(cx))
4132                        .pl(DynamicSpacing::Base04.rems(cx))
4133                        .child(agent_selector_menu)
4134                        .when(
4135                            has_visible_worktrees && self.project_has_git_repository(cx),
4136                            |this| this.child(self.render_start_thread_in_selector(cx)),
4137                        )
4138                        .when(
4139                            matches!(self.start_thread_in, StartThreadIn::NewWorktree { .. }),
4140                            |this| this.child(self.render_new_worktree_branch_selector(cx)),
4141                        ),
4142                )
4143                .child(
4144                    h_flex()
4145                        .h_full()
4146                        .flex_none()
4147                        .gap_1()
4148                        .pl_1()
4149                        .pr_1()
4150                        .child(full_screen_button)
4151                        .child(self.render_panel_options_menu(window, cx)),
4152                )
4153                .into_any_element()
4154        } else {
4155            let new_thread_menu = PopoverMenu::new("new_thread_menu")
4156                .trigger_with_tooltip(
4157                    IconButton::new("new_thread_menu_btn", IconName::Plus)
4158                        .icon_size(IconSize::Small),
4159                    {
4160                        move |_window, cx| {
4161                            Tooltip::for_action_in(
4162                                "New Thread\u{2026}",
4163                                &ToggleNewThreadMenu,
4164                                &focus_handle,
4165                                cx,
4166                            )
4167                        }
4168                    },
4169                )
4170                .anchor(Corner::TopRight)
4171                .with_handle(self.new_thread_menu_handle.clone())
4172                .menu(move |window, cx| new_thread_menu_builder(window, cx));
4173
4174            base_container
4175                .child(
4176                    h_flex()
4177                        .size_full()
4178                        .gap(DynamicSpacing::Base04.rems(cx))
4179                        .pl(DynamicSpacing::Base04.rems(cx))
4180                        .child(match &self.active_view {
4181                            ActiveView::History { .. } | ActiveView::Configuration => {
4182                                self.render_toolbar_back_button(cx).into_any_element()
4183                            }
4184                            _ => selected_agent.into_any_element(),
4185                        })
4186                        .child(self.render_title_view(window, cx)),
4187                )
4188                .child(
4189                    h_flex()
4190                        .h_full()
4191                        .flex_none()
4192                        .gap_1()
4193                        .pl_1()
4194                        .pr_1()
4195                        .child(new_thread_menu)
4196                        .child(full_screen_button)
4197                        .child(self.render_panel_options_menu(window, cx)),
4198                )
4199                .into_any_element()
4200        };
4201
4202        h_flex()
4203            .id("agent-panel-toolbar")
4204            .h(Tab::container_height(cx))
4205            .flex_shrink_0()
4206            .max_w_full()
4207            .bg(cx.theme().colors().tab_bar_background)
4208            .border_b_1()
4209            .border_color(cx.theme().colors().border)
4210            .child(toolbar_content)
4211    }
4212
4213    fn render_worktree_creation_status(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
4214        let status = self.worktree_creation_status.as_ref()?;
4215        match status {
4216            WorktreeCreationStatus::Creating => Some(
4217                h_flex()
4218                    .absolute()
4219                    .bottom_12()
4220                    .w_full()
4221                    .p_2()
4222                    .gap_1()
4223                    .justify_center()
4224                    .bg(cx.theme().colors().editor_background)
4225                    .child(
4226                        Icon::new(IconName::LoadCircle)
4227                            .size(IconSize::Small)
4228                            .color(Color::Muted)
4229                            .with_rotate_animation(3),
4230                    )
4231                    .child(
4232                        Label::new("Creating Worktree…")
4233                            .color(Color::Muted)
4234                            .size(LabelSize::Small),
4235                    )
4236                    .into_any_element(),
4237            ),
4238            WorktreeCreationStatus::Error(message) => Some(
4239                Callout::new()
4240                    .icon(IconName::XCircleFilled)
4241                    .severity(Severity::Error)
4242                    .title("Worktree Creation Error")
4243                    .description(message.clone())
4244                    .border_position(ui::BorderPosition::Bottom)
4245                    .dismiss_action(
4246                        IconButton::new("dismiss-worktree-error", IconName::Close)
4247                            .icon_size(IconSize::Small)
4248                            .tooltip(Tooltip::text("Dismiss"))
4249                            .on_click(cx.listener(|this, _, _, cx| {
4250                                this.worktree_creation_status = None;
4251                                cx.notify();
4252                            })),
4253                    )
4254                    .into_any_element(),
4255            ),
4256        }
4257    }
4258
4259    fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
4260        if TrialEndUpsell::dismissed(cx) {
4261            return false;
4262        }
4263
4264        match &self.active_view {
4265            ActiveView::AgentThread { .. } => {
4266                if LanguageModelRegistry::global(cx)
4267                    .read(cx)
4268                    .default_model()
4269                    .is_some_and(|model| {
4270                        model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
4271                    })
4272                {
4273                    return false;
4274                }
4275            }
4276            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
4277                return false;
4278            }
4279        }
4280
4281        let plan = self.user_store.read(cx).plan();
4282        let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
4283
4284        plan.is_some_and(|plan| plan == Plan::ZedFree) && has_previous_trial
4285    }
4286
4287    fn should_render_agent_layout_onboarding(&self, cx: &mut Context<Self>) -> bool {
4288        // We only want to show this for existing users: those who
4289        // have used the agent panel before the sidebar was introduced.
4290        // We can infer that state by users having seen the onboarding
4291        // at one point, but not the agent layout onboarding.
4292
4293        let has_messages = self.active_thread_has_messages(cx);
4294        let is_dismissed = self
4295            .agent_layout_onboarding_dismissed
4296            .load(Ordering::Acquire);
4297
4298        if is_dismissed || has_messages {
4299            return false;
4300        }
4301
4302        match &self.active_view {
4303            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
4304                false
4305            }
4306            ActiveView::AgentThread { .. } => {
4307                let existing_user = self
4308                    .new_user_onboarding_upsell_dismissed
4309                    .load(Ordering::Acquire);
4310                existing_user
4311            }
4312        }
4313    }
4314
4315    fn render_agent_layout_onboarding(
4316        &self,
4317        _window: &mut Window,
4318        cx: &mut Context<Self>,
4319    ) -> Option<impl IntoElement> {
4320        if !self.should_render_agent_layout_onboarding(cx) {
4321            return None;
4322        }
4323
4324        Some(div().child(self.agent_layout_onboarding.clone()))
4325    }
4326
4327    fn dismiss_agent_layout_onboarding(&mut self, cx: &mut Context<Self>) {
4328        self.agent_layout_onboarding_dismissed
4329            .store(true, Ordering::Release);
4330        AgentLayoutOnboarding::set_dismissed(true, cx);
4331        cx.notify();
4332    }
4333
4334    fn dismiss_ai_onboarding(&mut self, cx: &mut Context<Self>) {
4335        self.new_user_onboarding_upsell_dismissed
4336            .store(true, Ordering::Release);
4337        OnboardingUpsell::set_dismissed(true, cx);
4338        self.dismiss_agent_layout_onboarding(cx);
4339        cx.notify();
4340    }
4341
4342    fn should_render_new_user_onboarding(&mut self, cx: &mut Context<Self>) -> bool {
4343        if self
4344            .new_user_onboarding_upsell_dismissed
4345            .load(Ordering::Acquire)
4346        {
4347            return false;
4348        }
4349
4350        let user_store = self.user_store.read(cx);
4351
4352        if user_store.plan().is_some_and(|plan| plan == Plan::ZedPro)
4353            && user_store
4354                .subscription_period()
4355                .and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
4356                .is_some_and(|date| date < chrono::Utc::now())
4357        {
4358            if !self
4359                .new_user_onboarding_upsell_dismissed
4360                .load(Ordering::Acquire)
4361            {
4362                self.dismiss_ai_onboarding(cx);
4363            }
4364            return false;
4365        }
4366
4367        let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
4368            .visible_providers()
4369            .iter()
4370            .any(|provider| {
4371                provider.is_authenticated(cx)
4372                    && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
4373            });
4374
4375        match &self.active_view {
4376            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
4377                false
4378            }
4379            ActiveView::AgentThread {
4380                conversation_view, ..
4381            } if conversation_view.read(cx).as_native_thread(cx).is_none() => false,
4382            ActiveView::AgentThread { conversation_view } => {
4383                let history_is_empty = conversation_view
4384                    .read(cx)
4385                    .history()
4386                    .is_none_or(|h| h.read(cx).is_empty());
4387                history_is_empty || !has_configured_non_zed_providers
4388            }
4389        }
4390    }
4391
4392    fn render_new_user_onboarding(
4393        &mut self,
4394        _window: &mut Window,
4395        cx: &mut Context<Self>,
4396    ) -> Option<impl IntoElement> {
4397        if !self.should_render_new_user_onboarding(cx) {
4398            return None;
4399        }
4400
4401        Some(
4402            div()
4403                .bg(cx.theme().colors().editor_background)
4404                .child(self.new_user_onboarding.clone()),
4405        )
4406    }
4407
4408    fn render_trial_end_upsell(
4409        &self,
4410        _window: &mut Window,
4411        cx: &mut Context<Self>,
4412    ) -> Option<impl IntoElement> {
4413        if !self.should_render_trial_end_upsell(cx) {
4414            return None;
4415        }
4416
4417        Some(
4418            v_flex()
4419                .absolute()
4420                .inset_0()
4421                .size_full()
4422                .bg(cx.theme().colors().panel_background)
4423                .opacity(0.85)
4424                .block_mouse_except_scroll()
4425                .child(EndTrialUpsell::new(Arc::new({
4426                    let this = cx.entity();
4427                    move |_, cx| {
4428                        this.update(cx, |_this, cx| {
4429                            TrialEndUpsell::set_dismissed(true, cx);
4430                            cx.notify();
4431                        });
4432                    }
4433                }))),
4434        )
4435    }
4436
4437    fn render_drag_target(&self, cx: &Context<Self>) -> Div {
4438        let is_local = self.project.read(cx).is_local();
4439        div()
4440            .invisible()
4441            .absolute()
4442            .top_0()
4443            .right_0()
4444            .bottom_0()
4445            .left_0()
4446            .bg(cx.theme().colors().drop_target_background)
4447            .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
4448            .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
4449            .when(is_local, |this| {
4450                this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
4451            })
4452            .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
4453                let item = tab.pane.read(cx).item_for_index(tab.ix);
4454                let project_paths = item
4455                    .and_then(|item| item.project_path(cx))
4456                    .into_iter()
4457                    .collect::<Vec<_>>();
4458                this.handle_drop(project_paths, vec![], window, cx);
4459            }))
4460            .on_drop(
4461                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
4462                    let project_paths = selection
4463                        .items()
4464                        .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
4465                        .collect::<Vec<_>>();
4466                    this.handle_drop(project_paths, vec![], window, cx);
4467                }),
4468            )
4469            .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
4470                let tasks = paths
4471                    .paths()
4472                    .iter()
4473                    .map(|path| {
4474                        Workspace::project_path_for_path(this.project.clone(), path, false, cx)
4475                    })
4476                    .collect::<Vec<_>>();
4477                cx.spawn_in(window, async move |this, cx| {
4478                    let mut paths = vec![];
4479                    let mut added_worktrees = vec![];
4480                    let opened_paths = futures::future::join_all(tasks).await;
4481                    for entry in opened_paths {
4482                        if let Some((worktree, project_path)) = entry.log_err() {
4483                            added_worktrees.push(worktree);
4484                            paths.push(project_path);
4485                        }
4486                    }
4487                    this.update_in(cx, |this, window, cx| {
4488                        this.handle_drop(paths, added_worktrees, window, cx);
4489                    })
4490                    .ok();
4491                })
4492                .detach();
4493            }))
4494    }
4495
4496    fn handle_drop(
4497        &mut self,
4498        paths: Vec<ProjectPath>,
4499        added_worktrees: Vec<Entity<Worktree>>,
4500        window: &mut Window,
4501        cx: &mut Context<Self>,
4502    ) {
4503        match &self.active_view {
4504            ActiveView::AgentThread { conversation_view } => {
4505                conversation_view.update(cx, |conversation_view, cx| {
4506                    conversation_view.insert_dragged_files(paths, added_worktrees, window, cx);
4507                });
4508            }
4509            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
4510        }
4511    }
4512
4513    fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
4514        if !self.show_trust_workspace_message {
4515            return None;
4516        }
4517
4518        let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
4519
4520        Some(
4521            Callout::new()
4522                .icon(IconName::Warning)
4523                .severity(Severity::Warning)
4524                .border_position(ui::BorderPosition::Bottom)
4525                .title("You're in Restricted Mode")
4526                .description(description)
4527                .actions_slot(
4528                    Button::new("open-trust-modal", "Configure Project Trust")
4529                        .label_size(LabelSize::Small)
4530                        .style(ButtonStyle::Outlined)
4531                        .on_click({
4532                            cx.listener(move |this, _, window, cx| {
4533                                this.workspace
4534                                    .update(cx, |workspace, cx| {
4535                                        workspace
4536                                            .show_worktree_trust_security_modal(true, window, cx)
4537                                    })
4538                                    .log_err();
4539                            })
4540                        }),
4541                ),
4542        )
4543    }
4544
4545    fn key_context(&self) -> KeyContext {
4546        let mut key_context = KeyContext::new_with_defaults();
4547        key_context.add("AgentPanel");
4548        match &self.active_view {
4549            ActiveView::AgentThread { .. } => key_context.add("acp_thread"),
4550            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
4551        }
4552        key_context
4553    }
4554}
4555
4556impl Render for AgentPanel {
4557    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4558        // WARNING: Changes to this element hierarchy can have
4559        // non-obvious implications to the layout of children.
4560        //
4561        // If you need to change it, please confirm:
4562        // - The message editor expands (cmd-option-esc) correctly
4563        // - When expanded, the buttons at the bottom of the panel are displayed correctly
4564        // - Font size works as expected and can be changed with cmd-+/cmd-
4565        // - Scrolling in all views works as expected
4566        // - Files can be dropped into the panel
4567        let content = v_flex()
4568            .relative()
4569            .size_full()
4570            .justify_between()
4571            .key_context(self.key_context())
4572            .on_action(cx.listener(|this, action: &NewThread, window, cx| {
4573                this.new_thread(action, window, cx);
4574            }))
4575            .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
4576                this.open_history(window, cx);
4577            }))
4578            .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
4579                this.open_configuration(window, cx);
4580            }))
4581            .on_action(cx.listener(Self::open_active_thread_as_markdown))
4582            .on_action(cx.listener(Self::deploy_rules_library))
4583            .on_action(cx.listener(Self::go_back))
4584            .on_action(cx.listener(Self::toggle_navigation_menu))
4585            .on_action(cx.listener(Self::toggle_options_menu))
4586            .on_action(cx.listener(Self::increase_font_size))
4587            .on_action(cx.listener(Self::decrease_font_size))
4588            .on_action(cx.listener(Self::reset_font_size))
4589            .on_action(cx.listener(Self::toggle_zoom))
4590            .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
4591                if let Some(conversation_view) = this.active_conversation_view() {
4592                    conversation_view.update(cx, |conversation_view, cx| {
4593                        conversation_view.reauthenticate(window, cx)
4594                    })
4595                }
4596            }))
4597            .child(self.render_toolbar(window, cx))
4598            .children(self.render_workspace_trust_message(cx))
4599            .children(self.render_new_user_onboarding(window, cx))
4600            .children(self.render_agent_layout_onboarding(window, cx))
4601            .map(|parent| match &self.active_view {
4602                ActiveView::Uninitialized => parent,
4603                ActiveView::AgentThread {
4604                    conversation_view, ..
4605                } => parent
4606                    .child(conversation_view.clone())
4607                    .child(self.render_drag_target(cx)),
4608                ActiveView::History { view } => parent.child(view.clone()),
4609                ActiveView::Configuration => parent.children(self.configuration.clone()),
4610            })
4611            .children(self.render_worktree_creation_status(cx))
4612            .children(self.render_trial_end_upsell(window, cx));
4613
4614        match self.active_view.which_font_size_used() {
4615            WhichFontSize::AgentFont => {
4616                WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
4617                    .size_full()
4618                    .child(content)
4619                    .into_any()
4620            }
4621            _ => content.into_any(),
4622        }
4623    }
4624}
4625
4626struct PromptLibraryInlineAssist {
4627    workspace: WeakEntity<Workspace>,
4628}
4629
4630impl PromptLibraryInlineAssist {
4631    pub fn new(workspace: WeakEntity<Workspace>) -> Self {
4632        Self { workspace }
4633    }
4634}
4635
4636impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
4637    fn assist(
4638        &self,
4639        prompt_editor: &Entity<Editor>,
4640        initial_prompt: Option<String>,
4641        window: &mut Window,
4642        cx: &mut Context<RulesLibrary>,
4643    ) {
4644        InlineAssistant::update_global(cx, |assistant, cx| {
4645            let Some(workspace) = self.workspace.upgrade() else {
4646                return;
4647            };
4648            let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
4649                return;
4650            };
4651            let history = panel
4652                .read(cx)
4653                .connection_store()
4654                .read(cx)
4655                .entry(&crate::Agent::NativeAgent)
4656                .and_then(|s| s.read(cx).history())
4657                .map(|h| h.downgrade());
4658            let project = workspace.read(cx).project().downgrade();
4659            let panel = panel.read(cx);
4660            let thread_store = panel.thread_store().clone();
4661            assistant.assist(
4662                prompt_editor,
4663                self.workspace.clone(),
4664                project,
4665                thread_store,
4666                None,
4667                history,
4668                initial_prompt,
4669                window,
4670                cx,
4671            );
4672        })
4673    }
4674
4675    fn focus_agent_panel(
4676        &self,
4677        workspace: &mut Workspace,
4678        window: &mut Window,
4679        cx: &mut Context<Workspace>,
4680    ) -> bool {
4681        workspace.focus_panel::<AgentPanel>(window, cx).is_some()
4682    }
4683}
4684
4685struct OnboardingUpsell;
4686
4687impl Dismissable for OnboardingUpsell {
4688    const KEY: &'static str = "dismissed-trial-upsell";
4689}
4690
4691struct AgentLayoutOnboarding;
4692
4693impl Dismissable for AgentLayoutOnboarding {
4694    const KEY: &'static str = "dismissed-agent-layout-onboarding";
4695}
4696
4697struct TrialEndUpsell;
4698
4699impl Dismissable for TrialEndUpsell {
4700    const KEY: &'static str = "dismissed-trial-end-upsell";
4701}
4702
4703/// Test-only helper methods
4704#[cfg(any(test, feature = "test-support"))]
4705impl AgentPanel {
4706    pub fn test_new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
4707        Self::new(workspace, None, window, cx)
4708    }
4709
4710    /// Opens an external thread using an arbitrary AgentServer.
4711    ///
4712    /// This is a test-only helper that allows visual tests and integration tests
4713    /// to inject a stub server without modifying production code paths.
4714    /// Not compiled into production builds.
4715    pub fn open_external_thread_with_server(
4716        &mut self,
4717        server: Rc<dyn AgentServer>,
4718        window: &mut Window,
4719        cx: &mut Context<Self>,
4720    ) {
4721        let workspace = self.workspace.clone();
4722        let project = self.project.clone();
4723
4724        let ext_agent = Agent::Custom {
4725            id: server.agent_id(),
4726        };
4727
4728        self.create_agent_thread(
4729            server, None, None, None, None, workspace, project, ext_agent, true, window, cx,
4730        );
4731    }
4732
4733    /// Returns the currently active thread view, if any.
4734    ///
4735    /// This is a test-only accessor that exposes the private `active_thread_view()`
4736    /// method for test assertions. Not compiled into production builds.
4737    pub fn active_thread_view_for_tests(&self) -> Option<&Entity<ConversationView>> {
4738        self.active_conversation_view()
4739    }
4740
4741    /// Sets the start_thread_in value directly, bypassing validation.
4742    ///
4743    /// This is a test-only helper for visual tests that need to show specific
4744    /// start_thread_in states without requiring a real git repository.
4745    pub fn set_start_thread_in_for_tests(&mut self, target: StartThreadIn, cx: &mut Context<Self>) {
4746        self.start_thread_in = target;
4747        cx.notify();
4748    }
4749
4750    /// Returns the current worktree creation status.
4751    ///
4752    /// This is a test-only helper for visual tests.
4753    pub fn worktree_creation_status_for_tests(&self) -> Option<&WorktreeCreationStatus> {
4754        self.worktree_creation_status.as_ref()
4755    }
4756
4757    /// Sets the worktree creation status directly.
4758    ///
4759    /// This is a test-only helper for visual tests that need to show the
4760    /// "Creating worktree…" spinner or error banners.
4761    pub fn set_worktree_creation_status_for_tests(
4762        &mut self,
4763        status: Option<WorktreeCreationStatus>,
4764        cx: &mut Context<Self>,
4765    ) {
4766        self.worktree_creation_status = status;
4767        cx.notify();
4768    }
4769
4770    /// Opens the history view.
4771    ///
4772    /// This is a test-only helper that exposes the private `open_history()`
4773    /// method for visual tests.
4774    pub fn open_history_for_tests(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4775        self.open_history(window, cx);
4776    }
4777
4778    /// Opens the start_thread_in selector popover menu.
4779    ///
4780    /// This is a test-only helper for visual tests.
4781    pub fn open_start_thread_in_menu_for_tests(
4782        &mut self,
4783        window: &mut Window,
4784        cx: &mut Context<Self>,
4785    ) {
4786        self.start_thread_in_menu_handle.show(window, cx);
4787    }
4788
4789    /// Dismisses the start_thread_in dropdown menu.
4790    ///
4791    /// This is a test-only helper for visual tests.
4792    pub fn close_start_thread_in_menu_for_tests(&mut self, cx: &mut Context<Self>) {
4793        self.start_thread_in_menu_handle.hide(cx);
4794    }
4795}
4796
4797#[cfg(test)]
4798mod tests {
4799    use super::*;
4800    use crate::conversation_view::tests::{StubAgentServer, init_test};
4801    use crate::test_support::{
4802        active_session_id, open_thread_with_connection, open_thread_with_custom_connection,
4803        send_message,
4804    };
4805    use acp_thread::{StubAgentConnection, ThreadStatus};
4806    use agent_servers::CODEX_ID;
4807    use feature_flags::FeatureFlagAppExt;
4808    use fs::FakeFs;
4809    use gpui::{TestAppContext, VisualTestContext};
4810    use project::Project;
4811    use serde_json::json;
4812    use std::path::Path;
4813    use std::time::Instant;
4814    use workspace::MultiWorkspace;
4815
4816    #[gpui::test]
4817    async fn test_active_thread_serialize_and_load_round_trip(cx: &mut TestAppContext) {
4818        init_test(cx);
4819        cx.update(|cx| {
4820            agent::ThreadStore::init_global(cx);
4821            language_model::LanguageModelRegistry::test(cx);
4822        });
4823
4824        // Create a MultiWorkspace window with two workspaces.
4825        let fs = FakeFs::new(cx.executor());
4826        let project_a = Project::test(fs.clone(), [], cx).await;
4827        let project_b = Project::test(fs, [], cx).await;
4828
4829        let multi_workspace =
4830            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
4831
4832        let workspace_a = multi_workspace
4833            .read_with(cx, |multi_workspace, _cx| {
4834                multi_workspace.workspace().clone()
4835            })
4836            .unwrap();
4837
4838        let workspace_b = multi_workspace
4839            .update(cx, |multi_workspace, window, cx| {
4840                multi_workspace.test_add_workspace(project_b.clone(), window, cx)
4841            })
4842            .unwrap();
4843
4844        workspace_a.update(cx, |workspace, _cx| {
4845            workspace.set_random_database_id();
4846        });
4847        workspace_b.update(cx, |workspace, _cx| {
4848            workspace.set_random_database_id();
4849        });
4850
4851        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4852
4853        // Set up workspace A: with an active thread.
4854        let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
4855            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
4856        });
4857
4858        panel_a.update_in(cx, |panel, window, cx| {
4859            panel.open_external_thread_with_server(
4860                Rc::new(StubAgentServer::default_response()),
4861                window,
4862                cx,
4863            );
4864        });
4865
4866        cx.run_until_parked();
4867
4868        panel_a.read_with(cx, |panel, cx| {
4869            assert!(
4870                panel.active_agent_thread(cx).is_some(),
4871                "workspace A should have an active thread after connection"
4872            );
4873        });
4874
4875        send_message(&panel_a, cx);
4876
4877        let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent.clone());
4878
4879        // Set up workspace B: ClaudeCode, no active thread.
4880        let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
4881            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
4882        });
4883
4884        panel_b.update(cx, |panel, _cx| {
4885            panel.selected_agent = Agent::Custom {
4886                id: "claude-acp".into(),
4887            };
4888        });
4889
4890        // Serialize both panels.
4891        panel_a.update(cx, |panel, cx| panel.serialize(cx));
4892        panel_b.update(cx, |panel, cx| panel.serialize(cx));
4893        cx.run_until_parked();
4894
4895        // Load fresh panels for each workspace and verify independent state.
4896        let async_cx = cx.update(|window, cx| window.to_async(cx));
4897        let loaded_a = AgentPanel::load(workspace_a.downgrade(), async_cx)
4898            .await
4899            .expect("panel A load should succeed");
4900        cx.run_until_parked();
4901
4902        let async_cx = cx.update(|window, cx| window.to_async(cx));
4903        let loaded_b = AgentPanel::load(workspace_b.downgrade(), async_cx)
4904            .await
4905            .expect("panel B load should succeed");
4906        cx.run_until_parked();
4907
4908        // Workspace A should restore its thread and agent type
4909        loaded_a.read_with(cx, |panel, _cx| {
4910            assert_eq!(
4911                panel.selected_agent, agent_type_a,
4912                "workspace A agent type should be restored"
4913            );
4914            assert!(
4915                panel.active_conversation_view().is_some(),
4916                "workspace A should have its active thread restored"
4917            );
4918        });
4919
4920        // Workspace B should restore its own agent type, with no thread
4921        loaded_b.read_with(cx, |panel, _cx| {
4922            assert_eq!(
4923                panel.selected_agent,
4924                Agent::Custom {
4925                    id: "claude-acp".into()
4926                },
4927                "workspace B agent type should be restored"
4928            );
4929            assert!(
4930                panel.active_conversation_view().is_none(),
4931                "workspace B should have no active thread"
4932            );
4933        });
4934    }
4935
4936    #[gpui::test]
4937    async fn test_non_native_thread_without_metadata_is_not_restored(cx: &mut TestAppContext) {
4938        init_test(cx);
4939        cx.update(|cx| {
4940            agent::ThreadStore::init_global(cx);
4941            language_model::LanguageModelRegistry::test(cx);
4942        });
4943
4944        let fs = FakeFs::new(cx.executor());
4945        let project = Project::test(fs, [], cx).await;
4946
4947        let multi_workspace =
4948            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
4949
4950        let workspace = multi_workspace
4951            .read_with(cx, |multi_workspace, _cx| {
4952                multi_workspace.workspace().clone()
4953            })
4954            .unwrap();
4955
4956        workspace.update(cx, |workspace, _cx| {
4957            workspace.set_random_database_id();
4958        });
4959
4960        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4961
4962        let panel = workspace.update_in(cx, |workspace, window, cx| {
4963            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
4964        });
4965
4966        panel.update_in(cx, |panel, window, cx| {
4967            panel.open_external_thread_with_server(
4968                Rc::new(StubAgentServer::default_response()),
4969                window,
4970                cx,
4971            );
4972        });
4973
4974        cx.run_until_parked();
4975
4976        panel.read_with(cx, |panel, cx| {
4977            assert!(
4978                panel.active_agent_thread(cx).is_some(),
4979                "should have an active thread after connection"
4980            );
4981        });
4982
4983        // Serialize without ever sending a message, so no thread metadata exists.
4984        panel.update(cx, |panel, cx| panel.serialize(cx));
4985        cx.run_until_parked();
4986
4987        let async_cx = cx.update(|window, cx| window.to_async(cx));
4988        let loaded = AgentPanel::load(workspace.downgrade(), async_cx)
4989            .await
4990            .expect("panel load should succeed");
4991        cx.run_until_parked();
4992
4993        loaded.read_with(cx, |panel, _cx| {
4994            assert!(
4995                panel.active_conversation_view().is_none(),
4996                "thread without metadata should not be restored"
4997            );
4998        });
4999    }
5000
5001    /// Extracts the text from a Text content block, panicking if it's not Text.
5002    fn expect_text_block(block: &acp::ContentBlock) -> &str {
5003        match block {
5004            acp::ContentBlock::Text(t) => t.text.as_str(),
5005            other => panic!("expected Text block, got {:?}", other),
5006        }
5007    }
5008
5009    /// Extracts the (text_content, uri) from a Resource content block, panicking
5010    /// if it's not a TextResourceContents resource.
5011    fn expect_resource_block(block: &acp::ContentBlock) -> (&str, &str) {
5012        match block {
5013            acp::ContentBlock::Resource(r) => match &r.resource {
5014                acp::EmbeddedResourceResource::TextResourceContents(t) => {
5015                    (t.text.as_str(), t.uri.as_str())
5016                }
5017                other => panic!("expected TextResourceContents, got {:?}", other),
5018            },
5019            other => panic!("expected Resource block, got {:?}", other),
5020        }
5021    }
5022
5023    #[test]
5024    fn test_build_conflict_resolution_prompt_single_conflict() {
5025        let conflicts = vec![ConflictContent {
5026            file_path: "src/main.rs".to_string(),
5027            conflict_text: "<<<<<<< HEAD\nlet x = 1;\n=======\nlet x = 2;\n>>>>>>> feature"
5028                .to_string(),
5029            ours_branch_name: "HEAD".to_string(),
5030            theirs_branch_name: "feature".to_string(),
5031        }];
5032
5033        let blocks = build_conflict_resolution_prompt(&conflicts);
5034        // 2 Text blocks + 1 ResourceLink + 1 Resource for the conflict
5035        assert_eq!(
5036            blocks.len(),
5037            4,
5038            "expected 2 text + 1 resource link + 1 resource block"
5039        );
5040
5041        let intro_text = expect_text_block(&blocks[0]);
5042        assert!(
5043            intro_text.contains("Please resolve the following merge conflict in"),
5044            "prompt should include single-conflict intro text"
5045        );
5046
5047        match &blocks[1] {
5048            acp::ContentBlock::ResourceLink(link) => {
5049                assert!(
5050                    link.uri.contains("file://"),
5051                    "resource link URI should use file scheme"
5052                );
5053                assert!(
5054                    link.uri.contains("main.rs"),
5055                    "resource link URI should reference file path"
5056                );
5057            }
5058            other => panic!("expected ResourceLink block, got {:?}", other),
5059        }
5060
5061        let body_text = expect_text_block(&blocks[2]);
5062        assert!(
5063            body_text.contains("`HEAD` (ours)"),
5064            "prompt should mention ours branch"
5065        );
5066        assert!(
5067            body_text.contains("`feature` (theirs)"),
5068            "prompt should mention theirs branch"
5069        );
5070        assert!(
5071            body_text.contains("editing the file directly"),
5072            "prompt should instruct the agent to edit the file"
5073        );
5074
5075        let (resource_text, resource_uri) = expect_resource_block(&blocks[3]);
5076        assert!(
5077            resource_text.contains("<<<<<<< HEAD"),
5078            "resource should contain the conflict text"
5079        );
5080        assert!(
5081            resource_uri.contains("merge-conflict"),
5082            "resource URI should use the merge-conflict scheme"
5083        );
5084        assert!(
5085            resource_uri.contains("main.rs"),
5086            "resource URI should reference the file path"
5087        );
5088    }
5089
5090    #[test]
5091    fn test_build_conflict_resolution_prompt_multiple_conflicts_same_file() {
5092        let conflicts = vec![
5093            ConflictContent {
5094                file_path: "src/lib.rs".to_string(),
5095                conflict_text: "<<<<<<< main\nfn a() {}\n=======\nfn a_v2() {}\n>>>>>>> dev"
5096                    .to_string(),
5097                ours_branch_name: "main".to_string(),
5098                theirs_branch_name: "dev".to_string(),
5099            },
5100            ConflictContent {
5101                file_path: "src/lib.rs".to_string(),
5102                conflict_text: "<<<<<<< main\nfn b() {}\n=======\nfn b_v2() {}\n>>>>>>> dev"
5103                    .to_string(),
5104                ours_branch_name: "main".to_string(),
5105                theirs_branch_name: "dev".to_string(),
5106            },
5107        ];
5108
5109        let blocks = build_conflict_resolution_prompt(&conflicts);
5110        // 1 Text instruction + 2 Resource blocks
5111        assert_eq!(blocks.len(), 3, "expected 1 text + 2 resource blocks");
5112
5113        let text = expect_text_block(&blocks[0]);
5114        assert!(
5115            text.contains("all 2 merge conflicts"),
5116            "prompt should mention the total count"
5117        );
5118        assert!(
5119            text.contains("`main` (ours)"),
5120            "prompt should mention ours branch"
5121        );
5122        assert!(
5123            text.contains("`dev` (theirs)"),
5124            "prompt should mention theirs branch"
5125        );
5126        // Single file, so "file" not "files"
5127        assert!(
5128            text.contains("file directly"),
5129            "single file should use singular 'file'"
5130        );
5131
5132        let (resource_a, _) = expect_resource_block(&blocks[1]);
5133        let (resource_b, _) = expect_resource_block(&blocks[2]);
5134        assert!(
5135            resource_a.contains("fn a()"),
5136            "first resource should contain first conflict"
5137        );
5138        assert!(
5139            resource_b.contains("fn b()"),
5140            "second resource should contain second conflict"
5141        );
5142    }
5143
5144    #[test]
5145    fn test_build_conflict_resolution_prompt_multiple_conflicts_different_files() {
5146        let conflicts = vec![
5147            ConflictContent {
5148                file_path: "src/a.rs".to_string(),
5149                conflict_text: "<<<<<<< main\nA\n=======\nB\n>>>>>>> dev".to_string(),
5150                ours_branch_name: "main".to_string(),
5151                theirs_branch_name: "dev".to_string(),
5152            },
5153            ConflictContent {
5154                file_path: "src/b.rs".to_string(),
5155                conflict_text: "<<<<<<< main\nC\n=======\nD\n>>>>>>> dev".to_string(),
5156                ours_branch_name: "main".to_string(),
5157                theirs_branch_name: "dev".to_string(),
5158            },
5159        ];
5160
5161        let blocks = build_conflict_resolution_prompt(&conflicts);
5162        // 1 Text instruction + 2 Resource blocks
5163        assert_eq!(blocks.len(), 3, "expected 1 text + 2 resource blocks");
5164
5165        let text = expect_text_block(&blocks[0]);
5166        assert!(
5167            text.contains("files directly"),
5168            "multiple files should use plural 'files'"
5169        );
5170
5171        let (_, uri_a) = expect_resource_block(&blocks[1]);
5172        let (_, uri_b) = expect_resource_block(&blocks[2]);
5173        assert!(
5174            uri_a.contains("a.rs"),
5175            "first resource URI should reference a.rs"
5176        );
5177        assert!(
5178            uri_b.contains("b.rs"),
5179            "second resource URI should reference b.rs"
5180        );
5181    }
5182
5183    #[test]
5184    fn test_build_conflicted_files_resolution_prompt_file_paths_only() {
5185        let file_paths = vec![
5186            "src/main.rs".to_string(),
5187            "src/lib.rs".to_string(),
5188            "tests/integration.rs".to_string(),
5189        ];
5190
5191        let blocks = build_conflicted_files_resolution_prompt(&file_paths);
5192        // 1 instruction Text block + (ResourceLink + newline Text) per file
5193        assert_eq!(
5194            blocks.len(),
5195            1 + (file_paths.len() * 2),
5196            "expected instruction text plus resource links and separators"
5197        );
5198
5199        let text = expect_text_block(&blocks[0]);
5200        assert!(
5201            text.contains("unresolved merge conflicts"),
5202            "prompt should describe the task"
5203        );
5204        assert!(
5205            text.contains("conflict markers"),
5206            "prompt should mention conflict markers"
5207        );
5208
5209        for (index, path) in file_paths.iter().enumerate() {
5210            let link_index = 1 + (index * 2);
5211            let newline_index = link_index + 1;
5212
5213            match &blocks[link_index] {
5214                acp::ContentBlock::ResourceLink(link) => {
5215                    assert!(
5216                        link.uri.contains("file://"),
5217                        "resource link URI should use file scheme"
5218                    );
5219                    assert!(
5220                        link.uri.contains(path),
5221                        "resource link URI should reference file path: {path}"
5222                    );
5223                }
5224                other => panic!(
5225                    "expected ResourceLink block at index {}, got {:?}",
5226                    link_index, other
5227                ),
5228            }
5229
5230            let separator = expect_text_block(&blocks[newline_index]);
5231            assert_eq!(
5232                separator, "\n",
5233                "expected newline separator after each file"
5234            );
5235        }
5236    }
5237
5238    #[test]
5239    fn test_build_conflict_resolution_prompt_empty_conflicts() {
5240        let blocks = build_conflict_resolution_prompt(&[]);
5241        assert!(
5242            blocks.is_empty(),
5243            "empty conflicts should produce no blocks, got {} blocks",
5244            blocks.len()
5245        );
5246    }
5247
5248    #[test]
5249    fn test_build_conflicted_files_resolution_prompt_empty_paths() {
5250        let blocks = build_conflicted_files_resolution_prompt(&[]);
5251        assert!(
5252            blocks.is_empty(),
5253            "empty paths should produce no blocks, got {} blocks",
5254            blocks.len()
5255        );
5256    }
5257
5258    #[test]
5259    fn test_conflict_resource_block_structure() {
5260        let conflict = ConflictContent {
5261            file_path: "src/utils.rs".to_string(),
5262            conflict_text: "<<<<<<< HEAD\nold code\n=======\nnew code\n>>>>>>> branch".to_string(),
5263            ours_branch_name: "HEAD".to_string(),
5264            theirs_branch_name: "branch".to_string(),
5265        };
5266
5267        let block = conflict_resource_block(&conflict);
5268        let (text, uri) = expect_resource_block(&block);
5269
5270        assert_eq!(
5271            text, conflict.conflict_text,
5272            "resource text should be the raw conflict"
5273        );
5274        assert!(
5275            uri.starts_with("zed:///agent/merge-conflict"),
5276            "URI should use the zed merge-conflict scheme, got: {uri}"
5277        );
5278        assert!(uri.contains("utils.rs"), "URI should encode the file path");
5279    }
5280
5281    fn open_generating_thread_with_loadable_connection(
5282        panel: &Entity<AgentPanel>,
5283        connection: &StubAgentConnection,
5284        cx: &mut VisualTestContext,
5285    ) -> acp::SessionId {
5286        open_thread_with_custom_connection(panel, connection.clone(), cx);
5287        let session_id = active_session_id(panel, cx);
5288        send_message(panel, cx);
5289        cx.update(|_, cx| {
5290            connection.send_update(
5291                session_id.clone(),
5292                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("done".into())),
5293                cx,
5294            );
5295        });
5296        cx.run_until_parked();
5297        session_id
5298    }
5299
5300    fn open_idle_thread_with_non_loadable_connection(
5301        panel: &Entity<AgentPanel>,
5302        connection: &StubAgentConnection,
5303        cx: &mut VisualTestContext,
5304    ) -> acp::SessionId {
5305        open_thread_with_custom_connection(panel, connection.clone(), cx);
5306        let session_id = active_session_id(panel, cx);
5307
5308        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
5309            acp::ContentChunk::new("done".into()),
5310        )]);
5311        send_message(panel, cx);
5312
5313        session_id
5314    }
5315
5316    async fn setup_panel(cx: &mut TestAppContext) -> (Entity<AgentPanel>, VisualTestContext) {
5317        init_test(cx);
5318        cx.update(|cx| {
5319            agent::ThreadStore::init_global(cx);
5320            language_model::LanguageModelRegistry::test(cx);
5321        });
5322
5323        let fs = FakeFs::new(cx.executor());
5324        let project = Project::test(fs.clone(), [], cx).await;
5325
5326        let multi_workspace =
5327            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5328
5329        let workspace = multi_workspace
5330            .read_with(cx, |mw, _cx| mw.workspace().clone())
5331            .unwrap();
5332
5333        let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
5334
5335        let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
5336            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
5337        });
5338
5339        (panel, cx)
5340    }
5341
5342    #[gpui::test]
5343    async fn test_empty_draft_thread_not_retained_when_navigating_away(cx: &mut TestAppContext) {
5344        let (panel, mut cx) = setup_panel(cx).await;
5345
5346        let connection_a = StubAgentConnection::new();
5347        open_thread_with_connection(&panel, connection_a, &mut cx);
5348        let session_id_a = active_session_id(&panel, &cx);
5349
5350        panel.read_with(&cx, |panel, cx| {
5351            let thread = panel.active_agent_thread(cx).unwrap();
5352            assert!(
5353                thread.read(cx).entries().is_empty(),
5354                "newly opened draft thread should have no entries"
5355            );
5356            assert!(panel.background_threads.is_empty());
5357        });
5358
5359        let connection_b = StubAgentConnection::new();
5360        open_thread_with_connection(&panel, connection_b, &mut cx);
5361
5362        panel.read_with(&cx, |panel, _cx| {
5363            assert!(
5364                panel.background_threads.is_empty(),
5365                "empty draft thread should not be retained in background_threads"
5366            );
5367            assert!(
5368                !panel.background_threads.contains_key(&session_id_a),
5369                "empty draft thread should not be keyed in background_threads"
5370            );
5371        });
5372    }
5373
5374    #[gpui::test]
5375    async fn test_running_thread_retained_when_navigating_away(cx: &mut TestAppContext) {
5376        let (panel, mut cx) = setup_panel(cx).await;
5377
5378        let connection_a = StubAgentConnection::new();
5379        open_thread_with_connection(&panel, connection_a.clone(), &mut cx);
5380        send_message(&panel, &mut cx);
5381
5382        let session_id_a = active_session_id(&panel, &cx);
5383
5384        // Send a chunk to keep thread A generating (don't end the turn).
5385        cx.update(|_, cx| {
5386            connection_a.send_update(
5387                session_id_a.clone(),
5388                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("chunk".into())),
5389                cx,
5390            );
5391        });
5392        cx.run_until_parked();
5393
5394        // Verify thread A is generating.
5395        panel.read_with(&cx, |panel, cx| {
5396            let thread = panel.active_agent_thread(cx).unwrap();
5397            assert_eq!(thread.read(cx).status(), ThreadStatus::Generating);
5398            assert!(panel.background_threads.is_empty());
5399        });
5400
5401        // Open a new thread B — thread A should be retained in background.
5402        let connection_b = StubAgentConnection::new();
5403        open_thread_with_connection(&panel, connection_b, &mut cx);
5404
5405        panel.read_with(&cx, |panel, _cx| {
5406            assert_eq!(
5407                panel.background_threads.len(),
5408                1,
5409                "Running thread A should be retained in background_views"
5410            );
5411            assert!(
5412                panel.background_threads.contains_key(&session_id_a),
5413                "Background view should be keyed by thread A's session ID"
5414            );
5415        });
5416    }
5417
5418    #[gpui::test]
5419    async fn test_idle_non_loadable_thread_retained_when_navigating_away(cx: &mut TestAppContext) {
5420        let (panel, mut cx) = setup_panel(cx).await;
5421
5422        let connection_a = StubAgentConnection::new();
5423        connection_a.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
5424            acp::ContentChunk::new("Response".into()),
5425        )]);
5426        open_thread_with_connection(&panel, connection_a, &mut cx);
5427        send_message(&panel, &mut cx);
5428
5429        let weak_view_a = panel.read_with(&cx, |panel, _cx| {
5430            panel.active_conversation_view().unwrap().downgrade()
5431        });
5432        let session_id_a = active_session_id(&panel, &cx);
5433
5434        // Thread A should be idle (auto-completed via set_next_prompt_updates).
5435        panel.read_with(&cx, |panel, cx| {
5436            let thread = panel.active_agent_thread(cx).unwrap();
5437            assert_eq!(thread.read(cx).status(), ThreadStatus::Idle);
5438        });
5439
5440        // Open a new thread B — thread A should be retained because it is not loadable.
5441        let connection_b = StubAgentConnection::new();
5442        open_thread_with_connection(&panel, connection_b, &mut cx);
5443
5444        panel.read_with(&cx, |panel, _cx| {
5445            assert_eq!(
5446                panel.background_threads.len(),
5447                1,
5448                "Idle non-loadable thread A should be retained in background_views"
5449            );
5450            assert!(
5451                panel.background_threads.contains_key(&session_id_a),
5452                "Background view should be keyed by thread A's session ID"
5453            );
5454        });
5455
5456        assert!(
5457            weak_view_a.upgrade().is_some(),
5458            "Idle non-loadable ConnectionView should still be retained"
5459        );
5460    }
5461
5462    #[gpui::test]
5463    async fn test_background_thread_promoted_via_load(cx: &mut TestAppContext) {
5464        let (panel, mut cx) = setup_panel(cx).await;
5465
5466        let connection_a = StubAgentConnection::new();
5467        open_thread_with_connection(&panel, connection_a.clone(), &mut cx);
5468        send_message(&panel, &mut cx);
5469
5470        let session_id_a = active_session_id(&panel, &cx);
5471
5472        // Keep thread A generating.
5473        cx.update(|_, cx| {
5474            connection_a.send_update(
5475                session_id_a.clone(),
5476                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("chunk".into())),
5477                cx,
5478            );
5479        });
5480        cx.run_until_parked();
5481
5482        // Open thread B — thread A goes to background.
5483        let connection_b = StubAgentConnection::new();
5484        open_thread_with_connection(&panel, connection_b, &mut cx);
5485        send_message(&panel, &mut cx);
5486
5487        let session_id_b = active_session_id(&panel, &cx);
5488
5489        panel.read_with(&cx, |panel, _cx| {
5490            assert_eq!(panel.background_threads.len(), 1);
5491            assert!(panel.background_threads.contains_key(&session_id_a));
5492        });
5493
5494        // Load thread A back via load_agent_thread — should promote from background.
5495        panel.update_in(&mut cx, |panel, window, cx| {
5496            panel.load_agent_thread(
5497                panel.selected_agent().expect("selected agent must be set"),
5498                session_id_a.clone(),
5499                None,
5500                None,
5501                true,
5502                window,
5503                cx,
5504            );
5505        });
5506
5507        // Thread A should now be the active view, promoted from background.
5508        let active_session = active_session_id(&panel, &cx);
5509        assert_eq!(
5510            active_session, session_id_a,
5511            "Thread A should be the active thread after promotion"
5512        );
5513
5514        panel.read_with(&cx, |panel, _cx| {
5515            assert!(
5516                !panel.background_threads.contains_key(&session_id_a),
5517                "Promoted thread A should no longer be in background_views"
5518            );
5519            assert!(
5520                panel.background_threads.contains_key(&session_id_b),
5521                "Thread B (idle, non-loadable) should remain retained in background_views"
5522            );
5523        });
5524    }
5525
5526    #[gpui::test]
5527    async fn test_cleanup_background_threads_keeps_five_most_recent_idle_loadable_threads(
5528        cx: &mut TestAppContext,
5529    ) {
5530        let (panel, mut cx) = setup_panel(cx).await;
5531        let connection = StubAgentConnection::new()
5532            .with_supports_load_session(true)
5533            .with_agent_id("loadable-stub".into())
5534            .with_telemetry_id("loadable-stub".into());
5535        let mut session_ids = Vec::new();
5536
5537        for _ in 0..7 {
5538            session_ids.push(open_generating_thread_with_loadable_connection(
5539                &panel,
5540                &connection,
5541                &mut cx,
5542            ));
5543        }
5544
5545        let base_time = Instant::now();
5546
5547        for session_id in session_ids.iter().take(6) {
5548            connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
5549        }
5550        cx.run_until_parked();
5551
5552        panel.update(&mut cx, |panel, cx| {
5553            for (index, session_id) in session_ids.iter().take(6).enumerate() {
5554                let conversation_view = panel
5555                    .background_threads
5556                    .get(session_id)
5557                    .expect("background thread should exist")
5558                    .clone();
5559                conversation_view.update(cx, |view, cx| {
5560                    view.set_updated_at(base_time + Duration::from_secs(index as u64), cx);
5561                });
5562            }
5563            panel.cleanup_background_threads(cx);
5564        });
5565
5566        panel.read_with(&cx, |panel, _cx| {
5567            assert_eq!(
5568                panel.background_threads.len(),
5569                5,
5570                "cleanup should keep at most five idle loadable background threads"
5571            );
5572            assert!(
5573                !panel.background_threads.contains_key(&session_ids[0]),
5574                "oldest idle loadable background thread should be removed"
5575            );
5576            for session_id in &session_ids[1..6] {
5577                assert!(
5578                    panel.background_threads.contains_key(session_id),
5579                    "more recent idle loadable background threads should be retained"
5580                );
5581            }
5582            assert!(
5583                !panel.background_threads.contains_key(&session_ids[6]),
5584                "the active thread should not also be stored as a background thread"
5585            );
5586        });
5587    }
5588
5589    #[gpui::test]
5590    async fn test_cleanup_background_threads_preserves_idle_non_loadable_threads(
5591        cx: &mut TestAppContext,
5592    ) {
5593        let (panel, mut cx) = setup_panel(cx).await;
5594
5595        let non_loadable_connection = StubAgentConnection::new();
5596        let non_loadable_session_id = open_idle_thread_with_non_loadable_connection(
5597            &panel,
5598            &non_loadable_connection,
5599            &mut cx,
5600        );
5601
5602        let loadable_connection = StubAgentConnection::new()
5603            .with_supports_load_session(true)
5604            .with_agent_id("loadable-stub".into())
5605            .with_telemetry_id("loadable-stub".into());
5606        let mut loadable_session_ids = Vec::new();
5607
5608        for _ in 0..7 {
5609            loadable_session_ids.push(open_generating_thread_with_loadable_connection(
5610                &panel,
5611                &loadable_connection,
5612                &mut cx,
5613            ));
5614        }
5615
5616        let base_time = Instant::now();
5617
5618        for session_id in loadable_session_ids.iter().take(6) {
5619            loadable_connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
5620        }
5621        cx.run_until_parked();
5622
5623        panel.update(&mut cx, |panel, cx| {
5624            for (index, session_id) in loadable_session_ids.iter().take(6).enumerate() {
5625                let conversation_view = panel
5626                    .background_threads
5627                    .get(session_id)
5628                    .expect("background thread should exist")
5629                    .clone();
5630                conversation_view.update(cx, |view, cx| {
5631                    view.set_updated_at(base_time + Duration::from_secs(index as u64), cx);
5632                });
5633            }
5634            panel.cleanup_background_threads(cx);
5635        });
5636
5637        panel.read_with(&cx, |panel, _cx| {
5638            assert_eq!(
5639                panel.background_threads.len(),
5640                6,
5641                "cleanup should keep the non-loadable idle thread in addition to five loadable ones"
5642            );
5643            assert!(
5644                panel
5645                    .background_threads
5646                    .contains_key(&non_loadable_session_id),
5647                "idle non-loadable background threads should not be cleanup candidates"
5648            );
5649            assert!(
5650                !panel
5651                    .background_threads
5652                    .contains_key(&loadable_session_ids[0]),
5653                "oldest idle loadable background thread should still be removed"
5654            );
5655            for session_id in &loadable_session_ids[1..6] {
5656                assert!(
5657                    panel.background_threads.contains_key(session_id),
5658                    "more recent idle loadable background threads should be retained"
5659                );
5660            }
5661            assert!(
5662                !panel
5663                    .background_threads
5664                    .contains_key(&loadable_session_ids[6]),
5665                "the active loadable thread should not also be stored as a background thread"
5666            );
5667        });
5668    }
5669
5670    #[gpui::test]
5671    async fn test_thread_target_local_project(cx: &mut TestAppContext) {
5672        init_test(cx);
5673        cx.update(|cx| {
5674            agent::ThreadStore::init_global(cx);
5675            language_model::LanguageModelRegistry::test(cx);
5676        });
5677
5678        let fs = FakeFs::new(cx.executor());
5679        fs.insert_tree(
5680            "/project",
5681            json!({
5682                ".git": {},
5683                "src": {
5684                    "main.rs": "fn main() {}"
5685                }
5686            }),
5687        )
5688        .await;
5689        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
5690
5691        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
5692
5693        let multi_workspace =
5694            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5695
5696        let workspace = multi_workspace
5697            .read_with(cx, |multi_workspace, _cx| {
5698                multi_workspace.workspace().clone()
5699            })
5700            .unwrap();
5701
5702        workspace.update(cx, |workspace, _cx| {
5703            workspace.set_random_database_id();
5704        });
5705
5706        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5707
5708        // Wait for the project to discover the git repository.
5709        cx.run_until_parked();
5710
5711        let panel = workspace.update_in(cx, |workspace, window, cx| {
5712            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
5713            workspace.add_panel(panel.clone(), window, cx);
5714            panel
5715        });
5716
5717        cx.run_until_parked();
5718
5719        // Default thread target should be LocalProject.
5720        panel.read_with(cx, |panel, _cx| {
5721            assert_eq!(
5722                *panel.start_thread_in(),
5723                StartThreadIn::LocalProject,
5724                "default thread target should be LocalProject"
5725            );
5726        });
5727
5728        // Start a new thread with the default LocalProject target.
5729        // Use StubAgentServer so the thread connects immediately in tests.
5730        panel.update_in(cx, |panel, window, cx| {
5731            panel.open_external_thread_with_server(
5732                Rc::new(StubAgentServer::default_response()),
5733                window,
5734                cx,
5735            );
5736        });
5737
5738        cx.run_until_parked();
5739
5740        // MultiWorkspace should still have exactly one workspace (no worktree created).
5741        multi_workspace
5742            .read_with(cx, |multi_workspace, _cx| {
5743                assert_eq!(
5744                    multi_workspace.workspaces().count(),
5745                    1,
5746                    "LocalProject should not create a new workspace"
5747                );
5748            })
5749            .unwrap();
5750
5751        // The thread should be active in the panel.
5752        panel.read_with(cx, |panel, cx| {
5753            assert!(
5754                panel.active_agent_thread(cx).is_some(),
5755                "a thread should be running in the current workspace"
5756            );
5757        });
5758
5759        // The thread target should still be LocalProject (unchanged).
5760        panel.read_with(cx, |panel, _cx| {
5761            assert_eq!(
5762                *panel.start_thread_in(),
5763                StartThreadIn::LocalProject,
5764                "thread target should remain LocalProject"
5765            );
5766        });
5767
5768        // No worktree creation status should be set.
5769        panel.read_with(cx, |panel, _cx| {
5770            assert!(
5771                panel.worktree_creation_status.is_none(),
5772                "no worktree creation should have occurred"
5773            );
5774        });
5775    }
5776
5777    #[gpui::test]
5778    async fn test_thread_target_does_not_sync_to_external_linked_worktree_with_invalid_branch_target(
5779        cx: &mut TestAppContext,
5780    ) {
5781        use git::repository::Worktree as GitWorktree;
5782
5783        init_test(cx);
5784        cx.update(|cx| {
5785            agent::ThreadStore::init_global(cx);
5786            language_model::LanguageModelRegistry::test(cx);
5787        });
5788
5789        let fs = FakeFs::new(cx.executor());
5790        fs.insert_tree(
5791            "/project",
5792            json!({
5793                ".git": {},
5794                "src": {
5795                    "main.rs": "fn main() {}"
5796                }
5797            }),
5798        )
5799        .await;
5800        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
5801        fs.insert_branches(Path::new("/project/.git"), &["main", "feature-worktree"]);
5802
5803        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
5804
5805        let multi_workspace =
5806            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5807
5808        let workspace = multi_workspace
5809            .read_with(cx, |multi_workspace, _cx| {
5810                multi_workspace.workspace().clone()
5811            })
5812            .unwrap();
5813
5814        workspace.update(cx, |workspace, _cx| {
5815            workspace.set_random_database_id();
5816        });
5817
5818        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5819
5820        cx.run_until_parked();
5821
5822        let panel = workspace.update_in(cx, |workspace, window, cx| {
5823            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
5824            workspace.add_panel(panel.clone(), window, cx);
5825            panel
5826        });
5827
5828        cx.run_until_parked();
5829
5830        panel.update_in(cx, |panel, window, cx| {
5831            panel.set_start_thread_in(
5832                &StartThreadIn::NewWorktree {
5833                    worktree_name: Some("feature worktree".to_string()),
5834                    branch_target: NewWorktreeBranchTarget::CurrentBranch,
5835                },
5836                window,
5837                cx,
5838            );
5839        });
5840
5841        fs.add_linked_worktree_for_repo(
5842            Path::new("/project/.git"),
5843            true,
5844            GitWorktree {
5845                path: PathBuf::from("/linked-feature-worktree"),
5846                ref_name: Some("refs/heads/feature-worktree".into()),
5847                sha: "abcdef1".into(),
5848                is_main: false,
5849            },
5850        )
5851        .await;
5852
5853        project
5854            .update(cx, |project, cx| project.git_scans_complete(cx))
5855            .await;
5856        cx.run_until_parked();
5857
5858        panel.read_with(cx, |panel, _cx| {
5859            assert_eq!(
5860                *panel.start_thread_in(),
5861                StartThreadIn::NewWorktree {
5862                    worktree_name: Some("feature worktree".to_string()),
5863                    branch_target: NewWorktreeBranchTarget::CurrentBranch,
5864                },
5865                "thread target should remain a named new worktree when the external linked worktree does not match the selected branch target",
5866            );
5867        });
5868    }
5869
5870    #[gpui::test]
5871    async fn test_thread_target_serialization_round_trip(cx: &mut TestAppContext) {
5872        init_test(cx);
5873        cx.update(|cx| {
5874            agent::ThreadStore::init_global(cx);
5875            language_model::LanguageModelRegistry::test(cx);
5876        });
5877
5878        let fs = FakeFs::new(cx.executor());
5879        fs.insert_tree(
5880            "/project",
5881            json!({
5882                ".git": {},
5883                "src": {
5884                    "main.rs": "fn main() {}"
5885                }
5886            }),
5887        )
5888        .await;
5889        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
5890
5891        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
5892
5893        let multi_workspace =
5894            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5895
5896        let workspace = multi_workspace
5897            .read_with(cx, |multi_workspace, _cx| {
5898                multi_workspace.workspace().clone()
5899            })
5900            .unwrap();
5901
5902        workspace.update(cx, |workspace, _cx| {
5903            workspace.set_random_database_id();
5904        });
5905
5906        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5907
5908        // Wait for the project to discover the git repository.
5909        cx.run_until_parked();
5910
5911        let panel = workspace.update_in(cx, |workspace, window, cx| {
5912            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
5913            workspace.add_panel(panel.clone(), window, cx);
5914            panel
5915        });
5916
5917        cx.run_until_parked();
5918
5919        // Default should be LocalProject.
5920        panel.read_with(cx, |panel, _cx| {
5921            assert_eq!(*panel.start_thread_in(), StartThreadIn::LocalProject);
5922        });
5923
5924        // Change thread target to NewWorktree.
5925        panel.update_in(cx, |panel, window, cx| {
5926            panel.set_start_thread_in(
5927                &StartThreadIn::NewWorktree {
5928                    worktree_name: None,
5929                    branch_target: NewWorktreeBranchTarget::default(),
5930                },
5931                window,
5932                cx,
5933            );
5934        });
5935
5936        panel.read_with(cx, |panel, _cx| {
5937            assert_eq!(
5938                *panel.start_thread_in(),
5939                StartThreadIn::NewWorktree {
5940                    worktree_name: None,
5941                    branch_target: NewWorktreeBranchTarget::default(),
5942                },
5943                "thread target should be NewWorktree after set_thread_target"
5944            );
5945        });
5946
5947        // Let serialization complete.
5948        cx.run_until_parked();
5949
5950        // Load a fresh panel from the serialized data.
5951        let async_cx = cx.update(|window, cx| window.to_async(cx));
5952        let loaded_panel = AgentPanel::load(workspace.downgrade(), async_cx)
5953            .await
5954            .expect("panel load should succeed");
5955        cx.run_until_parked();
5956
5957        loaded_panel.read_with(cx, |panel, _cx| {
5958            assert_eq!(
5959                *panel.start_thread_in(),
5960                StartThreadIn::NewWorktree {
5961                    worktree_name: None,
5962                    branch_target: NewWorktreeBranchTarget::default(),
5963                },
5964                "thread target should survive serialization round-trip"
5965            );
5966        });
5967    }
5968
5969    #[gpui::test]
5970    async fn test_set_active_blocked_during_worktree_creation(cx: &mut TestAppContext) {
5971        init_test(cx);
5972
5973        let fs = FakeFs::new(cx.executor());
5974        cx.update(|cx| {
5975            agent::ThreadStore::init_global(cx);
5976            language_model::LanguageModelRegistry::test(cx);
5977            <dyn fs::Fs>::set_global(fs.clone(), cx);
5978        });
5979
5980        fs.insert_tree(
5981            "/project",
5982            json!({
5983                ".git": {},
5984                "src": {
5985                    "main.rs": "fn main() {}"
5986                }
5987            }),
5988        )
5989        .await;
5990
5991        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
5992
5993        let multi_workspace =
5994            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5995
5996        let workspace = multi_workspace
5997            .read_with(cx, |multi_workspace, _cx| {
5998                multi_workspace.workspace().clone()
5999            })
6000            .unwrap();
6001
6002        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
6003
6004        let panel = workspace.update_in(cx, |workspace, window, cx| {
6005            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
6006            workspace.add_panel(panel.clone(), window, cx);
6007            panel
6008        });
6009
6010        cx.run_until_parked();
6011
6012        // Simulate worktree creation in progress and reset to Uninitialized
6013        panel.update_in(cx, |panel, window, cx| {
6014            panel.worktree_creation_status = Some(WorktreeCreationStatus::Creating);
6015            panel.active_view = ActiveView::Uninitialized;
6016            Panel::set_active(panel, true, window, cx);
6017            assert!(
6018                matches!(panel.active_view, ActiveView::Uninitialized),
6019                "set_active should not create a thread while worktree is being created"
6020            );
6021        });
6022
6023        // Clear the creation status and use open_external_thread_with_server
6024        // (which bypasses new_agent_thread) to verify the panel can transition
6025        // out of Uninitialized. We can't call set_active directly because
6026        // new_agent_thread requires full agent server infrastructure.
6027        panel.update_in(cx, |panel, window, cx| {
6028            panel.worktree_creation_status = None;
6029            panel.active_view = ActiveView::Uninitialized;
6030            panel.open_external_thread_with_server(
6031                Rc::new(StubAgentServer::default_response()),
6032                window,
6033                cx,
6034            );
6035        });
6036
6037        cx.run_until_parked();
6038
6039        panel.read_with(cx, |panel, _cx| {
6040            assert!(
6041                !matches!(panel.active_view, ActiveView::Uninitialized),
6042                "panel should transition out of Uninitialized once worktree creation is cleared"
6043            );
6044        });
6045    }
6046
6047    #[test]
6048    fn test_deserialize_agent_variants() {
6049        // PascalCase (legacy AgentType format, persisted in panel state)
6050        assert_eq!(
6051            serde_json::from_str::<Agent>(r#""NativeAgent""#).unwrap(),
6052            Agent::NativeAgent,
6053        );
6054        assert_eq!(
6055            serde_json::from_str::<Agent>(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(),
6056            Agent::Custom {
6057                id: "my-agent".into(),
6058            },
6059        );
6060
6061        // Legacy TextThread variant deserializes to NativeAgent
6062        assert_eq!(
6063            serde_json::from_str::<Agent>(r#""TextThread""#).unwrap(),
6064            Agent::NativeAgent,
6065        );
6066
6067        // snake_case (canonical format)
6068        assert_eq!(
6069            serde_json::from_str::<Agent>(r#""native_agent""#).unwrap(),
6070            Agent::NativeAgent,
6071        );
6072        assert_eq!(
6073            serde_json::from_str::<Agent>(r#"{"custom":{"name":"my-agent"}}"#).unwrap(),
6074            Agent::Custom {
6075                id: "my-agent".into(),
6076            },
6077        );
6078
6079        // Serialization uses snake_case
6080        assert_eq!(
6081            serde_json::to_string(&Agent::NativeAgent).unwrap(),
6082            r#""native_agent""#,
6083        );
6084        assert_eq!(
6085            serde_json::to_string(&Agent::Custom {
6086                id: "my-agent".into()
6087            })
6088            .unwrap(),
6089            r#"{"custom":{"name":"my-agent"}}"#,
6090        );
6091    }
6092
6093    #[test]
6094    fn test_resolve_worktree_branch_target() {
6095        let existing_branches = HashSet::from_iter([
6096            "main".to_string(),
6097            "feature".to_string(),
6098            "origin/main".to_string(),
6099        ]);
6100
6101        let resolved = AgentPanel::resolve_worktree_branch_target(
6102            &NewWorktreeBranchTarget::CreateBranch {
6103                name: "new-branch".to_string(),
6104                from_ref: Some("main".to_string()),
6105            },
6106            &existing_branches,
6107            &HashSet::from_iter(["main".to_string()]),
6108        )
6109        .unwrap();
6110        assert_eq!(
6111            resolved,
6112            ("new-branch".to_string(), false, Some("main".to_string()))
6113        );
6114
6115        let resolved = AgentPanel::resolve_worktree_branch_target(
6116            &NewWorktreeBranchTarget::ExistingBranch {
6117                name: "feature".to_string(),
6118            },
6119            &existing_branches,
6120            &HashSet::default(),
6121        )
6122        .unwrap();
6123        assert_eq!(resolved, ("feature".to_string(), true, None));
6124
6125        let resolved = AgentPanel::resolve_worktree_branch_target(
6126            &NewWorktreeBranchTarget::ExistingBranch {
6127                name: "main".to_string(),
6128            },
6129            &existing_branches,
6130            &HashSet::from_iter(["main".to_string()]),
6131        )
6132        .unwrap();
6133        assert_eq!(resolved.1, false);
6134        assert_eq!(resolved.2, Some("main".to_string()));
6135        assert_ne!(resolved.0, "main");
6136        assert!(existing_branches.contains("main"));
6137        assert!(!existing_branches.contains(&resolved.0));
6138    }
6139
6140    #[gpui::test]
6141    async fn test_worktree_creation_preserves_selected_agent(cx: &mut TestAppContext) {
6142        init_test(cx);
6143
6144        let app_state = cx.update(|cx| {
6145            agent::ThreadStore::init_global(cx);
6146            language_model::LanguageModelRegistry::test(cx);
6147
6148            let app_state = workspace::AppState::test(cx);
6149            workspace::init(app_state.clone(), cx);
6150            app_state
6151        });
6152
6153        let fs = app_state.fs.as_fake();
6154        fs.insert_tree(
6155            "/project",
6156            json!({
6157                ".git": {},
6158                "src": {
6159                    "main.rs": "fn main() {}"
6160                }
6161            }),
6162        )
6163        .await;
6164        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
6165
6166        let project = Project::test(app_state.fs.clone(), [Path::new("/project")], cx).await;
6167
6168        let multi_workspace =
6169            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6170        multi_workspace
6171            .update(cx, |multi_workspace, _, cx| {
6172                multi_workspace.open_sidebar(cx);
6173            })
6174            .unwrap();
6175
6176        let workspace = multi_workspace
6177            .read_with(cx, |multi_workspace, _cx| {
6178                multi_workspace.workspace().clone()
6179            })
6180            .unwrap();
6181
6182        workspace.update(cx, |workspace, _cx| {
6183            workspace.set_random_database_id();
6184        });
6185
6186        // Register a callback so new workspaces also get an AgentPanel.
6187        cx.update(|cx| {
6188            cx.observe_new(
6189                |workspace: &mut Workspace,
6190                 window: Option<&mut Window>,
6191                 cx: &mut Context<Workspace>| {
6192                    if let Some(window) = window {
6193                        let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
6194                        workspace.add_panel(panel, window, cx);
6195                    }
6196                },
6197            )
6198            .detach();
6199        });
6200
6201        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
6202
6203        // Wait for the project to discover the git repository.
6204        cx.run_until_parked();
6205
6206        let panel = workspace.update_in(cx, |workspace, window, cx| {
6207            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
6208            workspace.add_panel(panel.clone(), window, cx);
6209            panel
6210        });
6211
6212        cx.run_until_parked();
6213
6214        // Open a thread (needed so there's an active thread view).
6215        panel.update_in(cx, |panel, window, cx| {
6216            panel.open_external_thread_with_server(
6217                Rc::new(StubAgentServer::default_response()),
6218                window,
6219                cx,
6220            );
6221        });
6222
6223        cx.run_until_parked();
6224
6225        // Set the selected agent to Codex (a custom agent) and start_thread_in
6226        // to NewWorktree. We do this AFTER opening the thread because
6227        // open_external_thread_with_server overrides selected_agent.
6228        panel.update_in(cx, |panel, window, cx| {
6229            panel.selected_agent = Agent::Custom {
6230                id: CODEX_ID.into(),
6231            };
6232            panel.set_start_thread_in(
6233                &StartThreadIn::NewWorktree {
6234                    worktree_name: None,
6235                    branch_target: NewWorktreeBranchTarget::default(),
6236                },
6237                window,
6238                cx,
6239            );
6240        });
6241
6242        // Verify the panel has the Codex agent selected.
6243        panel.read_with(cx, |panel, _cx| {
6244            assert_eq!(
6245                panel.selected_agent,
6246                Agent::Custom {
6247                    id: CODEX_ID.into()
6248                },
6249            );
6250        });
6251
6252        // Directly call handle_worktree_creation_requested, which is what
6253        // handle_first_send_requested does when start_thread_in == NewWorktree.
6254        let content = vec![acp::ContentBlock::Text(acp::TextContent::new(
6255            "Hello from test",
6256        ))];
6257        panel.update_in(cx, |panel, window, cx| {
6258            panel.handle_worktree_requested(
6259                content,
6260                WorktreeCreationArgs::New {
6261                    worktree_name: None,
6262                    branch_target: NewWorktreeBranchTarget::default(),
6263                },
6264                window,
6265                cx,
6266            );
6267        });
6268
6269        // Let the async worktree creation + workspace setup complete.
6270        cx.run_until_parked();
6271
6272        panel.read_with(cx, |panel, _cx| {
6273            assert_eq!(
6274                panel.start_thread_in(),
6275                &StartThreadIn::LocalProject,
6276                "the original panel should reset start_thread_in back to the local project after creating a worktree workspace",
6277            );
6278        });
6279
6280        // Find the new workspace's AgentPanel and verify it used the Codex agent.
6281        let found_codex = multi_workspace
6282            .read_with(cx, |multi_workspace, cx| {
6283                // There should be more than one workspace now (the original + the new worktree).
6284                assert!(
6285                    multi_workspace.workspaces().count() > 1,
6286                    "expected a new workspace to have been created, found {}",
6287                    multi_workspace.workspaces().count(),
6288                );
6289
6290                // Check the newest workspace's panel for the correct agent.
6291                let new_workspace = multi_workspace
6292                    .workspaces()
6293                    .find(|ws| ws.entity_id() != workspace.entity_id())
6294                    .expect("should find the new workspace");
6295                let new_panel = new_workspace
6296                    .read(cx)
6297                    .panel::<AgentPanel>(cx)
6298                    .expect("new workspace should have an AgentPanel");
6299
6300                new_panel.read(cx).selected_agent.clone()
6301            })
6302            .unwrap();
6303
6304        assert_eq!(
6305            found_codex,
6306            Agent::Custom {
6307                id: CODEX_ID.into()
6308            },
6309            "the new worktree workspace should use the same agent (Codex) that was selected in the original panel",
6310        );
6311    }
6312
6313    #[gpui::test]
6314    async fn test_work_dirs_update_when_worktrees_change(cx: &mut TestAppContext) {
6315        use crate::thread_metadata_store::ThreadMetadataStore;
6316
6317        init_test(cx);
6318        cx.update(|cx| {
6319            agent::ThreadStore::init_global(cx);
6320            language_model::LanguageModelRegistry::test(cx);
6321        });
6322
6323        // Set up a project with one worktree.
6324        let fs = FakeFs::new(cx.executor());
6325        fs.insert_tree("/project_a", json!({ "file.txt": "" }))
6326            .await;
6327        let project = Project::test(fs.clone(), [Path::new("/project_a")], cx).await;
6328
6329        let multi_workspace =
6330            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6331        let workspace = multi_workspace
6332            .read_with(cx, |mw, _cx| mw.workspace().clone())
6333            .unwrap();
6334        let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
6335
6336        let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
6337            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
6338        });
6339
6340        // Open thread A and send a message. With empty next_prompt_updates it
6341        // stays generating, so opening B will move A to background_threads.
6342        let connection_a = StubAgentConnection::new().with_agent_id("agent-a".into());
6343        open_thread_with_custom_connection(&panel, connection_a.clone(), &mut cx);
6344        send_message(&panel, &mut cx);
6345        let session_id_a = active_session_id(&panel, &cx);
6346
6347        // Open thread C — thread A (generating) moves to background.
6348        // Thread C completes immediately (idle), then opening B moves C to background too.
6349        let connection_c = StubAgentConnection::new().with_agent_id("agent-c".into());
6350        connection_c.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
6351            acp::ContentChunk::new("done".into()),
6352        )]);
6353        open_thread_with_custom_connection(&panel, connection_c.clone(), &mut cx);
6354        send_message(&panel, &mut cx);
6355        let session_id_c = active_session_id(&panel, &cx);
6356
6357        // Open thread B — thread C (idle, non-loadable) is retained in background.
6358        let connection_b = StubAgentConnection::new().with_agent_id("agent-b".into());
6359        open_thread_with_custom_connection(&panel, connection_b.clone(), &mut cx);
6360        send_message(&panel, &mut cx);
6361        let session_id_b = active_session_id(&panel, &cx);
6362
6363        let metadata_store = cx.update(|_, cx| ThreadMetadataStore::global(cx));
6364
6365        panel.read_with(&cx, |panel, _cx| {
6366            assert!(
6367                panel.background_threads.contains_key(&session_id_a),
6368                "Thread A should be in background_threads"
6369            );
6370            assert!(
6371                panel.background_threads.contains_key(&session_id_c),
6372                "Thread C should be in background_threads"
6373            );
6374        });
6375
6376        // Verify initial work_dirs for thread B contain only /project_a.
6377        let initial_b_paths = panel.read_with(&cx, |panel, cx| {
6378            let thread = panel.active_agent_thread(cx).unwrap();
6379            thread.read(cx).work_dirs().cloned().unwrap()
6380        });
6381        assert_eq!(
6382            initial_b_paths.ordered_paths().collect::<Vec<_>>(),
6383            vec![&PathBuf::from("/project_a")],
6384            "Thread B should initially have only /project_a"
6385        );
6386
6387        // Now add a second worktree to the project.
6388        fs.insert_tree("/project_b", json!({ "other.txt": "" }))
6389            .await;
6390        let (new_tree, _) = project
6391            .update(&mut cx, |project, cx| {
6392                project.find_or_create_worktree("/project_b", true, cx)
6393            })
6394            .await
6395            .unwrap();
6396        cx.read(|cx| new_tree.read(cx).as_local().unwrap().scan_complete())
6397            .await;
6398        cx.run_until_parked();
6399
6400        // Verify thread B's (active) work_dirs now include both worktrees.
6401        let updated_b_paths = panel.read_with(&cx, |panel, cx| {
6402            let thread = panel.active_agent_thread(cx).unwrap();
6403            thread.read(cx).work_dirs().cloned().unwrap()
6404        });
6405        let mut b_paths_sorted = updated_b_paths.ordered_paths().cloned().collect::<Vec<_>>();
6406        b_paths_sorted.sort();
6407        assert_eq!(
6408            b_paths_sorted,
6409            vec![PathBuf::from("/project_a"), PathBuf::from("/project_b")],
6410            "Thread B work_dirs should include both worktrees after adding /project_b"
6411        );
6412
6413        // Verify thread A's (background) work_dirs are also updated.
6414        let updated_a_paths = panel.read_with(&cx, |panel, cx| {
6415            let bg_view = panel.background_threads.get(&session_id_a).unwrap();
6416            let root_thread = bg_view.read(cx).root_thread(cx).unwrap();
6417            root_thread
6418                .read(cx)
6419                .thread
6420                .read(cx)
6421                .work_dirs()
6422                .cloned()
6423                .unwrap()
6424        });
6425        let mut a_paths_sorted = updated_a_paths.ordered_paths().cloned().collect::<Vec<_>>();
6426        a_paths_sorted.sort();
6427        assert_eq!(
6428            a_paths_sorted,
6429            vec![PathBuf::from("/project_a"), PathBuf::from("/project_b")],
6430            "Thread A work_dirs should include both worktrees after adding /project_b"
6431        );
6432
6433        // Verify thread idle C was also updated.
6434        let updated_c_paths = panel.read_with(&cx, |panel, cx| {
6435            let bg_view = panel.background_threads.get(&session_id_c).unwrap();
6436            let root_thread = bg_view.read(cx).root_thread(cx).unwrap();
6437            root_thread
6438                .read(cx)
6439                .thread
6440                .read(cx)
6441                .work_dirs()
6442                .cloned()
6443                .unwrap()
6444        });
6445        let mut c_paths_sorted = updated_c_paths.ordered_paths().cloned().collect::<Vec<_>>();
6446        c_paths_sorted.sort();
6447        assert_eq!(
6448            c_paths_sorted,
6449            vec![PathBuf::from("/project_a"), PathBuf::from("/project_b")],
6450            "Thread C (idle background) work_dirs should include both worktrees after adding /project_b"
6451        );
6452
6453        // Verify the metadata store reflects the new paths for running threads only.
6454        cx.run_until_parked();
6455        for (label, session_id) in [("thread B", &session_id_b), ("thread A", &session_id_a)] {
6456            let metadata_paths = metadata_store.read_with(&cx, |store, _cx| {
6457                let metadata = store
6458                    .entry(session_id)
6459                    .unwrap_or_else(|| panic!("{label} thread metadata should exist"));
6460                metadata.folder_paths.clone()
6461            });
6462            let mut sorted = metadata_paths.ordered_paths().cloned().collect::<Vec<_>>();
6463            sorted.sort();
6464            assert_eq!(
6465                sorted,
6466                vec![PathBuf::from("/project_a"), PathBuf::from("/project_b")],
6467                "{label} thread metadata folder_paths should include both worktrees"
6468            );
6469        }
6470
6471        // Now remove a worktree and verify work_dirs shrink.
6472        let worktree_b_id = new_tree.read_with(&cx, |tree, _| tree.id());
6473        project.update(&mut cx, |project, cx| {
6474            project.remove_worktree(worktree_b_id, cx);
6475        });
6476        cx.run_until_parked();
6477
6478        let after_remove_b = panel.read_with(&cx, |panel, cx| {
6479            let thread = panel.active_agent_thread(cx).unwrap();
6480            thread.read(cx).work_dirs().cloned().unwrap()
6481        });
6482        assert_eq!(
6483            after_remove_b.ordered_paths().collect::<Vec<_>>(),
6484            vec![&PathBuf::from("/project_a")],
6485            "Thread B work_dirs should revert to only /project_a after removing /project_b"
6486        );
6487
6488        let after_remove_a = panel.read_with(&cx, |panel, cx| {
6489            let bg_view = panel.background_threads.get(&session_id_a).unwrap();
6490            let root_thread = bg_view.read(cx).root_thread(cx).unwrap();
6491            root_thread
6492                .read(cx)
6493                .thread
6494                .read(cx)
6495                .work_dirs()
6496                .cloned()
6497                .unwrap()
6498        });
6499        assert_eq!(
6500            after_remove_a.ordered_paths().collect::<Vec<_>>(),
6501            vec![&PathBuf::from("/project_a")],
6502            "Thread A work_dirs should revert to only /project_a after removing /project_b"
6503        );
6504    }
6505
6506    #[gpui::test]
6507    async fn test_new_workspace_inherits_global_last_used_agent(cx: &mut TestAppContext) {
6508        init_test(cx);
6509        cx.update(|cx| {
6510            agent::ThreadStore::init_global(cx);
6511            language_model::LanguageModelRegistry::test(cx);
6512            // Use an isolated DB so parallel tests can't overwrite our global key.
6513            cx.set_global(db::AppDatabase::test_new());
6514        });
6515
6516        let custom_agent = Agent::Custom {
6517            id: "my-preferred-agent".into(),
6518        };
6519
6520        // Write a known agent to the global KVP to simulate a user who has
6521        // previously used this agent in another workspace.
6522        let kvp = cx.update(|cx| KeyValueStore::global(cx));
6523        write_global_last_used_agent(kvp, custom_agent.clone()).await;
6524
6525        let fs = FakeFs::new(cx.executor());
6526        let project = Project::test(fs.clone(), [], cx).await;
6527
6528        let multi_workspace =
6529            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6530
6531        let workspace = multi_workspace
6532            .read_with(cx, |multi_workspace, _cx| {
6533                multi_workspace.workspace().clone()
6534            })
6535            .unwrap();
6536
6537        workspace.update(cx, |workspace, _cx| {
6538            workspace.set_random_database_id();
6539        });
6540
6541        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
6542
6543        // Load the panel via `load()`, which reads the global fallback
6544        // asynchronously when no per-workspace state exists.
6545        let async_cx = cx.update(|window, cx| window.to_async(cx));
6546        let panel = AgentPanel::load(workspace.downgrade(), async_cx)
6547            .await
6548            .expect("panel load should succeed");
6549        cx.run_until_parked();
6550
6551        panel.read_with(cx, |panel, _cx| {
6552            assert_eq!(
6553                panel.selected_agent, custom_agent,
6554                "new workspace should inherit the global last-used agent"
6555            );
6556        });
6557    }
6558
6559    #[gpui::test]
6560    async fn test_workspaces_maintain_independent_agent_selection(cx: &mut TestAppContext) {
6561        init_test(cx);
6562        cx.update(|cx| {
6563            agent::ThreadStore::init_global(cx);
6564            language_model::LanguageModelRegistry::test(cx);
6565        });
6566
6567        let fs = FakeFs::new(cx.executor());
6568        let project_a = Project::test(fs.clone(), [], cx).await;
6569        let project_b = Project::test(fs, [], cx).await;
6570
6571        let multi_workspace =
6572            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
6573
6574        let workspace_a = multi_workspace
6575            .read_with(cx, |multi_workspace, _cx| {
6576                multi_workspace.workspace().clone()
6577            })
6578            .unwrap();
6579
6580        let workspace_b = multi_workspace
6581            .update(cx, |multi_workspace, window, cx| {
6582                multi_workspace.test_add_workspace(project_b.clone(), window, cx)
6583            })
6584            .unwrap();
6585
6586        workspace_a.update(cx, |workspace, _cx| {
6587            workspace.set_random_database_id();
6588        });
6589        workspace_b.update(cx, |workspace, _cx| {
6590            workspace.set_random_database_id();
6591        });
6592
6593        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
6594
6595        let agent_a = Agent::Custom {
6596            id: "agent-alpha".into(),
6597        };
6598        let agent_b = Agent::Custom {
6599            id: "agent-beta".into(),
6600        };
6601
6602        // Set up workspace A with agent_a
6603        let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
6604            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
6605        });
6606        panel_a.update(cx, |panel, _cx| {
6607            panel.selected_agent = agent_a.clone();
6608        });
6609
6610        // Set up workspace B with agent_b
6611        let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
6612            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
6613        });
6614        panel_b.update(cx, |panel, _cx| {
6615            panel.selected_agent = agent_b.clone();
6616        });
6617
6618        // Serialize both panels
6619        panel_a.update(cx, |panel, cx| panel.serialize(cx));
6620        panel_b.update(cx, |panel, cx| panel.serialize(cx));
6621        cx.run_until_parked();
6622
6623        // Load fresh panels from serialized state and verify independence
6624        let async_cx = cx.update(|window, cx| window.to_async(cx));
6625        let loaded_a = AgentPanel::load(workspace_a.downgrade(), async_cx)
6626            .await
6627            .expect("panel A load should succeed");
6628        cx.run_until_parked();
6629
6630        let async_cx = cx.update(|window, cx| window.to_async(cx));
6631        let loaded_b = AgentPanel::load(workspace_b.downgrade(), async_cx)
6632            .await
6633            .expect("panel B load should succeed");
6634        cx.run_until_parked();
6635
6636        loaded_a.read_with(cx, |panel, _cx| {
6637            assert_eq!(
6638                panel.selected_agent, agent_a,
6639                "workspace A should restore agent-alpha, not agent-beta"
6640            );
6641        });
6642
6643        loaded_b.read_with(cx, |panel, _cx| {
6644            assert_eq!(
6645                panel.selected_agent, agent_b,
6646                "workspace B should restore agent-beta, not agent-alpha"
6647            );
6648        });
6649    }
6650
6651    #[gpui::test]
6652    async fn test_new_thread_uses_workspace_selected_agent(cx: &mut TestAppContext) {
6653        init_test(cx);
6654        cx.update(|cx| {
6655            agent::ThreadStore::init_global(cx);
6656            language_model::LanguageModelRegistry::test(cx);
6657        });
6658
6659        let fs = FakeFs::new(cx.executor());
6660        let project = Project::test(fs.clone(), [], cx).await;
6661
6662        let multi_workspace =
6663            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6664
6665        let workspace = multi_workspace
6666            .read_with(cx, |multi_workspace, _cx| {
6667                multi_workspace.workspace().clone()
6668            })
6669            .unwrap();
6670
6671        workspace.update(cx, |workspace, _cx| {
6672            workspace.set_random_database_id();
6673        });
6674
6675        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
6676
6677        let custom_agent = Agent::Custom {
6678            id: "my-custom-agent".into(),
6679        };
6680
6681        let panel = workspace.update_in(cx, |workspace, window, cx| {
6682            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
6683            workspace.add_panel(panel.clone(), window, cx);
6684            panel
6685        });
6686
6687        // Set selected_agent to a custom agent
6688        panel.update(cx, |panel, _cx| {
6689            panel.selected_agent = custom_agent.clone();
6690        });
6691
6692        // Call new_thread, which internally calls external_thread(None, ...)
6693        // This resolves the agent from self.selected_agent
6694        panel.update_in(cx, |panel, window, cx| {
6695            panel.new_thread(&NewThread, window, cx);
6696        });
6697
6698        panel.read_with(cx, |panel, _cx| {
6699            assert_eq!(
6700                panel.selected_agent, custom_agent,
6701                "selected_agent should remain the custom agent after new_thread"
6702            );
6703            assert!(
6704                panel.active_conversation_view().is_some(),
6705                "a thread should have been created"
6706            );
6707        });
6708    }
6709
6710    #[gpui::test]
6711    async fn test_rollback_all_succeed_returns_ok(cx: &mut TestAppContext) {
6712        init_test(cx);
6713        let fs = FakeFs::new(cx.executor());
6714        cx.update(|cx| {
6715            cx.update_flags(true, vec!["agent-v2".to_string()]);
6716            agent::ThreadStore::init_global(cx);
6717            language_model::LanguageModelRegistry::test(cx);
6718            <dyn fs::Fs>::set_global(fs.clone(), cx);
6719        });
6720
6721        fs.insert_tree(
6722            "/project",
6723            json!({
6724                ".git": {},
6725                "src": { "main.rs": "fn main() {}" }
6726            }),
6727        )
6728        .await;
6729
6730        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
6731        cx.executor().run_until_parked();
6732
6733        let repository = project.read_with(cx, |project, cx| {
6734            project.repositories(cx).values().next().unwrap().clone()
6735        });
6736
6737        let multi_workspace =
6738            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6739
6740        let path_a = PathBuf::from("/worktrees/branch/project_a");
6741        let path_b = PathBuf::from("/worktrees/branch/project_b");
6742
6743        let (sender_a, receiver_a) = futures::channel::oneshot::channel::<Result<()>>();
6744        let (sender_b, receiver_b) = futures::channel::oneshot::channel::<Result<()>>();
6745        sender_a.send(Ok(())).unwrap();
6746        sender_b.send(Ok(())).unwrap();
6747
6748        let creation_infos = vec![
6749            (repository.clone(), path_a.clone(), receiver_a),
6750            (repository.clone(), path_b.clone(), receiver_b),
6751        ];
6752
6753        let fs_clone = fs.clone();
6754        let result = multi_workspace
6755            .update(cx, |_, window, cx| {
6756                window.spawn(cx, async move |cx| {
6757                    AgentPanel::await_and_rollback_on_failure(creation_infos, fs_clone, cx).await
6758                })
6759            })
6760            .unwrap()
6761            .await;
6762
6763        let paths = result.expect("all succeed should return Ok");
6764        assert_eq!(paths, vec![path_a, path_b]);
6765    }
6766
6767    #[gpui::test]
6768    async fn test_rollback_on_failure_attempts_all_worktrees(cx: &mut TestAppContext) {
6769        init_test(cx);
6770        let fs = FakeFs::new(cx.executor());
6771        cx.update(|cx| {
6772            cx.update_flags(true, vec!["agent-v2".to_string()]);
6773            agent::ThreadStore::init_global(cx);
6774            language_model::LanguageModelRegistry::test(cx);
6775            <dyn fs::Fs>::set_global(fs.clone(), cx);
6776        });
6777
6778        fs.insert_tree(
6779            "/project",
6780            json!({
6781                ".git": {},
6782                "src": { "main.rs": "fn main() {}" }
6783            }),
6784        )
6785        .await;
6786
6787        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
6788        cx.executor().run_until_parked();
6789
6790        let repository = project.read_with(cx, |project, cx| {
6791            project.repositories(cx).values().next().unwrap().clone()
6792        });
6793
6794        // Actually create a worktree so it exists in FakeFs for rollback to find.
6795        let success_path = PathBuf::from("/worktrees/branch/project");
6796        cx.update(|cx| {
6797            repository.update(cx, |repo, _| {
6798                repo.create_worktree(
6799                    git::repository::CreateWorktreeTarget::NewBranch {
6800                        branch_name: "branch".to_string(),
6801                        base_sha: None,
6802                    },
6803                    success_path.clone(),
6804                )
6805            })
6806        })
6807        .await
6808        .unwrap()
6809        .unwrap();
6810        cx.executor().run_until_parked();
6811
6812        // Verify the worktree directory exists before rollback.
6813        assert!(
6814            fs.is_dir(&success_path).await,
6815            "worktree directory should exist before rollback"
6816        );
6817
6818        let multi_workspace =
6819            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6820
6821        // Build creation_infos: one success, one failure.
6822        let failed_path = PathBuf::from("/worktrees/branch/failed_project");
6823
6824        let (sender_ok, receiver_ok) = futures::channel::oneshot::channel::<Result<()>>();
6825        let (sender_err, receiver_err) = futures::channel::oneshot::channel::<Result<()>>();
6826        sender_ok.send(Ok(())).unwrap();
6827        sender_err
6828            .send(Err(anyhow!("branch already exists")))
6829            .unwrap();
6830
6831        let creation_infos = vec![
6832            (repository.clone(), success_path.clone(), receiver_ok),
6833            (repository.clone(), failed_path.clone(), receiver_err),
6834        ];
6835
6836        let fs_clone = fs.clone();
6837        let result = multi_workspace
6838            .update(cx, |_, window, cx| {
6839                window.spawn(cx, async move |cx| {
6840                    AgentPanel::await_and_rollback_on_failure(creation_infos, fs_clone, cx).await
6841                })
6842            })
6843            .unwrap()
6844            .await;
6845
6846        assert!(
6847            result.is_err(),
6848            "should return error when any creation fails"
6849        );
6850        let err_msg = result.unwrap_err().to_string();
6851        assert!(
6852            err_msg.contains("branch already exists"),
6853            "error should mention the original failure: {err_msg}"
6854        );
6855
6856        // The successful worktree should have been rolled back by git.
6857        cx.executor().run_until_parked();
6858        assert!(
6859            !fs.is_dir(&success_path).await,
6860            "successful worktree directory should be removed by rollback"
6861        );
6862    }
6863
6864    #[gpui::test]
6865    async fn test_rollback_on_canceled_receiver(cx: &mut TestAppContext) {
6866        init_test(cx);
6867        let fs = FakeFs::new(cx.executor());
6868        cx.update(|cx| {
6869            cx.update_flags(true, vec!["agent-v2".to_string()]);
6870            agent::ThreadStore::init_global(cx);
6871            language_model::LanguageModelRegistry::test(cx);
6872            <dyn fs::Fs>::set_global(fs.clone(), cx);
6873        });
6874
6875        fs.insert_tree(
6876            "/project",
6877            json!({
6878                ".git": {},
6879                "src": { "main.rs": "fn main() {}" }
6880            }),
6881        )
6882        .await;
6883
6884        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
6885        cx.executor().run_until_parked();
6886
6887        let repository = project.read_with(cx, |project, cx| {
6888            project.repositories(cx).values().next().unwrap().clone()
6889        });
6890
6891        let multi_workspace =
6892            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6893
6894        let path = PathBuf::from("/worktrees/branch/project");
6895
6896        // Drop the sender to simulate a canceled receiver.
6897        let (_sender, receiver) = futures::channel::oneshot::channel::<Result<()>>();
6898        drop(_sender);
6899
6900        let creation_infos = vec![(repository.clone(), path.clone(), receiver)];
6901
6902        let fs_clone = fs.clone();
6903        let result = multi_workspace
6904            .update(cx, |_, window, cx| {
6905                window.spawn(cx, async move |cx| {
6906                    AgentPanel::await_and_rollback_on_failure(creation_infos, fs_clone, cx).await
6907                })
6908            })
6909            .unwrap()
6910            .await;
6911
6912        assert!(
6913            result.is_err(),
6914            "should return error when receiver is canceled"
6915        );
6916        let err_msg = result.unwrap_err().to_string();
6917        assert!(
6918            err_msg.contains("canceled"),
6919            "error should mention cancellation: {err_msg}"
6920        );
6921    }
6922
6923    #[gpui::test]
6924    async fn test_rollback_cleans_up_orphan_directories(cx: &mut TestAppContext) {
6925        init_test(cx);
6926        let fs = FakeFs::new(cx.executor());
6927        cx.update(|cx| {
6928            cx.update_flags(true, vec!["agent-v2".to_string()]);
6929            agent::ThreadStore::init_global(cx);
6930            language_model::LanguageModelRegistry::test(cx);
6931            <dyn fs::Fs>::set_global(fs.clone(), cx);
6932        });
6933
6934        fs.insert_tree(
6935            "/project",
6936            json!({
6937                ".git": {},
6938                "src": { "main.rs": "fn main() {}" }
6939            }),
6940        )
6941        .await;
6942
6943        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
6944        cx.executor().run_until_parked();
6945
6946        let repository = project.read_with(cx, |project, cx| {
6947            project.repositories(cx).values().next().unwrap().clone()
6948        });
6949
6950        let multi_workspace =
6951            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6952
6953        // Simulate the orphan state: create_dir_all was called but git
6954        // worktree add failed, leaving a directory with leftover files.
6955        let orphan_path = PathBuf::from("/worktrees/branch/orphan_project");
6956        fs.insert_tree(
6957            "/worktrees/branch/orphan_project",
6958            json!({ "leftover.txt": "junk" }),
6959        )
6960        .await;
6961
6962        assert!(
6963            fs.is_dir(&orphan_path).await,
6964            "orphan dir should exist before rollback"
6965        );
6966
6967        let (sender, receiver) = futures::channel::oneshot::channel::<Result<()>>();
6968        sender.send(Err(anyhow!("hook failed"))).unwrap();
6969
6970        let creation_infos = vec![(repository.clone(), orphan_path.clone(), receiver)];
6971
6972        let fs_clone = fs.clone();
6973        let result = multi_workspace
6974            .update(cx, |_, window, cx| {
6975                window.spawn(cx, async move |cx| {
6976                    AgentPanel::await_and_rollback_on_failure(creation_infos, fs_clone, cx).await
6977                })
6978            })
6979            .unwrap()
6980            .await;
6981
6982        cx.executor().run_until_parked();
6983
6984        assert!(result.is_err());
6985        assert!(
6986            !fs.is_dir(&orphan_path).await,
6987            "orphan worktree directory should be removed by filesystem cleanup"
6988        );
6989    }
6990
6991    #[gpui::test]
6992    async fn test_worktree_creation_for_remote_project(
6993        cx: &mut TestAppContext,
6994        server_cx: &mut TestAppContext,
6995    ) {
6996        init_test(cx);
6997
6998        let app_state = cx.update(|cx| {
6999            agent::ThreadStore::init_global(cx);
7000            language_model::LanguageModelRegistry::test(cx);
7001
7002            let app_state = workspace::AppState::test(cx);
7003            workspace::init(app_state.clone(), cx);
7004            app_state
7005        });
7006
7007        server_cx.update(|cx| {
7008            release_channel::init(semver::Version::new(0, 0, 0), cx);
7009        });
7010
7011        // Set up the remote server side with a git repo.
7012        let server_fs = FakeFs::new(server_cx.executor());
7013        server_fs
7014            .insert_tree(
7015                "/project",
7016                json!({
7017                    ".git": {},
7018                    "src": {
7019                        "main.rs": "fn main() {}"
7020                    }
7021                }),
7022            )
7023            .await;
7024        server_fs.set_branch_name(Path::new("/project/.git"), Some("main"));
7025
7026        // Create a mock remote connection.
7027        let (opts, server_session, _) = remote::RemoteClient::fake_server(cx, server_cx);
7028
7029        server_cx.update(remote_server::HeadlessProject::init);
7030        let server_executor = server_cx.executor();
7031        let _headless = server_cx.new(|cx| {
7032            remote_server::HeadlessProject::new(
7033                remote_server::HeadlessAppState {
7034                    session: server_session,
7035                    fs: server_fs.clone(),
7036                    http_client: Arc::new(http_client::BlockedHttpClient),
7037                    node_runtime: node_runtime::NodeRuntime::unavailable(),
7038                    languages: Arc::new(language::LanguageRegistry::new(server_executor.clone())),
7039                    extension_host_proxy: Arc::new(extension::ExtensionHostProxy::new()),
7040                    startup_time: Instant::now(),
7041                },
7042                false,
7043                cx,
7044            )
7045        });
7046
7047        // Connect the client side and build a remote project.
7048        // Use a separate Client to avoid double-registering proto handlers
7049        // (Workspace::test_new creates its own WorkspaceStore from the
7050        // project's client).
7051        let remote_client = remote::RemoteClient::connect_mock(opts, cx).await;
7052        let project = cx.update(|cx| {
7053            let project_client = client::Client::new(
7054                Arc::new(clock::FakeSystemClock::new()),
7055                http_client::FakeHttpClient::with_404_response(),
7056                cx,
7057            );
7058            let user_store = cx.new(|cx| client::UserStore::new(project_client.clone(), cx));
7059            project::Project::remote(
7060                remote_client,
7061                project_client,
7062                node_runtime::NodeRuntime::unavailable(),
7063                user_store,
7064                app_state.languages.clone(),
7065                app_state.fs.clone(),
7066                false,
7067                cx,
7068            )
7069        });
7070
7071        // Open the remote path as a worktree in the project.
7072        let worktree_path = Path::new("/project");
7073        project
7074            .update(cx, |project, cx| {
7075                project.find_or_create_worktree(worktree_path, true, cx)
7076            })
7077            .await
7078            .expect("should be able to open remote worktree");
7079        cx.run_until_parked();
7080
7081        // Verify the project is indeed remote.
7082        project.read_with(cx, |project, cx| {
7083            assert!(!project.is_local(), "project should be remote, not local");
7084            assert!(
7085                project.remote_connection_options(cx).is_some(),
7086                "project should have remote connection options"
7087            );
7088        });
7089
7090        // Create the workspace and agent panel.
7091        let multi_workspace =
7092            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
7093        multi_workspace
7094            .update(cx, |multi_workspace, _, cx| {
7095                multi_workspace.open_sidebar(cx);
7096            })
7097            .unwrap();
7098
7099        let workspace = multi_workspace
7100            .read_with(cx, |mw, _cx| mw.workspace().clone())
7101            .unwrap();
7102
7103        workspace.update(cx, |workspace, _cx| {
7104            workspace.set_random_database_id();
7105        });
7106
7107        // Register a callback so new workspaces also get an AgentPanel.
7108        cx.update(|cx| {
7109            cx.observe_new(
7110                |workspace: &mut Workspace,
7111                 window: Option<&mut Window>,
7112                 cx: &mut Context<Workspace>| {
7113                    if let Some(window) = window {
7114                        let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
7115                        workspace.add_panel(panel, window, cx);
7116                    }
7117                },
7118            )
7119            .detach();
7120        });
7121
7122        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
7123        cx.run_until_parked();
7124
7125        let panel = workspace.update_in(cx, |workspace, window, cx| {
7126            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
7127            workspace.add_panel(panel.clone(), window, cx);
7128            panel
7129        });
7130
7131        cx.run_until_parked();
7132
7133        // Open a thread.
7134        panel.update_in(cx, |panel, window, cx| {
7135            panel.open_external_thread_with_server(
7136                Rc::new(StubAgentServer::default_response()),
7137                window,
7138                cx,
7139            );
7140        });
7141        cx.run_until_parked();
7142
7143        // Set start_thread_in to LinkedWorktree to bypass git worktree
7144        // creation and directly test workspace opening for a known path.
7145        let linked_path = PathBuf::from("/project");
7146        panel.update_in(cx, |panel, window, cx| {
7147            panel.set_start_thread_in(
7148                &StartThreadIn::LinkedWorktree {
7149                    path: linked_path.clone(),
7150                    display_name: "project".to_string(),
7151                },
7152                window,
7153                cx,
7154            );
7155        });
7156
7157        // Trigger worktree creation.
7158        let content = vec![acp::ContentBlock::Text(acp::TextContent::new(
7159            "Hello from remote test",
7160        ))];
7161        panel.update_in(cx, |panel, window, cx| {
7162            panel.handle_worktree_requested(
7163                content,
7164                WorktreeCreationArgs::Linked {
7165                    worktree_path: linked_path,
7166                },
7167                window,
7168                cx,
7169            );
7170        });
7171
7172        // The refactored code uses `find_or_create_workspace`, which
7173        // finds the existing remote workspace (matching paths + host)
7174        // and reuses it instead of creating a new connection.
7175        cx.run_until_parked();
7176
7177        // The task should have completed: the existing workspace was
7178        // found and reused.
7179        panel.read_with(cx, |panel, _cx| {
7180            assert!(
7181                panel.worktree_creation_status.is_none(),
7182                "worktree creation should have completed, but status is: {:?}",
7183                panel.worktree_creation_status
7184            );
7185        });
7186
7187        // The existing remote workspace was reused — no new workspace
7188        // should have been created.
7189        multi_workspace
7190            .read_with(cx, |multi_workspace, cx| {
7191                let project = workspace.read(cx).project().clone();
7192                assert!(
7193                    !project.read(cx).is_local(),
7194                    "workspace project should still be remote, not local"
7195                );
7196                assert_eq!(
7197                    multi_workspace.workspaces().count(),
7198                    1,
7199                    "existing remote workspace should be reused, not a new one created"
7200                );
7201            })
7202            .unwrap();
7203    }
7204}