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        branch_name: &str,
2858        use_existing_branch: bool,
2859        start_point: Option<String>,
2860        worktree_directory_setting: &str,
2861        cx: &mut Context<Self>,
2862    ) -> Result<(
2863        Vec<(
2864            Entity<project::git_store::Repository>,
2865            PathBuf,
2866            futures::channel::oneshot::Receiver<Result<()>>,
2867        )>,
2868        Vec<(PathBuf, PathBuf)>,
2869    )> {
2870        let mut creation_infos = Vec::new();
2871        let mut path_remapping = Vec::new();
2872
2873        let worktree_name = worktree_name.unwrap_or_else(|| branch_name.to_string());
2874
2875        for repo in git_repos {
2876            let (work_dir, new_path, receiver) = repo.update(cx, |repo, _cx| {
2877                let new_path =
2878                    repo.path_for_new_linked_worktree(&worktree_name, worktree_directory_setting)?;
2879                let target = if use_existing_branch {
2880                    debug_assert!(
2881                        git_repos.len() == 1,
2882                        "use_existing_branch should only be true for a single repo"
2883                    );
2884                    git::repository::CreateWorktreeTarget::ExistingBranch {
2885                        branch_name: branch_name.to_string(),
2886                    }
2887                } else {
2888                    git::repository::CreateWorktreeTarget::NewBranch {
2889                        branch_name: branch_name.to_string(),
2890                        base_sha: start_point.clone(),
2891                    }
2892                };
2893                let receiver = repo.create_worktree(target, new_path.clone());
2894                let work_dir = repo.work_directory_abs_path.clone();
2895                anyhow::Ok((work_dir, new_path, receiver))
2896            })?;
2897            path_remapping.push((work_dir.to_path_buf(), new_path.clone()));
2898            creation_infos.push((repo.clone(), new_path, receiver));
2899        }
2900
2901        Ok((creation_infos, path_remapping))
2902    }
2903
2904    /// Waits for every in-flight worktree creation to complete. If any
2905    /// creation fails, all successfully-created worktrees are rolled back
2906    /// (removed) so the project isn't left in a half-migrated state.
2907    async fn await_and_rollback_on_failure(
2908        creation_infos: Vec<(
2909            Entity<project::git_store::Repository>,
2910            PathBuf,
2911            futures::channel::oneshot::Receiver<Result<()>>,
2912        )>,
2913        fs: Arc<dyn Fs>,
2914        cx: &mut AsyncWindowContext,
2915    ) -> Result<Vec<PathBuf>> {
2916        let mut created_paths: Vec<PathBuf> = Vec::new();
2917        let mut repos_and_paths: Vec<(Entity<project::git_store::Repository>, PathBuf)> =
2918            Vec::new();
2919        let mut first_error: Option<anyhow::Error> = None;
2920
2921        for (repo, new_path, receiver) in creation_infos {
2922            repos_and_paths.push((repo.clone(), new_path.clone()));
2923            match receiver.await {
2924                Ok(Ok(())) => {
2925                    created_paths.push(new_path);
2926                }
2927                Ok(Err(err)) => {
2928                    if first_error.is_none() {
2929                        first_error = Some(err);
2930                    }
2931                }
2932                Err(_canceled) => {
2933                    if first_error.is_none() {
2934                        first_error = Some(anyhow!("Worktree creation was canceled"));
2935                    }
2936                }
2937            }
2938        }
2939
2940        let Some(err) = first_error else {
2941            return Ok(created_paths);
2942        };
2943
2944        // Rollback all attempted worktrees (both successful and failed)
2945        let mut rollback_futures = Vec::new();
2946        for (rollback_repo, rollback_path) in &repos_and_paths {
2947            let receiver = cx
2948                .update(|_, cx| {
2949                    rollback_repo.update(cx, |repo, _cx| {
2950                        repo.remove_worktree(rollback_path.clone(), true)
2951                    })
2952                })
2953                .ok();
2954
2955            rollback_futures.push((rollback_path.clone(), receiver));
2956        }
2957
2958        let mut rollback_failures: Vec<String> = Vec::new();
2959        for (path, receiver_opt) in rollback_futures {
2960            let mut git_remove_failed = false;
2961
2962            if let Some(receiver) = receiver_opt {
2963                match receiver.await {
2964                    Ok(Ok(())) => {}
2965                    Ok(Err(rollback_err)) => {
2966                        log::error!(
2967                            "git worktree remove failed for {}: {rollback_err}",
2968                            path.display()
2969                        );
2970                        git_remove_failed = true;
2971                    }
2972                    Err(canceled) => {
2973                        log::error!(
2974                            "git worktree remove failed for {}: {canceled}",
2975                            path.display()
2976                        );
2977                        git_remove_failed = true;
2978                    }
2979                }
2980            } else {
2981                log::error!(
2982                    "failed to dispatch git worktree remove for {}",
2983                    path.display()
2984                );
2985                git_remove_failed = true;
2986            }
2987
2988            // `git worktree remove` normally removes this directory, but since
2989            // `git worktree remove` failed (or wasn't dispatched), manually rm the directory.
2990            if git_remove_failed {
2991                if let Err(fs_err) = fs
2992                    .remove_dir(
2993                        &path,
2994                        fs::RemoveOptions {
2995                            recursive: true,
2996                            ignore_if_not_exists: true,
2997                        },
2998                    )
2999                    .await
3000                {
3001                    let msg = format!("{}: failed to remove directory: {fs_err}", path.display());
3002                    log::error!("{}", msg);
3003                    rollback_failures.push(msg);
3004                }
3005            }
3006        }
3007        let mut error_message = format!("Failed to create worktree: {err}");
3008        if !rollback_failures.is_empty() {
3009            error_message.push_str("\n\nFailed to clean up: ");
3010            error_message.push_str(&rollback_failures.join(", "));
3011        }
3012        Err(anyhow!(error_message))
3013    }
3014
3015    fn set_worktree_creation_error(
3016        &mut self,
3017        message: SharedString,
3018        window: &mut Window,
3019        cx: &mut Context<Self>,
3020    ) {
3021        if let Some((_, status)) = &mut self.worktree_creation_status {
3022            *status = WorktreeCreationStatus::Error(message);
3023        }
3024        if matches!(self.active_view, ActiveView::Uninitialized) {
3025            let selected_agent = self.selected_agent.clone();
3026            self.new_agent_thread(selected_agent, window, cx);
3027        }
3028        cx.notify();
3029    }
3030
3031    fn handle_worktree_requested(
3032        &mut self,
3033        content: Vec<acp::ContentBlock>,
3034        args: WorktreeCreationArgs,
3035        window: &mut Window,
3036        cx: &mut Context<Self>,
3037    ) {
3038        if matches!(
3039            self.worktree_creation_status,
3040            Some((_, WorktreeCreationStatus::Creating))
3041        ) {
3042            return;
3043        }
3044
3045        let conversation_view_id = self
3046            .active_conversation_view()
3047            .map(|v| v.entity_id())
3048            .unwrap_or_else(|| EntityId::from(0u64));
3049        self.worktree_creation_status =
3050            Some((conversation_view_id, WorktreeCreationStatus::Creating));
3051        cx.notify();
3052
3053        let (git_repos, non_git_paths) = self.classify_worktrees(cx);
3054
3055        if matches!(args, WorktreeCreationArgs::New { .. }) && git_repos.is_empty() {
3056            self.set_worktree_creation_error(
3057                "No git repositories found in the project".into(),
3058                window,
3059                cx,
3060            );
3061            return;
3062        }
3063
3064        let (branch_receivers, worktree_receivers, worktree_directory_setting) =
3065            if matches!(args, WorktreeCreationArgs::New { .. }) {
3066                (
3067                    Some(
3068                        git_repos
3069                            .iter()
3070                            .map(|repo| repo.update(cx, |repo, _cx| repo.branches()))
3071                            .collect::<Vec<_>>(),
3072                    ),
3073                    Some(
3074                        git_repos
3075                            .iter()
3076                            .map(|repo| repo.update(cx, |repo, _cx| repo.worktrees()))
3077                            .collect::<Vec<_>>(),
3078                    ),
3079                    Some(
3080                        ProjectSettings::get_global(cx)
3081                            .git
3082                            .worktree_directory
3083                            .clone(),
3084                    ),
3085                )
3086            } else {
3087                (None, None, None)
3088            };
3089
3090        let active_file_path = self.workspace.upgrade().and_then(|workspace| {
3091            let workspace = workspace.read(cx);
3092            let active_item = workspace.active_item(cx)?;
3093            let project_path = active_item.project_path(cx)?;
3094            workspace
3095                .project()
3096                .read(cx)
3097                .absolute_path(&project_path, cx)
3098        });
3099
3100        let remote_connection_options = self.project.read(cx).remote_connection_options(cx);
3101
3102        if remote_connection_options.is_some() {
3103            let is_disconnected = self
3104                .project
3105                .read(cx)
3106                .remote_client()
3107                .is_some_and(|client| client.read(cx).is_disconnected());
3108            if is_disconnected {
3109                self.set_worktree_creation_error(
3110                    "Cannot create worktree: remote connection is not active".into(),
3111                    window,
3112                    cx,
3113                );
3114                return;
3115            }
3116        }
3117
3118        let workspace = self.workspace.clone();
3119        let window_handle = window
3120            .window_handle()
3121            .downcast::<workspace::MultiWorkspace>();
3122
3123        let selected_agent = self.selected_agent();
3124
3125        let task = cx.spawn_in(window, async move |this, cx| {
3126            let (all_paths, path_remapping, has_non_git) = match args {
3127                WorktreeCreationArgs::New {
3128                    worktree_name,
3129                    branch_target,
3130                } => {
3131                    let branch_receivers = branch_receivers
3132                        .expect("branch receivers must be prepared for new worktree creation");
3133                    let worktree_receivers = worktree_receivers
3134                        .expect("worktree receivers must be prepared for new worktree creation");
3135                    let worktree_directory_setting = worktree_directory_setting
3136                        .expect("worktree directory must be prepared for new worktree creation");
3137
3138                    let mut existing_branches = HashSet::default();
3139                    for result in futures::future::join_all(branch_receivers).await {
3140                        match result {
3141                            Ok(Ok(branches)) => {
3142                                for branch in branches {
3143                                    existing_branches.insert(branch.name().to_string());
3144                                }
3145                            }
3146                            Ok(Err(err)) => {
3147                                Err::<(), _>(err).log_err();
3148                            }
3149                            Err(_) => {}
3150                        }
3151                    }
3152
3153                    let mut occupied_branches = HashSet::default();
3154                    for result in futures::future::join_all(worktree_receivers).await {
3155                        match result {
3156                            Ok(Ok(worktrees)) => {
3157                                for worktree in worktrees {
3158                                    if let Some(branch_name) = worktree.branch_name() {
3159                                        occupied_branches.insert(branch_name.to_string());
3160                                    }
3161                                }
3162                            }
3163                            Ok(Err(err)) => {
3164                                Err::<(), _>(err).log_err();
3165                            }
3166                            Err(_) => {}
3167                        }
3168                    }
3169
3170                    let (branch_name, use_existing_branch, start_point) =
3171                        match Self::resolve_worktree_branch_target(
3172                            &branch_target,
3173                            &existing_branches,
3174                            &occupied_branches,
3175                        ) {
3176                            Ok(target) => target,
3177                            Err(err) => {
3178                                this.update_in(cx, |this, window, cx| {
3179                                    this.set_worktree_creation_error(
3180                                        err.to_string().into(),
3181                                        window,
3182                                        cx,
3183                                    );
3184                                })?;
3185                                return anyhow::Ok(());
3186                            }
3187                        };
3188
3189                    let (creation_infos, path_remapping) =
3190                        match this.update_in(cx, |_this, _window, cx| {
3191                            Self::start_worktree_creations(
3192                                &git_repos,
3193                                worktree_name,
3194                                &branch_name,
3195                                use_existing_branch,
3196                                start_point,
3197                                &worktree_directory_setting,
3198                                cx,
3199                            )
3200                        }) {
3201                            Ok(Ok(result)) => result,
3202                            Ok(Err(err)) | Err(err) => {
3203                                this.update_in(cx, |this, window, cx| {
3204                                    this.set_worktree_creation_error(
3205                                        format!("Failed to validate worktree directory: {err}")
3206                                            .into(),
3207                                        window,
3208                                        cx,
3209                                    );
3210                                })
3211                                .log_err();
3212                                return anyhow::Ok(());
3213                            }
3214                        };
3215
3216                    let fs = cx.update(|_, cx| <dyn Fs>::global(cx))?;
3217
3218                    let created_paths =
3219                        match Self::await_and_rollback_on_failure(creation_infos, fs, cx).await {
3220                            Ok(paths) => paths,
3221                            Err(err) => {
3222                                this.update_in(cx, |this, window, cx| {
3223                                    this.set_worktree_creation_error(
3224                                        format!("{err}").into(),
3225                                        window,
3226                                        cx,
3227                                    );
3228                                })?;
3229                                return anyhow::Ok(());
3230                            }
3231                        };
3232
3233                    let mut all_paths = created_paths;
3234                    let has_non_git = !non_git_paths.is_empty();
3235                    all_paths.extend(non_git_paths.iter().cloned());
3236                    (all_paths, path_remapping, has_non_git)
3237                }
3238                WorktreeCreationArgs::Linked { worktree_path } => {
3239                    let mut all_paths = vec![worktree_path];
3240                    let has_non_git = !non_git_paths.is_empty();
3241                    all_paths.extend(non_git_paths.iter().cloned());
3242                    (all_paths, Vec::new(), has_non_git)
3243                }
3244            };
3245
3246            if workspace.upgrade().is_none() {
3247                this.update_in(cx, |this, window, cx| {
3248                    this.set_worktree_creation_error(
3249                        "Workspace no longer available".into(),
3250                        window,
3251                        cx,
3252                    );
3253                })?;
3254                return anyhow::Ok(());
3255            }
3256
3257            let this_for_error = this.clone();
3258            if let Err(err) = Self::open_worktree_workspace_and_start_thread(
3259                this,
3260                all_paths,
3261                window_handle,
3262                active_file_path,
3263                path_remapping,
3264                non_git_paths,
3265                has_non_git,
3266                content,
3267                selected_agent,
3268                remote_connection_options,
3269                cx,
3270            )
3271            .await
3272            {
3273                this_for_error
3274                    .update_in(cx, |this, window, cx| {
3275                        this.set_worktree_creation_error(
3276                            format!("Failed to set up workspace: {err}").into(),
3277                            window,
3278                            cx,
3279                        );
3280                    })
3281                    .log_err();
3282            }
3283            anyhow::Ok(())
3284        });
3285
3286        self._worktree_creation_task = Some(cx.background_spawn(async move {
3287            task.await.log_err();
3288        }));
3289    }
3290
3291    async fn open_worktree_workspace_and_start_thread(
3292        this: WeakEntity<Self>,
3293        all_paths: Vec<PathBuf>,
3294        window_handle: Option<gpui::WindowHandle<workspace::MultiWorkspace>>,
3295        active_file_path: Option<PathBuf>,
3296        path_remapping: Vec<(PathBuf, PathBuf)>,
3297        non_git_paths: Vec<PathBuf>,
3298        has_non_git: bool,
3299        content: Vec<acp::ContentBlock>,
3300        selected_agent: Option<Agent>,
3301        remote_connection_options: Option<RemoteConnectionOptions>,
3302        cx: &mut AsyncWindowContext,
3303    ) -> Result<()> {
3304        let window_handle = window_handle
3305            .ok_or_else(|| anyhow!("No window handle available for workspace creation"))?;
3306
3307        let (workspace_task, modal_workspace) =
3308            window_handle.update(cx, |multi_workspace, window, cx| {
3309                let path_list = PathList::new(&all_paths);
3310                let active_workspace = multi_workspace.workspace().clone();
3311                let modal_workspace = active_workspace.clone();
3312
3313                let task = multi_workspace.find_or_create_workspace(
3314                    path_list,
3315                    remote_connection_options,
3316                    None,
3317                    move |connection_options, window, cx| {
3318                        remote_connection::connect_with_modal(
3319                            &active_workspace,
3320                            connection_options,
3321                            window,
3322                            cx,
3323                        )
3324                    },
3325                    window,
3326                    cx,
3327                );
3328                (task, modal_workspace)
3329            })?;
3330
3331        let result = workspace_task.await;
3332        remote_connection::dismiss_connection_modal(&modal_workspace, cx);
3333        let new_workspace = result?;
3334
3335        let panels_task = new_workspace.update(cx, |workspace, _cx| workspace.take_panels_task());
3336
3337        if let Some(task) = panels_task {
3338            task.await.log_err();
3339        }
3340
3341        new_workspace
3342            .update(cx, |workspace, cx| {
3343                workspace.project().read(cx).wait_for_initial_scan(cx)
3344            })
3345            .await;
3346
3347        new_workspace
3348            .update(cx, |workspace, cx| {
3349                let repos = workspace
3350                    .project()
3351                    .read(cx)
3352                    .repositories(cx)
3353                    .values()
3354                    .cloned()
3355                    .collect::<Vec<_>>();
3356
3357                let tasks = repos
3358                    .into_iter()
3359                    .map(|repo| repo.update(cx, |repo, _| repo.barrier()));
3360                futures::future::join_all(tasks)
3361            })
3362            .await;
3363
3364        let initial_content = AgentInitialContent::ContentBlock {
3365            blocks: content,
3366            auto_submit: true,
3367        };
3368
3369        window_handle.update(cx, |_multi_workspace, window, cx| {
3370            new_workspace.update(cx, |workspace, cx| {
3371                if has_non_git {
3372                    let toast_id = workspace::notifications::NotificationId::unique::<AgentPanel>();
3373                    workspace.show_toast(
3374                        workspace::Toast::new(
3375                            toast_id,
3376                            "Some project folders are not git repositories. \
3377                             They were included as-is without creating a worktree.",
3378                        ),
3379                        cx,
3380                    );
3381                }
3382
3383                // If we had an active buffer, remap its path and reopen it.
3384                let had_active_file = active_file_path.is_some();
3385                let remapped_active_path = active_file_path.and_then(|original_path| {
3386                    let best_match = path_remapping
3387                        .iter()
3388                        .filter_map(|(old_root, new_root)| {
3389                            original_path.strip_prefix(old_root).ok().map(|relative| {
3390                                (old_root.components().count(), new_root.join(relative))
3391                            })
3392                        })
3393                        .max_by_key(|(depth, _)| *depth);
3394
3395                    if let Some((_, remapped_path)) = best_match {
3396                        return Some(remapped_path);
3397                    }
3398
3399                    for non_git in &non_git_paths {
3400                        if original_path.starts_with(non_git) {
3401                            return Some(original_path);
3402                        }
3403                    }
3404                    None
3405                });
3406
3407                if had_active_file && remapped_active_path.is_none() {
3408                    log::warn!(
3409                        "Active file could not be remapped to the new worktree; it will not be reopened"
3410                    );
3411                }
3412
3413                if let Some(path) = remapped_active_path {
3414                    let open_task = workspace.open_paths(
3415                        vec![path],
3416                        workspace::OpenOptions::default(),
3417                        None,
3418                        window,
3419                        cx,
3420                    );
3421                    cx.spawn(async move |_, _| -> anyhow::Result<()> {
3422                        for item in open_task.await.into_iter().flatten() {
3423                            item?;
3424                        }
3425                        Ok(())
3426                    })
3427                    .detach_and_log_err(cx);
3428                }
3429
3430                workspace.focus_panel::<AgentPanel>(window, cx);
3431
3432                // If no active buffer was open, zoom the agent panel
3433                // (equivalent to cmd-esc fullscreen behavior).
3434                // This must happen after focus_panel, which activates
3435                // and opens the panel in the dock.
3436
3437                if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
3438                    panel.update(cx, |panel, cx| {
3439                        panel.external_thread(
3440                            selected_agent,
3441                            None,
3442                            None,
3443                            None,
3444                            Some(initial_content),
3445                            true,
3446                            window,
3447                            cx,
3448                        );
3449                    });
3450                }
3451            });
3452        })?;
3453
3454        window_handle.update(cx, |multi_workspace, window, cx| {
3455            multi_workspace.activate(new_workspace.clone(), window, cx);
3456
3457            new_workspace.update(cx, |workspace, cx| {
3458                workspace.run_create_worktree_tasks(window, cx);
3459            })
3460        })?;
3461
3462        this.update_in(cx, |this, window, cx| {
3463            this.worktree_creation_status = None;
3464
3465            if let Some(thread_view) = this.active_thread_view(cx) {
3466                thread_view.update(cx, |thread_view, cx| {
3467                    thread_view
3468                        .message_editor
3469                        .update(cx, |editor, cx| editor.clear(window, cx));
3470                });
3471            }
3472
3473            this.start_thread_in = StartThreadIn::LocalProject;
3474            this.serialize(cx);
3475            cx.notify();
3476        })?;
3477
3478        anyhow::Ok(())
3479    }
3480}
3481
3482impl Focusable for AgentPanel {
3483    fn focus_handle(&self, cx: &App) -> FocusHandle {
3484        match &self.active_view {
3485            ActiveView::Uninitialized => self.focus_handle.clone(),
3486            ActiveView::AgentThread {
3487                conversation_view, ..
3488            } => conversation_view.focus_handle(cx),
3489            ActiveView::History { view } => view.read(cx).focus_handle(cx),
3490            ActiveView::Configuration => {
3491                if let Some(configuration) = self.configuration.as_ref() {
3492                    configuration.focus_handle(cx)
3493                } else {
3494                    self.focus_handle.clone()
3495                }
3496            }
3497        }
3498    }
3499}
3500
3501fn agent_panel_dock_position(cx: &App) -> DockPosition {
3502    AgentSettings::get_global(cx).dock.into()
3503}
3504
3505pub enum AgentPanelEvent {
3506    ActiveViewChanged,
3507    ThreadFocused,
3508    BackgroundThreadChanged,
3509    MessageSentOrQueued { session_id: acp::SessionId },
3510}
3511
3512impl EventEmitter<PanelEvent> for AgentPanel {}
3513impl EventEmitter<AgentPanelEvent> for AgentPanel {}
3514
3515impl Panel for AgentPanel {
3516    fn persistent_name() -> &'static str {
3517        "AgentPanel"
3518    }
3519
3520    fn panel_key() -> &'static str {
3521        AGENT_PANEL_KEY
3522    }
3523
3524    fn position(&self, _window: &Window, cx: &App) -> DockPosition {
3525        agent_panel_dock_position(cx)
3526    }
3527
3528    fn position_is_valid(&self, position: DockPosition) -> bool {
3529        position != DockPosition::Bottom
3530    }
3531
3532    fn set_position(&mut self, position: DockPosition, _: &mut Window, cx: &mut Context<Self>) {
3533        settings::update_settings_file(self.fs.clone(), cx, move |settings, _| {
3534            settings
3535                .agent
3536                .get_or_insert_default()
3537                .set_dock(position.into());
3538        });
3539    }
3540
3541    fn default_size(&self, window: &Window, cx: &App) -> Pixels {
3542        let settings = AgentSettings::get_global(cx);
3543        match self.position(window, cx) {
3544            DockPosition::Left | DockPosition::Right => settings.default_width,
3545            DockPosition::Bottom => settings.default_height,
3546        }
3547    }
3548
3549    fn supports_flexible_size(&self) -> bool {
3550        true
3551    }
3552
3553    fn has_flexible_size(&self, _window: &Window, cx: &App) -> bool {
3554        AgentSettings::get_global(cx).flexible
3555    }
3556
3557    fn set_flexible_size(&mut self, flexible: bool, _window: &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_flexible_size(flexible);
3563        });
3564    }
3565
3566    fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
3567        if active
3568            && matches!(self.active_view, ActiveView::Uninitialized)
3569            && !matches!(
3570                self.worktree_creation_status,
3571                Some((_, WorktreeCreationStatus::Creating))
3572            )
3573        {
3574            let id = self.create_draft(window, cx);
3575            self.activate_draft(id, false, window, cx);
3576        }
3577    }
3578
3579    fn remote_id() -> Option<proto::PanelId> {
3580        Some(proto::PanelId::AssistantPanel)
3581    }
3582
3583    fn icon(&self, _window: &Window, cx: &App) -> Option<IconName> {
3584        (self.enabled(cx) && AgentSettings::get_global(cx).button).then_some(IconName::ZedAssistant)
3585    }
3586
3587    fn icon_tooltip(&self, _window: &Window, _cx: &App) -> Option<&'static str> {
3588        Some("Agent Panel")
3589    }
3590
3591    fn toggle_action(&self) -> Box<dyn Action> {
3592        Box::new(ToggleFocus)
3593    }
3594
3595    fn activation_priority(&self) -> u32 {
3596        0
3597    }
3598
3599    fn enabled(&self, cx: &App) -> bool {
3600        AgentSettings::get_global(cx).enabled(cx)
3601    }
3602
3603    fn is_agent_panel(&self) -> bool {
3604        true
3605    }
3606
3607    fn is_zoomed(&self, _window: &Window, _cx: &App) -> bool {
3608        self.zoomed
3609    }
3610
3611    fn set_zoomed(&mut self, zoomed: bool, _window: &mut Window, cx: &mut Context<Self>) {
3612        self.zoomed = zoomed;
3613        cx.notify();
3614    }
3615}
3616
3617impl AgentPanel {
3618    fn render_title_view(&self, _window: &mut Window, cx: &Context<Self>) -> AnyElement {
3619        let content = match &self.active_view {
3620            ActiveView::AgentThread { conversation_view } => {
3621                let server_view_ref = conversation_view.read(cx);
3622                let is_generating_title = server_view_ref.as_native_thread(cx).is_some()
3623                    && server_view_ref.root_thread(cx).map_or(false, |tv| {
3624                        tv.read(cx).thread.read(cx).has_provisional_title()
3625                    });
3626
3627                if let Some(title_editor) = server_view_ref
3628                    .root_thread(cx)
3629                    .map(|r| r.read(cx).title_editor.clone())
3630                {
3631                    if is_generating_title {
3632                        Label::new(DEFAULT_THREAD_TITLE)
3633                            .color(Color::Muted)
3634                            .truncate()
3635                            .with_animation(
3636                                "generating_title",
3637                                Animation::new(Duration::from_secs(2))
3638                                    .repeat()
3639                                    .with_easing(pulsating_between(0.4, 0.8)),
3640                                |label, delta| label.alpha(delta),
3641                            )
3642                            .into_any_element()
3643                    } else {
3644                        div()
3645                            .w_full()
3646                            .on_action({
3647                                let conversation_view = conversation_view.downgrade();
3648                                move |_: &menu::Confirm, window, cx| {
3649                                    if let Some(conversation_view) = conversation_view.upgrade() {
3650                                        conversation_view.focus_handle(cx).focus(window, cx);
3651                                    }
3652                                }
3653                            })
3654                            .on_action({
3655                                let conversation_view = conversation_view.downgrade();
3656                                move |_: &editor::actions::Cancel, window, cx| {
3657                                    if let Some(conversation_view) = conversation_view.upgrade() {
3658                                        conversation_view.focus_handle(cx).focus(window, cx);
3659                                    }
3660                                }
3661                            })
3662                            .child(title_editor)
3663                            .into_any_element()
3664                    }
3665                } else {
3666                    Label::new(conversation_view.read(cx).title(cx))
3667                        .color(Color::Muted)
3668                        .truncate()
3669                        .into_any_element()
3670                }
3671            }
3672            ActiveView::History { .. } => Label::new("History").truncate().into_any_element(),
3673            ActiveView::Configuration => Label::new("Settings").truncate().into_any_element(),
3674            ActiveView::Uninitialized => Label::new("Agent").truncate().into_any_element(),
3675        };
3676
3677        h_flex()
3678            .key_context("TitleEditor")
3679            .id("TitleEditor")
3680            .flex_grow()
3681            .w_full()
3682            .max_w_full()
3683            .overflow_x_scroll()
3684            .child(content)
3685            .into_any()
3686    }
3687
3688    fn handle_regenerate_thread_title(conversation_view: Entity<ConversationView>, cx: &mut App) {
3689        conversation_view.update(cx, |conversation_view, cx| {
3690            if let Some(thread) = conversation_view.as_native_thread(cx) {
3691                thread.update(cx, |thread, cx| {
3692                    thread.generate_title(cx);
3693                });
3694            }
3695        });
3696    }
3697
3698    fn render_panel_options_menu(
3699        &self,
3700        _window: &mut Window,
3701        cx: &mut Context<Self>,
3702    ) -> impl IntoElement {
3703        let focus_handle = self.focus_handle(cx);
3704
3705        let conversation_view = match &self.active_view {
3706            ActiveView::AgentThread { conversation_view } => Some(conversation_view.clone()),
3707            _ => None,
3708        };
3709        let thread_with_messages = match &self.active_view {
3710            ActiveView::AgentThread { conversation_view } => {
3711                conversation_view.read(cx).has_user_submitted_prompt(cx)
3712            }
3713            _ => false,
3714        };
3715        let has_auth_methods = match &self.active_view {
3716            ActiveView::AgentThread { conversation_view } => {
3717                conversation_view.read(cx).has_auth_methods()
3718            }
3719            _ => false,
3720        };
3721
3722        PopoverMenu::new("agent-options-menu")
3723            .trigger_with_tooltip(
3724                IconButton::new("agent-options-menu", IconName::Ellipsis)
3725                    .icon_size(IconSize::Small),
3726                {
3727                    let focus_handle = focus_handle.clone();
3728                    move |_window, cx| {
3729                        Tooltip::for_action_in(
3730                            "Toggle Agent Menu",
3731                            &ToggleOptionsMenu,
3732                            &focus_handle,
3733                            cx,
3734                        )
3735                    }
3736                },
3737            )
3738            .anchor(Corner::TopRight)
3739            .with_handle(self.agent_panel_menu_handle.clone())
3740            .menu({
3741                move |window, cx| {
3742                    Some(ContextMenu::build(window, cx, |mut menu, _window, _| {
3743                        menu = menu.context(focus_handle.clone());
3744
3745                        if thread_with_messages {
3746                            menu = menu.header("Current Thread");
3747
3748                            if let Some(conversation_view) = conversation_view.as_ref() {
3749                                menu = menu
3750                                    .entry("Regenerate Thread Title", None, {
3751                                        let conversation_view = conversation_view.clone();
3752                                        move |_, cx| {
3753                                            Self::handle_regenerate_thread_title(
3754                                                conversation_view.clone(),
3755                                                cx,
3756                                            );
3757                                        }
3758                                    })
3759                                    .separator();
3760                            }
3761                        }
3762
3763                        menu = menu
3764                            .header("MCP Servers")
3765                            .action(
3766                                "View Server Extensions",
3767                                Box::new(zed_actions::Extensions {
3768                                    category_filter: Some(
3769                                        zed_actions::ExtensionCategoryFilter::ContextServers,
3770                                    ),
3771                                    id: None,
3772                                }),
3773                            )
3774                            .action("Add Custom Server…", Box::new(AddContextServer))
3775                            .separator()
3776                            .action("Rules", Box::new(OpenRulesLibrary::default()))
3777                            .action("Profiles", Box::new(ManageProfiles::default()))
3778                            .action("Settings", Box::new(OpenSettings))
3779                            .separator()
3780                            .action("Toggle Threads Sidebar", Box::new(ToggleWorkspaceSidebar));
3781
3782                        if has_auth_methods {
3783                            menu = menu.action("Reauthenticate", Box::new(ReauthenticateAgent))
3784                        }
3785
3786                        menu
3787                    }))
3788                }
3789            })
3790    }
3791
3792    fn render_toolbar_back_button(&self, cx: &mut Context<Self>) -> impl IntoElement {
3793        let focus_handle = self.focus_handle(cx);
3794
3795        IconButton::new("go-back", IconName::ArrowLeft)
3796            .icon_size(IconSize::Small)
3797            .on_click(cx.listener(|this, _, window, cx| {
3798                this.go_back(&workspace::GoBack, window, cx);
3799            }))
3800            .tooltip({
3801                move |_window, cx| {
3802                    Tooltip::for_action_in("Go Back", &workspace::GoBack, &focus_handle, cx)
3803                }
3804            })
3805    }
3806
3807    fn project_has_git_repository(&self, cx: &App) -> bool {
3808        !self.project.read(cx).repositories(cx).is_empty()
3809    }
3810
3811    fn is_active_view_creating_worktree(&self, _cx: &App) -> bool {
3812        match &self.worktree_creation_status {
3813            Some((view_id, WorktreeCreationStatus::Creating)) => {
3814                self.active_conversation_view().map(|v| v.entity_id()) == Some(*view_id)
3815            }
3816            _ => false,
3817        }
3818    }
3819
3820    fn render_start_thread_in_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
3821        let focus_handle = self.focus_handle(cx);
3822
3823        let is_creating = self.is_active_view_creating_worktree(cx);
3824
3825        let trigger_parts = self
3826            .start_thread_in
3827            .trigger_label(self.project.read(cx), cx);
3828
3829        let icon = if self.start_thread_in_menu_handle.is_deployed() {
3830            IconName::ChevronUp
3831        } else {
3832            IconName::ChevronDown
3833        };
3834
3835        let trigger_button = ButtonLike::new("thread-target-trigger")
3836            .disabled(is_creating)
3837            .when_some(trigger_parts.prefix, |this, prefix| {
3838                this.child(Label::new(prefix).color(Color::Muted))
3839            })
3840            .child(Label::new(trigger_parts.label))
3841            .when_some(trigger_parts.suffix, |this, suffix| {
3842                this.child(Label::new(suffix).color(Color::Muted))
3843            })
3844            .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted));
3845
3846        let project = self.project.clone();
3847        let current_target = self.start_thread_in.clone();
3848        let fs = self.fs.clone();
3849
3850        PopoverMenu::new("thread-target-selector")
3851            .trigger_with_tooltip(trigger_button, {
3852                move |_window, cx| {
3853                    Tooltip::for_action_in(
3854                        "Start Thread In…",
3855                        &CycleStartThreadIn,
3856                        &focus_handle,
3857                        cx,
3858                    )
3859                }
3860            })
3861            .menu(move |window, cx| {
3862                let fs = fs.clone();
3863                Some(cx.new(|cx| {
3864                    ThreadWorktreePicker::new(project.clone(), &current_target, fs, window, cx)
3865                }))
3866            })
3867            .with_handle(self.start_thread_in_menu_handle.clone())
3868            .anchor(Corner::TopLeft)
3869            .offset(gpui::Point {
3870                x: px(1.0),
3871                y: px(1.0),
3872            })
3873    }
3874
3875    fn render_new_worktree_branch_selector(&self, cx: &mut Context<Self>) -> impl IntoElement {
3876        let is_creating = self.is_active_view_creating_worktree(cx);
3877
3878        let project_ref = self.project.read(cx);
3879        let trigger_parts = self
3880            .start_thread_in
3881            .branch_trigger_label(project_ref, cx)
3882            .unwrap_or_else(|| StartThreadInLabel {
3883                prefix: Some("From:".into()),
3884                label: "HEAD".into(),
3885                suffix: None,
3886            });
3887
3888        let icon = if self.thread_branch_menu_handle.is_deployed() {
3889            IconName::ChevronUp
3890        } else {
3891            IconName::ChevronDown
3892        };
3893
3894        let trigger_button = ButtonLike::new("thread-branch-trigger")
3895            .disabled(is_creating)
3896            .when_some(trigger_parts.prefix, |this, prefix| {
3897                this.child(Label::new(prefix).color(Color::Muted))
3898            })
3899            .child(Label::new(trigger_parts.label))
3900            .child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted));
3901
3902        let project = self.project.clone();
3903        let current_target = self.start_thread_in.clone();
3904
3905        PopoverMenu::new("thread-branch-selector")
3906            .trigger_with_tooltip(trigger_button, Tooltip::text("Choose Worktree Branch…"))
3907            .menu(move |window, cx| {
3908                Some(cx.new(|cx| {
3909                    ThreadBranchPicker::new(project.clone(), &current_target, window, cx)
3910                }))
3911            })
3912            .with_handle(self.thread_branch_menu_handle.clone())
3913            .anchor(Corner::TopLeft)
3914            .offset(gpui::Point {
3915                x: px(1.0),
3916                y: px(1.0),
3917            })
3918    }
3919
3920    fn render_toolbar(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
3921        let agent_server_store = self.project.read(cx).agent_server_store().clone();
3922        let has_visible_worktrees = self.project.read(cx).visible_worktrees(cx).next().is_some();
3923        let focus_handle = self.focus_handle(cx);
3924
3925        let (selected_agent_custom_icon, selected_agent_label) =
3926            if let Agent::Custom { id, .. } = &self.selected_agent {
3927                let store = agent_server_store.read(cx);
3928                let icon = store.agent_icon(&id);
3929
3930                let label = store
3931                    .agent_display_name(&id)
3932                    .unwrap_or_else(|| self.selected_agent.label());
3933                (icon, label)
3934            } else {
3935                (None, self.selected_agent.label())
3936            };
3937
3938        let active_thread = match &self.active_view {
3939            ActiveView::AgentThread { conversation_view } => {
3940                conversation_view.read(cx).as_native_thread(cx)
3941            }
3942            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
3943                None
3944            }
3945        };
3946
3947        let new_thread_menu_builder: Rc<
3948            dyn Fn(&mut Window, &mut App) -> Option<Entity<ContextMenu>>,
3949        > = {
3950            let selected_agent = self.selected_agent.clone();
3951            let is_agent_selected = move |agent: Agent| selected_agent == agent;
3952
3953            let workspace = self.workspace.clone();
3954            let is_via_collab = workspace
3955                .update(cx, |workspace, cx| {
3956                    workspace.project().read(cx).is_via_collab()
3957                })
3958                .unwrap_or_default();
3959
3960            let focus_handle = focus_handle.clone();
3961            let agent_server_store = agent_server_store;
3962
3963            Rc::new(move |window, cx| {
3964                telemetry::event!("New Thread Clicked");
3965
3966                let active_thread = active_thread.clone();
3967                Some(ContextMenu::build(window, cx, |menu, _window, cx| {
3968                    menu.context(focus_handle.clone())
3969                        .when_some(active_thread, |this, active_thread| {
3970                            let thread = active_thread.read(cx);
3971
3972                            if !thread.is_empty() {
3973                                let session_id = thread.id().clone();
3974                                this.item(
3975                                    ContextMenuEntry::new("New From Summary")
3976                                        .icon(IconName::ThreadFromSummary)
3977                                        .icon_color(Color::Muted)
3978                                        .handler(move |window, cx| {
3979                                            window.dispatch_action(
3980                                                Box::new(NewNativeAgentThreadFromSummary {
3981                                                    from_session_id: session_id.clone(),
3982                                                }),
3983                                                cx,
3984                                            );
3985                                        }),
3986                                )
3987                            } else {
3988                                this
3989                            }
3990                        })
3991                        .item(
3992                            ContextMenuEntry::new("Zed Agent")
3993                                .when(is_agent_selected(Agent::NativeAgent), |this| {
3994                                    this.action(Box::new(NewExternalAgentThread { agent: None }))
3995                                })
3996                                .icon(IconName::ZedAgent)
3997                                .icon_color(Color::Muted)
3998                                .handler({
3999                                    let workspace = workspace.clone();
4000                                    move |window, cx| {
4001                                        if let Some(workspace) = workspace.upgrade() {
4002                                            workspace.update(cx, |workspace, cx| {
4003                                                if let Some(panel) =
4004                                                    workspace.panel::<AgentPanel>(cx)
4005                                                {
4006                                                    panel.update(cx, |panel, cx| {
4007                                                        panel.new_agent_thread(
4008                                                            Agent::NativeAgent,
4009                                                            window,
4010                                                            cx,
4011                                                        );
4012                                                    });
4013                                                }
4014                                            });
4015                                        }
4016                                    }
4017                                }),
4018                        )
4019                        .map(|mut menu| {
4020                            let agent_server_store = agent_server_store.read(cx);
4021                            let registry_store = project::AgentRegistryStore::try_global(cx);
4022                            let registry_store_ref = registry_store.as_ref().map(|s| s.read(cx));
4023
4024                            struct AgentMenuItem {
4025                                id: AgentId,
4026                                display_name: SharedString,
4027                            }
4028
4029                            let agent_items = agent_server_store
4030                                .external_agents()
4031                                .map(|agent_id| {
4032                                    let display_name = agent_server_store
4033                                        .agent_display_name(agent_id)
4034                                        .or_else(|| {
4035                                            registry_store_ref
4036                                                .as_ref()
4037                                                .and_then(|store| store.agent(agent_id))
4038                                                .map(|a| a.name().clone())
4039                                        })
4040                                        .unwrap_or_else(|| agent_id.0.clone());
4041                                    AgentMenuItem {
4042                                        id: agent_id.clone(),
4043                                        display_name,
4044                                    }
4045                                })
4046                                .sorted_unstable_by_key(|e| e.display_name.to_lowercase())
4047                                .collect::<Vec<_>>();
4048
4049                            if !agent_items.is_empty() {
4050                                menu = menu.separator().header("External Agents");
4051                            }
4052                            for item in &agent_items {
4053                                let mut entry = ContextMenuEntry::new(item.display_name.clone());
4054
4055                                let icon_path =
4056                                    agent_server_store.agent_icon(&item.id).or_else(|| {
4057                                        registry_store_ref
4058                                            .as_ref()
4059                                            .and_then(|store| store.agent(&item.id))
4060                                            .and_then(|a| a.icon_path().cloned())
4061                                    });
4062
4063                                if let Some(icon_path) = icon_path {
4064                                    entry = entry.custom_icon_svg(icon_path);
4065                                } else {
4066                                    entry = entry.icon(IconName::Sparkle);
4067                                }
4068
4069                                entry = entry
4070                                    .when(
4071                                        is_agent_selected(Agent::Custom {
4072                                            id: item.id.clone(),
4073                                        }),
4074                                        |this| {
4075                                            this.action(Box::new(NewExternalAgentThread {
4076                                                agent: None,
4077                                            }))
4078                                        },
4079                                    )
4080                                    .icon_color(Color::Muted)
4081                                    .disabled(is_via_collab)
4082                                    .handler({
4083                                        let workspace = workspace.clone();
4084                                        let agent_id = item.id.clone();
4085                                        move |window, cx| {
4086                                            if let Some(workspace) = workspace.upgrade() {
4087                                                workspace.update(cx, |workspace, cx| {
4088                                                    if let Some(panel) =
4089                                                        workspace.panel::<AgentPanel>(cx)
4090                                                    {
4091                                                        panel.update(cx, |panel, cx| {
4092                                                            panel.new_agent_thread(
4093                                                                Agent::Custom {
4094                                                                    id: agent_id.clone(),
4095                                                                },
4096                                                                window,
4097                                                                cx,
4098                                                            );
4099                                                        });
4100                                                    }
4101                                                });
4102                                            }
4103                                        }
4104                                    });
4105
4106                                menu = menu.item(entry);
4107                            }
4108
4109                            menu
4110                        })
4111                        .separator()
4112                        .item(
4113                            ContextMenuEntry::new("Add More Agents")
4114                                .icon(IconName::Plus)
4115                                .icon_color(Color::Muted)
4116                                .handler({
4117                                    move |window, cx| {
4118                                        window
4119                                            .dispatch_action(Box::new(zed_actions::AcpRegistry), cx)
4120                                    }
4121                                }),
4122                        )
4123                }))
4124            })
4125        };
4126
4127        let is_thread_loading = self
4128            .active_conversation_view()
4129            .map(|thread| thread.read(cx).is_loading())
4130            .unwrap_or(false);
4131
4132        let has_custom_icon = selected_agent_custom_icon.is_some();
4133        let selected_agent_custom_icon_for_button = selected_agent_custom_icon.clone();
4134        let selected_agent_builtin_icon = self.selected_agent.icon();
4135        let selected_agent_label_for_tooltip = selected_agent_label.clone();
4136
4137        let selected_agent = div()
4138            .id("selected_agent_icon")
4139            .when_some(selected_agent_custom_icon, |this, icon_path| {
4140                this.px_1()
4141                    .child(Icon::from_external_svg(icon_path).color(Color::Muted))
4142            })
4143            .when(!has_custom_icon, |this| {
4144                this.when_some(selected_agent_builtin_icon, |this, icon| {
4145                    this.px_1().child(Icon::new(icon).color(Color::Muted))
4146                })
4147            })
4148            .tooltip(move |_, cx| {
4149                Tooltip::with_meta(
4150                    selected_agent_label_for_tooltip.clone(),
4151                    None,
4152                    "Selected Agent",
4153                    cx,
4154                )
4155            });
4156
4157        let selected_agent = if is_thread_loading {
4158            selected_agent
4159                .with_animation(
4160                    "pulsating-icon",
4161                    Animation::new(Duration::from_secs(1))
4162                        .repeat()
4163                        .with_easing(pulsating_between(0.2, 0.6)),
4164                    |icon, delta| icon.opacity(delta),
4165                )
4166                .into_any_element()
4167        } else {
4168            selected_agent.into_any_element()
4169        };
4170
4171        let is_empty_state = !self.active_thread_has_messages(cx);
4172
4173        let is_in_history_or_config = matches!(
4174            &self.active_view,
4175            ActiveView::History { .. } | ActiveView::Configuration
4176        );
4177
4178        let is_full_screen = self.is_zoomed(window, cx);
4179        let full_screen_button = if is_full_screen {
4180            IconButton::new("disable-full-screen", IconName::Minimize)
4181                .icon_size(IconSize::Small)
4182                .tooltip(move |_, cx| Tooltip::for_action("Disable Full Screen", &ToggleZoom, cx))
4183                .on_click(cx.listener(move |this, _, window, cx| {
4184                    this.toggle_zoom(&ToggleZoom, window, cx);
4185                }))
4186        } else {
4187            IconButton::new("enable-full-screen", IconName::Maximize)
4188                .icon_size(IconSize::Small)
4189                .tooltip(move |_, cx| Tooltip::for_action("Enable Full Screen", &ToggleZoom, cx))
4190                .on_click(cx.listener(move |this, _, window, cx| {
4191                    this.toggle_zoom(&ToggleZoom, window, cx);
4192                }))
4193        };
4194
4195        let use_v2_empty_toolbar = is_empty_state && !is_in_history_or_config;
4196
4197        let max_content_width = AgentSettings::get_global(cx).max_content_width;
4198
4199        let base_container = h_flex()
4200            .size_full()
4201            // TODO: This is only until we remove Agent settings from the panel.
4202            .when(!is_in_history_or_config, |this| {
4203                this.max_w(max_content_width).mx_auto()
4204            })
4205            .flex_none()
4206            .justify_between()
4207            .gap_2();
4208
4209        let toolbar_content = if use_v2_empty_toolbar {
4210            let (chevron_icon, icon_color, label_color) =
4211                if self.new_thread_menu_handle.is_deployed() {
4212                    (IconName::ChevronUp, Color::Accent, Color::Accent)
4213                } else {
4214                    (IconName::ChevronDown, Color::Muted, Color::Default)
4215                };
4216
4217            let agent_icon = if let Some(icon_path) = selected_agent_custom_icon_for_button {
4218                Icon::from_external_svg(icon_path)
4219                    .size(IconSize::Small)
4220                    .color(icon_color)
4221            } else {
4222                let icon_name = selected_agent_builtin_icon.unwrap_or(IconName::ZedAgent);
4223                Icon::new(icon_name).size(IconSize::Small).color(icon_color)
4224            };
4225
4226            let agent_selector_button = Button::new("agent-selector-trigger", selected_agent_label)
4227                .start_icon(agent_icon)
4228                .color(label_color)
4229                .end_icon(
4230                    Icon::new(chevron_icon)
4231                        .color(icon_color)
4232                        .size(IconSize::XSmall),
4233                );
4234
4235            let agent_selector_menu = PopoverMenu::new("new_thread_menu")
4236                .trigger_with_tooltip(agent_selector_button, {
4237                    move |_window, cx| {
4238                        Tooltip::for_action_in(
4239                            "New Thread…",
4240                            &ToggleNewThreadMenu,
4241                            &focus_handle,
4242                            cx,
4243                        )
4244                    }
4245                })
4246                .menu({
4247                    let builder = new_thread_menu_builder.clone();
4248                    move |window, cx| builder(window, cx)
4249                })
4250                .with_handle(self.new_thread_menu_handle.clone())
4251                .anchor(Corner::TopLeft)
4252                .offset(gpui::Point {
4253                    x: px(1.0),
4254                    y: px(1.0),
4255                });
4256
4257            base_container
4258                .child(
4259                    h_flex()
4260                        .size_full()
4261                        .gap(DynamicSpacing::Base04.rems(cx))
4262                        .pl(DynamicSpacing::Base04.rems(cx))
4263                        .child(agent_selector_menu)
4264                        .when(
4265                            has_visible_worktrees && self.project_has_git_repository(cx),
4266                            |this| this.child(self.render_start_thread_in_selector(cx)),
4267                        )
4268                        .when(
4269                            matches!(self.start_thread_in, StartThreadIn::NewWorktree { .. }),
4270                            |this| this.child(self.render_new_worktree_branch_selector(cx)),
4271                        ),
4272                )
4273                .child(
4274                    h_flex()
4275                        .h_full()
4276                        .flex_none()
4277                        .gap_1()
4278                        .pl_1()
4279                        .pr_1()
4280                        .child(full_screen_button)
4281                        .child(self.render_panel_options_menu(window, cx)),
4282                )
4283                .into_any_element()
4284        } else {
4285            let new_thread_menu = PopoverMenu::new("new_thread_menu")
4286                .trigger_with_tooltip(
4287                    IconButton::new("new_thread_menu_btn", IconName::Plus)
4288                        .icon_size(IconSize::Small),
4289                    {
4290                        move |_window, cx| {
4291                            Tooltip::for_action_in(
4292                                "New Thread\u{2026}",
4293                                &ToggleNewThreadMenu,
4294                                &focus_handle,
4295                                cx,
4296                            )
4297                        }
4298                    },
4299                )
4300                .anchor(Corner::TopRight)
4301                .with_handle(self.new_thread_menu_handle.clone())
4302                .menu(move |window, cx| new_thread_menu_builder(window, cx));
4303
4304            base_container
4305                .child(
4306                    h_flex()
4307                        .size_full()
4308                        .gap(DynamicSpacing::Base04.rems(cx))
4309                        .pl(DynamicSpacing::Base04.rems(cx))
4310                        .child(match &self.active_view {
4311                            ActiveView::History { .. } | ActiveView::Configuration => {
4312                                self.render_toolbar_back_button(cx).into_any_element()
4313                            }
4314                            _ => selected_agent.into_any_element(),
4315                        })
4316                        .child(self.render_title_view(window, cx)),
4317                )
4318                .child(
4319                    h_flex()
4320                        .h_full()
4321                        .flex_none()
4322                        .gap_1()
4323                        .pl_1()
4324                        .pr_1()
4325                        .child(new_thread_menu)
4326                        .child(full_screen_button)
4327                        .child(self.render_panel_options_menu(window, cx)),
4328                )
4329                .into_any_element()
4330        };
4331
4332        h_flex()
4333            .id("agent-panel-toolbar")
4334            .h(Tab::container_height(cx))
4335            .flex_shrink_0()
4336            .max_w_full()
4337            .bg(cx.theme().colors().tab_bar_background)
4338            .border_b_1()
4339            .border_color(cx.theme().colors().border)
4340            .child(toolbar_content)
4341    }
4342
4343    fn render_worktree_creation_status(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
4344        let (view_id, status) = self.worktree_creation_status.as_ref()?;
4345        let active_view_id = self.active_conversation_view().map(|v| v.entity_id());
4346        if active_view_id != Some(*view_id) {
4347            return None;
4348        }
4349        match status {
4350            WorktreeCreationStatus::Creating => Some(
4351                h_flex()
4352                    .absolute()
4353                    .bottom_12()
4354                    .w_full()
4355                    .p_2()
4356                    .gap_1()
4357                    .justify_center()
4358                    .bg(cx.theme().colors().editor_background)
4359                    .child(
4360                        Icon::new(IconName::LoadCircle)
4361                            .size(IconSize::Small)
4362                            .color(Color::Muted)
4363                            .with_rotate_animation(3),
4364                    )
4365                    .child(
4366                        Label::new("Creating Worktree…")
4367                            .color(Color::Muted)
4368                            .size(LabelSize::Small),
4369                    )
4370                    .into_any_element(),
4371            ),
4372            WorktreeCreationStatus::Error(message) => Some(
4373                Callout::new()
4374                    .icon(IconName::XCircleFilled)
4375                    .severity(Severity::Error)
4376                    .title("Worktree Creation Error")
4377                    .description(message.clone())
4378                    .border_position(ui::BorderPosition::Bottom)
4379                    .dismiss_action(
4380                        IconButton::new("dismiss-worktree-error", IconName::Close)
4381                            .icon_size(IconSize::Small)
4382                            .tooltip(Tooltip::text("Dismiss"))
4383                            .on_click(cx.listener(|this, _, _, cx| {
4384                                this.worktree_creation_status = None;
4385                                cx.notify();
4386                            })),
4387                    )
4388                    .into_any_element(),
4389            ),
4390        }
4391    }
4392
4393    fn should_render_trial_end_upsell(&self, cx: &mut Context<Self>) -> bool {
4394        if TrialEndUpsell::dismissed(cx) {
4395            return false;
4396        }
4397
4398        match &self.active_view {
4399            ActiveView::AgentThread { .. } => {
4400                if LanguageModelRegistry::global(cx)
4401                    .read(cx)
4402                    .default_model()
4403                    .is_some_and(|model| {
4404                        model.provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
4405                    })
4406                {
4407                    return false;
4408                }
4409            }
4410            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
4411                return false;
4412            }
4413        }
4414
4415        let plan = self.user_store.read(cx).plan();
4416        let has_previous_trial = self.user_store.read(cx).trial_started_at().is_some();
4417
4418        plan.is_some_and(|plan| plan == Plan::ZedFree) && has_previous_trial
4419    }
4420
4421    fn should_render_agent_layout_onboarding(&self, cx: &mut Context<Self>) -> bool {
4422        // We only want to show this for existing users: those who
4423        // have used the agent panel before the sidebar was introduced.
4424        // We can infer that state by users having seen the onboarding
4425        // at one point, but not the agent layout onboarding.
4426
4427        let has_messages = self.active_thread_has_messages(cx);
4428        let is_dismissed = self
4429            .agent_layout_onboarding_dismissed
4430            .load(Ordering::Acquire);
4431
4432        if is_dismissed || has_messages {
4433            return false;
4434        }
4435
4436        match &self.active_view {
4437            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
4438                false
4439            }
4440            ActiveView::AgentThread { .. } => {
4441                let existing_user = self
4442                    .new_user_onboarding_upsell_dismissed
4443                    .load(Ordering::Acquire);
4444                existing_user
4445            }
4446        }
4447    }
4448
4449    fn render_agent_layout_onboarding(
4450        &self,
4451        _window: &mut Window,
4452        cx: &mut Context<Self>,
4453    ) -> Option<impl IntoElement> {
4454        if !self.should_render_agent_layout_onboarding(cx) {
4455            return None;
4456        }
4457
4458        Some(div().child(self.agent_layout_onboarding.clone()))
4459    }
4460
4461    fn dismiss_agent_layout_onboarding(&mut self, cx: &mut Context<Self>) {
4462        self.agent_layout_onboarding_dismissed
4463            .store(true, Ordering::Release);
4464        AgentLayoutOnboarding::set_dismissed(true, cx);
4465        cx.notify();
4466    }
4467
4468    fn dismiss_ai_onboarding(&mut self, cx: &mut Context<Self>) {
4469        self.new_user_onboarding_upsell_dismissed
4470            .store(true, Ordering::Release);
4471        OnboardingUpsell::set_dismissed(true, cx);
4472        self.dismiss_agent_layout_onboarding(cx);
4473        cx.notify();
4474    }
4475
4476    fn should_render_new_user_onboarding(&mut self, cx: &mut Context<Self>) -> bool {
4477        if self
4478            .new_user_onboarding_upsell_dismissed
4479            .load(Ordering::Acquire)
4480        {
4481            return false;
4482        }
4483
4484        let user_store = self.user_store.read(cx);
4485
4486        if user_store.plan().is_some_and(|plan| plan == Plan::ZedPro)
4487            && user_store
4488                .subscription_period()
4489                .and_then(|period| period.0.checked_add_days(chrono::Days::new(1)))
4490                .is_some_and(|date| date < chrono::Utc::now())
4491        {
4492            if !self
4493                .new_user_onboarding_upsell_dismissed
4494                .load(Ordering::Acquire)
4495            {
4496                self.dismiss_ai_onboarding(cx);
4497            }
4498            return false;
4499        }
4500
4501        let has_configured_non_zed_providers = LanguageModelRegistry::read_global(cx)
4502            .visible_providers()
4503            .iter()
4504            .any(|provider| {
4505                provider.is_authenticated(cx)
4506                    && provider.id() != language_model::ZED_CLOUD_PROVIDER_ID
4507            });
4508
4509        match &self.active_view {
4510            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {
4511                false
4512            }
4513            ActiveView::AgentThread {
4514                conversation_view, ..
4515            } if conversation_view.read(cx).as_native_thread(cx).is_none() => false,
4516            ActiveView::AgentThread { conversation_view } => {
4517                let history_is_empty = conversation_view
4518                    .read(cx)
4519                    .history()
4520                    .is_none_or(|h| h.read(cx).is_empty());
4521                history_is_empty || !has_configured_non_zed_providers
4522            }
4523        }
4524    }
4525
4526    fn render_new_user_onboarding(
4527        &mut self,
4528        _window: &mut Window,
4529        cx: &mut Context<Self>,
4530    ) -> Option<impl IntoElement> {
4531        if !self.should_render_new_user_onboarding(cx) {
4532            return None;
4533        }
4534
4535        Some(
4536            div()
4537                .bg(cx.theme().colors().editor_background)
4538                .child(self.new_user_onboarding.clone()),
4539        )
4540    }
4541
4542    fn render_trial_end_upsell(
4543        &self,
4544        _window: &mut Window,
4545        cx: &mut Context<Self>,
4546    ) -> Option<impl IntoElement> {
4547        if !self.should_render_trial_end_upsell(cx) {
4548            return None;
4549        }
4550
4551        Some(
4552            v_flex()
4553                .absolute()
4554                .inset_0()
4555                .size_full()
4556                .bg(cx.theme().colors().panel_background)
4557                .opacity(0.85)
4558                .block_mouse_except_scroll()
4559                .child(EndTrialUpsell::new(Arc::new({
4560                    let this = cx.entity();
4561                    move |_, cx| {
4562                        this.update(cx, |_this, cx| {
4563                            TrialEndUpsell::set_dismissed(true, cx);
4564                            cx.notify();
4565                        });
4566                    }
4567                }))),
4568        )
4569    }
4570
4571    fn render_drag_target(&self, cx: &Context<Self>) -> Div {
4572        let is_local = self.project.read(cx).is_local();
4573        div()
4574            .invisible()
4575            .absolute()
4576            .top_0()
4577            .right_0()
4578            .bottom_0()
4579            .left_0()
4580            .bg(cx.theme().colors().drop_target_background)
4581            .drag_over::<DraggedTab>(|this, _, _, _| this.visible())
4582            .drag_over::<DraggedSelection>(|this, _, _, _| this.visible())
4583            .when(is_local, |this| {
4584                this.drag_over::<ExternalPaths>(|this, _, _, _| this.visible())
4585            })
4586            .on_drop(cx.listener(move |this, tab: &DraggedTab, window, cx| {
4587                let item = tab.pane.read(cx).item_for_index(tab.ix);
4588                let project_paths = item
4589                    .and_then(|item| item.project_path(cx))
4590                    .into_iter()
4591                    .collect::<Vec<_>>();
4592                this.handle_drop(project_paths, vec![], window, cx);
4593            }))
4594            .on_drop(
4595                cx.listener(move |this, selection: &DraggedSelection, window, cx| {
4596                    let project_paths = selection
4597                        .items()
4598                        .filter_map(|item| this.project.read(cx).path_for_entry(item.entry_id, cx))
4599                        .collect::<Vec<_>>();
4600                    this.handle_drop(project_paths, vec![], window, cx);
4601                }),
4602            )
4603            .on_drop(cx.listener(move |this, paths: &ExternalPaths, window, cx| {
4604                let tasks = paths
4605                    .paths()
4606                    .iter()
4607                    .map(|path| {
4608                        Workspace::project_path_for_path(this.project.clone(), path, false, cx)
4609                    })
4610                    .collect::<Vec<_>>();
4611                cx.spawn_in(window, async move |this, cx| {
4612                    let mut paths = vec![];
4613                    let mut added_worktrees = vec![];
4614                    let opened_paths = futures::future::join_all(tasks).await;
4615                    for entry in opened_paths {
4616                        if let Some((worktree, project_path)) = entry.log_err() {
4617                            added_worktrees.push(worktree);
4618                            paths.push(project_path);
4619                        }
4620                    }
4621                    this.update_in(cx, |this, window, cx| {
4622                        this.handle_drop(paths, added_worktrees, window, cx);
4623                    })
4624                    .ok();
4625                })
4626                .detach();
4627            }))
4628    }
4629
4630    fn handle_drop(
4631        &mut self,
4632        paths: Vec<ProjectPath>,
4633        added_worktrees: Vec<Entity<Worktree>>,
4634        window: &mut Window,
4635        cx: &mut Context<Self>,
4636    ) {
4637        match &self.active_view {
4638            ActiveView::AgentThread { conversation_view } => {
4639                conversation_view.update(cx, |conversation_view, cx| {
4640                    conversation_view.insert_dragged_files(paths, added_worktrees, window, cx);
4641                });
4642            }
4643            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
4644        }
4645    }
4646
4647    fn render_workspace_trust_message(&self, cx: &Context<Self>) -> Option<impl IntoElement> {
4648        if !self.show_trust_workspace_message {
4649            return None;
4650        }
4651
4652        let description = "To protect your system, third-party code—like MCP servers—won't run until you mark this workspace as safe.";
4653
4654        Some(
4655            Callout::new()
4656                .icon(IconName::Warning)
4657                .severity(Severity::Warning)
4658                .border_position(ui::BorderPosition::Bottom)
4659                .title("You're in Restricted Mode")
4660                .description(description)
4661                .actions_slot(
4662                    Button::new("open-trust-modal", "Configure Project Trust")
4663                        .label_size(LabelSize::Small)
4664                        .style(ButtonStyle::Outlined)
4665                        .on_click({
4666                            cx.listener(move |this, _, window, cx| {
4667                                this.workspace
4668                                    .update(cx, |workspace, cx| {
4669                                        workspace
4670                                            .show_worktree_trust_security_modal(true, window, cx)
4671                                    })
4672                                    .log_err();
4673                            })
4674                        }),
4675                ),
4676        )
4677    }
4678
4679    fn key_context(&self) -> KeyContext {
4680        let mut key_context = KeyContext::new_with_defaults();
4681        key_context.add("AgentPanel");
4682        match &self.active_view {
4683            ActiveView::AgentThread { .. } => key_context.add("acp_thread"),
4684            ActiveView::Uninitialized | ActiveView::History { .. } | ActiveView::Configuration => {}
4685        }
4686        key_context
4687    }
4688}
4689
4690impl Render for AgentPanel {
4691    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
4692        // WARNING: Changes to this element hierarchy can have
4693        // non-obvious implications to the layout of children.
4694        //
4695        // If you need to change it, please confirm:
4696        // - The message editor expands (cmd-option-esc) correctly
4697        // - When expanded, the buttons at the bottom of the panel are displayed correctly
4698        // - Font size works as expected and can be changed with cmd-+/cmd-
4699        // - Scrolling in all views works as expected
4700        // - Files can be dropped into the panel
4701        let content = v_flex()
4702            .relative()
4703            .size_full()
4704            .justify_between()
4705            .key_context(self.key_context())
4706            .on_action(cx.listener(|this, action: &NewThread, window, cx| {
4707                this.new_thread(action, window, cx);
4708            }))
4709            .on_action(cx.listener(|this, _: &OpenHistory, window, cx| {
4710                this.open_history(window, cx);
4711            }))
4712            .on_action(cx.listener(|this, _: &OpenSettings, window, cx| {
4713                this.open_configuration(window, cx);
4714            }))
4715            .on_action(cx.listener(Self::open_active_thread_as_markdown))
4716            .on_action(cx.listener(Self::deploy_rules_library))
4717            .on_action(cx.listener(Self::go_back))
4718            .on_action(cx.listener(Self::toggle_navigation_menu))
4719            .on_action(cx.listener(Self::toggle_options_menu))
4720            .on_action(cx.listener(Self::increase_font_size))
4721            .on_action(cx.listener(Self::decrease_font_size))
4722            .on_action(cx.listener(Self::reset_font_size))
4723            .on_action(cx.listener(Self::toggle_zoom))
4724            .on_action(cx.listener(|this, _: &ReauthenticateAgent, window, cx| {
4725                if let Some(conversation_view) = this.active_conversation_view() {
4726                    conversation_view.update(cx, |conversation_view, cx| {
4727                        conversation_view.reauthenticate(window, cx)
4728                    })
4729                }
4730            }))
4731            .child(self.render_toolbar(window, cx))
4732            .children(self.render_workspace_trust_message(cx))
4733            .children(self.render_new_user_onboarding(window, cx))
4734            .children(self.render_agent_layout_onboarding(window, cx))
4735            .map(|parent| match &self.active_view {
4736                ActiveView::Uninitialized => parent,
4737                ActiveView::AgentThread {
4738                    conversation_view, ..
4739                } => parent
4740                    .child(conversation_view.clone())
4741                    .child(self.render_drag_target(cx)),
4742                ActiveView::History { view } => parent.child(view.clone()),
4743                ActiveView::Configuration => parent.children(self.configuration.clone()),
4744            })
4745            .children(self.render_worktree_creation_status(cx))
4746            .children(self.render_trial_end_upsell(window, cx));
4747
4748        match self.active_view.which_font_size_used() {
4749            WhichFontSize::AgentFont => {
4750                WithRemSize::new(ThemeSettings::get_global(cx).agent_ui_font_size(cx))
4751                    .size_full()
4752                    .child(content)
4753                    .into_any()
4754            }
4755            _ => content.into_any(),
4756        }
4757    }
4758}
4759
4760struct PromptLibraryInlineAssist {
4761    workspace: WeakEntity<Workspace>,
4762}
4763
4764impl PromptLibraryInlineAssist {
4765    pub fn new(workspace: WeakEntity<Workspace>) -> Self {
4766        Self { workspace }
4767    }
4768}
4769
4770impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
4771    fn assist(
4772        &self,
4773        prompt_editor: &Entity<Editor>,
4774        initial_prompt: Option<String>,
4775        window: &mut Window,
4776        cx: &mut Context<RulesLibrary>,
4777    ) {
4778        InlineAssistant::update_global(cx, |assistant, cx| {
4779            let Some(workspace) = self.workspace.upgrade() else {
4780                return;
4781            };
4782            let Some(panel) = workspace.read(cx).panel::<AgentPanel>(cx) else {
4783                return;
4784            };
4785            let history = panel
4786                .read(cx)
4787                .connection_store()
4788                .read(cx)
4789                .entry(&crate::Agent::NativeAgent)
4790                .and_then(|s| s.read(cx).history())
4791                .map(|h| h.downgrade());
4792            let project = workspace.read(cx).project().downgrade();
4793            let panel = panel.read(cx);
4794            let thread_store = panel.thread_store().clone();
4795            assistant.assist(
4796                prompt_editor,
4797                self.workspace.clone(),
4798                project,
4799                thread_store,
4800                None,
4801                history,
4802                initial_prompt,
4803                window,
4804                cx,
4805            );
4806        })
4807    }
4808
4809    fn focus_agent_panel(
4810        &self,
4811        workspace: &mut Workspace,
4812        window: &mut Window,
4813        cx: &mut Context<Workspace>,
4814    ) -> bool {
4815        workspace.focus_panel::<AgentPanel>(window, cx).is_some()
4816    }
4817}
4818
4819struct OnboardingUpsell;
4820
4821impl Dismissable for OnboardingUpsell {
4822    const KEY: &'static str = "dismissed-trial-upsell";
4823}
4824
4825struct AgentLayoutOnboarding;
4826
4827impl Dismissable for AgentLayoutOnboarding {
4828    const KEY: &'static str = "dismissed-agent-layout-onboarding";
4829}
4830
4831struct TrialEndUpsell;
4832
4833impl Dismissable for TrialEndUpsell {
4834    const KEY: &'static str = "dismissed-trial-end-upsell";
4835}
4836
4837/// Test-only helper methods
4838#[cfg(any(test, feature = "test-support"))]
4839impl AgentPanel {
4840    pub fn test_new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
4841        Self::new(workspace, None, window, cx)
4842    }
4843
4844    /// Opens an external thread using an arbitrary AgentServer.
4845    ///
4846    /// This is a test-only helper that allows visual tests and integration tests
4847    /// to inject a stub server without modifying production code paths.
4848    /// Not compiled into production builds.
4849    pub fn open_external_thread_with_server(
4850        &mut self,
4851        server: Rc<dyn AgentServer>,
4852        window: &mut Window,
4853        cx: &mut Context<Self>,
4854    ) {
4855        let workspace = self.workspace.clone();
4856        let project = self.project.clone();
4857
4858        let ext_agent = Agent::Custom {
4859            id: server.agent_id(),
4860        };
4861
4862        let conversation_view = self.create_agent_thread(
4863            server, None, None, None, None, workspace, project, ext_agent, window, cx,
4864        );
4865        self.set_active_view(
4866            ActiveView::AgentThread { conversation_view },
4867            true,
4868            window,
4869            cx,
4870        );
4871    }
4872
4873    /// Returns the currently active thread view, if any.
4874    ///
4875    /// This is a test-only accessor that exposes the private `active_thread_view()`
4876    /// method for test assertions. Not compiled into production builds.
4877    pub fn active_thread_view_for_tests(&self) -> Option<&Entity<ConversationView>> {
4878        self.active_conversation_view()
4879    }
4880
4881    /// Sets the start_thread_in value directly, bypassing validation.
4882    ///
4883    /// This is a test-only helper for visual tests that need to show specific
4884    /// start_thread_in states without requiring a real git repository.
4885    pub fn set_start_thread_in_for_tests(&mut self, target: StartThreadIn, cx: &mut Context<Self>) {
4886        self.start_thread_in = target;
4887        cx.notify();
4888    }
4889
4890    /// Returns the current worktree creation status.
4891    ///
4892    /// This is a test-only helper for visual tests.
4893    pub fn worktree_creation_status_for_tests(&self) -> Option<&WorktreeCreationStatus> {
4894        self.worktree_creation_status.as_ref().map(|(_, s)| s)
4895    }
4896
4897    /// Sets the worktree creation status directly, associating it with the
4898    /// currently active conversation view.
4899    ///
4900    /// This is a test-only helper for visual tests that need to show the
4901    /// "Creating worktree…" spinner or error banners.
4902    pub fn set_worktree_creation_status_for_tests(
4903        &mut self,
4904        status: Option<WorktreeCreationStatus>,
4905        cx: &mut Context<Self>,
4906    ) {
4907        self.worktree_creation_status = status.map(|s| {
4908            let view_id = self
4909                .active_conversation_view()
4910                .map(|v| v.entity_id())
4911                .unwrap_or_else(|| EntityId::from(0u64));
4912            (view_id, s)
4913        });
4914        cx.notify();
4915    }
4916
4917    /// Opens the history view.
4918    ///
4919    /// This is a test-only helper that exposes the private `open_history()`
4920    /// method for visual tests.
4921    pub fn open_history_for_tests(&mut self, window: &mut Window, cx: &mut Context<Self>) {
4922        self.open_history(window, cx);
4923    }
4924
4925    /// Opens the start_thread_in selector popover menu.
4926    ///
4927    /// This is a test-only helper for visual tests.
4928    pub fn open_start_thread_in_menu_for_tests(
4929        &mut self,
4930        window: &mut Window,
4931        cx: &mut Context<Self>,
4932    ) {
4933        self.start_thread_in_menu_handle.show(window, cx);
4934    }
4935
4936    /// Dismisses the start_thread_in dropdown menu.
4937    ///
4938    /// This is a test-only helper for visual tests.
4939    pub fn close_start_thread_in_menu_for_tests(&mut self, cx: &mut Context<Self>) {
4940        self.start_thread_in_menu_handle.hide(cx);
4941    }
4942}
4943
4944#[cfg(test)]
4945mod tests {
4946    use super::*;
4947    use crate::conversation_view::tests::{StubAgentServer, init_test};
4948    use crate::test_support::{
4949        active_session_id, open_thread_with_connection, open_thread_with_custom_connection,
4950        send_message,
4951    };
4952    use acp_thread::{StubAgentConnection, ThreadStatus};
4953    use agent_servers::CODEX_ID;
4954    use feature_flags::FeatureFlagAppExt;
4955    use fs::FakeFs;
4956    use gpui::{TestAppContext, VisualTestContext};
4957    use project::Project;
4958    use serde_json::json;
4959    use std::path::Path;
4960    use std::time::Instant;
4961    use workspace::MultiWorkspace;
4962
4963    #[gpui::test]
4964    async fn test_active_thread_serialize_and_load_round_trip(cx: &mut TestAppContext) {
4965        init_test(cx);
4966        cx.update(|cx| {
4967            agent::ThreadStore::init_global(cx);
4968            language_model::LanguageModelRegistry::test(cx);
4969        });
4970
4971        // Create a MultiWorkspace window with two workspaces.
4972        let fs = FakeFs::new(cx.executor());
4973        let project_a = Project::test(fs.clone(), [], cx).await;
4974        let project_b = Project::test(fs, [], cx).await;
4975
4976        let multi_workspace =
4977            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
4978
4979        let workspace_a = multi_workspace
4980            .read_with(cx, |multi_workspace, _cx| {
4981                multi_workspace.workspace().clone()
4982            })
4983            .unwrap();
4984
4985        let workspace_b = multi_workspace
4986            .update(cx, |multi_workspace, window, cx| {
4987                multi_workspace.test_add_workspace(project_b.clone(), window, cx)
4988            })
4989            .unwrap();
4990
4991        workspace_a.update(cx, |workspace, _cx| {
4992            workspace.set_random_database_id();
4993        });
4994        workspace_b.update(cx, |workspace, _cx| {
4995            workspace.set_random_database_id();
4996        });
4997
4998        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
4999
5000        // Set up workspace A: with an active thread.
5001        let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
5002            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
5003        });
5004
5005        panel_a.update_in(cx, |panel, window, cx| {
5006            panel.open_external_thread_with_server(
5007                Rc::new(StubAgentServer::default_response()),
5008                window,
5009                cx,
5010            );
5011        });
5012
5013        cx.run_until_parked();
5014
5015        panel_a.read_with(cx, |panel, cx| {
5016            assert!(
5017                panel.active_agent_thread(cx).is_some(),
5018                "workspace A should have an active thread after connection"
5019            );
5020        });
5021
5022        send_message(&panel_a, cx);
5023
5024        let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent.clone());
5025
5026        // Set up workspace B: ClaudeCode, no active thread.
5027        let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
5028            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
5029        });
5030
5031        panel_b.update(cx, |panel, _cx| {
5032            panel.selected_agent = Agent::Custom {
5033                id: "claude-acp".into(),
5034            };
5035        });
5036
5037        // Serialize both panels.
5038        panel_a.update(cx, |panel, cx| panel.serialize(cx));
5039        panel_b.update(cx, |panel, cx| panel.serialize(cx));
5040        cx.run_until_parked();
5041
5042        // Load fresh panels for each workspace and verify independent state.
5043        let async_cx = cx.update(|window, cx| window.to_async(cx));
5044        let loaded_a = AgentPanel::load(workspace_a.downgrade(), async_cx)
5045            .await
5046            .expect("panel A load should succeed");
5047        cx.run_until_parked();
5048
5049        let async_cx = cx.update(|window, cx| window.to_async(cx));
5050        let loaded_b = AgentPanel::load(workspace_b.downgrade(), async_cx)
5051            .await
5052            .expect("panel B load should succeed");
5053        cx.run_until_parked();
5054
5055        // Workspace A should restore its thread and agent type
5056        loaded_a.read_with(cx, |panel, _cx| {
5057            assert_eq!(
5058                panel.selected_agent, agent_type_a,
5059                "workspace A agent type should be restored"
5060            );
5061            assert!(
5062                panel.active_conversation_view().is_some(),
5063                "workspace A should have its active thread restored"
5064            );
5065        });
5066
5067        // Workspace B should restore its own agent type, with no thread
5068        loaded_b.read_with(cx, |panel, _cx| {
5069            assert_eq!(
5070                panel.selected_agent,
5071                Agent::Custom {
5072                    id: "claude-acp".into()
5073                },
5074                "workspace B agent type should be restored"
5075            );
5076            assert!(
5077                panel.active_conversation_view().is_none(),
5078                "workspace B should have no active thread"
5079            );
5080        });
5081    }
5082
5083    #[gpui::test]
5084    async fn test_non_native_thread_without_metadata_is_not_restored(cx: &mut TestAppContext) {
5085        init_test(cx);
5086        cx.update(|cx| {
5087            agent::ThreadStore::init_global(cx);
5088            language_model::LanguageModelRegistry::test(cx);
5089        });
5090
5091        let fs = FakeFs::new(cx.executor());
5092        let project = Project::test(fs, [], cx).await;
5093
5094        let multi_workspace =
5095            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5096
5097        let workspace = multi_workspace
5098            .read_with(cx, |multi_workspace, _cx| {
5099                multi_workspace.workspace().clone()
5100            })
5101            .unwrap();
5102
5103        workspace.update(cx, |workspace, _cx| {
5104            workspace.set_random_database_id();
5105        });
5106
5107        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5108
5109        let panel = workspace.update_in(cx, |workspace, window, cx| {
5110            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
5111        });
5112
5113        panel.update_in(cx, |panel, window, cx| {
5114            panel.open_external_thread_with_server(
5115                Rc::new(StubAgentServer::default_response()),
5116                window,
5117                cx,
5118            );
5119        });
5120
5121        cx.run_until_parked();
5122
5123        panel.read_with(cx, |panel, cx| {
5124            assert!(
5125                panel.active_agent_thread(cx).is_some(),
5126                "should have an active thread after connection"
5127            );
5128        });
5129
5130        // Serialize without ever sending a message, so no thread metadata exists.
5131        panel.update(cx, |panel, cx| panel.serialize(cx));
5132        cx.run_until_parked();
5133
5134        let async_cx = cx.update(|window, cx| window.to_async(cx));
5135        let loaded = AgentPanel::load(workspace.downgrade(), async_cx)
5136            .await
5137            .expect("panel load should succeed");
5138        cx.run_until_parked();
5139
5140        loaded.read_with(cx, |panel, _cx| {
5141            assert!(
5142                panel.active_conversation_view().is_none(),
5143                "thread without metadata should not be restored"
5144            );
5145        });
5146    }
5147
5148    /// Extracts the text from a Text content block, panicking if it's not Text.
5149    fn expect_text_block(block: &acp::ContentBlock) -> &str {
5150        match block {
5151            acp::ContentBlock::Text(t) => t.text.as_str(),
5152            other => panic!("expected Text block, got {:?}", other),
5153        }
5154    }
5155
5156    /// Extracts the (text_content, uri) from a Resource content block, panicking
5157    /// if it's not a TextResourceContents resource.
5158    fn expect_resource_block(block: &acp::ContentBlock) -> (&str, &str) {
5159        match block {
5160            acp::ContentBlock::Resource(r) => match &r.resource {
5161                acp::EmbeddedResourceResource::TextResourceContents(t) => {
5162                    (t.text.as_str(), t.uri.as_str())
5163                }
5164                other => panic!("expected TextResourceContents, got {:?}", other),
5165            },
5166            other => panic!("expected Resource block, got {:?}", other),
5167        }
5168    }
5169
5170    #[test]
5171    fn test_build_conflict_resolution_prompt_single_conflict() {
5172        let conflicts = vec![ConflictContent {
5173            file_path: "src/main.rs".to_string(),
5174            conflict_text: "<<<<<<< HEAD\nlet x = 1;\n=======\nlet x = 2;\n>>>>>>> feature"
5175                .to_string(),
5176            ours_branch_name: "HEAD".to_string(),
5177            theirs_branch_name: "feature".to_string(),
5178        }];
5179
5180        let blocks = build_conflict_resolution_prompt(&conflicts);
5181        // 2 Text blocks + 1 ResourceLink + 1 Resource for the conflict
5182        assert_eq!(
5183            blocks.len(),
5184            4,
5185            "expected 2 text + 1 resource link + 1 resource block"
5186        );
5187
5188        let intro_text = expect_text_block(&blocks[0]);
5189        assert!(
5190            intro_text.contains("Please resolve the following merge conflict in"),
5191            "prompt should include single-conflict intro text"
5192        );
5193
5194        match &blocks[1] {
5195            acp::ContentBlock::ResourceLink(link) => {
5196                assert!(
5197                    link.uri.contains("file://"),
5198                    "resource link URI should use file scheme"
5199                );
5200                assert!(
5201                    link.uri.contains("main.rs"),
5202                    "resource link URI should reference file path"
5203                );
5204            }
5205            other => panic!("expected ResourceLink block, got {:?}", other),
5206        }
5207
5208        let body_text = expect_text_block(&blocks[2]);
5209        assert!(
5210            body_text.contains("`HEAD` (ours)"),
5211            "prompt should mention ours branch"
5212        );
5213        assert!(
5214            body_text.contains("`feature` (theirs)"),
5215            "prompt should mention theirs branch"
5216        );
5217        assert!(
5218            body_text.contains("editing the file directly"),
5219            "prompt should instruct the agent to edit the file"
5220        );
5221
5222        let (resource_text, resource_uri) = expect_resource_block(&blocks[3]);
5223        assert!(
5224            resource_text.contains("<<<<<<< HEAD"),
5225            "resource should contain the conflict text"
5226        );
5227        assert!(
5228            resource_uri.contains("merge-conflict"),
5229            "resource URI should use the merge-conflict scheme"
5230        );
5231        assert!(
5232            resource_uri.contains("main.rs"),
5233            "resource URI should reference the file path"
5234        );
5235    }
5236
5237    #[test]
5238    fn test_build_conflict_resolution_prompt_multiple_conflicts_same_file() {
5239        let conflicts = vec![
5240            ConflictContent {
5241                file_path: "src/lib.rs".to_string(),
5242                conflict_text: "<<<<<<< main\nfn a() {}\n=======\nfn a_v2() {}\n>>>>>>> dev"
5243                    .to_string(),
5244                ours_branch_name: "main".to_string(),
5245                theirs_branch_name: "dev".to_string(),
5246            },
5247            ConflictContent {
5248                file_path: "src/lib.rs".to_string(),
5249                conflict_text: "<<<<<<< main\nfn b() {}\n=======\nfn b_v2() {}\n>>>>>>> dev"
5250                    .to_string(),
5251                ours_branch_name: "main".to_string(),
5252                theirs_branch_name: "dev".to_string(),
5253            },
5254        ];
5255
5256        let blocks = build_conflict_resolution_prompt(&conflicts);
5257        // 1 Text instruction + 2 Resource blocks
5258        assert_eq!(blocks.len(), 3, "expected 1 text + 2 resource blocks");
5259
5260        let text = expect_text_block(&blocks[0]);
5261        assert!(
5262            text.contains("all 2 merge conflicts"),
5263            "prompt should mention the total count"
5264        );
5265        assert!(
5266            text.contains("`main` (ours)"),
5267            "prompt should mention ours branch"
5268        );
5269        assert!(
5270            text.contains("`dev` (theirs)"),
5271            "prompt should mention theirs branch"
5272        );
5273        // Single file, so "file" not "files"
5274        assert!(
5275            text.contains("file directly"),
5276            "single file should use singular 'file'"
5277        );
5278
5279        let (resource_a, _) = expect_resource_block(&blocks[1]);
5280        let (resource_b, _) = expect_resource_block(&blocks[2]);
5281        assert!(
5282            resource_a.contains("fn a()"),
5283            "first resource should contain first conflict"
5284        );
5285        assert!(
5286            resource_b.contains("fn b()"),
5287            "second resource should contain second conflict"
5288        );
5289    }
5290
5291    #[test]
5292    fn test_build_conflict_resolution_prompt_multiple_conflicts_different_files() {
5293        let conflicts = vec![
5294            ConflictContent {
5295                file_path: "src/a.rs".to_string(),
5296                conflict_text: "<<<<<<< main\nA\n=======\nB\n>>>>>>> dev".to_string(),
5297                ours_branch_name: "main".to_string(),
5298                theirs_branch_name: "dev".to_string(),
5299            },
5300            ConflictContent {
5301                file_path: "src/b.rs".to_string(),
5302                conflict_text: "<<<<<<< main\nC\n=======\nD\n>>>>>>> dev".to_string(),
5303                ours_branch_name: "main".to_string(),
5304                theirs_branch_name: "dev".to_string(),
5305            },
5306        ];
5307
5308        let blocks = build_conflict_resolution_prompt(&conflicts);
5309        // 1 Text instruction + 2 Resource blocks
5310        assert_eq!(blocks.len(), 3, "expected 1 text + 2 resource blocks");
5311
5312        let text = expect_text_block(&blocks[0]);
5313        assert!(
5314            text.contains("files directly"),
5315            "multiple files should use plural 'files'"
5316        );
5317
5318        let (_, uri_a) = expect_resource_block(&blocks[1]);
5319        let (_, uri_b) = expect_resource_block(&blocks[2]);
5320        assert!(
5321            uri_a.contains("a.rs"),
5322            "first resource URI should reference a.rs"
5323        );
5324        assert!(
5325            uri_b.contains("b.rs"),
5326            "second resource URI should reference b.rs"
5327        );
5328    }
5329
5330    #[test]
5331    fn test_build_conflicted_files_resolution_prompt_file_paths_only() {
5332        let file_paths = vec![
5333            "src/main.rs".to_string(),
5334            "src/lib.rs".to_string(),
5335            "tests/integration.rs".to_string(),
5336        ];
5337
5338        let blocks = build_conflicted_files_resolution_prompt(&file_paths);
5339        // 1 instruction Text block + (ResourceLink + newline Text) per file
5340        assert_eq!(
5341            blocks.len(),
5342            1 + (file_paths.len() * 2),
5343            "expected instruction text plus resource links and separators"
5344        );
5345
5346        let text = expect_text_block(&blocks[0]);
5347        assert!(
5348            text.contains("unresolved merge conflicts"),
5349            "prompt should describe the task"
5350        );
5351        assert!(
5352            text.contains("conflict markers"),
5353            "prompt should mention conflict markers"
5354        );
5355
5356        for (index, path) in file_paths.iter().enumerate() {
5357            let link_index = 1 + (index * 2);
5358            let newline_index = link_index + 1;
5359
5360            match &blocks[link_index] {
5361                acp::ContentBlock::ResourceLink(link) => {
5362                    assert!(
5363                        link.uri.contains("file://"),
5364                        "resource link URI should use file scheme"
5365                    );
5366                    assert!(
5367                        link.uri.contains(path),
5368                        "resource link URI should reference file path: {path}"
5369                    );
5370                }
5371                other => panic!(
5372                    "expected ResourceLink block at index {}, got {:?}",
5373                    link_index, other
5374                ),
5375            }
5376
5377            let separator = expect_text_block(&blocks[newline_index]);
5378            assert_eq!(
5379                separator, "\n",
5380                "expected newline separator after each file"
5381            );
5382        }
5383    }
5384
5385    #[test]
5386    fn test_build_conflict_resolution_prompt_empty_conflicts() {
5387        let blocks = build_conflict_resolution_prompt(&[]);
5388        assert!(
5389            blocks.is_empty(),
5390            "empty conflicts should produce no blocks, got {} blocks",
5391            blocks.len()
5392        );
5393    }
5394
5395    #[test]
5396    fn test_build_conflicted_files_resolution_prompt_empty_paths() {
5397        let blocks = build_conflicted_files_resolution_prompt(&[]);
5398        assert!(
5399            blocks.is_empty(),
5400            "empty paths should produce no blocks, got {} blocks",
5401            blocks.len()
5402        );
5403    }
5404
5405    #[test]
5406    fn test_conflict_resource_block_structure() {
5407        let conflict = ConflictContent {
5408            file_path: "src/utils.rs".to_string(),
5409            conflict_text: "<<<<<<< HEAD\nold code\n=======\nnew code\n>>>>>>> branch".to_string(),
5410            ours_branch_name: "HEAD".to_string(),
5411            theirs_branch_name: "branch".to_string(),
5412        };
5413
5414        let block = conflict_resource_block(&conflict);
5415        let (text, uri) = expect_resource_block(&block);
5416
5417        assert_eq!(
5418            text, conflict.conflict_text,
5419            "resource text should be the raw conflict"
5420        );
5421        assert!(
5422            uri.starts_with("zed:///agent/merge-conflict"),
5423            "URI should use the zed merge-conflict scheme, got: {uri}"
5424        );
5425        assert!(uri.contains("utils.rs"), "URI should encode the file path");
5426    }
5427
5428    fn open_generating_thread_with_loadable_connection(
5429        panel: &Entity<AgentPanel>,
5430        connection: &StubAgentConnection,
5431        cx: &mut VisualTestContext,
5432    ) -> acp::SessionId {
5433        open_thread_with_custom_connection(panel, connection.clone(), cx);
5434        let session_id = active_session_id(panel, cx);
5435        send_message(panel, cx);
5436        cx.update(|_, cx| {
5437            connection.send_update(
5438                session_id.clone(),
5439                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("done".into())),
5440                cx,
5441            );
5442        });
5443        cx.run_until_parked();
5444        session_id
5445    }
5446
5447    fn open_idle_thread_with_non_loadable_connection(
5448        panel: &Entity<AgentPanel>,
5449        connection: &StubAgentConnection,
5450        cx: &mut VisualTestContext,
5451    ) -> acp::SessionId {
5452        open_thread_with_custom_connection(panel, connection.clone(), cx);
5453        let session_id = active_session_id(panel, cx);
5454
5455        connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
5456            acp::ContentChunk::new("done".into()),
5457        )]);
5458        send_message(panel, cx);
5459
5460        session_id
5461    }
5462
5463    async fn setup_panel(cx: &mut TestAppContext) -> (Entity<AgentPanel>, VisualTestContext) {
5464        init_test(cx);
5465        cx.update(|cx| {
5466            agent::ThreadStore::init_global(cx);
5467            language_model::LanguageModelRegistry::test(cx);
5468        });
5469
5470        let fs = FakeFs::new(cx.executor());
5471        let project = Project::test(fs.clone(), [], cx).await;
5472
5473        let multi_workspace =
5474            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5475
5476        let workspace = multi_workspace
5477            .read_with(cx, |mw, _cx| mw.workspace().clone())
5478            .unwrap();
5479
5480        let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
5481
5482        let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
5483            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
5484        });
5485
5486        (panel, cx)
5487    }
5488
5489    #[gpui::test]
5490    async fn test_empty_draft_thread_not_retained_when_navigating_away(cx: &mut TestAppContext) {
5491        let (panel, mut cx) = setup_panel(cx).await;
5492
5493        let connection_a = StubAgentConnection::new();
5494        open_thread_with_connection(&panel, connection_a, &mut cx);
5495        let session_id_a = active_session_id(&panel, &cx);
5496
5497        panel.read_with(&cx, |panel, cx| {
5498            let thread = panel.active_agent_thread(cx).unwrap();
5499            assert!(
5500                thread.read(cx).entries().is_empty(),
5501                "newly opened draft thread should have no entries"
5502            );
5503            assert!(panel.background_threads.is_empty());
5504        });
5505
5506        let connection_b = StubAgentConnection::new();
5507        open_thread_with_connection(&panel, connection_b, &mut cx);
5508
5509        panel.read_with(&cx, |panel, _cx| {
5510            assert!(
5511                panel.background_threads.is_empty(),
5512                "empty draft thread should not be retained in background_threads"
5513            );
5514            assert!(
5515                !panel.background_threads.contains_key(&session_id_a),
5516                "empty draft thread should not be keyed in background_threads"
5517            );
5518        });
5519    }
5520
5521    #[gpui::test]
5522    async fn test_running_thread_retained_when_navigating_away(cx: &mut TestAppContext) {
5523        let (panel, mut cx) = setup_panel(cx).await;
5524
5525        let connection_a = StubAgentConnection::new();
5526        open_thread_with_connection(&panel, connection_a.clone(), &mut cx);
5527        send_message(&panel, &mut cx);
5528
5529        let session_id_a = active_session_id(&panel, &cx);
5530
5531        // Send a chunk to keep thread A generating (don't end the turn).
5532        cx.update(|_, cx| {
5533            connection_a.send_update(
5534                session_id_a.clone(),
5535                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("chunk".into())),
5536                cx,
5537            );
5538        });
5539        cx.run_until_parked();
5540
5541        // Verify thread A is generating.
5542        panel.read_with(&cx, |panel, cx| {
5543            let thread = panel.active_agent_thread(cx).unwrap();
5544            assert_eq!(thread.read(cx).status(), ThreadStatus::Generating);
5545            assert!(panel.background_threads.is_empty());
5546        });
5547
5548        // Open a new thread B — thread A should be retained in background.
5549        let connection_b = StubAgentConnection::new();
5550        open_thread_with_connection(&panel, connection_b, &mut cx);
5551
5552        panel.read_with(&cx, |panel, _cx| {
5553            assert_eq!(
5554                panel.background_threads.len(),
5555                1,
5556                "Running thread A should be retained in background_views"
5557            );
5558            assert!(
5559                panel.background_threads.contains_key(&session_id_a),
5560                "Background view should be keyed by thread A's session ID"
5561            );
5562        });
5563    }
5564
5565    #[gpui::test]
5566    async fn test_idle_non_loadable_thread_retained_when_navigating_away(cx: &mut TestAppContext) {
5567        let (panel, mut cx) = setup_panel(cx).await;
5568
5569        let connection_a = StubAgentConnection::new();
5570        connection_a.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
5571            acp::ContentChunk::new("Response".into()),
5572        )]);
5573        open_thread_with_connection(&panel, connection_a, &mut cx);
5574        send_message(&panel, &mut cx);
5575
5576        let weak_view_a = panel.read_with(&cx, |panel, _cx| {
5577            panel.active_conversation_view().unwrap().downgrade()
5578        });
5579        let session_id_a = active_session_id(&panel, &cx);
5580
5581        // Thread A should be idle (auto-completed via set_next_prompt_updates).
5582        panel.read_with(&cx, |panel, cx| {
5583            let thread = panel.active_agent_thread(cx).unwrap();
5584            assert_eq!(thread.read(cx).status(), ThreadStatus::Idle);
5585        });
5586
5587        // Open a new thread B — thread A should be retained because it is not loadable.
5588        let connection_b = StubAgentConnection::new();
5589        open_thread_with_connection(&panel, connection_b, &mut cx);
5590
5591        panel.read_with(&cx, |panel, _cx| {
5592            assert_eq!(
5593                panel.background_threads.len(),
5594                1,
5595                "Idle non-loadable thread A should be retained in background_views"
5596            );
5597            assert!(
5598                panel.background_threads.contains_key(&session_id_a),
5599                "Background view should be keyed by thread A's session ID"
5600            );
5601        });
5602
5603        assert!(
5604            weak_view_a.upgrade().is_some(),
5605            "Idle non-loadable ConnectionView should still be retained"
5606        );
5607    }
5608
5609    #[gpui::test]
5610    async fn test_background_thread_promoted_via_load(cx: &mut TestAppContext) {
5611        let (panel, mut cx) = setup_panel(cx).await;
5612
5613        let connection_a = StubAgentConnection::new();
5614        open_thread_with_connection(&panel, connection_a.clone(), &mut cx);
5615        send_message(&panel, &mut cx);
5616
5617        let session_id_a = active_session_id(&panel, &cx);
5618
5619        // Keep thread A generating.
5620        cx.update(|_, cx| {
5621            connection_a.send_update(
5622                session_id_a.clone(),
5623                acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("chunk".into())),
5624                cx,
5625            );
5626        });
5627        cx.run_until_parked();
5628
5629        // Open thread B — thread A goes to background.
5630        let connection_b = StubAgentConnection::new();
5631        open_thread_with_connection(&panel, connection_b, &mut cx);
5632        send_message(&panel, &mut cx);
5633
5634        let session_id_b = active_session_id(&panel, &cx);
5635
5636        panel.read_with(&cx, |panel, _cx| {
5637            assert_eq!(panel.background_threads.len(), 1);
5638            assert!(panel.background_threads.contains_key(&session_id_a));
5639        });
5640
5641        // Load thread A back via load_agent_thread — should promote from background.
5642        panel.update_in(&mut cx, |panel, window, cx| {
5643            panel.load_agent_thread(
5644                panel.selected_agent().expect("selected agent must be set"),
5645                session_id_a.clone(),
5646                None,
5647                None,
5648                true,
5649                window,
5650                cx,
5651            );
5652        });
5653
5654        // Thread A should now be the active view, promoted from background.
5655        let active_session = active_session_id(&panel, &cx);
5656        assert_eq!(
5657            active_session, session_id_a,
5658            "Thread A should be the active thread after promotion"
5659        );
5660
5661        panel.read_with(&cx, |panel, _cx| {
5662            assert!(
5663                !panel.background_threads.contains_key(&session_id_a),
5664                "Promoted thread A should no longer be in background_views"
5665            );
5666            assert!(
5667                panel.background_threads.contains_key(&session_id_b),
5668                "Thread B (idle, non-loadable) should remain retained in background_views"
5669            );
5670        });
5671    }
5672
5673    #[gpui::test]
5674    async fn test_cleanup_background_threads_keeps_five_most_recent_idle_loadable_threads(
5675        cx: &mut TestAppContext,
5676    ) {
5677        let (panel, mut cx) = setup_panel(cx).await;
5678        let connection = StubAgentConnection::new()
5679            .with_supports_load_session(true)
5680            .with_agent_id("loadable-stub".into())
5681            .with_telemetry_id("loadable-stub".into());
5682        let mut session_ids = Vec::new();
5683
5684        for _ in 0..7 {
5685            session_ids.push(open_generating_thread_with_loadable_connection(
5686                &panel,
5687                &connection,
5688                &mut cx,
5689            ));
5690        }
5691
5692        let base_time = Instant::now();
5693
5694        for session_id in session_ids.iter().take(6) {
5695            connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
5696        }
5697        cx.run_until_parked();
5698
5699        panel.update(&mut cx, |panel, cx| {
5700            for (index, session_id) in session_ids.iter().take(6).enumerate() {
5701                let conversation_view = panel
5702                    .background_threads
5703                    .get(session_id)
5704                    .expect("background thread should exist")
5705                    .clone();
5706                conversation_view.update(cx, |view, cx| {
5707                    view.set_updated_at(base_time + Duration::from_secs(index as u64), cx);
5708                });
5709            }
5710            panel.cleanup_background_threads(cx);
5711        });
5712
5713        panel.read_with(&cx, |panel, _cx| {
5714            assert_eq!(
5715                panel.background_threads.len(),
5716                5,
5717                "cleanup should keep at most five idle loadable background threads"
5718            );
5719            assert!(
5720                !panel.background_threads.contains_key(&session_ids[0]),
5721                "oldest idle loadable background thread should be removed"
5722            );
5723            for session_id in &session_ids[1..6] {
5724                assert!(
5725                    panel.background_threads.contains_key(session_id),
5726                    "more recent idle loadable background threads should be retained"
5727                );
5728            }
5729            assert!(
5730                !panel.background_threads.contains_key(&session_ids[6]),
5731                "the active thread should not also be stored as a background thread"
5732            );
5733        });
5734    }
5735
5736    #[gpui::test]
5737    async fn test_cleanup_background_threads_preserves_idle_non_loadable_threads(
5738        cx: &mut TestAppContext,
5739    ) {
5740        let (panel, mut cx) = setup_panel(cx).await;
5741
5742        let non_loadable_connection = StubAgentConnection::new();
5743        let non_loadable_session_id = open_idle_thread_with_non_loadable_connection(
5744            &panel,
5745            &non_loadable_connection,
5746            &mut cx,
5747        );
5748
5749        let loadable_connection = StubAgentConnection::new()
5750            .with_supports_load_session(true)
5751            .with_agent_id("loadable-stub".into())
5752            .with_telemetry_id("loadable-stub".into());
5753        let mut loadable_session_ids = Vec::new();
5754
5755        for _ in 0..7 {
5756            loadable_session_ids.push(open_generating_thread_with_loadable_connection(
5757                &panel,
5758                &loadable_connection,
5759                &mut cx,
5760            ));
5761        }
5762
5763        let base_time = Instant::now();
5764
5765        for session_id in loadable_session_ids.iter().take(6) {
5766            loadable_connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
5767        }
5768        cx.run_until_parked();
5769
5770        panel.update(&mut cx, |panel, cx| {
5771            for (index, session_id) in loadable_session_ids.iter().take(6).enumerate() {
5772                let conversation_view = panel
5773                    .background_threads
5774                    .get(session_id)
5775                    .expect("background thread should exist")
5776                    .clone();
5777                conversation_view.update(cx, |view, cx| {
5778                    view.set_updated_at(base_time + Duration::from_secs(index as u64), cx);
5779                });
5780            }
5781            panel.cleanup_background_threads(cx);
5782        });
5783
5784        panel.read_with(&cx, |panel, _cx| {
5785            assert_eq!(
5786                panel.background_threads.len(),
5787                6,
5788                "cleanup should keep the non-loadable idle thread in addition to five loadable ones"
5789            );
5790            assert!(
5791                panel
5792                    .background_threads
5793                    .contains_key(&non_loadable_session_id),
5794                "idle non-loadable background threads should not be cleanup candidates"
5795            );
5796            assert!(
5797                !panel
5798                    .background_threads
5799                    .contains_key(&loadable_session_ids[0]),
5800                "oldest idle loadable background thread should still be removed"
5801            );
5802            for session_id in &loadable_session_ids[1..6] {
5803                assert!(
5804                    panel.background_threads.contains_key(session_id),
5805                    "more recent idle loadable background threads should be retained"
5806                );
5807            }
5808            assert!(
5809                !panel
5810                    .background_threads
5811                    .contains_key(&loadable_session_ids[6]),
5812                "the active loadable thread should not also be stored as a background thread"
5813            );
5814        });
5815    }
5816
5817    #[gpui::test]
5818    async fn test_thread_target_local_project(cx: &mut TestAppContext) {
5819        init_test(cx);
5820        cx.update(|cx| {
5821            agent::ThreadStore::init_global(cx);
5822            language_model::LanguageModelRegistry::test(cx);
5823        });
5824
5825        let fs = FakeFs::new(cx.executor());
5826        fs.insert_tree(
5827            "/project",
5828            json!({
5829                ".git": {},
5830                "src": {
5831                    "main.rs": "fn main() {}"
5832                }
5833            }),
5834        )
5835        .await;
5836        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
5837
5838        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
5839
5840        let multi_workspace =
5841            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5842
5843        let workspace = multi_workspace
5844            .read_with(cx, |multi_workspace, _cx| {
5845                multi_workspace.workspace().clone()
5846            })
5847            .unwrap();
5848
5849        workspace.update(cx, |workspace, _cx| {
5850            workspace.set_random_database_id();
5851        });
5852
5853        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5854
5855        // Wait for the project to discover the git repository.
5856        cx.run_until_parked();
5857
5858        let panel = workspace.update_in(cx, |workspace, window, cx| {
5859            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
5860            workspace.add_panel(panel.clone(), window, cx);
5861            panel
5862        });
5863
5864        cx.run_until_parked();
5865
5866        // Default thread target should be LocalProject.
5867        panel.read_with(cx, |panel, _cx| {
5868            assert_eq!(
5869                *panel.start_thread_in(),
5870                StartThreadIn::LocalProject,
5871                "default thread target should be LocalProject"
5872            );
5873        });
5874
5875        // Start a new thread with the default LocalProject target.
5876        // Use StubAgentServer so the thread connects immediately in tests.
5877        panel.update_in(cx, |panel, window, cx| {
5878            panel.open_external_thread_with_server(
5879                Rc::new(StubAgentServer::default_response()),
5880                window,
5881                cx,
5882            );
5883        });
5884
5885        cx.run_until_parked();
5886
5887        // MultiWorkspace should still have exactly one workspace (no worktree created).
5888        multi_workspace
5889            .read_with(cx, |multi_workspace, cx| {
5890                assert_eq!(
5891                    multi_workspace.workspaces(cx).len(),
5892                    1,
5893                    "LocalProject should not create a new workspace"
5894                );
5895            })
5896            .unwrap();
5897
5898        // The thread should be active in the panel.
5899        panel.read_with(cx, |panel, cx| {
5900            assert!(
5901                panel.active_agent_thread(cx).is_some(),
5902                "a thread should be running in the current workspace"
5903            );
5904        });
5905
5906        // The thread target should still be LocalProject (unchanged).
5907        panel.read_with(cx, |panel, _cx| {
5908            assert_eq!(
5909                *panel.start_thread_in(),
5910                StartThreadIn::LocalProject,
5911                "thread target should remain LocalProject"
5912            );
5913        });
5914
5915        // No worktree creation status should be set.
5916        panel.read_with(cx, |panel, _cx| {
5917            assert!(
5918                panel.worktree_creation_status.is_none(),
5919                "no worktree creation should have occurred"
5920            );
5921        });
5922    }
5923
5924    #[gpui::test]
5925    async fn test_thread_target_does_not_sync_to_external_linked_worktree_with_invalid_branch_target(
5926        cx: &mut TestAppContext,
5927    ) {
5928        use git::repository::Worktree as GitWorktree;
5929
5930        init_test(cx);
5931        cx.update(|cx| {
5932            agent::ThreadStore::init_global(cx);
5933            language_model::LanguageModelRegistry::test(cx);
5934        });
5935
5936        let fs = FakeFs::new(cx.executor());
5937        fs.insert_tree(
5938            "/project",
5939            json!({
5940                ".git": {},
5941                "src": {
5942                    "main.rs": "fn main() {}"
5943                }
5944            }),
5945        )
5946        .await;
5947        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
5948        fs.insert_branches(Path::new("/project/.git"), &["main", "feature-worktree"]);
5949
5950        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
5951
5952        let multi_workspace =
5953            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
5954
5955        let workspace = multi_workspace
5956            .read_with(cx, |multi_workspace, _cx| {
5957                multi_workspace.workspace().clone()
5958            })
5959            .unwrap();
5960
5961        workspace.update(cx, |workspace, _cx| {
5962            workspace.set_random_database_id();
5963        });
5964
5965        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
5966
5967        cx.run_until_parked();
5968
5969        let panel = workspace.update_in(cx, |workspace, window, cx| {
5970            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
5971            workspace.add_panel(panel.clone(), window, cx);
5972            panel
5973        });
5974
5975        cx.run_until_parked();
5976
5977        panel.update_in(cx, |panel, window, cx| {
5978            panel.set_start_thread_in(
5979                &StartThreadIn::NewWorktree {
5980                    worktree_name: Some("feature worktree".to_string()),
5981                    branch_target: NewWorktreeBranchTarget::CurrentBranch,
5982                },
5983                window,
5984                cx,
5985            );
5986        });
5987
5988        fs.add_linked_worktree_for_repo(
5989            Path::new("/project/.git"),
5990            true,
5991            GitWorktree {
5992                path: PathBuf::from("/linked-feature-worktree"),
5993                ref_name: Some("refs/heads/feature-worktree".into()),
5994                sha: "abcdef1".into(),
5995                is_main: false,
5996            },
5997        )
5998        .await;
5999
6000        project
6001            .update(cx, |project, cx| project.git_scans_complete(cx))
6002            .await;
6003        cx.run_until_parked();
6004
6005        panel.read_with(cx, |panel, _cx| {
6006            assert_eq!(
6007                *panel.start_thread_in(),
6008                StartThreadIn::NewWorktree {
6009                    worktree_name: Some("feature worktree".to_string()),
6010                    branch_target: NewWorktreeBranchTarget::CurrentBranch,
6011                },
6012                "thread target should remain a named new worktree when the external linked worktree does not match the selected branch target",
6013            );
6014        });
6015    }
6016
6017    #[gpui::test]
6018    async fn test_thread_target_serialization_round_trip(cx: &mut TestAppContext) {
6019        init_test(cx);
6020        cx.update(|cx| {
6021            agent::ThreadStore::init_global(cx);
6022            language_model::LanguageModelRegistry::test(cx);
6023        });
6024
6025        let fs = FakeFs::new(cx.executor());
6026        fs.insert_tree(
6027            "/project",
6028            json!({
6029                ".git": {},
6030                "src": {
6031                    "main.rs": "fn main() {}"
6032                }
6033            }),
6034        )
6035        .await;
6036        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
6037
6038        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
6039
6040        let multi_workspace =
6041            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6042
6043        let workspace = multi_workspace
6044            .read_with(cx, |multi_workspace, _cx| {
6045                multi_workspace.workspace().clone()
6046            })
6047            .unwrap();
6048
6049        workspace.update(cx, |workspace, _cx| {
6050            workspace.set_random_database_id();
6051        });
6052
6053        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
6054
6055        // Wait for the project to discover the git repository.
6056        cx.run_until_parked();
6057
6058        let panel = workspace.update_in(cx, |workspace, window, cx| {
6059            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
6060            workspace.add_panel(panel.clone(), window, cx);
6061            panel
6062        });
6063
6064        cx.run_until_parked();
6065
6066        // Default should be LocalProject.
6067        panel.read_with(cx, |panel, _cx| {
6068            assert_eq!(*panel.start_thread_in(), StartThreadIn::LocalProject);
6069        });
6070
6071        // Change thread target to NewWorktree.
6072        panel.update_in(cx, |panel, window, cx| {
6073            panel.set_start_thread_in(
6074                &StartThreadIn::NewWorktree {
6075                    worktree_name: None,
6076                    branch_target: NewWorktreeBranchTarget::default(),
6077                },
6078                window,
6079                cx,
6080            );
6081        });
6082
6083        panel.read_with(cx, |panel, _cx| {
6084            assert_eq!(
6085                *panel.start_thread_in(),
6086                StartThreadIn::NewWorktree {
6087                    worktree_name: None,
6088                    branch_target: NewWorktreeBranchTarget::default(),
6089                },
6090                "thread target should be NewWorktree after set_thread_target"
6091            );
6092        });
6093
6094        // Let serialization complete.
6095        cx.run_until_parked();
6096
6097        // Load a fresh panel from the serialized data.
6098        let async_cx = cx.update(|window, cx| window.to_async(cx));
6099        let loaded_panel = AgentPanel::load(workspace.downgrade(), async_cx)
6100            .await
6101            .expect("panel load should succeed");
6102        cx.run_until_parked();
6103
6104        loaded_panel.read_with(cx, |panel, _cx| {
6105            assert_eq!(
6106                *panel.start_thread_in(),
6107                StartThreadIn::NewWorktree {
6108                    worktree_name: None,
6109                    branch_target: NewWorktreeBranchTarget::default(),
6110                },
6111                "thread target should survive serialization round-trip"
6112            );
6113        });
6114    }
6115
6116    #[gpui::test]
6117    async fn test_set_active_blocked_during_worktree_creation(cx: &mut TestAppContext) {
6118        init_test(cx);
6119
6120        let fs = FakeFs::new(cx.executor());
6121        cx.update(|cx| {
6122            agent::ThreadStore::init_global(cx);
6123            language_model::LanguageModelRegistry::test(cx);
6124            <dyn fs::Fs>::set_global(fs.clone(), cx);
6125        });
6126
6127        fs.insert_tree(
6128            "/project",
6129            json!({
6130                ".git": {},
6131                "src": {
6132                    "main.rs": "fn main() {}"
6133                }
6134            }),
6135        )
6136        .await;
6137
6138        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
6139
6140        let multi_workspace =
6141            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6142
6143        let workspace = multi_workspace
6144            .read_with(cx, |multi_workspace, _cx| {
6145                multi_workspace.workspace().clone()
6146            })
6147            .unwrap();
6148
6149        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
6150
6151        let panel = workspace.update_in(cx, |workspace, window, cx| {
6152            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
6153            workspace.add_panel(panel.clone(), window, cx);
6154            panel
6155        });
6156
6157        cx.run_until_parked();
6158
6159        // Simulate worktree creation in progress and reset to Uninitialized
6160        panel.update_in(cx, |panel, window, cx| {
6161            panel.worktree_creation_status =
6162                Some((EntityId::from(0u64), WorktreeCreationStatus::Creating));
6163            panel.active_view = ActiveView::Uninitialized;
6164            Panel::set_active(panel, true, window, cx);
6165            assert!(
6166                matches!(panel.active_view, ActiveView::Uninitialized),
6167                "set_active should not create a thread while worktree is being created"
6168            );
6169        });
6170
6171        // Clear the creation status and use open_external_thread_with_server
6172        // (which bypasses new_agent_thread) to verify the panel can transition
6173        // out of Uninitialized. We can't call set_active directly because
6174        // new_agent_thread requires full agent server infrastructure.
6175        panel.update_in(cx, |panel, window, cx| {
6176            panel.worktree_creation_status = None;
6177            panel.active_view = ActiveView::Uninitialized;
6178            panel.open_external_thread_with_server(
6179                Rc::new(StubAgentServer::default_response()),
6180                window,
6181                cx,
6182            );
6183        });
6184
6185        cx.run_until_parked();
6186
6187        panel.read_with(cx, |panel, _cx| {
6188            assert!(
6189                !matches!(panel.active_view, ActiveView::Uninitialized),
6190                "panel should transition out of Uninitialized once worktree creation is cleared"
6191            );
6192        });
6193    }
6194
6195    #[test]
6196    fn test_deserialize_agent_variants() {
6197        // PascalCase (legacy AgentType format, persisted in panel state)
6198        assert_eq!(
6199            serde_json::from_str::<Agent>(r#""NativeAgent""#).unwrap(),
6200            Agent::NativeAgent,
6201        );
6202        assert_eq!(
6203            serde_json::from_str::<Agent>(r#"{"Custom":{"name":"my-agent"}}"#).unwrap(),
6204            Agent::Custom {
6205                id: "my-agent".into(),
6206            },
6207        );
6208
6209        // Legacy TextThread variant deserializes to NativeAgent
6210        assert_eq!(
6211            serde_json::from_str::<Agent>(r#""TextThread""#).unwrap(),
6212            Agent::NativeAgent,
6213        );
6214
6215        // snake_case (canonical format)
6216        assert_eq!(
6217            serde_json::from_str::<Agent>(r#""native_agent""#).unwrap(),
6218            Agent::NativeAgent,
6219        );
6220        assert_eq!(
6221            serde_json::from_str::<Agent>(r#"{"custom":{"name":"my-agent"}}"#).unwrap(),
6222            Agent::Custom {
6223                id: "my-agent".into(),
6224            },
6225        );
6226
6227        // Serialization uses snake_case
6228        assert_eq!(
6229            serde_json::to_string(&Agent::NativeAgent).unwrap(),
6230            r#""native_agent""#,
6231        );
6232        assert_eq!(
6233            serde_json::to_string(&Agent::Custom {
6234                id: "my-agent".into()
6235            })
6236            .unwrap(),
6237            r#"{"custom":{"name":"my-agent"}}"#,
6238        );
6239    }
6240
6241    #[test]
6242    fn test_resolve_worktree_branch_target() {
6243        let existing_branches = HashSet::from_iter([
6244            "main".to_string(),
6245            "feature".to_string(),
6246            "origin/main".to_string(),
6247        ]);
6248
6249        let resolved = AgentPanel::resolve_worktree_branch_target(
6250            &NewWorktreeBranchTarget::CreateBranch {
6251                name: "new-branch".to_string(),
6252                from_ref: Some("main".to_string()),
6253            },
6254            &existing_branches,
6255            &HashSet::from_iter(["main".to_string()]),
6256        )
6257        .unwrap();
6258        assert_eq!(
6259            resolved,
6260            ("new-branch".to_string(), false, Some("main".to_string()))
6261        );
6262
6263        let resolved = AgentPanel::resolve_worktree_branch_target(
6264            &NewWorktreeBranchTarget::ExistingBranch {
6265                name: "feature".to_string(),
6266            },
6267            &existing_branches,
6268            &HashSet::default(),
6269        )
6270        .unwrap();
6271        assert_eq!(resolved, ("feature".to_string(), true, None));
6272
6273        let resolved = AgentPanel::resolve_worktree_branch_target(
6274            &NewWorktreeBranchTarget::ExistingBranch {
6275                name: "main".to_string(),
6276            },
6277            &existing_branches,
6278            &HashSet::from_iter(["main".to_string()]),
6279        )
6280        .unwrap();
6281        assert_eq!(resolved.1, false);
6282        assert_eq!(resolved.2, Some("main".to_string()));
6283        assert_ne!(resolved.0, "main");
6284        assert!(existing_branches.contains("main"));
6285        assert!(!existing_branches.contains(&resolved.0));
6286    }
6287
6288    #[gpui::test]
6289    async fn test_worktree_creation_preserves_selected_agent(cx: &mut TestAppContext) {
6290        init_test(cx);
6291
6292        let app_state = cx.update(|cx| {
6293            agent::ThreadStore::init_global(cx);
6294            language_model::LanguageModelRegistry::test(cx);
6295
6296            let app_state = workspace::AppState::test(cx);
6297            workspace::init(app_state.clone(), cx);
6298            app_state
6299        });
6300
6301        let fs = app_state.fs.as_fake();
6302        fs.insert_tree(
6303            "/project",
6304            json!({
6305                ".git": {},
6306                "src": {
6307                    "main.rs": "fn main() {}"
6308                }
6309            }),
6310        )
6311        .await;
6312        fs.set_branch_name(Path::new("/project/.git"), Some("main"));
6313
6314        let project = Project::test(app_state.fs.clone(), [Path::new("/project")], cx).await;
6315
6316        let multi_workspace =
6317            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6318        multi_workspace
6319            .update(cx, |multi_workspace, _, cx| {
6320                multi_workspace.open_sidebar(cx);
6321            })
6322            .unwrap();
6323
6324        let workspace = multi_workspace
6325            .read_with(cx, |multi_workspace, _cx| {
6326                multi_workspace.workspace().clone()
6327            })
6328            .unwrap();
6329
6330        workspace.update(cx, |workspace, _cx| {
6331            workspace.set_random_database_id();
6332        });
6333
6334        // Register a callback so new workspaces also get an AgentPanel.
6335        cx.update(|cx| {
6336            cx.observe_new(
6337                |workspace: &mut Workspace,
6338                 window: Option<&mut Window>,
6339                 cx: &mut Context<Workspace>| {
6340                    if let Some(window) = window {
6341                        let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
6342                        workspace.add_panel(panel, window, cx);
6343                    }
6344                },
6345            )
6346            .detach();
6347        });
6348
6349        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
6350
6351        // Wait for the project to discover the git repository.
6352        cx.run_until_parked();
6353
6354        let panel = workspace.update_in(cx, |workspace, window, cx| {
6355            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
6356            workspace.add_panel(panel.clone(), window, cx);
6357            panel
6358        });
6359
6360        cx.run_until_parked();
6361
6362        // Open a thread (needed so there's an active thread view).
6363        panel.update_in(cx, |panel, window, cx| {
6364            panel.open_external_thread_with_server(
6365                Rc::new(StubAgentServer::default_response()),
6366                window,
6367                cx,
6368            );
6369        });
6370
6371        cx.run_until_parked();
6372
6373        // Set the selected agent to Codex (a custom agent) and start_thread_in
6374        // to NewWorktree. We do this AFTER opening the thread because
6375        // open_external_thread_with_server overrides selected_agent.
6376        panel.update_in(cx, |panel, window, cx| {
6377            panel.selected_agent = Agent::Custom {
6378                id: CODEX_ID.into(),
6379            };
6380            panel.set_start_thread_in(
6381                &StartThreadIn::NewWorktree {
6382                    worktree_name: None,
6383                    branch_target: NewWorktreeBranchTarget::default(),
6384                },
6385                window,
6386                cx,
6387            );
6388        });
6389
6390        // Verify the panel has the Codex agent selected.
6391        panel.read_with(cx, |panel, _cx| {
6392            assert_eq!(
6393                panel.selected_agent,
6394                Agent::Custom {
6395                    id: CODEX_ID.into()
6396                },
6397            );
6398        });
6399
6400        // Directly call handle_worktree_creation_requested, which is what
6401        // handle_first_send_requested does when start_thread_in == NewWorktree.
6402        let content = vec![acp::ContentBlock::Text(acp::TextContent::new(
6403            "Hello from test",
6404        ))];
6405        panel.update_in(cx, |panel, window, cx| {
6406            panel.handle_worktree_requested(
6407                content,
6408                WorktreeCreationArgs::New {
6409                    worktree_name: None,
6410                    branch_target: NewWorktreeBranchTarget::default(),
6411                },
6412                window,
6413                cx,
6414            );
6415        });
6416
6417        // Let the async worktree creation + workspace setup complete.
6418        cx.run_until_parked();
6419
6420        panel.read_with(cx, |panel, _cx| {
6421            assert_eq!(
6422                panel.start_thread_in(),
6423                &StartThreadIn::LocalProject,
6424                "the original panel should reset start_thread_in back to the local project after creating a worktree workspace",
6425            );
6426        });
6427
6428        // Find the new workspace's AgentPanel and verify it used the Codex agent.
6429        let found_codex = multi_workspace
6430            .read_with(cx, |multi_workspace, cx| {
6431                // There should be more than one workspace now (the original + the new worktree).
6432                assert!(
6433                    multi_workspace.workspaces(cx).len() > 1,
6434                    "expected a new workspace to have been created, found {}",
6435                    multi_workspace.workspaces(cx).len(),
6436                );
6437
6438                // Check the newest workspace's panel for the correct agent.
6439                let new_workspace = multi_workspace
6440                    .workspaces(cx)
6441                    .into_iter()
6442                    .find(|ws| ws.entity_id() != workspace.entity_id())
6443                    .expect("should find the new workspace");
6444                let new_panel = new_workspace
6445                    .read(cx)
6446                    .panel::<AgentPanel>(cx)
6447                    .expect("new workspace should have an AgentPanel");
6448
6449                new_panel.read(cx).selected_agent.clone()
6450            })
6451            .unwrap();
6452
6453        assert_eq!(
6454            found_codex,
6455            Agent::Custom {
6456                id: CODEX_ID.into()
6457            },
6458            "the new worktree workspace should use the same agent (Codex) that was selected in the original panel",
6459        );
6460    }
6461
6462    #[gpui::test]
6463    async fn test_work_dirs_update_when_worktrees_change(cx: &mut TestAppContext) {
6464        use crate::thread_metadata_store::ThreadMetadataStore;
6465
6466        init_test(cx);
6467        cx.update(|cx| {
6468            agent::ThreadStore::init_global(cx);
6469            language_model::LanguageModelRegistry::test(cx);
6470        });
6471
6472        // Set up a project with one worktree.
6473        let fs = FakeFs::new(cx.executor());
6474        fs.insert_tree("/project_a", json!({ "file.txt": "" }))
6475            .await;
6476        let project = Project::test(fs.clone(), [Path::new("/project_a")], cx).await;
6477
6478        let multi_workspace =
6479            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6480        let workspace = multi_workspace
6481            .read_with(cx, |mw, _cx| mw.workspace().clone())
6482            .unwrap();
6483        let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
6484
6485        let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
6486            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
6487        });
6488
6489        // Open thread A and send a message. With empty next_prompt_updates it
6490        // stays generating, so opening B will move A to background_threads.
6491        let connection_a = StubAgentConnection::new().with_agent_id("agent-a".into());
6492        open_thread_with_custom_connection(&panel, connection_a.clone(), &mut cx);
6493        send_message(&panel, &mut cx);
6494        let session_id_a = active_session_id(&panel, &cx);
6495
6496        // Open thread C — thread A (generating) moves to background.
6497        // Thread C completes immediately (idle), then opening B moves C to background too.
6498        let connection_c = StubAgentConnection::new().with_agent_id("agent-c".into());
6499        connection_c.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
6500            acp::ContentChunk::new("done".into()),
6501        )]);
6502        open_thread_with_custom_connection(&panel, connection_c.clone(), &mut cx);
6503        send_message(&panel, &mut cx);
6504        let session_id_c = active_session_id(&panel, &cx);
6505
6506        // Open thread B — thread C (idle, non-loadable) is retained in background.
6507        let connection_b = StubAgentConnection::new().with_agent_id("agent-b".into());
6508        open_thread_with_custom_connection(&panel, connection_b.clone(), &mut cx);
6509        send_message(&panel, &mut cx);
6510        let session_id_b = active_session_id(&panel, &cx);
6511
6512        let metadata_store = cx.update(|_, cx| ThreadMetadataStore::global(cx));
6513
6514        panel.read_with(&cx, |panel, _cx| {
6515            assert!(
6516                panel.background_threads.contains_key(&session_id_a),
6517                "Thread A should be in background_threads"
6518            );
6519            assert!(
6520                panel.background_threads.contains_key(&session_id_c),
6521                "Thread C should be in background_threads"
6522            );
6523        });
6524
6525        // Verify initial work_dirs for thread B contain only /project_a.
6526        let initial_b_paths = panel.read_with(&cx, |panel, cx| {
6527            let thread = panel.active_agent_thread(cx).unwrap();
6528            thread.read(cx).work_dirs().cloned().unwrap()
6529        });
6530        assert_eq!(
6531            initial_b_paths.ordered_paths().collect::<Vec<_>>(),
6532            vec![&PathBuf::from("/project_a")],
6533            "Thread B should initially have only /project_a"
6534        );
6535
6536        // Now add a second worktree to the project.
6537        fs.insert_tree("/project_b", json!({ "other.txt": "" }))
6538            .await;
6539        let (new_tree, _) = project
6540            .update(&mut cx, |project, cx| {
6541                project.find_or_create_worktree("/project_b", true, cx)
6542            })
6543            .await
6544            .unwrap();
6545        cx.read(|cx| new_tree.read(cx).as_local().unwrap().scan_complete())
6546            .await;
6547        cx.run_until_parked();
6548
6549        // Verify thread B's (active) work_dirs now include both worktrees.
6550        let updated_b_paths = panel.read_with(&cx, |panel, cx| {
6551            let thread = panel.active_agent_thread(cx).unwrap();
6552            thread.read(cx).work_dirs().cloned().unwrap()
6553        });
6554        let mut b_paths_sorted = updated_b_paths.ordered_paths().cloned().collect::<Vec<_>>();
6555        b_paths_sorted.sort();
6556        assert_eq!(
6557            b_paths_sorted,
6558            vec![PathBuf::from("/project_a"), PathBuf::from("/project_b")],
6559            "Thread B work_dirs should include both worktrees after adding /project_b"
6560        );
6561
6562        // Verify thread A's (background) work_dirs are also updated.
6563        let updated_a_paths = panel.read_with(&cx, |panel, cx| {
6564            let bg_view = panel.background_threads.get(&session_id_a).unwrap();
6565            let root_thread = bg_view.read(cx).root_thread(cx).unwrap();
6566            root_thread
6567                .read(cx)
6568                .thread
6569                .read(cx)
6570                .work_dirs()
6571                .cloned()
6572                .unwrap()
6573        });
6574        let mut a_paths_sorted = updated_a_paths.ordered_paths().cloned().collect::<Vec<_>>();
6575        a_paths_sorted.sort();
6576        assert_eq!(
6577            a_paths_sorted,
6578            vec![PathBuf::from("/project_a"), PathBuf::from("/project_b")],
6579            "Thread A work_dirs should include both worktrees after adding /project_b"
6580        );
6581
6582        // Verify thread idle C was also updated.
6583        let updated_c_paths = panel.read_with(&cx, |panel, cx| {
6584            let bg_view = panel.background_threads.get(&session_id_c).unwrap();
6585            let root_thread = bg_view.read(cx).root_thread(cx).unwrap();
6586            root_thread
6587                .read(cx)
6588                .thread
6589                .read(cx)
6590                .work_dirs()
6591                .cloned()
6592                .unwrap()
6593        });
6594        let mut c_paths_sorted = updated_c_paths.ordered_paths().cloned().collect::<Vec<_>>();
6595        c_paths_sorted.sort();
6596        assert_eq!(
6597            c_paths_sorted,
6598            vec![PathBuf::from("/project_a"), PathBuf::from("/project_b")],
6599            "Thread C (idle background) work_dirs should include both worktrees after adding /project_b"
6600        );
6601
6602        // Verify the metadata store reflects the new paths for running threads only.
6603        cx.run_until_parked();
6604        for (label, session_id) in [("thread B", &session_id_b), ("thread A", &session_id_a)] {
6605            let metadata_paths = metadata_store.read_with(&cx, |store, _cx| {
6606                let metadata = store
6607                    .entry(session_id)
6608                    .unwrap_or_else(|| panic!("{label} thread metadata should exist"));
6609                metadata.folder_paths().clone()
6610            });
6611            let mut sorted = metadata_paths.ordered_paths().cloned().collect::<Vec<_>>();
6612            sorted.sort();
6613            assert_eq!(
6614                sorted,
6615                vec![PathBuf::from("/project_a"), PathBuf::from("/project_b")],
6616                "{label} thread metadata folder_paths should include both worktrees"
6617            );
6618        }
6619
6620        // Now remove a worktree and verify work_dirs shrink.
6621        let worktree_b_id = new_tree.read_with(&cx, |tree, _| tree.id());
6622        project.update(&mut cx, |project, cx| {
6623            project.remove_worktree(worktree_b_id, cx);
6624        });
6625        cx.run_until_parked();
6626
6627        let after_remove_b = panel.read_with(&cx, |panel, cx| {
6628            let thread = panel.active_agent_thread(cx).unwrap();
6629            thread.read(cx).work_dirs().cloned().unwrap()
6630        });
6631        assert_eq!(
6632            after_remove_b.ordered_paths().collect::<Vec<_>>(),
6633            vec![&PathBuf::from("/project_a")],
6634            "Thread B work_dirs should revert to only /project_a after removing /project_b"
6635        );
6636
6637        let after_remove_a = panel.read_with(&cx, |panel, cx| {
6638            let bg_view = panel.background_threads.get(&session_id_a).unwrap();
6639            let root_thread = bg_view.read(cx).root_thread(cx).unwrap();
6640            root_thread
6641                .read(cx)
6642                .thread
6643                .read(cx)
6644                .work_dirs()
6645                .cloned()
6646                .unwrap()
6647        });
6648        assert_eq!(
6649            after_remove_a.ordered_paths().collect::<Vec<_>>(),
6650            vec![&PathBuf::from("/project_a")],
6651            "Thread A work_dirs should revert to only /project_a after removing /project_b"
6652        );
6653    }
6654
6655    #[gpui::test]
6656    async fn test_new_workspace_inherits_global_last_used_agent(cx: &mut TestAppContext) {
6657        init_test(cx);
6658        cx.update(|cx| {
6659            agent::ThreadStore::init_global(cx);
6660            language_model::LanguageModelRegistry::test(cx);
6661            // Use an isolated DB so parallel tests can't overwrite our global key.
6662            cx.set_global(db::AppDatabase::test_new());
6663        });
6664
6665        let custom_agent = Agent::Custom {
6666            id: "my-preferred-agent".into(),
6667        };
6668
6669        // Write a known agent to the global KVP to simulate a user who has
6670        // previously used this agent in another workspace.
6671        let kvp = cx.update(|cx| KeyValueStore::global(cx));
6672        write_global_last_used_agent(kvp, custom_agent.clone()).await;
6673
6674        let fs = FakeFs::new(cx.executor());
6675        let project = Project::test(fs.clone(), [], cx).await;
6676
6677        let multi_workspace =
6678            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6679
6680        let workspace = multi_workspace
6681            .read_with(cx, |multi_workspace, _cx| {
6682                multi_workspace.workspace().clone()
6683            })
6684            .unwrap();
6685
6686        workspace.update(cx, |workspace, _cx| {
6687            workspace.set_random_database_id();
6688        });
6689
6690        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
6691
6692        // Load the panel via `load()`, which reads the global fallback
6693        // asynchronously when no per-workspace state exists.
6694        let async_cx = cx.update(|window, cx| window.to_async(cx));
6695        let panel = AgentPanel::load(workspace.downgrade(), async_cx)
6696            .await
6697            .expect("panel load should succeed");
6698        cx.run_until_parked();
6699
6700        panel.read_with(cx, |panel, _cx| {
6701            assert_eq!(
6702                panel.selected_agent, custom_agent,
6703                "new workspace should inherit the global last-used agent"
6704            );
6705        });
6706    }
6707
6708    #[gpui::test]
6709    async fn test_workspaces_maintain_independent_agent_selection(cx: &mut TestAppContext) {
6710        init_test(cx);
6711        cx.update(|cx| {
6712            agent::ThreadStore::init_global(cx);
6713            language_model::LanguageModelRegistry::test(cx);
6714        });
6715
6716        let fs = FakeFs::new(cx.executor());
6717        let project_a = Project::test(fs.clone(), [], cx).await;
6718        let project_b = Project::test(fs, [], cx).await;
6719
6720        let multi_workspace =
6721            cx.add_window(|window, cx| MultiWorkspace::test_new(project_a.clone(), window, cx));
6722
6723        let workspace_a = multi_workspace
6724            .read_with(cx, |multi_workspace, _cx| {
6725                multi_workspace.workspace().clone()
6726            })
6727            .unwrap();
6728
6729        let workspace_b = multi_workspace
6730            .update(cx, |multi_workspace, window, cx| {
6731                multi_workspace.test_add_workspace(project_b.clone(), window, cx)
6732            })
6733            .unwrap();
6734
6735        workspace_a.update(cx, |workspace, _cx| {
6736            workspace.set_random_database_id();
6737        });
6738        workspace_b.update(cx, |workspace, _cx| {
6739            workspace.set_random_database_id();
6740        });
6741
6742        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
6743
6744        let agent_a = Agent::Custom {
6745            id: "agent-alpha".into(),
6746        };
6747        let agent_b = Agent::Custom {
6748            id: "agent-beta".into(),
6749        };
6750
6751        // Set up workspace A with agent_a
6752        let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
6753            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
6754        });
6755        panel_a.update(cx, |panel, _cx| {
6756            panel.selected_agent = agent_a.clone();
6757        });
6758
6759        // Set up workspace B with agent_b
6760        let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
6761            cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
6762        });
6763        panel_b.update(cx, |panel, _cx| {
6764            panel.selected_agent = agent_b.clone();
6765        });
6766
6767        // Serialize both panels
6768        panel_a.update(cx, |panel, cx| panel.serialize(cx));
6769        panel_b.update(cx, |panel, cx| panel.serialize(cx));
6770        cx.run_until_parked();
6771
6772        // Load fresh panels from serialized state and verify independence
6773        let async_cx = cx.update(|window, cx| window.to_async(cx));
6774        let loaded_a = AgentPanel::load(workspace_a.downgrade(), async_cx)
6775            .await
6776            .expect("panel A load should succeed");
6777        cx.run_until_parked();
6778
6779        let async_cx = cx.update(|window, cx| window.to_async(cx));
6780        let loaded_b = AgentPanel::load(workspace_b.downgrade(), async_cx)
6781            .await
6782            .expect("panel B load should succeed");
6783        cx.run_until_parked();
6784
6785        loaded_a.read_with(cx, |panel, _cx| {
6786            assert_eq!(
6787                panel.selected_agent, agent_a,
6788                "workspace A should restore agent-alpha, not agent-beta"
6789            );
6790        });
6791
6792        loaded_b.read_with(cx, |panel, _cx| {
6793            assert_eq!(
6794                panel.selected_agent, agent_b,
6795                "workspace B should restore agent-beta, not agent-alpha"
6796            );
6797        });
6798    }
6799
6800    #[gpui::test]
6801    async fn test_new_thread_uses_workspace_selected_agent(cx: &mut TestAppContext) {
6802        init_test(cx);
6803        cx.update(|cx| {
6804            agent::ThreadStore::init_global(cx);
6805            language_model::LanguageModelRegistry::test(cx);
6806        });
6807
6808        let fs = FakeFs::new(cx.executor());
6809        let project = Project::test(fs.clone(), [], cx).await;
6810
6811        let multi_workspace =
6812            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6813
6814        let workspace = multi_workspace
6815            .read_with(cx, |multi_workspace, _cx| {
6816                multi_workspace.workspace().clone()
6817            })
6818            .unwrap();
6819
6820        workspace.update(cx, |workspace, _cx| {
6821            workspace.set_random_database_id();
6822        });
6823
6824        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
6825
6826        let custom_agent = Agent::Custom {
6827            id: "my-custom-agent".into(),
6828        };
6829
6830        let panel = workspace.update_in(cx, |workspace, window, cx| {
6831            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
6832            workspace.add_panel(panel.clone(), window, cx);
6833            panel
6834        });
6835
6836        // Set selected_agent to a custom agent
6837        panel.update(cx, |panel, _cx| {
6838            panel.selected_agent = custom_agent.clone();
6839        });
6840
6841        // Call new_thread, which internally calls external_thread(None, ...)
6842        // This resolves the agent from self.selected_agent
6843        panel.update_in(cx, |panel, window, cx| {
6844            panel.new_thread(&NewThread, window, cx);
6845        });
6846
6847        panel.read_with(cx, |panel, _cx| {
6848            assert_eq!(
6849                panel.selected_agent, custom_agent,
6850                "selected_agent should remain the custom agent after new_thread"
6851            );
6852            assert!(
6853                panel.active_conversation_view().is_some(),
6854                "a thread should have been created"
6855            );
6856        });
6857    }
6858
6859    #[gpui::test]
6860    async fn test_rollback_all_succeed_returns_ok(cx: &mut TestAppContext) {
6861        init_test(cx);
6862        let fs = FakeFs::new(cx.executor());
6863        cx.update(|cx| {
6864            cx.update_flags(true, vec!["agent-v2".to_string()]);
6865            agent::ThreadStore::init_global(cx);
6866            language_model::LanguageModelRegistry::test(cx);
6867            <dyn fs::Fs>::set_global(fs.clone(), cx);
6868        });
6869
6870        fs.insert_tree(
6871            "/project",
6872            json!({
6873                ".git": {},
6874                "src": { "main.rs": "fn main() {}" }
6875            }),
6876        )
6877        .await;
6878
6879        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
6880        cx.executor().run_until_parked();
6881
6882        let repository = project.read_with(cx, |project, cx| {
6883            project.repositories(cx).values().next().unwrap().clone()
6884        });
6885
6886        let multi_workspace =
6887            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6888
6889        let path_a = PathBuf::from("/worktrees/branch/project_a");
6890        let path_b = PathBuf::from("/worktrees/branch/project_b");
6891
6892        let (sender_a, receiver_a) = futures::channel::oneshot::channel::<Result<()>>();
6893        let (sender_b, receiver_b) = futures::channel::oneshot::channel::<Result<()>>();
6894        sender_a.send(Ok(())).unwrap();
6895        sender_b.send(Ok(())).unwrap();
6896
6897        let creation_infos = vec![
6898            (repository.clone(), path_a.clone(), receiver_a),
6899            (repository.clone(), path_b.clone(), receiver_b),
6900        ];
6901
6902        let fs_clone = fs.clone();
6903        let result = multi_workspace
6904            .update(cx, |_, window, cx| {
6905                window.spawn(cx, async move |cx| {
6906                    AgentPanel::await_and_rollback_on_failure(creation_infos, fs_clone, cx).await
6907                })
6908            })
6909            .unwrap()
6910            .await;
6911
6912        let paths = result.expect("all succeed should return Ok");
6913        assert_eq!(paths, vec![path_a, path_b]);
6914    }
6915
6916    #[gpui::test]
6917    async fn test_rollback_on_failure_attempts_all_worktrees(cx: &mut TestAppContext) {
6918        init_test(cx);
6919        let fs = FakeFs::new(cx.executor());
6920        cx.update(|cx| {
6921            cx.update_flags(true, vec!["agent-v2".to_string()]);
6922            agent::ThreadStore::init_global(cx);
6923            language_model::LanguageModelRegistry::test(cx);
6924            <dyn fs::Fs>::set_global(fs.clone(), cx);
6925        });
6926
6927        fs.insert_tree(
6928            "/project",
6929            json!({
6930                ".git": {},
6931                "src": { "main.rs": "fn main() {}" }
6932            }),
6933        )
6934        .await;
6935
6936        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
6937        cx.executor().run_until_parked();
6938
6939        let repository = project.read_with(cx, |project, cx| {
6940            project.repositories(cx).values().next().unwrap().clone()
6941        });
6942
6943        // Actually create a worktree so it exists in FakeFs for rollback to find.
6944        let success_path = PathBuf::from("/worktrees/branch/project");
6945        cx.update(|cx| {
6946            repository.update(cx, |repo, _| {
6947                repo.create_worktree(
6948                    git::repository::CreateWorktreeTarget::NewBranch {
6949                        branch_name: "branch".to_string(),
6950                        base_sha: None,
6951                    },
6952                    success_path.clone(),
6953                )
6954            })
6955        })
6956        .await
6957        .unwrap()
6958        .unwrap();
6959        cx.executor().run_until_parked();
6960
6961        // Verify the worktree directory exists before rollback.
6962        assert!(
6963            fs.is_dir(&success_path).await,
6964            "worktree directory should exist before rollback"
6965        );
6966
6967        let multi_workspace =
6968            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
6969
6970        // Build creation_infos: one success, one failure.
6971        let failed_path = PathBuf::from("/worktrees/branch/failed_project");
6972
6973        let (sender_ok, receiver_ok) = futures::channel::oneshot::channel::<Result<()>>();
6974        let (sender_err, receiver_err) = futures::channel::oneshot::channel::<Result<()>>();
6975        sender_ok.send(Ok(())).unwrap();
6976        sender_err
6977            .send(Err(anyhow!("branch already exists")))
6978            .unwrap();
6979
6980        let creation_infos = vec![
6981            (repository.clone(), success_path.clone(), receiver_ok),
6982            (repository.clone(), failed_path.clone(), receiver_err),
6983        ];
6984
6985        let fs_clone = fs.clone();
6986        let result = multi_workspace
6987            .update(cx, |_, window, cx| {
6988                window.spawn(cx, async move |cx| {
6989                    AgentPanel::await_and_rollback_on_failure(creation_infos, fs_clone, cx).await
6990                })
6991            })
6992            .unwrap()
6993            .await;
6994
6995        assert!(
6996            result.is_err(),
6997            "should return error when any creation fails"
6998        );
6999        let err_msg = result.unwrap_err().to_string();
7000        assert!(
7001            err_msg.contains("branch already exists"),
7002            "error should mention the original failure: {err_msg}"
7003        );
7004
7005        // The successful worktree should have been rolled back by git.
7006        cx.executor().run_until_parked();
7007        assert!(
7008            !fs.is_dir(&success_path).await,
7009            "successful worktree directory should be removed by rollback"
7010        );
7011    }
7012
7013    #[gpui::test]
7014    async fn test_rollback_on_canceled_receiver(cx: &mut TestAppContext) {
7015        init_test(cx);
7016        let fs = FakeFs::new(cx.executor());
7017        cx.update(|cx| {
7018            cx.update_flags(true, vec!["agent-v2".to_string()]);
7019            agent::ThreadStore::init_global(cx);
7020            language_model::LanguageModelRegistry::test(cx);
7021            <dyn fs::Fs>::set_global(fs.clone(), cx);
7022        });
7023
7024        fs.insert_tree(
7025            "/project",
7026            json!({
7027                ".git": {},
7028                "src": { "main.rs": "fn main() {}" }
7029            }),
7030        )
7031        .await;
7032
7033        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
7034        cx.executor().run_until_parked();
7035
7036        let repository = project.read_with(cx, |project, cx| {
7037            project.repositories(cx).values().next().unwrap().clone()
7038        });
7039
7040        let multi_workspace =
7041            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
7042
7043        let path = PathBuf::from("/worktrees/branch/project");
7044
7045        // Drop the sender to simulate a canceled receiver.
7046        let (_sender, receiver) = futures::channel::oneshot::channel::<Result<()>>();
7047        drop(_sender);
7048
7049        let creation_infos = vec![(repository.clone(), path.clone(), receiver)];
7050
7051        let fs_clone = fs.clone();
7052        let result = multi_workspace
7053            .update(cx, |_, window, cx| {
7054                window.spawn(cx, async move |cx| {
7055                    AgentPanel::await_and_rollback_on_failure(creation_infos, fs_clone, cx).await
7056                })
7057            })
7058            .unwrap()
7059            .await;
7060
7061        assert!(
7062            result.is_err(),
7063            "should return error when receiver is canceled"
7064        );
7065        let err_msg = result.unwrap_err().to_string();
7066        assert!(
7067            err_msg.contains("canceled"),
7068            "error should mention cancellation: {err_msg}"
7069        );
7070    }
7071
7072    #[gpui::test]
7073    async fn test_rollback_cleans_up_orphan_directories(cx: &mut TestAppContext) {
7074        init_test(cx);
7075        let fs = FakeFs::new(cx.executor());
7076        cx.update(|cx| {
7077            cx.update_flags(true, vec!["agent-v2".to_string()]);
7078            agent::ThreadStore::init_global(cx);
7079            language_model::LanguageModelRegistry::test(cx);
7080            <dyn fs::Fs>::set_global(fs.clone(), cx);
7081        });
7082
7083        fs.insert_tree(
7084            "/project",
7085            json!({
7086                ".git": {},
7087                "src": { "main.rs": "fn main() {}" }
7088            }),
7089        )
7090        .await;
7091
7092        let project = Project::test(fs.clone(), [Path::new("/project")], cx).await;
7093        cx.executor().run_until_parked();
7094
7095        let repository = project.read_with(cx, |project, cx| {
7096            project.repositories(cx).values().next().unwrap().clone()
7097        });
7098
7099        let multi_workspace =
7100            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
7101
7102        // Simulate the orphan state: create_dir_all was called but git
7103        // worktree add failed, leaving a directory with leftover files.
7104        let orphan_path = PathBuf::from("/worktrees/branch/orphan_project");
7105        fs.insert_tree(
7106            "/worktrees/branch/orphan_project",
7107            json!({ "leftover.txt": "junk" }),
7108        )
7109        .await;
7110
7111        assert!(
7112            fs.is_dir(&orphan_path).await,
7113            "orphan dir should exist before rollback"
7114        );
7115
7116        let (sender, receiver) = futures::channel::oneshot::channel::<Result<()>>();
7117        sender.send(Err(anyhow!("hook failed"))).unwrap();
7118
7119        let creation_infos = vec![(repository.clone(), orphan_path.clone(), receiver)];
7120
7121        let fs_clone = fs.clone();
7122        let result = multi_workspace
7123            .update(cx, |_, window, cx| {
7124                window.spawn(cx, async move |cx| {
7125                    AgentPanel::await_and_rollback_on_failure(creation_infos, fs_clone, cx).await
7126                })
7127            })
7128            .unwrap()
7129            .await;
7130
7131        cx.executor().run_until_parked();
7132
7133        assert!(result.is_err());
7134        assert!(
7135            !fs.is_dir(&orphan_path).await,
7136            "orphan worktree directory should be removed by filesystem cleanup"
7137        );
7138    }
7139
7140    #[gpui::test]
7141    async fn test_worktree_creation_for_remote_project(
7142        cx: &mut TestAppContext,
7143        server_cx: &mut TestAppContext,
7144    ) {
7145        init_test(cx);
7146
7147        let app_state = cx.update(|cx| {
7148            agent::ThreadStore::init_global(cx);
7149            language_model::LanguageModelRegistry::test(cx);
7150
7151            let app_state = workspace::AppState::test(cx);
7152            workspace::init(app_state.clone(), cx);
7153            app_state
7154        });
7155
7156        server_cx.update(|cx| {
7157            release_channel::init(semver::Version::new(0, 0, 0), cx);
7158        });
7159
7160        // Set up the remote server side with a git repo.
7161        let server_fs = FakeFs::new(server_cx.executor());
7162        server_fs
7163            .insert_tree(
7164                "/project",
7165                json!({
7166                    ".git": {},
7167                    "src": {
7168                        "main.rs": "fn main() {}"
7169                    }
7170                }),
7171            )
7172            .await;
7173        server_fs.set_branch_name(Path::new("/project/.git"), Some("main"));
7174
7175        // Create a mock remote connection.
7176        let (opts, server_session, _) = remote::RemoteClient::fake_server(cx, server_cx);
7177
7178        server_cx.update(remote_server::HeadlessProject::init);
7179        let server_executor = server_cx.executor();
7180        let _headless = server_cx.new(|cx| {
7181            remote_server::HeadlessProject::new(
7182                remote_server::HeadlessAppState {
7183                    session: server_session,
7184                    fs: server_fs.clone(),
7185                    http_client: Arc::new(http_client::BlockedHttpClient),
7186                    node_runtime: node_runtime::NodeRuntime::unavailable(),
7187                    languages: Arc::new(language::LanguageRegistry::new(server_executor.clone())),
7188                    extension_host_proxy: Arc::new(extension::ExtensionHostProxy::new()),
7189                    startup_time: Instant::now(),
7190                },
7191                false,
7192                cx,
7193            )
7194        });
7195
7196        // Connect the client side and build a remote project.
7197        // Use a separate Client to avoid double-registering proto handlers
7198        // (Workspace::test_new creates its own WorkspaceStore from the
7199        // project's client).
7200        let remote_client = remote::RemoteClient::connect_mock(opts, cx).await;
7201        let project = cx.update(|cx| {
7202            let project_client = client::Client::new(
7203                Arc::new(clock::FakeSystemClock::new()),
7204                http_client::FakeHttpClient::with_404_response(),
7205                cx,
7206            );
7207            let user_store = cx.new(|cx| client::UserStore::new(project_client.clone(), cx));
7208            project::Project::remote(
7209                remote_client,
7210                project_client,
7211                node_runtime::NodeRuntime::unavailable(),
7212                user_store,
7213                app_state.languages.clone(),
7214                app_state.fs.clone(),
7215                false,
7216                cx,
7217            )
7218        });
7219
7220        // Open the remote path as a worktree in the project.
7221        let worktree_path = Path::new("/project");
7222        project
7223            .update(cx, |project, cx| {
7224                project.find_or_create_worktree(worktree_path, true, cx)
7225            })
7226            .await
7227            .expect("should be able to open remote worktree");
7228        cx.run_until_parked();
7229
7230        // Verify the project is indeed remote.
7231        project.read_with(cx, |project, cx| {
7232            assert!(!project.is_local(), "project should be remote, not local");
7233            assert!(
7234                project.remote_connection_options(cx).is_some(),
7235                "project should have remote connection options"
7236            );
7237        });
7238
7239        // Create the workspace and agent panel.
7240        let multi_workspace =
7241            cx.add_window(|window, cx| MultiWorkspace::test_new(project.clone(), window, cx));
7242        multi_workspace
7243            .update(cx, |multi_workspace, _, cx| {
7244                multi_workspace.open_sidebar(cx);
7245            })
7246            .unwrap();
7247
7248        let workspace = multi_workspace
7249            .read_with(cx, |mw, _cx| mw.workspace().clone())
7250            .unwrap();
7251
7252        workspace.update(cx, |workspace, _cx| {
7253            workspace.set_random_database_id();
7254        });
7255
7256        // Register a callback so new workspaces also get an AgentPanel.
7257        cx.update(|cx| {
7258            cx.observe_new(
7259                |workspace: &mut Workspace,
7260                 window: Option<&mut Window>,
7261                 cx: &mut Context<Workspace>| {
7262                    if let Some(window) = window {
7263                        let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
7264                        workspace.add_panel(panel, window, cx);
7265                    }
7266                },
7267            )
7268            .detach();
7269        });
7270
7271        let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
7272        cx.run_until_parked();
7273
7274        let panel = workspace.update_in(cx, |workspace, window, cx| {
7275            let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
7276            workspace.add_panel(panel.clone(), window, cx);
7277            panel
7278        });
7279
7280        cx.run_until_parked();
7281
7282        // Open a thread.
7283        panel.update_in(cx, |panel, window, cx| {
7284            panel.open_external_thread_with_server(
7285                Rc::new(StubAgentServer::default_response()),
7286                window,
7287                cx,
7288            );
7289        });
7290        cx.run_until_parked();
7291
7292        // Set start_thread_in to LinkedWorktree to bypass git worktree
7293        // creation and directly test workspace opening for a known path.
7294        let linked_path = PathBuf::from("/project");
7295        panel.update_in(cx, |panel, window, cx| {
7296            panel.set_start_thread_in(
7297                &StartThreadIn::LinkedWorktree {
7298                    path: linked_path.clone(),
7299                    display_name: "project".to_string(),
7300                },
7301                window,
7302                cx,
7303            );
7304        });
7305
7306        // Trigger worktree creation.
7307        let content = vec![acp::ContentBlock::Text(acp::TextContent::new(
7308            "Hello from remote test",
7309        ))];
7310        panel.update_in(cx, |panel, window, cx| {
7311            panel.handle_worktree_requested(
7312                content,
7313                WorktreeCreationArgs::Linked {
7314                    worktree_path: linked_path,
7315                },
7316                window,
7317                cx,
7318            );
7319        });
7320
7321        // The refactored code uses `find_or_create_workspace`, which
7322        // finds the existing remote workspace (matching paths + host)
7323        // and reuses it instead of creating a new connection.
7324        cx.run_until_parked();
7325
7326        // The task should have completed: the existing workspace was
7327        // found and reused.
7328        panel.read_with(cx, |panel, _cx| {
7329            assert!(
7330                panel.worktree_creation_status.is_none(),
7331                "worktree creation should have completed, but status is: {:?}",
7332                panel.worktree_creation_status
7333            );
7334        });
7335
7336        // The existing remote workspace was reused — no new workspace
7337        // should have been created.
7338        multi_workspace
7339            .read_with(cx, |multi_workspace, cx| {
7340                let project = workspace.read(cx).project().clone();
7341                assert!(
7342                    !project.read(cx).is_local(),
7343                    "workspace project should still be remote, not local"
7344                );
7345                assert_eq!(
7346                    multi_workspace.workspaces(cx).len(),
7347                    1,
7348                    "existing remote workspace should be reused, not a new one created"
7349                );
7350            })
7351            .unwrap();
7352    }
7353}