agent_panel.rs

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