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