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