agent_panel.rs

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